sqlspec 0.36.0__cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ac8f31065839703b4e70__mypyc.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/__init__.py +140 -0
- sqlspec/__main__.py +12 -0
- sqlspec/__metadata__.py +14 -0
- sqlspec/_serialization.py +315 -0
- sqlspec/_typing.py +700 -0
- sqlspec/adapters/__init__.py +0 -0
- sqlspec/adapters/adbc/__init__.py +5 -0
- sqlspec/adapters/adbc/_typing.py +82 -0
- sqlspec/adapters/adbc/adk/__init__.py +5 -0
- sqlspec/adapters/adbc/adk/store.py +1273 -0
- sqlspec/adapters/adbc/config.py +295 -0
- sqlspec/adapters/adbc/core.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/adapters/adbc/core.py +735 -0
- sqlspec/adapters/adbc/data_dictionary.py +334 -0
- sqlspec/adapters/adbc/driver.py +529 -0
- sqlspec/adapters/adbc/events/__init__.py +5 -0
- sqlspec/adapters/adbc/events/store.py +285 -0
- sqlspec/adapters/adbc/litestar/__init__.py +5 -0
- sqlspec/adapters/adbc/litestar/store.py +502 -0
- sqlspec/adapters/adbc/type_converter.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/adapters/adbc/type_converter.py +140 -0
- sqlspec/adapters/aiosqlite/__init__.py +25 -0
- sqlspec/adapters/aiosqlite/_typing.py +82 -0
- sqlspec/adapters/aiosqlite/adk/__init__.py +5 -0
- sqlspec/adapters/aiosqlite/adk/store.py +818 -0
- sqlspec/adapters/aiosqlite/config.py +334 -0
- sqlspec/adapters/aiosqlite/core.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/adapters/aiosqlite/core.py +315 -0
- sqlspec/adapters/aiosqlite/data_dictionary.py +208 -0
- sqlspec/adapters/aiosqlite/driver.py +313 -0
- sqlspec/adapters/aiosqlite/events/__init__.py +5 -0
- sqlspec/adapters/aiosqlite/events/store.py +20 -0
- sqlspec/adapters/aiosqlite/litestar/__init__.py +5 -0
- sqlspec/adapters/aiosqlite/litestar/store.py +279 -0
- sqlspec/adapters/aiosqlite/pool.py +533 -0
- sqlspec/adapters/asyncmy/__init__.py +21 -0
- sqlspec/adapters/asyncmy/_typing.py +87 -0
- sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
- sqlspec/adapters/asyncmy/adk/store.py +703 -0
- sqlspec/adapters/asyncmy/config.py +302 -0
- sqlspec/adapters/asyncmy/core.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/adapters/asyncmy/core.py +360 -0
- sqlspec/adapters/asyncmy/data_dictionary.py +124 -0
- sqlspec/adapters/asyncmy/driver.py +383 -0
- sqlspec/adapters/asyncmy/events/__init__.py +5 -0
- sqlspec/adapters/asyncmy/events/store.py +104 -0
- sqlspec/adapters/asyncmy/litestar/__init__.py +5 -0
- sqlspec/adapters/asyncmy/litestar/store.py +296 -0
- sqlspec/adapters/asyncpg/__init__.py +19 -0
- sqlspec/adapters/asyncpg/_typing.py +88 -0
- sqlspec/adapters/asyncpg/adk/__init__.py +5 -0
- sqlspec/adapters/asyncpg/adk/store.py +748 -0
- sqlspec/adapters/asyncpg/config.py +569 -0
- sqlspec/adapters/asyncpg/core.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/adapters/asyncpg/core.py +367 -0
- sqlspec/adapters/asyncpg/data_dictionary.py +162 -0
- sqlspec/adapters/asyncpg/driver.py +487 -0
- sqlspec/adapters/asyncpg/events/__init__.py +6 -0
- sqlspec/adapters/asyncpg/events/backend.py +286 -0
- sqlspec/adapters/asyncpg/events/store.py +40 -0
- sqlspec/adapters/asyncpg/litestar/__init__.py +5 -0
- sqlspec/adapters/asyncpg/litestar/store.py +251 -0
- sqlspec/adapters/bigquery/__init__.py +14 -0
- sqlspec/adapters/bigquery/_typing.py +86 -0
- sqlspec/adapters/bigquery/adk/__init__.py +5 -0
- sqlspec/adapters/bigquery/adk/store.py +827 -0
- sqlspec/adapters/bigquery/config.py +353 -0
- sqlspec/adapters/bigquery/core.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/adapters/bigquery/core.py +715 -0
- sqlspec/adapters/bigquery/data_dictionary.py +128 -0
- sqlspec/adapters/bigquery/driver.py +548 -0
- sqlspec/adapters/bigquery/events/__init__.py +5 -0
- sqlspec/adapters/bigquery/events/store.py +139 -0
- sqlspec/adapters/bigquery/litestar/__init__.py +5 -0
- sqlspec/adapters/bigquery/litestar/store.py +325 -0
- sqlspec/adapters/bigquery/type_converter.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/adapters/bigquery/type_converter.py +107 -0
- sqlspec/adapters/cockroach_asyncpg/__init__.py +24 -0
- sqlspec/adapters/cockroach_asyncpg/_typing.py +72 -0
- sqlspec/adapters/cockroach_asyncpg/adk/__init__.py +3 -0
- sqlspec/adapters/cockroach_asyncpg/adk/store.py +410 -0
- sqlspec/adapters/cockroach_asyncpg/config.py +238 -0
- sqlspec/adapters/cockroach_asyncpg/core.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/adapters/cockroach_asyncpg/core.py +55 -0
- sqlspec/adapters/cockroach_asyncpg/data_dictionary.py +107 -0
- sqlspec/adapters/cockroach_asyncpg/driver.py +144 -0
- sqlspec/adapters/cockroach_asyncpg/events/__init__.py +3 -0
- sqlspec/adapters/cockroach_asyncpg/events/store.py +20 -0
- sqlspec/adapters/cockroach_asyncpg/litestar/__init__.py +3 -0
- sqlspec/adapters/cockroach_asyncpg/litestar/store.py +142 -0
- sqlspec/adapters/cockroach_psycopg/__init__.py +38 -0
- sqlspec/adapters/cockroach_psycopg/_typing.py +129 -0
- sqlspec/adapters/cockroach_psycopg/adk/__init__.py +13 -0
- sqlspec/adapters/cockroach_psycopg/adk/store.py +868 -0
- sqlspec/adapters/cockroach_psycopg/config.py +484 -0
- sqlspec/adapters/cockroach_psycopg/core.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/adapters/cockroach_psycopg/core.py +63 -0
- sqlspec/adapters/cockroach_psycopg/data_dictionary.py +215 -0
- sqlspec/adapters/cockroach_psycopg/driver.py +284 -0
- sqlspec/adapters/cockroach_psycopg/events/__init__.py +6 -0
- sqlspec/adapters/cockroach_psycopg/events/store.py +34 -0
- sqlspec/adapters/cockroach_psycopg/litestar/__init__.py +3 -0
- sqlspec/adapters/cockroach_psycopg/litestar/store.py +325 -0
- sqlspec/adapters/duckdb/__init__.py +25 -0
- sqlspec/adapters/duckdb/_typing.py +81 -0
- sqlspec/adapters/duckdb/adk/__init__.py +14 -0
- sqlspec/adapters/duckdb/adk/store.py +850 -0
- sqlspec/adapters/duckdb/config.py +463 -0
- sqlspec/adapters/duckdb/core.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/adapters/duckdb/core.py +257 -0
- sqlspec/adapters/duckdb/data_dictionary.py +140 -0
- sqlspec/adapters/duckdb/driver.py +430 -0
- sqlspec/adapters/duckdb/events/__init__.py +5 -0
- sqlspec/adapters/duckdb/events/store.py +57 -0
- sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
- sqlspec/adapters/duckdb/litestar/store.py +330 -0
- sqlspec/adapters/duckdb/pool.py +293 -0
- sqlspec/adapters/duckdb/type_converter.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/adapters/duckdb/type_converter.py +118 -0
- sqlspec/adapters/mock/__init__.py +72 -0
- sqlspec/adapters/mock/_typing.py +147 -0
- sqlspec/adapters/mock/config.py +483 -0
- sqlspec/adapters/mock/core.py +319 -0
- sqlspec/adapters/mock/data_dictionary.py +366 -0
- sqlspec/adapters/mock/driver.py +721 -0
- sqlspec/adapters/mysqlconnector/__init__.py +36 -0
- sqlspec/adapters/mysqlconnector/_typing.py +141 -0
- sqlspec/adapters/mysqlconnector/adk/__init__.py +15 -0
- sqlspec/adapters/mysqlconnector/adk/store.py +1060 -0
- sqlspec/adapters/mysqlconnector/config.py +394 -0
- sqlspec/adapters/mysqlconnector/core.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/adapters/mysqlconnector/core.py +303 -0
- sqlspec/adapters/mysqlconnector/data_dictionary.py +235 -0
- sqlspec/adapters/mysqlconnector/driver.py +483 -0
- sqlspec/adapters/mysqlconnector/events/__init__.py +8 -0
- sqlspec/adapters/mysqlconnector/events/store.py +98 -0
- sqlspec/adapters/mysqlconnector/litestar/__init__.py +5 -0
- sqlspec/adapters/mysqlconnector/litestar/store.py +426 -0
- sqlspec/adapters/oracledb/__init__.py +60 -0
- sqlspec/adapters/oracledb/_numpy_handlers.py +141 -0
- sqlspec/adapters/oracledb/_typing.py +182 -0
- sqlspec/adapters/oracledb/_uuid_handlers.py +166 -0
- sqlspec/adapters/oracledb/adk/__init__.py +10 -0
- sqlspec/adapters/oracledb/adk/store.py +2369 -0
- sqlspec/adapters/oracledb/config.py +550 -0
- sqlspec/adapters/oracledb/core.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/adapters/oracledb/core.py +543 -0
- sqlspec/adapters/oracledb/data_dictionary.py +536 -0
- sqlspec/adapters/oracledb/driver.py +1229 -0
- sqlspec/adapters/oracledb/events/__init__.py +16 -0
- sqlspec/adapters/oracledb/events/backend.py +347 -0
- sqlspec/adapters/oracledb/events/store.py +420 -0
- sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
- sqlspec/adapters/oracledb/litestar/store.py +781 -0
- sqlspec/adapters/oracledb/migrations.py +535 -0
- sqlspec/adapters/oracledb/type_converter.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/adapters/oracledb/type_converter.py +211 -0
- sqlspec/adapters/psqlpy/__init__.py +17 -0
- sqlspec/adapters/psqlpy/_typing.py +79 -0
- sqlspec/adapters/psqlpy/adk/__init__.py +5 -0
- sqlspec/adapters/psqlpy/adk/store.py +766 -0
- sqlspec/adapters/psqlpy/config.py +304 -0
- sqlspec/adapters/psqlpy/core.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/adapters/psqlpy/core.py +480 -0
- sqlspec/adapters/psqlpy/data_dictionary.py +126 -0
- sqlspec/adapters/psqlpy/driver.py +438 -0
- sqlspec/adapters/psqlpy/events/__init__.py +6 -0
- sqlspec/adapters/psqlpy/events/backend.py +310 -0
- sqlspec/adapters/psqlpy/events/store.py +20 -0
- sqlspec/adapters/psqlpy/litestar/__init__.py +5 -0
- sqlspec/adapters/psqlpy/litestar/store.py +270 -0
- sqlspec/adapters/psqlpy/type_converter.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/adapters/psqlpy/type_converter.py +113 -0
- sqlspec/adapters/psycopg/__init__.py +32 -0
- sqlspec/adapters/psycopg/_typing.py +164 -0
- sqlspec/adapters/psycopg/adk/__init__.py +10 -0
- sqlspec/adapters/psycopg/adk/store.py +1387 -0
- sqlspec/adapters/psycopg/config.py +576 -0
- sqlspec/adapters/psycopg/core.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/adapters/psycopg/core.py +450 -0
- sqlspec/adapters/psycopg/data_dictionary.py +289 -0
- sqlspec/adapters/psycopg/driver.py +975 -0
- sqlspec/adapters/psycopg/events/__init__.py +20 -0
- sqlspec/adapters/psycopg/events/backend.py +458 -0
- sqlspec/adapters/psycopg/events/store.py +42 -0
- sqlspec/adapters/psycopg/litestar/__init__.py +5 -0
- sqlspec/adapters/psycopg/litestar/store.py +552 -0
- sqlspec/adapters/psycopg/type_converter.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/adapters/psycopg/type_converter.py +93 -0
- sqlspec/adapters/pymysql/__init__.py +21 -0
- sqlspec/adapters/pymysql/_typing.py +71 -0
- sqlspec/adapters/pymysql/adk/__init__.py +5 -0
- sqlspec/adapters/pymysql/adk/store.py +540 -0
- sqlspec/adapters/pymysql/config.py +195 -0
- sqlspec/adapters/pymysql/core.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/adapters/pymysql/core.py +299 -0
- sqlspec/adapters/pymysql/data_dictionary.py +122 -0
- sqlspec/adapters/pymysql/driver.py +259 -0
- sqlspec/adapters/pymysql/events/__init__.py +5 -0
- sqlspec/adapters/pymysql/events/store.py +50 -0
- sqlspec/adapters/pymysql/litestar/__init__.py +5 -0
- sqlspec/adapters/pymysql/litestar/store.py +232 -0
- sqlspec/adapters/pymysql/pool.py +137 -0
- sqlspec/adapters/spanner/__init__.py +40 -0
- sqlspec/adapters/spanner/_typing.py +86 -0
- sqlspec/adapters/spanner/adk/__init__.py +5 -0
- sqlspec/adapters/spanner/adk/store.py +732 -0
- sqlspec/adapters/spanner/config.py +352 -0
- sqlspec/adapters/spanner/core.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/adapters/spanner/core.py +188 -0
- sqlspec/adapters/spanner/data_dictionary.py +120 -0
- sqlspec/adapters/spanner/dialect/__init__.py +6 -0
- sqlspec/adapters/spanner/dialect/_spangres.py +57 -0
- sqlspec/adapters/spanner/dialect/_spanner.py +130 -0
- sqlspec/adapters/spanner/driver.py +373 -0
- sqlspec/adapters/spanner/events/__init__.py +5 -0
- sqlspec/adapters/spanner/events/store.py +187 -0
- sqlspec/adapters/spanner/litestar/__init__.py +5 -0
- sqlspec/adapters/spanner/litestar/store.py +291 -0
- sqlspec/adapters/spanner/type_converter.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/adapters/spanner/type_converter.py +331 -0
- sqlspec/adapters/sqlite/__init__.py +19 -0
- sqlspec/adapters/sqlite/_typing.py +80 -0
- sqlspec/adapters/sqlite/adk/__init__.py +5 -0
- sqlspec/adapters/sqlite/adk/store.py +958 -0
- sqlspec/adapters/sqlite/config.py +280 -0
- sqlspec/adapters/sqlite/core.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/adapters/sqlite/core.py +312 -0
- sqlspec/adapters/sqlite/data_dictionary.py +202 -0
- sqlspec/adapters/sqlite/driver.py +359 -0
- sqlspec/adapters/sqlite/events/__init__.py +5 -0
- sqlspec/adapters/sqlite/events/store.py +20 -0
- sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
- sqlspec/adapters/sqlite/litestar/store.py +316 -0
- sqlspec/adapters/sqlite/pool.py +198 -0
- sqlspec/adapters/sqlite/type_converter.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/adapters/sqlite/type_converter.py +114 -0
- sqlspec/base.py +747 -0
- sqlspec/builder/__init__.py +179 -0
- sqlspec/builder/_base.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/builder/_base.py +1022 -0
- sqlspec/builder/_column.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/builder/_column.py +521 -0
- sqlspec/builder/_ddl.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/builder/_ddl.py +1642 -0
- sqlspec/builder/_delete.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/builder/_delete.py +95 -0
- sqlspec/builder/_dml.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/builder/_dml.py +365 -0
- sqlspec/builder/_explain.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/builder/_explain.py +579 -0
- sqlspec/builder/_expression_wrappers.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/builder/_expression_wrappers.py +46 -0
- sqlspec/builder/_factory.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/builder/_factory.py +1697 -0
- sqlspec/builder/_insert.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/builder/_insert.py +328 -0
- sqlspec/builder/_join.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/builder/_join.py +499 -0
- sqlspec/builder/_merge.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/builder/_merge.py +821 -0
- sqlspec/builder/_parsing_utils.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/builder/_parsing_utils.py +297 -0
- sqlspec/builder/_select.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/builder/_select.py +1660 -0
- sqlspec/builder/_temporal.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/builder/_temporal.py +139 -0
- sqlspec/builder/_update.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/builder/_update.py +173 -0
- sqlspec/builder/_vector_expressions.py +267 -0
- sqlspec/cli.py +911 -0
- sqlspec/config.py +1755 -0
- sqlspec/core/__init__.py +374 -0
- sqlspec/core/_correlation.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/_correlation.py +176 -0
- sqlspec/core/cache.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/cache.py +1069 -0
- sqlspec/core/compiler.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/compiler.py +954 -0
- sqlspec/core/explain.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/explain.py +275 -0
- sqlspec/core/filters.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/filters.py +952 -0
- sqlspec/core/hashing.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/hashing.py +262 -0
- sqlspec/core/metrics.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/metrics.py +83 -0
- sqlspec/core/parameters/__init__.py +71 -0
- sqlspec/core/parameters/_alignment.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/parameters/_alignment.py +270 -0
- sqlspec/core/parameters/_converter.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/parameters/_converter.py +543 -0
- sqlspec/core/parameters/_processor.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/parameters/_processor.py +505 -0
- sqlspec/core/parameters/_registry.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/parameters/_registry.py +206 -0
- sqlspec/core/parameters/_transformers.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/parameters/_transformers.py +292 -0
- sqlspec/core/parameters/_types.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/parameters/_types.py +499 -0
- sqlspec/core/parameters/_validator.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/parameters/_validator.py +180 -0
- sqlspec/core/pipeline.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/pipeline.py +319 -0
- sqlspec/core/query_modifiers.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/query_modifiers.py +437 -0
- sqlspec/core/result/__init__.py +23 -0
- sqlspec/core/result/_base.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/result/_base.py +1121 -0
- sqlspec/core/result/_io.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/result/_io.py +28 -0
- sqlspec/core/splitter.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/splitter.py +966 -0
- sqlspec/core/stack.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/stack.py +163 -0
- sqlspec/core/statement.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/statement.py +1503 -0
- sqlspec/core/type_converter.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/type_converter.py +339 -0
- sqlspec/data_dictionary/__init__.py +22 -0
- sqlspec/data_dictionary/_loader.py +123 -0
- sqlspec/data_dictionary/_registry.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/data_dictionary/_registry.py +74 -0
- sqlspec/data_dictionary/_types.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/data_dictionary/_types.py +121 -0
- sqlspec/data_dictionary/dialects/__init__.py +21 -0
- sqlspec/data_dictionary/dialects/bigquery.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/data_dictionary/dialects/bigquery.py +49 -0
- sqlspec/data_dictionary/dialects/cockroachdb.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/data_dictionary/dialects/cockroachdb.py +43 -0
- sqlspec/data_dictionary/dialects/duckdb.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/data_dictionary/dialects/duckdb.py +47 -0
- sqlspec/data_dictionary/dialects/mysql.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/data_dictionary/dialects/mysql.py +42 -0
- sqlspec/data_dictionary/dialects/oracle.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/data_dictionary/dialects/oracle.py +34 -0
- sqlspec/data_dictionary/dialects/postgres.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/data_dictionary/dialects/postgres.py +46 -0
- sqlspec/data_dictionary/dialects/spanner.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/data_dictionary/dialects/spanner.py +37 -0
- sqlspec/data_dictionary/dialects/sqlite.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/data_dictionary/dialects/sqlite.py +42 -0
- sqlspec/data_dictionary/sql/.gitkeep +0 -0
- sqlspec/data_dictionary/sql/bigquery/columns.sql +23 -0
- sqlspec/data_dictionary/sql/bigquery/foreign_keys.sql +34 -0
- sqlspec/data_dictionary/sql/bigquery/indexes.sql +19 -0
- sqlspec/data_dictionary/sql/bigquery/tables.sql +33 -0
- sqlspec/data_dictionary/sql/bigquery/version.sql +3 -0
- sqlspec/data_dictionary/sql/cockroachdb/columns.sql +34 -0
- sqlspec/data_dictionary/sql/cockroachdb/foreign_keys.sql +40 -0
- sqlspec/data_dictionary/sql/cockroachdb/indexes.sql +32 -0
- sqlspec/data_dictionary/sql/cockroachdb/tables.sql +44 -0
- sqlspec/data_dictionary/sql/cockroachdb/version.sql +3 -0
- sqlspec/data_dictionary/sql/duckdb/columns.sql +23 -0
- sqlspec/data_dictionary/sql/duckdb/foreign_keys.sql +36 -0
- sqlspec/data_dictionary/sql/duckdb/indexes.sql +19 -0
- sqlspec/data_dictionary/sql/duckdb/tables.sql +38 -0
- sqlspec/data_dictionary/sql/duckdb/version.sql +3 -0
- sqlspec/data_dictionary/sql/mysql/columns.sql +23 -0
- sqlspec/data_dictionary/sql/mysql/foreign_keys.sql +28 -0
- sqlspec/data_dictionary/sql/mysql/indexes.sql +26 -0
- sqlspec/data_dictionary/sql/mysql/tables.sql +33 -0
- sqlspec/data_dictionary/sql/mysql/version.sql +3 -0
- sqlspec/data_dictionary/sql/oracle/columns.sql +23 -0
- sqlspec/data_dictionary/sql/oracle/foreign_keys.sql +48 -0
- sqlspec/data_dictionary/sql/oracle/indexes.sql +44 -0
- sqlspec/data_dictionary/sql/oracle/tables.sql +25 -0
- sqlspec/data_dictionary/sql/oracle/version.sql +20 -0
- sqlspec/data_dictionary/sql/postgres/columns.sql +34 -0
- sqlspec/data_dictionary/sql/postgres/foreign_keys.sql +40 -0
- sqlspec/data_dictionary/sql/postgres/indexes.sql +56 -0
- sqlspec/data_dictionary/sql/postgres/tables.sql +44 -0
- sqlspec/data_dictionary/sql/postgres/version.sql +3 -0
- sqlspec/data_dictionary/sql/spanner/columns.sql +23 -0
- sqlspec/data_dictionary/sql/spanner/foreign_keys.sql +70 -0
- sqlspec/data_dictionary/sql/spanner/indexes.sql +30 -0
- sqlspec/data_dictionary/sql/spanner/tables.sql +9 -0
- sqlspec/data_dictionary/sql/spanner/version.sql +3 -0
- sqlspec/data_dictionary/sql/sqlite/columns.sql +23 -0
- sqlspec/data_dictionary/sql/sqlite/foreign_keys.sql +22 -0
- sqlspec/data_dictionary/sql/sqlite/indexes.sql +7 -0
- sqlspec/data_dictionary/sql/sqlite/tables.sql +28 -0
- sqlspec/data_dictionary/sql/sqlite/version.sql +3 -0
- sqlspec/driver/__init__.py +32 -0
- sqlspec/driver/_async.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/driver/_async.py +1737 -0
- sqlspec/driver/_common.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/driver/_common.py +1478 -0
- sqlspec/driver/_sql_helpers.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/driver/_sql_helpers.py +148 -0
- sqlspec/driver/_storage_helpers.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/driver/_storage_helpers.py +144 -0
- sqlspec/driver/_sync.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/driver/_sync.py +1710 -0
- sqlspec/exceptions.py +338 -0
- sqlspec/extensions/__init__.py +0 -0
- sqlspec/extensions/adk/__init__.py +70 -0
- sqlspec/extensions/adk/_types.py +51 -0
- sqlspec/extensions/adk/converters.py +172 -0
- sqlspec/extensions/adk/memory/__init__.py +69 -0
- sqlspec/extensions/adk/memory/_types.py +30 -0
- sqlspec/extensions/adk/memory/converters.py +149 -0
- sqlspec/extensions/adk/memory/service.py +217 -0
- sqlspec/extensions/adk/memory/store.py +569 -0
- sqlspec/extensions/adk/migrations/0001_create_adk_tables.py +246 -0
- sqlspec/extensions/adk/migrations/__init__.py +0 -0
- sqlspec/extensions/adk/service.py +225 -0
- sqlspec/extensions/adk/store.py +567 -0
- sqlspec/extensions/events/__init__.py +51 -0
- sqlspec/extensions/events/_channel.py +703 -0
- sqlspec/extensions/events/_hints.py +45 -0
- sqlspec/extensions/events/_models.py +23 -0
- sqlspec/extensions/events/_payload.py +69 -0
- sqlspec/extensions/events/_protocols.py +134 -0
- sqlspec/extensions/events/_queue.py +461 -0
- sqlspec/extensions/events/_store.py +209 -0
- sqlspec/extensions/events/migrations/0001_create_event_queue.py +59 -0
- sqlspec/extensions/events/migrations/__init__.py +3 -0
- sqlspec/extensions/fastapi/__init__.py +19 -0
- sqlspec/extensions/fastapi/extension.py +351 -0
- sqlspec/extensions/fastapi/providers.py +607 -0
- sqlspec/extensions/flask/__init__.py +37 -0
- sqlspec/extensions/flask/_state.py +76 -0
- sqlspec/extensions/flask/_utils.py +71 -0
- sqlspec/extensions/flask/extension.py +519 -0
- sqlspec/extensions/litestar/__init__.py +28 -0
- sqlspec/extensions/litestar/_utils.py +52 -0
- sqlspec/extensions/litestar/channels.py +165 -0
- sqlspec/extensions/litestar/cli.py +102 -0
- sqlspec/extensions/litestar/config.py +90 -0
- sqlspec/extensions/litestar/handlers.py +316 -0
- sqlspec/extensions/litestar/migrations/0001_create_session_table.py +137 -0
- sqlspec/extensions/litestar/migrations/__init__.py +3 -0
- sqlspec/extensions/litestar/plugin.py +671 -0
- sqlspec/extensions/litestar/providers.py +526 -0
- sqlspec/extensions/litestar/store.py +296 -0
- sqlspec/extensions/otel/__init__.py +58 -0
- sqlspec/extensions/prometheus/__init__.py +113 -0
- sqlspec/extensions/starlette/__init__.py +19 -0
- sqlspec/extensions/starlette/_state.py +30 -0
- sqlspec/extensions/starlette/_utils.py +96 -0
- sqlspec/extensions/starlette/extension.py +346 -0
- sqlspec/extensions/starlette/middleware.py +235 -0
- sqlspec/loader.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/loader.py +702 -0
- sqlspec/migrations/__init__.py +36 -0
- sqlspec/migrations/base.py +731 -0
- sqlspec/migrations/commands.py +1232 -0
- sqlspec/migrations/context.py +157 -0
- sqlspec/migrations/fix.py +204 -0
- sqlspec/migrations/loaders.py +443 -0
- sqlspec/migrations/runner.py +1172 -0
- sqlspec/migrations/templates.py +234 -0
- sqlspec/migrations/tracker.py +611 -0
- sqlspec/migrations/utils.py +256 -0
- sqlspec/migrations/validation.py +207 -0
- sqlspec/migrations/version.py +446 -0
- sqlspec/observability/__init__.py +55 -0
- sqlspec/observability/_common.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/observability/_common.py +77 -0
- sqlspec/observability/_config.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/observability/_config.py +348 -0
- sqlspec/observability/_diagnostics.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/observability/_diagnostics.py +74 -0
- sqlspec/observability/_dispatcher.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/observability/_dispatcher.py +152 -0
- sqlspec/observability/_formatters/__init__.py +13 -0
- sqlspec/observability/_formatters/_aws.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/observability/_formatters/_aws.py +102 -0
- sqlspec/observability/_formatters/_azure.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/observability/_formatters/_azure.py +96 -0
- sqlspec/observability/_formatters/_base.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/observability/_formatters/_base.py +57 -0
- sqlspec/observability/_formatters/_gcp.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/observability/_formatters/_gcp.py +131 -0
- sqlspec/observability/_formatting.py +58 -0
- sqlspec/observability/_observer.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/observability/_observer.py +357 -0
- sqlspec/observability/_runtime.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/observability/_runtime.py +420 -0
- sqlspec/observability/_sampling.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/observability/_sampling.py +188 -0
- sqlspec/observability/_spans.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/observability/_spans.py +161 -0
- sqlspec/protocols.py +916 -0
- sqlspec/py.typed +0 -0
- sqlspec/storage/__init__.py +48 -0
- sqlspec/storage/_utils.py +104 -0
- sqlspec/storage/backends/__init__.py +1 -0
- sqlspec/storage/backends/base.py +253 -0
- sqlspec/storage/backends/fsspec.py +529 -0
- sqlspec/storage/backends/local.py +441 -0
- sqlspec/storage/backends/obstore.py +916 -0
- sqlspec/storage/errors.py +104 -0
- sqlspec/storage/pipeline.py +582 -0
- sqlspec/storage/registry.py +301 -0
- sqlspec/typing.py +395 -0
- sqlspec/utils/__init__.py +7 -0
- sqlspec/utils/arrow_helpers.py +318 -0
- sqlspec/utils/config_tools.py +332 -0
- sqlspec/utils/correlation.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/correlation.py +134 -0
- sqlspec/utils/deprecation.py +190 -0
- sqlspec/utils/fixtures.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/fixtures.py +258 -0
- sqlspec/utils/logging.py +222 -0
- sqlspec/utils/module_loader.py +306 -0
- sqlspec/utils/portal.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/portal.py +375 -0
- sqlspec/utils/schema.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/schema.py +485 -0
- sqlspec/utils/serializers.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/serializers.py +408 -0
- sqlspec/utils/singleton.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/singleton.py +41 -0
- sqlspec/utils/sync_tools.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/sync_tools.py +311 -0
- sqlspec/utils/text.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/text.py +108 -0
- sqlspec/utils/type_converters.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/type_converters.py +128 -0
- sqlspec/utils/type_guards.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/type_guards.py +1360 -0
- sqlspec/utils/uuids.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/uuids.py +225 -0
- sqlspec-0.36.0.dist-info/METADATA +205 -0
- sqlspec-0.36.0.dist-info/RECORD +531 -0
- sqlspec-0.36.0.dist-info/WHEEL +7 -0
- sqlspec-0.36.0.dist-info/entry_points.txt +2 -0
- sqlspec-0.36.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,1232 @@
|
|
|
1
|
+
"""Migration command implementations for SQLSpec.
|
|
2
|
+
|
|
3
|
+
This module provides the main command interface for database migrations.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import functools
|
|
7
|
+
import inspect
|
|
8
|
+
import logging
|
|
9
|
+
import time
|
|
10
|
+
from collections.abc import Awaitable, Callable
|
|
11
|
+
from typing import TYPE_CHECKING, Any, ParamSpec, TypeVar, cast
|
|
12
|
+
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
from rich.table import Table
|
|
15
|
+
|
|
16
|
+
from sqlspec.builder import sql
|
|
17
|
+
from sqlspec.migrations.base import BaseMigrationCommands
|
|
18
|
+
from sqlspec.migrations.context import MigrationContext
|
|
19
|
+
from sqlspec.migrations.fix import MigrationFixer
|
|
20
|
+
from sqlspec.migrations.runner import AsyncMigrationRunner, SyncMigrationRunner
|
|
21
|
+
from sqlspec.migrations.utils import create_migration_file
|
|
22
|
+
from sqlspec.migrations.validation import validate_migration_order
|
|
23
|
+
from sqlspec.migrations.version import generate_conversion_map, generate_timestamp_version, parse_version
|
|
24
|
+
from sqlspec.observability import resolve_db_system
|
|
25
|
+
from sqlspec.utils.logging import get_logger, log_with_context
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
from sqlspec.config import AsyncConfigT, SyncConfigT
|
|
31
|
+
|
|
32
|
+
__all__ = ("AsyncMigrationCommands", "SyncMigrationCommands", "create_migration_commands")
|
|
33
|
+
|
|
34
|
+
logger = get_logger("sqlspec.migrations.commands")
|
|
35
|
+
console = Console()
|
|
36
|
+
P = ParamSpec("P")
|
|
37
|
+
R = TypeVar("R")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
MetadataBuilder = Callable[[dict[str, Any]], tuple[str | None, dict[str, Any]]]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _bind_arguments(signature: inspect.Signature, args: tuple[Any, ...], kwargs: dict[str, Any]) -> dict[str, Any]:
|
|
44
|
+
bound = signature.bind_partial(*args, **kwargs)
|
|
45
|
+
arguments = dict(bound.arguments)
|
|
46
|
+
arguments.pop("self", None)
|
|
47
|
+
return arguments
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _with_command_span(
|
|
51
|
+
event: str, metadata_fn: "MetadataBuilder | None" = None, *, dry_run_param: str | None = "dry_run"
|
|
52
|
+
) -> Callable[[Callable[P, R]], Callable[P, R]]:
|
|
53
|
+
"""Attach span lifecycle and command metric management to command methods."""
|
|
54
|
+
|
|
55
|
+
metric_prefix = f"migrations.command.{event}"
|
|
56
|
+
|
|
57
|
+
def decorator(func: Callable[P, R]) -> Callable[P, R]:
|
|
58
|
+
signature = inspect.signature(func)
|
|
59
|
+
|
|
60
|
+
def _prepare(self: Any, args: tuple[Any, ...], kwargs: dict[str, Any]) -> tuple[Any, bool, Any]:
|
|
61
|
+
runtime = self._runtime
|
|
62
|
+
metadata_args = _bind_arguments(signature, args, kwargs)
|
|
63
|
+
dry_run = False
|
|
64
|
+
if dry_run_param is not None:
|
|
65
|
+
dry_run = bool(metadata_args.get(dry_run_param, False))
|
|
66
|
+
metadata: dict[str, Any] | None = None
|
|
67
|
+
version: str | None = None
|
|
68
|
+
span = None
|
|
69
|
+
if runtime is not None:
|
|
70
|
+
runtime.increment_metric(f"{metric_prefix}.invocations")
|
|
71
|
+
if dry_run_param is not None and dry_run:
|
|
72
|
+
runtime.increment_metric(f"{metric_prefix}.dry_run")
|
|
73
|
+
if metadata_fn is not None:
|
|
74
|
+
version, metadata = metadata_fn(metadata_args)
|
|
75
|
+
span = runtime.start_migration_span(f"command.{event}", version=version, metadata=metadata)
|
|
76
|
+
return runtime, dry_run, span
|
|
77
|
+
|
|
78
|
+
def _finalize(
|
|
79
|
+
self: Any,
|
|
80
|
+
runtime: Any,
|
|
81
|
+
span: Any,
|
|
82
|
+
start: float,
|
|
83
|
+
error: "Exception | None",
|
|
84
|
+
recorded_error: bool,
|
|
85
|
+
dry_run: bool,
|
|
86
|
+
) -> None:
|
|
87
|
+
command_error = self._last_command_error
|
|
88
|
+
self._last_command_error = None
|
|
89
|
+
command_metrics = self._last_command_metrics
|
|
90
|
+
self._last_command_metrics = None
|
|
91
|
+
if runtime is None:
|
|
92
|
+
return
|
|
93
|
+
if command_error is not None and not recorded_error:
|
|
94
|
+
runtime.increment_metric(f"{metric_prefix}.errors")
|
|
95
|
+
if not dry_run and command_metrics:
|
|
96
|
+
for metric, value in command_metrics.items():
|
|
97
|
+
runtime.increment_metric(f"{metric_prefix}.{metric}", value)
|
|
98
|
+
duration_ms = int((time.perf_counter() - start) * 1000)
|
|
99
|
+
runtime.end_migration_span(span, duration_ms=duration_ms, error=error or command_error)
|
|
100
|
+
|
|
101
|
+
if inspect.iscoroutinefunction(func):
|
|
102
|
+
|
|
103
|
+
@functools.wraps(func)
|
|
104
|
+
async def async_wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
105
|
+
self = args[0]
|
|
106
|
+
runtime, dry_run, span = _prepare(self, args, kwargs)
|
|
107
|
+
start = time.perf_counter()
|
|
108
|
+
error: Exception | None = None
|
|
109
|
+
error_recorded = False
|
|
110
|
+
try:
|
|
111
|
+
async_func = cast("Callable[P, Awaitable[R]]", func)
|
|
112
|
+
return await async_func(*args, **kwargs)
|
|
113
|
+
except Exception as exc: # pragma: no cover - passthrough
|
|
114
|
+
error = exc
|
|
115
|
+
if runtime is not None:
|
|
116
|
+
runtime.increment_metric(f"{metric_prefix}.errors")
|
|
117
|
+
error_recorded = True
|
|
118
|
+
raise
|
|
119
|
+
finally:
|
|
120
|
+
_finalize(self, runtime, span, start, error, error_recorded, dry_run)
|
|
121
|
+
|
|
122
|
+
return cast("Callable[P, R]", async_wrapper)
|
|
123
|
+
|
|
124
|
+
@functools.wraps(func)
|
|
125
|
+
def sync_wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
126
|
+
self = args[0]
|
|
127
|
+
runtime, dry_run, span = _prepare(self, args, kwargs)
|
|
128
|
+
start = time.perf_counter()
|
|
129
|
+
error: Exception | None = None
|
|
130
|
+
error_recorded = False
|
|
131
|
+
try:
|
|
132
|
+
return func(*args, **kwargs)
|
|
133
|
+
except Exception as exc: # pragma: no cover - passthrough
|
|
134
|
+
error = exc
|
|
135
|
+
if runtime is not None:
|
|
136
|
+
runtime.increment_metric(f"{metric_prefix}.errors")
|
|
137
|
+
error_recorded = True
|
|
138
|
+
raise
|
|
139
|
+
finally:
|
|
140
|
+
_finalize(self, runtime, span, start, error, error_recorded, dry_run)
|
|
141
|
+
|
|
142
|
+
return cast("Callable[P, R]", sync_wrapper)
|
|
143
|
+
|
|
144
|
+
return decorator
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _upgrade_metadata(args: dict[str, Any]) -> tuple[str | None, dict[str, Any]]:
|
|
148
|
+
revision = cast("str | None", args.get("revision"))
|
|
149
|
+
metadata = {"dry_run": str(args.get("dry_run", False)).lower()}
|
|
150
|
+
return revision, metadata
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _downgrade_metadata(args: dict[str, Any]) -> tuple[str | None, dict[str, Any]]:
|
|
154
|
+
revision = cast("str | None", args.get("revision"))
|
|
155
|
+
metadata = {"dry_run": str(args.get("dry_run", False)).lower()}
|
|
156
|
+
return revision, metadata
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class SyncMigrationCommands(BaseMigrationCommands["SyncConfigT", Any]):
|
|
160
|
+
"""Synchronous migration commands."""
|
|
161
|
+
|
|
162
|
+
def __init__(self, config: "SyncConfigT") -> None:
|
|
163
|
+
"""Initialize migration commands.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
config: The SQLSpec configuration.
|
|
167
|
+
"""
|
|
168
|
+
super().__init__(config)
|
|
169
|
+
self.tracker = config.migration_tracker_type(self.version_table)
|
|
170
|
+
|
|
171
|
+
# Create context with extension configurations
|
|
172
|
+
context = MigrationContext.from_config(config)
|
|
173
|
+
context.extension_config = self.extension_configs
|
|
174
|
+
|
|
175
|
+
self.runner = SyncMigrationRunner(
|
|
176
|
+
self.migrations_path,
|
|
177
|
+
self._discover_extension_migrations(),
|
|
178
|
+
context,
|
|
179
|
+
self.extension_configs,
|
|
180
|
+
runtime=self._runtime,
|
|
181
|
+
description_hints=self._template_settings.description_hints,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
def init(self, directory: str, package: bool = True) -> None:
|
|
185
|
+
"""Initialize migration directory structure.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
directory: Directory to initialize migrations in.
|
|
189
|
+
package: Whether to create __init__.py file.
|
|
190
|
+
"""
|
|
191
|
+
self.init_directory(directory, package)
|
|
192
|
+
|
|
193
|
+
def current(self, verbose: bool = False) -> "str | None":
|
|
194
|
+
"""Show current migration version.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
verbose: Whether to show detailed migration history.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
The current migration version or None if no migrations applied.
|
|
201
|
+
"""
|
|
202
|
+
with self.config.provide_session() as driver:
|
|
203
|
+
self.tracker.ensure_tracking_table(driver)
|
|
204
|
+
|
|
205
|
+
current = self.tracker.get_current_version(driver)
|
|
206
|
+
if not current:
|
|
207
|
+
log_with_context(
|
|
208
|
+
logger,
|
|
209
|
+
logging.DEBUG,
|
|
210
|
+
"migration.list",
|
|
211
|
+
db_system=resolve_db_system(type(driver).__name__),
|
|
212
|
+
current_version=None,
|
|
213
|
+
applied_count=0,
|
|
214
|
+
verbose=verbose,
|
|
215
|
+
status="empty",
|
|
216
|
+
)
|
|
217
|
+
console.print("[yellow]No migrations applied yet[/]")
|
|
218
|
+
return None
|
|
219
|
+
|
|
220
|
+
console.print(f"[green]Current version:[/] {current}")
|
|
221
|
+
|
|
222
|
+
applied: list[dict[str, Any]] = []
|
|
223
|
+
if verbose:
|
|
224
|
+
applied = self.tracker.get_applied_migrations(driver)
|
|
225
|
+
|
|
226
|
+
table = Table(title="Applied Migrations")
|
|
227
|
+
table.add_column("Version", style="cyan")
|
|
228
|
+
table.add_column("Description")
|
|
229
|
+
table.add_column("Applied At")
|
|
230
|
+
table.add_column("Time (ms)", justify="right")
|
|
231
|
+
table.add_column("Applied By")
|
|
232
|
+
|
|
233
|
+
for migration in applied:
|
|
234
|
+
table.add_row(
|
|
235
|
+
migration["version_num"],
|
|
236
|
+
migration.get("description", ""),
|
|
237
|
+
str(migration.get("applied_at", "")),
|
|
238
|
+
str(migration.get("execution_time_ms", "")),
|
|
239
|
+
migration.get("applied_by", ""),
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
console.print(table)
|
|
243
|
+
|
|
244
|
+
applied_count = len(applied) if verbose else None
|
|
245
|
+
log_with_context(
|
|
246
|
+
logger,
|
|
247
|
+
logging.DEBUG,
|
|
248
|
+
"migration.list",
|
|
249
|
+
db_system=resolve_db_system(type(driver).__name__),
|
|
250
|
+
current_version=current,
|
|
251
|
+
applied_count=applied_count,
|
|
252
|
+
verbose=verbose,
|
|
253
|
+
status="complete",
|
|
254
|
+
)
|
|
255
|
+
return cast("str | None", current)
|
|
256
|
+
|
|
257
|
+
def _load_single_migration_checksum(self, version: str, file_path: "Path") -> "tuple[str, tuple[str, Path]] | None":
|
|
258
|
+
"""Load checksum for a single migration.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
version: Migration version.
|
|
262
|
+
file_path: Path to migration file.
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
Tuple of (version, (checksum, file_path)) or None if load fails.
|
|
266
|
+
"""
|
|
267
|
+
try:
|
|
268
|
+
migration = self.runner.load_migration(file_path, version)
|
|
269
|
+
return (version, (migration["checksum"], file_path))
|
|
270
|
+
except Exception as exc:
|
|
271
|
+
log_with_context(
|
|
272
|
+
logger,
|
|
273
|
+
logging.DEBUG,
|
|
274
|
+
"migration.list",
|
|
275
|
+
db_system=resolve_db_system(type(self.config).__name__),
|
|
276
|
+
version=version,
|
|
277
|
+
file_path=str(file_path),
|
|
278
|
+
error_type=type(exc).__name__,
|
|
279
|
+
status="failed",
|
|
280
|
+
operation="load_checksum",
|
|
281
|
+
)
|
|
282
|
+
return None
|
|
283
|
+
|
|
284
|
+
def _load_migration_checksums(self, all_migrations: "list[tuple[str, Path]]") -> "dict[str, tuple[str, Path]]":
|
|
285
|
+
"""Load checksums for all migrations.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
all_migrations: List of (version, file_path) tuples.
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
Dictionary mapping version to (checksum, file_path) tuples.
|
|
292
|
+
"""
|
|
293
|
+
file_checksums = {}
|
|
294
|
+
for version, file_path in all_migrations:
|
|
295
|
+
result = self._load_single_migration_checksum(version, file_path)
|
|
296
|
+
if result:
|
|
297
|
+
file_checksums[result[0]] = result[1]
|
|
298
|
+
return file_checksums
|
|
299
|
+
|
|
300
|
+
def _synchronize_version_records(self, driver: Any) -> int:
|
|
301
|
+
"""Synchronize database version records with migration files.
|
|
302
|
+
|
|
303
|
+
Auto-updates DB tracking when migrations have been renamed by fix command.
|
|
304
|
+
This allows developers to just run upgrade after pulling changes without
|
|
305
|
+
manually running fix.
|
|
306
|
+
|
|
307
|
+
Validates checksums match before updating to prevent incorrect matches.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
driver: Database driver instance.
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
Number of version records updated.
|
|
314
|
+
"""
|
|
315
|
+
all_migrations = self.runner.get_migration_files()
|
|
316
|
+
|
|
317
|
+
try:
|
|
318
|
+
applied_migrations = self.tracker.get_applied_migrations(driver)
|
|
319
|
+
except Exception as exc:
|
|
320
|
+
log_with_context(
|
|
321
|
+
logger,
|
|
322
|
+
logging.DEBUG,
|
|
323
|
+
"migration.list",
|
|
324
|
+
db_system=resolve_db_system(type(driver).__name__),
|
|
325
|
+
error_type=type(exc).__name__,
|
|
326
|
+
status="failed",
|
|
327
|
+
operation="applied_fetch",
|
|
328
|
+
)
|
|
329
|
+
return 0
|
|
330
|
+
|
|
331
|
+
applied_map = {m["version_num"]: m for m in applied_migrations}
|
|
332
|
+
|
|
333
|
+
conversion_map = generate_conversion_map(all_migrations)
|
|
334
|
+
|
|
335
|
+
updated_count = 0
|
|
336
|
+
if conversion_map:
|
|
337
|
+
for old_version, new_version in conversion_map.items():
|
|
338
|
+
if old_version in applied_map and new_version not in applied_map:
|
|
339
|
+
applied_checksum = applied_map[old_version]["checksum"]
|
|
340
|
+
|
|
341
|
+
file_path = next((path for v, path in all_migrations if v == new_version), None)
|
|
342
|
+
if file_path:
|
|
343
|
+
migration = self.runner.load_migration(file_path, new_version)
|
|
344
|
+
if migration["checksum"] == applied_checksum:
|
|
345
|
+
self.tracker.update_version_record(driver, old_version, new_version)
|
|
346
|
+
console.print(f" [dim]Reconciled version:[/] {old_version} → {new_version}")
|
|
347
|
+
updated_count += 1
|
|
348
|
+
else:
|
|
349
|
+
console.print(
|
|
350
|
+
f" [yellow]Warning: Checksum mismatch for {old_version} → {new_version}, skipping auto-sync[/]"
|
|
351
|
+
)
|
|
352
|
+
else:
|
|
353
|
+
file_checksums = self._load_migration_checksums(all_migrations)
|
|
354
|
+
|
|
355
|
+
for applied_version, applied_record in applied_map.items():
|
|
356
|
+
for file_version, (file_checksum, _) in file_checksums.items():
|
|
357
|
+
if file_version not in applied_map and applied_record["checksum"] == file_checksum:
|
|
358
|
+
self.tracker.update_version_record(driver, applied_version, file_version)
|
|
359
|
+
console.print(f" [dim]Reconciled version:[/] {applied_version} → {file_version}")
|
|
360
|
+
updated_count += 1
|
|
361
|
+
break
|
|
362
|
+
|
|
363
|
+
if updated_count > 0:
|
|
364
|
+
console.print(f"[cyan]Reconciled {updated_count} version record(s)[/]")
|
|
365
|
+
|
|
366
|
+
return updated_count
|
|
367
|
+
|
|
368
|
+
@_with_command_span("upgrade", metadata_fn=_upgrade_metadata)
|
|
369
|
+
def upgrade(
|
|
370
|
+
self, revision: str = "head", allow_missing: bool = False, auto_sync: bool = True, dry_run: bool = False
|
|
371
|
+
) -> None:
|
|
372
|
+
"""Upgrade to a target revision.
|
|
373
|
+
|
|
374
|
+
Validates migration order and warns if out-of-order migrations are detected.
|
|
375
|
+
Out-of-order migrations can occur when branches merge in different orders
|
|
376
|
+
across environments.
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
revision: Target revision or "head" for latest.
|
|
380
|
+
allow_missing: If True, allow out-of-order migrations even in strict mode.
|
|
381
|
+
Defaults to False.
|
|
382
|
+
auto_sync: If True, automatically reconcile renamed migrations in database.
|
|
383
|
+
Defaults to True. Can be disabled via --no-auto-sync flag.
|
|
384
|
+
dry_run: If True, show what would be done without making changes.
|
|
385
|
+
"""
|
|
386
|
+
runtime = self._runtime
|
|
387
|
+
applied_count = 0
|
|
388
|
+
|
|
389
|
+
if dry_run:
|
|
390
|
+
console.print("[bold yellow]DRY RUN MODE:[/] No database changes will be applied\n")
|
|
391
|
+
|
|
392
|
+
with self.config.provide_session() as driver:
|
|
393
|
+
self.tracker.ensure_tracking_table(driver)
|
|
394
|
+
|
|
395
|
+
if auto_sync:
|
|
396
|
+
config_auto_sync = self.config.migration_config.get("auto_sync", True)
|
|
397
|
+
if config_auto_sync:
|
|
398
|
+
self._synchronize_version_records(driver)
|
|
399
|
+
|
|
400
|
+
applied_migrations = self.tracker.get_applied_migrations(driver)
|
|
401
|
+
applied_versions = [m["version_num"] for m in applied_migrations]
|
|
402
|
+
applied_set = set(applied_versions)
|
|
403
|
+
|
|
404
|
+
all_migrations = self.runner.get_migration_files()
|
|
405
|
+
if runtime is not None:
|
|
406
|
+
runtime.increment_metric("migrations.command.upgrade.available", float(len(all_migrations)))
|
|
407
|
+
|
|
408
|
+
pending = []
|
|
409
|
+
for version, file_path in all_migrations:
|
|
410
|
+
if version not in applied_set:
|
|
411
|
+
if revision == "head":
|
|
412
|
+
pending.append((version, file_path))
|
|
413
|
+
else:
|
|
414
|
+
parsed_version = parse_version(version)
|
|
415
|
+
parsed_revision = parse_version(revision)
|
|
416
|
+
if parsed_version <= parsed_revision:
|
|
417
|
+
pending.append((version, file_path))
|
|
418
|
+
|
|
419
|
+
if runtime is not None:
|
|
420
|
+
runtime.increment_metric("migrations.command.upgrade.pending", float(len(pending)))
|
|
421
|
+
|
|
422
|
+
if not pending:
|
|
423
|
+
if not all_migrations:
|
|
424
|
+
console.print(
|
|
425
|
+
"[yellow]No migrations found. Create your first migration with 'sqlspec create-migration'.[/]"
|
|
426
|
+
)
|
|
427
|
+
else:
|
|
428
|
+
console.print("[green]Already at latest version[/]")
|
|
429
|
+
return
|
|
430
|
+
pending_versions = [v for v, _ in pending]
|
|
431
|
+
|
|
432
|
+
migration_config = cast("dict[str, Any]", self.config.migration_config) or {}
|
|
433
|
+
strict_ordering = migration_config.get("strict_ordering", False) and not allow_missing
|
|
434
|
+
|
|
435
|
+
validate_migration_order(pending_versions, applied_versions, strict_ordering)
|
|
436
|
+
|
|
437
|
+
console.print(f"[yellow]Found {len(pending)} pending migrations[/]")
|
|
438
|
+
|
|
439
|
+
for version, file_path in pending:
|
|
440
|
+
migration = self.runner.load_migration(file_path, version)
|
|
441
|
+
|
|
442
|
+
action_verb = "Would apply" if dry_run else "Applying"
|
|
443
|
+
console.print(f"\n[cyan]{action_verb} {version}:[/] {migration['description']}")
|
|
444
|
+
|
|
445
|
+
if dry_run:
|
|
446
|
+
console.print(f"[dim]Migration file: {file_path}[/]")
|
|
447
|
+
continue
|
|
448
|
+
|
|
449
|
+
try:
|
|
450
|
+
|
|
451
|
+
def record_version(exec_time: int, migration: "dict[str, Any]" = migration) -> None:
|
|
452
|
+
self.tracker.record_migration(
|
|
453
|
+
driver, migration["version"], migration["description"], exec_time, migration["checksum"]
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
_, execution_time = self.runner.execute_upgrade(driver, migration, on_success=record_version)
|
|
457
|
+
applied_count += 1
|
|
458
|
+
console.print(f"[green]✓ Applied in {execution_time}ms[/]")
|
|
459
|
+
|
|
460
|
+
except Exception as exc:
|
|
461
|
+
use_txn = self.runner.should_use_transaction(migration, self.config)
|
|
462
|
+
rollback_msg = " (transaction rolled back)" if use_txn else ""
|
|
463
|
+
console.print(f"[red]✗ Failed{rollback_msg}: {exc}[/]")
|
|
464
|
+
self._last_command_error = exc
|
|
465
|
+
return
|
|
466
|
+
|
|
467
|
+
if dry_run:
|
|
468
|
+
console.print("\n[bold yellow]Dry run complete.[/] No changes were made to the database.")
|
|
469
|
+
elif applied_count:
|
|
470
|
+
self._record_command_metric("applied", float(applied_count))
|
|
471
|
+
|
|
472
|
+
@_with_command_span("downgrade", metadata_fn=_downgrade_metadata)
|
|
473
|
+
def downgrade(self, revision: str = "-1", *, dry_run: bool = False) -> None:
|
|
474
|
+
"""Downgrade to a target revision.
|
|
475
|
+
|
|
476
|
+
Args:
|
|
477
|
+
revision: Target revision or "-1" for one step back.
|
|
478
|
+
dry_run: If True, show what would be done without making changes.
|
|
479
|
+
"""
|
|
480
|
+
runtime = self._runtime
|
|
481
|
+
reverted_count = 0
|
|
482
|
+
|
|
483
|
+
if dry_run:
|
|
484
|
+
console.print("[bold yellow]DRY RUN MODE:[/] No database changes will be applied\n")
|
|
485
|
+
|
|
486
|
+
with self.config.provide_session() as driver:
|
|
487
|
+
self.tracker.ensure_tracking_table(driver)
|
|
488
|
+
applied = self.tracker.get_applied_migrations(driver)
|
|
489
|
+
if runtime is not None:
|
|
490
|
+
runtime.increment_metric("migrations.command.downgrade.available", float(len(applied)))
|
|
491
|
+
if not applied:
|
|
492
|
+
console.print("[yellow]No migrations to downgrade[/]")
|
|
493
|
+
return
|
|
494
|
+
|
|
495
|
+
to_revert = []
|
|
496
|
+
if revision == "-1":
|
|
497
|
+
to_revert = [applied[-1]]
|
|
498
|
+
elif revision == "base":
|
|
499
|
+
to_revert = list(reversed(applied))
|
|
500
|
+
else:
|
|
501
|
+
parsed_revision = parse_version(revision)
|
|
502
|
+
for migration in reversed(applied):
|
|
503
|
+
parsed_migration_version = parse_version(migration["version_num"])
|
|
504
|
+
if parsed_migration_version > parsed_revision:
|
|
505
|
+
to_revert.append(migration)
|
|
506
|
+
|
|
507
|
+
if runtime is not None:
|
|
508
|
+
runtime.increment_metric("migrations.command.downgrade.pending", float(len(to_revert)))
|
|
509
|
+
|
|
510
|
+
if not to_revert:
|
|
511
|
+
console.print("[yellow]Nothing to downgrade[/]")
|
|
512
|
+
return
|
|
513
|
+
|
|
514
|
+
console.print(f"[yellow]Reverting {len(to_revert)} migrations[/]")
|
|
515
|
+
all_files = dict(self.runner.get_migration_files())
|
|
516
|
+
for migration_record in to_revert:
|
|
517
|
+
version = migration_record["version_num"]
|
|
518
|
+
if version not in all_files:
|
|
519
|
+
console.print(f"[red]Migration file not found for {version}[/]")
|
|
520
|
+
if runtime is not None:
|
|
521
|
+
runtime.increment_metric("migrations.command.downgrade.missing_files")
|
|
522
|
+
continue
|
|
523
|
+
migration = self.runner.load_migration(all_files[version], version)
|
|
524
|
+
|
|
525
|
+
action_verb = "Would revert" if dry_run else "Reverting"
|
|
526
|
+
console.print(f"\n[cyan]{action_verb} {version}:[/] {migration['description']}")
|
|
527
|
+
|
|
528
|
+
if dry_run:
|
|
529
|
+
console.print(f"[dim]Migration file: {all_files[version]}[/]")
|
|
530
|
+
continue
|
|
531
|
+
|
|
532
|
+
try:
|
|
533
|
+
|
|
534
|
+
def remove_version(exec_time: int, version: str = version) -> None:
|
|
535
|
+
self.tracker.remove_migration(driver, version)
|
|
536
|
+
|
|
537
|
+
_, execution_time = self.runner.execute_downgrade(driver, migration, on_success=remove_version)
|
|
538
|
+
reverted_count += 1
|
|
539
|
+
console.print(f"[green]✓ Reverted in {execution_time}ms[/]")
|
|
540
|
+
except Exception as exc:
|
|
541
|
+
use_txn = self.runner.should_use_transaction(migration, self.config)
|
|
542
|
+
rollback_msg = " (transaction rolled back)" if use_txn else ""
|
|
543
|
+
console.print(f"[red]✗ Failed{rollback_msg}: {exc}[/]")
|
|
544
|
+
self._last_command_error = exc
|
|
545
|
+
return
|
|
546
|
+
|
|
547
|
+
if dry_run:
|
|
548
|
+
console.print("\n[bold yellow]Dry run complete.[/] No changes were made to the database.")
|
|
549
|
+
elif reverted_count:
|
|
550
|
+
self._record_command_metric("applied", float(reverted_count))
|
|
551
|
+
|
|
552
|
+
def stamp(self, revision: str) -> None:
|
|
553
|
+
"""Mark database as being at a specific revision without running migrations.
|
|
554
|
+
|
|
555
|
+
Args:
|
|
556
|
+
revision: The revision to stamp.
|
|
557
|
+
"""
|
|
558
|
+
with self.config.provide_session() as driver:
|
|
559
|
+
self.tracker.ensure_tracking_table(driver)
|
|
560
|
+
all_migrations = dict(self.runner.get_migration_files())
|
|
561
|
+
if revision not in all_migrations:
|
|
562
|
+
console.print(f"[red]Unknown revision: {revision}[/]")
|
|
563
|
+
return
|
|
564
|
+
clear_sql = sql.delete().from_(self.tracker.version_table)
|
|
565
|
+
driver.execute(clear_sql)
|
|
566
|
+
self.tracker.record_migration(driver, revision, f"Stamped to {revision}", 0, "manual-stamp")
|
|
567
|
+
console.print(f"[green]Database stamped at revision {revision}[/]")
|
|
568
|
+
|
|
569
|
+
def revision(self, message: str, file_type: str | None = None) -> None:
|
|
570
|
+
"""Create a new migration file with timestamp-based versioning.
|
|
571
|
+
|
|
572
|
+
Generates a unique timestamp version (YYYYMMDDHHmmss format) to avoid
|
|
573
|
+
conflicts when multiple developers create migrations concurrently.
|
|
574
|
+
|
|
575
|
+
Args:
|
|
576
|
+
message: Description for the migration.
|
|
577
|
+
file_type: Type of migration file to create ('sql' or 'py').
|
|
578
|
+
"""
|
|
579
|
+
version = generate_timestamp_version()
|
|
580
|
+
selected_format = file_type or self._template_settings.default_format
|
|
581
|
+
file_path = create_migration_file(
|
|
582
|
+
self.migrations_path,
|
|
583
|
+
version,
|
|
584
|
+
message,
|
|
585
|
+
selected_format,
|
|
586
|
+
config=self.config,
|
|
587
|
+
template_settings=self._template_settings,
|
|
588
|
+
)
|
|
589
|
+
log_with_context(
|
|
590
|
+
logger,
|
|
591
|
+
logging.DEBUG,
|
|
592
|
+
"migration.create",
|
|
593
|
+
db_system=resolve_db_system(type(self.config).__name__),
|
|
594
|
+
version=version,
|
|
595
|
+
file_path=str(file_path),
|
|
596
|
+
file_type=selected_format,
|
|
597
|
+
description=message,
|
|
598
|
+
)
|
|
599
|
+
console.print(f"[green]Created migration:[/] {file_path}")
|
|
600
|
+
|
|
601
|
+
def fix(self, dry_run: bool = False, update_database: bool = True, yes: bool = False) -> None:
|
|
602
|
+
"""Convert timestamp migrations to sequential format.
|
|
603
|
+
|
|
604
|
+
Implements hybrid versioning workflow where development uses timestamps
|
|
605
|
+
and production uses sequential numbers. Creates backup before changes
|
|
606
|
+
and provides rollback on errors.
|
|
607
|
+
|
|
608
|
+
Args:
|
|
609
|
+
dry_run: Preview changes without applying.
|
|
610
|
+
update_database: Update migration records in database.
|
|
611
|
+
yes: Skip confirmation prompt.
|
|
612
|
+
|
|
613
|
+
Examples:
|
|
614
|
+
>>> commands.fix(dry_run=True) # Preview only
|
|
615
|
+
>>> commands.fix(yes=True) # Auto-approve
|
|
616
|
+
>>> commands.fix(update_database=False) # Files only
|
|
617
|
+
"""
|
|
618
|
+
all_migrations = self.runner.get_migration_files()
|
|
619
|
+
|
|
620
|
+
conversion_map = generate_conversion_map(all_migrations)
|
|
621
|
+
|
|
622
|
+
if not conversion_map:
|
|
623
|
+
console.print("[yellow]No timestamp migrations found - nothing to convert[/]")
|
|
624
|
+
return
|
|
625
|
+
|
|
626
|
+
fixer = MigrationFixer(self.migrations_path)
|
|
627
|
+
renames = fixer.plan_renames(conversion_map)
|
|
628
|
+
|
|
629
|
+
table = Table(title="Migration Conversions")
|
|
630
|
+
table.add_column("Current Version", style="cyan")
|
|
631
|
+
table.add_column("New Version", style="green")
|
|
632
|
+
table.add_column("File")
|
|
633
|
+
|
|
634
|
+
for rename in renames:
|
|
635
|
+
table.add_row(rename.old_version, rename.new_version, rename.old_path.name)
|
|
636
|
+
|
|
637
|
+
console.print(table)
|
|
638
|
+
console.print(f"\n[yellow]{len(renames)} migrations will be converted[/]")
|
|
639
|
+
|
|
640
|
+
if dry_run:
|
|
641
|
+
console.print("[yellow][Preview Mode - No changes made][/]")
|
|
642
|
+
return
|
|
643
|
+
|
|
644
|
+
if not yes:
|
|
645
|
+
response = input("\nProceed with conversion? [y/N]: ")
|
|
646
|
+
if response.lower() != "y":
|
|
647
|
+
console.print("[yellow]Conversion cancelled[/]")
|
|
648
|
+
return
|
|
649
|
+
|
|
650
|
+
try:
|
|
651
|
+
backup_path = fixer.create_backup()
|
|
652
|
+
console.print(f"[green]✓ Created backup in {backup_path.name}[/]")
|
|
653
|
+
|
|
654
|
+
fixer.apply_renames(renames)
|
|
655
|
+
for rename in renames:
|
|
656
|
+
console.print(f"[green]✓ Renamed {rename.old_path.name} → {rename.new_path.name}[/]")
|
|
657
|
+
|
|
658
|
+
if update_database:
|
|
659
|
+
with self.config.provide_session() as driver:
|
|
660
|
+
self.tracker.ensure_tracking_table(driver)
|
|
661
|
+
applied_migrations = self.tracker.get_applied_migrations(driver)
|
|
662
|
+
applied_versions = {m["version_num"] for m in applied_migrations}
|
|
663
|
+
|
|
664
|
+
updated_count = 0
|
|
665
|
+
for old_version, new_version in conversion_map.items():
|
|
666
|
+
if old_version in applied_versions:
|
|
667
|
+
self.tracker.update_version_record(driver, old_version, new_version)
|
|
668
|
+
updated_count += 1
|
|
669
|
+
|
|
670
|
+
if updated_count > 0:
|
|
671
|
+
console.print(
|
|
672
|
+
f"[green]✓ Updated {updated_count} version records in migration tracking table[/]"
|
|
673
|
+
)
|
|
674
|
+
else:
|
|
675
|
+
console.print("[green]✓ No applied migrations to update in tracking table[/]")
|
|
676
|
+
|
|
677
|
+
fixer.cleanup()
|
|
678
|
+
console.print("[green]✓ Conversion complete![/]")
|
|
679
|
+
|
|
680
|
+
except Exception as e:
|
|
681
|
+
console.print(f"[red]✗ Error: {e}[/]")
|
|
682
|
+
fixer.rollback()
|
|
683
|
+
console.print("[yellow]Restored files from backup[/]")
|
|
684
|
+
raise
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
class AsyncMigrationCommands(BaseMigrationCommands["AsyncConfigT", Any]):
|
|
688
|
+
"""Asynchronous migration commands."""
|
|
689
|
+
|
|
690
|
+
def __init__(self, config: "AsyncConfigT") -> None:
|
|
691
|
+
"""Initialize migration commands.
|
|
692
|
+
|
|
693
|
+
Args:
|
|
694
|
+
config: The SQLSpec configuration.
|
|
695
|
+
"""
|
|
696
|
+
super().__init__(config)
|
|
697
|
+
self.tracker = config.migration_tracker_type(self.version_table)
|
|
698
|
+
|
|
699
|
+
# Create context with extension configurations
|
|
700
|
+
context = MigrationContext.from_config(config)
|
|
701
|
+
context.extension_config = self.extension_configs
|
|
702
|
+
|
|
703
|
+
self.runner = AsyncMigrationRunner(
|
|
704
|
+
self.migrations_path,
|
|
705
|
+
self._discover_extension_migrations(),
|
|
706
|
+
context,
|
|
707
|
+
self.extension_configs,
|
|
708
|
+
runtime=self._runtime,
|
|
709
|
+
description_hints=self._template_settings.description_hints,
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
async def init(self, directory: str, package: bool = True) -> None:
|
|
713
|
+
"""Initialize migration directory structure.
|
|
714
|
+
|
|
715
|
+
Args:
|
|
716
|
+
directory: Directory path for migrations.
|
|
717
|
+
package: Whether to create __init__.py in the directory.
|
|
718
|
+
"""
|
|
719
|
+
self.init_directory(directory, package)
|
|
720
|
+
|
|
721
|
+
async def current(self, verbose: bool = False) -> "str | None":
|
|
722
|
+
"""Show current migration version.
|
|
723
|
+
|
|
724
|
+
Args:
|
|
725
|
+
verbose: Whether to show detailed migration history.
|
|
726
|
+
|
|
727
|
+
Returns:
|
|
728
|
+
The current migration version or None if no migrations applied.
|
|
729
|
+
"""
|
|
730
|
+
async with self.config.provide_session() as driver:
|
|
731
|
+
await self.tracker.ensure_tracking_table(driver)
|
|
732
|
+
|
|
733
|
+
current = await self.tracker.get_current_version(driver)
|
|
734
|
+
if not current:
|
|
735
|
+
log_with_context(
|
|
736
|
+
logger,
|
|
737
|
+
logging.DEBUG,
|
|
738
|
+
"migration.list",
|
|
739
|
+
db_system=resolve_db_system(type(driver).__name__),
|
|
740
|
+
current_version=None,
|
|
741
|
+
applied_count=0,
|
|
742
|
+
verbose=verbose,
|
|
743
|
+
status="empty",
|
|
744
|
+
)
|
|
745
|
+
console.print("[yellow]No migrations applied yet[/]")
|
|
746
|
+
return None
|
|
747
|
+
|
|
748
|
+
console.print(f"[green]Current version:[/] {current}")
|
|
749
|
+
applied: list[dict[str, Any]] = []
|
|
750
|
+
if verbose:
|
|
751
|
+
applied = await self.tracker.get_applied_migrations(driver)
|
|
752
|
+
table = Table(title="Applied Migrations")
|
|
753
|
+
table.add_column("Version", style="cyan")
|
|
754
|
+
table.add_column("Description")
|
|
755
|
+
table.add_column("Applied At")
|
|
756
|
+
table.add_column("Time (ms)", justify="right")
|
|
757
|
+
table.add_column("Applied By")
|
|
758
|
+
for migration in applied:
|
|
759
|
+
table.add_row(
|
|
760
|
+
migration["version_num"],
|
|
761
|
+
migration.get("description", ""),
|
|
762
|
+
str(migration.get("applied_at", "")),
|
|
763
|
+
str(migration.get("execution_time_ms", "")),
|
|
764
|
+
migration.get("applied_by", ""),
|
|
765
|
+
)
|
|
766
|
+
console.print(table)
|
|
767
|
+
|
|
768
|
+
applied_count = len(applied) if verbose else None
|
|
769
|
+
log_with_context(
|
|
770
|
+
logger,
|
|
771
|
+
logging.DEBUG,
|
|
772
|
+
"migration.list",
|
|
773
|
+
db_system=resolve_db_system(type(driver).__name__),
|
|
774
|
+
current_version=current,
|
|
775
|
+
applied_count=applied_count,
|
|
776
|
+
verbose=verbose,
|
|
777
|
+
status="complete",
|
|
778
|
+
)
|
|
779
|
+
return cast("str | None", current)
|
|
780
|
+
|
|
781
|
+
async def _load_single_migration_checksum(
|
|
782
|
+
self, version: str, file_path: "Path"
|
|
783
|
+
) -> "tuple[str, tuple[str, Path]] | None":
|
|
784
|
+
"""Load checksum for a single migration.
|
|
785
|
+
|
|
786
|
+
Args:
|
|
787
|
+
version: Migration version.
|
|
788
|
+
file_path: Path to migration file.
|
|
789
|
+
|
|
790
|
+
Returns:
|
|
791
|
+
Tuple of (version, (checksum, file_path)) or None if load fails.
|
|
792
|
+
"""
|
|
793
|
+
try:
|
|
794
|
+
migration = await self.runner.load_migration(file_path, version)
|
|
795
|
+
return (version, (migration["checksum"], file_path))
|
|
796
|
+
except Exception as exc:
|
|
797
|
+
log_with_context(
|
|
798
|
+
logger,
|
|
799
|
+
logging.DEBUG,
|
|
800
|
+
"migration.list",
|
|
801
|
+
db_system=resolve_db_system(type(self.config).__name__),
|
|
802
|
+
version=version,
|
|
803
|
+
file_path=str(file_path),
|
|
804
|
+
error_type=type(exc).__name__,
|
|
805
|
+
status="failed",
|
|
806
|
+
operation="load_checksum",
|
|
807
|
+
)
|
|
808
|
+
return None
|
|
809
|
+
|
|
810
|
+
async def _load_migration_checksums(
|
|
811
|
+
self, all_migrations: "list[tuple[str, Path]]"
|
|
812
|
+
) -> "dict[str, tuple[str, Path]]":
|
|
813
|
+
"""Load checksums for all migrations.
|
|
814
|
+
|
|
815
|
+
Args:
|
|
816
|
+
all_migrations: List of (version, file_path) tuples.
|
|
817
|
+
|
|
818
|
+
Returns:
|
|
819
|
+
Dictionary mapping version to (checksum, file_path) tuples.
|
|
820
|
+
"""
|
|
821
|
+
file_checksums = {}
|
|
822
|
+
for version, file_path in all_migrations:
|
|
823
|
+
result = await self._load_single_migration_checksum(version, file_path)
|
|
824
|
+
if result:
|
|
825
|
+
file_checksums[result[0]] = result[1]
|
|
826
|
+
return file_checksums
|
|
827
|
+
|
|
828
|
+
async def _synchronize_version_records(self, driver: Any) -> int:
|
|
829
|
+
"""Synchronize database version records with migration files.
|
|
830
|
+
|
|
831
|
+
Auto-updates DB tracking when migrations have been renamed by fix command.
|
|
832
|
+
This allows developers to just run upgrade after pulling changes without
|
|
833
|
+
manually running fix.
|
|
834
|
+
|
|
835
|
+
Validates checksums match before updating to prevent incorrect matches.
|
|
836
|
+
|
|
837
|
+
Args:
|
|
838
|
+
driver: Database driver instance.
|
|
839
|
+
|
|
840
|
+
Returns:
|
|
841
|
+
Number of version records updated.
|
|
842
|
+
"""
|
|
843
|
+
all_migrations = await self.runner.get_migration_files()
|
|
844
|
+
|
|
845
|
+
try:
|
|
846
|
+
applied_migrations = await self.tracker.get_applied_migrations(driver)
|
|
847
|
+
except Exception as exc:
|
|
848
|
+
log_with_context(
|
|
849
|
+
logger,
|
|
850
|
+
logging.DEBUG,
|
|
851
|
+
"migration.list",
|
|
852
|
+
db_system=resolve_db_system(type(driver).__name__),
|
|
853
|
+
error_type=type(exc).__name__,
|
|
854
|
+
status="failed",
|
|
855
|
+
operation="applied_fetch",
|
|
856
|
+
)
|
|
857
|
+
return 0
|
|
858
|
+
|
|
859
|
+
applied_map = {m["version_num"]: m for m in applied_migrations}
|
|
860
|
+
|
|
861
|
+
conversion_map = generate_conversion_map(all_migrations)
|
|
862
|
+
|
|
863
|
+
updated_count = 0
|
|
864
|
+
if conversion_map:
|
|
865
|
+
for old_version, new_version in conversion_map.items():
|
|
866
|
+
if old_version in applied_map and new_version not in applied_map:
|
|
867
|
+
applied_checksum = applied_map[old_version]["checksum"]
|
|
868
|
+
|
|
869
|
+
file_path = next((path for v, path in all_migrations if v == new_version), None)
|
|
870
|
+
if file_path:
|
|
871
|
+
migration = await self.runner.load_migration(file_path, new_version)
|
|
872
|
+
if migration["checksum"] == applied_checksum:
|
|
873
|
+
await self.tracker.update_version_record(driver, old_version, new_version)
|
|
874
|
+
console.print(f" [dim]Reconciled version:[/] {old_version} → {new_version}")
|
|
875
|
+
updated_count += 1
|
|
876
|
+
else:
|
|
877
|
+
console.print(
|
|
878
|
+
f" [yellow]Warning: Checksum mismatch for {old_version} → {new_version}, skipping auto-sync[/]"
|
|
879
|
+
)
|
|
880
|
+
else:
|
|
881
|
+
file_checksums = await self._load_migration_checksums(all_migrations)
|
|
882
|
+
|
|
883
|
+
for applied_version, applied_record in applied_map.items():
|
|
884
|
+
for file_version, (file_checksum, _) in file_checksums.items():
|
|
885
|
+
if file_version not in applied_map and applied_record["checksum"] == file_checksum:
|
|
886
|
+
await self.tracker.update_version_record(driver, applied_version, file_version)
|
|
887
|
+
console.print(f" [dim]Reconciled version:[/] {applied_version} → {file_version}")
|
|
888
|
+
updated_count += 1
|
|
889
|
+
break
|
|
890
|
+
|
|
891
|
+
if updated_count > 0:
|
|
892
|
+
console.print(f"[cyan]Reconciled {updated_count} version record(s)[/]")
|
|
893
|
+
|
|
894
|
+
return updated_count
|
|
895
|
+
|
|
896
|
+
@_with_command_span("upgrade", metadata_fn=_upgrade_metadata)
|
|
897
|
+
async def upgrade(
|
|
898
|
+
self, revision: str = "head", allow_missing: bool = False, auto_sync: bool = True, dry_run: bool = False
|
|
899
|
+
) -> None:
|
|
900
|
+
"""Upgrade to a target revision.
|
|
901
|
+
|
|
902
|
+
Validates migration order and warns if out-of-order migrations are detected.
|
|
903
|
+
Out-of-order migrations can occur when branches merge in different orders
|
|
904
|
+
across environments.
|
|
905
|
+
|
|
906
|
+
Args:
|
|
907
|
+
revision: Target revision or "head" for latest.
|
|
908
|
+
allow_missing: If True, allow out-of-order migrations even in strict mode.
|
|
909
|
+
Defaults to False.
|
|
910
|
+
auto_sync: If True, automatically reconcile renamed migrations in database.
|
|
911
|
+
Defaults to True. Can be disabled via --no-auto-sync flag.
|
|
912
|
+
dry_run: If True, show what would be done without making changes.
|
|
913
|
+
"""
|
|
914
|
+
runtime = self._runtime
|
|
915
|
+
applied_count = 0
|
|
916
|
+
|
|
917
|
+
if dry_run:
|
|
918
|
+
console.print("[bold yellow]DRY RUN MODE:[/] No database changes will be applied\n")
|
|
919
|
+
|
|
920
|
+
async with self.config.provide_session() as driver:
|
|
921
|
+
await self.tracker.ensure_tracking_table(driver)
|
|
922
|
+
|
|
923
|
+
if auto_sync:
|
|
924
|
+
migration_config = cast("dict[str, Any]", self.config.migration_config) or {}
|
|
925
|
+
config_auto_sync = migration_config.get("auto_sync", True)
|
|
926
|
+
if config_auto_sync:
|
|
927
|
+
await self._synchronize_version_records(driver)
|
|
928
|
+
|
|
929
|
+
applied_migrations = await self.tracker.get_applied_migrations(driver)
|
|
930
|
+
applied_versions = [m["version_num"] for m in applied_migrations]
|
|
931
|
+
applied_set = set(applied_versions)
|
|
932
|
+
|
|
933
|
+
all_migrations = await self.runner.get_migration_files()
|
|
934
|
+
if runtime is not None:
|
|
935
|
+
runtime.increment_metric("migrations.command.upgrade.available", float(len(all_migrations)))
|
|
936
|
+
|
|
937
|
+
pending = []
|
|
938
|
+
for version, file_path in all_migrations:
|
|
939
|
+
if version not in applied_set:
|
|
940
|
+
if revision == "head":
|
|
941
|
+
pending.append((version, file_path))
|
|
942
|
+
else:
|
|
943
|
+
parsed_version = parse_version(version)
|
|
944
|
+
parsed_revision = parse_version(revision)
|
|
945
|
+
if parsed_version <= parsed_revision:
|
|
946
|
+
pending.append((version, file_path))
|
|
947
|
+
|
|
948
|
+
if runtime is not None:
|
|
949
|
+
runtime.increment_metric("migrations.command.upgrade.pending", float(len(pending)))
|
|
950
|
+
|
|
951
|
+
if not pending:
|
|
952
|
+
if not all_migrations:
|
|
953
|
+
console.print(
|
|
954
|
+
"[yellow]No migrations found. Create your first migration with 'sqlspec create-migration'.[/]"
|
|
955
|
+
)
|
|
956
|
+
else:
|
|
957
|
+
console.print("[green]Already at latest version[/]")
|
|
958
|
+
return
|
|
959
|
+
pending_versions = [v for v, _ in pending]
|
|
960
|
+
|
|
961
|
+
migration_config = cast("dict[str, Any]", self.config.migration_config) or {}
|
|
962
|
+
strict_ordering = migration_config.get("strict_ordering", False) and not allow_missing
|
|
963
|
+
|
|
964
|
+
validate_migration_order(pending_versions, applied_versions, strict_ordering)
|
|
965
|
+
|
|
966
|
+
console.print(f"[yellow]Found {len(pending)} pending migrations[/]")
|
|
967
|
+
for version, file_path in pending:
|
|
968
|
+
migration = await self.runner.load_migration(file_path, version)
|
|
969
|
+
|
|
970
|
+
action_verb = "Would apply" if dry_run else "Applying"
|
|
971
|
+
console.print(f"\n[cyan]{action_verb} {version}:[/] {migration['description']}")
|
|
972
|
+
|
|
973
|
+
if dry_run:
|
|
974
|
+
console.print(f"[dim]Migration file: {file_path}[/]")
|
|
975
|
+
continue
|
|
976
|
+
|
|
977
|
+
try:
|
|
978
|
+
|
|
979
|
+
async def record_version(exec_time: int, migration: "dict[str, Any]" = migration) -> None:
|
|
980
|
+
await self.tracker.record_migration(
|
|
981
|
+
driver, migration["version"], migration["description"], exec_time, migration["checksum"]
|
|
982
|
+
)
|
|
983
|
+
|
|
984
|
+
_, execution_time = await self.runner.execute_upgrade(driver, migration, on_success=record_version)
|
|
985
|
+
applied_count += 1
|
|
986
|
+
console.print(f"[green]✓ Applied in {execution_time}ms[/]")
|
|
987
|
+
except Exception as exc:
|
|
988
|
+
use_txn = self.runner.should_use_transaction(migration, self.config)
|
|
989
|
+
rollback_msg = " (transaction rolled back)" if use_txn else ""
|
|
990
|
+
console.print(f"[red]✗ Failed{rollback_msg}: {exc}[/]")
|
|
991
|
+
self._last_command_error = exc
|
|
992
|
+
return
|
|
993
|
+
|
|
994
|
+
if dry_run:
|
|
995
|
+
console.print("\n[bold yellow]Dry run complete.[/] No changes were made to the database.")
|
|
996
|
+
elif applied_count:
|
|
997
|
+
self._record_command_metric("applied", float(applied_count))
|
|
998
|
+
|
|
999
|
+
@_with_command_span("downgrade", metadata_fn=_downgrade_metadata)
|
|
1000
|
+
async def downgrade(self, revision: str = "-1", *, dry_run: bool = False) -> None:
|
|
1001
|
+
"""Downgrade to a target revision.
|
|
1002
|
+
|
|
1003
|
+
Args:
|
|
1004
|
+
revision: Target revision or "-1" for one step back.
|
|
1005
|
+
dry_run: If True, show what would be done without making changes.
|
|
1006
|
+
"""
|
|
1007
|
+
runtime = self._runtime
|
|
1008
|
+
reverted_count = 0
|
|
1009
|
+
|
|
1010
|
+
if dry_run:
|
|
1011
|
+
console.print("[bold yellow]DRY RUN MODE:[/] No database changes will be applied\n")
|
|
1012
|
+
|
|
1013
|
+
async with self.config.provide_session() as driver:
|
|
1014
|
+
await self.tracker.ensure_tracking_table(driver)
|
|
1015
|
+
|
|
1016
|
+
applied = await self.tracker.get_applied_migrations(driver)
|
|
1017
|
+
if runtime is not None:
|
|
1018
|
+
runtime.increment_metric("migrations.command.downgrade.available", float(len(applied)))
|
|
1019
|
+
if not applied:
|
|
1020
|
+
console.print("[yellow]No migrations to downgrade[/]")
|
|
1021
|
+
return
|
|
1022
|
+
to_revert = []
|
|
1023
|
+
if revision == "-1":
|
|
1024
|
+
to_revert = [applied[-1]]
|
|
1025
|
+
elif revision == "base":
|
|
1026
|
+
to_revert = list(reversed(applied))
|
|
1027
|
+
else:
|
|
1028
|
+
parsed_revision = parse_version(revision)
|
|
1029
|
+
for migration in reversed(applied):
|
|
1030
|
+
parsed_migration_version = parse_version(migration["version_num"])
|
|
1031
|
+
if parsed_migration_version > parsed_revision:
|
|
1032
|
+
to_revert.append(migration)
|
|
1033
|
+
|
|
1034
|
+
if runtime is not None:
|
|
1035
|
+
runtime.increment_metric("migrations.command.downgrade.pending", float(len(to_revert)))
|
|
1036
|
+
|
|
1037
|
+
if not to_revert:
|
|
1038
|
+
console.print("[yellow]Nothing to downgrade[/]")
|
|
1039
|
+
return
|
|
1040
|
+
|
|
1041
|
+
console.print(f"[yellow]Reverting {len(to_revert)} migrations[/]")
|
|
1042
|
+
all_files = dict(await self.runner.get_migration_files())
|
|
1043
|
+
for migration_record in to_revert:
|
|
1044
|
+
version = migration_record["version_num"]
|
|
1045
|
+
if version not in all_files:
|
|
1046
|
+
console.print(f"[red]Migration file not found for {version}[/]")
|
|
1047
|
+
if runtime is not None:
|
|
1048
|
+
runtime.increment_metric("migrations.command.downgrade.missing_files")
|
|
1049
|
+
continue
|
|
1050
|
+
|
|
1051
|
+
migration = await self.runner.load_migration(all_files[version], version)
|
|
1052
|
+
|
|
1053
|
+
action_verb = "Would revert" if dry_run else "Reverting"
|
|
1054
|
+
console.print(f"\n[cyan]{action_verb} {version}:[/] {migration['description']}")
|
|
1055
|
+
|
|
1056
|
+
if dry_run:
|
|
1057
|
+
console.print(f"[dim]Migration file: {all_files[version]}[/]")
|
|
1058
|
+
continue
|
|
1059
|
+
|
|
1060
|
+
try:
|
|
1061
|
+
|
|
1062
|
+
async def remove_version(exec_time: int, version: str = version) -> None:
|
|
1063
|
+
await self.tracker.remove_migration(driver, version)
|
|
1064
|
+
|
|
1065
|
+
_, execution_time = await self.runner.execute_downgrade(
|
|
1066
|
+
driver, migration, on_success=remove_version
|
|
1067
|
+
)
|
|
1068
|
+
reverted_count += 1
|
|
1069
|
+
console.print(f"[green]✓ Reverted in {execution_time}ms[/]")
|
|
1070
|
+
except Exception as exc:
|
|
1071
|
+
use_txn = self.runner.should_use_transaction(migration, self.config)
|
|
1072
|
+
rollback_msg = " (transaction rolled back)" if use_txn else ""
|
|
1073
|
+
console.print(f"[red]✗ Failed{rollback_msg}: {exc}[/]")
|
|
1074
|
+
self._last_command_error = exc
|
|
1075
|
+
return
|
|
1076
|
+
|
|
1077
|
+
if dry_run:
|
|
1078
|
+
console.print("\n[bold yellow]Dry run complete.[/] No changes were made to the database.")
|
|
1079
|
+
elif reverted_count:
|
|
1080
|
+
self._record_command_metric("applied", float(reverted_count))
|
|
1081
|
+
|
|
1082
|
+
async def stamp(self, revision: str) -> None:
|
|
1083
|
+
"""Mark database as being at a specific revision without running migrations.
|
|
1084
|
+
|
|
1085
|
+
Args:
|
|
1086
|
+
revision: The revision to stamp.
|
|
1087
|
+
"""
|
|
1088
|
+
async with self.config.provide_session() as driver:
|
|
1089
|
+
await self.tracker.ensure_tracking_table(driver)
|
|
1090
|
+
|
|
1091
|
+
all_migrations = dict(await self.runner.get_migration_files())
|
|
1092
|
+
if revision not in all_migrations:
|
|
1093
|
+
console.print(f"[red]Unknown revision: {revision}[/]")
|
|
1094
|
+
return
|
|
1095
|
+
|
|
1096
|
+
clear_sql = sql.delete().from_(self.tracker.version_table)
|
|
1097
|
+
await driver.execute(clear_sql)
|
|
1098
|
+
await self.tracker.record_migration(driver, revision, f"Stamped to {revision}", 0, "manual-stamp")
|
|
1099
|
+
console.print(f"[green]Database stamped at revision {revision}[/]")
|
|
1100
|
+
|
|
1101
|
+
async def revision(self, message: str, file_type: str | None = None) -> None:
|
|
1102
|
+
"""Create a new migration file with timestamp-based versioning.
|
|
1103
|
+
|
|
1104
|
+
Generates a unique timestamp version (YYYYMMDDHHmmss format) to avoid
|
|
1105
|
+
conflicts when multiple developers create migrations concurrently.
|
|
1106
|
+
|
|
1107
|
+
Args:
|
|
1108
|
+
message: Description for the migration.
|
|
1109
|
+
file_type: Type of migration file to create ('sql' or 'py').
|
|
1110
|
+
"""
|
|
1111
|
+
version = generate_timestamp_version()
|
|
1112
|
+
selected_format = file_type or self._template_settings.default_format
|
|
1113
|
+
file_path = create_migration_file(
|
|
1114
|
+
self.migrations_path,
|
|
1115
|
+
version,
|
|
1116
|
+
message,
|
|
1117
|
+
selected_format,
|
|
1118
|
+
config=self.config,
|
|
1119
|
+
template_settings=self._template_settings,
|
|
1120
|
+
)
|
|
1121
|
+
log_with_context(
|
|
1122
|
+
logger,
|
|
1123
|
+
logging.DEBUG,
|
|
1124
|
+
"migration.create",
|
|
1125
|
+
db_system=resolve_db_system(type(self.config).__name__),
|
|
1126
|
+
version=version,
|
|
1127
|
+
file_path=str(file_path),
|
|
1128
|
+
file_type=selected_format,
|
|
1129
|
+
description=message,
|
|
1130
|
+
)
|
|
1131
|
+
console.print(f"[green]Created migration:[/] {file_path}")
|
|
1132
|
+
|
|
1133
|
+
async def fix(self, dry_run: bool = False, update_database: bool = True, yes: bool = False) -> None:
|
|
1134
|
+
"""Convert timestamp migrations to sequential format.
|
|
1135
|
+
|
|
1136
|
+
Implements hybrid versioning workflow where development uses timestamps
|
|
1137
|
+
and production uses sequential numbers. Creates backup before changes
|
|
1138
|
+
and provides rollback on errors.
|
|
1139
|
+
|
|
1140
|
+
Args:
|
|
1141
|
+
dry_run: Preview changes without applying.
|
|
1142
|
+
update_database: Update migration records in database.
|
|
1143
|
+
yes: Skip confirmation prompt.
|
|
1144
|
+
|
|
1145
|
+
Examples:
|
|
1146
|
+
>>> await commands.fix(dry_run=True) # Preview only
|
|
1147
|
+
>>> await commands.fix(yes=True) # Auto-approve
|
|
1148
|
+
>>> await commands.fix(update_database=False) # Files only
|
|
1149
|
+
"""
|
|
1150
|
+
all_migrations = await self.runner.get_migration_files()
|
|
1151
|
+
|
|
1152
|
+
conversion_map = generate_conversion_map(all_migrations)
|
|
1153
|
+
|
|
1154
|
+
if not conversion_map:
|
|
1155
|
+
console.print("[yellow]No timestamp migrations found - nothing to convert[/]")
|
|
1156
|
+
return
|
|
1157
|
+
|
|
1158
|
+
fixer = MigrationFixer(self.migrations_path)
|
|
1159
|
+
renames = fixer.plan_renames(conversion_map)
|
|
1160
|
+
|
|
1161
|
+
table = Table(title="Migration Conversions")
|
|
1162
|
+
table.add_column("Current Version", style="cyan")
|
|
1163
|
+
table.add_column("New Version", style="green")
|
|
1164
|
+
table.add_column("File")
|
|
1165
|
+
|
|
1166
|
+
for rename in renames:
|
|
1167
|
+
table.add_row(rename.old_version, rename.new_version, rename.old_path.name)
|
|
1168
|
+
|
|
1169
|
+
console.print(table)
|
|
1170
|
+
console.print(f"\n[yellow]{len(renames)} migrations will be converted[/]")
|
|
1171
|
+
|
|
1172
|
+
if dry_run:
|
|
1173
|
+
console.print("[yellow][Preview Mode - No changes made][/]")
|
|
1174
|
+
return
|
|
1175
|
+
|
|
1176
|
+
if not yes:
|
|
1177
|
+
response = input("\nProceed with conversion? [y/N]: ")
|
|
1178
|
+
if response.lower() != "y":
|
|
1179
|
+
console.print("[yellow]Conversion cancelled[/]")
|
|
1180
|
+
return
|
|
1181
|
+
|
|
1182
|
+
try:
|
|
1183
|
+
backup_path = fixer.create_backup()
|
|
1184
|
+
console.print(f"[green]✓ Created backup in {backup_path.name}[/]")
|
|
1185
|
+
|
|
1186
|
+
fixer.apply_renames(renames)
|
|
1187
|
+
for rename in renames:
|
|
1188
|
+
console.print(f"[green]✓ Renamed {rename.old_path.name} → {rename.new_path.name}[/]")
|
|
1189
|
+
|
|
1190
|
+
if update_database:
|
|
1191
|
+
async with self.config.provide_session() as driver:
|
|
1192
|
+
await self.tracker.ensure_tracking_table(driver)
|
|
1193
|
+
applied_migrations = await self.tracker.get_applied_migrations(driver)
|
|
1194
|
+
applied_versions = {m["version_num"] for m in applied_migrations}
|
|
1195
|
+
|
|
1196
|
+
updated_count = 0
|
|
1197
|
+
for old_version, new_version in conversion_map.items():
|
|
1198
|
+
if old_version in applied_versions:
|
|
1199
|
+
await self.tracker.update_version_record(driver, old_version, new_version)
|
|
1200
|
+
updated_count += 1
|
|
1201
|
+
|
|
1202
|
+
if updated_count > 0:
|
|
1203
|
+
console.print(
|
|
1204
|
+
f"[green]✓ Updated {updated_count} version records in migration tracking table[/]"
|
|
1205
|
+
)
|
|
1206
|
+
else:
|
|
1207
|
+
console.print("[green]✓ No applied migrations to update in tracking table[/]")
|
|
1208
|
+
|
|
1209
|
+
fixer.cleanup()
|
|
1210
|
+
console.print("[green]✓ Conversion complete![/]")
|
|
1211
|
+
|
|
1212
|
+
except Exception as e:
|
|
1213
|
+
console.print(f"[red]✗ Error: {e}[/]")
|
|
1214
|
+
fixer.rollback()
|
|
1215
|
+
console.print("[yellow]Restored files from backup[/]")
|
|
1216
|
+
raise
|
|
1217
|
+
|
|
1218
|
+
|
|
1219
|
+
def create_migration_commands(
|
|
1220
|
+
config: "SyncConfigT | AsyncConfigT",
|
|
1221
|
+
) -> "SyncMigrationCommands[SyncConfigT] | AsyncMigrationCommands[AsyncConfigT]":
|
|
1222
|
+
"""Factory function to create the appropriate migration commands.
|
|
1223
|
+
|
|
1224
|
+
Args:
|
|
1225
|
+
config: The SQLSpec configuration.
|
|
1226
|
+
|
|
1227
|
+
Returns:
|
|
1228
|
+
Appropriate migration commands instance.
|
|
1229
|
+
"""
|
|
1230
|
+
if config.is_async:
|
|
1231
|
+
return cast("AsyncMigrationCommands[AsyncConfigT]", AsyncMigrationCommands(cast("AsyncConfigT", config)))
|
|
1232
|
+
return cast("SyncMigrationCommands[SyncConfigT]", SyncMigrationCommands(cast("SyncConfigT", config)))
|