Skip to content

feat: let any package ship extension migrations - #660

Merged
cofin merged 2 commits into
mainfrom
fix/third-party-extension-migrations
Jul 27, 2026
Merged

feat: let any package ship extension migrations#660
cofin merged 2 commits into
mainfrom
fix/third-party-extension-migrations

Conversation

@cofin

@cofin cofin commented Jul 27, 2026

Copy link
Copy Markdown
Member

Extension migrations were found by mapping the extension_config key onto the hardcoded sqlspec.extensions.<name> module namespace, so a package distributed separately from SQLSpec could not register migrations at all — it logged Extension <name> not found and then silently registered nothing. Any package can now point SQLSpec at its own migrations directory.

  • Third-party packages can ship migrations. Set migrations_path in an extension_config entry. Declaring it opts the extension in automatically, so it does not also need to be listed in include_extensions; exclude_extensions still opts it back out.
  • Extension names containing underscores no longer lose their version. A migration such as ext_my_extension_0001_init.sql previously resolved to the version ext_my_extension, dropping 0001 and writing a malformed version to the migration tracking table.
  • Failures say what went wrong. A configured extension that cannot be resolved now names the module it tried and states that no migrations were registered. A migrations_path that is not a string or path raises instead of being ignored; one that points at a missing directory warns.
  • Less noise. Extensions that ship no migrations directory no longer warn — most of the bundled extensions have none by design.
  • module_to_os_path reports the real condition. It raises ModuleNotFoundError rather than TypeError, and resolves namespace packages to their search location instead of returning a path named None.

How you use it

Point at a directory, or at a module and subdirectory:

config = AsyncpgConfig(
    connection_config={"dsn": "postgresql://localhost/app"},
    extension_config={
        "my_package": {
            "migrations_path": "my_package.backends.sqlspec:migrations",
            "table_name": "my_table",
        }
    },
)

The module form is resolved against the installed package, so it stays portable across machines and works in [tool.sqlspec] configuration. A plain filesystem path is also accepted, resolved relative to the working directory like script_location.

Packages that register at runtime can call the API instead of declaring the key:

config.add_extension_migrations(
    "my_package",
    Path(__file__).parent / "migrations",
    settings={"table_name": "my_table"},
)

Both forms record the extension and opt it into the migration run. Call add_extension_migrations before get_migration_commands().

Migrations are versioned under an ext_{name}_ prefix that is written to the tracking table, so the extension name must stay stable once migrations have been applied.

Known limitation

Extension migrations written as .sql files still cannot resolve their named query when placed inside an extension's own directory, because the runner and the SQL loader derive the query name differently. The bundled extensions all use Python migrations, so this is unchanged behavior rather than a regression, but third-party packages shipping .sql migrations will hit it. Tracked separately.

https://claude.ai/code/session_01CKBtk2CVvc3nNRAx4NJ56K

Extension migrations were discovered by mapping the extension_config key
onto the hardcoded sqlspec.extensions.<name> module namespace, so a package
distributed separately from SQLSpec could not register migrations through
any public surface. Doing so logged "Extension <name> not found" and then
silently registered nothing.

Set migrations_path in an extension_config entry to point at a directory or
a '<dotted.module>:<subdir>' specification. Declaring it auto-includes the
extension, so it does not also need to appear in include_extensions;
exclude_extensions still opts back out. Packages that register at runtime
can call config.add_extension_migrations(name, path, settings) instead.

Also fixes ext_<name>_<version> parsing for extension names containing
underscores. A file such as ext_my_extension_0001_init.sql resolved to the
version ext_my_extension, dropping 0001 and recording a malformed version
in the tracking table. Three call sites now share parse_extension_stem,
which anchors the version to the first all-numeric segment.

Failures are now legible and proportionate: a malformed migrations_path
raises MigrationError, a missing directory warns, an unimportable module
warns naming the module and the consequence, and an extension that ships no
migrations directory logs at debug rather than warning - six of the bundled
extensions have none by design.

module_to_os_path raises ModuleNotFoundError rather than TypeError so
callers can catch the real condition, and resolves namespace packages to
their search location instead of returning a path named "None".

Claude-Session: https://claude.ai/code/session_01CKBtk2CVvc3nNRAx4NJ56K
@codecov-commenter

codecov-commenter commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.85714% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.21%. Comparing base (6b697d4) to head (516c247).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
sqlspec/migrations/runner.py 66.66% 1 Missing and 2 partials ⚠️
sqlspec/migrations/loaders.py 66.66% 1 Missing and 1 partial ⚠️
sqlspec/utils/module_loader.py 83.33% 1 Missing and 1 partial ⚠️
sqlspec/config.py 96.77% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #660      +/-   ##
==========================================
+ Coverage   77.18%   77.21%   +0.03%     
==========================================
  Files         475      475              
  Lines       67750    67825      +75     
  Branches     9308     9322      +14     
==========================================
+ Hits        52290    52370      +80     
+ Misses      12047    12044       -3     
+ Partials     3413     3411       -2     
Flag Coverage Δ
integration 60.78% <58.03%> (+<0.01%) ⬆️
py3.10 75.52% <92.85%> (+0.02%) ⬆️
py3.11 75.54% <92.85%> (+0.04%) ⬆️
py3.12 75.54% <92.85%> (+0.04%) ⬆️
py3.13 75.53% <92.85%> (+0.02%) ⬆️
py3.14 76.41% <92.66%> (+0.02%) ⬆️
unit 64.73% <90.17%> (+0.05%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
sqlspec/extensions/events/_store.py 95.65% <ø> (ø)
sqlspec/extensions/litestar/store.py 80.00% <ø> (ø)
sqlspec/migrations/base.py 93.36% <100.00%> (+1.15%) ⬆️
sqlspec/migrations/utils.py 82.96% <100.00%> (+3.09%) ⬆️
sqlspec/migrations/version.py 100.00% <100.00%> (ø)
sqlspec/config.py 93.23% <96.77%> (+0.52%) ⬆️
sqlspec/migrations/loaders.py 67.97% <66.66%> (ø)
sqlspec/utils/module_loader.py 80.26% <83.33%> (-0.16%) ⬇️
sqlspec/migrations/runner.py 85.26% <66.66%> (+0.53%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@cofin
cofin merged commit 611d367 into main Jul 27, 2026
20 checks passed
@cofin
cofin deleted the fix/third-party-extension-migrations branch July 27, 2026 20:26
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.

2 participants