Skip to content

Add Alembic integration tests and ship the YDB Alembic impl - #118

Open
vgvoleg wants to merge 8 commits into
mainfrom
alembic-integration-tests
Open

Add Alembic integration tests and ship the YDB Alembic impl#118
vgvoleg wants to merge 8 commits into
mainfrom
alembic-integration-tests

Conversation

@vgvoleg

@vgvoleg vgvoleg commented Sep 3, 2026

Copy link
Copy Markdown
Member

Closes #117.

Alembic support was documented and exercised only by examples/alembic, with nothing verifying it. Writing the tests surfaced that the integration code was never shipped: every env.py had to define its own DefaultImpl subclass and overwrite the private MigrationContext._version. That now lives in ydb_sqlalchemy.alembic, built on Alembic's public version_table_impl hook, so env.py just imports it.

docs/migrations.rst now opens with a Support Status section stating per command and per operation what works and what does not. Every row in it is backed by a test, in both directions — the unsupported ones are asserted too, so a future YDB release lifting a restriction shows up as a failing test rather than as stale prose.

Tests. test/test_alembic.py (31 cases, ~30s): version table shape and head tracking; upgrade/downgrade over a three-revision chain, stepwise and to base; stamp; the operations usable in a revision body; autogenerate diffs including the empty diff when the model is in sync; YDB types; and the operations YDB rejects. Table names carry a unique suffix, so the suite is safe against a shared database.

Bugs found:

  • op.bulk_insert() with sa.table()/sa.column() — the form Alembic documents — raised AttributeError because _is_bound_to_nullable_column assumed .nullable/.primary_key, which ColumnClause does not have. Fixed in the compiler.
  • The docs presented alter_column widening a string as supported, and used alter_column(..., nullable=False) in two examples. YDB accepts neither. Replaced with add-copy-drop.
  • The create_table example declared a ForeignKeyConstraint. YDB has no foreign keys, so that example could never have run.

Where alter_column actually stands, since it is the fiddly part: YDB's ALTER COLUMN changes column options and can DROP NOT NULL, but cannot change a type. So alter_column(nullable=True) works, while nullable=False and type_= do not. The remaining options have no Alembic operation and need op.execute(sa.schema.DDL(...)) — a plain string is not marked as DDL by SQLAlchemy, so the dialect sends it to the query service and YDB rejects it.

Worth a look in review: version_table_impl keeps the documented layout — a surrogate always-NULL id as primary key — rather than the more obvious version_num primary key. version_num cannot be the key because Alembic advances a revision with UPDATE ... SET version_num, and YDB cannot update a key column. The consequence, now tested and documented, is that branched history is unsupported.

Alembic also ships alembic.testing.suite for third-party dialects, which nothing was running here. It now runs from test/test_alembic_suite.py with the YDB feature flags in test/alembic_requirements.py: 22 passed, 84 skipped, none failing. Wiring it needs one requirements class serving both suites, since Alembic reads its flags off the same object as SQLAlchemy.

A third bug, found while comparing against the Liquibase dialect: create_index(unique=True) compiles to ADD INDEX ... GLOBAL SYNC without UNIQUE, so the index does not enforce uniqueness, and reflection reports unique: False even for one that does. YDB supports unique indexes and rejects duplicates on them. Not fixed here — it is a dialect concern, not an Alembic one — but the test now inserts a duplicate and asserts it is accepted, so fixing it will fail the test and force the docs to follow. Liquibase rejects unique indexes outright rather than downgrading them silently.

Offline mode (upgrade --sql) is untested and the table says so rather than guessing.

On a clean database the main suite has one pre-existing failure (test_ydb_credentials_good, an Unauthorized from the environment), identical before and after, including after the requirement_cls change.

Also drops the ## Unreleased ## heading this branch had added to CHANGELOG.md: python-publish.yml reads the release notes as everything above the first ## line, so it would have made them empty and failed the publish job with CHANGELOG empty, and increment_version.py would have skipped inserting the real version header. Pending entries are bare bullets now, and the rule is written down in a new AGENTS.md — same reasoning as ydb-platform/ydb-python-sdk#892, which this repository had no equivalent of.

vgvoleg added 7 commits September 3, 2026 19:02
Alembic support was documented and exercised only by examples/alembic, with
no test covering it. Adding the tests surfaced that the integration code
itself was not shipped: env.py had to define a DefaultImpl subclass and
override the private MigrationContext._version to get a version table YDB
accepts. That now lives in ydb_sqlalchemy.alembic, built on the public
version_table_impl hook, so env.py only has to import it.

The suite covers the version table, upgrade/downgrade over a revision chain,
the operations available inside a revision, autogenerate diffs, and the
YDB restrictions migrations have to work around. It caught a bind-type
inference crash on sa.table()/sa.column() constructs, which is the form
alembic documents for op.bulk_insert().

Documentation claimed alter_column worked for widening a string and for
adding NOT NULL. YDB rejects both, so those examples are replaced with
add-copy-drop and the limits are now asserted by tests.
The previous commit left the support level implicit: it was spread across
test names and scattered notes. Add a Support Status section stating, per
command and per operation, what works and what does not.

Every row is backed by a test. Filling the gaps between the table and the
suite added coverage for op.execute data migrations, unique indexes and two
limits that had only been reasoned about: foreign keys are rejected, and a
second head cannot be inserted into the version table.

The create_table example still declared a ForeignKeyConstraint, which YDB
rejects; it now uses a secondary index instead.
The previous commit claimed YDB has no ALTER COLUMN at all. It does: per
the YQL reference it changes column options and can DROP NOT NULL. What it
cannot do is change a type, which is what Alembic emits for type_=.

The claim came from generalising a single parser error instead of reading
the reference, and only the failing directions had tests, so nothing
contradicted it. op.alter_column(nullable=True) in fact works, and now has
a test.

Also correct the advice to reach the remaining column options through
op.execute: a plain DDL string is not marked as DDL by SQLAlchemy, so the
dialect sends it to the query service and YDB rejects it. It has to be
wrapped in sa.schema.DDL. Both directions are now tested.
Alembic ships alembic.testing.suite for dialect authors, the same way
SQLAlchemy ships its compliance suite, and nothing was running it here.

Wiring it needs one requirements class serving both suites, since Alembic
reads its feature flags off the same object; test/alembic_requirements.py
extends the dialect requirements with the Alembic flags and closes the ones
YDB does not have. Two cases no flag covers are skipped explicitly: the FK
options class, and the one autogenerate fixture whose table has no primary
key.

Result on a clean database: 22 passed, 84 skipped, none failing. The main
suite is unaffected by the requirements change -- same single pre-existing
failure before and after.
create_index(unique=True) compiles to ADD INDEX ... GLOBAL SYNC, without
UNIQUE, so the index does not enforce uniqueness; reflection also reports
unique: False for an index that is unique. YDB supports unique indexes and
rejects duplicates on them, so this is a dialect gap.

The earlier test only asserted the index existed, which passed while the
guarantee was absent. It now inserts a duplicate and asserts it is accepted,
so fixing the dialect will fail this test and force the docs to be updated.
upgrade --sql renders schema statements correctly but emits the version
table insert with an unfilled placeholder, because Alembic only supplies
version_num and the surrogate key column has no value to render. Removing
the surrogate column does not help: version_num would become the primary
key and the update that advances a revision cannot run on YDB.

The support table said "not covered"; it can now say why it does not work.
A heading at the top of CHANGELOG.md breaks the release. python-publish.yml
reads the release notes as everything above the first "## " line, so the
"## Unreleased ##" added earlier in this branch made them empty and the
publish job would have exited with "CHANGELOG empty". add_changelog_version
in increment_version.py would then also have skipped inserting the real
version header, since it returns early when the file starts with "##".

Pending entries are now bare bullets. The two internal-only ones went away
as well: tests and docs do not belong in release notes.

Same rule and reasoning as ydb-platform/ydb-python-sdk#892, which put it in
AGENTS.md. This repository had no such file, so this adds one.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Several documented command guarantees lack their promised integration coverage, and the migration guide contains an invalid empty directive.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds packaged Alembic integration for YDB, expands migration coverage, and documents supported behavior and limitations.

Changes:

  • Adds YDBImpl and fixes lightweight-table bulk inserts.
  • Adds YDB-specific Alembic integration/compliance tests.
  • Updates migration documentation, examples, and release guidance.
File summaries
File Description
ydb_sqlalchemy/sqlalchemy/compiler/base.py Handles lightweight columns during bind inference.
ydb_sqlalchemy/alembic.py Adds the YDB Alembic implementation.
test/test_alembic.py Adds migration integration tests.
test/test_alembic_suite.py Enables Alembic’s dialect suite.
test/alembic_requirements.py Defines YDB feature requirements.
test-requirements.txt Adds Alembic 1.14+.
setup.cfg Selects combined test requirements.
examples/alembic/README.md Simplifies integration instructions.
examples/alembic/migrations/env.py Uses the packaged implementation.
docs/migrations.rst Documents support and limitations.
CHANGELOG.md Records user-facing changes.
AGENTS.md Documents repository workflows.
.gitignore Ignores Alembic test scratch files.
Review details
  • Files reviewed: 12/13 changed files
  • Comments generated: 3
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread docs/migrations.rst
Comment on lines +47 to +51
* - ``current``, ``history``
- Supported
-
* - ``revision --autogenerate``
- Supported
Comment thread test/test_alembic.py
Comment thread docs/migrations.rst Outdated
The support table promised that every supported row is covered by a test,
but nothing invoked command.current(), command.history() or
command.revision(autogenerate=True) -- the autogenerate tests went through
compare_metadata() and the generated env.py passed target_metadata=None, so
the on-disk environment could not autogenerate at all.

The test environment now takes target_metadata from config.attributes and
scopes autogenerate with include_name, and TestCommands drives the four
commands end to end. Capturing their output needs an explicit buffer:
Config.stdout defaults to the sys.stdout bound at import time, which pytest's
capture never sees.

Autogenerate also only checked that an added index is detected; it now checks
the removal too.

Removes a stray empty code-block directive left in the create_table section
when the foreign key example was replaced.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Alembic migrations are documented but have no test coverage in CI

2 participants