From 3d37186fa72007390af08013cf559ae9c5c066da Mon Sep 17 00:00:00 2001 From: Justin Ko Date: Fri, 3 Apr 2026 08:48:01 +0000 Subject: [PATCH] fix: harden strategy set migration upgrade --- docs/db-schema-spec.md | 2 +- .../20260403_0002_strategy_set_columns.py | 33 +++++++++++++++++-- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/docs/db-schema-spec.md b/docs/db-schema-spec.md index 771081b..ba2e21b 100644 --- a/docs/db-schema-spec.md +++ b/docs/db-schema-spec.md @@ -168,5 +168,5 @@ When schema changes: | Revision ID | File | Summary | |---|---|---| | 20260403_0001 | migrations/versions/20260403_0001_initial_portfolio_schema.py | Initial schema for strategy sets, portfolios, ledger, indexes, optional hypertable | -| 20260403_0002 | migrations/versions/20260403_0002_strategy_set_columns.py | Rename `strategy_name`->`name`, `strategy_config`->`config`, add unique `nick` | +| 20260403_0002 | migrations/versions/20260403_0002_strategy_set_columns.py | Rename `strategy_name`->`name`, `strategy_config`->`config`, add unique `nick`, handle legacy unnamed unique constraint | | 20260403_0003 | migrations/versions/20260403_0003_portfolio_strategy_refs.py | Remove embedded portfolio strategy fields, add `active_strategy_id` and `avabile_strategies` | diff --git a/migrations/versions/20260403_0002_strategy_set_columns.py b/migrations/versions/20260403_0002_strategy_set_columns.py index bb109bf..a26d537 100644 --- a/migrations/versions/20260403_0002_strategy_set_columns.py +++ b/migrations/versions/20260403_0002_strategy_set_columns.py @@ -17,6 +17,33 @@ branch_labels = None depends_on = None +def _drop_unique_constraint_for_columns(table_name: str, columns: tuple[str, ...]) -> None: + bind = op.get_bind() + rows = bind.execute( + sa.text( + """ + SELECT c.conname, + array_agg(a.attname ORDER BY u.ord) AS column_names + FROM pg_constraint c + JOIN pg_class t ON t.oid = c.conrelid + JOIN pg_namespace n ON n.oid = t.relnamespace + JOIN unnest(c.conkey) WITH ORDINALITY AS u(attnum, ord) ON TRUE + JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = u.attnum + WHERE c.contype = 'u' + AND n.nspname = current_schema() + AND t.relname = :table_name + GROUP BY c.conname + """ + ), + {"table_name": table_name}, + ) + expected = list(columns) + for row in rows: + if list(row.column_names) == expected: + op.drop_constraint(row.conname, table_name, type_="unique") + return + + def upgrade() -> None: op.add_column("strategy_sets", sa.Column("name", sa.Text(), nullable=True)) op.add_column("strategy_sets", sa.Column("config", sa.Text(), nullable=True, server_default="")) @@ -38,7 +65,7 @@ def upgrade() -> None: op.alter_column("strategy_sets", "config", nullable=False) op.alter_column("strategy_sets", "nick", nullable=False) - op.drop_constraint("uq_strategy_sets_name_config", "strategy_sets", type_="unique") + _drop_unique_constraint_for_columns("strategy_sets", ("strategy_name", "strategy_config")) op.create_unique_constraint("uq_strategy_sets_name_config", "strategy_sets", ["name", "config"]) op.create_unique_constraint("uq_strategy_sets_nick", "strategy_sets", ["nick"]) @@ -64,8 +91,8 @@ def downgrade() -> None: op.alter_column("strategy_sets", "strategy_name", nullable=False) op.alter_column("strategy_sets", "strategy_config", nullable=False) - op.drop_constraint("uq_strategy_sets_nick", "strategy_sets", type_="unique") - op.drop_constraint("uq_strategy_sets_name_config", "strategy_sets", type_="unique") + _drop_unique_constraint_for_columns("strategy_sets", ("nick",)) + _drop_unique_constraint_for_columns("strategy_sets", ("name", "config")) op.create_unique_constraint("uq_strategy_sets_name_config", "strategy_sets", ["strategy_name", "strategy_config"]) op.drop_column("strategy_sets", "nick")