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.
Files changed (621) hide show
  1. f68e0789eb443ecb1c2c__mypyc.cp314-win_amd64.pyd +0 -0
  2. sqlspec/__init__.py +167 -0
  3. sqlspec/__main__.py +12 -0
  4. sqlspec/__metadata__.py +14 -0
  5. sqlspec/_typing.py +714 -0
  6. sqlspec/adapters/__init__.py +0 -0
  7. sqlspec/adapters/adbc/__init__.py +13 -0
  8. sqlspec/adapters/adbc/_typing.py +106 -0
  9. sqlspec/adapters/adbc/adk/__init__.py +5 -0
  10. sqlspec/adapters/adbc/adk/store.py +1280 -0
  11. sqlspec/adapters/adbc/config.py +378 -0
  12. sqlspec/adapters/adbc/core.cp314-win_amd64.pyd +0 -0
  13. sqlspec/adapters/adbc/core.py +922 -0
  14. sqlspec/adapters/adbc/data_dictionary.py +339 -0
  15. sqlspec/adapters/adbc/driver.py +534 -0
  16. sqlspec/adapters/adbc/events/__init__.py +5 -0
  17. sqlspec/adapters/adbc/events/store.py +285 -0
  18. sqlspec/adapters/adbc/litestar/__init__.py +5 -0
  19. sqlspec/adapters/adbc/litestar/store.py +534 -0
  20. sqlspec/adapters/adbc/type_converter.cp314-win_amd64.pyd +0 -0
  21. sqlspec/adapters/adbc/type_converter.py +142 -0
  22. sqlspec/adapters/aiomysql/__init__.py +21 -0
  23. sqlspec/adapters/aiomysql/_typing.py +137 -0
  24. sqlspec/adapters/aiomysql/adk/__init__.py +5 -0
  25. sqlspec/adapters/aiomysql/adk/store.py +678 -0
  26. sqlspec/adapters/aiomysql/config.py +305 -0
  27. sqlspec/adapters/aiomysql/core.cp314-win_amd64.pyd +0 -0
  28. sqlspec/adapters/aiomysql/core.py +536 -0
  29. sqlspec/adapters/aiomysql/data_dictionary.py +121 -0
  30. sqlspec/adapters/aiomysql/driver.py +386 -0
  31. sqlspec/adapters/aiomysql/events/__init__.py +5 -0
  32. sqlspec/adapters/aiomysql/events/store.py +104 -0
  33. sqlspec/adapters/aiomysql/litestar/__init__.py +5 -0
  34. sqlspec/adapters/aiomysql/litestar/store.py +314 -0
  35. sqlspec/adapters/aiosqlite/__init__.py +26 -0
  36. sqlspec/adapters/aiosqlite/_typing.py +109 -0
  37. sqlspec/adapters/aiosqlite/adk/__init__.py +5 -0
  38. sqlspec/adapters/aiosqlite/adk/store.py +829 -0
  39. sqlspec/adapters/aiosqlite/config.py +315 -0
  40. sqlspec/adapters/aiosqlite/core.cp314-win_amd64.pyd +0 -0
  41. sqlspec/adapters/aiosqlite/core.py +315 -0
  42. sqlspec/adapters/aiosqlite/data_dictionary.py +202 -0
  43. sqlspec/adapters/aiosqlite/driver.py +311 -0
  44. sqlspec/adapters/aiosqlite/events/__init__.py +5 -0
  45. sqlspec/adapters/aiosqlite/events/store.py +20 -0
  46. sqlspec/adapters/aiosqlite/litestar/__init__.py +5 -0
  47. sqlspec/adapters/aiosqlite/litestar/store.py +279 -0
  48. sqlspec/adapters/aiosqlite/pool.cp314-win_amd64.pyd +0 -0
  49. sqlspec/adapters/aiosqlite/pool.py +734 -0
  50. sqlspec/adapters/asyncmy/__init__.py +21 -0
  51. sqlspec/adapters/asyncmy/_typing.py +113 -0
  52. sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
  53. sqlspec/adapters/asyncmy/adk/store.py +644 -0
  54. sqlspec/adapters/asyncmy/config.py +307 -0
  55. sqlspec/adapters/asyncmy/core.cp314-win_amd64.pyd +0 -0
  56. sqlspec/adapters/asyncmy/core.py +538 -0
  57. sqlspec/adapters/asyncmy/data_dictionary.py +122 -0
  58. sqlspec/adapters/asyncmy/driver.py +391 -0
  59. sqlspec/adapters/asyncmy/events/__init__.py +5 -0
  60. sqlspec/adapters/asyncmy/events/store.py +104 -0
  61. sqlspec/adapters/asyncmy/litestar/__init__.py +5 -0
  62. sqlspec/adapters/asyncmy/litestar/store.py +296 -0
  63. sqlspec/adapters/asyncpg/__init__.py +26 -0
  64. sqlspec/adapters/asyncpg/_typing.py +103 -0
  65. sqlspec/adapters/asyncpg/adk/__init__.py +5 -0
  66. sqlspec/adapters/asyncpg/adk/store.py +483 -0
  67. sqlspec/adapters/asyncpg/config.py +575 -0
  68. sqlspec/adapters/asyncpg/core.cp314-win_amd64.pyd +0 -0
  69. sqlspec/adapters/asyncpg/core.py +480 -0
  70. sqlspec/adapters/asyncpg/data_dictionary.py +157 -0
  71. sqlspec/adapters/asyncpg/driver.py +487 -0
  72. sqlspec/adapters/asyncpg/events/__init__.py +6 -0
  73. sqlspec/adapters/asyncpg/events/_hub.py +181 -0
  74. sqlspec/adapters/asyncpg/events/backend.py +210 -0
  75. sqlspec/adapters/asyncpg/events/store.py +40 -0
  76. sqlspec/adapters/asyncpg/litestar/__init__.py +5 -0
  77. sqlspec/adapters/asyncpg/litestar/store.py +251 -0
  78. sqlspec/adapters/bigquery/__init__.py +15 -0
  79. sqlspec/adapters/bigquery/_typing.py +108 -0
  80. sqlspec/adapters/bigquery/config.py +362 -0
  81. sqlspec/adapters/bigquery/core.cp314-win_amd64.pyd +0 -0
  82. sqlspec/adapters/bigquery/core.py +768 -0
  83. sqlspec/adapters/bigquery/data_dictionary.py +120 -0
  84. sqlspec/adapters/bigquery/driver.py +542 -0
  85. sqlspec/adapters/bigquery/events/__init__.py +5 -0
  86. sqlspec/adapters/bigquery/events/store.py +139 -0
  87. sqlspec/adapters/bigquery/litestar/__init__.py +5 -0
  88. sqlspec/adapters/bigquery/litestar/store.py +325 -0
  89. sqlspec/adapters/bigquery/type_converter.cp314-win_amd64.pyd +0 -0
  90. sqlspec/adapters/bigquery/type_converter.py +107 -0
  91. sqlspec/adapters/cockroach_asyncpg/__init__.py +26 -0
  92. sqlspec/adapters/cockroach_asyncpg/_typing.py +73 -0
  93. sqlspec/adapters/cockroach_asyncpg/adk/__init__.py +3 -0
  94. sqlspec/adapters/cockroach_asyncpg/adk/store.py +465 -0
  95. sqlspec/adapters/cockroach_asyncpg/config.py +248 -0
  96. sqlspec/adapters/cockroach_asyncpg/core.cp314-win_amd64.pyd +0 -0
  97. sqlspec/adapters/cockroach_asyncpg/core.py +55 -0
  98. sqlspec/adapters/cockroach_asyncpg/data_dictionary.py +110 -0
  99. sqlspec/adapters/cockroach_asyncpg/driver.py +142 -0
  100. sqlspec/adapters/cockroach_asyncpg/events/__init__.py +3 -0
  101. sqlspec/adapters/cockroach_asyncpg/events/store.py +20 -0
  102. sqlspec/adapters/cockroach_asyncpg/litestar/__init__.py +3 -0
  103. sqlspec/adapters/cockroach_asyncpg/litestar/store.py +142 -0
  104. sqlspec/adapters/cockroach_psycopg/__init__.py +39 -0
  105. sqlspec/adapters/cockroach_psycopg/_typing.py +137 -0
  106. sqlspec/adapters/cockroach_psycopg/adk/__init__.py +13 -0
  107. sqlspec/adapters/cockroach_psycopg/adk/store.py +1039 -0
  108. sqlspec/adapters/cockroach_psycopg/config.py +511 -0
  109. sqlspec/adapters/cockroach_psycopg/core.cp314-win_amd64.pyd +0 -0
  110. sqlspec/adapters/cockroach_psycopg/core.py +63 -0
  111. sqlspec/adapters/cockroach_psycopg/data_dictionary.py +220 -0
  112. sqlspec/adapters/cockroach_psycopg/driver.py +273 -0
  113. sqlspec/adapters/cockroach_psycopg/events/__init__.py +6 -0
  114. sqlspec/adapters/cockroach_psycopg/events/store.py +34 -0
  115. sqlspec/adapters/cockroach_psycopg/litestar/__init__.py +3 -0
  116. sqlspec/adapters/cockroach_psycopg/litestar/store.py +327 -0
  117. sqlspec/adapters/duckdb/__init__.py +29 -0
  118. sqlspec/adapters/duckdb/_typing.py +104 -0
  119. sqlspec/adapters/duckdb/adk/__init__.py +14 -0
  120. sqlspec/adapters/duckdb/adk/store.py +935 -0
  121. sqlspec/adapters/duckdb/config.py +386 -0
  122. sqlspec/adapters/duckdb/core.cp314-win_amd64.pyd +0 -0
  123. sqlspec/adapters/duckdb/core.py +332 -0
  124. sqlspec/adapters/duckdb/data_dictionary.py +140 -0
  125. sqlspec/adapters/duckdb/driver.py +426 -0
  126. sqlspec/adapters/duckdb/events/__init__.py +5 -0
  127. sqlspec/adapters/duckdb/events/store.py +57 -0
  128. sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
  129. sqlspec/adapters/duckdb/litestar/store.py +330 -0
  130. sqlspec/adapters/duckdb/pool.cp314-win_amd64.pyd +0 -0
  131. sqlspec/adapters/duckdb/pool.py +350 -0
  132. sqlspec/adapters/duckdb/type_converter.cp314-win_amd64.pyd +0 -0
  133. sqlspec/adapters/duckdb/type_converter.py +118 -0
  134. sqlspec/adapters/mysqlconnector/__init__.py +39 -0
  135. sqlspec/adapters/mysqlconnector/_typing.py +186 -0
  136. sqlspec/adapters/mysqlconnector/adk/__init__.py +15 -0
  137. sqlspec/adapters/mysqlconnector/adk/store.py +1183 -0
  138. sqlspec/adapters/mysqlconnector/config.py +421 -0
  139. sqlspec/adapters/mysqlconnector/core.cp314-win_amd64.pyd +0 -0
  140. sqlspec/adapters/mysqlconnector/core.py +472 -0
  141. sqlspec/adapters/mysqlconnector/data_dictionary.py +230 -0
  142. sqlspec/adapters/mysqlconnector/driver.py +516 -0
  143. sqlspec/adapters/mysqlconnector/events/__init__.py +8 -0
  144. sqlspec/adapters/mysqlconnector/events/store.py +98 -0
  145. sqlspec/adapters/mysqlconnector/litestar/__init__.py +5 -0
  146. sqlspec/adapters/mysqlconnector/litestar/store.py +426 -0
  147. sqlspec/adapters/oracledb/__init__.py +39 -0
  148. sqlspec/adapters/oracledb/_json_handlers.cp314-win_amd64.pyd +0 -0
  149. sqlspec/adapters/oracledb/_json_handlers.py +196 -0
  150. sqlspec/adapters/oracledb/_param_types.cp314-win_amd64.pyd +0 -0
  151. sqlspec/adapters/oracledb/_param_types.py +46 -0
  152. sqlspec/adapters/oracledb/_typing.py +258 -0
  153. sqlspec/adapters/oracledb/_uuid_handlers.cp314-win_amd64.pyd +0 -0
  154. sqlspec/adapters/oracledb/_uuid_handlers.py +163 -0
  155. sqlspec/adapters/oracledb/_vector_handlers.cp314-win_amd64.pyd +0 -0
  156. sqlspec/adapters/oracledb/_vector_handlers.py +228 -0
  157. sqlspec/adapters/oracledb/adk/__init__.py +21 -0
  158. sqlspec/adapters/oracledb/adk/store.py +2453 -0
  159. sqlspec/adapters/oracledb/config.py +575 -0
  160. sqlspec/adapters/oracledb/core.cp314-win_amd64.pyd +0 -0
  161. sqlspec/adapters/oracledb/core.py +820 -0
  162. sqlspec/adapters/oracledb/data_dictionary.py +404 -0
  163. sqlspec/adapters/oracledb/driver.py +1277 -0
  164. sqlspec/adapters/oracledb/events/__init__.py +16 -0
  165. sqlspec/adapters/oracledb/events/_hub.py +345 -0
  166. sqlspec/adapters/oracledb/events/backend.py +300 -0
  167. sqlspec/adapters/oracledb/events/store.py +420 -0
  168. sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
  169. sqlspec/adapters/oracledb/litestar/store.py +781 -0
  170. sqlspec/adapters/oracledb/migrations.py +539 -0
  171. sqlspec/adapters/oracledb/type_converter.cp314-win_amd64.pyd +0 -0
  172. sqlspec/adapters/oracledb/type_converter.py +211 -0
  173. sqlspec/adapters/psqlpy/__init__.py +18 -0
  174. sqlspec/adapters/psqlpy/_typing.py +121 -0
  175. sqlspec/adapters/psqlpy/adk/__init__.py +5 -0
  176. sqlspec/adapters/psqlpy/adk/store.py +591 -0
  177. sqlspec/adapters/psqlpy/config.py +376 -0
  178. sqlspec/adapters/psqlpy/core.cp314-win_amd64.pyd +0 -0
  179. sqlspec/adapters/psqlpy/core.py +694 -0
  180. sqlspec/adapters/psqlpy/data_dictionary.py +121 -0
  181. sqlspec/adapters/psqlpy/driver.py +411 -0
  182. sqlspec/adapters/psqlpy/events/__init__.py +6 -0
  183. sqlspec/adapters/psqlpy/events/_hub.py +204 -0
  184. sqlspec/adapters/psqlpy/events/backend.py +210 -0
  185. sqlspec/adapters/psqlpy/events/store.py +20 -0
  186. sqlspec/adapters/psqlpy/litestar/__init__.py +5 -0
  187. sqlspec/adapters/psqlpy/litestar/store.py +270 -0
  188. sqlspec/adapters/psqlpy/type_converter.cp314-win_amd64.pyd +0 -0
  189. sqlspec/adapters/psqlpy/type_converter.py +113 -0
  190. sqlspec/adapters/psycopg/__init__.py +38 -0
  191. sqlspec/adapters/psycopg/_typing.py +218 -0
  192. sqlspec/adapters/psycopg/adk/__init__.py +10 -0
  193. sqlspec/adapters/psycopg/adk/store.py +1106 -0
  194. sqlspec/adapters/psycopg/config.py +695 -0
  195. sqlspec/adapters/psycopg/core.cp314-win_amd64.pyd +0 -0
  196. sqlspec/adapters/psycopg/core.py +520 -0
  197. sqlspec/adapters/psycopg/data_dictionary.py +278 -0
  198. sqlspec/adapters/psycopg/driver.py +1033 -0
  199. sqlspec/adapters/psycopg/events/__init__.py +20 -0
  200. sqlspec/adapters/psycopg/events/_hub.py +388 -0
  201. sqlspec/adapters/psycopg/events/backend.py +398 -0
  202. sqlspec/adapters/psycopg/events/store.py +42 -0
  203. sqlspec/adapters/psycopg/litestar/__init__.py +5 -0
  204. sqlspec/adapters/psycopg/litestar/store.py +554 -0
  205. sqlspec/adapters/psycopg/type_converter.cp314-win_amd64.pyd +0 -0
  206. sqlspec/adapters/psycopg/type_converter.py +93 -0
  207. sqlspec/adapters/pymysql/__init__.py +21 -0
  208. sqlspec/adapters/pymysql/_typing.py +92 -0
  209. sqlspec/adapters/pymysql/adk/__init__.py +5 -0
  210. sqlspec/adapters/pymysql/adk/store.py +657 -0
  211. sqlspec/adapters/pymysql/config.py +176 -0
  212. sqlspec/adapters/pymysql/core.cp314-win_amd64.pyd +0 -0
  213. sqlspec/adapters/pymysql/core.py +469 -0
  214. sqlspec/adapters/pymysql/data_dictionary.py +120 -0
  215. sqlspec/adapters/pymysql/driver.py +271 -0
  216. sqlspec/adapters/pymysql/events/__init__.py +5 -0
  217. sqlspec/adapters/pymysql/events/store.py +50 -0
  218. sqlspec/adapters/pymysql/litestar/__init__.py +5 -0
  219. sqlspec/adapters/pymysql/litestar/store.py +232 -0
  220. sqlspec/adapters/pymysql/pool.cp314-win_amd64.pyd +0 -0
  221. sqlspec/adapters/pymysql/pool.py +184 -0
  222. sqlspec/adapters/spanner/__init__.py +33 -0
  223. sqlspec/adapters/spanner/_typing.py +102 -0
  224. sqlspec/adapters/spanner/adk/__init__.py +5 -0
  225. sqlspec/adapters/spanner/adk/store.py +758 -0
  226. sqlspec/adapters/spanner/config.py +355 -0
  227. sqlspec/adapters/spanner/core.cp314-win_amd64.pyd +0 -0
  228. sqlspec/adapters/spanner/core.py +263 -0
  229. sqlspec/adapters/spanner/data_dictionary.py +120 -0
  230. sqlspec/adapters/spanner/driver.py +407 -0
  231. sqlspec/adapters/spanner/events/__init__.py +5 -0
  232. sqlspec/adapters/spanner/events/store.py +187 -0
  233. sqlspec/adapters/spanner/litestar/__init__.py +5 -0
  234. sqlspec/adapters/spanner/litestar/store.py +291 -0
  235. sqlspec/adapters/spanner/type_converter.cp314-win_amd64.pyd +0 -0
  236. sqlspec/adapters/spanner/type_converter.py +342 -0
  237. sqlspec/adapters/sqlite/__init__.py +19 -0
  238. sqlspec/adapters/sqlite/_typing.py +123 -0
  239. sqlspec/adapters/sqlite/adk/__init__.py +5 -0
  240. sqlspec/adapters/sqlite/adk/store.py +992 -0
  241. sqlspec/adapters/sqlite/config.py +240 -0
  242. sqlspec/adapters/sqlite/core.cp314-win_amd64.pyd +0 -0
  243. sqlspec/adapters/sqlite/core.py +357 -0
  244. sqlspec/adapters/sqlite/data_dictionary.py +198 -0
  245. sqlspec/adapters/sqlite/driver.py +527 -0
  246. sqlspec/adapters/sqlite/events/__init__.py +5 -0
  247. sqlspec/adapters/sqlite/events/store.py +20 -0
  248. sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
  249. sqlspec/adapters/sqlite/litestar/store.py +316 -0
  250. sqlspec/adapters/sqlite/pool.cp314-win_amd64.pyd +0 -0
  251. sqlspec/adapters/sqlite/pool.py +237 -0
  252. sqlspec/adapters/sqlite/type_converter.cp314-win_amd64.pyd +0 -0
  253. sqlspec/adapters/sqlite/type_converter.py +114 -0
  254. sqlspec/base.py +832 -0
  255. sqlspec/builder/__init__.py +181 -0
  256. sqlspec/builder/_base.cp314-win_amd64.pyd +0 -0
  257. sqlspec/builder/_base.py +1071 -0
  258. sqlspec/builder/_column.cp314-win_amd64.pyd +0 -0
  259. sqlspec/builder/_column.py +521 -0
  260. sqlspec/builder/_ddl.cp314-win_amd64.pyd +0 -0
  261. sqlspec/builder/_ddl.py +1691 -0
  262. sqlspec/builder/_delete.cp314-win_amd64.pyd +0 -0
  263. sqlspec/builder/_delete.py +95 -0
  264. sqlspec/builder/_dml.cp314-win_amd64.pyd +0 -0
  265. sqlspec/builder/_dml.py +386 -0
  266. sqlspec/builder/_explain.cp314-win_amd64.pyd +0 -0
  267. sqlspec/builder/_explain.py +579 -0
  268. sqlspec/builder/_expression_wrappers.cp314-win_amd64.pyd +0 -0
  269. sqlspec/builder/_expression_wrappers.py +46 -0
  270. sqlspec/builder/_factory.cp314-win_amd64.pyd +0 -0
  271. sqlspec/builder/_factory.py +1884 -0
  272. sqlspec/builder/_insert.cp314-win_amd64.pyd +0 -0
  273. sqlspec/builder/_insert.py +405 -0
  274. sqlspec/builder/_join.cp314-win_amd64.pyd +0 -0
  275. sqlspec/builder/_join.py +489 -0
  276. sqlspec/builder/_merge.cp314-win_amd64.pyd +0 -0
  277. sqlspec/builder/_merge.py +823 -0
  278. sqlspec/builder/_parsing_utils.cp314-win_amd64.pyd +0 -0
  279. sqlspec/builder/_parsing_utils.py +295 -0
  280. sqlspec/builder/_select.cp314-win_amd64.pyd +0 -0
  281. sqlspec/builder/_select.py +1666 -0
  282. sqlspec/builder/_temporal.cp314-win_amd64.pyd +0 -0
  283. sqlspec/builder/_temporal.py +167 -0
  284. sqlspec/builder/_update.cp314-win_amd64.pyd +0 -0
  285. sqlspec/builder/_update.py +173 -0
  286. sqlspec/builder/_vector_distance.cp314-win_amd64.pyd +0 -0
  287. sqlspec/builder/_vector_distance.py +330 -0
  288. sqlspec/cli.py +1095 -0
  289. sqlspec/config.py +2383 -0
  290. sqlspec/core/__init__.py +372 -0
  291. sqlspec/core/_correlation.cp314-win_amd64.pyd +0 -0
  292. sqlspec/core/_correlation.py +176 -0
  293. sqlspec/core/_pagination.py +42 -0
  294. sqlspec/core/_pool.cp314-win_amd64.pyd +0 -0
  295. sqlspec/core/_pool.py +76 -0
  296. sqlspec/core/cache.cp314-win_amd64.pyd +0 -0
  297. sqlspec/core/cache.py +1085 -0
  298. sqlspec/core/compiler.cp314-win_amd64.pyd +0 -0
  299. sqlspec/core/compiler.py +1090 -0
  300. sqlspec/core/config_runtime.cp314-win_amd64.pyd +0 -0
  301. sqlspec/core/config_runtime.py +174 -0
  302. sqlspec/core/explain.cp314-win_amd64.pyd +0 -0
  303. sqlspec/core/explain.py +275 -0
  304. sqlspec/core/filters.cp314-win_amd64.pyd +0 -0
  305. sqlspec/core/filters.py +969 -0
  306. sqlspec/core/hashing.cp314-win_amd64.pyd +0 -0
  307. sqlspec/core/hashing.py +266 -0
  308. sqlspec/core/metrics.cp314-win_amd64.pyd +0 -0
  309. sqlspec/core/metrics.py +83 -0
  310. sqlspec/core/parameters/__init__.py +72 -0
  311. sqlspec/core/parameters/_alignment.cp314-win_amd64.pyd +0 -0
  312. sqlspec/core/parameters/_alignment.py +283 -0
  313. sqlspec/core/parameters/_converter.cp314-win_amd64.pyd +0 -0
  314. sqlspec/core/parameters/_converter.py +554 -0
  315. sqlspec/core/parameters/_processor.cp314-win_amd64.pyd +0 -0
  316. sqlspec/core/parameters/_processor.py +1182 -0
  317. sqlspec/core/parameters/_registry.cp314-win_amd64.pyd +0 -0
  318. sqlspec/core/parameters/_registry.py +206 -0
  319. sqlspec/core/parameters/_transformers.cp314-win_amd64.pyd +0 -0
  320. sqlspec/core/parameters/_transformers.py +324 -0
  321. sqlspec/core/parameters/_types.cp314-win_amd64.pyd +0 -0
  322. sqlspec/core/parameters/_types.py +536 -0
  323. sqlspec/core/parameters/_validator.cp314-win_amd64.pyd +0 -0
  324. sqlspec/core/parameters/_validator.py +171 -0
  325. sqlspec/core/pipeline.cp314-win_amd64.pyd +0 -0
  326. sqlspec/core/pipeline.py +333 -0
  327. sqlspec/core/query_modifiers.cp314-win_amd64.pyd +0 -0
  328. sqlspec/core/query_modifiers.py +508 -0
  329. sqlspec/core/result/__init__.py +25 -0
  330. sqlspec/core/result/_base.cp314-win_amd64.pyd +0 -0
  331. sqlspec/core/result/_base.py +1232 -0
  332. sqlspec/core/result/_io.cp314-win_amd64.pyd +0 -0
  333. sqlspec/core/result/_io.py +28 -0
  334. sqlspec/core/splitter.cp314-win_amd64.pyd +0 -0
  335. sqlspec/core/splitter.py +1021 -0
  336. sqlspec/core/sqlcommenter.cp314-win_amd64.pyd +0 -0
  337. sqlspec/core/sqlcommenter.py +249 -0
  338. sqlspec/core/stack.cp314-win_amd64.pyd +0 -0
  339. sqlspec/core/stack.py +163 -0
  340. sqlspec/core/statement.cp314-win_amd64.pyd +0 -0
  341. sqlspec/core/statement.py +1865 -0
  342. sqlspec/core/type_converter.cp314-win_amd64.pyd +0 -0
  343. sqlspec/core/type_converter.py +340 -0
  344. sqlspec/data_dictionary/__init__.py +22 -0
  345. sqlspec/data_dictionary/_loader.cp314-win_amd64.pyd +0 -0
  346. sqlspec/data_dictionary/_loader.py +138 -0
  347. sqlspec/data_dictionary/_registry.cp314-win_amd64.pyd +0 -0
  348. sqlspec/data_dictionary/_registry.py +74 -0
  349. sqlspec/data_dictionary/_types.cp314-win_amd64.pyd +0 -0
  350. sqlspec/data_dictionary/_types.py +121 -0
  351. sqlspec/data_dictionary/dialects/__init__.py +21 -0
  352. sqlspec/data_dictionary/dialects/bigquery.cp314-win_amd64.pyd +0 -0
  353. sqlspec/data_dictionary/dialects/bigquery.py +81 -0
  354. sqlspec/data_dictionary/dialects/cockroachdb.cp314-win_amd64.pyd +0 -0
  355. sqlspec/data_dictionary/dialects/cockroachdb.py +54 -0
  356. sqlspec/data_dictionary/dialects/duckdb.cp314-win_amd64.pyd +0 -0
  357. sqlspec/data_dictionary/dialects/duckdb.py +47 -0
  358. sqlspec/data_dictionary/dialects/mysql.cp314-win_amd64.pyd +0 -0
  359. sqlspec/data_dictionary/dialects/mysql.py +53 -0
  360. sqlspec/data_dictionary/dialects/oracle.cp314-win_amd64.pyd +0 -0
  361. sqlspec/data_dictionary/dialects/oracle.py +197 -0
  362. sqlspec/data_dictionary/dialects/postgres.cp314-win_amd64.pyd +0 -0
  363. sqlspec/data_dictionary/dialects/postgres.py +69 -0
  364. sqlspec/data_dictionary/dialects/spanner.cp314-win_amd64.pyd +0 -0
  365. sqlspec/data_dictionary/dialects/spanner.py +37 -0
  366. sqlspec/data_dictionary/dialects/sqlite.cp314-win_amd64.pyd +0 -0
  367. sqlspec/data_dictionary/dialects/sqlite.py +59 -0
  368. sqlspec/data_dictionary/sql/.gitkeep +0 -0
  369. sqlspec/data_dictionary/sql/bigquery/columns.sql +23 -0
  370. sqlspec/data_dictionary/sql/bigquery/foreign_keys.sql +34 -0
  371. sqlspec/data_dictionary/sql/bigquery/indexes.sql +19 -0
  372. sqlspec/data_dictionary/sql/bigquery/tables.sql +33 -0
  373. sqlspec/data_dictionary/sql/bigquery/version.sql +3 -0
  374. sqlspec/data_dictionary/sql/cockroachdb/columns.sql +34 -0
  375. sqlspec/data_dictionary/sql/cockroachdb/foreign_keys.sql +40 -0
  376. sqlspec/data_dictionary/sql/cockroachdb/indexes.sql +32 -0
  377. sqlspec/data_dictionary/sql/cockroachdb/tables.sql +44 -0
  378. sqlspec/data_dictionary/sql/cockroachdb/version.sql +3 -0
  379. sqlspec/data_dictionary/sql/duckdb/columns.sql +23 -0
  380. sqlspec/data_dictionary/sql/duckdb/foreign_keys.sql +36 -0
  381. sqlspec/data_dictionary/sql/duckdb/indexes.sql +19 -0
  382. sqlspec/data_dictionary/sql/duckdb/tables.sql +38 -0
  383. sqlspec/data_dictionary/sql/duckdb/version.sql +3 -0
  384. sqlspec/data_dictionary/sql/mysql/columns.sql +23 -0
  385. sqlspec/data_dictionary/sql/mysql/foreign_keys.sql +28 -0
  386. sqlspec/data_dictionary/sql/mysql/indexes.sql +26 -0
  387. sqlspec/data_dictionary/sql/mysql/tables.sql +33 -0
  388. sqlspec/data_dictionary/sql/mysql/version.sql +3 -0
  389. sqlspec/data_dictionary/sql/oracle/columns.sql +23 -0
  390. sqlspec/data_dictionary/sql/oracle/foreign_keys.sql +48 -0
  391. sqlspec/data_dictionary/sql/oracle/indexes.sql +44 -0
  392. sqlspec/data_dictionary/sql/oracle/tables.sql +25 -0
  393. sqlspec/data_dictionary/sql/oracle/version.sql +20 -0
  394. sqlspec/data_dictionary/sql/postgres/columns.sql +34 -0
  395. sqlspec/data_dictionary/sql/postgres/foreign_keys.sql +40 -0
  396. sqlspec/data_dictionary/sql/postgres/indexes.sql +56 -0
  397. sqlspec/data_dictionary/sql/postgres/tables.sql +44 -0
  398. sqlspec/data_dictionary/sql/postgres/version.sql +3 -0
  399. sqlspec/data_dictionary/sql/spanner/columns.sql +23 -0
  400. sqlspec/data_dictionary/sql/spanner/foreign_keys.sql +70 -0
  401. sqlspec/data_dictionary/sql/spanner/indexes.sql +30 -0
  402. sqlspec/data_dictionary/sql/spanner/tables.sql +9 -0
  403. sqlspec/data_dictionary/sql/spanner/version.sql +3 -0
  404. sqlspec/data_dictionary/sql/sqlite/columns.sql +23 -0
  405. sqlspec/data_dictionary/sql/sqlite/foreign_keys.sql +22 -0
  406. sqlspec/data_dictionary/sql/sqlite/indexes.sql +7 -0
  407. sqlspec/data_dictionary/sql/sqlite/tables.sql +28 -0
  408. sqlspec/data_dictionary/sql/sqlite/version.sql +3 -0
  409. sqlspec/dialects/__init__.py +22 -0
  410. sqlspec/dialects/_compat.cp314-win_amd64.pyd +0 -0
  411. sqlspec/dialects/_compat.py +14 -0
  412. sqlspec/dialects/postgres/__init__.py +9 -0
  413. sqlspec/dialects/postgres/_generators.cp314-win_amd64.pyd +0 -0
  414. sqlspec/dialects/postgres/_generators.py +57 -0
  415. sqlspec/dialects/postgres/_operators.cp314-win_amd64.pyd +0 -0
  416. sqlspec/dialects/postgres/_operators.py +81 -0
  417. sqlspec/dialects/postgres/_paradedb.py +50 -0
  418. sqlspec/dialects/postgres/_pgvector.py +36 -0
  419. sqlspec/dialects/spanner/__init__.py +6 -0
  420. sqlspec/dialects/spanner/_generators.cp314-win_amd64.pyd +0 -0
  421. sqlspec/dialects/spanner/_generators.py +206 -0
  422. sqlspec/dialects/spanner/_spangres.py +77 -0
  423. sqlspec/dialects/spanner/_spanner.py +179 -0
  424. sqlspec/driver/__init__.py +49 -0
  425. sqlspec/driver/_async.cp314-win_amd64.pyd +0 -0
  426. sqlspec/driver/_async.py +1830 -0
  427. sqlspec/driver/_common.cp314-win_amd64.pyd +0 -0
  428. sqlspec/driver/_common.py +2292 -0
  429. sqlspec/driver/_exception_handler.cp314-win_amd64.pyd +0 -0
  430. sqlspec/driver/_exception_handler.py +108 -0
  431. sqlspec/driver/_query_cache.cp314-win_amd64.pyd +0 -0
  432. sqlspec/driver/_query_cache.py +96 -0
  433. sqlspec/driver/_sql_helpers.cp314-win_amd64.pyd +0 -0
  434. sqlspec/driver/_sql_helpers.py +139 -0
  435. sqlspec/driver/_storage_helpers.cp314-win_amd64.pyd +0 -0
  436. sqlspec/driver/_storage_helpers.py +153 -0
  437. sqlspec/driver/_sync.cp314-win_amd64.pyd +0 -0
  438. sqlspec/driver/_sync.py +1817 -0
  439. sqlspec/exceptions.cp314-win_amd64.pyd +0 -0
  440. sqlspec/exceptions.py +480 -0
  441. sqlspec/extensions/__init__.py +0 -0
  442. sqlspec/extensions/adk/__init__.py +84 -0
  443. sqlspec/extensions/adk/_config_utils.py +199 -0
  444. sqlspec/extensions/adk/_types.cp314-win_amd64.pyd +0 -0
  445. sqlspec/extensions/adk/_types.py +41 -0
  446. sqlspec/extensions/adk/artifact/__init__.py +57 -0
  447. sqlspec/extensions/adk/artifact/_types.cp314-win_amd64.pyd +0 -0
  448. sqlspec/extensions/adk/artifact/_types.py +32 -0
  449. sqlspec/extensions/adk/artifact/service.py +508 -0
  450. sqlspec/extensions/adk/artifact/store.py +361 -0
  451. sqlspec/extensions/adk/converters.py +212 -0
  452. sqlspec/extensions/adk/memory/__init__.py +69 -0
  453. sqlspec/extensions/adk/memory/_types.cp314-win_amd64.pyd +0 -0
  454. sqlspec/extensions/adk/memory/_types.py +30 -0
  455. sqlspec/extensions/adk/memory/converters.py +225 -0
  456. sqlspec/extensions/adk/memory/service.py +316 -0
  457. sqlspec/extensions/adk/memory/store.py +525 -0
  458. sqlspec/extensions/adk/migrations/0001_create_adk_tables.py +184 -0
  459. sqlspec/extensions/adk/migrations/__init__.py +0 -0
  460. sqlspec/extensions/adk/service.py +279 -0
  461. sqlspec/extensions/adk/store.py +590 -0
  462. sqlspec/extensions/events/__init__.py +51 -0
  463. sqlspec/extensions/events/_channel.py +703 -0
  464. sqlspec/extensions/events/_hints.cp314-win_amd64.pyd +0 -0
  465. sqlspec/extensions/events/_hints.py +45 -0
  466. sqlspec/extensions/events/_models.py +23 -0
  467. sqlspec/extensions/events/_payload.cp314-win_amd64.pyd +0 -0
  468. sqlspec/extensions/events/_payload.py +69 -0
  469. sqlspec/extensions/events/_protocols.py +134 -0
  470. sqlspec/extensions/events/_queue.py +462 -0
  471. sqlspec/extensions/events/_store.py +209 -0
  472. sqlspec/extensions/events/migrations/0001_create_event_queue.py +59 -0
  473. sqlspec/extensions/events/migrations/__init__.py +3 -0
  474. sqlspec/extensions/fastapi/__init__.py +22 -0
  475. sqlspec/extensions/fastapi/extension.py +391 -0
  476. sqlspec/extensions/fastapi/providers.cp314-win_amd64.pyd +0 -0
  477. sqlspec/extensions/fastapi/providers.py +712 -0
  478. sqlspec/extensions/flask/__init__.py +38 -0
  479. sqlspec/extensions/flask/_state.py +87 -0
  480. sqlspec/extensions/flask/_utils.py +71 -0
  481. sqlspec/extensions/flask/extension.py +539 -0
  482. sqlspec/extensions/litestar/__init__.py +31 -0
  483. sqlspec/extensions/litestar/_utils.py +52 -0
  484. sqlspec/extensions/litestar/channels.py +165 -0
  485. sqlspec/extensions/litestar/cli.py +102 -0
  486. sqlspec/extensions/litestar/config.py +90 -0
  487. sqlspec/extensions/litestar/handlers.py +316 -0
  488. sqlspec/extensions/litestar/migrations/0001_create_session_table.py +137 -0
  489. sqlspec/extensions/litestar/migrations/__init__.py +3 -0
  490. sqlspec/extensions/litestar/plugin.py +1066 -0
  491. sqlspec/extensions/litestar/providers.cp314-win_amd64.pyd +0 -0
  492. sqlspec/extensions/litestar/providers.py +784 -0
  493. sqlspec/extensions/litestar/store.py +298 -0
  494. sqlspec/extensions/otel/__init__.py +58 -0
  495. sqlspec/extensions/prometheus/__init__.py +113 -0
  496. sqlspec/extensions/sanic/__init__.py +19 -0
  497. sqlspec/extensions/sanic/_state.py +43 -0
  498. sqlspec/extensions/sanic/_utils.py +127 -0
  499. sqlspec/extensions/sanic/extension.py +647 -0
  500. sqlspec/extensions/starlette/__init__.py +22 -0
  501. sqlspec/extensions/starlette/_state.py +42 -0
  502. sqlspec/extensions/starlette/_utils.py +96 -0
  503. sqlspec/extensions/starlette/extension.py +374 -0
  504. sqlspec/extensions/starlette/middleware.py +281 -0
  505. sqlspec/loader.cp314-win_amd64.pyd +0 -0
  506. sqlspec/loader.py +727 -0
  507. sqlspec/migrations/__init__.py +39 -0
  508. sqlspec/migrations/base.cp314-win_amd64.pyd +0 -0
  509. sqlspec/migrations/base.py +862 -0
  510. sqlspec/migrations/commands.py +2151 -0
  511. sqlspec/migrations/context.cp314-win_amd64.pyd +0 -0
  512. sqlspec/migrations/context.py +157 -0
  513. sqlspec/migrations/fix.cp314-win_amd64.pyd +0 -0
  514. sqlspec/migrations/fix.py +204 -0
  515. sqlspec/migrations/loaders.cp314-win_amd64.pyd +0 -0
  516. sqlspec/migrations/loaders.py +443 -0
  517. sqlspec/migrations/runner.cp314-win_amd64.pyd +0 -0
  518. sqlspec/migrations/runner.py +1195 -0
  519. sqlspec/migrations/squash.cp314-win_amd64.pyd +0 -0
  520. sqlspec/migrations/squash.py +490 -0
  521. sqlspec/migrations/templates.cp314-win_amd64.pyd +0 -0
  522. sqlspec/migrations/templates.py +234 -0
  523. sqlspec/migrations/tracker.cp314-win_amd64.pyd +0 -0
  524. sqlspec/migrations/tracker.py +792 -0
  525. sqlspec/migrations/utils.cp314-win_amd64.pyd +0 -0
  526. sqlspec/migrations/utils.py +256 -0
  527. sqlspec/migrations/validation.cp314-win_amd64.pyd +0 -0
  528. sqlspec/migrations/validation.py +359 -0
  529. sqlspec/migrations/version.cp314-win_amd64.pyd +0 -0
  530. sqlspec/migrations/version.py +446 -0
  531. sqlspec/observability/__init__.py +57 -0
  532. sqlspec/observability/_common.cp314-win_amd64.pyd +0 -0
  533. sqlspec/observability/_common.py +77 -0
  534. sqlspec/observability/_config.cp314-win_amd64.pyd +0 -0
  535. sqlspec/observability/_config.py +364 -0
  536. sqlspec/observability/_diagnostics.cp314-win_amd64.pyd +0 -0
  537. sqlspec/observability/_diagnostics.py +74 -0
  538. sqlspec/observability/_dispatcher.cp314-win_amd64.pyd +0 -0
  539. sqlspec/observability/_dispatcher.py +200 -0
  540. sqlspec/observability/_formatters/__init__.py +13 -0
  541. sqlspec/observability/_formatters/_aws.cp314-win_amd64.pyd +0 -0
  542. sqlspec/observability/_formatters/_aws.py +102 -0
  543. sqlspec/observability/_formatters/_azure.cp314-win_amd64.pyd +0 -0
  544. sqlspec/observability/_formatters/_azure.py +96 -0
  545. sqlspec/observability/_formatters/_base.cp314-win_amd64.pyd +0 -0
  546. sqlspec/observability/_formatters/_base.py +57 -0
  547. sqlspec/observability/_formatters/_gcp.cp314-win_amd64.pyd +0 -0
  548. sqlspec/observability/_formatters/_gcp.py +131 -0
  549. sqlspec/observability/_formatting.py +58 -0
  550. sqlspec/observability/_observer.cp314-win_amd64.pyd +0 -0
  551. sqlspec/observability/_observer.py +361 -0
  552. sqlspec/observability/_runtime.cp314-win_amd64.pyd +0 -0
  553. sqlspec/observability/_runtime.py +461 -0
  554. sqlspec/observability/_sampling.cp314-win_amd64.pyd +0 -0
  555. sqlspec/observability/_sampling.py +188 -0
  556. sqlspec/observability/_spans.cp314-win_amd64.pyd +0 -0
  557. sqlspec/observability/_spans.py +161 -0
  558. sqlspec/protocols.py +955 -0
  559. sqlspec/py.typed +0 -0
  560. sqlspec/service.py +433 -0
  561. sqlspec/storage/__init__.py +48 -0
  562. sqlspec/storage/_arrow_payload.py +68 -0
  563. sqlspec/storage/_paths.cp314-win_amd64.pyd +0 -0
  564. sqlspec/storage/_paths.py +58 -0
  565. sqlspec/storage/_utils.py +46 -0
  566. sqlspec/storage/backends/__init__.py +1 -0
  567. sqlspec/storage/backends/base.cp314-win_amd64.pyd +0 -0
  568. sqlspec/storage/backends/base.py +374 -0
  569. sqlspec/storage/backends/fsspec.py +574 -0
  570. sqlspec/storage/backends/local.py +468 -0
  571. sqlspec/storage/backends/obstore.py +956 -0
  572. sqlspec/storage/errors.cp314-win_amd64.pyd +0 -0
  573. sqlspec/storage/errors.py +102 -0
  574. sqlspec/storage/pipeline.cp314-win_amd64.pyd +0 -0
  575. sqlspec/storage/pipeline.py +628 -0
  576. sqlspec/storage/registry.cp314-win_amd64.pyd +0 -0
  577. sqlspec/storage/registry.py +329 -0
  578. sqlspec/typing.py +405 -0
  579. sqlspec/utils/__init__.py +7 -0
  580. sqlspec/utils/arrow_helpers.py +384 -0
  581. sqlspec/utils/config_tools.cp314-win_amd64.pyd +0 -0
  582. sqlspec/utils/config_tools.py +314 -0
  583. sqlspec/utils/correlation.cp314-win_amd64.pyd +0 -0
  584. sqlspec/utils/correlation.py +134 -0
  585. sqlspec/utils/deprecation.cp314-win_amd64.pyd +0 -0
  586. sqlspec/utils/deprecation.py +157 -0
  587. sqlspec/utils/dispatch.cp314-win_amd64.pyd +0 -0
  588. sqlspec/utils/dispatch.py +101 -0
  589. sqlspec/utils/fixtures.cp314-win_amd64.pyd +0 -0
  590. sqlspec/utils/fixtures.py +260 -0
  591. sqlspec/utils/logging.cp314-win_amd64.pyd +0 -0
  592. sqlspec/utils/logging.py +251 -0
  593. sqlspec/utils/module_loader.py +306 -0
  594. sqlspec/utils/portal.cp314-win_amd64.pyd +0 -0
  595. sqlspec/utils/portal.py +377 -0
  596. sqlspec/utils/schema.cp314-win_amd64.pyd +0 -0
  597. sqlspec/utils/schema.py +1040 -0
  598. sqlspec/utils/serializers/__init__.py +30 -0
  599. sqlspec/utils/serializers/_json.cp314-win_amd64.pyd +0 -0
  600. sqlspec/utils/serializers/_json.py +415 -0
  601. sqlspec/utils/serializers/_numpy.cp314-win_amd64.pyd +0 -0
  602. sqlspec/utils/serializers/_numpy.py +65 -0
  603. sqlspec/utils/serializers/_schema.cp314-win_amd64.pyd +0 -0
  604. sqlspec/utils/serializers/_schema.py +285 -0
  605. sqlspec/utils/singleton.cp314-win_amd64.pyd +0 -0
  606. sqlspec/utils/singleton.py +41 -0
  607. sqlspec/utils/sync_tools.cp314-win_amd64.pyd +0 -0
  608. sqlspec/utils/sync_tools.py +316 -0
  609. sqlspec/utils/text.cp314-win_amd64.pyd +0 -0
  610. sqlspec/utils/text.py +109 -0
  611. sqlspec/utils/type_converters.cp314-win_amd64.pyd +0 -0
  612. sqlspec/utils/type_converters.py +216 -0
  613. sqlspec/utils/type_guards.cp314-win_amd64.pyd +0 -0
  614. sqlspec/utils/type_guards.py +1508 -0
  615. sqlspec/utils/uuids.cp314-win_amd64.pyd +0 -0
  616. sqlspec/utils/uuids.py +241 -0
  617. sqlspec-0.47.0.dist-info/METADATA +202 -0
  618. sqlspec-0.47.0.dist-info/RECORD +621 -0
  619. sqlspec-0.47.0.dist-info/WHEEL +4 -0
  620. sqlspec-0.47.0.dist-info/entry_points.txt +6 -0
  621. 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)))