sqlspec 0.47.0__cp314-cp314-win_amd64.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.
- f68e0789eb443ecb1c2c__mypyc.cp314-win_amd64.pyd +0 -0
- sqlspec/__init__.py +167 -0
- sqlspec/__main__.py +12 -0
- sqlspec/__metadata__.py +14 -0
- sqlspec/_typing.py +714 -0
- sqlspec/adapters/__init__.py +0 -0
- sqlspec/adapters/adbc/__init__.py +13 -0
- sqlspec/adapters/adbc/_typing.py +106 -0
- sqlspec/adapters/adbc/adk/__init__.py +5 -0
- sqlspec/adapters/adbc/adk/store.py +1280 -0
- sqlspec/adapters/adbc/config.py +378 -0
- sqlspec/adapters/adbc/core.cp314-win_amd64.pyd +0 -0
- sqlspec/adapters/adbc/core.py +922 -0
- sqlspec/adapters/adbc/data_dictionary.py +339 -0
- sqlspec/adapters/adbc/driver.py +534 -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 +534 -0
- sqlspec/adapters/adbc/type_converter.cp314-win_amd64.pyd +0 -0
- sqlspec/adapters/adbc/type_converter.py +142 -0
- sqlspec/adapters/aiomysql/__init__.py +21 -0
- sqlspec/adapters/aiomysql/_typing.py +137 -0
- sqlspec/adapters/aiomysql/adk/__init__.py +5 -0
- sqlspec/adapters/aiomysql/adk/store.py +678 -0
- sqlspec/adapters/aiomysql/config.py +305 -0
- sqlspec/adapters/aiomysql/core.cp314-win_amd64.pyd +0 -0
- sqlspec/adapters/aiomysql/core.py +536 -0
- sqlspec/adapters/aiomysql/data_dictionary.py +121 -0
- sqlspec/adapters/aiomysql/driver.py +386 -0
- sqlspec/adapters/aiomysql/events/__init__.py +5 -0
- sqlspec/adapters/aiomysql/events/store.py +104 -0
- sqlspec/adapters/aiomysql/litestar/__init__.py +5 -0
- sqlspec/adapters/aiomysql/litestar/store.py +314 -0
- sqlspec/adapters/aiosqlite/__init__.py +26 -0
- sqlspec/adapters/aiosqlite/_typing.py +109 -0
- sqlspec/adapters/aiosqlite/adk/__init__.py +5 -0
- sqlspec/adapters/aiosqlite/adk/store.py +829 -0
- sqlspec/adapters/aiosqlite/config.py +315 -0
- sqlspec/adapters/aiosqlite/core.cp314-win_amd64.pyd +0 -0
- sqlspec/adapters/aiosqlite/core.py +315 -0
- sqlspec/adapters/aiosqlite/data_dictionary.py +202 -0
- sqlspec/adapters/aiosqlite/driver.py +311 -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.cp314-win_amd64.pyd +0 -0
- sqlspec/adapters/aiosqlite/pool.py +734 -0
- sqlspec/adapters/asyncmy/__init__.py +21 -0
- sqlspec/adapters/asyncmy/_typing.py +113 -0
- sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
- sqlspec/adapters/asyncmy/adk/store.py +644 -0
- sqlspec/adapters/asyncmy/config.py +307 -0
- sqlspec/adapters/asyncmy/core.cp314-win_amd64.pyd +0 -0
- sqlspec/adapters/asyncmy/core.py +538 -0
- sqlspec/adapters/asyncmy/data_dictionary.py +122 -0
- sqlspec/adapters/asyncmy/driver.py +391 -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 +26 -0
- sqlspec/adapters/asyncpg/_typing.py +103 -0
- sqlspec/adapters/asyncpg/adk/__init__.py +5 -0
- sqlspec/adapters/asyncpg/adk/store.py +483 -0
- sqlspec/adapters/asyncpg/config.py +575 -0
- sqlspec/adapters/asyncpg/core.cp314-win_amd64.pyd +0 -0
- sqlspec/adapters/asyncpg/core.py +480 -0
- sqlspec/adapters/asyncpg/data_dictionary.py +157 -0
- sqlspec/adapters/asyncpg/driver.py +487 -0
- sqlspec/adapters/asyncpg/events/__init__.py +6 -0
- sqlspec/adapters/asyncpg/events/_hub.py +181 -0
- sqlspec/adapters/asyncpg/events/backend.py +210 -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 +15 -0
- sqlspec/adapters/bigquery/_typing.py +108 -0
- sqlspec/adapters/bigquery/config.py +362 -0
- sqlspec/adapters/bigquery/core.cp314-win_amd64.pyd +0 -0
- sqlspec/adapters/bigquery/core.py +768 -0
- sqlspec/adapters/bigquery/data_dictionary.py +120 -0
- sqlspec/adapters/bigquery/driver.py +542 -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.cp314-win_amd64.pyd +0 -0
- sqlspec/adapters/bigquery/type_converter.py +107 -0
- sqlspec/adapters/cockroach_asyncpg/__init__.py +26 -0
- sqlspec/adapters/cockroach_asyncpg/_typing.py +73 -0
- sqlspec/adapters/cockroach_asyncpg/adk/__init__.py +3 -0
- sqlspec/adapters/cockroach_asyncpg/adk/store.py +465 -0
- sqlspec/adapters/cockroach_asyncpg/config.py +248 -0
- sqlspec/adapters/cockroach_asyncpg/core.cp314-win_amd64.pyd +0 -0
- sqlspec/adapters/cockroach_asyncpg/core.py +55 -0
- sqlspec/adapters/cockroach_asyncpg/data_dictionary.py +110 -0
- sqlspec/adapters/cockroach_asyncpg/driver.py +142 -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 +39 -0
- sqlspec/adapters/cockroach_psycopg/_typing.py +137 -0
- sqlspec/adapters/cockroach_psycopg/adk/__init__.py +13 -0
- sqlspec/adapters/cockroach_psycopg/adk/store.py +1039 -0
- sqlspec/adapters/cockroach_psycopg/config.py +511 -0
- sqlspec/adapters/cockroach_psycopg/core.cp314-win_amd64.pyd +0 -0
- sqlspec/adapters/cockroach_psycopg/core.py +63 -0
- sqlspec/adapters/cockroach_psycopg/data_dictionary.py +220 -0
- sqlspec/adapters/cockroach_psycopg/driver.py +273 -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 +327 -0
- sqlspec/adapters/duckdb/__init__.py +29 -0
- sqlspec/adapters/duckdb/_typing.py +104 -0
- sqlspec/adapters/duckdb/adk/__init__.py +14 -0
- sqlspec/adapters/duckdb/adk/store.py +935 -0
- sqlspec/adapters/duckdb/config.py +386 -0
- sqlspec/adapters/duckdb/core.cp314-win_amd64.pyd +0 -0
- sqlspec/adapters/duckdb/core.py +332 -0
- sqlspec/adapters/duckdb/data_dictionary.py +140 -0
- sqlspec/adapters/duckdb/driver.py +426 -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.cp314-win_amd64.pyd +0 -0
- sqlspec/adapters/duckdb/pool.py +350 -0
- sqlspec/adapters/duckdb/type_converter.cp314-win_amd64.pyd +0 -0
- sqlspec/adapters/duckdb/type_converter.py +118 -0
- sqlspec/adapters/mysqlconnector/__init__.py +39 -0
- sqlspec/adapters/mysqlconnector/_typing.py +186 -0
- sqlspec/adapters/mysqlconnector/adk/__init__.py +15 -0
- sqlspec/adapters/mysqlconnector/adk/store.py +1183 -0
- sqlspec/adapters/mysqlconnector/config.py +421 -0
- sqlspec/adapters/mysqlconnector/core.cp314-win_amd64.pyd +0 -0
- sqlspec/adapters/mysqlconnector/core.py +472 -0
- sqlspec/adapters/mysqlconnector/data_dictionary.py +230 -0
- sqlspec/adapters/mysqlconnector/driver.py +516 -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 +39 -0
- sqlspec/adapters/oracledb/_json_handlers.cp314-win_amd64.pyd +0 -0
- sqlspec/adapters/oracledb/_json_handlers.py +196 -0
- sqlspec/adapters/oracledb/_param_types.cp314-win_amd64.pyd +0 -0
- sqlspec/adapters/oracledb/_param_types.py +46 -0
- sqlspec/adapters/oracledb/_typing.py +258 -0
- sqlspec/adapters/oracledb/_uuid_handlers.cp314-win_amd64.pyd +0 -0
- sqlspec/adapters/oracledb/_uuid_handlers.py +163 -0
- sqlspec/adapters/oracledb/_vector_handlers.cp314-win_amd64.pyd +0 -0
- sqlspec/adapters/oracledb/_vector_handlers.py +228 -0
- sqlspec/adapters/oracledb/adk/__init__.py +21 -0
- sqlspec/adapters/oracledb/adk/store.py +2453 -0
- sqlspec/adapters/oracledb/config.py +575 -0
- sqlspec/adapters/oracledb/core.cp314-win_amd64.pyd +0 -0
- sqlspec/adapters/oracledb/core.py +820 -0
- sqlspec/adapters/oracledb/data_dictionary.py +404 -0
- sqlspec/adapters/oracledb/driver.py +1277 -0
- sqlspec/adapters/oracledb/events/__init__.py +16 -0
- sqlspec/adapters/oracledb/events/_hub.py +345 -0
- sqlspec/adapters/oracledb/events/backend.py +300 -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 +539 -0
- sqlspec/adapters/oracledb/type_converter.cp314-win_amd64.pyd +0 -0
- sqlspec/adapters/oracledb/type_converter.py +211 -0
- sqlspec/adapters/psqlpy/__init__.py +18 -0
- sqlspec/adapters/psqlpy/_typing.py +121 -0
- sqlspec/adapters/psqlpy/adk/__init__.py +5 -0
- sqlspec/adapters/psqlpy/adk/store.py +591 -0
- sqlspec/adapters/psqlpy/config.py +376 -0
- sqlspec/adapters/psqlpy/core.cp314-win_amd64.pyd +0 -0
- sqlspec/adapters/psqlpy/core.py +694 -0
- sqlspec/adapters/psqlpy/data_dictionary.py +121 -0
- sqlspec/adapters/psqlpy/driver.py +411 -0
- sqlspec/adapters/psqlpy/events/__init__.py +6 -0
- sqlspec/adapters/psqlpy/events/_hub.py +204 -0
- sqlspec/adapters/psqlpy/events/backend.py +210 -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.cp314-win_amd64.pyd +0 -0
- sqlspec/adapters/psqlpy/type_converter.py +113 -0
- sqlspec/adapters/psycopg/__init__.py +38 -0
- sqlspec/adapters/psycopg/_typing.py +218 -0
- sqlspec/adapters/psycopg/adk/__init__.py +10 -0
- sqlspec/adapters/psycopg/adk/store.py +1106 -0
- sqlspec/adapters/psycopg/config.py +695 -0
- sqlspec/adapters/psycopg/core.cp314-win_amd64.pyd +0 -0
- sqlspec/adapters/psycopg/core.py +520 -0
- sqlspec/adapters/psycopg/data_dictionary.py +278 -0
- sqlspec/adapters/psycopg/driver.py +1033 -0
- sqlspec/adapters/psycopg/events/__init__.py +20 -0
- sqlspec/adapters/psycopg/events/_hub.py +388 -0
- sqlspec/adapters/psycopg/events/backend.py +398 -0
- sqlspec/adapters/psycopg/events/store.py +42 -0
- sqlspec/adapters/psycopg/litestar/__init__.py +5 -0
- sqlspec/adapters/psycopg/litestar/store.py +554 -0
- sqlspec/adapters/psycopg/type_converter.cp314-win_amd64.pyd +0 -0
- sqlspec/adapters/psycopg/type_converter.py +93 -0
- sqlspec/adapters/pymysql/__init__.py +21 -0
- sqlspec/adapters/pymysql/_typing.py +92 -0
- sqlspec/adapters/pymysql/adk/__init__.py +5 -0
- sqlspec/adapters/pymysql/adk/store.py +657 -0
- sqlspec/adapters/pymysql/config.py +176 -0
- sqlspec/adapters/pymysql/core.cp314-win_amd64.pyd +0 -0
- sqlspec/adapters/pymysql/core.py +469 -0
- sqlspec/adapters/pymysql/data_dictionary.py +120 -0
- sqlspec/adapters/pymysql/driver.py +271 -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.cp314-win_amd64.pyd +0 -0
- sqlspec/adapters/pymysql/pool.py +184 -0
- sqlspec/adapters/spanner/__init__.py +33 -0
- sqlspec/adapters/spanner/_typing.py +102 -0
- sqlspec/adapters/spanner/adk/__init__.py +5 -0
- sqlspec/adapters/spanner/adk/store.py +758 -0
- sqlspec/adapters/spanner/config.py +355 -0
- sqlspec/adapters/spanner/core.cp314-win_amd64.pyd +0 -0
- sqlspec/adapters/spanner/core.py +263 -0
- sqlspec/adapters/spanner/data_dictionary.py +120 -0
- sqlspec/adapters/spanner/driver.py +407 -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.cp314-win_amd64.pyd +0 -0
- sqlspec/adapters/spanner/type_converter.py +342 -0
- sqlspec/adapters/sqlite/__init__.py +19 -0
- sqlspec/adapters/sqlite/_typing.py +123 -0
- sqlspec/adapters/sqlite/adk/__init__.py +5 -0
- sqlspec/adapters/sqlite/adk/store.py +992 -0
- sqlspec/adapters/sqlite/config.py +240 -0
- sqlspec/adapters/sqlite/core.cp314-win_amd64.pyd +0 -0
- sqlspec/adapters/sqlite/core.py +357 -0
- sqlspec/adapters/sqlite/data_dictionary.py +198 -0
- sqlspec/adapters/sqlite/driver.py +527 -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.cp314-win_amd64.pyd +0 -0
- sqlspec/adapters/sqlite/pool.py +237 -0
- sqlspec/adapters/sqlite/type_converter.cp314-win_amd64.pyd +0 -0
- sqlspec/adapters/sqlite/type_converter.py +114 -0
- sqlspec/base.py +832 -0
- sqlspec/builder/__init__.py +181 -0
- sqlspec/builder/_base.cp314-win_amd64.pyd +0 -0
- sqlspec/builder/_base.py +1071 -0
- sqlspec/builder/_column.cp314-win_amd64.pyd +0 -0
- sqlspec/builder/_column.py +521 -0
- sqlspec/builder/_ddl.cp314-win_amd64.pyd +0 -0
- sqlspec/builder/_ddl.py +1691 -0
- sqlspec/builder/_delete.cp314-win_amd64.pyd +0 -0
- sqlspec/builder/_delete.py +95 -0
- sqlspec/builder/_dml.cp314-win_amd64.pyd +0 -0
- sqlspec/builder/_dml.py +386 -0
- sqlspec/builder/_explain.cp314-win_amd64.pyd +0 -0
- sqlspec/builder/_explain.py +579 -0
- sqlspec/builder/_expression_wrappers.cp314-win_amd64.pyd +0 -0
- sqlspec/builder/_expression_wrappers.py +46 -0
- sqlspec/builder/_factory.cp314-win_amd64.pyd +0 -0
- sqlspec/builder/_factory.py +1884 -0
- sqlspec/builder/_insert.cp314-win_amd64.pyd +0 -0
- sqlspec/builder/_insert.py +405 -0
- sqlspec/builder/_join.cp314-win_amd64.pyd +0 -0
- sqlspec/builder/_join.py +489 -0
- sqlspec/builder/_merge.cp314-win_amd64.pyd +0 -0
- sqlspec/builder/_merge.py +823 -0
- sqlspec/builder/_parsing_utils.cp314-win_amd64.pyd +0 -0
- sqlspec/builder/_parsing_utils.py +295 -0
- sqlspec/builder/_select.cp314-win_amd64.pyd +0 -0
- sqlspec/builder/_select.py +1666 -0
- sqlspec/builder/_temporal.cp314-win_amd64.pyd +0 -0
- sqlspec/builder/_temporal.py +167 -0
- sqlspec/builder/_update.cp314-win_amd64.pyd +0 -0
- sqlspec/builder/_update.py +173 -0
- sqlspec/builder/_vector_distance.cp314-win_amd64.pyd +0 -0
- sqlspec/builder/_vector_distance.py +330 -0
- sqlspec/cli.py +1095 -0
- sqlspec/config.py +2383 -0
- sqlspec/core/__init__.py +372 -0
- sqlspec/core/_correlation.cp314-win_amd64.pyd +0 -0
- sqlspec/core/_correlation.py +176 -0
- sqlspec/core/_pagination.py +42 -0
- sqlspec/core/_pool.cp314-win_amd64.pyd +0 -0
- sqlspec/core/_pool.py +76 -0
- sqlspec/core/cache.cp314-win_amd64.pyd +0 -0
- sqlspec/core/cache.py +1085 -0
- sqlspec/core/compiler.cp314-win_amd64.pyd +0 -0
- sqlspec/core/compiler.py +1090 -0
- sqlspec/core/config_runtime.cp314-win_amd64.pyd +0 -0
- sqlspec/core/config_runtime.py +174 -0
- sqlspec/core/explain.cp314-win_amd64.pyd +0 -0
- sqlspec/core/explain.py +275 -0
- sqlspec/core/filters.cp314-win_amd64.pyd +0 -0
- sqlspec/core/filters.py +969 -0
- sqlspec/core/hashing.cp314-win_amd64.pyd +0 -0
- sqlspec/core/hashing.py +266 -0
- sqlspec/core/metrics.cp314-win_amd64.pyd +0 -0
- sqlspec/core/metrics.py +83 -0
- sqlspec/core/parameters/__init__.py +72 -0
- sqlspec/core/parameters/_alignment.cp314-win_amd64.pyd +0 -0
- sqlspec/core/parameters/_alignment.py +283 -0
- sqlspec/core/parameters/_converter.cp314-win_amd64.pyd +0 -0
- sqlspec/core/parameters/_converter.py +554 -0
- sqlspec/core/parameters/_processor.cp314-win_amd64.pyd +0 -0
- sqlspec/core/parameters/_processor.py +1182 -0
- sqlspec/core/parameters/_registry.cp314-win_amd64.pyd +0 -0
- sqlspec/core/parameters/_registry.py +206 -0
- sqlspec/core/parameters/_transformers.cp314-win_amd64.pyd +0 -0
- sqlspec/core/parameters/_transformers.py +324 -0
- sqlspec/core/parameters/_types.cp314-win_amd64.pyd +0 -0
- sqlspec/core/parameters/_types.py +536 -0
- sqlspec/core/parameters/_validator.cp314-win_amd64.pyd +0 -0
- sqlspec/core/parameters/_validator.py +171 -0
- sqlspec/core/pipeline.cp314-win_amd64.pyd +0 -0
- sqlspec/core/pipeline.py +333 -0
- sqlspec/core/query_modifiers.cp314-win_amd64.pyd +0 -0
- sqlspec/core/query_modifiers.py +508 -0
- sqlspec/core/result/__init__.py +25 -0
- sqlspec/core/result/_base.cp314-win_amd64.pyd +0 -0
- sqlspec/core/result/_base.py +1232 -0
- sqlspec/core/result/_io.cp314-win_amd64.pyd +0 -0
- sqlspec/core/result/_io.py +28 -0
- sqlspec/core/splitter.cp314-win_amd64.pyd +0 -0
- sqlspec/core/splitter.py +1021 -0
- sqlspec/core/sqlcommenter.cp314-win_amd64.pyd +0 -0
- sqlspec/core/sqlcommenter.py +249 -0
- sqlspec/core/stack.cp314-win_amd64.pyd +0 -0
- sqlspec/core/stack.py +163 -0
- sqlspec/core/statement.cp314-win_amd64.pyd +0 -0
- sqlspec/core/statement.py +1865 -0
- sqlspec/core/type_converter.cp314-win_amd64.pyd +0 -0
- sqlspec/core/type_converter.py +340 -0
- sqlspec/data_dictionary/__init__.py +22 -0
- sqlspec/data_dictionary/_loader.cp314-win_amd64.pyd +0 -0
- sqlspec/data_dictionary/_loader.py +138 -0
- sqlspec/data_dictionary/_registry.cp314-win_amd64.pyd +0 -0
- sqlspec/data_dictionary/_registry.py +74 -0
- sqlspec/data_dictionary/_types.cp314-win_amd64.pyd +0 -0
- sqlspec/data_dictionary/_types.py +121 -0
- sqlspec/data_dictionary/dialects/__init__.py +21 -0
- sqlspec/data_dictionary/dialects/bigquery.cp314-win_amd64.pyd +0 -0
- sqlspec/data_dictionary/dialects/bigquery.py +81 -0
- sqlspec/data_dictionary/dialects/cockroachdb.cp314-win_amd64.pyd +0 -0
- sqlspec/data_dictionary/dialects/cockroachdb.py +54 -0
- sqlspec/data_dictionary/dialects/duckdb.cp314-win_amd64.pyd +0 -0
- sqlspec/data_dictionary/dialects/duckdb.py +47 -0
- sqlspec/data_dictionary/dialects/mysql.cp314-win_amd64.pyd +0 -0
- sqlspec/data_dictionary/dialects/mysql.py +53 -0
- sqlspec/data_dictionary/dialects/oracle.cp314-win_amd64.pyd +0 -0
- sqlspec/data_dictionary/dialects/oracle.py +197 -0
- sqlspec/data_dictionary/dialects/postgres.cp314-win_amd64.pyd +0 -0
- sqlspec/data_dictionary/dialects/postgres.py +69 -0
- sqlspec/data_dictionary/dialects/spanner.cp314-win_amd64.pyd +0 -0
- sqlspec/data_dictionary/dialects/spanner.py +37 -0
- sqlspec/data_dictionary/dialects/sqlite.cp314-win_amd64.pyd +0 -0
- sqlspec/data_dictionary/dialects/sqlite.py +59 -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/dialects/__init__.py +22 -0
- sqlspec/dialects/_compat.cp314-win_amd64.pyd +0 -0
- sqlspec/dialects/_compat.py +14 -0
- sqlspec/dialects/postgres/__init__.py +9 -0
- sqlspec/dialects/postgres/_generators.cp314-win_amd64.pyd +0 -0
- sqlspec/dialects/postgres/_generators.py +57 -0
- sqlspec/dialects/postgres/_operators.cp314-win_amd64.pyd +0 -0
- sqlspec/dialects/postgres/_operators.py +81 -0
- sqlspec/dialects/postgres/_paradedb.py +50 -0
- sqlspec/dialects/postgres/_pgvector.py +36 -0
- sqlspec/dialects/spanner/__init__.py +6 -0
- sqlspec/dialects/spanner/_generators.cp314-win_amd64.pyd +0 -0
- sqlspec/dialects/spanner/_generators.py +206 -0
- sqlspec/dialects/spanner/_spangres.py +77 -0
- sqlspec/dialects/spanner/_spanner.py +179 -0
- sqlspec/driver/__init__.py +49 -0
- sqlspec/driver/_async.cp314-win_amd64.pyd +0 -0
- sqlspec/driver/_async.py +1830 -0
- sqlspec/driver/_common.cp314-win_amd64.pyd +0 -0
- sqlspec/driver/_common.py +2292 -0
- sqlspec/driver/_exception_handler.cp314-win_amd64.pyd +0 -0
- sqlspec/driver/_exception_handler.py +108 -0
- sqlspec/driver/_query_cache.cp314-win_amd64.pyd +0 -0
- sqlspec/driver/_query_cache.py +96 -0
- sqlspec/driver/_sql_helpers.cp314-win_amd64.pyd +0 -0
- sqlspec/driver/_sql_helpers.py +139 -0
- sqlspec/driver/_storage_helpers.cp314-win_amd64.pyd +0 -0
- sqlspec/driver/_storage_helpers.py +153 -0
- sqlspec/driver/_sync.cp314-win_amd64.pyd +0 -0
- sqlspec/driver/_sync.py +1817 -0
- sqlspec/exceptions.cp314-win_amd64.pyd +0 -0
- sqlspec/exceptions.py +480 -0
- sqlspec/extensions/__init__.py +0 -0
- sqlspec/extensions/adk/__init__.py +84 -0
- sqlspec/extensions/adk/_config_utils.py +199 -0
- sqlspec/extensions/adk/_types.cp314-win_amd64.pyd +0 -0
- sqlspec/extensions/adk/_types.py +41 -0
- sqlspec/extensions/adk/artifact/__init__.py +57 -0
- sqlspec/extensions/adk/artifact/_types.cp314-win_amd64.pyd +0 -0
- sqlspec/extensions/adk/artifact/_types.py +32 -0
- sqlspec/extensions/adk/artifact/service.py +508 -0
- sqlspec/extensions/adk/artifact/store.py +361 -0
- sqlspec/extensions/adk/converters.py +212 -0
- sqlspec/extensions/adk/memory/__init__.py +69 -0
- sqlspec/extensions/adk/memory/_types.cp314-win_amd64.pyd +0 -0
- sqlspec/extensions/adk/memory/_types.py +30 -0
- sqlspec/extensions/adk/memory/converters.py +225 -0
- sqlspec/extensions/adk/memory/service.py +316 -0
- sqlspec/extensions/adk/memory/store.py +525 -0
- sqlspec/extensions/adk/migrations/0001_create_adk_tables.py +184 -0
- sqlspec/extensions/adk/migrations/__init__.py +0 -0
- sqlspec/extensions/adk/service.py +279 -0
- sqlspec/extensions/adk/store.py +590 -0
- sqlspec/extensions/events/__init__.py +51 -0
- sqlspec/extensions/events/_channel.py +703 -0
- sqlspec/extensions/events/_hints.cp314-win_amd64.pyd +0 -0
- sqlspec/extensions/events/_hints.py +45 -0
- sqlspec/extensions/events/_models.py +23 -0
- sqlspec/extensions/events/_payload.cp314-win_amd64.pyd +0 -0
- sqlspec/extensions/events/_payload.py +69 -0
- sqlspec/extensions/events/_protocols.py +134 -0
- sqlspec/extensions/events/_queue.py +462 -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 +22 -0
- sqlspec/extensions/fastapi/extension.py +391 -0
- sqlspec/extensions/fastapi/providers.cp314-win_amd64.pyd +0 -0
- sqlspec/extensions/fastapi/providers.py +712 -0
- sqlspec/extensions/flask/__init__.py +38 -0
- sqlspec/extensions/flask/_state.py +87 -0
- sqlspec/extensions/flask/_utils.py +71 -0
- sqlspec/extensions/flask/extension.py +539 -0
- sqlspec/extensions/litestar/__init__.py +31 -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 +1066 -0
- sqlspec/extensions/litestar/providers.cp314-win_amd64.pyd +0 -0
- sqlspec/extensions/litestar/providers.py +784 -0
- sqlspec/extensions/litestar/store.py +298 -0
- sqlspec/extensions/otel/__init__.py +58 -0
- sqlspec/extensions/prometheus/__init__.py +113 -0
- sqlspec/extensions/sanic/__init__.py +19 -0
- sqlspec/extensions/sanic/_state.py +43 -0
- sqlspec/extensions/sanic/_utils.py +127 -0
- sqlspec/extensions/sanic/extension.py +647 -0
- sqlspec/extensions/starlette/__init__.py +22 -0
- sqlspec/extensions/starlette/_state.py +42 -0
- sqlspec/extensions/starlette/_utils.py +96 -0
- sqlspec/extensions/starlette/extension.py +374 -0
- sqlspec/extensions/starlette/middleware.py +281 -0
- sqlspec/loader.cp314-win_amd64.pyd +0 -0
- sqlspec/loader.py +727 -0
- sqlspec/migrations/__init__.py +39 -0
- sqlspec/migrations/base.cp314-win_amd64.pyd +0 -0
- sqlspec/migrations/base.py +862 -0
- sqlspec/migrations/commands.py +2151 -0
- sqlspec/migrations/context.cp314-win_amd64.pyd +0 -0
- sqlspec/migrations/context.py +157 -0
- sqlspec/migrations/fix.cp314-win_amd64.pyd +0 -0
- sqlspec/migrations/fix.py +204 -0
- sqlspec/migrations/loaders.cp314-win_amd64.pyd +0 -0
- sqlspec/migrations/loaders.py +443 -0
- sqlspec/migrations/runner.cp314-win_amd64.pyd +0 -0
- sqlspec/migrations/runner.py +1195 -0
- sqlspec/migrations/squash.cp314-win_amd64.pyd +0 -0
- sqlspec/migrations/squash.py +490 -0
- sqlspec/migrations/templates.cp314-win_amd64.pyd +0 -0
- sqlspec/migrations/templates.py +234 -0
- sqlspec/migrations/tracker.cp314-win_amd64.pyd +0 -0
- sqlspec/migrations/tracker.py +792 -0
- sqlspec/migrations/utils.cp314-win_amd64.pyd +0 -0
- sqlspec/migrations/utils.py +256 -0
- sqlspec/migrations/validation.cp314-win_amd64.pyd +0 -0
- sqlspec/migrations/validation.py +359 -0
- sqlspec/migrations/version.cp314-win_amd64.pyd +0 -0
- sqlspec/migrations/version.py +446 -0
- sqlspec/observability/__init__.py +57 -0
- sqlspec/observability/_common.cp314-win_amd64.pyd +0 -0
- sqlspec/observability/_common.py +77 -0
- sqlspec/observability/_config.cp314-win_amd64.pyd +0 -0
- sqlspec/observability/_config.py +364 -0
- sqlspec/observability/_diagnostics.cp314-win_amd64.pyd +0 -0
- sqlspec/observability/_diagnostics.py +74 -0
- sqlspec/observability/_dispatcher.cp314-win_amd64.pyd +0 -0
- sqlspec/observability/_dispatcher.py +200 -0
- sqlspec/observability/_formatters/__init__.py +13 -0
- sqlspec/observability/_formatters/_aws.cp314-win_amd64.pyd +0 -0
- sqlspec/observability/_formatters/_aws.py +102 -0
- sqlspec/observability/_formatters/_azure.cp314-win_amd64.pyd +0 -0
- sqlspec/observability/_formatters/_azure.py +96 -0
- sqlspec/observability/_formatters/_base.cp314-win_amd64.pyd +0 -0
- sqlspec/observability/_formatters/_base.py +57 -0
- sqlspec/observability/_formatters/_gcp.cp314-win_amd64.pyd +0 -0
- sqlspec/observability/_formatters/_gcp.py +131 -0
- sqlspec/observability/_formatting.py +58 -0
- sqlspec/observability/_observer.cp314-win_amd64.pyd +0 -0
- sqlspec/observability/_observer.py +361 -0
- sqlspec/observability/_runtime.cp314-win_amd64.pyd +0 -0
- sqlspec/observability/_runtime.py +461 -0
- sqlspec/observability/_sampling.cp314-win_amd64.pyd +0 -0
- sqlspec/observability/_sampling.py +188 -0
- sqlspec/observability/_spans.cp314-win_amd64.pyd +0 -0
- sqlspec/observability/_spans.py +161 -0
- sqlspec/protocols.py +955 -0
- sqlspec/py.typed +0 -0
- sqlspec/service.py +433 -0
- sqlspec/storage/__init__.py +48 -0
- sqlspec/storage/_arrow_payload.py +68 -0
- sqlspec/storage/_paths.cp314-win_amd64.pyd +0 -0
- sqlspec/storage/_paths.py +58 -0
- sqlspec/storage/_utils.py +46 -0
- sqlspec/storage/backends/__init__.py +1 -0
- sqlspec/storage/backends/base.cp314-win_amd64.pyd +0 -0
- sqlspec/storage/backends/base.py +374 -0
- sqlspec/storage/backends/fsspec.py +574 -0
- sqlspec/storage/backends/local.py +468 -0
- sqlspec/storage/backends/obstore.py +956 -0
- sqlspec/storage/errors.cp314-win_amd64.pyd +0 -0
- sqlspec/storage/errors.py +102 -0
- sqlspec/storage/pipeline.cp314-win_amd64.pyd +0 -0
- sqlspec/storage/pipeline.py +628 -0
- sqlspec/storage/registry.cp314-win_amd64.pyd +0 -0
- sqlspec/storage/registry.py +329 -0
- sqlspec/typing.py +405 -0
- sqlspec/utils/__init__.py +7 -0
- sqlspec/utils/arrow_helpers.py +384 -0
- sqlspec/utils/config_tools.cp314-win_amd64.pyd +0 -0
- sqlspec/utils/config_tools.py +314 -0
- sqlspec/utils/correlation.cp314-win_amd64.pyd +0 -0
- sqlspec/utils/correlation.py +134 -0
- sqlspec/utils/deprecation.cp314-win_amd64.pyd +0 -0
- sqlspec/utils/deprecation.py +157 -0
- sqlspec/utils/dispatch.cp314-win_amd64.pyd +0 -0
- sqlspec/utils/dispatch.py +101 -0
- sqlspec/utils/fixtures.cp314-win_amd64.pyd +0 -0
- sqlspec/utils/fixtures.py +260 -0
- sqlspec/utils/logging.cp314-win_amd64.pyd +0 -0
- sqlspec/utils/logging.py +251 -0
- sqlspec/utils/module_loader.py +306 -0
- sqlspec/utils/portal.cp314-win_amd64.pyd +0 -0
- sqlspec/utils/portal.py +377 -0
- sqlspec/utils/schema.cp314-win_amd64.pyd +0 -0
- sqlspec/utils/schema.py +1040 -0
- sqlspec/utils/serializers/__init__.py +30 -0
- sqlspec/utils/serializers/_json.cp314-win_amd64.pyd +0 -0
- sqlspec/utils/serializers/_json.py +415 -0
- sqlspec/utils/serializers/_numpy.cp314-win_amd64.pyd +0 -0
- sqlspec/utils/serializers/_numpy.py +65 -0
- sqlspec/utils/serializers/_schema.cp314-win_amd64.pyd +0 -0
- sqlspec/utils/serializers/_schema.py +285 -0
- sqlspec/utils/singleton.cp314-win_amd64.pyd +0 -0
- sqlspec/utils/singleton.py +41 -0
- sqlspec/utils/sync_tools.cp314-win_amd64.pyd +0 -0
- sqlspec/utils/sync_tools.py +316 -0
- sqlspec/utils/text.cp314-win_amd64.pyd +0 -0
- sqlspec/utils/text.py +109 -0
- sqlspec/utils/type_converters.cp314-win_amd64.pyd +0 -0
- sqlspec/utils/type_converters.py +216 -0
- sqlspec/utils/type_guards.cp314-win_amd64.pyd +0 -0
- sqlspec/utils/type_guards.py +1508 -0
- sqlspec/utils/uuids.cp314-win_amd64.pyd +0 -0
- sqlspec/utils/uuids.py +241 -0
- sqlspec-0.47.0.dist-info/METADATA +202 -0
- sqlspec-0.47.0.dist-info/RECORD +621 -0
- sqlspec-0.47.0.dist-info/WHEEL +4 -0
- sqlspec-0.47.0.dist-info/entry_points.txt +6 -0
- sqlspec-0.47.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,2151 @@
|
|
|
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.squash import MigrationSquasher
|
|
22
|
+
from sqlspec.migrations.utils import create_migration_file
|
|
23
|
+
from sqlspec.migrations.validation import validate_migration_order
|
|
24
|
+
from sqlspec.migrations.version import generate_conversion_map, generate_timestamp_version, parse_version
|
|
25
|
+
from sqlspec.observability import resolve_db_system
|
|
26
|
+
from sqlspec.utils.logging import get_logger, log_with_context
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
from sqlspec.config import AsyncConfigT, SyncConfigT
|
|
32
|
+
|
|
33
|
+
__all__ = ("AsyncMigrationCommands", "SyncMigrationCommands", "create_migration_commands")
|
|
34
|
+
|
|
35
|
+
logger = get_logger("sqlspec.migrations.commands")
|
|
36
|
+
console = Console()
|
|
37
|
+
P = ParamSpec("P")
|
|
38
|
+
R = TypeVar("R")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _output_info(
|
|
42
|
+
use_logger: bool, echo: bool, summary_only: bool, message: str, *args: Any, rich_message: str | None = None
|
|
43
|
+
) -> None:
|
|
44
|
+
"""Output an info message to logger or console."""
|
|
45
|
+
if use_logger:
|
|
46
|
+
if summary_only:
|
|
47
|
+
return
|
|
48
|
+
logger.info(message, *args)
|
|
49
|
+
else:
|
|
50
|
+
if not echo:
|
|
51
|
+
return
|
|
52
|
+
console.print(rich_message or message % args if args else message)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _output_warning(
|
|
56
|
+
use_logger: bool, echo: bool, summary_only: bool, message: str, *args: Any, rich_message: str | None = None
|
|
57
|
+
) -> None:
|
|
58
|
+
"""Output a warning message to logger or console."""
|
|
59
|
+
if use_logger:
|
|
60
|
+
logger.warning(message, *args)
|
|
61
|
+
else:
|
|
62
|
+
if not echo:
|
|
63
|
+
return
|
|
64
|
+
console.print(rich_message or message % args if args else message)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _output_error(
|
|
68
|
+
use_logger: bool, echo: bool, summary_only: bool, message: str, *args: Any, rich_message: str | None = None
|
|
69
|
+
) -> None:
|
|
70
|
+
"""Output an error message to logger or console."""
|
|
71
|
+
if use_logger:
|
|
72
|
+
logger.error(message, *args)
|
|
73
|
+
else:
|
|
74
|
+
if not echo:
|
|
75
|
+
return
|
|
76
|
+
console.print(rich_message or message % args if args else message)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _output_exception(
|
|
80
|
+
use_logger: bool, echo: bool, summary_only: bool, message: str, *args: Any, rich_message: str | None = None
|
|
81
|
+
) -> None:
|
|
82
|
+
"""Output an exception message to logger or console."""
|
|
83
|
+
if use_logger:
|
|
84
|
+
logger.exception(message, *args)
|
|
85
|
+
else:
|
|
86
|
+
if not echo:
|
|
87
|
+
return
|
|
88
|
+
console.print(rich_message or message % args if args else message)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _log_command_summary(
|
|
92
|
+
*,
|
|
93
|
+
use_logger: bool,
|
|
94
|
+
summary_only: bool,
|
|
95
|
+
command: str,
|
|
96
|
+
status: str,
|
|
97
|
+
revision: str,
|
|
98
|
+
dry_run: bool,
|
|
99
|
+
pending_count: int,
|
|
100
|
+
applied_count: int | None,
|
|
101
|
+
reverted_count: int | None,
|
|
102
|
+
duration_ms: int,
|
|
103
|
+
db_system: str | None,
|
|
104
|
+
bind_key: str | None,
|
|
105
|
+
config_name: str,
|
|
106
|
+
error: Exception | None = None,
|
|
107
|
+
allow_missing: bool | None = None,
|
|
108
|
+
auto_sync: bool | None = None,
|
|
109
|
+
) -> None:
|
|
110
|
+
"""Emit a single summary log entry for migration commands."""
|
|
111
|
+
if not use_logger or not summary_only:
|
|
112
|
+
return
|
|
113
|
+
level = logging.ERROR if status == "failed" else logging.INFO
|
|
114
|
+
extra_fields: dict[str, Any] = {
|
|
115
|
+
"command": command,
|
|
116
|
+
"status": status,
|
|
117
|
+
"revision": revision,
|
|
118
|
+
"dry_run": dry_run,
|
|
119
|
+
"pending_count": pending_count,
|
|
120
|
+
"duration_ms": duration_ms,
|
|
121
|
+
"db_system": db_system,
|
|
122
|
+
"bind_key": bind_key,
|
|
123
|
+
"config_name": config_name,
|
|
124
|
+
}
|
|
125
|
+
if applied_count is not None:
|
|
126
|
+
extra_fields["applied_count"] = applied_count
|
|
127
|
+
if reverted_count is not None:
|
|
128
|
+
extra_fields["reverted_count"] = reverted_count
|
|
129
|
+
if allow_missing is not None:
|
|
130
|
+
extra_fields["allow_missing"] = allow_missing
|
|
131
|
+
if auto_sync is not None:
|
|
132
|
+
extra_fields["auto_sync"] = auto_sync
|
|
133
|
+
if error is not None:
|
|
134
|
+
extra_fields["error_type"] = type(error).__name__
|
|
135
|
+
log_with_context(logger, level, "migration.command.summary", **extra_fields)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
MetadataBuilder = Callable[[dict[str, Any]], tuple[str | None, dict[str, Any]]]
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _bind_arguments(signature: inspect.Signature, args: tuple[Any, ...], kwargs: dict[str, Any]) -> dict[str, Any]:
|
|
142
|
+
bound = signature.bind_partial(*args, **kwargs)
|
|
143
|
+
arguments = dict(bound.arguments)
|
|
144
|
+
arguments.pop("self", None)
|
|
145
|
+
return arguments
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _with_command_span(
|
|
149
|
+
event: str, metadata_fn: "MetadataBuilder | None" = None, *, dry_run_param: str | None = "dry_run"
|
|
150
|
+
) -> Callable[[Callable[P, R]], Callable[P, R]]:
|
|
151
|
+
"""Attach span lifecycle and command metric management to command methods."""
|
|
152
|
+
|
|
153
|
+
metric_prefix = f"migrations.command.{event}"
|
|
154
|
+
|
|
155
|
+
def decorator(func: Callable[P, R]) -> Callable[P, R]:
|
|
156
|
+
signature = inspect.signature(func)
|
|
157
|
+
|
|
158
|
+
def _prepare(self: Any, args: tuple[Any, ...], kwargs: dict[str, Any]) -> tuple[Any, bool, Any]:
|
|
159
|
+
runtime = self._runtime
|
|
160
|
+
metadata_args = _bind_arguments(signature, args, kwargs)
|
|
161
|
+
dry_run = False
|
|
162
|
+
if dry_run_param is not None:
|
|
163
|
+
dry_run = bool(metadata_args.get(dry_run_param, False))
|
|
164
|
+
metadata: dict[str, Any] | None = None
|
|
165
|
+
version: str | None = None
|
|
166
|
+
span = None
|
|
167
|
+
if runtime is not None:
|
|
168
|
+
runtime.increment_metric(f"{metric_prefix}.invocations")
|
|
169
|
+
if dry_run_param is not None and dry_run:
|
|
170
|
+
runtime.increment_metric(f"{metric_prefix}.dry_run")
|
|
171
|
+
if metadata_fn is not None:
|
|
172
|
+
version, metadata = metadata_fn(metadata_args)
|
|
173
|
+
span = runtime.start_migration_span(f"command.{event}", version=version, metadata=metadata)
|
|
174
|
+
return runtime, dry_run, span
|
|
175
|
+
|
|
176
|
+
def _finalize(
|
|
177
|
+
self: Any,
|
|
178
|
+
runtime: Any,
|
|
179
|
+
span: Any,
|
|
180
|
+
start: float,
|
|
181
|
+
error: "Exception | None",
|
|
182
|
+
recorded_error: bool,
|
|
183
|
+
dry_run: bool,
|
|
184
|
+
) -> None:
|
|
185
|
+
command_error = self._last_command_error
|
|
186
|
+
self._last_command_error = None
|
|
187
|
+
command_metrics = self._last_command_metrics
|
|
188
|
+
self._last_command_metrics = None
|
|
189
|
+
if runtime is None:
|
|
190
|
+
return
|
|
191
|
+
if command_error is not None and not recorded_error:
|
|
192
|
+
runtime.increment_metric(f"{metric_prefix}.errors")
|
|
193
|
+
if not dry_run and command_metrics:
|
|
194
|
+
for metric, value in command_metrics.items():
|
|
195
|
+
runtime.increment_metric(f"{metric_prefix}.{metric}", value)
|
|
196
|
+
duration_ms = int((time.perf_counter() - start) * 1000)
|
|
197
|
+
runtime.end_migration_span(span, duration_ms=duration_ms, error=error or command_error)
|
|
198
|
+
|
|
199
|
+
if inspect.iscoroutinefunction(func):
|
|
200
|
+
|
|
201
|
+
@functools.wraps(func)
|
|
202
|
+
async def async_wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
203
|
+
self = args[0]
|
|
204
|
+
runtime, dry_run, span = _prepare(self, args, kwargs)
|
|
205
|
+
start = time.perf_counter()
|
|
206
|
+
error: Exception | None = None
|
|
207
|
+
error_recorded = False
|
|
208
|
+
try:
|
|
209
|
+
async_func = cast("Callable[P, Awaitable[R]]", func)
|
|
210
|
+
return await async_func(*args, **kwargs)
|
|
211
|
+
except Exception as exc: # pragma: no cover - passthrough
|
|
212
|
+
error = exc
|
|
213
|
+
if runtime is not None:
|
|
214
|
+
runtime.increment_metric(f"{metric_prefix}.errors")
|
|
215
|
+
error_recorded = True
|
|
216
|
+
raise
|
|
217
|
+
finally:
|
|
218
|
+
_finalize(self, runtime, span, start, error, error_recorded, dry_run)
|
|
219
|
+
|
|
220
|
+
return cast("Callable[P, R]", async_wrapper)
|
|
221
|
+
|
|
222
|
+
@functools.wraps(func)
|
|
223
|
+
def sync_wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
224
|
+
self = args[0]
|
|
225
|
+
runtime, dry_run, span = _prepare(self, args, kwargs)
|
|
226
|
+
start = time.perf_counter()
|
|
227
|
+
error: Exception | None = None
|
|
228
|
+
error_recorded = False
|
|
229
|
+
try:
|
|
230
|
+
return func(*args, **kwargs)
|
|
231
|
+
except Exception as exc: # pragma: no cover - passthrough
|
|
232
|
+
error = exc
|
|
233
|
+
if runtime is not None:
|
|
234
|
+
runtime.increment_metric(f"{metric_prefix}.errors")
|
|
235
|
+
error_recorded = True
|
|
236
|
+
raise
|
|
237
|
+
finally:
|
|
238
|
+
_finalize(self, runtime, span, start, error, error_recorded, dry_run)
|
|
239
|
+
|
|
240
|
+
return cast("Callable[P, R]", sync_wrapper)
|
|
241
|
+
|
|
242
|
+
return decorator
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _upgrade_metadata(args: dict[str, Any]) -> tuple[str | None, dict[str, Any]]:
|
|
246
|
+
revision = cast("str | None", args.get("revision"))
|
|
247
|
+
metadata = {"dry_run": str(args.get("dry_run", False)).lower()}
|
|
248
|
+
return revision, metadata
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _downgrade_metadata(args: dict[str, Any]) -> tuple[str | None, dict[str, Any]]:
|
|
252
|
+
revision = cast("str | None", args.get("revision"))
|
|
253
|
+
metadata = {"dry_run": str(args.get("dry_run", False)).lower()}
|
|
254
|
+
return revision, metadata
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class SyncMigrationCommands(BaseMigrationCommands["SyncConfigT", Any]):
|
|
258
|
+
"""Synchronous migration commands."""
|
|
259
|
+
|
|
260
|
+
def __init__(self, config: "SyncConfigT") -> None:
|
|
261
|
+
"""Initialize migration commands.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
config: The SQLSpec configuration.
|
|
265
|
+
"""
|
|
266
|
+
super().__init__(config)
|
|
267
|
+
self.tracker = config.migration_tracker_type(self.version_table)
|
|
268
|
+
|
|
269
|
+
# Create context with extension configurations
|
|
270
|
+
context = MigrationContext.from_config(config)
|
|
271
|
+
context.extension_config = self.extension_configs
|
|
272
|
+
|
|
273
|
+
self.runner = SyncMigrationRunner(
|
|
274
|
+
self.migrations_path,
|
|
275
|
+
self._discover_extension_migrations(),
|
|
276
|
+
context,
|
|
277
|
+
self.extension_configs,
|
|
278
|
+
runtime=self._runtime,
|
|
279
|
+
description_hints=self._template_settings.description_hints,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
def init(self, directory: str, package: bool = True) -> None:
|
|
283
|
+
"""Initialize migration directory structure.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
directory: Directory to initialize migrations in.
|
|
287
|
+
package: Whether to create __init__.py file.
|
|
288
|
+
"""
|
|
289
|
+
self.init_directory(directory, package)
|
|
290
|
+
|
|
291
|
+
def current(self, verbose: bool = False) -> "str | None":
|
|
292
|
+
"""Show current migration version.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
verbose: Whether to show detailed migration history.
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
The current migration version or None if no migrations applied.
|
|
299
|
+
"""
|
|
300
|
+
with self.config.provide_session() as driver:
|
|
301
|
+
self.tracker.ensure_tracking_table(driver)
|
|
302
|
+
|
|
303
|
+
current = self.tracker.get_current_version(driver)
|
|
304
|
+
if not current:
|
|
305
|
+
log_with_context(
|
|
306
|
+
logger,
|
|
307
|
+
logging.DEBUG,
|
|
308
|
+
"migration.list",
|
|
309
|
+
db_system=resolve_db_system(type(driver).__name__),
|
|
310
|
+
current_version=None,
|
|
311
|
+
applied_count=0,
|
|
312
|
+
verbose=verbose,
|
|
313
|
+
status="empty",
|
|
314
|
+
)
|
|
315
|
+
console.print("[yellow]No migrations applied yet[/]")
|
|
316
|
+
return None
|
|
317
|
+
|
|
318
|
+
console.print(f"[green]Current version:[/] {current}")
|
|
319
|
+
|
|
320
|
+
applied: list[dict[str, Any]] = []
|
|
321
|
+
if verbose:
|
|
322
|
+
applied = self.tracker.get_applied_migrations(driver)
|
|
323
|
+
|
|
324
|
+
table = Table(title="Applied Migrations")
|
|
325
|
+
table.add_column("Version", style="cyan")
|
|
326
|
+
table.add_column("Description")
|
|
327
|
+
table.add_column("Applied At")
|
|
328
|
+
table.add_column("Time (ms)", justify="right")
|
|
329
|
+
table.add_column("Applied By")
|
|
330
|
+
|
|
331
|
+
for migration in applied:
|
|
332
|
+
table.add_row(
|
|
333
|
+
migration["version_num"],
|
|
334
|
+
migration.get("description", ""),
|
|
335
|
+
str(migration.get("applied_at", "")),
|
|
336
|
+
str(migration.get("execution_time_ms", "")),
|
|
337
|
+
migration.get("applied_by", ""),
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
console.print(table)
|
|
341
|
+
|
|
342
|
+
applied_count = len(applied) if verbose else None
|
|
343
|
+
log_with_context(
|
|
344
|
+
logger,
|
|
345
|
+
logging.DEBUG,
|
|
346
|
+
"migration.list",
|
|
347
|
+
db_system=resolve_db_system(type(driver).__name__),
|
|
348
|
+
current_version=current,
|
|
349
|
+
applied_count=applied_count,
|
|
350
|
+
verbose=verbose,
|
|
351
|
+
status="complete",
|
|
352
|
+
)
|
|
353
|
+
return cast("str | None", current)
|
|
354
|
+
|
|
355
|
+
def _load_single_migration_checksum(self, version: str, file_path: "Path") -> "tuple[str, tuple[str, Path]] | None":
|
|
356
|
+
"""Load checksum for a single migration.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
version: Migration version.
|
|
360
|
+
file_path: Path to migration file.
|
|
361
|
+
|
|
362
|
+
Returns:
|
|
363
|
+
Tuple of (version, (checksum, file_path)) or None if load fails.
|
|
364
|
+
"""
|
|
365
|
+
try:
|
|
366
|
+
migration = self.runner.load_migration(file_path, version)
|
|
367
|
+
return (version, (migration["checksum"], file_path))
|
|
368
|
+
except Exception as exc:
|
|
369
|
+
log_with_context(
|
|
370
|
+
logger,
|
|
371
|
+
logging.DEBUG,
|
|
372
|
+
"migration.list",
|
|
373
|
+
db_system=resolve_db_system(type(self.config).__name__),
|
|
374
|
+
version=version,
|
|
375
|
+
file_path=str(file_path),
|
|
376
|
+
error_type=type(exc).__name__,
|
|
377
|
+
status="failed",
|
|
378
|
+
operation="load_checksum",
|
|
379
|
+
)
|
|
380
|
+
return None
|
|
381
|
+
|
|
382
|
+
def _load_migration_checksums(self, all_migrations: "list[tuple[str, Path]]") -> "dict[str, tuple[str, Path]]":
|
|
383
|
+
"""Load checksums for all migrations.
|
|
384
|
+
|
|
385
|
+
Args:
|
|
386
|
+
all_migrations: List of (version, file_path) tuples.
|
|
387
|
+
|
|
388
|
+
Returns:
|
|
389
|
+
Dictionary mapping version to (checksum, file_path) tuples.
|
|
390
|
+
"""
|
|
391
|
+
file_checksums = {}
|
|
392
|
+
for version, file_path in all_migrations:
|
|
393
|
+
result = self._load_single_migration_checksum(version, file_path)
|
|
394
|
+
if result:
|
|
395
|
+
file_checksums[result[0]] = result[1]
|
|
396
|
+
return file_checksums
|
|
397
|
+
|
|
398
|
+
def _collect_pending_migrations(
|
|
399
|
+
self, all_migrations: "list[tuple[str, Path]]", applied_set: set[str], revision: str
|
|
400
|
+
) -> "list[tuple[str, Path]]":
|
|
401
|
+
"""Collect pending migrations that need to be applied.
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
all_migrations: List of all (version, file_path) tuples.
|
|
405
|
+
applied_set: Set of already applied version strings.
|
|
406
|
+
revision: Target revision ("head" or specific version).
|
|
407
|
+
|
|
408
|
+
Returns:
|
|
409
|
+
List of (version, file_path) tuples for pending migrations.
|
|
410
|
+
"""
|
|
411
|
+
pending = []
|
|
412
|
+
for version, file_path in all_migrations:
|
|
413
|
+
if version not in applied_set:
|
|
414
|
+
if revision == "head":
|
|
415
|
+
pending.append((version, file_path))
|
|
416
|
+
else:
|
|
417
|
+
parsed_version = parse_version(version)
|
|
418
|
+
parsed_revision = parse_version(revision)
|
|
419
|
+
if parsed_version <= parsed_revision:
|
|
420
|
+
pending.append((version, file_path))
|
|
421
|
+
return pending
|
|
422
|
+
|
|
423
|
+
def _report_no_pending_migrations(
|
|
424
|
+
self, use_logger: bool, echo: bool, summary_only: bool, has_migrations: bool
|
|
425
|
+
) -> None:
|
|
426
|
+
"""Report that there are no pending migrations.
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
use_logger: Whether to output to logger instead of console.
|
|
430
|
+
echo: Whether to echo output to the console.
|
|
431
|
+
summary_only: Whether summary-only logging is enabled.
|
|
432
|
+
has_migrations: Whether any migrations exist at all.
|
|
433
|
+
"""
|
|
434
|
+
if not has_migrations:
|
|
435
|
+
_output_info(
|
|
436
|
+
use_logger,
|
|
437
|
+
echo,
|
|
438
|
+
summary_only,
|
|
439
|
+
"No migrations found. Create your first migration with 'sqlspec create-migration'.",
|
|
440
|
+
rich_message="[yellow]No migrations found. Create your first migration with 'sqlspec create-migration'.[/]",
|
|
441
|
+
)
|
|
442
|
+
else:
|
|
443
|
+
_output_info(
|
|
444
|
+
use_logger,
|
|
445
|
+
echo,
|
|
446
|
+
summary_only,
|
|
447
|
+
"Already at latest version",
|
|
448
|
+
rich_message="[green]Already at latest version[/]",
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
def _apply_single_migration(
|
|
452
|
+
self, driver: Any, migration: "dict[str, Any]", version: str, use_logger: bool, echo: bool, summary_only: bool
|
|
453
|
+
) -> int | None:
|
|
454
|
+
"""Apply a single migration and record it.
|
|
455
|
+
|
|
456
|
+
Args:
|
|
457
|
+
driver: Database driver instance.
|
|
458
|
+
migration: Migration dictionary with version, description, checksum.
|
|
459
|
+
version: Version string.
|
|
460
|
+
use_logger: Whether to output to logger instead of console.
|
|
461
|
+
echo: Whether to echo output to the console.
|
|
462
|
+
summary_only: Whether summary-only logging is enabled.
|
|
463
|
+
|
|
464
|
+
Returns:
|
|
465
|
+
Execution time in ms on success, None on failure.
|
|
466
|
+
"""
|
|
467
|
+
try:
|
|
468
|
+
|
|
469
|
+
def record_version(exec_time: int, migration: "dict[str, Any]" = migration) -> None:
|
|
470
|
+
self.tracker.record_migration(
|
|
471
|
+
driver, migration["version"], migration["description"], exec_time, migration["checksum"]
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
_, execution_time = self.runner.execute_upgrade(driver, migration, on_success=record_version)
|
|
475
|
+
except Exception as exc:
|
|
476
|
+
use_txn = self.runner.should_use_transaction(migration, self.config)
|
|
477
|
+
rollback_msg = " (transaction rolled back)" if use_txn else ""
|
|
478
|
+
_output_exception(
|
|
479
|
+
use_logger,
|
|
480
|
+
echo,
|
|
481
|
+
summary_only,
|
|
482
|
+
"Migration %s failed%s",
|
|
483
|
+
version,
|
|
484
|
+
rollback_msg,
|
|
485
|
+
rich_message=f"[red]✗ Failed{rollback_msg}: {exc}[/]",
|
|
486
|
+
)
|
|
487
|
+
self._last_command_error = exc
|
|
488
|
+
return None
|
|
489
|
+
else:
|
|
490
|
+
_output_info(
|
|
491
|
+
use_logger,
|
|
492
|
+
echo,
|
|
493
|
+
summary_only,
|
|
494
|
+
"Applied migration %s in %dms",
|
|
495
|
+
version,
|
|
496
|
+
execution_time,
|
|
497
|
+
rich_message=f"[green]✓ Applied in {execution_time}ms[/]",
|
|
498
|
+
)
|
|
499
|
+
return execution_time
|
|
500
|
+
|
|
501
|
+
def _collect_revert_migrations(self, applied: "list[dict[str, Any]]", revision: str) -> "list[dict[str, Any]]":
|
|
502
|
+
"""Collect migrations to revert based on target revision.
|
|
503
|
+
|
|
504
|
+
Args:
|
|
505
|
+
applied: List of applied migration records.
|
|
506
|
+
revision: Target revision ("-1", "base", or specific version).
|
|
507
|
+
|
|
508
|
+
Returns:
|
|
509
|
+
List of migration records to revert.
|
|
510
|
+
"""
|
|
511
|
+
if revision == "-1":
|
|
512
|
+
return [applied[-1]]
|
|
513
|
+
if revision == "base":
|
|
514
|
+
return list(reversed(applied))
|
|
515
|
+
parsed_revision = parse_version(revision)
|
|
516
|
+
to_revert = []
|
|
517
|
+
for migration in reversed(applied):
|
|
518
|
+
parsed_migration_version = parse_version(migration["version_num"])
|
|
519
|
+
if parsed_migration_version > parsed_revision:
|
|
520
|
+
to_revert.append(migration)
|
|
521
|
+
return to_revert
|
|
522
|
+
|
|
523
|
+
def _revert_single_migration(
|
|
524
|
+
self, driver: Any, migration: "dict[str, Any]", version: str, use_logger: bool, echo: bool, summary_only: bool
|
|
525
|
+
) -> int | None:
|
|
526
|
+
"""Revert a single migration.
|
|
527
|
+
|
|
528
|
+
Args:
|
|
529
|
+
driver: Database driver instance.
|
|
530
|
+
migration: Migration dictionary.
|
|
531
|
+
version: Version string.
|
|
532
|
+
use_logger: Whether to output to logger instead of console.
|
|
533
|
+
echo: Whether to echo output to the console.
|
|
534
|
+
summary_only: Whether summary-only logging is enabled.
|
|
535
|
+
|
|
536
|
+
Returns:
|
|
537
|
+
Execution time in ms on success, None on failure.
|
|
538
|
+
"""
|
|
539
|
+
try:
|
|
540
|
+
|
|
541
|
+
def remove_version(exec_time: int, version: str = version) -> None:
|
|
542
|
+
self.tracker.remove_migration(driver, version)
|
|
543
|
+
|
|
544
|
+
_, execution_time = self.runner.execute_downgrade(driver, migration, on_success=remove_version)
|
|
545
|
+
except Exception as exc:
|
|
546
|
+
use_txn = self.runner.should_use_transaction(migration, self.config)
|
|
547
|
+
rollback_msg = " (transaction rolled back)" if use_txn else ""
|
|
548
|
+
_output_exception(
|
|
549
|
+
use_logger,
|
|
550
|
+
echo,
|
|
551
|
+
summary_only,
|
|
552
|
+
"Migration %s failed%s",
|
|
553
|
+
version,
|
|
554
|
+
rollback_msg,
|
|
555
|
+
rich_message=f"[red]✗ Failed{rollback_msg}: {exc}[/]",
|
|
556
|
+
)
|
|
557
|
+
self._last_command_error = exc
|
|
558
|
+
return None
|
|
559
|
+
else:
|
|
560
|
+
_output_info(
|
|
561
|
+
use_logger,
|
|
562
|
+
echo,
|
|
563
|
+
summary_only,
|
|
564
|
+
"Reverted migration %s in %dms",
|
|
565
|
+
version,
|
|
566
|
+
execution_time,
|
|
567
|
+
rich_message=f"[green]✓ Reverted in {execution_time}ms[/]",
|
|
568
|
+
)
|
|
569
|
+
return execution_time
|
|
570
|
+
|
|
571
|
+
def _synchronize_version_records(
|
|
572
|
+
self, driver: Any, *, use_logger: bool = False, echo: bool = True, summary_only: bool = False
|
|
573
|
+
) -> int:
|
|
574
|
+
"""Synchronize database version records with migration files.
|
|
575
|
+
|
|
576
|
+
Auto-updates DB tracking when migrations have been renamed by fix command.
|
|
577
|
+
This allows developers to just run upgrade after pulling changes without
|
|
578
|
+
manually running fix.
|
|
579
|
+
|
|
580
|
+
Validates checksums match before updating to prevent incorrect matches.
|
|
581
|
+
|
|
582
|
+
Args:
|
|
583
|
+
driver: Database driver instance.
|
|
584
|
+
use_logger: If True, output to logger instead of Rich console.
|
|
585
|
+
echo: Whether to echo output to the console.
|
|
586
|
+
summary_only: Whether summary-only logging is enabled.
|
|
587
|
+
|
|
588
|
+
Returns:
|
|
589
|
+
Number of version records updated.
|
|
590
|
+
"""
|
|
591
|
+
all_migrations = self.runner.get_migration_files()
|
|
592
|
+
|
|
593
|
+
try:
|
|
594
|
+
applied_migrations = self.tracker.get_applied_migrations(driver)
|
|
595
|
+
except Exception as exc:
|
|
596
|
+
log_with_context(
|
|
597
|
+
logger,
|
|
598
|
+
logging.DEBUG,
|
|
599
|
+
"migration.list",
|
|
600
|
+
db_system=resolve_db_system(type(driver).__name__),
|
|
601
|
+
error_type=type(exc).__name__,
|
|
602
|
+
status="failed",
|
|
603
|
+
operation="applied_fetch",
|
|
604
|
+
)
|
|
605
|
+
return 0
|
|
606
|
+
|
|
607
|
+
applied_map = {m["version_num"]: m for m in applied_migrations}
|
|
608
|
+
|
|
609
|
+
conversion_map = generate_conversion_map(all_migrations)
|
|
610
|
+
|
|
611
|
+
updated_count = 0
|
|
612
|
+
if conversion_map:
|
|
613
|
+
for old_version, new_version in conversion_map.items():
|
|
614
|
+
if old_version in applied_map and new_version not in applied_map:
|
|
615
|
+
applied_checksum = applied_map[old_version]["checksum"]
|
|
616
|
+
|
|
617
|
+
file_path = next((path for v, path in all_migrations if v == new_version), None)
|
|
618
|
+
if file_path:
|
|
619
|
+
migration = self.runner.load_migration(file_path, new_version)
|
|
620
|
+
if migration["checksum"] == applied_checksum:
|
|
621
|
+
self.tracker.update_version_record(driver, old_version, new_version)
|
|
622
|
+
if use_logger:
|
|
623
|
+
if not summary_only:
|
|
624
|
+
logger.info("Reconciled version: %s -> %s", old_version, new_version)
|
|
625
|
+
elif echo:
|
|
626
|
+
console.print(f" [dim]Reconciled version:[/] {old_version} → {new_version}")
|
|
627
|
+
updated_count += 1
|
|
628
|
+
elif use_logger:
|
|
629
|
+
logger.warning(
|
|
630
|
+
"Checksum mismatch for %s -> %s, skipping auto-sync", old_version, new_version
|
|
631
|
+
)
|
|
632
|
+
elif echo:
|
|
633
|
+
console.print(
|
|
634
|
+
f" [yellow]Warning: Checksum mismatch for {old_version} → {new_version}, skipping auto-sync[/]"
|
|
635
|
+
)
|
|
636
|
+
else:
|
|
637
|
+
file_checksums = self._load_migration_checksums(all_migrations)
|
|
638
|
+
|
|
639
|
+
for applied_version, applied_record in applied_map.items():
|
|
640
|
+
for file_version, (file_checksum, _) in file_checksums.items():
|
|
641
|
+
if file_version not in applied_map and applied_record["checksum"] == file_checksum:
|
|
642
|
+
self.tracker.update_version_record(driver, applied_version, file_version)
|
|
643
|
+
if use_logger:
|
|
644
|
+
if not summary_only:
|
|
645
|
+
logger.info("Reconciled version: %s -> %s", applied_version, file_version)
|
|
646
|
+
elif echo:
|
|
647
|
+
console.print(f" [dim]Reconciled version:[/] {applied_version} → {file_version}")
|
|
648
|
+
updated_count += 1
|
|
649
|
+
break
|
|
650
|
+
|
|
651
|
+
if updated_count > 0:
|
|
652
|
+
if use_logger:
|
|
653
|
+
if not summary_only:
|
|
654
|
+
logger.info("Reconciled %d version record(s)", updated_count)
|
|
655
|
+
elif echo:
|
|
656
|
+
console.print(f"[cyan]Reconciled {updated_count} version record(s)[/]")
|
|
657
|
+
|
|
658
|
+
return updated_count
|
|
659
|
+
|
|
660
|
+
@_with_command_span("upgrade", metadata_fn=_upgrade_metadata)
|
|
661
|
+
def upgrade(
|
|
662
|
+
self,
|
|
663
|
+
revision: str = "head",
|
|
664
|
+
allow_missing: bool = False,
|
|
665
|
+
auto_sync: bool = True,
|
|
666
|
+
dry_run: bool = False,
|
|
667
|
+
*,
|
|
668
|
+
use_logger: bool = False,
|
|
669
|
+
echo: bool | None = None,
|
|
670
|
+
summary_only: bool | None = None,
|
|
671
|
+
) -> None:
|
|
672
|
+
"""Upgrade to a target revision.
|
|
673
|
+
|
|
674
|
+
Validates migration order and warns if out-of-order migrations are detected.
|
|
675
|
+
Out-of-order migrations can occur when branches merge in different orders
|
|
676
|
+
across environments.
|
|
677
|
+
|
|
678
|
+
Args:
|
|
679
|
+
revision: Target revision or "head" for latest.
|
|
680
|
+
allow_missing: If True, allow out-of-order migrations even in strict mode.
|
|
681
|
+
Defaults to False.
|
|
682
|
+
auto_sync: If True, automatically reconcile renamed migrations in database.
|
|
683
|
+
Defaults to True. Can be disabled via --no-auto-sync flag.
|
|
684
|
+
dry_run: If True, show what would be done without making changes.
|
|
685
|
+
use_logger: If True, output to logger instead of Rich console.
|
|
686
|
+
Defaults to False. Can be set via MigrationConfig for persistent default.
|
|
687
|
+
echo: Echo output to the console. Defaults to True when unset.
|
|
688
|
+
summary_only: Emit a single summary log entry when logger output is enabled.
|
|
689
|
+
"""
|
|
690
|
+
runtime = self._runtime
|
|
691
|
+
applied_count = 0
|
|
692
|
+
pending_count = 0
|
|
693
|
+
db_system: str | None = None
|
|
694
|
+
error: Exception | None = None
|
|
695
|
+
ul, echo_value, summary_value = self._resolve_output_policy(use_logger, echo, summary_only)
|
|
696
|
+
self.runner.set_use_logger(ul)
|
|
697
|
+
self.runner.set_summary_only(summary_value)
|
|
698
|
+
self.tracker.set_output_policy(use_logger=ul, echo=echo_value, summary_only=summary_value)
|
|
699
|
+
output_info = functools.partial(_output_info, ul, echo_value, summary_value)
|
|
700
|
+
start_time = time.perf_counter()
|
|
701
|
+
|
|
702
|
+
try:
|
|
703
|
+
if dry_run:
|
|
704
|
+
output_info(
|
|
705
|
+
"DRY RUN MODE: No database changes will be applied",
|
|
706
|
+
rich_message="[bold yellow]DRY RUN MODE:[/] No database changes will be applied\n",
|
|
707
|
+
)
|
|
708
|
+
|
|
709
|
+
with self.config.provide_session() as driver:
|
|
710
|
+
db_system = resolve_db_system(type(driver).__name__)
|
|
711
|
+
self.tracker.ensure_tracking_table(driver)
|
|
712
|
+
|
|
713
|
+
if auto_sync and self.config.migration_config.get("auto_sync", True):
|
|
714
|
+
self._synchronize_version_records(
|
|
715
|
+
driver, use_logger=ul, echo=echo_value, summary_only=summary_value
|
|
716
|
+
)
|
|
717
|
+
|
|
718
|
+
applied_migrations = self.tracker.get_applied_migrations(driver)
|
|
719
|
+
applied_versions = [m["version_num"] for m in applied_migrations]
|
|
720
|
+
applied_set = set(applied_versions)
|
|
721
|
+
|
|
722
|
+
all_migrations = self.runner.get_migration_files()
|
|
723
|
+
if runtime is not None:
|
|
724
|
+
runtime.increment_metric("migrations.command.upgrade.available", float(len(all_migrations)))
|
|
725
|
+
|
|
726
|
+
pending = self._collect_pending_migrations(all_migrations, applied_set, revision)
|
|
727
|
+
pending_count = len(pending)
|
|
728
|
+
|
|
729
|
+
if runtime is not None:
|
|
730
|
+
runtime.increment_metric("migrations.command.upgrade.pending", float(len(pending)))
|
|
731
|
+
|
|
732
|
+
if not pending:
|
|
733
|
+
self._report_no_pending_migrations(ul, echo_value, summary_value, bool(all_migrations))
|
|
734
|
+
return
|
|
735
|
+
|
|
736
|
+
migration_config = cast("dict[str, Any]", self.config.migration_config) or {}
|
|
737
|
+
strict_ordering = migration_config.get("strict_ordering", False) and not allow_missing
|
|
738
|
+
validate_migration_order(
|
|
739
|
+
[v for v, _ in pending],
|
|
740
|
+
applied_versions,
|
|
741
|
+
strict_ordering,
|
|
742
|
+
use_logger=ul,
|
|
743
|
+
echo=echo_value,
|
|
744
|
+
summary_only=summary_value,
|
|
745
|
+
)
|
|
746
|
+
|
|
747
|
+
output_info(
|
|
748
|
+
"Found %d pending migrations",
|
|
749
|
+
len(pending),
|
|
750
|
+
rich_message=f"[yellow]Found {len(pending)} pending migrations[/]",
|
|
751
|
+
)
|
|
752
|
+
|
|
753
|
+
for version, file_path in pending:
|
|
754
|
+
migration = self.runner.load_migration(file_path, version)
|
|
755
|
+
action_verb = "Would apply" if dry_run else "Applying"
|
|
756
|
+
output_info(
|
|
757
|
+
"%s %s: %s",
|
|
758
|
+
action_verb,
|
|
759
|
+
version,
|
|
760
|
+
migration["description"],
|
|
761
|
+
rich_message=f"\n[cyan]{action_verb} {version}:[/] {migration['description']}",
|
|
762
|
+
)
|
|
763
|
+
|
|
764
|
+
if dry_run:
|
|
765
|
+
output_info(
|
|
766
|
+
"Migration file: %s", file_path, rich_message=f"[dim]Migration file: {file_path}[/]"
|
|
767
|
+
)
|
|
768
|
+
continue
|
|
769
|
+
|
|
770
|
+
result = self._apply_single_migration(driver, migration, version, ul, echo_value, summary_value)
|
|
771
|
+
if result is None:
|
|
772
|
+
return
|
|
773
|
+
applied_count += 1
|
|
774
|
+
|
|
775
|
+
except Exception as exc: # pragma: no cover - passthrough
|
|
776
|
+
error = exc
|
|
777
|
+
raise
|
|
778
|
+
finally:
|
|
779
|
+
duration_ms = int((time.perf_counter() - start_time) * 1000)
|
|
780
|
+
_log_command_summary(
|
|
781
|
+
use_logger=ul,
|
|
782
|
+
summary_only=summary_value,
|
|
783
|
+
command="upgrade",
|
|
784
|
+
status="failed" if error else "complete",
|
|
785
|
+
revision=revision,
|
|
786
|
+
dry_run=dry_run,
|
|
787
|
+
pending_count=pending_count,
|
|
788
|
+
applied_count=applied_count,
|
|
789
|
+
reverted_count=None,
|
|
790
|
+
duration_ms=duration_ms,
|
|
791
|
+
db_system=db_system,
|
|
792
|
+
bind_key=getattr(self.config, "bind_key", None),
|
|
793
|
+
config_name=type(self.config).__name__,
|
|
794
|
+
error=error,
|
|
795
|
+
allow_missing=allow_missing,
|
|
796
|
+
auto_sync=auto_sync,
|
|
797
|
+
)
|
|
798
|
+
|
|
799
|
+
if dry_run:
|
|
800
|
+
output_info(
|
|
801
|
+
"Dry run complete. No changes were made to the database.",
|
|
802
|
+
rich_message="\n[bold yellow]Dry run complete.[/] No changes were made to the database.",
|
|
803
|
+
)
|
|
804
|
+
elif applied_count:
|
|
805
|
+
self._record_command_metric("applied", float(applied_count))
|
|
806
|
+
|
|
807
|
+
@_with_command_span("downgrade", metadata_fn=_downgrade_metadata)
|
|
808
|
+
def downgrade(
|
|
809
|
+
self,
|
|
810
|
+
revision: str = "-1",
|
|
811
|
+
*,
|
|
812
|
+
dry_run: bool = False,
|
|
813
|
+
use_logger: bool = False,
|
|
814
|
+
echo: bool | None = None,
|
|
815
|
+
summary_only: bool | None = None,
|
|
816
|
+
) -> None:
|
|
817
|
+
"""Downgrade to a target revision.
|
|
818
|
+
|
|
819
|
+
Args:
|
|
820
|
+
revision: Target revision or "-1" for one step back.
|
|
821
|
+
dry_run: If True, show what would be done without making changes.
|
|
822
|
+
use_logger: If True, output to logger instead of Rich console.
|
|
823
|
+
Defaults to False. Can be set via MigrationConfig for persistent default.
|
|
824
|
+
echo: Echo output to the console. Defaults to True when unset.
|
|
825
|
+
summary_only: Emit a single summary log entry when logger output is enabled.
|
|
826
|
+
"""
|
|
827
|
+
runtime = self._runtime
|
|
828
|
+
reverted_count = 0
|
|
829
|
+
pending_count = 0
|
|
830
|
+
db_system: str | None = None
|
|
831
|
+
error: Exception | None = None
|
|
832
|
+
ul, echo_value, summary_value = self._resolve_output_policy(use_logger, echo, summary_only)
|
|
833
|
+
self.runner.set_use_logger(ul)
|
|
834
|
+
self.runner.set_summary_only(summary_value)
|
|
835
|
+
self.tracker.set_output_policy(use_logger=ul, echo=echo_value, summary_only=summary_value)
|
|
836
|
+
output_info = functools.partial(_output_info, ul, echo_value, summary_value)
|
|
837
|
+
output_error = functools.partial(_output_error, ul, echo_value, summary_value)
|
|
838
|
+
start_time = time.perf_counter()
|
|
839
|
+
|
|
840
|
+
try:
|
|
841
|
+
if dry_run:
|
|
842
|
+
output_info(
|
|
843
|
+
"DRY RUN MODE: No database changes will be applied",
|
|
844
|
+
rich_message="[bold yellow]DRY RUN MODE:[/] No database changes will be applied\n",
|
|
845
|
+
)
|
|
846
|
+
|
|
847
|
+
with self.config.provide_session() as driver:
|
|
848
|
+
db_system = resolve_db_system(type(driver).__name__)
|
|
849
|
+
self.tracker.ensure_tracking_table(driver)
|
|
850
|
+
applied = self.tracker.get_applied_migrations(driver)
|
|
851
|
+
if runtime is not None:
|
|
852
|
+
runtime.increment_metric("migrations.command.downgrade.available", float(len(applied)))
|
|
853
|
+
if not applied:
|
|
854
|
+
output_info("No migrations to downgrade", rich_message="[yellow]No migrations to downgrade[/]")
|
|
855
|
+
return
|
|
856
|
+
|
|
857
|
+
to_revert = self._collect_revert_migrations(applied, revision)
|
|
858
|
+
pending_count = len(to_revert)
|
|
859
|
+
|
|
860
|
+
if runtime is not None:
|
|
861
|
+
runtime.increment_metric("migrations.command.downgrade.pending", float(len(to_revert)))
|
|
862
|
+
|
|
863
|
+
if not to_revert:
|
|
864
|
+
output_info("Nothing to downgrade", rich_message="[yellow]Nothing to downgrade[/]")
|
|
865
|
+
return
|
|
866
|
+
|
|
867
|
+
output_info(
|
|
868
|
+
"Reverting %d migrations",
|
|
869
|
+
len(to_revert),
|
|
870
|
+
rich_message=f"[yellow]Reverting {len(to_revert)} migrations[/]",
|
|
871
|
+
)
|
|
872
|
+
all_files = dict(self.runner.get_migration_files())
|
|
873
|
+
|
|
874
|
+
for migration_record in to_revert:
|
|
875
|
+
version = migration_record["version_num"]
|
|
876
|
+
if version not in all_files:
|
|
877
|
+
output_error(
|
|
878
|
+
"Migration file not found for %s",
|
|
879
|
+
version,
|
|
880
|
+
rich_message=f"[red]Migration file not found for {version}[/]",
|
|
881
|
+
)
|
|
882
|
+
if runtime is not None:
|
|
883
|
+
runtime.increment_metric("migrations.command.downgrade.missing_files")
|
|
884
|
+
continue
|
|
885
|
+
|
|
886
|
+
migration = self.runner.load_migration(all_files[version], version)
|
|
887
|
+
action_verb = "Would revert" if dry_run else "Reverting"
|
|
888
|
+
output_info(
|
|
889
|
+
"%s %s: %s",
|
|
890
|
+
action_verb,
|
|
891
|
+
version,
|
|
892
|
+
migration["description"],
|
|
893
|
+
rich_message=f"\n[cyan]{action_verb} {version}:[/] {migration['description']}",
|
|
894
|
+
)
|
|
895
|
+
|
|
896
|
+
if dry_run:
|
|
897
|
+
output_info(
|
|
898
|
+
"Migration file: %s",
|
|
899
|
+
all_files[version],
|
|
900
|
+
rich_message=f"[dim]Migration file: {all_files[version]}[/]",
|
|
901
|
+
)
|
|
902
|
+
continue
|
|
903
|
+
|
|
904
|
+
result = self._revert_single_migration(driver, migration, version, ul, echo_value, summary_value)
|
|
905
|
+
if result is None:
|
|
906
|
+
return
|
|
907
|
+
reverted_count += 1
|
|
908
|
+
|
|
909
|
+
except Exception as exc: # pragma: no cover - passthrough
|
|
910
|
+
error = exc
|
|
911
|
+
raise
|
|
912
|
+
finally:
|
|
913
|
+
duration_ms = int((time.perf_counter() - start_time) * 1000)
|
|
914
|
+
_log_command_summary(
|
|
915
|
+
use_logger=ul,
|
|
916
|
+
summary_only=summary_value,
|
|
917
|
+
command="downgrade",
|
|
918
|
+
status="failed" if error else "complete",
|
|
919
|
+
revision=revision,
|
|
920
|
+
dry_run=dry_run,
|
|
921
|
+
pending_count=pending_count,
|
|
922
|
+
applied_count=None,
|
|
923
|
+
reverted_count=reverted_count,
|
|
924
|
+
duration_ms=duration_ms,
|
|
925
|
+
db_system=db_system,
|
|
926
|
+
bind_key=getattr(self.config, "bind_key", None),
|
|
927
|
+
config_name=type(self.config).__name__,
|
|
928
|
+
error=error,
|
|
929
|
+
)
|
|
930
|
+
|
|
931
|
+
if dry_run:
|
|
932
|
+
output_info(
|
|
933
|
+
"Dry run complete. No changes were made to the database.",
|
|
934
|
+
rich_message="\n[bold yellow]Dry run complete.[/] No changes were made to the database.",
|
|
935
|
+
)
|
|
936
|
+
elif reverted_count:
|
|
937
|
+
self._record_command_metric("applied", float(reverted_count))
|
|
938
|
+
|
|
939
|
+
def stamp(self, revision: str) -> None:
|
|
940
|
+
"""Mark database as being at a specific revision without running migrations.
|
|
941
|
+
|
|
942
|
+
Args:
|
|
943
|
+
revision: The revision to stamp.
|
|
944
|
+
"""
|
|
945
|
+
with self.config.provide_session() as driver:
|
|
946
|
+
self.tracker.ensure_tracking_table(driver)
|
|
947
|
+
all_migrations = dict(self.runner.get_migration_files())
|
|
948
|
+
if revision not in all_migrations:
|
|
949
|
+
console.print(f"[red]Unknown revision: {revision}[/]")
|
|
950
|
+
return
|
|
951
|
+
clear_sql = sql.delete().from_(self.tracker.version_table)
|
|
952
|
+
driver.execute(clear_sql)
|
|
953
|
+
self.tracker.record_migration(driver, revision, f"Stamped to {revision}", 0, "manual-stamp")
|
|
954
|
+
console.print(f"[green]Database stamped at revision {revision}[/]")
|
|
955
|
+
|
|
956
|
+
def revision(self, message: str, file_type: str | None = None) -> None:
|
|
957
|
+
"""Create a new migration file with timestamp-based versioning.
|
|
958
|
+
|
|
959
|
+
Generates a unique timestamp version (YYYYMMDDHHmmss format) to avoid
|
|
960
|
+
conflicts when multiple developers create migrations concurrently.
|
|
961
|
+
|
|
962
|
+
Args:
|
|
963
|
+
message: Description for the migration.
|
|
964
|
+
file_type: Type of migration file to create ('sql' or 'py').
|
|
965
|
+
"""
|
|
966
|
+
version = generate_timestamp_version()
|
|
967
|
+
selected_format = file_type or self._template_settings.default_format
|
|
968
|
+
file_path = create_migration_file(
|
|
969
|
+
self.migrations_path,
|
|
970
|
+
version,
|
|
971
|
+
message,
|
|
972
|
+
selected_format,
|
|
973
|
+
config=self.config,
|
|
974
|
+
template_settings=self._template_settings,
|
|
975
|
+
)
|
|
976
|
+
log_with_context(
|
|
977
|
+
logger,
|
|
978
|
+
logging.DEBUG,
|
|
979
|
+
"migration.create",
|
|
980
|
+
db_system=resolve_db_system(type(self.config).__name__),
|
|
981
|
+
version=version,
|
|
982
|
+
file_path=str(file_path),
|
|
983
|
+
file_type=selected_format,
|
|
984
|
+
description=message,
|
|
985
|
+
)
|
|
986
|
+
console.print(f"[green]Created migration:[/] {file_path}")
|
|
987
|
+
|
|
988
|
+
def squash(
|
|
989
|
+
self,
|
|
990
|
+
start_version: str | None = None,
|
|
991
|
+
end_version: str | None = None,
|
|
992
|
+
description: str | None = None,
|
|
993
|
+
*,
|
|
994
|
+
dry_run: bool = False,
|
|
995
|
+
update_database: bool = True,
|
|
996
|
+
yes: bool = False,
|
|
997
|
+
allow_gaps: bool = False,
|
|
998
|
+
output_format: str = "sql",
|
|
999
|
+
) -> None:
|
|
1000
|
+
"""Squash a range of migrations into a single file.
|
|
1001
|
+
|
|
1002
|
+
Combines multiple sequential migrations into a single "release" migration.
|
|
1003
|
+
UP statements are merged in version order, DOWN statements in reverse order.
|
|
1004
|
+
|
|
1005
|
+
Args:
|
|
1006
|
+
start_version: First version in the range to squash (inclusive).
|
|
1007
|
+
When None, defaults to the first sequential migration found.
|
|
1008
|
+
end_version: Last version in the range to squash (inclusive).
|
|
1009
|
+
When None, defaults to the last sequential migration found.
|
|
1010
|
+
description: Description for the squashed migration file.
|
|
1011
|
+
When None, prompts interactively.
|
|
1012
|
+
dry_run: Preview changes without applying.
|
|
1013
|
+
update_database: Update migration records in database.
|
|
1014
|
+
yes: Skip confirmation prompt.
|
|
1015
|
+
allow_gaps: Allow gaps in version sequence.
|
|
1016
|
+
output_format: Output format ("sql" or "py").
|
|
1017
|
+
|
|
1018
|
+
Raises:
|
|
1019
|
+
SquashValidationError: If validation fails (invalid range, gaps, etc.).
|
|
1020
|
+
"""
|
|
1021
|
+
squasher = MigrationSquasher(self.migrations_path, self.runner, self._template_settings)
|
|
1022
|
+
|
|
1023
|
+
# Infer start/end from all sequential migrations when not provided
|
|
1024
|
+
if start_version is None or end_version is None:
|
|
1025
|
+
all_migrations = self.runner.get_migration_files()
|
|
1026
|
+
sequential = [(v, p) for v, p in all_migrations if v.isdigit() or v.lstrip("0").isdigit()]
|
|
1027
|
+
if not sequential:
|
|
1028
|
+
console.print("[yellow]No sequential migrations found to squash[/]")
|
|
1029
|
+
return
|
|
1030
|
+
if start_version is None:
|
|
1031
|
+
start_version = sequential[0][0]
|
|
1032
|
+
if end_version is None:
|
|
1033
|
+
end_version = sequential[-1][0]
|
|
1034
|
+
console.print(f"[cyan]Squashing range: {start_version} to {end_version}[/]")
|
|
1035
|
+
|
|
1036
|
+
# Prompt for description when not provided
|
|
1037
|
+
if description is None:
|
|
1038
|
+
from rich.prompt import Prompt
|
|
1039
|
+
|
|
1040
|
+
description = Prompt.ask("Migration description", default="squashed_migrations")
|
|
1041
|
+
|
|
1042
|
+
plans = squasher.plan_squash(
|
|
1043
|
+
start_version, end_version, description, allow_gaps=allow_gaps, output_format=output_format
|
|
1044
|
+
)
|
|
1045
|
+
|
|
1046
|
+
# Display plan for each squash group
|
|
1047
|
+
table = Table(title="Squash Plan")
|
|
1048
|
+
table.add_column("Version", style="cyan")
|
|
1049
|
+
table.add_column("File")
|
|
1050
|
+
table.add_column("Target", style="green")
|
|
1051
|
+
|
|
1052
|
+
total_migrations = 0
|
|
1053
|
+
for plan in plans:
|
|
1054
|
+
for version, file_path in plan.source_migrations:
|
|
1055
|
+
table.add_row(version, file_path.name, plan.target_path.name)
|
|
1056
|
+
total_migrations += 1
|
|
1057
|
+
|
|
1058
|
+
console.print(table)
|
|
1059
|
+
target_files = ", ".join(p.target_path.name for p in plans)
|
|
1060
|
+
console.print(
|
|
1061
|
+
f"\n[yellow]{total_migrations} migrations will be squashed into {len(plans)} file(s): {target_files}[/]"
|
|
1062
|
+
)
|
|
1063
|
+
|
|
1064
|
+
if dry_run:
|
|
1065
|
+
console.print("[yellow][Preview Mode - No changes made][/]")
|
|
1066
|
+
return
|
|
1067
|
+
|
|
1068
|
+
if not yes:
|
|
1069
|
+
response = input("\nProceed with squash? [y/N]: ")
|
|
1070
|
+
if response.lower() != "y":
|
|
1071
|
+
console.print("[yellow]Squash cancelled[/]")
|
|
1072
|
+
return
|
|
1073
|
+
|
|
1074
|
+
squasher.apply_squash(plans)
|
|
1075
|
+
for plan in plans:
|
|
1076
|
+
console.print(f"[green]✓ Created squashed migration: {plan.target_path.name}[/]")
|
|
1077
|
+
|
|
1078
|
+
if update_database:
|
|
1079
|
+
with self.config.provide_session() as driver:
|
|
1080
|
+
self.tracker.ensure_tracking_table(driver)
|
|
1081
|
+
|
|
1082
|
+
for plan in plans:
|
|
1083
|
+
if self.tracker.is_squash_already_applied(driver, plan.target_version, plan.source_versions):
|
|
1084
|
+
up_sql, down_sql = squasher.extract_sql(plan.source_migrations)
|
|
1085
|
+
if plan.target_path.suffix == ".py":
|
|
1086
|
+
content = squasher.generate_python_squash(plan, up_sql, down_sql)
|
|
1087
|
+
else:
|
|
1088
|
+
content = squasher.generate_squashed_content(plan, up_sql, down_sql)
|
|
1089
|
+
checksum = self.runner.calculate_checksum(content)
|
|
1090
|
+
|
|
1091
|
+
self.tracker.replace_with_squash(
|
|
1092
|
+
driver, plan.target_version, plan.source_versions, description, checksum
|
|
1093
|
+
)
|
|
1094
|
+
console.print("[green]✓ Updated migration tracking table[/]")
|
|
1095
|
+
|
|
1096
|
+
console.print("[green]✓ Squash complete![/]")
|
|
1097
|
+
|
|
1098
|
+
def fix(self, dry_run: bool = False, update_database: bool = True, yes: bool = False) -> None:
|
|
1099
|
+
"""Convert timestamp migrations to sequential format.
|
|
1100
|
+
|
|
1101
|
+
Implements hybrid versioning workflow where development uses timestamps
|
|
1102
|
+
and production uses sequential numbers. Creates backup before changes
|
|
1103
|
+
and provides rollback on errors.
|
|
1104
|
+
|
|
1105
|
+
Args:
|
|
1106
|
+
dry_run: Preview changes without applying.
|
|
1107
|
+
update_database: Update migration records in database.
|
|
1108
|
+
yes: Skip confirmation prompt.
|
|
1109
|
+
|
|
1110
|
+
Examples:
|
|
1111
|
+
>>> commands.fix(dry_run=True) # Preview only
|
|
1112
|
+
>>> commands.fix(yes=True) # Auto-approve
|
|
1113
|
+
>>> commands.fix(update_database=False) # Files only
|
|
1114
|
+
"""
|
|
1115
|
+
all_migrations = self.runner.get_migration_files()
|
|
1116
|
+
|
|
1117
|
+
conversion_map = generate_conversion_map(all_migrations)
|
|
1118
|
+
|
|
1119
|
+
if not conversion_map:
|
|
1120
|
+
console.print("[yellow]No timestamp migrations found - nothing to convert[/]")
|
|
1121
|
+
return
|
|
1122
|
+
|
|
1123
|
+
fixer = MigrationFixer(self.migrations_path)
|
|
1124
|
+
renames = fixer.plan_renames(conversion_map)
|
|
1125
|
+
|
|
1126
|
+
table = Table(title="Migration Conversions")
|
|
1127
|
+
table.add_column("Current Version", style="cyan")
|
|
1128
|
+
table.add_column("New Version", style="green")
|
|
1129
|
+
table.add_column("File")
|
|
1130
|
+
|
|
1131
|
+
for rename in renames:
|
|
1132
|
+
table.add_row(rename.old_version, rename.new_version, rename.old_path.name)
|
|
1133
|
+
|
|
1134
|
+
console.print(table)
|
|
1135
|
+
console.print(f"\n[yellow]{len(renames)} migrations will be converted[/]")
|
|
1136
|
+
|
|
1137
|
+
if dry_run:
|
|
1138
|
+
console.print("[yellow][Preview Mode - No changes made][/]")
|
|
1139
|
+
return
|
|
1140
|
+
|
|
1141
|
+
if not yes:
|
|
1142
|
+
response = input("\nProceed with conversion? [y/N]: ")
|
|
1143
|
+
if response.lower() != "y":
|
|
1144
|
+
console.print("[yellow]Conversion cancelled[/]")
|
|
1145
|
+
return
|
|
1146
|
+
|
|
1147
|
+
try:
|
|
1148
|
+
backup_path = fixer.create_backup()
|
|
1149
|
+
console.print(f"[green]✓ Created backup in {backup_path.name}[/]")
|
|
1150
|
+
|
|
1151
|
+
fixer.apply_renames(renames)
|
|
1152
|
+
for rename in renames:
|
|
1153
|
+
console.print(f"[green]✓ Renamed {rename.old_path.name} → {rename.new_path.name}[/]")
|
|
1154
|
+
|
|
1155
|
+
if update_database:
|
|
1156
|
+
with self.config.provide_session() as driver:
|
|
1157
|
+
self.tracker.ensure_tracking_table(driver)
|
|
1158
|
+
applied_migrations = self.tracker.get_applied_migrations(driver)
|
|
1159
|
+
applied_versions = {m["version_num"] for m in applied_migrations}
|
|
1160
|
+
|
|
1161
|
+
updated_count = 0
|
|
1162
|
+
for old_version, new_version in conversion_map.items():
|
|
1163
|
+
if old_version in applied_versions:
|
|
1164
|
+
self.tracker.update_version_record(driver, old_version, new_version)
|
|
1165
|
+
updated_count += 1
|
|
1166
|
+
|
|
1167
|
+
if updated_count > 0:
|
|
1168
|
+
console.print(
|
|
1169
|
+
f"[green]✓ Updated {updated_count} version records in migration tracking table[/]"
|
|
1170
|
+
)
|
|
1171
|
+
else:
|
|
1172
|
+
console.print("[green]✓ No applied migrations to update in tracking table[/]")
|
|
1173
|
+
|
|
1174
|
+
fixer.cleanup()
|
|
1175
|
+
console.print("[green]✓ Conversion complete![/]")
|
|
1176
|
+
|
|
1177
|
+
except Exception as e:
|
|
1178
|
+
console.print(f"[red]✗ Error: {e}[/]")
|
|
1179
|
+
fixer.rollback()
|
|
1180
|
+
console.print("[yellow]Restored files from backup[/]")
|
|
1181
|
+
raise
|
|
1182
|
+
|
|
1183
|
+
|
|
1184
|
+
class AsyncMigrationCommands(BaseMigrationCommands["AsyncConfigT", Any]):
|
|
1185
|
+
"""Asynchronous migration commands."""
|
|
1186
|
+
|
|
1187
|
+
def __init__(self, config: "AsyncConfigT") -> None:
|
|
1188
|
+
"""Initialize migration commands.
|
|
1189
|
+
|
|
1190
|
+
Args:
|
|
1191
|
+
config: The SQLSpec configuration.
|
|
1192
|
+
"""
|
|
1193
|
+
super().__init__(config)
|
|
1194
|
+
self.tracker = config.migration_tracker_type(self.version_table)
|
|
1195
|
+
|
|
1196
|
+
# Create context with extension configurations
|
|
1197
|
+
context = MigrationContext.from_config(config)
|
|
1198
|
+
context.extension_config = self.extension_configs
|
|
1199
|
+
|
|
1200
|
+
self.runner = AsyncMigrationRunner(
|
|
1201
|
+
self.migrations_path,
|
|
1202
|
+
self._discover_extension_migrations(),
|
|
1203
|
+
context,
|
|
1204
|
+
self.extension_configs,
|
|
1205
|
+
runtime=self._runtime,
|
|
1206
|
+
description_hints=self._template_settings.description_hints,
|
|
1207
|
+
)
|
|
1208
|
+
|
|
1209
|
+
async def init(self, directory: str, package: bool = True) -> None:
|
|
1210
|
+
"""Initialize migration directory structure.
|
|
1211
|
+
|
|
1212
|
+
Args:
|
|
1213
|
+
directory: Directory path for migrations.
|
|
1214
|
+
package: Whether to create __init__.py in the directory.
|
|
1215
|
+
"""
|
|
1216
|
+
self.init_directory(directory, package)
|
|
1217
|
+
|
|
1218
|
+
async def current(self, verbose: bool = False) -> "str | None":
|
|
1219
|
+
"""Show current migration version.
|
|
1220
|
+
|
|
1221
|
+
Args:
|
|
1222
|
+
verbose: Whether to show detailed migration history.
|
|
1223
|
+
|
|
1224
|
+
Returns:
|
|
1225
|
+
The current migration version or None if no migrations applied.
|
|
1226
|
+
"""
|
|
1227
|
+
async with self.config.provide_session() as driver:
|
|
1228
|
+
await self.tracker.ensure_tracking_table(driver)
|
|
1229
|
+
|
|
1230
|
+
current = await self.tracker.get_current_version(driver)
|
|
1231
|
+
if not current:
|
|
1232
|
+
log_with_context(
|
|
1233
|
+
logger,
|
|
1234
|
+
logging.DEBUG,
|
|
1235
|
+
"migration.list",
|
|
1236
|
+
db_system=resolve_db_system(type(driver).__name__),
|
|
1237
|
+
current_version=None,
|
|
1238
|
+
applied_count=0,
|
|
1239
|
+
verbose=verbose,
|
|
1240
|
+
status="empty",
|
|
1241
|
+
)
|
|
1242
|
+
console.print("[yellow]No migrations applied yet[/]")
|
|
1243
|
+
return None
|
|
1244
|
+
|
|
1245
|
+
console.print(f"[green]Current version:[/] {current}")
|
|
1246
|
+
applied: list[dict[str, Any]] = []
|
|
1247
|
+
if verbose:
|
|
1248
|
+
applied = await self.tracker.get_applied_migrations(driver)
|
|
1249
|
+
table = Table(title="Applied Migrations")
|
|
1250
|
+
table.add_column("Version", style="cyan")
|
|
1251
|
+
table.add_column("Description")
|
|
1252
|
+
table.add_column("Applied At")
|
|
1253
|
+
table.add_column("Time (ms)", justify="right")
|
|
1254
|
+
table.add_column("Applied By")
|
|
1255
|
+
for migration in applied:
|
|
1256
|
+
table.add_row(
|
|
1257
|
+
migration["version_num"],
|
|
1258
|
+
migration.get("description", ""),
|
|
1259
|
+
str(migration.get("applied_at", "")),
|
|
1260
|
+
str(migration.get("execution_time_ms", "")),
|
|
1261
|
+
migration.get("applied_by", ""),
|
|
1262
|
+
)
|
|
1263
|
+
console.print(table)
|
|
1264
|
+
|
|
1265
|
+
applied_count = len(applied) if verbose else None
|
|
1266
|
+
log_with_context(
|
|
1267
|
+
logger,
|
|
1268
|
+
logging.DEBUG,
|
|
1269
|
+
"migration.list",
|
|
1270
|
+
db_system=resolve_db_system(type(driver).__name__),
|
|
1271
|
+
current_version=current,
|
|
1272
|
+
applied_count=applied_count,
|
|
1273
|
+
verbose=verbose,
|
|
1274
|
+
status="complete",
|
|
1275
|
+
)
|
|
1276
|
+
return cast("str | None", current)
|
|
1277
|
+
|
|
1278
|
+
async def _load_single_migration_checksum(
|
|
1279
|
+
self, version: str, file_path: "Path"
|
|
1280
|
+
) -> "tuple[str, tuple[str, Path]] | None":
|
|
1281
|
+
"""Load checksum for a single migration.
|
|
1282
|
+
|
|
1283
|
+
Args:
|
|
1284
|
+
version: Migration version.
|
|
1285
|
+
file_path: Path to migration file.
|
|
1286
|
+
|
|
1287
|
+
Returns:
|
|
1288
|
+
Tuple of (version, (checksum, file_path)) or None if load fails.
|
|
1289
|
+
"""
|
|
1290
|
+
try:
|
|
1291
|
+
migration = await self.runner.load_migration(file_path, version)
|
|
1292
|
+
return (version, (migration["checksum"], file_path))
|
|
1293
|
+
except Exception as exc:
|
|
1294
|
+
log_with_context(
|
|
1295
|
+
logger,
|
|
1296
|
+
logging.DEBUG,
|
|
1297
|
+
"migration.list",
|
|
1298
|
+
db_system=resolve_db_system(type(self.config).__name__),
|
|
1299
|
+
version=version,
|
|
1300
|
+
file_path=str(file_path),
|
|
1301
|
+
error_type=type(exc).__name__,
|
|
1302
|
+
status="failed",
|
|
1303
|
+
operation="load_checksum",
|
|
1304
|
+
)
|
|
1305
|
+
return None
|
|
1306
|
+
|
|
1307
|
+
async def _load_migration_checksums(
|
|
1308
|
+
self, all_migrations: "list[tuple[str, Path]]"
|
|
1309
|
+
) -> "dict[str, tuple[str, Path]]":
|
|
1310
|
+
"""Load checksums for all migrations.
|
|
1311
|
+
|
|
1312
|
+
Args:
|
|
1313
|
+
all_migrations: List of (version, file_path) tuples.
|
|
1314
|
+
|
|
1315
|
+
Returns:
|
|
1316
|
+
Dictionary mapping version to (checksum, file_path) tuples.
|
|
1317
|
+
"""
|
|
1318
|
+
file_checksums = {}
|
|
1319
|
+
for version, file_path in all_migrations:
|
|
1320
|
+
result = await self._load_single_migration_checksum(version, file_path)
|
|
1321
|
+
if result:
|
|
1322
|
+
file_checksums[result[0]] = result[1]
|
|
1323
|
+
return file_checksums
|
|
1324
|
+
|
|
1325
|
+
def _collect_pending_migrations(
|
|
1326
|
+
self, all_migrations: "list[tuple[str, Path]]", applied_set: set[str], revision: str
|
|
1327
|
+
) -> "list[tuple[str, Path]]":
|
|
1328
|
+
"""Collect pending migrations that need to be applied.
|
|
1329
|
+
|
|
1330
|
+
Args:
|
|
1331
|
+
all_migrations: List of all (version, file_path) tuples.
|
|
1332
|
+
applied_set: Set of already applied version strings.
|
|
1333
|
+
revision: Target revision ("head" or specific version).
|
|
1334
|
+
|
|
1335
|
+
Returns:
|
|
1336
|
+
List of (version, file_path) tuples for pending migrations.
|
|
1337
|
+
"""
|
|
1338
|
+
pending = []
|
|
1339
|
+
for version, file_path in all_migrations:
|
|
1340
|
+
if version not in applied_set:
|
|
1341
|
+
if revision == "head":
|
|
1342
|
+
pending.append((version, file_path))
|
|
1343
|
+
else:
|
|
1344
|
+
parsed_version = parse_version(version)
|
|
1345
|
+
parsed_revision = parse_version(revision)
|
|
1346
|
+
if parsed_version <= parsed_revision:
|
|
1347
|
+
pending.append((version, file_path))
|
|
1348
|
+
return pending
|
|
1349
|
+
|
|
1350
|
+
def _report_no_pending_migrations(
|
|
1351
|
+
self, use_logger: bool, echo: bool, summary_only: bool, has_migrations: bool
|
|
1352
|
+
) -> None:
|
|
1353
|
+
"""Report that there are no pending migrations.
|
|
1354
|
+
|
|
1355
|
+
Args:
|
|
1356
|
+
use_logger: Whether to output to logger instead of console.
|
|
1357
|
+
echo: Whether to echo output to the console.
|
|
1358
|
+
summary_only: Whether summary-only logging is enabled.
|
|
1359
|
+
has_migrations: Whether any migrations exist at all.
|
|
1360
|
+
"""
|
|
1361
|
+
if not has_migrations:
|
|
1362
|
+
_output_info(
|
|
1363
|
+
use_logger,
|
|
1364
|
+
echo,
|
|
1365
|
+
summary_only,
|
|
1366
|
+
"No migrations found. Create your first migration with 'sqlspec create-migration'.",
|
|
1367
|
+
rich_message="[yellow]No migrations found. Create your first migration with 'sqlspec create-migration'.[/]",
|
|
1368
|
+
)
|
|
1369
|
+
else:
|
|
1370
|
+
_output_info(
|
|
1371
|
+
use_logger,
|
|
1372
|
+
echo,
|
|
1373
|
+
summary_only,
|
|
1374
|
+
"Already at latest version",
|
|
1375
|
+
rich_message="[green]Already at latest version[/]",
|
|
1376
|
+
)
|
|
1377
|
+
|
|
1378
|
+
async def _apply_single_migration(
|
|
1379
|
+
self, driver: Any, migration: "dict[str, Any]", version: str, use_logger: bool, echo: bool, summary_only: bool
|
|
1380
|
+
) -> int | None:
|
|
1381
|
+
"""Apply a single migration and record it.
|
|
1382
|
+
|
|
1383
|
+
Args:
|
|
1384
|
+
driver: Database driver instance.
|
|
1385
|
+
migration: Migration dictionary with version, description, checksum.
|
|
1386
|
+
version: Version string.
|
|
1387
|
+
use_logger: Whether to output to logger instead of console.
|
|
1388
|
+
echo: Whether to echo output to the console.
|
|
1389
|
+
summary_only: Whether summary-only logging is enabled.
|
|
1390
|
+
|
|
1391
|
+
Returns:
|
|
1392
|
+
Execution time in ms on success, None on failure.
|
|
1393
|
+
"""
|
|
1394
|
+
try:
|
|
1395
|
+
|
|
1396
|
+
async def record_version(exec_time: int, migration: "dict[str, Any]" = migration) -> None:
|
|
1397
|
+
await self.tracker.record_migration(
|
|
1398
|
+
driver, migration["version"], migration["description"], exec_time, migration["checksum"]
|
|
1399
|
+
)
|
|
1400
|
+
|
|
1401
|
+
_, execution_time = await self.runner.execute_upgrade(driver, migration, on_success=record_version)
|
|
1402
|
+
except Exception as exc:
|
|
1403
|
+
use_txn = self.runner.should_use_transaction(migration, self.config)
|
|
1404
|
+
rollback_msg = " (transaction rolled back)" if use_txn else ""
|
|
1405
|
+
_output_exception(
|
|
1406
|
+
use_logger,
|
|
1407
|
+
echo,
|
|
1408
|
+
summary_only,
|
|
1409
|
+
"Migration %s failed%s",
|
|
1410
|
+
version,
|
|
1411
|
+
rollback_msg,
|
|
1412
|
+
rich_message=f"[red]✗ Failed{rollback_msg}: {exc}[/]",
|
|
1413
|
+
)
|
|
1414
|
+
self._last_command_error = exc
|
|
1415
|
+
return None
|
|
1416
|
+
else:
|
|
1417
|
+
_output_info(
|
|
1418
|
+
use_logger,
|
|
1419
|
+
echo,
|
|
1420
|
+
summary_only,
|
|
1421
|
+
"Applied migration %s in %dms",
|
|
1422
|
+
version,
|
|
1423
|
+
execution_time,
|
|
1424
|
+
rich_message=f"[green]✓ Applied in {execution_time}ms[/]",
|
|
1425
|
+
)
|
|
1426
|
+
return execution_time
|
|
1427
|
+
|
|
1428
|
+
def _collect_revert_migrations(self, applied: "list[dict[str, Any]]", revision: str) -> "list[dict[str, Any]]":
|
|
1429
|
+
"""Collect migrations to revert based on target revision.
|
|
1430
|
+
|
|
1431
|
+
Args:
|
|
1432
|
+
applied: List of applied migration records.
|
|
1433
|
+
revision: Target revision ("-1", "base", or specific version).
|
|
1434
|
+
|
|
1435
|
+
Returns:
|
|
1436
|
+
List of migration records to revert.
|
|
1437
|
+
"""
|
|
1438
|
+
if revision == "-1":
|
|
1439
|
+
return [applied[-1]]
|
|
1440
|
+
if revision == "base":
|
|
1441
|
+
return list(reversed(applied))
|
|
1442
|
+
parsed_revision = parse_version(revision)
|
|
1443
|
+
to_revert = []
|
|
1444
|
+
for migration in reversed(applied):
|
|
1445
|
+
parsed_migration_version = parse_version(migration["version_num"])
|
|
1446
|
+
if parsed_migration_version > parsed_revision:
|
|
1447
|
+
to_revert.append(migration)
|
|
1448
|
+
return to_revert
|
|
1449
|
+
|
|
1450
|
+
async def _revert_single_migration(
|
|
1451
|
+
self, driver: Any, migration: "dict[str, Any]", version: str, use_logger: bool, echo: bool, summary_only: bool
|
|
1452
|
+
) -> int | None:
|
|
1453
|
+
"""Revert a single migration.
|
|
1454
|
+
|
|
1455
|
+
Args:
|
|
1456
|
+
driver: Database driver instance.
|
|
1457
|
+
migration: Migration dictionary.
|
|
1458
|
+
version: Version string.
|
|
1459
|
+
use_logger: Whether to output to logger instead of console.
|
|
1460
|
+
echo: Whether to echo output to the console.
|
|
1461
|
+
summary_only: Whether summary-only logging is enabled.
|
|
1462
|
+
|
|
1463
|
+
Returns:
|
|
1464
|
+
Execution time in ms on success, None on failure.
|
|
1465
|
+
"""
|
|
1466
|
+
try:
|
|
1467
|
+
|
|
1468
|
+
async def remove_version(exec_time: int, version: str = version) -> None:
|
|
1469
|
+
await self.tracker.remove_migration(driver, version)
|
|
1470
|
+
|
|
1471
|
+
_, execution_time = await self.runner.execute_downgrade(driver, migration, on_success=remove_version)
|
|
1472
|
+
except Exception as exc:
|
|
1473
|
+
use_txn = self.runner.should_use_transaction(migration, self.config)
|
|
1474
|
+
rollback_msg = " (transaction rolled back)" if use_txn else ""
|
|
1475
|
+
_output_exception(
|
|
1476
|
+
use_logger,
|
|
1477
|
+
echo,
|
|
1478
|
+
summary_only,
|
|
1479
|
+
"Migration %s failed%s",
|
|
1480
|
+
version,
|
|
1481
|
+
rollback_msg,
|
|
1482
|
+
rich_message=f"[red]✗ Failed{rollback_msg}: {exc}[/]",
|
|
1483
|
+
)
|
|
1484
|
+
self._last_command_error = exc
|
|
1485
|
+
return None
|
|
1486
|
+
else:
|
|
1487
|
+
_output_info(
|
|
1488
|
+
use_logger,
|
|
1489
|
+
echo,
|
|
1490
|
+
summary_only,
|
|
1491
|
+
"Reverted migration %s in %dms",
|
|
1492
|
+
version,
|
|
1493
|
+
execution_time,
|
|
1494
|
+
rich_message=f"[green]✓ Reverted in {execution_time}ms[/]",
|
|
1495
|
+
)
|
|
1496
|
+
return execution_time
|
|
1497
|
+
|
|
1498
|
+
async def _synchronize_version_records(
|
|
1499
|
+
self, driver: Any, *, use_logger: bool = False, echo: bool = True, summary_only: bool = False
|
|
1500
|
+
) -> int:
|
|
1501
|
+
"""Synchronize database version records with migration files.
|
|
1502
|
+
|
|
1503
|
+
Auto-updates DB tracking when migrations have been renamed by fix command.
|
|
1504
|
+
This allows developers to just run upgrade after pulling changes without
|
|
1505
|
+
manually running fix.
|
|
1506
|
+
|
|
1507
|
+
Validates checksums match before updating to prevent incorrect matches.
|
|
1508
|
+
|
|
1509
|
+
Args:
|
|
1510
|
+
driver: Database driver instance.
|
|
1511
|
+
use_logger: If True, output to logger instead of Rich console.
|
|
1512
|
+
echo: Whether to echo output to the console.
|
|
1513
|
+
summary_only: Whether summary-only logging is enabled.
|
|
1514
|
+
|
|
1515
|
+
Returns:
|
|
1516
|
+
Number of version records updated.
|
|
1517
|
+
"""
|
|
1518
|
+
all_migrations = await self.runner.get_migration_files()
|
|
1519
|
+
|
|
1520
|
+
try:
|
|
1521
|
+
applied_migrations = await self.tracker.get_applied_migrations(driver)
|
|
1522
|
+
except Exception as exc:
|
|
1523
|
+
log_with_context(
|
|
1524
|
+
logger,
|
|
1525
|
+
logging.DEBUG,
|
|
1526
|
+
"migration.list",
|
|
1527
|
+
db_system=resolve_db_system(type(driver).__name__),
|
|
1528
|
+
error_type=type(exc).__name__,
|
|
1529
|
+
status="failed",
|
|
1530
|
+
operation="applied_fetch",
|
|
1531
|
+
)
|
|
1532
|
+
return 0
|
|
1533
|
+
|
|
1534
|
+
applied_map = {m["version_num"]: m for m in applied_migrations}
|
|
1535
|
+
|
|
1536
|
+
conversion_map = generate_conversion_map(all_migrations)
|
|
1537
|
+
|
|
1538
|
+
updated_count = 0
|
|
1539
|
+
if conversion_map:
|
|
1540
|
+
for old_version, new_version in conversion_map.items():
|
|
1541
|
+
if old_version in applied_map and new_version not in applied_map:
|
|
1542
|
+
applied_checksum = applied_map[old_version]["checksum"]
|
|
1543
|
+
|
|
1544
|
+
file_path = next((path for v, path in all_migrations if v == new_version), None)
|
|
1545
|
+
if file_path:
|
|
1546
|
+
migration = await self.runner.load_migration(file_path, new_version)
|
|
1547
|
+
if migration["checksum"] == applied_checksum:
|
|
1548
|
+
await self.tracker.update_version_record(driver, old_version, new_version)
|
|
1549
|
+
if use_logger:
|
|
1550
|
+
if not summary_only:
|
|
1551
|
+
logger.info("Reconciled version: %s -> %s", old_version, new_version)
|
|
1552
|
+
elif echo:
|
|
1553
|
+
console.print(f" [dim]Reconciled version:[/] {old_version} → {new_version}")
|
|
1554
|
+
updated_count += 1
|
|
1555
|
+
elif use_logger:
|
|
1556
|
+
logger.warning(
|
|
1557
|
+
"Checksum mismatch for %s -> %s, skipping auto-sync", old_version, new_version
|
|
1558
|
+
)
|
|
1559
|
+
elif echo:
|
|
1560
|
+
console.print(
|
|
1561
|
+
f" [yellow]Warning: Checksum mismatch for {old_version} → {new_version}, skipping auto-sync[/]"
|
|
1562
|
+
)
|
|
1563
|
+
else:
|
|
1564
|
+
file_checksums = await self._load_migration_checksums(all_migrations)
|
|
1565
|
+
|
|
1566
|
+
for applied_version, applied_record in applied_map.items():
|
|
1567
|
+
for file_version, (file_checksum, _) in file_checksums.items():
|
|
1568
|
+
if file_version not in applied_map and applied_record["checksum"] == file_checksum:
|
|
1569
|
+
await self.tracker.update_version_record(driver, applied_version, file_version)
|
|
1570
|
+
if use_logger:
|
|
1571
|
+
if not summary_only:
|
|
1572
|
+
logger.info("Reconciled version: %s -> %s", applied_version, file_version)
|
|
1573
|
+
elif echo:
|
|
1574
|
+
console.print(f" [dim]Reconciled version:[/] {applied_version} → {file_version}")
|
|
1575
|
+
updated_count += 1
|
|
1576
|
+
break
|
|
1577
|
+
|
|
1578
|
+
if updated_count > 0:
|
|
1579
|
+
if use_logger:
|
|
1580
|
+
if not summary_only:
|
|
1581
|
+
logger.info("Reconciled %d version record(s)", updated_count)
|
|
1582
|
+
elif echo:
|
|
1583
|
+
console.print(f"[cyan]Reconciled {updated_count} version record(s)[/]")
|
|
1584
|
+
|
|
1585
|
+
return updated_count
|
|
1586
|
+
|
|
1587
|
+
@_with_command_span("upgrade", metadata_fn=_upgrade_metadata)
|
|
1588
|
+
async def upgrade(
|
|
1589
|
+
self,
|
|
1590
|
+
revision: str = "head",
|
|
1591
|
+
allow_missing: bool = False,
|
|
1592
|
+
auto_sync: bool = True,
|
|
1593
|
+
dry_run: bool = False,
|
|
1594
|
+
*,
|
|
1595
|
+
use_logger: bool = False,
|
|
1596
|
+
echo: bool | None = None,
|
|
1597
|
+
summary_only: bool | None = None,
|
|
1598
|
+
) -> None:
|
|
1599
|
+
"""Upgrade to a target revision.
|
|
1600
|
+
|
|
1601
|
+
Validates migration order and warns if out-of-order migrations are detected.
|
|
1602
|
+
Out-of-order migrations can occur when branches merge in different orders
|
|
1603
|
+
across environments.
|
|
1604
|
+
|
|
1605
|
+
Args:
|
|
1606
|
+
revision: Target revision or "head" for latest.
|
|
1607
|
+
allow_missing: If True, allow out-of-order migrations even in strict mode.
|
|
1608
|
+
Defaults to False.
|
|
1609
|
+
auto_sync: If True, automatically reconcile renamed migrations in database.
|
|
1610
|
+
Defaults to True. Can be disabled via --no-auto-sync flag.
|
|
1611
|
+
dry_run: If True, show what would be done without making changes.
|
|
1612
|
+
use_logger: If True, output to logger instead of Rich console.
|
|
1613
|
+
Defaults to False. Can be set via MigrationConfig for persistent default.
|
|
1614
|
+
echo: Echo output to the console. Defaults to True when unset.
|
|
1615
|
+
summary_only: Emit a single summary log entry when logger output is enabled.
|
|
1616
|
+
"""
|
|
1617
|
+
runtime = self._runtime
|
|
1618
|
+
applied_count = 0
|
|
1619
|
+
pending_count = 0
|
|
1620
|
+
db_system: str | None = None
|
|
1621
|
+
error: Exception | None = None
|
|
1622
|
+
ul, echo_value, summary_value = self._resolve_output_policy(use_logger, echo, summary_only)
|
|
1623
|
+
self.runner.set_use_logger(ul)
|
|
1624
|
+
self.runner.set_summary_only(summary_value)
|
|
1625
|
+
self.tracker.set_output_policy(use_logger=ul, echo=echo_value, summary_only=summary_value)
|
|
1626
|
+
output_info = functools.partial(_output_info, ul, echo_value, summary_value)
|
|
1627
|
+
start_time = time.perf_counter()
|
|
1628
|
+
|
|
1629
|
+
try:
|
|
1630
|
+
if dry_run:
|
|
1631
|
+
output_info(
|
|
1632
|
+
"DRY RUN MODE: No database changes will be applied",
|
|
1633
|
+
rich_message="[bold yellow]DRY RUN MODE:[/] No database changes will be applied\n",
|
|
1634
|
+
)
|
|
1635
|
+
|
|
1636
|
+
async with self.config.provide_session() as driver:
|
|
1637
|
+
db_system = resolve_db_system(type(driver).__name__)
|
|
1638
|
+
await self.tracker.ensure_tracking_table(driver)
|
|
1639
|
+
|
|
1640
|
+
if auto_sync and self.config.migration_config.get("auto_sync", True):
|
|
1641
|
+
await self._synchronize_version_records(
|
|
1642
|
+
driver, use_logger=ul, echo=echo_value, summary_only=summary_value
|
|
1643
|
+
)
|
|
1644
|
+
|
|
1645
|
+
applied_migrations = await self.tracker.get_applied_migrations(driver)
|
|
1646
|
+
applied_versions = [m["version_num"] for m in applied_migrations]
|
|
1647
|
+
applied_set = set(applied_versions)
|
|
1648
|
+
|
|
1649
|
+
all_migrations = await self.runner.get_migration_files()
|
|
1650
|
+
if runtime is not None:
|
|
1651
|
+
runtime.increment_metric("migrations.command.upgrade.available", float(len(all_migrations)))
|
|
1652
|
+
|
|
1653
|
+
pending = self._collect_pending_migrations(all_migrations, applied_set, revision)
|
|
1654
|
+
pending_count = len(pending)
|
|
1655
|
+
|
|
1656
|
+
if runtime is not None:
|
|
1657
|
+
runtime.increment_metric("migrations.command.upgrade.pending", float(len(pending)))
|
|
1658
|
+
|
|
1659
|
+
if not pending:
|
|
1660
|
+
self._report_no_pending_migrations(ul, echo_value, summary_value, bool(all_migrations))
|
|
1661
|
+
return
|
|
1662
|
+
|
|
1663
|
+
migration_config = cast("dict[str, Any]", self.config.migration_config) or {}
|
|
1664
|
+
strict_ordering = migration_config.get("strict_ordering", False) and not allow_missing
|
|
1665
|
+
validate_migration_order(
|
|
1666
|
+
[v for v, _ in pending],
|
|
1667
|
+
applied_versions,
|
|
1668
|
+
strict_ordering,
|
|
1669
|
+
use_logger=ul,
|
|
1670
|
+
echo=echo_value,
|
|
1671
|
+
summary_only=summary_value,
|
|
1672
|
+
)
|
|
1673
|
+
|
|
1674
|
+
output_info(
|
|
1675
|
+
"Found %d pending migrations",
|
|
1676
|
+
len(pending),
|
|
1677
|
+
rich_message=f"[yellow]Found {len(pending)} pending migrations[/]",
|
|
1678
|
+
)
|
|
1679
|
+
|
|
1680
|
+
for version, file_path in pending:
|
|
1681
|
+
migration = await self.runner.load_migration(file_path, version)
|
|
1682
|
+
action_verb = "Would apply" if dry_run else "Applying"
|
|
1683
|
+
output_info(
|
|
1684
|
+
"%s %s: %s",
|
|
1685
|
+
action_verb,
|
|
1686
|
+
version,
|
|
1687
|
+
migration["description"],
|
|
1688
|
+
rich_message=f"\n[cyan]{action_verb} {version}:[/] {migration['description']}",
|
|
1689
|
+
)
|
|
1690
|
+
|
|
1691
|
+
if dry_run:
|
|
1692
|
+
output_info(
|
|
1693
|
+
"Migration file: %s", file_path, rich_message=f"[dim]Migration file: {file_path}[/]"
|
|
1694
|
+
)
|
|
1695
|
+
continue
|
|
1696
|
+
|
|
1697
|
+
result = await self._apply_single_migration(
|
|
1698
|
+
driver, migration, version, ul, echo_value, summary_value
|
|
1699
|
+
)
|
|
1700
|
+
if result is None:
|
|
1701
|
+
return
|
|
1702
|
+
applied_count += 1
|
|
1703
|
+
|
|
1704
|
+
except Exception as exc: # pragma: no cover - passthrough
|
|
1705
|
+
error = exc
|
|
1706
|
+
raise
|
|
1707
|
+
finally:
|
|
1708
|
+
duration_ms = int((time.perf_counter() - start_time) * 1000)
|
|
1709
|
+
_log_command_summary(
|
|
1710
|
+
use_logger=ul,
|
|
1711
|
+
summary_only=summary_value,
|
|
1712
|
+
command="upgrade",
|
|
1713
|
+
status="failed" if error else "complete",
|
|
1714
|
+
revision=revision,
|
|
1715
|
+
dry_run=dry_run,
|
|
1716
|
+
pending_count=pending_count,
|
|
1717
|
+
applied_count=applied_count,
|
|
1718
|
+
reverted_count=None,
|
|
1719
|
+
duration_ms=duration_ms,
|
|
1720
|
+
db_system=db_system,
|
|
1721
|
+
bind_key=getattr(self.config, "bind_key", None),
|
|
1722
|
+
config_name=type(self.config).__name__,
|
|
1723
|
+
error=error,
|
|
1724
|
+
allow_missing=allow_missing,
|
|
1725
|
+
auto_sync=auto_sync,
|
|
1726
|
+
)
|
|
1727
|
+
|
|
1728
|
+
if dry_run:
|
|
1729
|
+
output_info(
|
|
1730
|
+
"Dry run complete. No changes were made to the database.",
|
|
1731
|
+
rich_message="\n[bold yellow]Dry run complete.[/] No changes were made to the database.",
|
|
1732
|
+
)
|
|
1733
|
+
elif applied_count:
|
|
1734
|
+
self._record_command_metric("applied", float(applied_count))
|
|
1735
|
+
|
|
1736
|
+
@_with_command_span("downgrade", metadata_fn=_downgrade_metadata)
|
|
1737
|
+
async def downgrade(
|
|
1738
|
+
self,
|
|
1739
|
+
revision: str = "-1",
|
|
1740
|
+
*,
|
|
1741
|
+
dry_run: bool = False,
|
|
1742
|
+
use_logger: bool = False,
|
|
1743
|
+
echo: bool | None = None,
|
|
1744
|
+
summary_only: bool | None = None,
|
|
1745
|
+
) -> None:
|
|
1746
|
+
"""Downgrade to a target revision.
|
|
1747
|
+
|
|
1748
|
+
Args:
|
|
1749
|
+
revision: Target revision or "-1" for one step back.
|
|
1750
|
+
dry_run: If True, show what would be done without making changes.
|
|
1751
|
+
use_logger: If True, output to logger instead of Rich console.
|
|
1752
|
+
Defaults to False. Can be set via MigrationConfig for persistent default.
|
|
1753
|
+
echo: Echo output to the console. Defaults to True when unset.
|
|
1754
|
+
summary_only: Emit a single summary log entry when logger output is enabled.
|
|
1755
|
+
"""
|
|
1756
|
+
runtime = self._runtime
|
|
1757
|
+
reverted_count = 0
|
|
1758
|
+
pending_count = 0
|
|
1759
|
+
db_system: str | None = None
|
|
1760
|
+
error: Exception | None = None
|
|
1761
|
+
ul, echo_value, summary_value = self._resolve_output_policy(use_logger, echo, summary_only)
|
|
1762
|
+
self.runner.set_use_logger(ul)
|
|
1763
|
+
self.runner.set_summary_only(summary_value)
|
|
1764
|
+
self.tracker.set_output_policy(use_logger=ul, echo=echo_value, summary_only=summary_value)
|
|
1765
|
+
output_info = functools.partial(_output_info, ul, echo_value, summary_value)
|
|
1766
|
+
output_error = functools.partial(_output_error, ul, echo_value, summary_value)
|
|
1767
|
+
start_time = time.perf_counter()
|
|
1768
|
+
|
|
1769
|
+
try:
|
|
1770
|
+
if dry_run:
|
|
1771
|
+
output_info(
|
|
1772
|
+
"DRY RUN MODE: No database changes will be applied",
|
|
1773
|
+
rich_message="[bold yellow]DRY RUN MODE:[/] No database changes will be applied\n",
|
|
1774
|
+
)
|
|
1775
|
+
|
|
1776
|
+
async with self.config.provide_session() as driver:
|
|
1777
|
+
db_system = resolve_db_system(type(driver).__name__)
|
|
1778
|
+
await self.tracker.ensure_tracking_table(driver)
|
|
1779
|
+
|
|
1780
|
+
applied = await self.tracker.get_applied_migrations(driver)
|
|
1781
|
+
if runtime is not None:
|
|
1782
|
+
runtime.increment_metric("migrations.command.downgrade.available", float(len(applied)))
|
|
1783
|
+
if not applied:
|
|
1784
|
+
output_info("No migrations to downgrade", rich_message="[yellow]No migrations to downgrade[/]")
|
|
1785
|
+
return
|
|
1786
|
+
|
|
1787
|
+
to_revert = self._collect_revert_migrations(applied, revision)
|
|
1788
|
+
pending_count = len(to_revert)
|
|
1789
|
+
|
|
1790
|
+
if runtime is not None:
|
|
1791
|
+
runtime.increment_metric("migrations.command.downgrade.pending", float(len(to_revert)))
|
|
1792
|
+
|
|
1793
|
+
if not to_revert:
|
|
1794
|
+
output_info("Nothing to downgrade", rich_message="[yellow]Nothing to downgrade[/]")
|
|
1795
|
+
return
|
|
1796
|
+
|
|
1797
|
+
output_info(
|
|
1798
|
+
"Reverting %d migrations",
|
|
1799
|
+
len(to_revert),
|
|
1800
|
+
rich_message=f"[yellow]Reverting {len(to_revert)} migrations[/]",
|
|
1801
|
+
)
|
|
1802
|
+
all_files = dict(await self.runner.get_migration_files())
|
|
1803
|
+
|
|
1804
|
+
for migration_record in to_revert:
|
|
1805
|
+
version = migration_record["version_num"]
|
|
1806
|
+
if version not in all_files:
|
|
1807
|
+
output_error(
|
|
1808
|
+
"Migration file not found for %s",
|
|
1809
|
+
version,
|
|
1810
|
+
rich_message=f"[red]Migration file not found for {version}[/]",
|
|
1811
|
+
)
|
|
1812
|
+
if runtime is not None:
|
|
1813
|
+
runtime.increment_metric("migrations.command.downgrade.missing_files")
|
|
1814
|
+
continue
|
|
1815
|
+
|
|
1816
|
+
migration = await self.runner.load_migration(all_files[version], version)
|
|
1817
|
+
action_verb = "Would revert" if dry_run else "Reverting"
|
|
1818
|
+
output_info(
|
|
1819
|
+
"%s %s: %s",
|
|
1820
|
+
action_verb,
|
|
1821
|
+
version,
|
|
1822
|
+
migration["description"],
|
|
1823
|
+
rich_message=f"\n[cyan]{action_verb} {version}:[/] {migration['description']}",
|
|
1824
|
+
)
|
|
1825
|
+
|
|
1826
|
+
if dry_run:
|
|
1827
|
+
output_info(
|
|
1828
|
+
"Migration file: %s",
|
|
1829
|
+
all_files[version],
|
|
1830
|
+
rich_message=f"[dim]Migration file: {all_files[version]}[/]",
|
|
1831
|
+
)
|
|
1832
|
+
continue
|
|
1833
|
+
|
|
1834
|
+
result = await self._revert_single_migration(
|
|
1835
|
+
driver, migration, version, ul, echo_value, summary_value
|
|
1836
|
+
)
|
|
1837
|
+
if result is None:
|
|
1838
|
+
return
|
|
1839
|
+
reverted_count += 1
|
|
1840
|
+
|
|
1841
|
+
except Exception as exc: # pragma: no cover - passthrough
|
|
1842
|
+
error = exc
|
|
1843
|
+
raise
|
|
1844
|
+
finally:
|
|
1845
|
+
duration_ms = int((time.perf_counter() - start_time) * 1000)
|
|
1846
|
+
_log_command_summary(
|
|
1847
|
+
use_logger=ul,
|
|
1848
|
+
summary_only=summary_value,
|
|
1849
|
+
command="downgrade",
|
|
1850
|
+
status="failed" if error else "complete",
|
|
1851
|
+
revision=revision,
|
|
1852
|
+
dry_run=dry_run,
|
|
1853
|
+
pending_count=pending_count,
|
|
1854
|
+
applied_count=None,
|
|
1855
|
+
reverted_count=reverted_count,
|
|
1856
|
+
duration_ms=duration_ms,
|
|
1857
|
+
db_system=db_system,
|
|
1858
|
+
bind_key=getattr(self.config, "bind_key", None),
|
|
1859
|
+
config_name=type(self.config).__name__,
|
|
1860
|
+
error=error,
|
|
1861
|
+
)
|
|
1862
|
+
|
|
1863
|
+
if dry_run:
|
|
1864
|
+
output_info(
|
|
1865
|
+
"Dry run complete. No changes were made to the database.",
|
|
1866
|
+
rich_message="\n[bold yellow]Dry run complete.[/] No changes were made to the database.",
|
|
1867
|
+
)
|
|
1868
|
+
elif reverted_count:
|
|
1869
|
+
self._record_command_metric("applied", float(reverted_count))
|
|
1870
|
+
|
|
1871
|
+
async def stamp(self, revision: str) -> None:
|
|
1872
|
+
"""Mark database as being at a specific revision without running migrations.
|
|
1873
|
+
|
|
1874
|
+
Args:
|
|
1875
|
+
revision: The revision to stamp.
|
|
1876
|
+
"""
|
|
1877
|
+
async with self.config.provide_session() as driver:
|
|
1878
|
+
await self.tracker.ensure_tracking_table(driver)
|
|
1879
|
+
|
|
1880
|
+
all_migrations = dict(await self.runner.get_migration_files())
|
|
1881
|
+
if revision not in all_migrations:
|
|
1882
|
+
console.print(f"[red]Unknown revision: {revision}[/]")
|
|
1883
|
+
return
|
|
1884
|
+
|
|
1885
|
+
clear_sql = sql.delete().from_(self.tracker.version_table)
|
|
1886
|
+
await driver.execute(clear_sql)
|
|
1887
|
+
await self.tracker.record_migration(driver, revision, f"Stamped to {revision}", 0, "manual-stamp")
|
|
1888
|
+
console.print(f"[green]Database stamped at revision {revision}[/]")
|
|
1889
|
+
|
|
1890
|
+
async def revision(self, message: str, file_type: str | None = None) -> None:
|
|
1891
|
+
"""Create a new migration file with timestamp-based versioning.
|
|
1892
|
+
|
|
1893
|
+
Generates a unique timestamp version (YYYYMMDDHHmmss format) to avoid
|
|
1894
|
+
conflicts when multiple developers create migrations concurrently.
|
|
1895
|
+
|
|
1896
|
+
Args:
|
|
1897
|
+
message: Description for the migration.
|
|
1898
|
+
file_type: Type of migration file to create ('sql' or 'py').
|
|
1899
|
+
"""
|
|
1900
|
+
version = generate_timestamp_version()
|
|
1901
|
+
selected_format = file_type or self._template_settings.default_format
|
|
1902
|
+
file_path = create_migration_file(
|
|
1903
|
+
self.migrations_path,
|
|
1904
|
+
version,
|
|
1905
|
+
message,
|
|
1906
|
+
selected_format,
|
|
1907
|
+
config=self.config,
|
|
1908
|
+
template_settings=self._template_settings,
|
|
1909
|
+
)
|
|
1910
|
+
log_with_context(
|
|
1911
|
+
logger,
|
|
1912
|
+
logging.DEBUG,
|
|
1913
|
+
"migration.create",
|
|
1914
|
+
db_system=resolve_db_system(type(self.config).__name__),
|
|
1915
|
+
version=version,
|
|
1916
|
+
file_path=str(file_path),
|
|
1917
|
+
file_type=selected_format,
|
|
1918
|
+
description=message,
|
|
1919
|
+
)
|
|
1920
|
+
console.print(f"[green]Created migration:[/] {file_path}")
|
|
1921
|
+
|
|
1922
|
+
async def squash(
|
|
1923
|
+
self,
|
|
1924
|
+
start_version: str | None = None,
|
|
1925
|
+
end_version: str | None = None,
|
|
1926
|
+
description: str | None = None,
|
|
1927
|
+
*,
|
|
1928
|
+
dry_run: bool = False,
|
|
1929
|
+
update_database: bool = True,
|
|
1930
|
+
yes: bool = False,
|
|
1931
|
+
allow_gaps: bool = False,
|
|
1932
|
+
output_format: str = "sql",
|
|
1933
|
+
) -> None:
|
|
1934
|
+
"""Squash a range of migrations into a single file.
|
|
1935
|
+
|
|
1936
|
+
Combines multiple sequential migrations into a single "release" migration.
|
|
1937
|
+
UP statements are merged in version order, DOWN statements in reverse order.
|
|
1938
|
+
|
|
1939
|
+
Args:
|
|
1940
|
+
start_version: First version in the range to squash (inclusive).
|
|
1941
|
+
When None, defaults to the first sequential migration found.
|
|
1942
|
+
end_version: Last version in the range to squash (inclusive).
|
|
1943
|
+
When None, defaults to the last sequential migration found.
|
|
1944
|
+
description: Description for the squashed migration file.
|
|
1945
|
+
When None, prompts interactively.
|
|
1946
|
+
dry_run: Preview changes without applying.
|
|
1947
|
+
update_database: Update migration records in database.
|
|
1948
|
+
yes: Skip confirmation prompt.
|
|
1949
|
+
allow_gaps: Allow gaps in version sequence.
|
|
1950
|
+
output_format: Output format ("sql" or "py").
|
|
1951
|
+
|
|
1952
|
+
Raises:
|
|
1953
|
+
SquashValidationError: If validation fails (invalid range, gaps, etc.).
|
|
1954
|
+
"""
|
|
1955
|
+
sync_runner = SyncMigrationRunner(
|
|
1956
|
+
self.migrations_path,
|
|
1957
|
+
self._discover_extension_migrations(),
|
|
1958
|
+
None,
|
|
1959
|
+
self.extension_configs,
|
|
1960
|
+
runtime=self._runtime,
|
|
1961
|
+
description_hints=self._template_settings.description_hints,
|
|
1962
|
+
)
|
|
1963
|
+
|
|
1964
|
+
squasher = MigrationSquasher(self.migrations_path, sync_runner, self._template_settings)
|
|
1965
|
+
|
|
1966
|
+
# Infer start/end from all sequential migrations when not provided
|
|
1967
|
+
if start_version is None or end_version is None:
|
|
1968
|
+
all_migrations = sync_runner.get_migration_files()
|
|
1969
|
+
sequential = [(v, p) for v, p in all_migrations if v.isdigit() or v.lstrip("0").isdigit()]
|
|
1970
|
+
if not sequential:
|
|
1971
|
+
console.print("[yellow]No sequential migrations found to squash[/]")
|
|
1972
|
+
return
|
|
1973
|
+
if start_version is None:
|
|
1974
|
+
start_version = sequential[0][0]
|
|
1975
|
+
if end_version is None:
|
|
1976
|
+
end_version = sequential[-1][0]
|
|
1977
|
+
console.print(f"[cyan]Squashing range: {start_version} to {end_version}[/]")
|
|
1978
|
+
|
|
1979
|
+
# Prompt for description when not provided
|
|
1980
|
+
if description is None:
|
|
1981
|
+
import anyio
|
|
1982
|
+
from rich.prompt import Prompt
|
|
1983
|
+
|
|
1984
|
+
description = await anyio.to_thread.run_sync( # pyright: ignore[reportAttributeAccessIssue]
|
|
1985
|
+
lambda: Prompt.ask("Migration description", default="squashed_migrations")
|
|
1986
|
+
)
|
|
1987
|
+
|
|
1988
|
+
plans = squasher.plan_squash(
|
|
1989
|
+
start_version,
|
|
1990
|
+
end_version,
|
|
1991
|
+
description, # pyright: ignore[reportArgumentType]
|
|
1992
|
+
allow_gaps=allow_gaps,
|
|
1993
|
+
output_format=output_format,
|
|
1994
|
+
)
|
|
1995
|
+
|
|
1996
|
+
# Display plan for each squash group
|
|
1997
|
+
table = Table(title="Squash Plan")
|
|
1998
|
+
table.add_column("Version", style="cyan")
|
|
1999
|
+
table.add_column("File")
|
|
2000
|
+
table.add_column("Target", style="green")
|
|
2001
|
+
|
|
2002
|
+
total_migrations = 0
|
|
2003
|
+
for plan in plans:
|
|
2004
|
+
for version, file_path in plan.source_migrations:
|
|
2005
|
+
table.add_row(version, file_path.name, plan.target_path.name)
|
|
2006
|
+
total_migrations += 1
|
|
2007
|
+
|
|
2008
|
+
console.print(table)
|
|
2009
|
+
target_files = ", ".join(p.target_path.name for p in plans)
|
|
2010
|
+
console.print(
|
|
2011
|
+
f"\n[yellow]{total_migrations} migrations will be squashed into {len(plans)} file(s): {target_files}[/]"
|
|
2012
|
+
)
|
|
2013
|
+
|
|
2014
|
+
if dry_run:
|
|
2015
|
+
console.print("[yellow][Preview Mode - No changes made][/]")
|
|
2016
|
+
return
|
|
2017
|
+
|
|
2018
|
+
if not yes:
|
|
2019
|
+
import anyio
|
|
2020
|
+
|
|
2021
|
+
response = await anyio.to_thread.run_sync(input, "\nProceed with squash? [y/N]: ") # pyright: ignore[reportAttributeAccessIssue]
|
|
2022
|
+
if response.lower() != "y":
|
|
2023
|
+
console.print("[yellow]Squash cancelled[/]")
|
|
2024
|
+
return
|
|
2025
|
+
|
|
2026
|
+
squasher.apply_squash(plans)
|
|
2027
|
+
for plan in plans:
|
|
2028
|
+
console.print(f"[green]✓ Created squashed migration: {plan.target_path.name}[/]")
|
|
2029
|
+
|
|
2030
|
+
if update_database:
|
|
2031
|
+
async with self.config.provide_session() as driver:
|
|
2032
|
+
await self.tracker.ensure_tracking_table(driver)
|
|
2033
|
+
|
|
2034
|
+
for plan in plans:
|
|
2035
|
+
if await self.tracker.is_squash_already_applied(driver, plan.target_version, plan.source_versions):
|
|
2036
|
+
up_sql, down_sql = squasher.extract_sql(plan.source_migrations)
|
|
2037
|
+
if plan.target_path.suffix == ".py":
|
|
2038
|
+
content = squasher.generate_python_squash(plan, up_sql, down_sql)
|
|
2039
|
+
else:
|
|
2040
|
+
content = squasher.generate_squashed_content(plan, up_sql, down_sql)
|
|
2041
|
+
checksum = sync_runner.calculate_checksum(content)
|
|
2042
|
+
|
|
2043
|
+
await self.tracker.replace_with_squash(
|
|
2044
|
+
driver, plan.target_version, plan.source_versions, description, checksum
|
|
2045
|
+
)
|
|
2046
|
+
console.print("[green]✓ Updated migration tracking table[/]")
|
|
2047
|
+
|
|
2048
|
+
console.print("[green]✓ Squash complete![/]")
|
|
2049
|
+
|
|
2050
|
+
async def fix(self, dry_run: bool = False, update_database: bool = True, yes: bool = False) -> None:
|
|
2051
|
+
"""Convert timestamp migrations to sequential format.
|
|
2052
|
+
|
|
2053
|
+
Implements hybrid versioning workflow where development uses timestamps
|
|
2054
|
+
and production uses sequential numbers. Creates backup before changes
|
|
2055
|
+
and provides rollback on errors.
|
|
2056
|
+
|
|
2057
|
+
Args:
|
|
2058
|
+
dry_run: Preview changes without applying.
|
|
2059
|
+
update_database: Update migration records in database.
|
|
2060
|
+
yes: Skip confirmation prompt.
|
|
2061
|
+
|
|
2062
|
+
Examples:
|
|
2063
|
+
>>> await commands.fix(dry_run=True) # Preview only
|
|
2064
|
+
>>> await commands.fix(yes=True) # Auto-approve
|
|
2065
|
+
>>> await commands.fix(update_database=False) # Files only
|
|
2066
|
+
"""
|
|
2067
|
+
all_migrations = await self.runner.get_migration_files()
|
|
2068
|
+
|
|
2069
|
+
conversion_map = generate_conversion_map(all_migrations)
|
|
2070
|
+
|
|
2071
|
+
if not conversion_map:
|
|
2072
|
+
console.print("[yellow]No timestamp migrations found - nothing to convert[/]")
|
|
2073
|
+
return
|
|
2074
|
+
|
|
2075
|
+
fixer = MigrationFixer(self.migrations_path)
|
|
2076
|
+
renames = fixer.plan_renames(conversion_map)
|
|
2077
|
+
|
|
2078
|
+
table = Table(title="Migration Conversions")
|
|
2079
|
+
table.add_column("Current Version", style="cyan")
|
|
2080
|
+
table.add_column("New Version", style="green")
|
|
2081
|
+
table.add_column("File")
|
|
2082
|
+
|
|
2083
|
+
for rename in renames:
|
|
2084
|
+
table.add_row(rename.old_version, rename.new_version, rename.old_path.name)
|
|
2085
|
+
|
|
2086
|
+
console.print(table)
|
|
2087
|
+
console.print(f"\n[yellow]{len(renames)} migrations will be converted[/]")
|
|
2088
|
+
|
|
2089
|
+
if dry_run:
|
|
2090
|
+
console.print("[yellow][Preview Mode - No changes made][/]")
|
|
2091
|
+
return
|
|
2092
|
+
|
|
2093
|
+
if not yes:
|
|
2094
|
+
import anyio
|
|
2095
|
+
|
|
2096
|
+
response = await anyio.to_thread.run_sync(input, "\nProceed with conversion? [y/N]: ") # pyright: ignore[reportAttributeAccessIssue]
|
|
2097
|
+
if response.lower() != "y":
|
|
2098
|
+
console.print("[yellow]Conversion cancelled[/]")
|
|
2099
|
+
return
|
|
2100
|
+
|
|
2101
|
+
try:
|
|
2102
|
+
backup_path = fixer.create_backup()
|
|
2103
|
+
console.print(f"[green]✓ Created backup in {backup_path.name}[/]")
|
|
2104
|
+
|
|
2105
|
+
fixer.apply_renames(renames)
|
|
2106
|
+
for rename in renames:
|
|
2107
|
+
console.print(f"[green]✓ Renamed {rename.old_path.name} → {rename.new_path.name}[/]")
|
|
2108
|
+
|
|
2109
|
+
if update_database:
|
|
2110
|
+
async with self.config.provide_session() as driver:
|
|
2111
|
+
await self.tracker.ensure_tracking_table(driver)
|
|
2112
|
+
applied_migrations = await self.tracker.get_applied_migrations(driver)
|
|
2113
|
+
applied_versions = {m["version_num"] for m in applied_migrations}
|
|
2114
|
+
|
|
2115
|
+
updated_count = 0
|
|
2116
|
+
for old_version, new_version in conversion_map.items():
|
|
2117
|
+
if old_version in applied_versions:
|
|
2118
|
+
await self.tracker.update_version_record(driver, old_version, new_version)
|
|
2119
|
+
updated_count += 1
|
|
2120
|
+
|
|
2121
|
+
if updated_count > 0:
|
|
2122
|
+
console.print(
|
|
2123
|
+
f"[green]✓ Updated {updated_count} version records in migration tracking table[/]"
|
|
2124
|
+
)
|
|
2125
|
+
else:
|
|
2126
|
+
console.print("[green]✓ No applied migrations to update in tracking table[/]")
|
|
2127
|
+
|
|
2128
|
+
fixer.cleanup()
|
|
2129
|
+
console.print("[green]✓ Conversion complete![/]")
|
|
2130
|
+
|
|
2131
|
+
except Exception as e:
|
|
2132
|
+
console.print(f"[red]✗ Error: {e}[/]")
|
|
2133
|
+
fixer.rollback()
|
|
2134
|
+
console.print("[yellow]Restored files from backup[/]")
|
|
2135
|
+
raise
|
|
2136
|
+
|
|
2137
|
+
|
|
2138
|
+
def create_migration_commands(
|
|
2139
|
+
config: "SyncConfigT | AsyncConfigT",
|
|
2140
|
+
) -> "SyncMigrationCommands[SyncConfigT] | AsyncMigrationCommands[AsyncConfigT]":
|
|
2141
|
+
"""Factory function to create the appropriate migration commands.
|
|
2142
|
+
|
|
2143
|
+
Args:
|
|
2144
|
+
config: The SQLSpec configuration.
|
|
2145
|
+
|
|
2146
|
+
Returns:
|
|
2147
|
+
Appropriate migration commands instance.
|
|
2148
|
+
"""
|
|
2149
|
+
if config.is_async:
|
|
2150
|
+
return cast("AsyncMigrationCommands[AsyncConfigT]", AsyncMigrationCommands(cast("AsyncConfigT", config)))
|
|
2151
|
+
return cast("SyncMigrationCommands[SyncConfigT]", SyncMigrationCommands(cast("SyncConfigT", config)))
|