"""Store an empty optional unique field as NULL, not as an empty string. A column that is `unique=True` and nullable accepts any number of NULLs, which is what makes "optional but unique" work at all. It accepts exactly ONE empty string. So the first record saved with a blank code succeeds and the second fails with "Duplicate entry '' for key", surfacing as a 500 from an endpoint that did nothing wrong. That is not a business-unit problem. A dozen columns across core and the plugins are unique and nullable - asset numbers, host names, item codes, subnet names - and every one of them is a blank web form away from the same collision. Fixing them one endpoint at a time would leave the next one to be discovered by a user. So it is fixed once, at the mapper: before any insert or update, a string column that is unique and nullable and has been set to '' or whitespace is stored as NULL instead. An empty string carries no information in a unique column - there is no case where two records both meaning "blank" should collide - so nothing is lost by normalising it. Registered from create_app. Applies to every model, including plugin models loaded later, because it hooks the shared Mapper class rather than a list of models known at import time. """ from sqlalchemy import event, String, inspect from sqlalchemy.orm import Session _CACHE = {} def _blankable_columns(mapper): """Unique, nullable, string columns on this mapper - worked out once each.""" cached = _CACHE.get(mapper) if cached is not None: return cached columns = [] for prop in mapper.column_attrs: for column in prop.columns: if not column.unique or not column.nullable: continue # Only text. A unique nullable integer cannot be handed '' by a form # without failing type coercion long before it reaches here. if not isinstance(column.type, String): continue columns.append(prop.key) break _CACHE[mapper] = columns return columns def _normalise_instance(obj): try: mapper = inspect(obj).mapper except Exception: return for key in _blankable_columns(mapper): value = getattr(obj, key, None) if isinstance(value, str) and not value.strip(): setattr(obj, key, None) def _before_flush(session, _flush_context, _instances): # Both new and modified: editing a record to clear its code has to become # NULL for the same reason creating one with a blank code does. for obj in list(session.new) + list(session.dirty): _normalise_instance(obj) def register_blank_unique_normaliser(): """Hook every session flush, for every model including plugins'. Listening on Session rather than on individual mappers means models imported later - which is every plugin model - are covered without registration, and it avoids depending on mapper-level event semantics that differ across SQLAlchemy versions. """ if getattr(register_blank_unique_normaliser, '_installed', False): return event.listen(Session, 'before_flush', _before_flush) register_blank_unique_normaliser._installed = True