From f3b73419ac1e2092f65e6e9badec898cfce53509 Mon Sep 17 00:00:00 2001 From: Sanghun Lee Date: Fri, 27 Mar 2026 11:03:34 +0900 Subject: [PATCH 1/5] docs(BA-5414): document Alembic migration backport strategy Add ALEMBIC.md with the full backport procedure, idempotent writing rules, and real-world examples so developers can safely backport migrations to release branches. Co-Authored-By: Claude Opus 4.6 --- ALEMBIC.md | 134 ++++++++++++++++++ CLAUDE.md | 6 + .../account_manager/models/alembic/README | 4 +- .../coordinator/models/alembic/README | 4 +- src/ai/backend/manager/models/alembic/README | 4 +- 5 files changed, 149 insertions(+), 3 deletions(-) create mode 100644 ALEMBIC.md diff --git a/ALEMBIC.md b/ALEMBIC.md new file mode 100644 index 00000000000..6d8653c8b12 --- /dev/null +++ b/ALEMBIC.md @@ -0,0 +1,134 @@ +# Alembic Migration Backport Strategy + +This document describes how to safely backport Alembic migrations to release branches +while keeping the main branch consistent. + +## Principles + +1. **Fixes only** -- Backport migrations must contain schema fixes only (e.g., missing + columns, incorrect types, enum coexistence issues). Feature-level schema changes are + never backported. +2. **Idempotent** -- Both the backport migration and its duplicate on main must be + idempotent so they are safe to re-apply on databases that may already have the change. +3. **No manual revision editing** -- Never modify the `revision` or `down_revision` of + an existing migration. Always create new migration files. + +## Applicable Components + +| Component | Alembic directory | +|---|---| +| Manager | `src/ai/backend/manager/models/alembic/` | +| Account Manager | `src/ai/backend/account_manager/models/alembic/` | +| App Proxy Coordinator | `src/ai/backend/appproxy/coordinator/models/alembic/` | + +## Backport Procedure + +Given the following migration chain on **main**: + +``` +a --> b --> c (a = backport target head, c = main head) +``` + +### Step 1: Create migration `d` on the release branch + +On the release branch, create a new migration whose `down_revision` is `a` (the +release branch head at the point you are targeting): + +``` +a --> d (release branch result) +``` + +### Step 2: Insert `d` into main and add duplicate `d'` + +On **main**, the same migration `d` is inserted between `a` and `b`, and a duplicate +`d'` is appended on top of the current main head `c`: + +``` +a --> d --> b --> c --> d' +``` + +- `d` — Set `down_revision = a` and `revision` to the same value used on the release + branch. Update `b`'s `down_revision` to point to `d`. +- `d'` — A new migration file with a fresh revision ID, `down_revision = c`, that + performs the **same schema change** as `d` but written idempotently so it is a no-op + on databases that already applied `d`. + +### Step 3: Merge the head if necessary + +After inserting `d` into the chain, run `alembic heads` on main. If there are multiple +heads, create a merge migration: + +```bash +alembic merge heads -m "merge backport head" +``` + +## Idempotent Writing Rules + +Every backport migration (both `d` and `d'`) **must** be idempotent. Use the following +patterns: + +### DDL guards + +```python +# Creating a table +conn = op.get_bind() +inspector = sa.inspect(conn) +if "my_table" not in inspector.get_table_names(): + op.create_table("my_table", ...) + +# Adding a column +columns = [c["name"] for c in inspector.get_columns("my_table")] +if "new_col" not in columns: + op.add_column("my_table", sa.Column("new_col", sa.String)) + +# Creating an index +indexes = [idx["name"] for idx in inspector.get_indexes("my_table")] +if "ix_my_table_col" not in indexes: + op.create_index("ix_my_table_col", "my_table", ["col"]) +``` + +### Enum type guards + +```python +conn = op.get_bind() +result = conn.exec_driver_sql( + "SELECT 1 FROM pg_type WHERE typname = 'myenum'" +) +if result.fetchone() is None: + # Create the enum type + my_enum = sa.Enum("A", "B", name="myenum") + my_enum.create(conn) +``` + +### Raw SQL guards + +```sql +-- Column +ALTER TABLE my_table ADD COLUMN IF NOT EXISTS new_col TEXT; + +-- Index +CREATE INDEX IF NOT EXISTS ix_my_table_col ON my_table (col); + +-- Dropping +DROP INDEX IF EXISTS ix_my_table_col; +ALTER TABLE my_table DROP COLUMN IF EXISTS old_col; +``` + +### Downgrade + +Downgrade functions follow the same idempotent rules. If the downgrade is handled by +another migration in the chain, use `pass`: + +```python +def downgrade() -> None: + pass +``` + +## Real-World Example + +See the following migrations in the codebase for reference: + +- `manager/models/alembic/versions/1cc9b47e0a8e_fix_sessionresult_enum_type_coexistence_backport.py` + -- Checks multiple possible enum states and fixes whichever scenario it finds. +- `manager/models/alembic/versions/c4ea15b77136_ensure_auditlogs_table_exist.py` + -- Uses `inspector.get_table_names()` to skip table creation when it already exists. diff --git a/CLAUDE.md b/CLAUDE.md index 6f8253f8b19..199a936f973 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,6 +7,7 @@ This file contains core rules for AI coding agents. For detailed patterns and wo **Core Documents (Read directly):** - `tests/CLAUDE.md` - Testing guidelines and strategies - `BUILDING.md` - Build system, quality enforcement, BUILD policies +- `ALEMBIC.md` - Alembic migration backport strategy - `README.md` - Project overview and architecture - `proposals/README.md` - BEP (Backend.AI Enhancement Proposals) @@ -49,6 +50,11 @@ pants test --changed-since=HEAD~1 **Fix all lint, type, and test errors — never suppress or skip.** +## Alembic Migration Backport + +When backporting migrations to release branches, both the backport and main branch +migrations must be idempotent. See `ALEMBIC.md` for the full strategy and examples. + ## Layer Architecture API Handler → Processor → Service → Repository → DB diff --git a/src/ai/backend/account_manager/models/alembic/README b/src/ai/backend/account_manager/models/alembic/README index e0d0858f266..1ab37f6270f 100644 --- a/src/ai/backend/account_manager/models/alembic/README +++ b/src/ai/backend/account_manager/models/alembic/README @@ -1 +1,3 @@ -Generic single-database configuration with an async dbapi. \ No newline at end of file +Generic single-database configuration with an async dbapi. + +For the migration backport strategy, see `ALEMBIC.md` at the repository root. \ No newline at end of file diff --git a/src/ai/backend/appproxy/coordinator/models/alembic/README b/src/ai/backend/appproxy/coordinator/models/alembic/README index 98e4f9c44ef..de2cb6d5fd8 100644 --- a/src/ai/backend/appproxy/coordinator/models/alembic/README +++ b/src/ai/backend/appproxy/coordinator/models/alembic/README @@ -1 +1,3 @@ -Generic single-database configuration. \ No newline at end of file +Generic single-database configuration. + +For the migration backport strategy, see `ALEMBIC.md` at the repository root. \ No newline at end of file diff --git a/src/ai/backend/manager/models/alembic/README b/src/ai/backend/manager/models/alembic/README index 98e4f9c44ef..de2cb6d5fd8 100644 --- a/src/ai/backend/manager/models/alembic/README +++ b/src/ai/backend/manager/models/alembic/README @@ -1 +1,3 @@ -Generic single-database configuration. \ No newline at end of file +Generic single-database configuration. + +For the migration backport strategy, see `ALEMBIC.md` at the repository root. \ No newline at end of file From 99df5f00b37686a08b1818b8f4101f194e03a5d7 Mon Sep 17 00:00:00 2001 From: Sanghun Lee Date: Fri, 27 Mar 2026 12:53:55 +0900 Subject: [PATCH 2/5] docs(BA-5414): add changelog entry for Alembic backport strategy doc Co-Authored-By: Claude Opus 4.6 --- changes/10607.doc.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/10607.doc.md diff --git a/changes/10607.doc.md b/changes/10607.doc.md new file mode 100644 index 00000000000..d729b1db78f --- /dev/null +++ b/changes/10607.doc.md @@ -0,0 +1 @@ +Add `ALEMBIC.md` documenting the Alembic migration backport strategy with idempotent writing rules and step-by-step procedure. From c52d0c47e3da514f4f7db03cd2698063bfa50b95 Mon Sep 17 00:00:00 2001 From: Sanghun Lee Date: Fri, 27 Mar 2026 12:55:16 +0900 Subject: [PATCH 3/5] docs(BA-5414): address Copilot review feedback on ALEMBIC.md - Clarify Principle 3: editing down_revision is allowed only when inserting a backport into the main chain before merge - Use full repo paths in Real-World Example references Co-Authored-By: Claude Opus 4.6 --- ALEMBIC.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ALEMBIC.md b/ALEMBIC.md index 6d8653c8b12..2db94a8de38 100644 --- a/ALEMBIC.md +++ b/ALEMBIC.md @@ -11,7 +11,9 @@ while keeping the main branch consistent. 2. **Idempotent** -- Both the backport migration and its duplicate on main must be idempotent so they are safe to re-apply on databases that may already have the change. 3. **No manual revision editing** -- Never modify the `revision` or `down_revision` of - an existing migration. Always create new migration files. + an existing migration that has already been released. Editing `down_revision` is + allowed only when inserting a backport migration into the main branch chain before + the change is merged (see Step 2 below). ## Applicable Components @@ -128,7 +130,7 @@ def downgrade() -> None: See the following migrations in the codebase for reference: -- `manager/models/alembic/versions/1cc9b47e0a8e_fix_sessionresult_enum_type_coexistence_backport.py` +- `src/ai/backend/manager/models/alembic/versions/1cc9b47e0a8e_fix_sessionresult_enum_type_coexistence_backport.py` -- Checks multiple possible enum states and fixes whichever scenario it finds. -- `manager/models/alembic/versions/c4ea15b77136_ensure_auditlogs_table_exist.py` +- `src/ai/backend/manager/models/alembic/versions/c4ea15b77136_ensure_auditlogs_table_exist.py` -- Uses `inspector.get_table_names()` to skip table creation when it already exists. From a6338c166f9bfcb32c518c1514d581f7815e988e Mon Sep 17 00:00:00 2001 From: Sanghun Lee Date: Fri, 27 Mar 2026 13:12:13 +0900 Subject: [PATCH 4/5] docs(BA-5414): add release version comment rule to ALEMBIC.md Every migration file must include a "# Part of: " comment next to the revision identifiers to trace which release introduced the schema change. Co-Authored-By: Claude Opus 4.6 --- ALEMBIC.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/ALEMBIC.md b/ALEMBIC.md index 2db94a8de38..2b5a57b8f2d 100644 --- a/ALEMBIC.md +++ b/ALEMBIC.md @@ -64,6 +64,30 @@ heads, create a merge migration: alembic merge heads -m "merge backport head" ``` +## Release Version Comment + +Every migration file must include a comment indicating which release version +(including the minor version, e.g., `26.3.0`) it belongs to. Place the comment +next to the revision identifiers: + +```python +# revision identifiers, used by Alembic. +revision = "1cc9b47e0a8e" +down_revision = "ffcf0ed13a26" +# Part of: 26.3.0 +branch_labels = None +depends_on = None +``` + +For backport migrations, note both the target release branch and the main branch: + +```python +# Part of: 26.2.1 (backport), 26.3.0 (main) +``` + +This makes it easy to trace which release introduced a given schema change and to +identify backport migrations when reviewing the version history. + ## Idempotent Writing Rules Every backport migration (both `d` and `d'`) **must** be idempotent. Use the following From f42473e5d7553f6cb9daaa78ba0bf18e74b7b08c Mon Sep 17 00:00:00 2001 From: Sanghun Lee Date: Fri, 27 Mar 2026 13:31:30 +0900 Subject: [PATCH 5/5] docs(BA-5414): move ALEMBIC.md into manager/models/alembic/ Move backport strategy documentation from root ALEMBIC.md to manager/models/alembic/ as README.md (full docs) and CLAUDE.md (AI agent guardrails). Account manager and appproxy alembic READMEs now link to the manager's canonical document. Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 5 +- .../account_manager/models/alembic/README | 2 +- .../coordinator/models/alembic/README | 2 +- .../backend/manager/models/alembic/CLAUDE.md | 16 ++++++ src/ai/backend/manager/models/alembic/README | 3 -- .../backend/manager/models/alembic/README.md | 51 ++++++++++--------- 6 files changed, 47 insertions(+), 32 deletions(-) create mode 100644 src/ai/backend/manager/models/alembic/CLAUDE.md delete mode 100644 src/ai/backend/manager/models/alembic/README rename ALEMBIC.md => src/ai/backend/manager/models/alembic/README.md (87%) diff --git a/CLAUDE.md b/CLAUDE.md index 199a936f973..dcfda34f4e6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,7 +7,7 @@ This file contains core rules for AI coding agents. For detailed patterns and wo **Core Documents (Read directly):** - `tests/CLAUDE.md` - Testing guidelines and strategies - `BUILDING.md` - Build system, quality enforcement, BUILD policies -- `ALEMBIC.md` - Alembic migration backport strategy +- `src/ai/backend/manager/models/alembic/README.md` - Alembic migration backport strategy - `README.md` - Project overview and architecture - `proposals/README.md` - BEP (Backend.AI Enhancement Proposals) @@ -53,7 +53,8 @@ pants test --changed-since=HEAD~1 ## Alembic Migration Backport When backporting migrations to release branches, both the backport and main branch -migrations must be idempotent. See `ALEMBIC.md` for the full strategy and examples. +migrations must be idempotent. See `src/ai/backend/manager/models/alembic/README.md` +for the full strategy and examples. ## Layer Architecture diff --git a/src/ai/backend/account_manager/models/alembic/README b/src/ai/backend/account_manager/models/alembic/README index 1ab37f6270f..523ce67b8ea 100644 --- a/src/ai/backend/account_manager/models/alembic/README +++ b/src/ai/backend/account_manager/models/alembic/README @@ -1,3 +1,3 @@ Generic single-database configuration with an async dbapi. -For the migration backport strategy, see `ALEMBIC.md` at the repository root. \ No newline at end of file +For the migration backport strategy, see `src/ai/backend/manager/models/alembic/README.md`. diff --git a/src/ai/backend/appproxy/coordinator/models/alembic/README b/src/ai/backend/appproxy/coordinator/models/alembic/README index de2cb6d5fd8..a33ac3de1fb 100644 --- a/src/ai/backend/appproxy/coordinator/models/alembic/README +++ b/src/ai/backend/appproxy/coordinator/models/alembic/README @@ -1,3 +1,3 @@ Generic single-database configuration. -For the migration backport strategy, see `ALEMBIC.md` at the repository root. \ No newline at end of file +For the migration backport strategy, see `src/ai/backend/manager/models/alembic/README.md`. diff --git a/src/ai/backend/manager/models/alembic/CLAUDE.md b/src/ai/backend/manager/models/alembic/CLAUDE.md new file mode 100644 index 00000000000..427ea88f3c5 --- /dev/null +++ b/src/ai/backend/manager/models/alembic/CLAUDE.md @@ -0,0 +1,16 @@ +# Alembic Migrations — Guardrails + +> For the full backport procedure and examples, see `README.md` in this directory. + +## Rules + +- **Backport = fixes only** -- Never backport feature-level schema changes. +- **Idempotent** -- All backport migrations (both `d` and `d'`) must use existence + checks (`IF NOT EXISTS`, `IF EXISTS`, `inspector`) so they are safe to re-apply. +- **Release version comment** -- Every migration file must have a + `# Part of: ..` comment next to the revision identifiers. + For backports: `# Part of: 26.2.1 (backport), 26.3.0 (main)`. +- **No revision editing on released migrations** -- Never modify `revision` or + `down_revision` of a migration that has already been released. Editing + `down_revision` is allowed only when inserting a backport into the main chain + before merge. diff --git a/src/ai/backend/manager/models/alembic/README b/src/ai/backend/manager/models/alembic/README deleted file mode 100644 index de2cb6d5fd8..00000000000 --- a/src/ai/backend/manager/models/alembic/README +++ /dev/null @@ -1,3 +0,0 @@ -Generic single-database configuration. - -For the migration backport strategy, see `ALEMBIC.md` at the repository root. \ No newline at end of file diff --git a/ALEMBIC.md b/src/ai/backend/manager/models/alembic/README.md similarity index 87% rename from ALEMBIC.md rename to src/ai/backend/manager/models/alembic/README.md index 2b5a57b8f2d..6f55fe0286a 100644 --- a/ALEMBIC.md +++ b/src/ai/backend/manager/models/alembic/README.md @@ -1,9 +1,21 @@ -# Alembic Migration Backport Strategy +# Alembic Migrations -This document describes how to safely backport Alembic migrations to release branches +Generic single-database configuration. + +## Migration Backport Strategy + +This section describes how to safely backport Alembic migrations to release branches while keeping the main branch consistent. -## Principles +The same strategy applies to all components: + +| Component | Alembic directory | +|---|---| +| Manager | `src/ai/backend/manager/models/alembic/` | +| Account Manager | `src/ai/backend/account_manager/models/alembic/` | +| App Proxy Coordinator | `src/ai/backend/appproxy/coordinator/models/alembic/` | + +### Principles 1. **Fixes only** -- Backport migrations must contain schema fixes only (e.g., missing columns, incorrect types, enum coexistence issues). Feature-level schema changes are @@ -15,15 +27,7 @@ while keeping the main branch consistent. allowed only when inserting a backport migration into the main branch chain before the change is merged (see Step 2 below). -## Applicable Components - -| Component | Alembic directory | -|---|---| -| Manager | `src/ai/backend/manager/models/alembic/` | -| Account Manager | `src/ai/backend/account_manager/models/alembic/` | -| App Proxy Coordinator | `src/ai/backend/appproxy/coordinator/models/alembic/` | - -## Backport Procedure +### Backport Procedure Given the following migration chain on **main**: @@ -31,7 +35,7 @@ Given the following migration chain on **main**: a --> b --> c (a = backport target head, c = main head) ``` -### Step 1: Create migration `d` on the release branch +#### Step 1: Create migration `d` on the release branch On the release branch, create a new migration whose `down_revision` is `a` (the release branch head at the point you are targeting): @@ -40,7 +44,7 @@ release branch head at the point you are targeting): a --> d (release branch result) ``` -### Step 2: Insert `d` into main and add duplicate `d'` +#### Step 2: Insert `d` into main and add duplicate `d'` On **main**, the same migration `d` is inserted between `a` and `b`, and a duplicate `d'` is appended on top of the current main head `c`: @@ -55,7 +59,7 @@ a --> d --> b --> c --> d' performs the **same schema change** as `d` but written idempotently so it is a no-op on databases that already applied `d`. -### Step 3: Merge the head if necessary +#### Step 3: Merge the head if necessary After inserting `d` into the chain, run `alembic heads` on main. If there are multiple heads, create a merge migration: @@ -64,7 +68,7 @@ heads, create a merge migration: alembic merge heads -m "merge backport head" ``` -## Release Version Comment +### Release Version Comment Every migration file must include a comment indicating which release version (including the minor version, e.g., `26.3.0`) it belongs to. Place the comment @@ -85,15 +89,12 @@ For backport migrations, note both the target release branch and the main branch # Part of: 26.2.1 (backport), 26.3.0 (main) ``` -This makes it easy to trace which release introduced a given schema change and to -identify backport migrations when reviewing the version history. - -## Idempotent Writing Rules +### Idempotent Writing Rules Every backport migration (both `d` and `d'`) **must** be idempotent. Use the following patterns: -### DDL guards +#### DDL guards ```python # Creating a table @@ -113,7 +114,7 @@ if "ix_my_table_col" not in indexes: op.create_index("ix_my_table_col", "my_table", ["col"]) ``` -### Enum type guards +#### Enum type guards ```python conn = op.get_bind() @@ -126,7 +127,7 @@ if result.fetchone() is None: my_enum.create(conn) ``` -### Raw SQL guards +#### Raw SQL guards ```sql -- Column @@ -140,7 +141,7 @@ DROP INDEX IF EXISTS ix_my_table_col; ALTER TABLE my_table DROP COLUMN IF EXISTS old_col; ``` -### Downgrade +#### Downgrade Downgrade functions follow the same idempotent rules. If the downgrade is handled by another migration in the chain, use `pass`: @@ -150,7 +151,7 @@ def downgrade() -> None: pass ``` -## Real-World Example +### Real-World Examples See the following migrations in the codebase for reference: