Skip to content

Commit a4e2d46

Browse files
author
Francisco
committed
Merge branch 'dev/entities-current-dev'
2 parents ab19b14 + f66ec23 commit a4e2d46

8 files changed

Lines changed: 182 additions & 283 deletions

migrations/versions/1c9784351972_add_owner_id_to_thread_table.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@
1313
from sqlalchemy.dialects import mysql
1414

1515
from migrations.utils.safe_ddl import (add_column_if_missing,
16+
create_index_if_missing,
1617
drop_column_if_exists,
17-
safe_alter_column)
18+
drop_index_if_exists, safe_alter_column)
1819

1920
# revision identifiers, used by Alembic.
20-
revision: str = '1c9784351972'
21-
down_revision: Union[str, None] = '1e55188b6b26'
21+
revision: str = "1c9784351972"
22+
down_revision: Union[str, None] = "1e55188b6b26"
2223
branch_labels: Union[str, Sequence[str], None] = None
2324
depends_on: Union[str, Sequence[str], None] = None
2425

@@ -47,7 +48,7 @@ def upgrade() -> None:
4748

4849
# ── threads.owner_id ────────────────────────────────────────────────────
4950
# Canonical ownership column — mirrors the pattern introduced for
50-
# assistants.owner_id. Nullable during the back-fill window; tighten
51+
# assistants.owner_id. Nullable during the back-fill window; tighten
5152
# to NOT NULL once every existing thread row has been back-filled.
5253
add_column_if_missing(
5354
"threads",
@@ -56,14 +57,25 @@ def upgrade() -> None:
5657
sa.String(length=64),
5758
sa.ForeignKey("users.id", ondelete="SET NULL"),
5859
nullable=True,
59-
index=True,
60+
# index=True is intentionally omitted: it is a metadata-level hint
61+
# only honoured by create_all(), not by Alembic batch migrations.
62+
# The index is created explicitly below.
6063
comment="Canonical creator/owner of this thread. Used for row-level access control.",
6164
),
6265
)
6366

67+
# Create the index explicitly so it is reliably present on all environments,
68+
# including fresh containers where the table did not previously exist.
69+
create_index_if_missing(
70+
"ix_threads_owner_id",
71+
"threads",
72+
["owner_id"],
73+
)
74+
6475

6576
def downgrade() -> None:
6677
# ── threads.owner_id ────────────────────────────────────────────────────
78+
drop_index_if_exists("ix_threads_owner_id", "threads")
6779
drop_column_if_exists("threads", "owner_id")
6880

6981
# ── messages.reasoning ──────────────────────────────────────────────────

migrations/versions/1e55188b6b26_add_owner_id_to_assistants.py

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,14 @@
3939
def _constraint_exists(constraint_name: str, table_name: str) -> bool:
4040
"""
4141
Return True if a named constraint already exists on the given table.
42+
Returns False immediately if the table itself does not exist — prevents
43+
op.drop_constraint / op.create_unique_constraint from being called on a
44+
missing table in fresh-container scenarios.
4245
Used for unique constraints, which safe_ddl does not yet cover.
4346
"""
47+
if not has_table(table_name):
48+
return False
49+
4450
bind = op.get_bind()
4551
result = bind.execute(
4652
sa.text(
@@ -92,6 +98,7 @@ def upgrade() -> None:
9298
)
9399

94100
# ── batfish_snapshots: comment-only column updates ──────────────────────
101+
# All safe_alter_column calls check has_table internally — safe on fresh containers.
95102
safe_alter_column(
96103
"batfish_snapshots",
97104
"id",
@@ -150,17 +157,22 @@ def upgrade() -> None:
150157
)
151158

152159
# ── batfish_snapshots: rename unique constraint ─────────────────────────
153-
# Unique constraints are not yet covered by safe_ddl — using local
154-
# _constraint_exists guard for these operations only
160+
# _constraint_exists now returns False when the table is absent, so both
161+
# branches below are safe on a fresh container (neither op will fire).
155162
if _constraint_exists("uq_batfish_user_snapshot", "batfish_snapshots"):
156163
op.drop_constraint("uq_batfish_user_snapshot", "batfish_snapshots", type_="unique")
157164

158165
if not _constraint_exists("uq_batfish_user_snapshot_name", "batfish_snapshots"):
159-
op.create_unique_constraint(
160-
"uq_batfish_user_snapshot_name",
161-
"batfish_snapshots",
162-
["user_id", "snapshot_name"],
163-
)
166+
# Only attempt to create if the table actually exists; the has_table
167+
# check inside _constraint_exists already gates this when the table is
168+
# missing (it returns False, making `not False` = True, which would
169+
# incorrectly enter this branch on a missing table).
170+
if has_table("batfish_snapshots"):
171+
op.create_unique_constraint(
172+
"uq_batfish_user_snapshot_name",
173+
"batfish_snapshots",
174+
["user_id", "snapshot_name"],
175+
)
164176

165177
# ── messages ────────────────────────────────────────────────────────────
166178
safe_alter_column(
@@ -207,12 +219,13 @@ def downgrade() -> None:
207219
op.drop_constraint("uq_batfish_user_snapshot_name", "batfish_snapshots", type_="unique")
208220

209221
if not _constraint_exists("uq_batfish_user_snapshot", "batfish_snapshots"):
210-
op.create_index(
211-
"uq_batfish_user_snapshot",
212-
"batfish_snapshots",
213-
["user_id", "snapshot_name"],
214-
unique=True,
215-
)
222+
if has_table("batfish_snapshots"):
223+
op.create_index(
224+
"uq_batfish_user_snapshot",
225+
"batfish_snapshots",
226+
["user_id", "snapshot_name"],
227+
unique=True,
228+
)
216229

217230
# ── batfish_snapshots: restore original comments ────────────────────────
218231
safe_alter_column(
@@ -273,7 +286,6 @@ def downgrade() -> None:
273286
)
274287

275288
# ── assistants: owner_id ────────────────────────────────────────────────
276-
# Safe FK + index drop via helpers, then column removal
277289
drop_fk_if_exists("assistants", "fk_assistants_owner_id_users")
278290
drop_index_if_exists("ix_assistants_owner_id", "assistants")
279291
drop_column_if_exists("assistants", "owner_id")

migrations/versions/6413efed1b18_add_decision_telemetry_to_assistants_.py

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
# Import the safe DDL helpers
1616
from migrations.utils.safe_ddl import (add_column_if_missing,
17-
drop_column_if_exists,
17+
drop_column_if_exists, has_table,
1818
safe_alter_column)
1919

2020
# revision identifiers, used by Alembic.
@@ -26,8 +26,6 @@
2626

2727
def upgrade() -> None:
2828
"""Upgrade schema safely."""
29-
bind = op.get_bind()
30-
insp = sa.inspect(bind)
3129

3230
# --- Table: assistants ---
3331

@@ -44,11 +42,13 @@ def upgrade() -> None:
4442
)
4543

4644
# 2. DATA SANITIZATION (Critical for Type Conversion)
47-
# Guard against clean database runs where the table doesn't exist yet
48-
if insp.has_table("assistants"):
49-
# We must convert strings like 'standard' to '0' so MySQL can cast to Boolean/TINYINT.
45+
# Guard against fresh-container runs where the table doesn't exist yet.
46+
# Uses safe_ddl.has_table() — consistent with the rest of the codebase and
47+
# avoids the deprecated sa.inspect(bind) pattern.
48+
if has_table("assistants"):
49+
# Convert strings like 'standard' → '0' so MySQL can cast to TINYINT/Boolean.
5050
op.execute("UPDATE assistants SET agent_mode = '0' WHERE agent_mode = 'standard'")
51-
# Catch-all for any other strings to ensure they are truthy (1)
51+
# Catch-all: any remaining non-'0' string is treated as truthy (1).
5252
op.execute("UPDATE assistants SET agent_mode = '1' WHERE agent_mode != '0'")
5353

5454
# 3. Convert agent_mode from String/Enum to Boolean
@@ -64,8 +64,6 @@ def upgrade() -> None:
6464

6565
def downgrade() -> None:
6666
"""Downgrade schema safely."""
67-
bind = op.get_bind()
68-
insp = sa.inspect(bind)
6967

7068
# --- Table: assistants ---
7169

@@ -78,9 +76,9 @@ def downgrade() -> None:
7876
nullable=True,
7977
)
8078

81-
# 2. DATA REVERSION (Optional: Restore the 'standard' string)
82-
# Guard against clean database runs where the table doesn't exist
83-
if insp.has_table("assistants"):
79+
# 2. DATA REVERSION — restore the 'standard' string for any '0' rows.
80+
# Guard against fresh-container runs where the table doesn't exist yet.
81+
if has_table("assistants"):
8482
op.execute("UPDATE assistants SET agent_mode = 'standard' WHERE agent_mode = '0'")
8583

8684
# 3. Drop the decision_telemetry column

migrations/versions/741d86dd5ac8_add_decision_telemetry_to_actions.py

Lines changed: 12 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@
1111
import sqlalchemy as sa
1212
from alembic import op
1313
from sqlalchemy.dialects import mysql
14-
from sqlalchemy.engine.reflection import Inspector
1514

1615
# Import the safe DDL helpers
1716
from migrations.utils.safe_ddl import (add_column_if_missing,
17+
create_index_if_missing,
1818
drop_column_if_exists,
19-
safe_alter_column)
19+
drop_index_if_exists, safe_alter_column)
2020

2121
# revision identifiers, used by Alembic.
2222
revision: str = "741d86dd5ac8"
@@ -51,23 +51,12 @@ def upgrade() -> None:
5151
),
5252
)
5353

54-
# 3. Create Index safely (Check if exists first)
55-
# Note: add_column_if_missing handles the column, but we need to handle the index explicitly
56-
bind = op.get_bind()
57-
inspector = Inspector.from_engine(bind)
58-
indexes = inspector.get_indexes("actions")
59-
index_names = [idx["name"] for idx in indexes]
60-
61-
if "ix_actions_confidence_score" not in index_names:
62-
op.create_index(
63-
op.f("ix_actions_confidence_score"),
64-
"actions",
65-
["confidence_score"],
66-
unique=False,
67-
)
68-
print("[Alembic-safeDDL] ✅ Created index: ix_actions_confidence_score")
69-
else:
70-
print("[Alembic-safeDDL] ⚠️ Skipped – index already exists: ix_actions_confidence_score")
54+
# 3. Create index safely via helper (guards against missing table)
55+
create_index_if_missing(
56+
"ix_actions_confidence_score",
57+
"actions",
58+
["confidence_score"],
59+
)
7160

7261
# --- Table: messages ---
7362
# 4. Enforce non-nullable content (Maintenance from auto-gen)
@@ -100,14 +89,9 @@ def downgrade() -> None:
10089
safe_alter_column("messages", "content", existing_type=mysql.TEXT(), nullable=True)
10190

10291
# --- Table: actions ---
103-
# 1. Drop Index safely
104-
# Note: drop_column_if_exists usually drops associated indexes in MySQL/PG,
105-
# but explicitly dropping it prevents warnings in some drivers.
106-
try:
107-
op.drop_index(op.f("ix_actions_confidence_score"), table_name="actions")
108-
except Exception:
109-
pass # Index likely missing or dropped by column drop below
110-
111-
# 2. Drop Columns
92+
# 1. Drop index safely via helper (guards against missing table/index)
93+
drop_index_if_exists("ix_actions_confidence_score", "actions")
94+
95+
# 2. Drop columns
11296
drop_column_if_exists("actions", "confidence_score")
11397
drop_column_if_exists("actions", "decision_payload")

migrations/versions/ba35b4620058_add_soft_delete_to_vectorstore.py

Lines changed: 11 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@
1212
from alembic import op
1313

1414
from migrations.utils.safe_ddl import (add_column_if_missing,
15-
drop_column_if_exists)
15+
create_index_if_missing,
16+
drop_column_if_exists,
17+
drop_index_if_exists)
1618

1719
# revision identifiers, used by Alembic.
18-
revision: str = 'ba35b4620058'
19-
down_revision: Union[str, None] = '3e16915ae60f'
20+
revision: str = "ba35b4620058"
21+
down_revision: Union[str, None] = "3e16915ae60f"
2022
branch_labels: Union[str, Sequence[str], None] = None
2123
depends_on: Union[str, Sequence[str], None] = None
2224

@@ -32,27 +34,13 @@ def upgrade() -> None:
3234
),
3335
)
3436

35-
bind = op.get_bind()
36-
insp = sa.inspect(bind)
37-
38-
if insp.has_table("vector_stores"):
39-
indexes = [idx["name"] for idx in insp.get_indexes("vector_stores")]
40-
if "ix_vector_stores_deleted_at" not in indexes:
41-
op.create_index(
42-
op.f("ix_vector_stores_deleted_at"),
43-
"vector_stores",
44-
["deleted_at"],
45-
unique=False,
46-
)
37+
create_index_if_missing(
38+
"ix_vector_stores_deleted_at",
39+
"vector_stores",
40+
["deleted_at"],
41+
)
4742

4843

4944
def downgrade() -> None:
50-
bind = op.get_bind()
51-
insp = sa.inspect(bind)
52-
53-
if insp.has_table("vector_stores"):
54-
indexes = [idx["name"] for idx in insp.get_indexes("vector_stores")]
55-
if "ix_vector_stores_deleted_at" in indexes:
56-
op.drop_index(op.f("ix_vector_stores_deleted_at"), table_name="vector_stores")
57-
45+
drop_index_if_exists("ix_vector_stores_deleted_at", "vector_stores")
5846
drop_column_if_exists("vector_stores", "deleted_at")

migrations/versions/dc84d53c3c46_restore_missing_columns.py

Lines changed: 13 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -5,61 +5,15 @@
55
Create Date: 2025-08-20 11:46:00.251355
66
"""
77

8-
from typing import Optional, Sequence, Union
8+
from typing import Sequence, Union
99

1010
import sqlalchemy as sa
1111
from alembic import op
1212

13-
# --- Try to import your DDL safety helpers; fall back to local shims ---
14-
try:
15-
from migrations.utils.safe_ddl import (add_column_if_missing,
16-
column_exists,
17-
drop_column_if_exists,
18-
rename_column_if_exists,
19-
safe_alter_column)
20-
except Exception:
21-
22-
def _insp():
23-
bind = op.get_bind()
24-
return sa.inspect(bind)
25-
26-
def column_exists(table: str, col: str) -> bool:
27-
insp = _insp()
28-
# GUARD: Check if table exists before inspecting columns
29-
if not insp.has_table(table):
30-
return False
31-
return col in {c["name"] for c in insp.get_columns(table)}
32-
33-
def add_column_if_missing(table: str, column: sa.Column) -> None:
34-
if not column_exists(table, column.name):
35-
op.add_column(table, column)
36-
37-
def drop_column_if_exists(table: str, col_name: str) -> None:
38-
if column_exists(table, col_name):
39-
op.drop_column(table, col_name)
40-
41-
def safe_alter_column(table: str, col_name: str, **kw) -> None:
42-
insp = _insp()
43-
# GUARD: Check if table and column exist before altering
44-
if insp.has_table(table) and column_exists(table, col_name):
45-
op.alter_column(table, col_name, **kw)
46-
47-
def rename_column_if_exists(
48-
table: str,
49-
old_name: str,
50-
new_name: str,
51-
existing_type: Optional[sa.types.TypeEngine] = None,
52-
existing_nullable: Optional[bool] = None,
53-
) -> None:
54-
if column_exists(table, old_name) and not column_exists(table, new_name):
55-
op.alter_column(
56-
table,
57-
old_name,
58-
new_column_name=new_name,
59-
existing_type=existing_type,
60-
existing_nullable=existing_nullable,
61-
)
62-
13+
from migrations.utils.safe_ddl import (add_column_if_missing, column_exists,
14+
drop_column_if_exists, has_table,
15+
rename_column_if_exists,
16+
safe_alter_column)
6317

6418
# Alembic identifiers
6519
revision: str = "dc84d53c3c46"
@@ -72,7 +26,11 @@ def _bigint_to_int_swap(table: str, base_col: str) -> None:
7226
"""
7327
Safely convert BIGINT column to INT using temp column + backfill + swap.
7428
NOTE: This can truncate values > INT range; ensure data is within bounds.
29+
Exits early if the table doesn't exist — safe on fresh containers.
7530
"""
31+
if not has_table(table):
32+
return
33+
7634
temp = f"{base_col}_i"
7735

7836
# 1) Add temp INT column if missing
@@ -98,7 +56,11 @@ def _bigint_to_int_swap(table: str, base_col: str) -> None:
9856
def _int_to_bigint_swap(table: str, base_col: str) -> None:
9957
"""
10058
Downgrade path: convert INT back to BIGINT via temp column + backfill + swap.
59+
Exits early if the table doesn't exist — safe on fresh containers.
10160
"""
61+
if not has_table(table):
62+
return
63+
10264
temp = f"{base_col}_bi"
10365

10466
# 1) Add temp BIGINT column if missing

0 commit comments

Comments
 (0)