1010
1111import sqlalchemy as sa
1212from alembic import op
13+ from sqlalchemy import text
14+
15+
16+ def column_exists (connection , table : str , column : str ) -> bool :
17+ """Check if a column exists in a table (idempotent migration support)."""
18+ if connection .dialect .name == "postgresql" :
19+ result = connection .execute (
20+ text (
21+ "SELECT 1 FROM information_schema.columns "
22+ "WHERE table_name = :table AND column_name = :column"
23+ ),
24+ {"table" : table , "column" : column },
25+ )
26+ return result .fetchone () is not None
27+ else :
28+ # SQLite
29+ result = connection .execute (text (f"PRAGMA table_info({ table } )" ))
30+ columns = [row [1 ] for row in result ]
31+ return column in columns
32+
33+
34+ def index_exists (connection , index_name : str ) -> bool :
35+ """Check if an index exists (idempotent migration support)."""
36+ if connection .dialect .name == "postgresql" :
37+ result = connection .execute (
38+ text ("SELECT 1 FROM pg_indexes WHERE indexname = :index_name" ),
39+ {"index_name" : index_name },
40+ )
41+ return result .fetchone () is not None
42+ else :
43+ # SQLite
44+ result = connection .execute (
45+ text ("SELECT 1 FROM sqlite_master WHERE type='index' AND name = :index_name" ),
46+ {"index_name" : index_name },
47+ )
48+ return result .fetchone () is not None
1349
1450
1551# revision identifiers, used by Alembic.
@@ -36,101 +72,105 @@ def upgrade() -> None:
3672 # Add project_id to relation table
3773 # -------------------------------------------------------------------------
3874
39- # Step 1: Add project_id column as nullable first
40- op .add_column ("relation" , sa .Column ("project_id" , sa .Integer (), nullable = True ))
75+ # Step 1: Add project_id column as nullable first (idempotent)
76+ if not column_exists (connection , "relation" , "project_id" ):
77+ op .add_column ("relation" , sa .Column ("project_id" , sa .Integer (), nullable = True ))
4178
42- # Step 2: Backfill project_id from entity.project_id via from_id
43- if dialect == "postgresql" :
44- op .execute ("""
45- UPDATE relation
46- SET project_id = entity.project_id
47- FROM entity
48- WHERE relation.from_id = entity.id
49- """ )
50- else :
51- # SQLite syntax
52- op .execute ("""
53- UPDATE relation
54- SET project_id = (
55- SELECT entity.project_id
79+ # Step 2: Backfill project_id from entity.project_id via from_id
80+ if dialect == "postgresql" :
81+ op .execute ("""
82+ UPDATE relation
83+ SET project_id = entity.project_id
5684 FROM entity
57- WHERE entity.id = relation.from_id
58- )
59- """ )
60-
61- # Step 3: Make project_id NOT NULL and add foreign key
62- if dialect == "postgresql" :
63- op .alter_column ("relation" , "project_id" , nullable = False )
64- op .create_foreign_key (
65- "fk_relation_project_id" ,
66- "relation" ,
67- "project" ,
68- ["project_id" ],
69- ["id" ],
70- )
71- else :
72- # SQLite requires batch operations for ALTER COLUMN
73- with op .batch_alter_table ("relation" ) as batch_op :
74- batch_op .alter_column ("project_id" , nullable = False )
75- batch_op .create_foreign_key (
85+ WHERE relation.from_id = entity.id
86+ """ )
87+ else :
88+ # SQLite syntax
89+ op .execute ("""
90+ UPDATE relation
91+ SET project_id = (
92+ SELECT entity.project_id
93+ FROM entity
94+ WHERE entity.id = relation.from_id
95+ )
96+ """ )
97+
98+ # Step 3: Make project_id NOT NULL and add foreign key
99+ if dialect == "postgresql" :
100+ op .alter_column ("relation" , "project_id" , nullable = False )
101+ op .create_foreign_key (
76102 "fk_relation_project_id" ,
103+ "relation" ,
77104 "project" ,
78105 ["project_id" ],
79106 ["id" ],
80107 )
81-
82- # Step 4: Create index on relation.project_id
83- op .create_index ("ix_relation_project_id" , "relation" , ["project_id" ])
108+ else :
109+ # SQLite requires batch operations for ALTER COLUMN
110+ with op .batch_alter_table ("relation" ) as batch_op :
111+ batch_op .alter_column ("project_id" , nullable = False )
112+ batch_op .create_foreign_key (
113+ "fk_relation_project_id" ,
114+ "project" ,
115+ ["project_id" ],
116+ ["id" ],
117+ )
118+
119+ # Step 4: Create index on relation.project_id (idempotent)
120+ if not index_exists (connection , "ix_relation_project_id" ):
121+ op .create_index ("ix_relation_project_id" , "relation" , ["project_id" ])
84122
85123 # -------------------------------------------------------------------------
86124 # Add project_id to observation table
87125 # -------------------------------------------------------------------------
88126
89- # Step 1: Add project_id column as nullable first
90- op .add_column ("observation" , sa .Column ("project_id" , sa .Integer (), nullable = True ))
127+ # Step 1: Add project_id column as nullable first (idempotent)
128+ if not column_exists (connection , "observation" , "project_id" ):
129+ op .add_column ("observation" , sa .Column ("project_id" , sa .Integer (), nullable = True ))
91130
92- # Step 2: Backfill project_id from entity.project_id via entity_id
93- if dialect == "postgresql" :
94- op .execute ("""
95- UPDATE observation
96- SET project_id = entity.project_id
97- FROM entity
98- WHERE observation.entity_id = entity.id
99- """ )
100- else :
101- # SQLite syntax
102- op .execute ("""
103- UPDATE observation
104- SET project_id = (
105- SELECT entity.project_id
131+ # Step 2: Backfill project_id from entity.project_id via entity_id
132+ if dialect == "postgresql" :
133+ op .execute ("""
134+ UPDATE observation
135+ SET project_id = entity.project_id
106136 FROM entity
107- WHERE entity.id = observation.entity_id
108- )
109- """ )
110-
111- # Step 3: Make project_id NOT NULL and add foreign key
112- if dialect == "postgresql" :
113- op .alter_column ("observation" , "project_id" , nullable = False )
114- op .create_foreign_key (
115- "fk_observation_project_id" ,
116- "observation" ,
117- "project" ,
118- ["project_id" ],
119- ["id" ],
120- )
121- else :
122- # SQLite requires batch operations for ALTER COLUMN
123- with op .batch_alter_table ("observation" ) as batch_op :
124- batch_op .alter_column ("project_id" , nullable = False )
125- batch_op .create_foreign_key (
137+ WHERE observation.entity_id = entity.id
138+ """ )
139+ else :
140+ # SQLite syntax
141+ op .execute ("""
142+ UPDATE observation
143+ SET project_id = (
144+ SELECT entity.project_id
145+ FROM entity
146+ WHERE entity.id = observation.entity_id
147+ )
148+ """ )
149+
150+ # Step 3: Make project_id NOT NULL and add foreign key
151+ if dialect == "postgresql" :
152+ op .alter_column ("observation" , "project_id" , nullable = False )
153+ op .create_foreign_key (
126154 "fk_observation_project_id" ,
155+ "observation" ,
127156 "project" ,
128157 ["project_id" ],
129158 ["id" ],
130159 )
131-
132- # Step 4: Create index on observation.project_id
133- op .create_index ("ix_observation_project_id" , "observation" , ["project_id" ])
160+ else :
161+ # SQLite requires batch operations for ALTER COLUMN
162+ with op .batch_alter_table ("observation" ) as batch_op :
163+ batch_op .alter_column ("project_id" , nullable = False )
164+ batch_op .create_foreign_key (
165+ "fk_observation_project_id" ,
166+ "project" ,
167+ ["project_id" ],
168+ ["id" ],
169+ )
170+
171+ # Step 4: Create index on observation.project_id (idempotent)
172+ if not index_exists (connection , "ix_observation_project_id" ):
173+ op .create_index ("ix_observation_project_id" , "observation" , ["project_id" ])
134174
135175 # Postgres-specific: pg_trgm and GIN indexes
136176 if dialect == "postgresql" :
0 commit comments