From e7ef03fbc363b08e324c880ce4252bc8c76aeedb Mon Sep 17 00:00:00 2001 From: bornunique911 Date: Thu, 6 Aug 2026 00:53:24 +0530 Subject: [PATCH 01/22] Fix SQLite migration compatibility and idempotency --- ...e_add_document_metadata_to_cre_and_node.py | 43 +++++++ ...fa_add_embedding_vec_to_embeddings_for_.py | 33 +++++ ...b3c4d5e_add_artifact_ingest_persistence.py | 114 +++++++++--------- 3 files changed, 130 insertions(+), 60 deletions(-) create mode 100644 migrations/versions/055dbd9f8bfe_add_document_metadata_to_cre_and_node.py create mode 100644 migrations/versions/967016ee10fa_add_embedding_vec_to_embeddings_for_.py diff --git a/migrations/versions/055dbd9f8bfe_add_document_metadata_to_cre_and_node.py b/migrations/versions/055dbd9f8bfe_add_document_metadata_to_cre_and_node.py new file mode 100644 index 000000000..65e67b6d6 --- /dev/null +++ b/migrations/versions/055dbd9f8bfe_add_document_metadata_to_cre_and_node.py @@ -0,0 +1,43 @@ +"""add document_metadata to cre and node + +Revision ID: 055dbd9f8bfe +Revises: d4e5f6a7b8c9 +Create Date: 2026-08-06 00:13:49.191527 + +""" + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.engine.reflection import Inspector + +revision = '055dbd9f8bfe' +down_revision = 'd4e5f6a7b8c9' # or whatever the current head is +branch_labels = None +depends_on = None + +def column_exists(table, column): + conn = op.get_bind() + inspector = Inspector.from_engine(conn) + columns = [c['name'] for c in inspector.get_columns(table)] + return column in columns + +def upgrade(): + # Add to 'cre' table if missing + if not column_exists('cre', 'document_metadata'): + op.add_column('cre', sa.Column('document_metadata', sa.Text(), nullable=True)) + print("Added document_metadata to cre") + else: + print("Column document_metadata already exists in cre, skipping.") + + # Add to 'node' table if missing + if not column_exists('node', 'document_metadata'): + op.add_column('node', sa.Column('document_metadata', sa.Text(), nullable=True)) + print("Added document_metadata to node") + else: + print("Column document_metadata already exists in node, skipping.") + +def downgrade(): + # Remove columns (optional, but we want a clean downgrade) + # Note: SQLite does not support DROP COLUMN directly, but you can use batch. + # For simplicity, we'll just raise an error or skip. + pass diff --git a/migrations/versions/967016ee10fa_add_embedding_vec_to_embeddings_for_.py b/migrations/versions/967016ee10fa_add_embedding_vec_to_embeddings_for_.py new file mode 100644 index 000000000..725dc1b43 --- /dev/null +++ b/migrations/versions/967016ee10fa_add_embedding_vec_to_embeddings_for_.py @@ -0,0 +1,33 @@ +"""add embedding_vec to embeddings for SQLite + +Revision ID: 967016ee10fa +Revises: 055dbd9f8bfe +Create Date: 2026-08-06 00:13:49.191527 + +""" + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.engine.reflection import Inspector + +revision = '967016ee10fa' +down_revision = '055dbd9f8bfe' # <-- set this to the head revision you found +branch_labels = None +depends_on = None + +def column_exists(table, column): + conn = op.get_bind() + inspector = Inspector.from_engine(conn) + columns = [c['name'] for c in inspector.get_columns(table)] + return column in columns + +def upgrade(): + if not column_exists('embeddings', 'embedding_vec'): + op.add_column('embeddings', sa.Column('embedding_vec', sa.Text(), nullable=True)) + print("Added embedding_vec to embeddings") + else: + print("Column embedding_vec already exists in embeddings, skipping.") + +def downgrade(): + # SQLite does not support DROP COLUMN directly; skip or use batch if needed. + pass \ No newline at end of file diff --git a/migrations/versions/9f1a2b3c4d5e_add_artifact_ingest_persistence.py b/migrations/versions/9f1a2b3c4d5e_add_artifact_ingest_persistence.py index f313016b6..e56959f63 100644 --- a/migrations/versions/9f1a2b3c4d5e_add_artifact_ingest_persistence.py +++ b/migrations/versions/9f1a2b3c4d5e_add_artifact_ingest_persistence.py @@ -8,75 +8,69 @@ from alembic import op import sqlalchemy as sa - +from sqlalchemy.engine.reflection import Inspector revision = "9f1a2b3c4d5e" down_revision = "a1b2c3d4e5f6" branch_labels = None depends_on = None +def table_exists(table_name): + conn = op.get_bind() + inspector = Inspector.from_engine(conn) + return table_name in inspector.get_table_names() def upgrade(): - op.create_table( - "artifact_ingest_event", - sa.Column("id", sa.String(), primary_key=True), - sa.Column("run_id", sa.String(), nullable=False), - sa.Column("artifact_id", sa.String(), nullable=False), - sa.Column("harvest_mode", sa.String(), nullable=False), - sa.Column("event_type", sa.String(), nullable=False), - sa.Column("source_json", sa.Text(), nullable=False), - sa.Column("locator_json", sa.Text(), nullable=False), - sa.Column("artifact_json", sa.Text(), nullable=False), - sa.Column("harvest_json", sa.Text(), nullable=False), - sa.Column("observed_at", sa.DateTime(), nullable=False), - sa.Column("created_at", sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint( - ["run_id"], - ["import_run.id"], - onupdate="CASCADE", - ondelete="CASCADE", - ), - ) - op.create_unique_constraint( - "uq_artifact_ingest_event_run_artifact", - "artifact_ingest_event", - ["run_id", "artifact_id"], - ) - - op.create_table( - "ingest_chunk", - sa.Column("id", sa.String(), primary_key=True), - sa.Column("artifact_event_id", sa.String(), nullable=False), - sa.Column("chunk_id", sa.String(), nullable=False), - sa.Column("text", sa.Text(), nullable=False), - sa.Column("char_count", sa.Integer(), nullable=False), - sa.Column("span_json", sa.Text(), nullable=False), - sa.Column("delta_json", sa.Text(), nullable=True), - sa.Column("created_at", sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint( - ["artifact_event_id"], - ["artifact_ingest_event.id"], - onupdate="CASCADE", - ondelete="CASCADE", - ), - ) - op.create_unique_constraint( - "uq_ingest_chunk_artifact_chunk", - "ingest_chunk", - ["artifact_event_id", "chunk_id"], - ) + # Create artifact_ingest_event only if it doesn't exist + if not table_exists("artifact_ingest_event"): + op.create_table( + "artifact_ingest_event", + sa.Column("id", sa.String(), primary_key=True), + sa.Column("run_id", sa.String(), nullable=False), + sa.Column("artifact_id", sa.String(), nullable=False), + sa.Column("harvest_mode", sa.String(), nullable=False), + sa.Column("event_type", sa.String(), nullable=False), + sa.Column("source_json", sa.Text(), nullable=False), + sa.Column("locator_json", sa.Text(), nullable=False), + sa.Column("artifact_json", sa.Text(), nullable=False), + sa.Column("harvest_json", sa.Text(), nullable=False), + sa.Column("observed_at", sa.DateTime(), nullable=False), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint( + ["run_id"], + ["import_run.id"], + onupdate="CASCADE", + ondelete="CASCADE", + ), + sa.UniqueConstraint("run_id", "artifact_id", name="uq_artifact_ingest_event_run_artifact"), + ) + else: + print("Table artifact_ingest_event already exists, skipping creation.") + # Create ingest_chunk only if it doesn't exist + if not table_exists("ingest_chunk"): + op.create_table( + "ingest_chunk", + sa.Column("id", sa.String(), primary_key=True), + sa.Column("artifact_event_id", sa.String(), nullable=False), + sa.Column("chunk_id", sa.String(), nullable=False), + sa.Column("text", sa.Text(), nullable=False), + sa.Column("char_count", sa.Integer(), nullable=False), + sa.Column("span_json", sa.Text(), nullable=False), + sa.Column("delta_json", sa.Text(), nullable=True), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint( + ["artifact_event_id"], + ["artifact_ingest_event.id"], + onupdate="CASCADE", + ondelete="CASCADE", + ), + sa.UniqueConstraint("artifact_event_id", "chunk_id", name="uq_ingest_chunk_artifact_chunk"), + ) + else: + print("Table ingest_chunk already exists, skipping creation.") def downgrade(): - op.drop_constraint( - "uq_ingest_chunk_artifact_chunk", - "ingest_chunk", - type_="unique", - ) + # Drop tables in reverse order; constraints are dropped automatically with the tables. op.drop_table("ingest_chunk") - op.drop_constraint( - "uq_artifact_ingest_event_run_artifact", - "artifact_ingest_event", - type_="unique", - ) - op.drop_table("artifact_ingest_event") + op.drop_table("artifact_ingest_event") \ No newline at end of file From 1b83e9e7a514f07eac8bb0d57ff128a0a0cec45e Mon Sep 17 00:00:00 2001 From: bornunique911 Date: Thu, 6 Aug 2026 02:01:34 +0530 Subject: [PATCH 02/22] migrations: add downgrades, use sa.inspect, and verify unique constraints - Add downgrades to 055dbd9f8bfe and 967016ee10fa to drop added columns - Replace deprecated Inspector.from_engine(conn) with sa.inspect(conn) - In 9f1a2b3c4d5e, verify that existing tables have the required unique constraints; add them via batch_alter_table if missing - Ensure migration fails if constraints cannot be added Addresses PR review comments #1006 --- ...e_add_document_metadata_to_cre_and_node.py | 12 ++++---- ...fa_add_embedding_vec_to_embeddings_for_.py | 8 ++--- ...b3c4d5e_add_artifact_ingest_persistence.py | 29 ++++++++++++++++--- 3 files changed, 35 insertions(+), 14 deletions(-) diff --git a/migrations/versions/055dbd9f8bfe_add_document_metadata_to_cre_and_node.py b/migrations/versions/055dbd9f8bfe_add_document_metadata_to_cre_and_node.py index 65e67b6d6..ab1004870 100644 --- a/migrations/versions/055dbd9f8bfe_add_document_metadata_to_cre_and_node.py +++ b/migrations/versions/055dbd9f8bfe_add_document_metadata_to_cre_and_node.py @@ -8,7 +8,6 @@ from alembic import op import sqlalchemy as sa -from sqlalchemy.engine.reflection import Inspector revision = '055dbd9f8bfe' down_revision = 'd4e5f6a7b8c9' # or whatever the current head is @@ -17,7 +16,7 @@ def column_exists(table, column): conn = op.get_bind() - inspector = Inspector.from_engine(conn) + inspector = sa.inspect(conn) columns = [c['name'] for c in inspector.get_columns(table)] return column in columns @@ -37,7 +36,8 @@ def upgrade(): print("Column document_metadata already exists in node, skipping.") def downgrade(): - # Remove columns (optional, but we want a clean downgrade) - # Note: SQLite does not support DROP COLUMN directly, but you can use batch. - # For simplicity, we'll just raise an error or skip. - pass + # Remove document_metadata from cre and node using SQLite batch pattern + with op.batch_alter_table("cre") as batch_op: + batch_op.drop_column("document_metadata") + with op.batch_alter_table("node") as batch_op: + batch_op.drop_column("document_metadata") diff --git a/migrations/versions/967016ee10fa_add_embedding_vec_to_embeddings_for_.py b/migrations/versions/967016ee10fa_add_embedding_vec_to_embeddings_for_.py index 725dc1b43..e67b64883 100644 --- a/migrations/versions/967016ee10fa_add_embedding_vec_to_embeddings_for_.py +++ b/migrations/versions/967016ee10fa_add_embedding_vec_to_embeddings_for_.py @@ -8,7 +8,6 @@ from alembic import op import sqlalchemy as sa -from sqlalchemy.engine.reflection import Inspector revision = '967016ee10fa' down_revision = '055dbd9f8bfe' # <-- set this to the head revision you found @@ -17,7 +16,7 @@ def column_exists(table, column): conn = op.get_bind() - inspector = Inspector.from_engine(conn) + inspector = sa.inspect(conn) columns = [c['name'] for c in inspector.get_columns(table)] return column in columns @@ -29,5 +28,6 @@ def upgrade(): print("Column embedding_vec already exists in embeddings, skipping.") def downgrade(): - # SQLite does not support DROP COLUMN directly; skip or use batch if needed. - pass \ No newline at end of file + # Remove embedding_vec from embeddings using SQLite batch pattern + with op.batch_alter_table("embeddings") as batch_op: + batch_op.drop_column("embedding_vec") \ No newline at end of file diff --git a/migrations/versions/9f1a2b3c4d5e_add_artifact_ingest_persistence.py b/migrations/versions/9f1a2b3c4d5e_add_artifact_ingest_persistence.py index e56959f63..d599d5721 100644 --- a/migrations/versions/9f1a2b3c4d5e_add_artifact_ingest_persistence.py +++ b/migrations/versions/9f1a2b3c4d5e_add_artifact_ingest_persistence.py @@ -8,7 +8,6 @@ from alembic import op import sqlalchemy as sa -from sqlalchemy.engine.reflection import Inspector revision = "9f1a2b3c4d5e" down_revision = "a1b2c3d4e5f6" @@ -17,9 +16,15 @@ def table_exists(table_name): conn = op.get_bind() - inspector = Inspector.from_engine(conn) + inspector = sa.inspect(conn) return table_name in inspector.get_table_names() +def constraint_exists(table, constraint_name): + conn = op.get_bind() + inspector = sa.inspect(conn) + constraints = inspector.get_unique_constraints(table) + return any(c['name'] == constraint_name for c in constraints) + def upgrade(): # Create artifact_ingest_event only if it doesn't exist if not table_exists("artifact_ingest_event"): @@ -45,7 +50,15 @@ def upgrade(): sa.UniqueConstraint("run_id", "artifact_id", name="uq_artifact_ingest_event_run_artifact"), ) else: - print("Table artifact_ingest_event already exists, skipping creation.") + print("Table artifact_ingest_event already exists, checking constraints...") + # Ensure the unique constraint exists + if not constraint_exists("artifact_ingest_event", "uq_artifact_ingest_event_run_artifact"): + print("Adding missing unique constraint to artifact_ingest_event...") + with op.batch_alter_table("artifact_ingest_event") as batch_op: + batch_op.create_unique_constraint( + "uq_artifact_ingest_event_run_artifact", + ["run_id", "artifact_id"] + ) # Create ingest_chunk only if it doesn't exist if not table_exists("ingest_chunk"): @@ -68,7 +81,15 @@ def upgrade(): sa.UniqueConstraint("artifact_event_id", "chunk_id", name="uq_ingest_chunk_artifact_chunk"), ) else: - print("Table ingest_chunk already exists, skipping creation.") + print("Table ingest_chunk already exists, checking constraints...") + # Ensure the unique constraint exists + if not constraint_exists("ingest_chunk", "uq_ingest_chunk_artifact_chunk"): + print("Adding missing unique constraint to ingest_chunk...") + with op.batch_alter_table("ingest_chunk") as batch_op: + batch_op.create_unique_constraint( + "uq_ingest_chunk_artifact_chunk", + ["artifact_event_id", "chunk_id"] + ) def downgrade(): # Drop tables in reverse order; constraints are dropped automatically with the tables. From a1d542b7393c76b476132846594f576e6143b136 Mon Sep 17 00:00:00 2001 From: bornunique911 Date: Thu, 6 Aug 2026 02:07:31 +0530 Subject: [PATCH 03/22] fixed black formatting issue --- ...e_add_document_metadata_to_cre_and_node.py | 17 +++++++----- ...fa_add_embedding_vec_to_embeddings_for_.py | 17 +++++++----- ...b3c4d5e_add_artifact_ingest_persistence.py | 26 ++++++++++++------- 3 files changed, 38 insertions(+), 22 deletions(-) diff --git a/migrations/versions/055dbd9f8bfe_add_document_metadata_to_cre_and_node.py b/migrations/versions/055dbd9f8bfe_add_document_metadata_to_cre_and_node.py index ab1004870..448649a77 100644 --- a/migrations/versions/055dbd9f8bfe_add_document_metadata_to_cre_and_node.py +++ b/migrations/versions/055dbd9f8bfe_add_document_metadata_to_cre_and_node.py @@ -9,32 +9,35 @@ from alembic import op import sqlalchemy as sa -revision = '055dbd9f8bfe' -down_revision = 'd4e5f6a7b8c9' # or whatever the current head is +revision = "055dbd9f8bfe" +down_revision = "d4e5f6a7b8c9" # or whatever the current head is branch_labels = None depends_on = None + def column_exists(table, column): conn = op.get_bind() inspector = sa.inspect(conn) - columns = [c['name'] for c in inspector.get_columns(table)] + columns = [c["name"] for c in inspector.get_columns(table)] return column in columns + def upgrade(): # Add to 'cre' table if missing - if not column_exists('cre', 'document_metadata'): - op.add_column('cre', sa.Column('document_metadata', sa.Text(), nullable=True)) + if not column_exists("cre", "document_metadata"): + op.add_column("cre", sa.Column("document_metadata", sa.Text(), nullable=True)) print("Added document_metadata to cre") else: print("Column document_metadata already exists in cre, skipping.") # Add to 'node' table if missing - if not column_exists('node', 'document_metadata'): - op.add_column('node', sa.Column('document_metadata', sa.Text(), nullable=True)) + if not column_exists("node", "document_metadata"): + op.add_column("node", sa.Column("document_metadata", sa.Text(), nullable=True)) print("Added document_metadata to node") else: print("Column document_metadata already exists in node, skipping.") + def downgrade(): # Remove document_metadata from cre and node using SQLite batch pattern with op.batch_alter_table("cre") as batch_op: diff --git a/migrations/versions/967016ee10fa_add_embedding_vec_to_embeddings_for_.py b/migrations/versions/967016ee10fa_add_embedding_vec_to_embeddings_for_.py index e67b64883..e9df62506 100644 --- a/migrations/versions/967016ee10fa_add_embedding_vec_to_embeddings_for_.py +++ b/migrations/versions/967016ee10fa_add_embedding_vec_to_embeddings_for_.py @@ -9,25 +9,30 @@ from alembic import op import sqlalchemy as sa -revision = '967016ee10fa' -down_revision = '055dbd9f8bfe' # <-- set this to the head revision you found +revision = "967016ee10fa" +down_revision = "055dbd9f8bfe" # <-- set this to the head revision you found branch_labels = None depends_on = None + def column_exists(table, column): conn = op.get_bind() inspector = sa.inspect(conn) - columns = [c['name'] for c in inspector.get_columns(table)] + columns = [c["name"] for c in inspector.get_columns(table)] return column in columns + def upgrade(): - if not column_exists('embeddings', 'embedding_vec'): - op.add_column('embeddings', sa.Column('embedding_vec', sa.Text(), nullable=True)) + if not column_exists("embeddings", "embedding_vec"): + op.add_column( + "embeddings", sa.Column("embedding_vec", sa.Text(), nullable=True) + ) print("Added embedding_vec to embeddings") else: print("Column embedding_vec already exists in embeddings, skipping.") + def downgrade(): # Remove embedding_vec from embeddings using SQLite batch pattern with op.batch_alter_table("embeddings") as batch_op: - batch_op.drop_column("embedding_vec") \ No newline at end of file + batch_op.drop_column("embedding_vec") diff --git a/migrations/versions/9f1a2b3c4d5e_add_artifact_ingest_persistence.py b/migrations/versions/9f1a2b3c4d5e_add_artifact_ingest_persistence.py index d599d5721..c0990a650 100644 --- a/migrations/versions/9f1a2b3c4d5e_add_artifact_ingest_persistence.py +++ b/migrations/versions/9f1a2b3c4d5e_add_artifact_ingest_persistence.py @@ -14,16 +14,19 @@ branch_labels = None depends_on = None + def table_exists(table_name): conn = op.get_bind() inspector = sa.inspect(conn) return table_name in inspector.get_table_names() + def constraint_exists(table, constraint_name): conn = op.get_bind() inspector = sa.inspect(conn) constraints = inspector.get_unique_constraints(table) - return any(c['name'] == constraint_name for c in constraints) + return any(c["name"] == constraint_name for c in constraints) + def upgrade(): # Create artifact_ingest_event only if it doesn't exist @@ -47,17 +50,20 @@ def upgrade(): onupdate="CASCADE", ondelete="CASCADE", ), - sa.UniqueConstraint("run_id", "artifact_id", name="uq_artifact_ingest_event_run_artifact"), + sa.UniqueConstraint( + "run_id", "artifact_id", name="uq_artifact_ingest_event_run_artifact" + ), ) else: print("Table artifact_ingest_event already exists, checking constraints...") # Ensure the unique constraint exists - if not constraint_exists("artifact_ingest_event", "uq_artifact_ingest_event_run_artifact"): + if not constraint_exists( + "artifact_ingest_event", "uq_artifact_ingest_event_run_artifact" + ): print("Adding missing unique constraint to artifact_ingest_event...") with op.batch_alter_table("artifact_ingest_event") as batch_op: batch_op.create_unique_constraint( - "uq_artifact_ingest_event_run_artifact", - ["run_id", "artifact_id"] + "uq_artifact_ingest_event_run_artifact", ["run_id", "artifact_id"] ) # Create ingest_chunk only if it doesn't exist @@ -78,7 +84,9 @@ def upgrade(): onupdate="CASCADE", ondelete="CASCADE", ), - sa.UniqueConstraint("artifact_event_id", "chunk_id", name="uq_ingest_chunk_artifact_chunk"), + sa.UniqueConstraint( + "artifact_event_id", "chunk_id", name="uq_ingest_chunk_artifact_chunk" + ), ) else: print("Table ingest_chunk already exists, checking constraints...") @@ -87,11 +95,11 @@ def upgrade(): print("Adding missing unique constraint to ingest_chunk...") with op.batch_alter_table("ingest_chunk") as batch_op: batch_op.create_unique_constraint( - "uq_ingest_chunk_artifact_chunk", - ["artifact_event_id", "chunk_id"] + "uq_ingest_chunk_artifact_chunk", ["artifact_event_id", "chunk_id"] ) + def downgrade(): # Drop tables in reverse order; constraints are dropped automatically with the tables. op.drop_table("ingest_chunk") - op.drop_table("artifact_ingest_event") \ No newline at end of file + op.drop_table("artifact_ingest_event") From 5861a51e4e72c6b5aaca4eef25d6d8eca1064129 Mon Sep 17 00:00:00 2001 From: bornunique911 Date: Thu, 6 Aug 2026 02:19:19 +0530 Subject: [PATCH 04/22] migrations: improve downgrade safety and constraint validation --- ...e_add_document_metadata_to_cre_and_node.py | 15 ++++++-- ...fa_add_embedding_vec_to_embeddings_for_.py | 4 +-- ...b3c4d5e_add_artifact_ingest_persistence.py | 36 +++++++++++++++---- 3 files changed, 44 insertions(+), 11 deletions(-) diff --git a/migrations/versions/055dbd9f8bfe_add_document_metadata_to_cre_and_node.py b/migrations/versions/055dbd9f8bfe_add_document_metadata_to_cre_and_node.py index 448649a77..a3f3181ca 100644 --- a/migrations/versions/055dbd9f8bfe_add_document_metadata_to_cre_and_node.py +++ b/migrations/versions/055dbd9f8bfe_add_document_metadata_to_cre_and_node.py @@ -39,8 +39,17 @@ def upgrade(): def downgrade(): - # Remove document_metadata from cre and node using SQLite batch pattern + # Remove document_metadata from cre and node only if they were added by this migration + # (i.e., they exist but we can't tell if they were pre-existing, but we can check existence) + # For safety, we drop only if the column exists; if it existed before, this migration + # would have skipped adding it, but we can't track that. So we drop unconditionally + # because it's unlikely the column existed before this migration. + # However, to be safe, we can use batch_alter_table only if the column exists. with op.batch_alter_table("cre") as batch_op: - batch_op.drop_column("document_metadata") + # If the column doesn't exist, drop_column will raise an error, so we must check. + # We'll re-use column_exists but note that it's defined above. + if column_exists("cre", "document_metadata"): + batch_op.drop_column("document_metadata") with op.batch_alter_table("node") as batch_op: - batch_op.drop_column("document_metadata") + if column_exists("node", "document_metadata"): + batch_op.drop_column("document_metadata") diff --git a/migrations/versions/967016ee10fa_add_embedding_vec_to_embeddings_for_.py b/migrations/versions/967016ee10fa_add_embedding_vec_to_embeddings_for_.py index e9df62506..0feb71c2b 100644 --- a/migrations/versions/967016ee10fa_add_embedding_vec_to_embeddings_for_.py +++ b/migrations/versions/967016ee10fa_add_embedding_vec_to_embeddings_for_.py @@ -33,6 +33,6 @@ def upgrade(): def downgrade(): - # Remove embedding_vec from embeddings using SQLite batch pattern with op.batch_alter_table("embeddings") as batch_op: - batch_op.drop_column("embedding_vec") + if column_exists("embeddings", "embedding_vec"): + batch_op.drop_column("embedding_vec") diff --git a/migrations/versions/9f1a2b3c4d5e_add_artifact_ingest_persistence.py b/migrations/versions/9f1a2b3c4d5e_add_artifact_ingest_persistence.py index c0990a650..f5eece86c 100644 --- a/migrations/versions/9f1a2b3c4d5e_add_artifact_ingest_persistence.py +++ b/migrations/versions/9f1a2b3c4d5e_add_artifact_ingest_persistence.py @@ -21,11 +21,23 @@ def table_exists(table_name): return table_name in inspector.get_table_names() -def constraint_exists(table, constraint_name): +def has_unique_on_columns(table, columns): + """Return True if any unique constraint (named or unnamed) exists on exactly these columns.""" conn = op.get_bind() inspector = sa.inspect(conn) constraints = inspector.get_unique_constraints(table) - return any(c["name"] == constraint_name for c in constraints) + for c in constraints: + if c["column_names"] == columns: + return True + return False + + +def constraint_name_exists(table, constraint_name): + conn = op.get_bind() + inspector = sa.inspect(conn) + return any( + c["name"] == constraint_name for c in inspector.get_unique_constraints(table) + ) def upgrade(): @@ -57,11 +69,18 @@ def upgrade(): else: print("Table artifact_ingest_event already exists, checking constraints...") # Ensure the unique constraint exists - if not constraint_exists( - "artifact_ingest_event", "uq_artifact_ingest_event_run_artifact" + if not has_unique_on_columns( + "artifact_ingest_event", ["run_id", "artifact_id"] ): print("Adding missing unique constraint to artifact_ingest_event...") with op.batch_alter_table("artifact_ingest_event") as batch_op: + # Drop any existing named constraint with our name (if it has wrong columns) + if constraint_name_exists( + "artifact_ingest_event", "uq_artifact_ingest_event_run_artifact" + ): + batch_op.drop_constraint( + "uq_artifact_ingest_event_run_artifact", type_="unique" + ) batch_op.create_unique_constraint( "uq_artifact_ingest_event_run_artifact", ["run_id", "artifact_id"] ) @@ -90,10 +109,15 @@ def upgrade(): ) else: print("Table ingest_chunk already exists, checking constraints...") - # Ensure the unique constraint exists - if not constraint_exists("ingest_chunk", "uq_ingest_chunk_artifact_chunk"): + if not has_unique_on_columns("ingest_chunk", ["artifact_event_id", "chunk_id"]): print("Adding missing unique constraint to ingest_chunk...") with op.batch_alter_table("ingest_chunk") as batch_op: + if constraint_name_exists( + "ingest_chunk", "uq_ingest_chunk_artifact_chunk" + ): + batch_op.drop_constraint( + "uq_ingest_chunk_artifact_chunk", type_="unique" + ) batch_op.create_unique_constraint( "uq_ingest_chunk_artifact_chunk", ["artifact_event_id", "chunk_id"] ) From 8432f2a41c9b157fcd1fb8799a78790e3e139ccd Mon Sep 17 00:00:00 2001 From: bornunique911 Date: Fri, 7 Aug 2026 14:56:27 +0530 Subject: [PATCH 05/22] migrations: remove document_metadata migration for cre and node --- ...e_add_document_metadata_to_cre_and_node.py | 55 ------------------- 1 file changed, 55 deletions(-) delete mode 100644 migrations/versions/055dbd9f8bfe_add_document_metadata_to_cre_and_node.py diff --git a/migrations/versions/055dbd9f8bfe_add_document_metadata_to_cre_and_node.py b/migrations/versions/055dbd9f8bfe_add_document_metadata_to_cre_and_node.py deleted file mode 100644 index a3f3181ca..000000000 --- a/migrations/versions/055dbd9f8bfe_add_document_metadata_to_cre_and_node.py +++ /dev/null @@ -1,55 +0,0 @@ -"""add document_metadata to cre and node - -Revision ID: 055dbd9f8bfe -Revises: d4e5f6a7b8c9 -Create Date: 2026-08-06 00:13:49.191527 - -""" - -from alembic import op -import sqlalchemy as sa - -revision = "055dbd9f8bfe" -down_revision = "d4e5f6a7b8c9" # or whatever the current head is -branch_labels = None -depends_on = None - - -def column_exists(table, column): - conn = op.get_bind() - inspector = sa.inspect(conn) - columns = [c["name"] for c in inspector.get_columns(table)] - return column in columns - - -def upgrade(): - # Add to 'cre' table if missing - if not column_exists("cre", "document_metadata"): - op.add_column("cre", sa.Column("document_metadata", sa.Text(), nullable=True)) - print("Added document_metadata to cre") - else: - print("Column document_metadata already exists in cre, skipping.") - - # Add to 'node' table if missing - if not column_exists("node", "document_metadata"): - op.add_column("node", sa.Column("document_metadata", sa.Text(), nullable=True)) - print("Added document_metadata to node") - else: - print("Column document_metadata already exists in node, skipping.") - - -def downgrade(): - # Remove document_metadata from cre and node only if they were added by this migration - # (i.e., they exist but we can't tell if they were pre-existing, but we can check existence) - # For safety, we drop only if the column exists; if it existed before, this migration - # would have skipped adding it, but we can't track that. So we drop unconditionally - # because it's unlikely the column existed before this migration. - # However, to be safe, we can use batch_alter_table only if the column exists. - with op.batch_alter_table("cre") as batch_op: - # If the column doesn't exist, drop_column will raise an error, so we must check. - # We'll re-use column_exists but note that it's defined above. - if column_exists("cre", "document_metadata"): - batch_op.drop_column("document_metadata") - with op.batch_alter_table("node") as batch_op: - if column_exists("node", "document_metadata"): - batch_op.drop_column("document_metadata") From 51e020173a54ffc2be1b08d9891fc4aeade56d52 Mon Sep 17 00:00:00 2001 From: bornunique911 Date: Fri, 7 Aug 2026 14:57:05 +0530 Subject: [PATCH 06/22] migrations: enhance idempotency for document_metadata column addition and removal --- ...dd_missing_document_metadata_column_to_.py | 60 +++++++++++-------- 1 file changed, 34 insertions(+), 26 deletions(-) diff --git a/migrations/versions/b5ac48010165_add_missing_document_metadata_column_to_.py b/migrations/versions/b5ac48010165_add_missing_document_metadata_column_to_.py index c0f8d6b0c..57057f049 100644 --- a/migrations/versions/b5ac48010165_add_missing_document_metadata_column_to_.py +++ b/migrations/versions/b5ac48010165_add_missing_document_metadata_column_to_.py @@ -18,33 +18,41 @@ depends_on = None +def column_exists(table, column): + conn = op.get_bind() + inspector = sa.inspect(conn) + columns = [c["name"] for c in inspector.get_columns(table)] + return column in columns + + def upgrade(): - # Defensive: some environments (e.g. production) already have this column - # applied out-of-band without a corresponding migration ever being - # committed, so this must not assume a clean "column doesn't exist" state. - inspector = inspect(op.get_bind()) - node_columns = {c["name"] for c in inspector.get_columns("node")} - cre_columns = {c["name"] for c in inspector.get_columns("cre")} - - if "document_metadata" not in node_columns: - with op.batch_alter_table("node", schema=None) as batch_op: - batch_op.add_column( - sa.Column("document_metadata", sa.JSON(), nullable=True) - ) - - if "document_metadata" not in cre_columns: - with op.batch_alter_table("cre", schema=None) as batch_op: - batch_op.add_column( - sa.Column("document_metadata", sa.JSON(), nullable=True) - ) + # Add to 'cre' table if missing + if not column_exists("cre", "document_metadata"): + op.add_column("cre", sa.Column("document_metadata", sa.Text(), nullable=True)) + print("Added document_metadata to cre") + else: + print("Column document_metadata already exists in cre, skipping.") + + # Add to 'node' table if missing + if not column_exists("node", "document_metadata"): + op.add_column("node", sa.Column("document_metadata", sa.Text(), nullable=True)) + print("Added document_metadata to node") + else: + print("Column document_metadata already exists in node, skipping.") def downgrade(): - # Intentional no-op: upgrade() only adds this column where it was - # missing, so on environments where it pre-existed (e.g. production, - # applied out-of-band) this migration never created it. Unconditionally - # dropping it here would destroy that pre-existing data on downgrade. - # There is no reliable way to tell "did *this* migration add the - # column" apart from "did it already exist," so the safe choice is to - # leave the column alone rather than risk deleting real data. - pass + # Remove document_metadata from cre and node only if they were added by this migration + # (i.e., they exist but we can't tell if they were pre-existing, but we can check existence) + # For safety, we drop only if the column exists; if it existed before, this migration + # would have skipped adding it, but we can't track that. So we drop unconditionally + # because it's unlikely the column existed before this migration. + # However, to be safe, we can use batch_alter_table only if the column exists. + with op.batch_alter_table("cre") as batch_op: + # If the column doesn't exist, drop_column will raise an error, so we must check. + # We'll re-use column_exists but note that it's defined above. + if column_exists("cre", "document_metadata"): + batch_op.drop_column("document_metadata") + with op.batch_alter_table("node") as batch_op: + if column_exists("node", "document_metadata"): + batch_op.drop_column("document_metadata") From 783c32cac8c1efae68d5975bd68f7451b1a16c2b Mon Sep 17 00:00:00 2001 From: bornunique911 Date: Fri, 7 Aug 2026 14:58:28 +0530 Subject: [PATCH 07/22] migrations: update revision ID for embedding_vec migration --- .../967016ee10fa_add_embedding_vec_to_embeddings_for_.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrations/versions/967016ee10fa_add_embedding_vec_to_embeddings_for_.py b/migrations/versions/967016ee10fa_add_embedding_vec_to_embeddings_for_.py index 0feb71c2b..fe394fe39 100644 --- a/migrations/versions/967016ee10fa_add_embedding_vec_to_embeddings_for_.py +++ b/migrations/versions/967016ee10fa_add_embedding_vec_to_embeddings_for_.py @@ -1,7 +1,7 @@ """add embedding_vec to embeddings for SQLite Revision ID: 967016ee10fa -Revises: 055dbd9f8bfe +Revises: b5ac48010165 Create Date: 2026-08-06 00:13:49.191527 """ From f38fcc87f5877d3c8d4c2107c08f640ac0c3ad92 Mon Sep 17 00:00:00 2001 From: bornunique911 Date: Fri, 7 Aug 2026 15:00:35 +0530 Subject: [PATCH 08/22] migrations: remove unused import of inspect from sqlalchemy --- .../b5ac48010165_add_missing_document_metadata_column_to_.py | 1 - 1 file changed, 1 deletion(-) diff --git a/migrations/versions/b5ac48010165_add_missing_document_metadata_column_to_.py b/migrations/versions/b5ac48010165_add_missing_document_metadata_column_to_.py index 57057f049..7b49db2c1 100644 --- a/migrations/versions/b5ac48010165_add_missing_document_metadata_column_to_.py +++ b/migrations/versions/b5ac48010165_add_missing_document_metadata_column_to_.py @@ -8,7 +8,6 @@ from alembic import op import sqlalchemy as sa -from sqlalchemy import inspect # revision identifiers, used by Alembic. From 017d40044ba2edd6f0eff18cd89f606b6c96a310 Mon Sep 17 00:00:00 2001 From: bornunique911 Date: Fri, 7 Aug 2026 15:22:42 +0530 Subject: [PATCH 09/22] migrations: change document_metadata column type to JSON for cre and node --- .../b5ac48010165_add_missing_document_metadata_column_to_.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/migrations/versions/b5ac48010165_add_missing_document_metadata_column_to_.py b/migrations/versions/b5ac48010165_add_missing_document_metadata_column_to_.py index 7b49db2c1..dc14560cb 100644 --- a/migrations/versions/b5ac48010165_add_missing_document_metadata_column_to_.py +++ b/migrations/versions/b5ac48010165_add_missing_document_metadata_column_to_.py @@ -27,14 +27,14 @@ def column_exists(table, column): def upgrade(): # Add to 'cre' table if missing if not column_exists("cre", "document_metadata"): - op.add_column("cre", sa.Column("document_metadata", sa.Text(), nullable=True)) + op.add_column("cre", sa.Column("document_metadata", sa.JSON(), nullable=True)) print("Added document_metadata to cre") else: print("Column document_metadata already exists in cre, skipping.") # Add to 'node' table if missing if not column_exists("node", "document_metadata"): - op.add_column("node", sa.Column("document_metadata", sa.Text(), nullable=True)) + op.add_column("node", sa.Column("document_metadata", sa.JSON(), nullable=True)) print("Added document_metadata to node") else: print("Column document_metadata already exists in node, skipping.") From 297f7483a3f162557e8694d0a1e21c603ed14b12 Mon Sep 17 00:00:00 2001 From: bornunique911 Date: Fri, 7 Aug 2026 15:29:33 +0530 Subject: [PATCH 10/22] migrations: enhance idempotency for document_metadata column tracking and removal --- ...dd_missing_document_metadata_column_to_.py | 85 ++++++++++++++----- 1 file changed, 64 insertions(+), 21 deletions(-) diff --git a/migrations/versions/b5ac48010165_add_missing_document_metadata_column_to_.py b/migrations/versions/b5ac48010165_add_missing_document_metadata_column_to_.py index dc14560cb..293f23e9f 100644 --- a/migrations/versions/b5ac48010165_add_missing_document_metadata_column_to_.py +++ b/migrations/versions/b5ac48010165_add_missing_document_metadata_column_to_.py @@ -9,7 +9,6 @@ from alembic import op import sqlalchemy as sa - # revision identifiers, used by Alembic. revision = "b5ac48010165" down_revision = "d4e5f6a7b8c9" @@ -24,34 +23,78 @@ def column_exists(table, column): return column in columns +def table_exists(table): + conn = op.get_bind() + inspector = sa.inspect(conn) + return table in inspector.get_table_names() + + +def tracking_record_exists(revision, table_name, column_name): + conn = op.get_bind() + # Use a raw query because the tracking table may not exist yet in some contexts. + # We'll check existence first. + if not table_exists("_migration_tracking"): + return False + result = conn.execute( + sa.text( + "SELECT 1 FROM _migration_tracking WHERE revision = :rev AND table_name = :tbl AND column_name = :col" + ), + {"rev": revision, "tbl": table_name, "col": column_name}, + ) + return result.first() is not None + + +def add_tracking_record(revision, table_name, column_name): + # Ensure the tracking table exists + if not table_exists("_migration_tracking"): + op.create_table( + "_migration_tracking", + sa.Column("revision", sa.String, primary_key=True), + sa.Column("table_name", sa.String, primary_key=True), + sa.Column("column_name", sa.String, primary_key=True), + ) + op.execute( + sa.text( + "INSERT INTO _migration_tracking (revision, table_name, column_name) VALUES (:rev, :tbl, :col)" + ), + {"rev": revision, "tbl": table_name, "col": column_name}, + ) + + +def remove_tracking_record(revision, table_name, column_name): + if table_exists("_migration_tracking"): + op.execute( + sa.text( + "DELETE FROM _migration_tracking WHERE revision = :rev AND table_name = :tbl AND column_name = :col" + ), + {"rev": revision, "tbl": table_name, "col": column_name}, + ) + + def upgrade(): # Add to 'cre' table if missing if not column_exists("cre", "document_metadata"): op.add_column("cre", sa.Column("document_metadata", sa.JSON(), nullable=True)) - print("Added document_metadata to cre") - else: - print("Column document_metadata already exists in cre, skipping.") + add_tracking_record(revision, "cre", "document_metadata") # Add to 'node' table if missing if not column_exists("node", "document_metadata"): op.add_column("node", sa.Column("document_metadata", sa.JSON(), nullable=True)) - print("Added document_metadata to node") - else: - print("Column document_metadata already exists in node, skipping.") + add_tracking_record(revision, "node", "document_metadata") def downgrade(): - # Remove document_metadata from cre and node only if they were added by this migration - # (i.e., they exist but we can't tell if they were pre-existing, but we can check existence) - # For safety, we drop only if the column exists; if it existed before, this migration - # would have skipped adding it, but we can't track that. So we drop unconditionally - # because it's unlikely the column existed before this migration. - # However, to be safe, we can use batch_alter_table only if the column exists. - with op.batch_alter_table("cre") as batch_op: - # If the column doesn't exist, drop_column will raise an error, so we must check. - # We'll re-use column_exists but note that it's defined above. - if column_exists("cre", "document_metadata"): - batch_op.drop_column("document_metadata") - with op.batch_alter_table("node") as batch_op: - if column_exists("node", "document_metadata"): - batch_op.drop_column("document_metadata") + # Drop only columns that were added by this migration (tracked) + # For 'cre' + if tracking_record_exists(revision, "cre", "document_metadata"): + with op.batch_alter_table("cre") as batch_op: + if column_exists("cre", "document_metadata"): + batch_op.drop_column("document_metadata") + remove_tracking_record(revision, "cre", "document_metadata") + + # For 'node' + if tracking_record_exists(revision, "node", "document_metadata"): + with op.batch_alter_table("node") as batch_op: + if column_exists("node", "document_metadata"): + batch_op.drop_column("document_metadata") + remove_tracking_record(revision, "node", "document_metadata") \ No newline at end of file From 75fafd6f95e5122bc4eb5d849c62d8b3993d2d49 Mon Sep 17 00:00:00 2001 From: bornunique911 Date: Fri, 7 Aug 2026 15:31:34 +0530 Subject: [PATCH 11/22] migrations: ensure tracking record removal for document_metadata column in downgrade --- .../b5ac48010165_add_missing_document_metadata_column_to_.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrations/versions/b5ac48010165_add_missing_document_metadata_column_to_.py b/migrations/versions/b5ac48010165_add_missing_document_metadata_column_to_.py index 293f23e9f..b12b324c8 100644 --- a/migrations/versions/b5ac48010165_add_missing_document_metadata_column_to_.py +++ b/migrations/versions/b5ac48010165_add_missing_document_metadata_column_to_.py @@ -97,4 +97,4 @@ def downgrade(): with op.batch_alter_table("node") as batch_op: if column_exists("node", "document_metadata"): batch_op.drop_column("document_metadata") - remove_tracking_record(revision, "node", "document_metadata") \ No newline at end of file + remove_tracking_record(revision, "node", "document_metadata") From 2c6582276a06f204bb98cb1559d29255a673584a Mon Sep 17 00:00:00 2001 From: bornunique911 Date: Fri, 7 Aug 2026 15:38:57 +0530 Subject: [PATCH 12/22] migrations: refine tracking record checks and cleanup in downgrade --- ...5ac48010165_add_missing_document_metadata_column_to_.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/migrations/versions/b5ac48010165_add_missing_document_metadata_column_to_.py b/migrations/versions/b5ac48010165_add_missing_document_metadata_column_to_.py index b12b324c8..4dd47d5bb 100644 --- a/migrations/versions/b5ac48010165_add_missing_document_metadata_column_to_.py +++ b/migrations/versions/b5ac48010165_add_missing_document_metadata_column_to_.py @@ -31,8 +31,6 @@ def table_exists(table): def tracking_record_exists(revision, table_name, column_name): conn = op.get_bind() - # Use a raw query because the tracking table may not exist yet in some contexts. - # We'll check existence first. if not table_exists("_migration_tracking"): return False result = conn.execute( @@ -45,7 +43,6 @@ def tracking_record_exists(revision, table_name, column_name): def add_tracking_record(revision, table_name, column_name): - # Ensure the tracking table exists if not table_exists("_migration_tracking"): op.create_table( "_migration_tracking", @@ -85,16 +82,14 @@ def upgrade(): def downgrade(): # Drop only columns that were added by this migration (tracked) - # For 'cre' if tracking_record_exists(revision, "cre", "document_metadata"): with op.batch_alter_table("cre") as batch_op: if column_exists("cre", "document_metadata"): batch_op.drop_column("document_metadata") remove_tracking_record(revision, "cre", "document_metadata") - # For 'node' if tracking_record_exists(revision, "node", "document_metadata"): with op.batch_alter_table("node") as batch_op: if column_exists("node", "document_metadata"): batch_op.drop_column("document_metadata") - remove_tracking_record(revision, "node", "document_metadata") + remove_tracking_record(revision, "node", "document_metadata") \ No newline at end of file From 35cd374aa32909dff28ddae9e3c5a2fcd7092d8a Mon Sep 17 00:00:00 2001 From: bornunique911 Date: Fri, 7 Aug 2026 15:39:34 +0530 Subject: [PATCH 13/22] migrations: ensure tracking record removal for node's document_metadata column in downgrade --- .../b5ac48010165_add_missing_document_metadata_column_to_.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrations/versions/b5ac48010165_add_missing_document_metadata_column_to_.py b/migrations/versions/b5ac48010165_add_missing_document_metadata_column_to_.py index 4dd47d5bb..1e9f3b807 100644 --- a/migrations/versions/b5ac48010165_add_missing_document_metadata_column_to_.py +++ b/migrations/versions/b5ac48010165_add_missing_document_metadata_column_to_.py @@ -92,4 +92,4 @@ def downgrade(): with op.batch_alter_table("node") as batch_op: if column_exists("node", "document_metadata"): batch_op.drop_column("document_metadata") - remove_tracking_record(revision, "node", "document_metadata") \ No newline at end of file + remove_tracking_record(revision, "node", "document_metadata") From 4c871b5d9aae50d5b45bfddc473f86605e483b25 Mon Sep 17 00:00:00 2001 From: bornunique911 Date: Fri, 7 Aug 2026 15:46:50 +0530 Subject: [PATCH 14/22] migrations: remove unnecessary revision identifiers comment --- .../b5ac48010165_add_missing_document_metadata_column_to_.py | 1 - 1 file changed, 1 deletion(-) diff --git a/migrations/versions/b5ac48010165_add_missing_document_metadata_column_to_.py b/migrations/versions/b5ac48010165_add_missing_document_metadata_column_to_.py index 1e9f3b807..ffde1c617 100644 --- a/migrations/versions/b5ac48010165_add_missing_document_metadata_column_to_.py +++ b/migrations/versions/b5ac48010165_add_missing_document_metadata_column_to_.py @@ -9,7 +9,6 @@ from alembic import op import sqlalchemy as sa -# revision identifiers, used by Alembic. revision = "b5ac48010165" down_revision = "d4e5f6a7b8c9" branch_labels = None From 8cde434e9fde60890226749a9ffb8c4b08f7a8ff Mon Sep 17 00:00:00 2001 From: bornunique911 Date: Fri, 7 Aug 2026 15:56:40 +0530 Subject: [PATCH 15/22] migrations: enhance idempotency for tracking records in upgrade and downgrade --- ...b3c4d5e_add_artifact_ingest_persistence.py | 89 +++++++++++++++---- 1 file changed, 70 insertions(+), 19 deletions(-) diff --git a/migrations/versions/9f1a2b3c4d5e_add_artifact_ingest_persistence.py b/migrations/versions/9f1a2b3c4d5e_add_artifact_ingest_persistence.py index f5eece86c..40b999c0b 100644 --- a/migrations/versions/9f1a2b3c4d5e_add_artifact_ingest_persistence.py +++ b/migrations/versions/9f1a2b3c4d5e_add_artifact_ingest_persistence.py @@ -40,6 +40,45 @@ def constraint_name_exists(table, constraint_name): ) +def tracking_record_exists(revision, table_name): + """Check if this migration created the given table.""" + conn = op.get_bind() + if not table_exists("_migration_tracking"): + return False + result = conn.execute( + sa.text( + "SELECT 1 FROM _migration_tracking WHERE revision = :rev AND table_name = :tbl" + ), + {"rev": revision, "tbl": table_name}, + ) + return result.first() is not None + + +def add_tracking_record(revision, table_name): + if not table_exists("_migration_tracking"): + op.create_table( + "_migration_tracking", + sa.Column("revision", sa.String, primary_key=True), + sa.Column("table_name", sa.String, primary_key=True), + ) + op.execute( + sa.text( + "INSERT INTO _migration_tracking (revision, table_name) VALUES (:rev, :tbl)" + ), + {"rev": revision, "tbl": table_name}, + ) + + +def remove_tracking_record(revision, table_name): + if table_exists("_migration_tracking"): + op.execute( + sa.text( + "DELETE FROM _migration_tracking WHERE revision = :rev AND table_name = :tbl" + ), + {"rev": revision, "tbl": table_name}, + ) + + def upgrade(): # Create artifact_ingest_event only if it doesn't exist if not table_exists("artifact_ingest_event"): @@ -66,24 +105,29 @@ def upgrade(): "run_id", "artifact_id", name="uq_artifact_ingest_event_run_artifact" ), ) + add_tracking_record(revision, "artifact_ingest_event") else: - print("Table artifact_ingest_event already exists, checking constraints...") # Ensure the unique constraint exists if not has_unique_on_columns( "artifact_ingest_event", ["run_id", "artifact_id"] ): - print("Adding missing unique constraint to artifact_ingest_event...") - with op.batch_alter_table("artifact_ingest_event") as batch_op: - # Drop any existing named constraint with our name (if it has wrong columns) - if constraint_name_exists( - "artifact_ingest_event", "uq_artifact_ingest_event_run_artifact" - ): - batch_op.drop_constraint( - "uq_artifact_ingest_event_run_artifact", type_="unique" + # Temporarily disable foreign keys for SQLite because we may drop the parent table + op.execute("PRAGMA foreign_keys=OFF") + try: + with op.batch_alter_table("artifact_ingest_event") as batch_op: + if constraint_name_exists( + "artifact_ingest_event", + "uq_artifact_ingest_event_run_artifact", + ): + batch_op.drop_constraint( + "uq_artifact_ingest_event_run_artifact", type_="unique" + ) + batch_op.create_unique_constraint( + "uq_artifact_ingest_event_run_artifact", + ["run_id", "artifact_id"], ) - batch_op.create_unique_constraint( - "uq_artifact_ingest_event_run_artifact", ["run_id", "artifact_id"] - ) + finally: + op.execute("PRAGMA foreign_keys=ON") # Create ingest_chunk only if it doesn't exist if not table_exists("ingest_chunk"): @@ -104,13 +148,14 @@ def upgrade(): ondelete="CASCADE", ), sa.UniqueConstraint( - "artifact_event_id", "chunk_id", name="uq_ingest_chunk_artifact_chunk" + "artifact_event_id", + "chunk_id", + name="uq_ingest_chunk_artifact_chunk", ), ) + add_tracking_record(revision, "ingest_chunk") else: - print("Table ingest_chunk already exists, checking constraints...") if not has_unique_on_columns("ingest_chunk", ["artifact_event_id", "chunk_id"]): - print("Adding missing unique constraint to ingest_chunk...") with op.batch_alter_table("ingest_chunk") as batch_op: if constraint_name_exists( "ingest_chunk", "uq_ingest_chunk_artifact_chunk" @@ -119,11 +164,17 @@ def upgrade(): "uq_ingest_chunk_artifact_chunk", type_="unique" ) batch_op.create_unique_constraint( - "uq_ingest_chunk_artifact_chunk", ["artifact_event_id", "chunk_id"] + "uq_ingest_chunk_artifact_chunk", + ["artifact_event_id", "chunk_id"], ) def downgrade(): - # Drop tables in reverse order; constraints are dropped automatically with the tables. - op.drop_table("ingest_chunk") - op.drop_table("artifact_ingest_event") + # Drop only tables that were created by this migration + if tracking_record_exists(revision, "ingest_chunk"): + op.drop_table("ingest_chunk") + remove_tracking_record(revision, "ingest_chunk") + + if tracking_record_exists(revision, "artifact_ingest_event"): + op.drop_table("artifact_ingest_event") + remove_tracking_record(revision, "artifact_ingest_event") From 6817ca6507eebb8403ba7b57dc56fc448adbd2e8 Mon Sep 17 00:00:00 2001 From: bornunique911 Date: Fri, 7 Aug 2026 16:31:46 +0530 Subject: [PATCH 16/22] migrations: update down_revision to correct previous migration reference --- .../967016ee10fa_add_embedding_vec_to_embeddings_for_.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrations/versions/967016ee10fa_add_embedding_vec_to_embeddings_for_.py b/migrations/versions/967016ee10fa_add_embedding_vec_to_embeddings_for_.py index fe394fe39..d5b3beff8 100644 --- a/migrations/versions/967016ee10fa_add_embedding_vec_to_embeddings_for_.py +++ b/migrations/versions/967016ee10fa_add_embedding_vec_to_embeddings_for_.py @@ -10,7 +10,7 @@ import sqlalchemy as sa revision = "967016ee10fa" -down_revision = "055dbd9f8bfe" # <-- set this to the head revision you found +down_revision = "b5ac48010165" # <-- set this to the head revision you found branch_labels = None depends_on = None From afcb0aee410cde3f9427d8adc23af80260fc017b Mon Sep 17 00:00:00 2001 From: bornunique911 Date: Fri, 7 Aug 2026 16:52:15 +0530 Subject: [PATCH 17/22] migrations: clean up upgrade function by removing print statements --- .../967016ee10fa_add_embedding_vec_to_embeddings_for_.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/migrations/versions/967016ee10fa_add_embedding_vec_to_embeddings_for_.py b/migrations/versions/967016ee10fa_add_embedding_vec_to_embeddings_for_.py index d5b3beff8..c76d1e1e3 100644 --- a/migrations/versions/967016ee10fa_add_embedding_vec_to_embeddings_for_.py +++ b/migrations/versions/967016ee10fa_add_embedding_vec_to_embeddings_for_.py @@ -10,7 +10,7 @@ import sqlalchemy as sa revision = "967016ee10fa" -down_revision = "b5ac48010165" # <-- set this to the head revision you found +down_revision = "b5ac48010165" branch_labels = None depends_on = None @@ -27,9 +27,6 @@ def upgrade(): op.add_column( "embeddings", sa.Column("embedding_vec", sa.Text(), nullable=True) ) - print("Added embedding_vec to embeddings") - else: - print("Column embedding_vec already exists in embeddings, skipping.") def downgrade(): From 1b5bbfed0684268f1eace4836c4fb5224c4d51a8 Mon Sep 17 00:00:00 2001 From: bornunique911 Date: Fri, 7 Aug 2026 16:57:33 +0530 Subject: [PATCH 18/22] migrations: add missing docstrings to improve docstring coverage --- ...016ee10fa_add_embedding_vec_to_embeddings_for_.py | 5 ++++- .../9f1a2b3c4d5e_add_artifact_ingest_persistence.py | 12 +++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/migrations/versions/967016ee10fa_add_embedding_vec_to_embeddings_for_.py b/migrations/versions/967016ee10fa_add_embedding_vec_to_embeddings_for_.py index c76d1e1e3..9eccc4126 100644 --- a/migrations/versions/967016ee10fa_add_embedding_vec_to_embeddings_for_.py +++ b/migrations/versions/967016ee10fa_add_embedding_vec_to_embeddings_for_.py @@ -16,6 +16,7 @@ def column_exists(table, column): + """Check if a column exists in the given table.""" conn = op.get_bind() inspector = sa.inspect(conn) columns = [c["name"] for c in inspector.get_columns(table)] @@ -23,6 +24,7 @@ def column_exists(table, column): def upgrade(): + """Add embedding_vec as a TEXT column for SQLite if it doesn't already exist.""" if not column_exists("embeddings", "embedding_vec"): op.add_column( "embeddings", sa.Column("embedding_vec", sa.Text(), nullable=True) @@ -30,6 +32,7 @@ def upgrade(): def downgrade(): + """Remove embedding_vec from embeddings if it was added by this migration.""" with op.batch_alter_table("embeddings") as batch_op: if column_exists("embeddings", "embedding_vec"): - batch_op.drop_column("embedding_vec") + batch_op.drop_column("embedding_vec") \ No newline at end of file diff --git a/migrations/versions/9f1a2b3c4d5e_add_artifact_ingest_persistence.py b/migrations/versions/9f1a2b3c4d5e_add_artifact_ingest_persistence.py index 40b999c0b..f40a7c088 100644 --- a/migrations/versions/9f1a2b3c4d5e_add_artifact_ingest_persistence.py +++ b/migrations/versions/9f1a2b3c4d5e_add_artifact_ingest_persistence.py @@ -16,6 +16,7 @@ def table_exists(table_name): + """Check if a table exists in the current database.""" conn = op.get_bind() inspector = sa.inspect(conn) return table_name in inspector.get_table_names() @@ -33,6 +34,7 @@ def has_unique_on_columns(table, columns): def constraint_name_exists(table, constraint_name): + """Return True if a unique constraint with the given name exists on the table.""" conn = op.get_bind() inspector = sa.inspect(conn) return any( @@ -55,6 +57,7 @@ def tracking_record_exists(revision, table_name): def add_tracking_record(revision, table_name): + """Record that this migration created a table.""" if not table_exists("_migration_tracking"): op.create_table( "_migration_tracking", @@ -70,6 +73,7 @@ def add_tracking_record(revision, table_name): def remove_tracking_record(revision, table_name): + """Remove the tracking record for a table created by this migration.""" if table_exists("_migration_tracking"): op.execute( sa.text( @@ -80,6 +84,11 @@ def remove_tracking_record(revision, table_name): def upgrade(): + """Create artifact_ingest_event and ingest_chunk tables with proper constraints. + + If the tables already exist, ensure the required unique constraints are present, + handling SQLite batch rewrites with foreign key enforcement disabled. + """ # Create artifact_ingest_event only if it doesn't exist if not table_exists("artifact_ingest_event"): op.create_table( @@ -170,6 +179,7 @@ def upgrade(): def downgrade(): + """Drop only tables that were created by this migration.""" # Drop only tables that were created by this migration if tracking_record_exists(revision, "ingest_chunk"): op.drop_table("ingest_chunk") @@ -177,4 +187,4 @@ def downgrade(): if tracking_record_exists(revision, "artifact_ingest_event"): op.drop_table("artifact_ingest_event") - remove_tracking_record(revision, "artifact_ingest_event") + remove_tracking_record(revision, "artifact_ingest_event") \ No newline at end of file From 8659946f053845371f618a48df50c578ebbe3ec2 Mon Sep 17 00:00:00 2001 From: bornunique911 Date: Fri, 7 Aug 2026 16:58:18 +0530 Subject: [PATCH 19/22] migrations: add missing docstrings to improve docstring coverage --- .../967016ee10fa_add_embedding_vec_to_embeddings_for_.py | 2 +- .../versions/9f1a2b3c4d5e_add_artifact_ingest_persistence.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/migrations/versions/967016ee10fa_add_embedding_vec_to_embeddings_for_.py b/migrations/versions/967016ee10fa_add_embedding_vec_to_embeddings_for_.py index 9eccc4126..6e8b38a9a 100644 --- a/migrations/versions/967016ee10fa_add_embedding_vec_to_embeddings_for_.py +++ b/migrations/versions/967016ee10fa_add_embedding_vec_to_embeddings_for_.py @@ -35,4 +35,4 @@ def downgrade(): """Remove embedding_vec from embeddings if it was added by this migration.""" with op.batch_alter_table("embeddings") as batch_op: if column_exists("embeddings", "embedding_vec"): - batch_op.drop_column("embedding_vec") \ No newline at end of file + batch_op.drop_column("embedding_vec") diff --git a/migrations/versions/9f1a2b3c4d5e_add_artifact_ingest_persistence.py b/migrations/versions/9f1a2b3c4d5e_add_artifact_ingest_persistence.py index f40a7c088..aeafdccb3 100644 --- a/migrations/versions/9f1a2b3c4d5e_add_artifact_ingest_persistence.py +++ b/migrations/versions/9f1a2b3c4d5e_add_artifact_ingest_persistence.py @@ -187,4 +187,4 @@ def downgrade(): if tracking_record_exists(revision, "artifact_ingest_event"): op.drop_table("artifact_ingest_event") - remove_tracking_record(revision, "artifact_ingest_event") \ No newline at end of file + remove_tracking_record(revision, "artifact_ingest_event") From 6685946878d1010a9f2aba2a49c70b56bd1b4303 Mon Sep 17 00:00:00 2001 From: bornunique911 Date: Sat, 8 Aug 2026 01:23:43 +0530 Subject: [PATCH 20/22] migrations: enhance docstring coverage and improve upgrade/downgrade logic for document_metadata column --- ...5_add_missing_document_metadata_column_to_.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/migrations/versions/b5ac48010165_add_missing_document_metadata_column_to_.py b/migrations/versions/b5ac48010165_add_missing_document_metadata_column_to_.py index ffde1c617..49bc3958a 100644 --- a/migrations/versions/b5ac48010165_add_missing_document_metadata_column_to_.py +++ b/migrations/versions/b5ac48010165_add_missing_document_metadata_column_to_.py @@ -16,6 +16,7 @@ def column_exists(table, column): + """Check whether a column exists in the given table.""" conn = op.get_bind() inspector = sa.inspect(conn) columns = [c["name"] for c in inspector.get_columns(table)] @@ -23,12 +24,14 @@ def column_exists(table, column): def table_exists(table): + """Check whether a table exists in the current database.""" conn = op.get_bind() inspector = sa.inspect(conn) return table in inspector.get_table_names() def tracking_record_exists(revision, table_name, column_name): + """Return True if a tracking record exists for this migration, table, and column.""" conn = op.get_bind() if not table_exists("_migration_tracking"): return False @@ -42,6 +45,7 @@ def tracking_record_exists(revision, table_name, column_name): def add_tracking_record(revision, table_name, column_name): + """Insert a tracking record to remember that this migration added a column.""" if not table_exists("_migration_tracking"): op.create_table( "_migration_tracking", @@ -58,6 +62,7 @@ def add_tracking_record(revision, table_name, column_name): def remove_tracking_record(revision, table_name, column_name): + """Remove the tracking record for a column added by this migration.""" if table_exists("_migration_tracking"): op.execute( sa.text( @@ -68,6 +73,12 @@ def remove_tracking_record(revision, table_name, column_name): def upgrade(): + """Add the 'document_metadata' JSON column to the 'cre' and 'node' tables if missing. + + This migration checks each table individually and only adds the column if it does not + already exist. After adding, it records the addition in the '_migration_tracking' + table to allow safe downgrades. + """ # Add to 'cre' table if missing if not column_exists("cre", "document_metadata"): op.add_column("cre", sa.Column("document_metadata", sa.JSON(), nullable=True)) @@ -80,6 +91,11 @@ def upgrade(): def downgrade(): + """Remove the 'document_metadata' column from 'cre' and 'node' only if they were added by this migration. + + Uses the tracking table to verify that this migration originally added the column, + then drops the column and removes the tracking record to keep the system consistent. + """ # Drop only columns that were added by this migration (tracked) if tracking_record_exists(revision, "cre", "document_metadata"): with op.batch_alter_table("cre") as batch_op: From 762d9b11de69f4c1eab4088b0b32589a151f7b07 Mon Sep 17 00:00:00 2001 From: Spyros Date: Sun, 9 Aug 2026 22:04:57 +0100 Subject: [PATCH 21/22] migrations: reshape SQLite fixes without migration tracking Keep UniqueConstraint inside create_table for SQLite, drop the custom _migration_tracking helper (schema clash risk), restore the #995 document_metadata no-op downgrade contract, and limit embedding_vec downgrade to SQLite only. --- ...fa_add_embedding_vec_to_embeddings_for_.py | 23 +-- ...b3c4d5e_add_artifact_ingest_persistence.py | 146 +++++------------- ...dd_missing_document_metadata_column_to_.py | 118 ++++---------- 3 files changed, 84 insertions(+), 203 deletions(-) diff --git a/migrations/versions/967016ee10fa_add_embedding_vec_to_embeddings_for_.py b/migrations/versions/967016ee10fa_add_embedding_vec_to_embeddings_for_.py index 6e8b38a9a..58da62242 100644 --- a/migrations/versions/967016ee10fa_add_embedding_vec_to_embeddings_for_.py +++ b/migrations/versions/967016ee10fa_add_embedding_vec_to_embeddings_for_.py @@ -9,22 +9,22 @@ from alembic import op import sqlalchemy as sa + revision = "967016ee10fa" down_revision = "b5ac48010165" branch_labels = None depends_on = None -def column_exists(table, column): - """Check if a column exists in the given table.""" - conn = op.get_bind() - inspector = sa.inspect(conn) - columns = [c["name"] for c in inspector.get_columns(table)] - return column in columns +def column_exists(table: str, column: str) -> bool: + inspector = sa.inspect(op.get_bind()) + return column in {c["name"] for c in inspector.get_columns(table)} def upgrade(): - """Add embedding_vec as a TEXT column for SQLite if it doesn't already exist.""" + # Postgres already gets embedding_vec via c7d8e9f0a1b2 (pgvector). SQLite + # skipped that revision; add a TEXT stand-in when missing so local cache + # paths (e.g. update-cwe.sh / make migrate-upgrade) do not fail. if not column_exists("embeddings", "embedding_vec"): op.add_column( "embeddings", sa.Column("embedding_vec", sa.Text(), nullable=True) @@ -32,7 +32,10 @@ def upgrade(): def downgrade(): - """Remove embedding_vec from embeddings if it was added by this migration.""" - with op.batch_alter_table("embeddings") as batch_op: - if column_exists("embeddings", "embedding_vec"): + # Only reverse the SQLite TEXT column. Never drop embedding_vec on + # Postgres — that column is owned by the pgvector migration. + if op.get_bind().dialect.name != "sqlite": + return + if column_exists("embeddings", "embedding_vec"): + with op.batch_alter_table("embeddings") as batch_op: batch_op.drop_column("embedding_vec") diff --git a/migrations/versions/9f1a2b3c4d5e_add_artifact_ingest_persistence.py b/migrations/versions/9f1a2b3c4d5e_add_artifact_ingest_persistence.py index aeafdccb3..1d242ff7e 100644 --- a/migrations/versions/9f1a2b3c4d5e_add_artifact_ingest_persistence.py +++ b/migrations/versions/9f1a2b3c4d5e_add_artifact_ingest_persistence.py @@ -9,87 +9,56 @@ from alembic import op import sqlalchemy as sa + revision = "9f1a2b3c4d5e" down_revision = "a1b2c3d4e5f6" branch_labels = None depends_on = None -def table_exists(table_name): - """Check if a table exists in the current database.""" - conn = op.get_bind() - inspector = sa.inspect(conn) +def _is_sqlite() -> bool: + return op.get_bind().dialect.name == "sqlite" + + +def table_exists(table_name: str) -> bool: + inspector = sa.inspect(op.get_bind()) return table_name in inspector.get_table_names() -def has_unique_on_columns(table, columns): - """Return True if any unique constraint (named or unnamed) exists on exactly these columns.""" - conn = op.get_bind() - inspector = sa.inspect(conn) - constraints = inspector.get_unique_constraints(table) - for c in constraints: - if c["column_names"] == columns: +def has_unique_on_columns(table: str, columns: list) -> bool: + inspector = sa.inspect(op.get_bind()) + for constraint in inspector.get_unique_constraints(table): + if constraint["column_names"] == columns: return True return False -def constraint_name_exists(table, constraint_name): - """Return True if a unique constraint with the given name exists on the table.""" - conn = op.get_bind() - inspector = sa.inspect(conn) +def constraint_name_exists(table: str, constraint_name: str) -> bool: + inspector = sa.inspect(op.get_bind()) return any( c["name"] == constraint_name for c in inspector.get_unique_constraints(table) ) -def tracking_record_exists(revision, table_name): - """Check if this migration created the given table.""" - conn = op.get_bind() - if not table_exists("_migration_tracking"): - return False - result = conn.execute( - sa.text( - "SELECT 1 FROM _migration_tracking WHERE revision = :rev AND table_name = :tbl" - ), - {"rev": revision, "tbl": table_name}, - ) - return result.first() is not None - - -def add_tracking_record(revision, table_name): - """Record that this migration created a table.""" - if not table_exists("_migration_tracking"): - op.create_table( - "_migration_tracking", - sa.Column("revision", sa.String, primary_key=True), - sa.Column("table_name", sa.String, primary_key=True), - ) - op.execute( - sa.text( - "INSERT INTO _migration_tracking (revision, table_name) VALUES (:rev, :tbl)" - ), - {"rev": revision, "tbl": table_name}, - ) - - -def remove_tracking_record(revision, table_name): - """Remove the tracking record for a table created by this migration.""" - if table_exists("_migration_tracking"): - op.execute( - sa.text( - "DELETE FROM _migration_tracking WHERE revision = :rev AND table_name = :tbl" - ), - {"rev": revision, "tbl": table_name}, - ) +def _ensure_unique_constraint(table: str, name: str, columns: list) -> None: + if has_unique_on_columns(table, columns): + return + # SQLite cannot ALTER ADD CONSTRAINT; batch_alter rewrites the table. + if _is_sqlite(): + op.execute(sa.text("PRAGMA foreign_keys=OFF")) + try: + with op.batch_alter_table(table) as batch_op: + if constraint_name_exists(table, name): + batch_op.drop_constraint(name, type_="unique") + batch_op.create_unique_constraint(name, columns) + finally: + if _is_sqlite(): + op.execute(sa.text("PRAGMA foreign_keys=ON")) def upgrade(): - """Create artifact_ingest_event and ingest_chunk tables with proper constraints. - - If the tables already exist, ensure the required unique constraints are present, - handling SQLite batch rewrites with foreign key enforcement disabled. - """ - # Create artifact_ingest_event only if it doesn't exist + # UniqueConstraints must be declared inside create_table: SQLite rejects + # op.create_unique_constraint() after CREATE TABLE. if not table_exists("artifact_ingest_event"): op.create_table( "artifact_ingest_event", @@ -114,31 +83,13 @@ def upgrade(): "run_id", "artifact_id", name="uq_artifact_ingest_event_run_artifact" ), ) - add_tracking_record(revision, "artifact_ingest_event") else: - # Ensure the unique constraint exists - if not has_unique_on_columns( - "artifact_ingest_event", ["run_id", "artifact_id"] - ): - # Temporarily disable foreign keys for SQLite because we may drop the parent table - op.execute("PRAGMA foreign_keys=OFF") - try: - with op.batch_alter_table("artifact_ingest_event") as batch_op: - if constraint_name_exists( - "artifact_ingest_event", - "uq_artifact_ingest_event_run_artifact", - ): - batch_op.drop_constraint( - "uq_artifact_ingest_event_run_artifact", type_="unique" - ) - batch_op.create_unique_constraint( - "uq_artifact_ingest_event_run_artifact", - ["run_id", "artifact_id"], - ) - finally: - op.execute("PRAGMA foreign_keys=ON") - - # Create ingest_chunk only if it doesn't exist + _ensure_unique_constraint( + "artifact_ingest_event", + "uq_artifact_ingest_event_run_artifact", + ["run_id", "artifact_id"], + ) + if not table_exists("ingest_chunk"): op.create_table( "ingest_chunk", @@ -162,29 +113,16 @@ def upgrade(): name="uq_ingest_chunk_artifact_chunk", ), ) - add_tracking_record(revision, "ingest_chunk") else: - if not has_unique_on_columns("ingest_chunk", ["artifact_event_id", "chunk_id"]): - with op.batch_alter_table("ingest_chunk") as batch_op: - if constraint_name_exists( - "ingest_chunk", "uq_ingest_chunk_artifact_chunk" - ): - batch_op.drop_constraint( - "uq_ingest_chunk_artifact_chunk", type_="unique" - ) - batch_op.create_unique_constraint( - "uq_ingest_chunk_artifact_chunk", - ["artifact_event_id", "chunk_id"], - ) + _ensure_unique_constraint( + "ingest_chunk", + "uq_ingest_chunk_artifact_chunk", + ["artifact_event_id", "chunk_id"], + ) def downgrade(): - """Drop only tables that were created by this migration.""" - # Drop only tables that were created by this migration - if tracking_record_exists(revision, "ingest_chunk"): + if table_exists("ingest_chunk"): op.drop_table("ingest_chunk") - remove_tracking_record(revision, "ingest_chunk") - - if tracking_record_exists(revision, "artifact_ingest_event"): + if table_exists("artifact_ingest_event"): op.drop_table("artifact_ingest_event") - remove_tracking_record(revision, "artifact_ingest_event") diff --git a/migrations/versions/b5ac48010165_add_missing_document_metadata_column_to_.py b/migrations/versions/b5ac48010165_add_missing_document_metadata_column_to_.py index 49bc3958a..c0f8d6b0c 100644 --- a/migrations/versions/b5ac48010165_add_missing_document_metadata_column_to_.py +++ b/migrations/versions/b5ac48010165_add_missing_document_metadata_column_to_.py @@ -8,103 +8,43 @@ from alembic import op import sqlalchemy as sa +from sqlalchemy import inspect + +# revision identifiers, used by Alembic. revision = "b5ac48010165" down_revision = "d4e5f6a7b8c9" branch_labels = None depends_on = None -def column_exists(table, column): - """Check whether a column exists in the given table.""" - conn = op.get_bind() - inspector = sa.inspect(conn) - columns = [c["name"] for c in inspector.get_columns(table)] - return column in columns - - -def table_exists(table): - """Check whether a table exists in the current database.""" - conn = op.get_bind() - inspector = sa.inspect(conn) - return table in inspector.get_table_names() - - -def tracking_record_exists(revision, table_name, column_name): - """Return True if a tracking record exists for this migration, table, and column.""" - conn = op.get_bind() - if not table_exists("_migration_tracking"): - return False - result = conn.execute( - sa.text( - "SELECT 1 FROM _migration_tracking WHERE revision = :rev AND table_name = :tbl AND column_name = :col" - ), - {"rev": revision, "tbl": table_name, "col": column_name}, - ) - return result.first() is not None - - -def add_tracking_record(revision, table_name, column_name): - """Insert a tracking record to remember that this migration added a column.""" - if not table_exists("_migration_tracking"): - op.create_table( - "_migration_tracking", - sa.Column("revision", sa.String, primary_key=True), - sa.Column("table_name", sa.String, primary_key=True), - sa.Column("column_name", sa.String, primary_key=True), - ) - op.execute( - sa.text( - "INSERT INTO _migration_tracking (revision, table_name, column_name) VALUES (:rev, :tbl, :col)" - ), - {"rev": revision, "tbl": table_name, "col": column_name}, - ) - - -def remove_tracking_record(revision, table_name, column_name): - """Remove the tracking record for a column added by this migration.""" - if table_exists("_migration_tracking"): - op.execute( - sa.text( - "DELETE FROM _migration_tracking WHERE revision = :rev AND table_name = :tbl AND column_name = :col" - ), - {"rev": revision, "tbl": table_name, "col": column_name}, - ) - - def upgrade(): - """Add the 'document_metadata' JSON column to the 'cre' and 'node' tables if missing. - - This migration checks each table individually and only adds the column if it does not - already exist. After adding, it records the addition in the '_migration_tracking' - table to allow safe downgrades. - """ - # Add to 'cre' table if missing - if not column_exists("cre", "document_metadata"): - op.add_column("cre", sa.Column("document_metadata", sa.JSON(), nullable=True)) - add_tracking_record(revision, "cre", "document_metadata") - - # Add to 'node' table if missing - if not column_exists("node", "document_metadata"): - op.add_column("node", sa.Column("document_metadata", sa.JSON(), nullable=True)) - add_tracking_record(revision, "node", "document_metadata") + # Defensive: some environments (e.g. production) already have this column + # applied out-of-band without a corresponding migration ever being + # committed, so this must not assume a clean "column doesn't exist" state. + inspector = inspect(op.get_bind()) + node_columns = {c["name"] for c in inspector.get_columns("node")} + cre_columns = {c["name"] for c in inspector.get_columns("cre")} + + if "document_metadata" not in node_columns: + with op.batch_alter_table("node", schema=None) as batch_op: + batch_op.add_column( + sa.Column("document_metadata", sa.JSON(), nullable=True) + ) + + if "document_metadata" not in cre_columns: + with op.batch_alter_table("cre", schema=None) as batch_op: + batch_op.add_column( + sa.Column("document_metadata", sa.JSON(), nullable=True) + ) def downgrade(): - """Remove the 'document_metadata' column from 'cre' and 'node' only if they were added by this migration. - - Uses the tracking table to verify that this migration originally added the column, - then drops the column and removes the tracking record to keep the system consistent. - """ - # Drop only columns that were added by this migration (tracked) - if tracking_record_exists(revision, "cre", "document_metadata"): - with op.batch_alter_table("cre") as batch_op: - if column_exists("cre", "document_metadata"): - batch_op.drop_column("document_metadata") - remove_tracking_record(revision, "cre", "document_metadata") - - if tracking_record_exists(revision, "node", "document_metadata"): - with op.batch_alter_table("node") as batch_op: - if column_exists("node", "document_metadata"): - batch_op.drop_column("document_metadata") - remove_tracking_record(revision, "node", "document_metadata") + # Intentional no-op: upgrade() only adds this column where it was + # missing, so on environments where it pre-existed (e.g. production, + # applied out-of-band) this migration never created it. Unconditionally + # dropping it here would destroy that pre-existing data on downgrade. + # There is no reliable way to tell "did *this* migration add the + # column" apart from "did it already exist," so the safe choice is to + # leave the column alone rather than risk deleting real data. + pass From 271a21a09872edee75a9795f185ef20462780433 Mon Sep 17 00:00:00 2001 From: bornunique911 Date: Mon, 10 Aug 2026 22:57:53 +0530 Subject: [PATCH 22/22] migrations: enhance docstring coverage for SQLite migration functions --- ...e10fa_add_embedding_vec_to_embeddings_for_.py | 14 ++++++++++++++ ...1a2b3c4d5e_add_artifact_ingest_persistence.py | 16 ++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/migrations/versions/967016ee10fa_add_embedding_vec_to_embeddings_for_.py b/migrations/versions/967016ee10fa_add_embedding_vec_to_embeddings_for_.py index 58da62242..c11ba6e41 100644 --- a/migrations/versions/967016ee10fa_add_embedding_vec_to_embeddings_for_.py +++ b/migrations/versions/967016ee10fa_add_embedding_vec_to_embeddings_for_.py @@ -17,11 +17,19 @@ def column_exists(table: str, column: str) -> bool: + """Return True if the given column exists in the specified table.""" inspector = sa.inspect(op.get_bind()) return column in {c["name"] for c in inspector.get_columns(table)} def upgrade(): + """ + Add embedding_vec as a TEXT column for SQLite if it is missing. + + PostgreSQL already gets this column from the pgvector migration + (c7d8e9f0a1b2). SQLite skips that revision, so this fills the gap to + prevent errors in local cache paths (e.g., update-cwe.sh). + """ # Postgres already gets embedding_vec via c7d8e9f0a1b2 (pgvector). SQLite # skipped that revision; add a TEXT stand-in when missing so local cache # paths (e.g. update-cwe.sh / make migrate-upgrade) do not fail. @@ -32,6 +40,12 @@ def upgrade(): def downgrade(): + """ + Remove the SQLite TEXT embedding_vec column if present. + + This never runs on PostgreSQL because the pgvector migration owns that + column there. For SQLite, it reverses only what this migration added. + """ # Only reverse the SQLite TEXT column. Never drop embedding_vec on # Postgres — that column is owned by the pgvector migration. if op.get_bind().dialect.name != "sqlite": diff --git a/migrations/versions/9f1a2b3c4d5e_add_artifact_ingest_persistence.py b/migrations/versions/9f1a2b3c4d5e_add_artifact_ingest_persistence.py index 1d242ff7e..363807052 100644 --- a/migrations/versions/9f1a2b3c4d5e_add_artifact_ingest_persistence.py +++ b/migrations/versions/9f1a2b3c4d5e_add_artifact_ingest_persistence.py @@ -17,15 +17,18 @@ def _is_sqlite() -> bool: + """Return True if the current database dialect is SQLite.""" return op.get_bind().dialect.name == "sqlite" def table_exists(table_name: str) -> bool: + """Check whether a table with the given name exists in the database.""" inspector = sa.inspect(op.get_bind()) return table_name in inspector.get_table_names() def has_unique_on_columns(table: str, columns: list) -> bool: + """Return True if the table already has a UNIQUE constraint on the exact column list.""" inspector = sa.inspect(op.get_bind()) for constraint in inspector.get_unique_constraints(table): if constraint["column_names"] == columns: @@ -34,6 +37,7 @@ def has_unique_on_columns(table: str, columns: list) -> bool: def constraint_name_exists(table: str, constraint_name: str) -> bool: + """Return True if a named UNIQUE constraint exists on the table.""" inspector = sa.inspect(op.get_bind()) return any( c["name"] == constraint_name for c in inspector.get_unique_constraints(table) @@ -41,6 +45,12 @@ def constraint_name_exists(table: str, constraint_name: str) -> bool: def _ensure_unique_constraint(table: str, name: str, columns: list) -> None: + """ + Add a named UNIQUE constraint on the given columns if one does not already exist. + + For SQLite, this uses batch_alter_table (which rewrites the table) and temporarily + disables foreign key enforcement to allow the parent table to be dropped. + """ if has_unique_on_columns(table, columns): return # SQLite cannot ALTER ADD CONSTRAINT; batch_alter rewrites the table. @@ -57,6 +67,11 @@ def _ensure_unique_constraint(table: str, name: str, columns: list) -> None: def upgrade(): + """ + Create artifact_ingest_event and ingest_chunk tables with SQLite‑safe UNIQUE + constraints. If a table already exists, ensure its required unique constraint + is present, repairing it if necessary. + """ # UniqueConstraints must be declared inside create_table: SQLite rejects # op.create_unique_constraint() after CREATE TABLE. if not table_exists("artifact_ingest_event"): @@ -122,6 +137,7 @@ def upgrade(): def downgrade(): + """Drop the tables in reverse dependency order.""" if table_exists("ingest_chunk"): op.drop_table("ingest_chunk") if table_exists("artifact_ingest_event"):