petter2025 commited on
Commit
4531dfd
·
verified ·
1 Parent(s): 7a5b697

Upload folder using huggingface_hub

Browse files
alembic/env.py CHANGED
@@ -10,8 +10,35 @@ sys.path.append(str(Path(__file__).parent.parent))
10
 
11
  from app.database.base import Base
12
  from app.database import models_intents
 
13
  from app.core.config import settings
14
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  config = context.config
16
  # fileConfig defaults to disable_existing_loggers=True, which silences every
17
  # logger that already exists -- including the application's own. That is
@@ -36,6 +63,7 @@ def run_migrations_offline() -> None:
36
  target_metadata=target_metadata,
37
  literal_binds=True,
38
  dialect_opts={"paramstyle": "named"},
 
39
  )
40
 
41
  with context.begin_transaction():
@@ -52,7 +80,9 @@ def run_migrations_online() -> None:
52
 
53
  with connectable.connect() as connection:
54
  context.configure(
55
- connection=connection, target_metadata=target_metadata
 
 
56
  )
57
 
58
  with context.begin_transaction():
 
10
 
11
  from app.database.base import Base
12
  from app.database import models_intents
13
+ from app.database import models_onchain
14
  from app.core.config import settings
15
 
16
+ # Tables that are deliberately not ORM-mapped. UsageTracker and arf-gateway
17
+ # create and query these with raw SQL -- the Go service needs them without
18
+ # going through Python at all -- so they have no entry in Base.metadata.
19
+ # Autogenerate compares the database against that metadata, so without this
20
+ # it sees them as tables to DROP.
21
+ #
22
+ # models_onchain is imported above for the same reason: it was missing, and
23
+ # `alembic check` duly proposed dropping onchain_rationales.
24
+
25
+ UNMANAGED_TABLES = {
26
+ "api_keys",
27
+ "monthly_counts",
28
+ "usage_log",
29
+ "idempotency_keys",
30
+ }
31
+
32
+
33
+ def include_object(object_, name, type_, reflected, compare_to):
34
+ """Keep unmanaged tables out of autogenerate/check comparisons."""
35
+ if type_ == "table" and name in UNMANAGED_TABLES:
36
+ return False
37
+ if type_ == "index" and getattr(object_, "table", None) is not None:
38
+ return object_.table.name not in UNMANAGED_TABLES
39
+ return True
40
+
41
+
42
  config = context.config
43
  # fileConfig defaults to disable_existing_loggers=True, which silences every
44
  # logger that already exists -- including the application's own. That is
 
63
  target_metadata=target_metadata,
64
  literal_binds=True,
65
  dialect_opts={"paramstyle": "named"},
66
+ include_object=include_object,
67
  )
68
 
69
  with context.begin_transaction():
 
80
 
81
  with connectable.connect() as connection:
82
  context.configure(
83
+ connection=connection,
84
+ target_metadata=target_metadata,
85
+ include_object=include_object,
86
  )
87
 
88
  with context.begin_transaction():
alembic/versions/f1a2b3c4d5e6_add_tenant_id_to_intents_and_beta_state.py ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """add tenant_id to intents and beta_state, and fix the beta_state unique index
2
+
3
+ The models have declared `tenant_id` on both tables since multi-tenancy
4
+ landed, but no migration ever created the column. Nothing caught it: the
5
+ test suite builds its schema with `create_all` from the models, so tests
6
+ see a column production does not have, and the migration check only
7
+ asserted that `alembic upgrade head` *succeeds* -- never that the schema it
8
+ produces matches the models.
9
+
10
+ Production surfaced it the moment `intents` first existed (arf-api-004 had
11
+ been rolling the whole chain back, hiding this behind a missing table):
12
+
13
+ psycopg2.errors.UndefinedColumn:
14
+ column intents.tenant_id does not exist
15
+
16
+ Also corrects `ix_beta_state_category`, which d36deffe7fa2 created as
17
+ UNIQUE on `category` alone. That is wrong for a multi-tenant table -- it
18
+ lets exactly one tenant hold any given category, and the second tenant to
19
+ write one gets an integrity error. The model's intent is the composite
20
+ `uq_beta_state_tenant_category` on (tenant_id, category).
21
+
22
+ Both columns are declared NOT NULL in the models. This adds them nullable,
23
+ then tightens to NOT NULL only when no NULL rows remain, so the migration
24
+ cannot fail on a database that already holds rows -- a failure here would
25
+ abort the whole chain in a single transaction, which is exactly the
26
+ arf-api-004 outage. A database left with NULLs keeps the column nullable
27
+ and says so in the deploy log rather than taking the service down.
28
+
29
+ Revision ID: f1a2b3c4d5e6
30
+ Revises: 0c89c9727bc0
31
+ Create Date: 2026-09-14 14:30:00.000000
32
+
33
+ """
34
+ from typing import Sequence, Union
35
+
36
+ from alembic import op
37
+ import sqlalchemy as sa
38
+
39
+
40
+ # revision identifiers, used by Alembic.
41
+ revision: str = 'f1a2b3c4d5e6'
42
+ down_revision: Union[str, Sequence[str], None] = '0c89c9727bc0'
43
+ branch_labels: Union[str, Sequence[str], None] = None
44
+ depends_on: Union[str, Sequence[str], None] = None
45
+
46
+
47
+ def _add_tenant_id(table: str, index_name: str, fk_name: str) -> None:
48
+ bind = op.get_bind()
49
+ inspector = sa.inspect(bind)
50
+
51
+ columns = {c['name'] for c in inspector.get_columns(table)}
52
+ if 'tenant_id' not in columns:
53
+ op.add_column(table, sa.Column('tenant_id', sa.String(length=64), nullable=True))
54
+
55
+ indexes = {ix['name'] for ix in inspector.get_indexes(table)}
56
+ if index_name not in indexes:
57
+ op.create_index(index_name, table, ['tenant_id'], unique=False)
58
+
59
+ fks = {fk.get('name') for fk in inspector.get_foreign_keys(table)}
60
+ if fk_name not in fks:
61
+ op.create_foreign_key(fk_name, table, 'tenants', ['tenant_id'], ['id'], ondelete='CASCADE')
62
+
63
+ # Tighten to NOT NULL only if nothing would violate it. On an empty or
64
+ # fully-populated table this reaches the models' declared schema; on a
65
+ # table with legacy NULLs it stays nullable instead of aborting the
66
+ # entire migration transaction.
67
+ orphans = bind.execute(
68
+ sa.text(f"SELECT COUNT(*) FROM {table} WHERE tenant_id IS NULL") # noqa: S608
69
+ ).scalar()
70
+ if orphans:
71
+ print(
72
+ f"[{revision}] {table}: {orphans} row(s) have a NULL tenant_id; "
73
+ f"leaving the column nullable. Backfill them and then run "
74
+ f"ALTER TABLE {table} ALTER COLUMN tenant_id SET NOT NULL."
75
+ )
76
+ return
77
+ op.alter_column(table, 'tenant_id', existing_type=sa.String(length=64), nullable=False)
78
+
79
+
80
+ def upgrade() -> None:
81
+ """Upgrade schema."""
82
+ _add_tenant_id('intents', 'ix_intents_tenant_id', 'fk_intents_tenant_id')
83
+ _add_tenant_id('beta_state', 'ix_beta_state_tenant_id', 'fk_beta_state_tenant_id')
84
+
85
+ bind = op.get_bind()
86
+ inspector = sa.inspect(bind)
87
+
88
+ # d36deffe7fa2 made this UNIQUE on category alone, which is wrong once
89
+ # the table is partitioned by tenant. Recreate it non-unique and put the
90
+ # uniqueness on (tenant_id, category), as the model declares.
91
+ beta_indexes = {ix['name']: ix for ix in inspector.get_indexes('beta_state')}
92
+ category_index = beta_indexes.get('ix_beta_state_category')
93
+ if category_index is not None and category_index.get('unique'):
94
+ op.drop_index('ix_beta_state_category', table_name='beta_state')
95
+ op.create_index('ix_beta_state_category', 'beta_state', ['category'], unique=False)
96
+
97
+ constraints = {uc['name'] for uc in inspector.get_unique_constraints('beta_state')}
98
+ constraints |= set(beta_indexes)
99
+ if 'uq_beta_state_tenant_category' not in constraints:
100
+ op.create_unique_constraint(
101
+ 'uq_beta_state_tenant_category', 'beta_state', ['tenant_id', 'category']
102
+ )
103
+
104
+
105
+ def downgrade() -> None:
106
+ """Downgrade schema."""
107
+ bind = op.get_bind()
108
+ inspector = sa.inspect(bind)
109
+
110
+ if 'uq_beta_state_tenant_category' in {
111
+ uc['name'] for uc in inspector.get_unique_constraints('beta_state')
112
+ }:
113
+ op.drop_constraint('uq_beta_state_tenant_category', 'beta_state', type_='unique')
114
+
115
+ for table, index_name, fk_name in (
116
+ ('beta_state', 'ix_beta_state_tenant_id', 'fk_beta_state_tenant_id'),
117
+ ('intents', 'ix_intents_tenant_id', 'fk_intents_tenant_id'),
118
+ ):
119
+ insp = sa.inspect(bind)
120
+ if fk_name in {fk.get('name') for fk in insp.get_foreign_keys(table)}:
121
+ op.drop_constraint(fk_name, table, type_='foreignkey')
122
+ if index_name in {ix['name'] for ix in insp.get_indexes(table)}:
123
+ op.drop_index(index_name, table_name=table)
124
+ if 'tenant_id' in {c['name'] for c in insp.get_columns(table)}:
125
+ op.drop_column(table, 'tenant_id')