ADR 0020: Alembic-Based MetaTable Migrations
Historical decision: SDK-owned MetaTables functionality described here was superseded by ADR 0034. Follow the current removal migration guide for supported imports and commands.
Date: 2026-06-01
Status: Superseded
Superseded in part by ADR 0021: Migration-First Platform-Managed MetaTables for platform-managed lifecycle details, by ADR 0022: Thin SDK Alembic-Owned MetaTable Migrations for the direct-Alembic SDK flow, and by backend ADR 013 for Alembic-managed finalization and reset semantics. FK/index migration-resolution policy is superseded by ADR 0023: Alembic-Owned Foreign Keys And Indexes.
Context
SQLAlchemy MetaTable declarations are the SDK authoring source for table contracts. They are not a migration engine.
The SDK custom migration operation format duplicated Alembic with a weaker language: fake schema operation plans and SDK-managed artifact rows. That path is not maintainable and must be removed.
The intended lifecycle is:
SQLAlchemy models
-> Alembic revision
-> SDK reserves provider MetaTables and obtains a scoped database URI
-> Alembic executes current/revision/upgrade directly
-> SDK calls backend finalize-managed once for the provider MetaTable UIDs
-> project finalizes derived catalog state after backend finalization succeeds
Alembic owns schema diffs, revision files, downgrade/upgrade semantics, and the
physical alembic_version table. The SDK owns a single migration provider
object, MetaTable registration/catalog bindings, and typed backend requests.
Decision
Use Alembic as the only schema migration engine.
The SDK will not provide a migration artifact table. There is no SDK
migration-row model and no SDK migration-row upsert path. The only MetaTable
model the SDK provides for Alembic migration state is AlembicVersionMetaTable.
Each client project defines one AlembicMetaTableMigration provider for now.
That provider is the boundary between the host project, Alembic, SDK catalog
registration, and backend execution. It supplies:
- the package and migration namespace used for request identity
- the Alembic script location
- the SQLAlchemy
target_metadataAlembic uses for optional autogenerate - the
AlembicVersionMetaTablebinding for Alembic's physical version table - the MetaTable model classes that project tooling may register or refresh after SQL execution
- the optional post-registration catalog hook
AlembicVersionMetaTable is the platform-managed root catalog binding for an
Alembic migration stream. The SDK reserves it with
schema_management_mode="alembic_managed" before Alembic runs and with the
minimal revision column the backend needs to read current state:
{
"version": "relational-table.v1",
"physical": {"schema": "public", "table_name": "msm_alembic_version"},
"columns": [
{
"name": "version_num",
"data_type": "string",
"backend_type": "VARCHAR",
"nullable": false,
"primary_key": true
}
],
"constraints": [],
"authoring": {
"owner": "alembic",
"schema": "public",
"version_table": "msm_alembic_version"
}
}
That contract means:
- Alembic owns the actual version-table DDL.
- The SDK reserves a stable managed root for catalog/discovery and current-revision reads.
- The
version_numdeclaration is not an SDK-owned SQLAlchemy model. It is the minimal metadata required by the managed MetaTable reservation contract. - The backend does not use this contract to validate Alembic inserts or DDL. PostgreSQL and Alembic remain the authority for the physical version table.
- The binding is
platform_managedbecause the SDK owns its catalog lifecycle. - Its schema management mode is
alembic_managedbecause Alembic owns the physical version-table DDL. - It starts
reservedand becomesactiveonly after managed finalization confirms that the physical version table exists.
There is no [tool.mainsequence.migrations].enabled provider list yet. The
first implementation uses one conventional provider per host project, with an
explicit --provider CLI override when convention cannot find it. Multiple
provider or dependency-package migration selection is a future extension, not
part of this ADR's first implementation.
Alembic MetaTable Migration Provider
Client packages define one provider object:
from mainsequence.meta_tables.migrations import (
AlembicMetaTableMigration,
AlembicVersionMetaTable,
)
from msm.catalog import refresh_markets_catalog_from_registered_metatables
from msm.config import markets_namespace
from msm.models import Base
from msm.models.accounts import AccountTable
from msm.models.assets import AssetTable, AssetTypeTable
from msm.migrations.registry import migration_model_registry
class MarketsAlembicVersion(AlembicVersionMetaTable):
__metatable_namespace__ = "msm"
__metatable_identifier__ = "msm.alembic_version"
__alembic_version_schema__ = "public"
__alembic_version_table_name__ = "msm_alembic_version"
__alembic_version_column_name__ = "version_num"
migration = AlembicMetaTableMigration(
package="msm",
migration_namespace=markets_namespace(),
script_location="msm:migrations",
target_metadata=Base.metadata,
alembic_registry=MarketsAlembicVersion,
metatable_models=migration_model_registry(),
after_register_metatables=refresh_markets_catalog_from_registered_metatables,
)
The provider is the only source the CLI needs for migration behavior. The SDK must not infer migrations from every imported model or every installed dependency. Imported libraries may expose their own provider objects, but the host project must choose the provider being operated on.
after_register_metatables is an optional project hook for catalog side effects
that must happen after provider-scoped MetaTable registration. The SDK calls it
only after every model in migration.metatable_models has registered or
refreshed successfully. The hook receives an
AlembicMetaTableCatalogRefreshContext containing the ordered registered
MetaTable objects, the matching ordered provider model classes, provider
identity, and the reserved-table policy for any post-Alembic catalog writes:
def refresh_markets_catalog_from_registered_metatables(context):
models = context.metatable_models
registered_metatables = context.registered_metatables
reserved_policy = context.reserved_policy
...
Hooks must use the provider-scoped context.metatable_models; importing a
separate global registry can compare the current provider against the wrong
model scope.
The hook is client/project tooling. It is not part of the backend apply request,
is not a second migration language, and must not run during current,
revision, failed upgrade, or failed downgrade. It should be idempotent
because SQL execution and MetaTable registration may already have succeeded
before a hook failure is retried.
The alembic_registry attribute is the source for Alembic's version table
configuration. The SDK must derive Alembic config from it:
context.configure(
target_metadata=migration.target_metadata,
version_table=migration.alembic_registry.__alembic_version_table_name__,
version_table_schema=migration.alembic_registry.__alembic_version_schema__,
compare_type=True,
compare_server_default=True,
)
For autogenerate, the provider must also support an inclusion filter so Alembic
does not generate migrations for unrelated imported SQLAlchemy tables. The first
implementation may default to tables present in target_metadata, but the
provider shape must leave room for an explicit include_object or
include_name hook.
Recommended provider discovery order:
mainsequence_migrations.py:migrationin the current project root.<current_package>.migrations:migrationwhen the CLI can infer the package.--provider module.path:migrationas an explicit override.
The CLI must not require data_source_uid in project config. Initial
AlembicVersionMetaTable reservation resolves its data source through the same
resolver used by other MetaTables. After reservation, the
bound AlembicVersionMetaTable supplies the target data source for Alembic
status checks and backend SQL execution. Migration provider methods and CLI
commands must not accept a data_source_uid override.
That target data source is for Alembic status checks and backend SQL execution. It must not be copied into application MetaTable registrations during catalog sync. Each application MetaTable registers through its own normal model binding/configuration.
Stable MetaTable Identity For Alembic Catalog Sync
Application MetaTable catalog sync in the Alembic workflow uses the SQLAlchemy
table name as the stable lookup key. During prepare_for_alembic(), the SDK
reads each provider model's current Table.name, sends that value as the
reservation identifier, and emits the same value as same-batch FK
target_identifier before backend UIDs exist.
__metatable_identifier__ is not the Alembic migration identity. A model rename
or module move does not affect migration identity as long as the SQLAlchemy
table name is stable.
Catalog sync after a migration must:
- Resolve each model's SQLAlchemy table name.
- Send every provider model to
reserve-managed/with that table name as the reservationidentifier. - Let TS Manager resolve existing catalog rows or create missing reserved rows in one batch.
- Bind the model class to the MetaTable UID, data source UID, and physical table name returned by the reservation response.
- Fail if TS Manager reports that the table-name identity is not unique.
This is the mechanism that lets Alembic change a shape-addressed
PlatformManagedMetaTable without losing the original catalog path. The new
SQLAlchemy class shape must not decide which deployed MetaTable is refreshed,
and the SDK must not skip reservation because a previous catalog contract
appears to match.
Alembic Version MetaTable Reservation Contract
The SDK reservation wire request for Alembic's version table is the root
platform_managed and alembic_managed MetaTable for the provider. Under ADR
0031, the shown code_repository_context is inserted internally from the
frozen Git-native context for local requests. Authenticated deployed runtimes
omit this transport field because the backend derives the same context from
the verified runtime target. Application callers never provide it:
{
"data_source_uid": "uuid",
"management_mode": "platform_managed",
"provisioning_status": "reserved",
"is_alembic_managed": true,
"migration_package": "msm",
"migration_namespace": "markets",
"migration_provider_key": "msm:markets",
"alembic_version_meta_table_uid": null,
"alembic_revision": null,
"physical_schema": "public",
"physical_table_name": "msm_alembic_version",
"code_repository_context": {
"code_repository_branch_uid": "uuid"
},
"identifier": "msm.alembic_version",
"namespace": "msm",
"description": "Alembic revision state table.",
"labels": [],
"table_contract": {
"version": "relational-table.v1",
"physical": {
"schema": "public",
"table_name": "msm_alembic_version"
},
"columns": [
{
"name": "version_num",
"data_type": "string",
"backend_type": "VARCHAR",
"nullable": false,
"primary_key": true
}
],
"constraints": [],
"authoring": {
"owner": "alembic",
"schema": "public",
"version_table": "msm_alembic_version"
}
}
}
The registry has no alembic_version_meta_table_uid because it is the provider
root. Initial migration setup does not require the physical alembic_version
table to exist: the catalog row remains reserved until Alembic runs and
managed finalization confirms the physical table. The backend still enforces
CodeRepository context, permissions, data-source ownership, provider identity,
physical identity, and non-empty contract columns.
Client Workflow
The client workflow has one provider-scoped lifecycle:
- Reserve the platform-managed Alembic registry root.
- Reserve provider tables as children of that registry.
- Run Alembic through a backend-issued migration connection.
- Finalize the registry and provider tables together.
The client does not run a migration "on" a MetaTable. The reserved
AlembicVersionMetaTable only tells the backend where Alembic revision state
lives for a data source. The migration SQL runs on the target data source and
Alembic's own SQL updates the physical alembic_version table.
1. Reserve The Alembic Version MetaTable
The selected AlembicMetaTableMigration provider must first reserve its
alembic_registry binding. This reservation uses the same data-source
resolver as other MetaTable operations: an explicit class binding when one
exists, otherwise the process-frozen CodeRepositoryBranch MetaTables data
source.
from mainsequence.meta_tables.migrations import AlembicVersionMetaTable
class MarketsAlembicVersion(AlembicVersionMetaTable):
__metatable_namespace__ = "msm"
__metatable_identifier__ = "msm.alembic_version"
__alembic_version_schema__ = "public"
__alembic_version_table_name__ = "msm_alembic_version"
__alembic_version_column_name__ = "version_num"
alembic_version_meta_table = migration.ensure_alembic_registry()
ensure_alembic_registry() is a remote resolve-or-create operation, not an
unconditional collection create and not a class-cache lookup. It first resolves
the exact DataSource/schema/table physical identity, validates that any match is
the expected platform-managed, Alembic-managed provider root, and reuses it.
Only a missing registry is collection-created.
The reservation request generated by the SDK contains the resolved data source
and provider identity. The JSON below is the local-checkout wire projection;
the SDK inserts code_repository_context internally, while an authenticated
deployed runtime omits it and relies on backend-derived runtime context:
request = migration.alembic_registry.build_registration_request(
migration_package=migration.package,
migration_namespace=migration.migration_namespace,
migration_provider_key=migration.migration_provider_key,
data_source_uid=resolved_registration_data_source_uid,
)
{
"data_source_uid": "uuid",
"management_mode": "platform_managed",
"provisioning_status": "reserved",
"is_alembic_managed": true,
"migration_package": "msm",
"migration_namespace": "markets",
"migration_provider_key": "msm:markets",
"alembic_version_meta_table_uid": null,
"alembic_revision": null,
"physical_schema": "public",
"physical_table_name": "msm_alembic_version",
"code_repository_context": {
"code_repository_branch_uid": "uuid"
},
"identifier": "msm.alembic_version",
"namespace": "msm",
"description": "Alembic revision state table.",
"protect_from_deletion": false,
"labels": [],
"table_contract": {
"version": "relational-table.v1",
"physical": {
"table_name": "msm_alembic_version"
},
"columns": [
{
"name": "version_num",
"data_type": "string",
"backend_type": "VARCHAR",
"nullable": false,
"primary_key": true
}
],
"constraints": [],
"indexes": [],
"foreign_keys": [],
"authoring": {
"owner": "alembic",
"schema": "public",
"version_table": "msm_alembic_version"
}
}
}
The schema, version_table, and version_num column must match the project's
Alembic configuration. For an initial migration, the physical
alembic_version table may not exist yet. The reservation remains unusable
until Alembic creates the table and finalize-managed changes the registry to
active.
2. Generate An Alembic Revision
The client project uses Alembic revision generation through the SDK CLI or a project-local Alembic command wired to the same provider:
mainsequence migrations revision
-m/--message is optional; when omitted, the CLI passes migration as the
Alembic revision message.
Autogenerate is enabled by default. If --sqlalchemy-url is supplied, Alembic
reflects that database. Otherwise the CLI requests a provider migration
connection without MetaTable UIDs.
The Alembic environment must point target_metadata at
migration.target_metadata, and it must derive version_table and
version_table_schema from migration.alembic_registry. Alembic owns
op.create_table(...), op.add_column(...), downgrade bodies, branch labels,
dependencies, and the physical alembic_version table.
Minimal Alembic env.py direction:
from alembic import context
from msm.migrations import migration
def include_name(name, type_, parent_names):
return migration.include_name(name, type_, parent_names)
context.configure(
target_metadata=migration.target_metadata,
version_table=migration.alembic_registry.__alembic_version_table_name__,
version_table_schema=migration.alembic_registry.__alembic_version_schema__,
include_name=include_name,
compare_type=True,
compare_server_default=True,
)
The inclusion hook is the extension point that prevents unrelated imported SQLAlchemy tables from entering this migration provider.
SDK migration files must not define custom operations() functions and must not
emit SDK operation lists.
3. Current Revision Discovery
ADR 0022 removed the backend migration-status helper. Current SDK tooling reads
the current Alembic revision by configuring Alembic with the provider's
reserved version-table binding and calling Alembic current through the
migration connection.
The version table remains discoverable because the provider declares an
AlembicVersionMetaTable. The backend catalog stores that MetaTable pointer;
Alembic, not a TS Manager status endpoint, reads the row content.
4. Superseded: Direct Alembic Execution
ADR 0022 removes the normal-user SQL artifact path. The current SDK CLI calls
Alembic directly through a provider migration connection instead of exposing a
render command or backend SQL apply command.
The older design rendered SQL from Alembic without applying it locally. It used SQL output similar to:
alembic upgrade <current_revision_or_base>:<target_revision> --sql
For the first migration, <current_revision_or_base> was base. For a normal
upgrade, the older design used a backend status helper that no longer exists.
The SDK packages the rendered SQL with a small manifest that describes the Alembic revision and the provider's registry table. The backend echoes this metadata for observability; it does not validate or approve the artifact.
manifest = {
"package": migration.package,
"migration_namespace": migration.migration_namespace,
"revision": "0001_initial",
"down_revision": None,
"direction": "upgrade",
"alembic_version_table": "public.msm_alembic_version",
}
alembic_version_table is computed from
migration.alembic_registry.__alembic_version_schema__ and
migration.alembic_registry.__alembic_version_table_name__.
statement_boundaries is optional diagnostic metadata. It can identify rendered
statement positions for error reporting, but it is not executable migration
logic.
5. Superseded: Backend SQL Artifact Apply
ADR 0022 removes the normal-user dry-run/apply artifact path. Current SDK
migration commands do not construct AlembicMigrationOperation, do not call
MetaTable.apply_migration(...), and do not send rendered SQL to a backend
apply endpoint. The SDK requests a scoped database connection and calls Alembic
directly.
7. Confirm Revision State
After Alembic runs directly, the SDK confirms state through Alembic itself using the same scoped connection and configured provider version table. There is no TS Manager migration-status request body in the active contract.
8. Register Or Refresh Application MetaTables By Identifier
After Alembic applies the physical schema change, project tooling registers or refreshes the application MetaTable catalog bindings listed on the selected provider. This is part of the migration success path, not an optional separate operation:
registered_metatables = migration.sync_metatable_catalog()
That catalog step resolves each model by stable identifier first. If the
MetaTable already exists, the SDK binds the model to that existing MetaTable and
refreshes registration metadata for that same catalog path. If it does not
exist, the SDK performs the initial registration for that identifier.
The provider controls the catalog scope; imported-but-unlisted MetaTable classes are not registered by migration tooling.
If migration.after_register_metatables is configured, the SDK calls it after
the provider-scoped catalog registration succeeds:
migration.after_register_metatables(
AlembicMetaTableCatalogRefreshContext(
package=migration.package,
migration_namespace=migration.migration_namespace,
registered_metatables=registered_metatables,
reserved_policy="reconcile",
)
)
This hook lets a project update project-specific catalog tables, such as a
markets catalog derived from the registered MetaTable UIDs, without adding
Markets-specific behavior to the SDK or backend.
The reserved_policy="reconcile" value is emitted by
refresh_metatable_catalog() after Alembic upgrade. Normal runtime repository
operations omit it and keep TS Manager's default reserved-table rejection.
CLI Workflow
The SDK CLI should expose this workflow through one migration provider:
mainsequence migrations current --provider module.path:migration
mainsequence migrations revision --provider module.path:migration -m "schema change"
mainsequence migrations upgrade --provider module.path:migration head
mainsequence migrations downgrade --provider module.path:migration <revision>
All commands resolve the same provider object by convention or by
--provider module.path:migration. Runtime commands request a temporary
provider migration database connection from the target data source. MetaTable
UIDs remain in prepared/finalization state; they are not sent to the
migration-connection endpoint.
revision writes a normal Alembic revision file. current, upgrade, and
downgrade call Alembic directly through the migration connection. The
backend does not render or apply SQL artifacts for normal users.
Command responsibilities:
currentresolves the provider, reserves or resolves the provider's managedalembic_registryroot when needed, requests a read-only scoped connection, and calls Alembiccurrent.revisioncreates a normal Alembic revision file undermigration.script_location; CodeRepository code may keep generated Alembic operations or edit them like any normal Alembic repository.upgradeprepares provider-scoped MetaTable reservations, binds backend physical table names into SQLAlchemy metadata, requests a scoped migration connection, calls Alembicupgrade, finalizes the MetaTable catalog once, and runsmigration.after_register_metatableswhen configured.downgradeuses the same scoped connection flow and calls Alembicdowngrade.
There is no --register-metatables flag in the final workflow. Catalog sync is
part of upgrade success. The command exits successfully only when Alembic
execution and provider-scoped catalog finalization succeed.
Existing reserved MetaTables found during prepare are bound and reused; they are not collection-created again. If reserved state cannot be reused safely, the client must fail with a clear error.
Superseded Backend Apply Contract
The earlier apply-migration/ SQL artifact request and response contract is
removed. The SDK no longer renders Alembic SQL for TS Manager to execute.
The active backend coordination contract is:
reserve-managed/ -> reserve MetaTable rows and physical table names
migration-connection/ -> issue a provider temporary database credential
finalize-managed/ -> activate reserved rows after Alembic runs
Alembic reads current revision state and executes upgrade/downgrade directly through the provider migration credential. TS Manager must not maintain SDK migration artifact rows, migration-run rows, or a separate SQL apply endpoint.
SDK Responsibilities
The SDK owns:
AlembicMetaTableMigrationAlembicVersionMetaTable- provider discovery for one conventional migration provider plus explicit
--provideroverride - CLI commands that read current state, generate revisions locally, run Alembic upgrade/downgrade directly, reset provider state, register the version table for backend-scoped commands, reserve provider MetaTables for apply-time commands, and finalize provider MetaTables
- typed reservation and finalization request/response models
- helpers that bind MetaTable UID/storage metadata while preserving authored SQLAlchemy table names before Alembic runs
- docs and examples that show Alembic, not SDK operation lists, including the then-current migration walkthrough later relocated to the canonical tutorial project
The SDK does not own:
- custom revision generation outside Alembic
- schema diffing outside Alembic
- a migration artifact table
- artifact sync/upsert helpers
- custom
operations()migration modules - execution of DDL locally
Extensibility Requirements
The first implementation supports one selected provider. It must still be designed so multiple packages can later expose migration providers without making imports dangerous.
Required extension points:
- Provider discovery must be explicit and side-effect free. Importing
mainsequenceormsmmust not run migrations or register MetaTables. - Provider selection must remain user-controlled. Installed dependency packages may expose providers, but the host project decides which provider to operate.
AlembicMetaTableMigrationmust acceptalembic_registryinstead of separate looseversion_tableandversion_schemastrings.AlembicMetaTableMigrationmust accepttarget_metadata; Alembic autogenerate/rendering must never scan every imported SQLAlchemy model.AlembicMetaTableMigrationmust acceptmetatable_models; catalog reservation and finalization must be scoped to that list.- Application MetaTable catalog sync in the Alembic workflow must resolve by exact SQLAlchemy table-name identifier.
- Application MetaTable preparation must reserve each provider-scoped model as a child of the managed Alembic registry without copying a provider-wide data source override. Each model owns its own data-source binding.
__metatable_identifier__must not be used as Alembic migration identity. Normal non-migration MetaTable registration may still expose it as catalog metadata.AlembicMetaTableMigrationmust accept an optionalafter_register_metatableshook for project-specific catalog refresh after provider-scoped MetaTable registration.- The provider should support
include_nameorinclude_objecthooks for packages that share a SQLAlchemyMetaDatawith unrelated tables. - The CLI should support
--provider module.path:migrationfrom day one, even if convention-based discovery covers the main project. - Future multi-provider config may add a selection list, but it must not be required for the single-provider workflow.
Catalog Binding
MetaTables are catalog metadata. Migration connection requests use the reserved
AlembicVersionMetaTable root so the backend can locate Alembic's version table
on the target data source. Provider application tables are reserved as children
of that root and finalized with it after Alembic succeeds.
Removal Inventory
Remove these concepts from SDK public behavior:
- SDK-managed migration artifact table models
- default migration artifact table factories
- migration artifact row Pydantic models
- packaged migration artifact row builders
- artifact table upsert SQL helpers
- sync helpers that insert migrations into a MetaTable
operations()functions in migration files- custom SDK schema-operation names such as
add_column,drop_column,alter_column, andcreate_index - legacy SDK schema-migration SQLAlchemy base classes
- operation-only manifests
- SQL-or-operations fallbacks
- custom schema-operation tests, examples, and docs
Remove or rewrite this supporting material:
- the then-current SDK MetaTable migration example tree, later removed when runnable applications moved to self-contained project repositories
- migration sections in the then-current SDK-hosted tutorial
- stale artifact-table content in
docs/knowledge/meta_tables/migrations.md - stale artifact-table content in
docs/knowledge/meta_tables/api.md
There is no compatibility alias and no fallback path.
Consequences
Alembic is the single source of truth for DDL.
The SDK surface is smaller: one Alembic version MetaTable binding plus typed backend migration requests.
Initial migrations are valid because the backend runs SQL before affected table introspection.
Shape-addressed platform-managed tables remain useful for create-new table patterns, but they are not sufficient for in-place schema migration without a stable identifier.
Implementation Tasks
- [x] Make SQLAlchemy a required SDK dependency.
- [x] Remove SDK schema-operation models.
- [x] Remove SDK-managed migration artifact table models.
- [x] Remove default migration artifact table factory.
- [x] Remove migration artifact registry helpers.
- [x] Remove registry upsert and sync helpers.
- [x] Remove custom schema-operation tests.
- [x] Add
AlembicVersionMetaTable. - [x] Make
AlembicVersionMetaTablea platform-managed, Alembic-managed root reservation with the minimalversion_numcontract. - [x] Superseded by ADR 0022: remove the normal-user offline SQL render/backend apply artifact path. The CLI calls Alembic directly through a scoped migration connection.
- [x] Add
AlembicMetaTableMigrationprovider object withpackage,migration_namespace,script_location,target_metadata,alembic_registry,metatable_models, and optional Alembic inclusion hooks. - [x] Add provider discovery by convention plus
--provider module.path:migration. - [x] Add SDK CLI
mainsequence migrationscommand group. - [x] Add CLI support for
current,revision,upgrade,downgrade, andreset. - [x] Superseded by ADR 0022: CLI migration status/apply requests were removed from the normal user path.
- [x] Make revision/autogenerate use
migration.target_metadata,migration.script_location, and the Alembic version table declared bymigration.alembic_registry. - [x] Superseded by ADR 0022: no
renderorupgrade --dry-runcommand exists in the current SDK CLI. - [x] Make
upgradeperform provider-scoped backend finalization by default after Alembic succeeds, with no--register-metatablesflag. - [x] Make Alembic migration catalog sync use the SQLAlchemy table name as the
reservation
identifierand same-batch FKtarget_identifier. - [x] Make migration catalog sync bind models to existing catalog rows before refresh, reserve missing table-name identifiers, and fail on duplicate table-name identifiers.
- [x] Update the then-current tutorial migration page so the user-facing flow is
one
mainsequence migrations upgrade --provider ... headpath with table-name catalog sync. The canonical tutorial was later moved to its own project. - [x] Update
docs/knowledge/meta_tables/migrations.mdanddocs/knowledge/meta_tables/api.mdto remove the--register-metatablesworkflow and document table-name catalog sync. - [x] Update MetaTable examples to use the final upgrade flow and document that model renames keep identity when the SQLAlchemy table name remains stable.
- [x] Update the MetaTable skill so migration guidance follows the final ADR
workflow and does not mention optional
--register-metatables. - [x] Add optional
after_register_metatablesprovider hook and run it with anAlembicMetaTableCatalogRefreshContextafter successful provider-scoped MetaTable registration. - [x] Add CLI/provider tests covering initial migration, current revision, direct Alembic upgrade, and provider-scoped catalog finalization.
- [x] Update tutorials and examples around the provider-based Alembic lifecycle.
Non-Goals
This ADR does not make the backend run Alembic.
This ADR does not introduce a backend-owned migration artifact registry.
This ADR does not introduce an SDK migration artifact registry MetaTable.
This ADR does not introduce a general-purpose SQL execution endpoint. SQL is accepted only through the Alembic migration apply contract, tied to a registered Alembic version MetaTable, provider identity, revision metadata, and current revision checks. Alembic revisions may render custom SQL, but that SQL is part of an authorized migration artifact, not an ad hoc SQL request.