"""Enforce NOT NULL on server-defaulted timestamps the models already require. 0011, 0012, 0014 and 0017 created these columns with ``server_default=now()`` but without ``nullable=False``, so the database accepted NULL while the models declare them non-optional. No application path writes NULL to them (inserts omit them and rely on the default; updates set ``now``), and before this revision was written no NULL existed in production or in the development database. The upgrade refuses to run — changing nothing — if any NULL is found, instead of guessing a value for it. Revision ID: 0020_not_null_timestamps Revises: 0019_fix_timestamp_defaults """ from alembic import op import sqlalchemy as sa revision = "0020_not_null_timestamps" down_revision = "0019_fix_timestamp_defaults" branch_labels = None depends_on = None COLUMNS = ( ("admin_users", "created_at"), ("agent_events", "occurred_at"), ("agent_snapshots", "updated_at"), ("ai_eval_cache", "created_at"), ("ai_runs", "started_at"), ("ai_work_items", "created_at"), ("ai_work_items", "updated_at"), ("audit_events", "occurred_at"), ("fiscal_context_entries", "created_at"), ("fiscal_context_entries", "updated_at"), ("installations", "created_at"), ("installations", "updated_at"), ("kb_knowledge", "created_at"), ("kb_observations", "observed_at"), ("kb_products", "created_at"), ("kb_products", "updated_at"), ("tenant_product_context", "created_at"), ("tenant_product_context", "updated_at"), ("web_sessions", "created_at"), ("web_sessions", "last_seen_at"), ) def upgrade(): bind = op.get_bind() offenders = [] for table, column in COLUMNS: nulls = bind.execute(sa.text(f'SELECT count(*) FROM "{table}" WHERE "{column}" IS NULL')).scalar() if nulls: offenders.append(f"{table}.{column}={nulls}") if offenders: raise RuntimeError("0020 aborted, NULL values present (fix them explicitly first): " + ", ".join(offenders)) for table, column in COLUMNS: op.alter_column(table, column, nullable=False, existing_type=sa.DateTime(timezone=True), existing_server_default=sa.func.now()) def downgrade(): for table, column in reversed(COLUMNS): op.alter_column(table, column, nullable=True, existing_type=sa.DateTime(timezone=True), existing_server_default=sa.func.now())