sqlspec 0.36.0__cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (531) hide show
  1. ac8f31065839703b4e70__mypyc.cpython-310-aarch64-linux-gnu.so +0 -0
  2. sqlspec/__init__.py +140 -0
  3. sqlspec/__main__.py +12 -0
  4. sqlspec/__metadata__.py +14 -0
  5. sqlspec/_serialization.py +315 -0
  6. sqlspec/_typing.py +700 -0
  7. sqlspec/adapters/__init__.py +0 -0
  8. sqlspec/adapters/adbc/__init__.py +5 -0
  9. sqlspec/adapters/adbc/_typing.py +82 -0
  10. sqlspec/adapters/adbc/adk/__init__.py +5 -0
  11. sqlspec/adapters/adbc/adk/store.py +1273 -0
  12. sqlspec/adapters/adbc/config.py +295 -0
  13. sqlspec/adapters/adbc/core.cpython-310-aarch64-linux-gnu.so +0 -0
  14. sqlspec/adapters/adbc/core.py +735 -0
  15. sqlspec/adapters/adbc/data_dictionary.py +334 -0
  16. sqlspec/adapters/adbc/driver.py +529 -0
  17. sqlspec/adapters/adbc/events/__init__.py +5 -0
  18. sqlspec/adapters/adbc/events/store.py +285 -0
  19. sqlspec/adapters/adbc/litestar/__init__.py +5 -0
  20. sqlspec/adapters/adbc/litestar/store.py +502 -0
  21. sqlspec/adapters/adbc/type_converter.cpython-310-aarch64-linux-gnu.so +0 -0
  22. sqlspec/adapters/adbc/type_converter.py +140 -0
  23. sqlspec/adapters/aiosqlite/__init__.py +25 -0
  24. sqlspec/adapters/aiosqlite/_typing.py +82 -0
  25. sqlspec/adapters/aiosqlite/adk/__init__.py +5 -0
  26. sqlspec/adapters/aiosqlite/adk/store.py +818 -0
  27. sqlspec/adapters/aiosqlite/config.py +334 -0
  28. sqlspec/adapters/aiosqlite/core.cpython-310-aarch64-linux-gnu.so +0 -0
  29. sqlspec/adapters/aiosqlite/core.py +315 -0
  30. sqlspec/adapters/aiosqlite/data_dictionary.py +208 -0
  31. sqlspec/adapters/aiosqlite/driver.py +313 -0
  32. sqlspec/adapters/aiosqlite/events/__init__.py +5 -0
  33. sqlspec/adapters/aiosqlite/events/store.py +20 -0
  34. sqlspec/adapters/aiosqlite/litestar/__init__.py +5 -0
  35. sqlspec/adapters/aiosqlite/litestar/store.py +279 -0
  36. sqlspec/adapters/aiosqlite/pool.py +533 -0
  37. sqlspec/adapters/asyncmy/__init__.py +21 -0
  38. sqlspec/adapters/asyncmy/_typing.py +87 -0
  39. sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
  40. sqlspec/adapters/asyncmy/adk/store.py +703 -0
  41. sqlspec/adapters/asyncmy/config.py +302 -0
  42. sqlspec/adapters/asyncmy/core.cpython-310-aarch64-linux-gnu.so +0 -0
  43. sqlspec/adapters/asyncmy/core.py +360 -0
  44. sqlspec/adapters/asyncmy/data_dictionary.py +124 -0
  45. sqlspec/adapters/asyncmy/driver.py +383 -0
  46. sqlspec/adapters/asyncmy/events/__init__.py +5 -0
  47. sqlspec/adapters/asyncmy/events/store.py +104 -0
  48. sqlspec/adapters/asyncmy/litestar/__init__.py +5 -0
  49. sqlspec/adapters/asyncmy/litestar/store.py +296 -0
  50. sqlspec/adapters/asyncpg/__init__.py +19 -0
  51. sqlspec/adapters/asyncpg/_typing.py +88 -0
  52. sqlspec/adapters/asyncpg/adk/__init__.py +5 -0
  53. sqlspec/adapters/asyncpg/adk/store.py +748 -0
  54. sqlspec/adapters/asyncpg/config.py +569 -0
  55. sqlspec/adapters/asyncpg/core.cpython-310-aarch64-linux-gnu.so +0 -0
  56. sqlspec/adapters/asyncpg/core.py +367 -0
  57. sqlspec/adapters/asyncpg/data_dictionary.py +162 -0
  58. sqlspec/adapters/asyncpg/driver.py +487 -0
  59. sqlspec/adapters/asyncpg/events/__init__.py +6 -0
  60. sqlspec/adapters/asyncpg/events/backend.py +286 -0
  61. sqlspec/adapters/asyncpg/events/store.py +40 -0
  62. sqlspec/adapters/asyncpg/litestar/__init__.py +5 -0
  63. sqlspec/adapters/asyncpg/litestar/store.py +251 -0
  64. sqlspec/adapters/bigquery/__init__.py +14 -0
  65. sqlspec/adapters/bigquery/_typing.py +86 -0
  66. sqlspec/adapters/bigquery/adk/__init__.py +5 -0
  67. sqlspec/adapters/bigquery/adk/store.py +827 -0
  68. sqlspec/adapters/bigquery/config.py +353 -0
  69. sqlspec/adapters/bigquery/core.cpython-310-aarch64-linux-gnu.so +0 -0
  70. sqlspec/adapters/bigquery/core.py +715 -0
  71. sqlspec/adapters/bigquery/data_dictionary.py +128 -0
  72. sqlspec/adapters/bigquery/driver.py +548 -0
  73. sqlspec/adapters/bigquery/events/__init__.py +5 -0
  74. sqlspec/adapters/bigquery/events/store.py +139 -0
  75. sqlspec/adapters/bigquery/litestar/__init__.py +5 -0
  76. sqlspec/adapters/bigquery/litestar/store.py +325 -0
  77. sqlspec/adapters/bigquery/type_converter.cpython-310-aarch64-linux-gnu.so +0 -0
  78. sqlspec/adapters/bigquery/type_converter.py +107 -0
  79. sqlspec/adapters/cockroach_asyncpg/__init__.py +24 -0
  80. sqlspec/adapters/cockroach_asyncpg/_typing.py +72 -0
  81. sqlspec/adapters/cockroach_asyncpg/adk/__init__.py +3 -0
  82. sqlspec/adapters/cockroach_asyncpg/adk/store.py +410 -0
  83. sqlspec/adapters/cockroach_asyncpg/config.py +238 -0
  84. sqlspec/adapters/cockroach_asyncpg/core.cpython-310-aarch64-linux-gnu.so +0 -0
  85. sqlspec/adapters/cockroach_asyncpg/core.py +55 -0
  86. sqlspec/adapters/cockroach_asyncpg/data_dictionary.py +107 -0
  87. sqlspec/adapters/cockroach_asyncpg/driver.py +144 -0
  88. sqlspec/adapters/cockroach_asyncpg/events/__init__.py +3 -0
  89. sqlspec/adapters/cockroach_asyncpg/events/store.py +20 -0
  90. sqlspec/adapters/cockroach_asyncpg/litestar/__init__.py +3 -0
  91. sqlspec/adapters/cockroach_asyncpg/litestar/store.py +142 -0
  92. sqlspec/adapters/cockroach_psycopg/__init__.py +38 -0
  93. sqlspec/adapters/cockroach_psycopg/_typing.py +129 -0
  94. sqlspec/adapters/cockroach_psycopg/adk/__init__.py +13 -0
  95. sqlspec/adapters/cockroach_psycopg/adk/store.py +868 -0
  96. sqlspec/adapters/cockroach_psycopg/config.py +484 -0
  97. sqlspec/adapters/cockroach_psycopg/core.cpython-310-aarch64-linux-gnu.so +0 -0
  98. sqlspec/adapters/cockroach_psycopg/core.py +63 -0
  99. sqlspec/adapters/cockroach_psycopg/data_dictionary.py +215 -0
  100. sqlspec/adapters/cockroach_psycopg/driver.py +284 -0
  101. sqlspec/adapters/cockroach_psycopg/events/__init__.py +6 -0
  102. sqlspec/adapters/cockroach_psycopg/events/store.py +34 -0
  103. sqlspec/adapters/cockroach_psycopg/litestar/__init__.py +3 -0
  104. sqlspec/adapters/cockroach_psycopg/litestar/store.py +325 -0
  105. sqlspec/adapters/duckdb/__init__.py +25 -0
  106. sqlspec/adapters/duckdb/_typing.py +81 -0
  107. sqlspec/adapters/duckdb/adk/__init__.py +14 -0
  108. sqlspec/adapters/duckdb/adk/store.py +850 -0
  109. sqlspec/adapters/duckdb/config.py +463 -0
  110. sqlspec/adapters/duckdb/core.cpython-310-aarch64-linux-gnu.so +0 -0
  111. sqlspec/adapters/duckdb/core.py +257 -0
  112. sqlspec/adapters/duckdb/data_dictionary.py +140 -0
  113. sqlspec/adapters/duckdb/driver.py +430 -0
  114. sqlspec/adapters/duckdb/events/__init__.py +5 -0
  115. sqlspec/adapters/duckdb/events/store.py +57 -0
  116. sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
  117. sqlspec/adapters/duckdb/litestar/store.py +330 -0
  118. sqlspec/adapters/duckdb/pool.py +293 -0
  119. sqlspec/adapters/duckdb/type_converter.cpython-310-aarch64-linux-gnu.so +0 -0
  120. sqlspec/adapters/duckdb/type_converter.py +118 -0
  121. sqlspec/adapters/mock/__init__.py +72 -0
  122. sqlspec/adapters/mock/_typing.py +147 -0
  123. sqlspec/adapters/mock/config.py +483 -0
  124. sqlspec/adapters/mock/core.py +319 -0
  125. sqlspec/adapters/mock/data_dictionary.py +366 -0
  126. sqlspec/adapters/mock/driver.py +721 -0
  127. sqlspec/adapters/mysqlconnector/__init__.py +36 -0
  128. sqlspec/adapters/mysqlconnector/_typing.py +141 -0
  129. sqlspec/adapters/mysqlconnector/adk/__init__.py +15 -0
  130. sqlspec/adapters/mysqlconnector/adk/store.py +1060 -0
  131. sqlspec/adapters/mysqlconnector/config.py +394 -0
  132. sqlspec/adapters/mysqlconnector/core.cpython-310-aarch64-linux-gnu.so +0 -0
  133. sqlspec/adapters/mysqlconnector/core.py +303 -0
  134. sqlspec/adapters/mysqlconnector/data_dictionary.py +235 -0
  135. sqlspec/adapters/mysqlconnector/driver.py +483 -0
  136. sqlspec/adapters/mysqlconnector/events/__init__.py +8 -0
  137. sqlspec/adapters/mysqlconnector/events/store.py +98 -0
  138. sqlspec/adapters/mysqlconnector/litestar/__init__.py +5 -0
  139. sqlspec/adapters/mysqlconnector/litestar/store.py +426 -0
  140. sqlspec/adapters/oracledb/__init__.py +60 -0
  141. sqlspec/adapters/oracledb/_numpy_handlers.py +141 -0
  142. sqlspec/adapters/oracledb/_typing.py +182 -0
  143. sqlspec/adapters/oracledb/_uuid_handlers.py +166 -0
  144. sqlspec/adapters/oracledb/adk/__init__.py +10 -0
  145. sqlspec/adapters/oracledb/adk/store.py +2369 -0
  146. sqlspec/adapters/oracledb/config.py +550 -0
  147. sqlspec/adapters/oracledb/core.cpython-310-aarch64-linux-gnu.so +0 -0
  148. sqlspec/adapters/oracledb/core.py +543 -0
  149. sqlspec/adapters/oracledb/data_dictionary.py +536 -0
  150. sqlspec/adapters/oracledb/driver.py +1229 -0
  151. sqlspec/adapters/oracledb/events/__init__.py +16 -0
  152. sqlspec/adapters/oracledb/events/backend.py +347 -0
  153. sqlspec/adapters/oracledb/events/store.py +420 -0
  154. sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
  155. sqlspec/adapters/oracledb/litestar/store.py +781 -0
  156. sqlspec/adapters/oracledb/migrations.py +535 -0
  157. sqlspec/adapters/oracledb/type_converter.cpython-310-aarch64-linux-gnu.so +0 -0
  158. sqlspec/adapters/oracledb/type_converter.py +211 -0
  159. sqlspec/adapters/psqlpy/__init__.py +17 -0
  160. sqlspec/adapters/psqlpy/_typing.py +79 -0
  161. sqlspec/adapters/psqlpy/adk/__init__.py +5 -0
  162. sqlspec/adapters/psqlpy/adk/store.py +766 -0
  163. sqlspec/adapters/psqlpy/config.py +304 -0
  164. sqlspec/adapters/psqlpy/core.cpython-310-aarch64-linux-gnu.so +0 -0
  165. sqlspec/adapters/psqlpy/core.py +480 -0
  166. sqlspec/adapters/psqlpy/data_dictionary.py +126 -0
  167. sqlspec/adapters/psqlpy/driver.py +438 -0
  168. sqlspec/adapters/psqlpy/events/__init__.py +6 -0
  169. sqlspec/adapters/psqlpy/events/backend.py +310 -0
  170. sqlspec/adapters/psqlpy/events/store.py +20 -0
  171. sqlspec/adapters/psqlpy/litestar/__init__.py +5 -0
  172. sqlspec/adapters/psqlpy/litestar/store.py +270 -0
  173. sqlspec/adapters/psqlpy/type_converter.cpython-310-aarch64-linux-gnu.so +0 -0
  174. sqlspec/adapters/psqlpy/type_converter.py +113 -0
  175. sqlspec/adapters/psycopg/__init__.py +32 -0
  176. sqlspec/adapters/psycopg/_typing.py +164 -0
  177. sqlspec/adapters/psycopg/adk/__init__.py +10 -0
  178. sqlspec/adapters/psycopg/adk/store.py +1387 -0
  179. sqlspec/adapters/psycopg/config.py +576 -0
  180. sqlspec/adapters/psycopg/core.cpython-310-aarch64-linux-gnu.so +0 -0
  181. sqlspec/adapters/psycopg/core.py +450 -0
  182. sqlspec/adapters/psycopg/data_dictionary.py +289 -0
  183. sqlspec/adapters/psycopg/driver.py +975 -0
  184. sqlspec/adapters/psycopg/events/__init__.py +20 -0
  185. sqlspec/adapters/psycopg/events/backend.py +458 -0
  186. sqlspec/adapters/psycopg/events/store.py +42 -0
  187. sqlspec/adapters/psycopg/litestar/__init__.py +5 -0
  188. sqlspec/adapters/psycopg/litestar/store.py +552 -0
  189. sqlspec/adapters/psycopg/type_converter.cpython-310-aarch64-linux-gnu.so +0 -0
  190. sqlspec/adapters/psycopg/type_converter.py +93 -0
  191. sqlspec/adapters/pymysql/__init__.py +21 -0
  192. sqlspec/adapters/pymysql/_typing.py +71 -0
  193. sqlspec/adapters/pymysql/adk/__init__.py +5 -0
  194. sqlspec/adapters/pymysql/adk/store.py +540 -0
  195. sqlspec/adapters/pymysql/config.py +195 -0
  196. sqlspec/adapters/pymysql/core.cpython-310-aarch64-linux-gnu.so +0 -0
  197. sqlspec/adapters/pymysql/core.py +299 -0
  198. sqlspec/adapters/pymysql/data_dictionary.py +122 -0
  199. sqlspec/adapters/pymysql/driver.py +259 -0
  200. sqlspec/adapters/pymysql/events/__init__.py +5 -0
  201. sqlspec/adapters/pymysql/events/store.py +50 -0
  202. sqlspec/adapters/pymysql/litestar/__init__.py +5 -0
  203. sqlspec/adapters/pymysql/litestar/store.py +232 -0
  204. sqlspec/adapters/pymysql/pool.py +137 -0
  205. sqlspec/adapters/spanner/__init__.py +40 -0
  206. sqlspec/adapters/spanner/_typing.py +86 -0
  207. sqlspec/adapters/spanner/adk/__init__.py +5 -0
  208. sqlspec/adapters/spanner/adk/store.py +732 -0
  209. sqlspec/adapters/spanner/config.py +352 -0
  210. sqlspec/adapters/spanner/core.cpython-310-aarch64-linux-gnu.so +0 -0
  211. sqlspec/adapters/spanner/core.py +188 -0
  212. sqlspec/adapters/spanner/data_dictionary.py +120 -0
  213. sqlspec/adapters/spanner/dialect/__init__.py +6 -0
  214. sqlspec/adapters/spanner/dialect/_spangres.py +57 -0
  215. sqlspec/adapters/spanner/dialect/_spanner.py +130 -0
  216. sqlspec/adapters/spanner/driver.py +373 -0
  217. sqlspec/adapters/spanner/events/__init__.py +5 -0
  218. sqlspec/adapters/spanner/events/store.py +187 -0
  219. sqlspec/adapters/spanner/litestar/__init__.py +5 -0
  220. sqlspec/adapters/spanner/litestar/store.py +291 -0
  221. sqlspec/adapters/spanner/type_converter.cpython-310-aarch64-linux-gnu.so +0 -0
  222. sqlspec/adapters/spanner/type_converter.py +331 -0
  223. sqlspec/adapters/sqlite/__init__.py +19 -0
  224. sqlspec/adapters/sqlite/_typing.py +80 -0
  225. sqlspec/adapters/sqlite/adk/__init__.py +5 -0
  226. sqlspec/adapters/sqlite/adk/store.py +958 -0
  227. sqlspec/adapters/sqlite/config.py +280 -0
  228. sqlspec/adapters/sqlite/core.cpython-310-aarch64-linux-gnu.so +0 -0
  229. sqlspec/adapters/sqlite/core.py +312 -0
  230. sqlspec/adapters/sqlite/data_dictionary.py +202 -0
  231. sqlspec/adapters/sqlite/driver.py +359 -0
  232. sqlspec/adapters/sqlite/events/__init__.py +5 -0
  233. sqlspec/adapters/sqlite/events/store.py +20 -0
  234. sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
  235. sqlspec/adapters/sqlite/litestar/store.py +316 -0
  236. sqlspec/adapters/sqlite/pool.py +198 -0
  237. sqlspec/adapters/sqlite/type_converter.cpython-310-aarch64-linux-gnu.so +0 -0
  238. sqlspec/adapters/sqlite/type_converter.py +114 -0
  239. sqlspec/base.py +747 -0
  240. sqlspec/builder/__init__.py +179 -0
  241. sqlspec/builder/_base.cpython-310-aarch64-linux-gnu.so +0 -0
  242. sqlspec/builder/_base.py +1022 -0
  243. sqlspec/builder/_column.cpython-310-aarch64-linux-gnu.so +0 -0
  244. sqlspec/builder/_column.py +521 -0
  245. sqlspec/builder/_ddl.cpython-310-aarch64-linux-gnu.so +0 -0
  246. sqlspec/builder/_ddl.py +1642 -0
  247. sqlspec/builder/_delete.cpython-310-aarch64-linux-gnu.so +0 -0
  248. sqlspec/builder/_delete.py +95 -0
  249. sqlspec/builder/_dml.cpython-310-aarch64-linux-gnu.so +0 -0
  250. sqlspec/builder/_dml.py +365 -0
  251. sqlspec/builder/_explain.cpython-310-aarch64-linux-gnu.so +0 -0
  252. sqlspec/builder/_explain.py +579 -0
  253. sqlspec/builder/_expression_wrappers.cpython-310-aarch64-linux-gnu.so +0 -0
  254. sqlspec/builder/_expression_wrappers.py +46 -0
  255. sqlspec/builder/_factory.cpython-310-aarch64-linux-gnu.so +0 -0
  256. sqlspec/builder/_factory.py +1697 -0
  257. sqlspec/builder/_insert.cpython-310-aarch64-linux-gnu.so +0 -0
  258. sqlspec/builder/_insert.py +328 -0
  259. sqlspec/builder/_join.cpython-310-aarch64-linux-gnu.so +0 -0
  260. sqlspec/builder/_join.py +499 -0
  261. sqlspec/builder/_merge.cpython-310-aarch64-linux-gnu.so +0 -0
  262. sqlspec/builder/_merge.py +821 -0
  263. sqlspec/builder/_parsing_utils.cpython-310-aarch64-linux-gnu.so +0 -0
  264. sqlspec/builder/_parsing_utils.py +297 -0
  265. sqlspec/builder/_select.cpython-310-aarch64-linux-gnu.so +0 -0
  266. sqlspec/builder/_select.py +1660 -0
  267. sqlspec/builder/_temporal.cpython-310-aarch64-linux-gnu.so +0 -0
  268. sqlspec/builder/_temporal.py +139 -0
  269. sqlspec/builder/_update.cpython-310-aarch64-linux-gnu.so +0 -0
  270. sqlspec/builder/_update.py +173 -0
  271. sqlspec/builder/_vector_expressions.py +267 -0
  272. sqlspec/cli.py +911 -0
  273. sqlspec/config.py +1755 -0
  274. sqlspec/core/__init__.py +374 -0
  275. sqlspec/core/_correlation.cpython-310-aarch64-linux-gnu.so +0 -0
  276. sqlspec/core/_correlation.py +176 -0
  277. sqlspec/core/cache.cpython-310-aarch64-linux-gnu.so +0 -0
  278. sqlspec/core/cache.py +1069 -0
  279. sqlspec/core/compiler.cpython-310-aarch64-linux-gnu.so +0 -0
  280. sqlspec/core/compiler.py +954 -0
  281. sqlspec/core/explain.cpython-310-aarch64-linux-gnu.so +0 -0
  282. sqlspec/core/explain.py +275 -0
  283. sqlspec/core/filters.cpython-310-aarch64-linux-gnu.so +0 -0
  284. sqlspec/core/filters.py +952 -0
  285. sqlspec/core/hashing.cpython-310-aarch64-linux-gnu.so +0 -0
  286. sqlspec/core/hashing.py +262 -0
  287. sqlspec/core/metrics.cpython-310-aarch64-linux-gnu.so +0 -0
  288. sqlspec/core/metrics.py +83 -0
  289. sqlspec/core/parameters/__init__.py +71 -0
  290. sqlspec/core/parameters/_alignment.cpython-310-aarch64-linux-gnu.so +0 -0
  291. sqlspec/core/parameters/_alignment.py +270 -0
  292. sqlspec/core/parameters/_converter.cpython-310-aarch64-linux-gnu.so +0 -0
  293. sqlspec/core/parameters/_converter.py +543 -0
  294. sqlspec/core/parameters/_processor.cpython-310-aarch64-linux-gnu.so +0 -0
  295. sqlspec/core/parameters/_processor.py +505 -0
  296. sqlspec/core/parameters/_registry.cpython-310-aarch64-linux-gnu.so +0 -0
  297. sqlspec/core/parameters/_registry.py +206 -0
  298. sqlspec/core/parameters/_transformers.cpython-310-aarch64-linux-gnu.so +0 -0
  299. sqlspec/core/parameters/_transformers.py +292 -0
  300. sqlspec/core/parameters/_types.cpython-310-aarch64-linux-gnu.so +0 -0
  301. sqlspec/core/parameters/_types.py +499 -0
  302. sqlspec/core/parameters/_validator.cpython-310-aarch64-linux-gnu.so +0 -0
  303. sqlspec/core/parameters/_validator.py +180 -0
  304. sqlspec/core/pipeline.cpython-310-aarch64-linux-gnu.so +0 -0
  305. sqlspec/core/pipeline.py +319 -0
  306. sqlspec/core/query_modifiers.cpython-310-aarch64-linux-gnu.so +0 -0
  307. sqlspec/core/query_modifiers.py +437 -0
  308. sqlspec/core/result/__init__.py +23 -0
  309. sqlspec/core/result/_base.cpython-310-aarch64-linux-gnu.so +0 -0
  310. sqlspec/core/result/_base.py +1121 -0
  311. sqlspec/core/result/_io.cpython-310-aarch64-linux-gnu.so +0 -0
  312. sqlspec/core/result/_io.py +28 -0
  313. sqlspec/core/splitter.cpython-310-aarch64-linux-gnu.so +0 -0
  314. sqlspec/core/splitter.py +966 -0
  315. sqlspec/core/stack.cpython-310-aarch64-linux-gnu.so +0 -0
  316. sqlspec/core/stack.py +163 -0
  317. sqlspec/core/statement.cpython-310-aarch64-linux-gnu.so +0 -0
  318. sqlspec/core/statement.py +1503 -0
  319. sqlspec/core/type_converter.cpython-310-aarch64-linux-gnu.so +0 -0
  320. sqlspec/core/type_converter.py +339 -0
  321. sqlspec/data_dictionary/__init__.py +22 -0
  322. sqlspec/data_dictionary/_loader.py +123 -0
  323. sqlspec/data_dictionary/_registry.cpython-310-aarch64-linux-gnu.so +0 -0
  324. sqlspec/data_dictionary/_registry.py +74 -0
  325. sqlspec/data_dictionary/_types.cpython-310-aarch64-linux-gnu.so +0 -0
  326. sqlspec/data_dictionary/_types.py +121 -0
  327. sqlspec/data_dictionary/dialects/__init__.py +21 -0
  328. sqlspec/data_dictionary/dialects/bigquery.cpython-310-aarch64-linux-gnu.so +0 -0
  329. sqlspec/data_dictionary/dialects/bigquery.py +49 -0
  330. sqlspec/data_dictionary/dialects/cockroachdb.cpython-310-aarch64-linux-gnu.so +0 -0
  331. sqlspec/data_dictionary/dialects/cockroachdb.py +43 -0
  332. sqlspec/data_dictionary/dialects/duckdb.cpython-310-aarch64-linux-gnu.so +0 -0
  333. sqlspec/data_dictionary/dialects/duckdb.py +47 -0
  334. sqlspec/data_dictionary/dialects/mysql.cpython-310-aarch64-linux-gnu.so +0 -0
  335. sqlspec/data_dictionary/dialects/mysql.py +42 -0
  336. sqlspec/data_dictionary/dialects/oracle.cpython-310-aarch64-linux-gnu.so +0 -0
  337. sqlspec/data_dictionary/dialects/oracle.py +34 -0
  338. sqlspec/data_dictionary/dialects/postgres.cpython-310-aarch64-linux-gnu.so +0 -0
  339. sqlspec/data_dictionary/dialects/postgres.py +46 -0
  340. sqlspec/data_dictionary/dialects/spanner.cpython-310-aarch64-linux-gnu.so +0 -0
  341. sqlspec/data_dictionary/dialects/spanner.py +37 -0
  342. sqlspec/data_dictionary/dialects/sqlite.cpython-310-aarch64-linux-gnu.so +0 -0
  343. sqlspec/data_dictionary/dialects/sqlite.py +42 -0
  344. sqlspec/data_dictionary/sql/.gitkeep +0 -0
  345. sqlspec/data_dictionary/sql/bigquery/columns.sql +23 -0
  346. sqlspec/data_dictionary/sql/bigquery/foreign_keys.sql +34 -0
  347. sqlspec/data_dictionary/sql/bigquery/indexes.sql +19 -0
  348. sqlspec/data_dictionary/sql/bigquery/tables.sql +33 -0
  349. sqlspec/data_dictionary/sql/bigquery/version.sql +3 -0
  350. sqlspec/data_dictionary/sql/cockroachdb/columns.sql +34 -0
  351. sqlspec/data_dictionary/sql/cockroachdb/foreign_keys.sql +40 -0
  352. sqlspec/data_dictionary/sql/cockroachdb/indexes.sql +32 -0
  353. sqlspec/data_dictionary/sql/cockroachdb/tables.sql +44 -0
  354. sqlspec/data_dictionary/sql/cockroachdb/version.sql +3 -0
  355. sqlspec/data_dictionary/sql/duckdb/columns.sql +23 -0
  356. sqlspec/data_dictionary/sql/duckdb/foreign_keys.sql +36 -0
  357. sqlspec/data_dictionary/sql/duckdb/indexes.sql +19 -0
  358. sqlspec/data_dictionary/sql/duckdb/tables.sql +38 -0
  359. sqlspec/data_dictionary/sql/duckdb/version.sql +3 -0
  360. sqlspec/data_dictionary/sql/mysql/columns.sql +23 -0
  361. sqlspec/data_dictionary/sql/mysql/foreign_keys.sql +28 -0
  362. sqlspec/data_dictionary/sql/mysql/indexes.sql +26 -0
  363. sqlspec/data_dictionary/sql/mysql/tables.sql +33 -0
  364. sqlspec/data_dictionary/sql/mysql/version.sql +3 -0
  365. sqlspec/data_dictionary/sql/oracle/columns.sql +23 -0
  366. sqlspec/data_dictionary/sql/oracle/foreign_keys.sql +48 -0
  367. sqlspec/data_dictionary/sql/oracle/indexes.sql +44 -0
  368. sqlspec/data_dictionary/sql/oracle/tables.sql +25 -0
  369. sqlspec/data_dictionary/sql/oracle/version.sql +20 -0
  370. sqlspec/data_dictionary/sql/postgres/columns.sql +34 -0
  371. sqlspec/data_dictionary/sql/postgres/foreign_keys.sql +40 -0
  372. sqlspec/data_dictionary/sql/postgres/indexes.sql +56 -0
  373. sqlspec/data_dictionary/sql/postgres/tables.sql +44 -0
  374. sqlspec/data_dictionary/sql/postgres/version.sql +3 -0
  375. sqlspec/data_dictionary/sql/spanner/columns.sql +23 -0
  376. sqlspec/data_dictionary/sql/spanner/foreign_keys.sql +70 -0
  377. sqlspec/data_dictionary/sql/spanner/indexes.sql +30 -0
  378. sqlspec/data_dictionary/sql/spanner/tables.sql +9 -0
  379. sqlspec/data_dictionary/sql/spanner/version.sql +3 -0
  380. sqlspec/data_dictionary/sql/sqlite/columns.sql +23 -0
  381. sqlspec/data_dictionary/sql/sqlite/foreign_keys.sql +22 -0
  382. sqlspec/data_dictionary/sql/sqlite/indexes.sql +7 -0
  383. sqlspec/data_dictionary/sql/sqlite/tables.sql +28 -0
  384. sqlspec/data_dictionary/sql/sqlite/version.sql +3 -0
  385. sqlspec/driver/__init__.py +32 -0
  386. sqlspec/driver/_async.cpython-310-aarch64-linux-gnu.so +0 -0
  387. sqlspec/driver/_async.py +1737 -0
  388. sqlspec/driver/_common.cpython-310-aarch64-linux-gnu.so +0 -0
  389. sqlspec/driver/_common.py +1478 -0
  390. sqlspec/driver/_sql_helpers.cpython-310-aarch64-linux-gnu.so +0 -0
  391. sqlspec/driver/_sql_helpers.py +148 -0
  392. sqlspec/driver/_storage_helpers.cpython-310-aarch64-linux-gnu.so +0 -0
  393. sqlspec/driver/_storage_helpers.py +144 -0
  394. sqlspec/driver/_sync.cpython-310-aarch64-linux-gnu.so +0 -0
  395. sqlspec/driver/_sync.py +1710 -0
  396. sqlspec/exceptions.py +338 -0
  397. sqlspec/extensions/__init__.py +0 -0
  398. sqlspec/extensions/adk/__init__.py +70 -0
  399. sqlspec/extensions/adk/_types.py +51 -0
  400. sqlspec/extensions/adk/converters.py +172 -0
  401. sqlspec/extensions/adk/memory/__init__.py +69 -0
  402. sqlspec/extensions/adk/memory/_types.py +30 -0
  403. sqlspec/extensions/adk/memory/converters.py +149 -0
  404. sqlspec/extensions/adk/memory/service.py +217 -0
  405. sqlspec/extensions/adk/memory/store.py +569 -0
  406. sqlspec/extensions/adk/migrations/0001_create_adk_tables.py +246 -0
  407. sqlspec/extensions/adk/migrations/__init__.py +0 -0
  408. sqlspec/extensions/adk/service.py +225 -0
  409. sqlspec/extensions/adk/store.py +567 -0
  410. sqlspec/extensions/events/__init__.py +51 -0
  411. sqlspec/extensions/events/_channel.py +703 -0
  412. sqlspec/extensions/events/_hints.py +45 -0
  413. sqlspec/extensions/events/_models.py +23 -0
  414. sqlspec/extensions/events/_payload.py +69 -0
  415. sqlspec/extensions/events/_protocols.py +134 -0
  416. sqlspec/extensions/events/_queue.py +461 -0
  417. sqlspec/extensions/events/_store.py +209 -0
  418. sqlspec/extensions/events/migrations/0001_create_event_queue.py +59 -0
  419. sqlspec/extensions/events/migrations/__init__.py +3 -0
  420. sqlspec/extensions/fastapi/__init__.py +19 -0
  421. sqlspec/extensions/fastapi/extension.py +351 -0
  422. sqlspec/extensions/fastapi/providers.py +607 -0
  423. sqlspec/extensions/flask/__init__.py +37 -0
  424. sqlspec/extensions/flask/_state.py +76 -0
  425. sqlspec/extensions/flask/_utils.py +71 -0
  426. sqlspec/extensions/flask/extension.py +519 -0
  427. sqlspec/extensions/litestar/__init__.py +28 -0
  428. sqlspec/extensions/litestar/_utils.py +52 -0
  429. sqlspec/extensions/litestar/channels.py +165 -0
  430. sqlspec/extensions/litestar/cli.py +102 -0
  431. sqlspec/extensions/litestar/config.py +90 -0
  432. sqlspec/extensions/litestar/handlers.py +316 -0
  433. sqlspec/extensions/litestar/migrations/0001_create_session_table.py +137 -0
  434. sqlspec/extensions/litestar/migrations/__init__.py +3 -0
  435. sqlspec/extensions/litestar/plugin.py +671 -0
  436. sqlspec/extensions/litestar/providers.py +526 -0
  437. sqlspec/extensions/litestar/store.py +296 -0
  438. sqlspec/extensions/otel/__init__.py +58 -0
  439. sqlspec/extensions/prometheus/__init__.py +113 -0
  440. sqlspec/extensions/starlette/__init__.py +19 -0
  441. sqlspec/extensions/starlette/_state.py +30 -0
  442. sqlspec/extensions/starlette/_utils.py +96 -0
  443. sqlspec/extensions/starlette/extension.py +346 -0
  444. sqlspec/extensions/starlette/middleware.py +235 -0
  445. sqlspec/loader.cpython-310-aarch64-linux-gnu.so +0 -0
  446. sqlspec/loader.py +702 -0
  447. sqlspec/migrations/__init__.py +36 -0
  448. sqlspec/migrations/base.py +731 -0
  449. sqlspec/migrations/commands.py +1232 -0
  450. sqlspec/migrations/context.py +157 -0
  451. sqlspec/migrations/fix.py +204 -0
  452. sqlspec/migrations/loaders.py +443 -0
  453. sqlspec/migrations/runner.py +1172 -0
  454. sqlspec/migrations/templates.py +234 -0
  455. sqlspec/migrations/tracker.py +611 -0
  456. sqlspec/migrations/utils.py +256 -0
  457. sqlspec/migrations/validation.py +207 -0
  458. sqlspec/migrations/version.py +446 -0
  459. sqlspec/observability/__init__.py +55 -0
  460. sqlspec/observability/_common.cpython-310-aarch64-linux-gnu.so +0 -0
  461. sqlspec/observability/_common.py +77 -0
  462. sqlspec/observability/_config.cpython-310-aarch64-linux-gnu.so +0 -0
  463. sqlspec/observability/_config.py +348 -0
  464. sqlspec/observability/_diagnostics.cpython-310-aarch64-linux-gnu.so +0 -0
  465. sqlspec/observability/_diagnostics.py +74 -0
  466. sqlspec/observability/_dispatcher.cpython-310-aarch64-linux-gnu.so +0 -0
  467. sqlspec/observability/_dispatcher.py +152 -0
  468. sqlspec/observability/_formatters/__init__.py +13 -0
  469. sqlspec/observability/_formatters/_aws.cpython-310-aarch64-linux-gnu.so +0 -0
  470. sqlspec/observability/_formatters/_aws.py +102 -0
  471. sqlspec/observability/_formatters/_azure.cpython-310-aarch64-linux-gnu.so +0 -0
  472. sqlspec/observability/_formatters/_azure.py +96 -0
  473. sqlspec/observability/_formatters/_base.cpython-310-aarch64-linux-gnu.so +0 -0
  474. sqlspec/observability/_formatters/_base.py +57 -0
  475. sqlspec/observability/_formatters/_gcp.cpython-310-aarch64-linux-gnu.so +0 -0
  476. sqlspec/observability/_formatters/_gcp.py +131 -0
  477. sqlspec/observability/_formatting.py +58 -0
  478. sqlspec/observability/_observer.cpython-310-aarch64-linux-gnu.so +0 -0
  479. sqlspec/observability/_observer.py +357 -0
  480. sqlspec/observability/_runtime.cpython-310-aarch64-linux-gnu.so +0 -0
  481. sqlspec/observability/_runtime.py +420 -0
  482. sqlspec/observability/_sampling.cpython-310-aarch64-linux-gnu.so +0 -0
  483. sqlspec/observability/_sampling.py +188 -0
  484. sqlspec/observability/_spans.cpython-310-aarch64-linux-gnu.so +0 -0
  485. sqlspec/observability/_spans.py +161 -0
  486. sqlspec/protocols.py +916 -0
  487. sqlspec/py.typed +0 -0
  488. sqlspec/storage/__init__.py +48 -0
  489. sqlspec/storage/_utils.py +104 -0
  490. sqlspec/storage/backends/__init__.py +1 -0
  491. sqlspec/storage/backends/base.py +253 -0
  492. sqlspec/storage/backends/fsspec.py +529 -0
  493. sqlspec/storage/backends/local.py +441 -0
  494. sqlspec/storage/backends/obstore.py +916 -0
  495. sqlspec/storage/errors.py +104 -0
  496. sqlspec/storage/pipeline.py +582 -0
  497. sqlspec/storage/registry.py +301 -0
  498. sqlspec/typing.py +395 -0
  499. sqlspec/utils/__init__.py +7 -0
  500. sqlspec/utils/arrow_helpers.py +318 -0
  501. sqlspec/utils/config_tools.py +332 -0
  502. sqlspec/utils/correlation.cpython-310-aarch64-linux-gnu.so +0 -0
  503. sqlspec/utils/correlation.py +134 -0
  504. sqlspec/utils/deprecation.py +190 -0
  505. sqlspec/utils/fixtures.cpython-310-aarch64-linux-gnu.so +0 -0
  506. sqlspec/utils/fixtures.py +258 -0
  507. sqlspec/utils/logging.py +222 -0
  508. sqlspec/utils/module_loader.py +306 -0
  509. sqlspec/utils/portal.cpython-310-aarch64-linux-gnu.so +0 -0
  510. sqlspec/utils/portal.py +375 -0
  511. sqlspec/utils/schema.cpython-310-aarch64-linux-gnu.so +0 -0
  512. sqlspec/utils/schema.py +485 -0
  513. sqlspec/utils/serializers.cpython-310-aarch64-linux-gnu.so +0 -0
  514. sqlspec/utils/serializers.py +408 -0
  515. sqlspec/utils/singleton.cpython-310-aarch64-linux-gnu.so +0 -0
  516. sqlspec/utils/singleton.py +41 -0
  517. sqlspec/utils/sync_tools.cpython-310-aarch64-linux-gnu.so +0 -0
  518. sqlspec/utils/sync_tools.py +311 -0
  519. sqlspec/utils/text.cpython-310-aarch64-linux-gnu.so +0 -0
  520. sqlspec/utils/text.py +108 -0
  521. sqlspec/utils/type_converters.cpython-310-aarch64-linux-gnu.so +0 -0
  522. sqlspec/utils/type_converters.py +128 -0
  523. sqlspec/utils/type_guards.cpython-310-aarch64-linux-gnu.so +0 -0
  524. sqlspec/utils/type_guards.py +1360 -0
  525. sqlspec/utils/uuids.cpython-310-aarch64-linux-gnu.so +0 -0
  526. sqlspec/utils/uuids.py +225 -0
  527. sqlspec-0.36.0.dist-info/METADATA +205 -0
  528. sqlspec-0.36.0.dist-info/RECORD +531 -0
  529. sqlspec-0.36.0.dist-info/WHEEL +7 -0
  530. sqlspec-0.36.0.dist-info/entry_points.txt +2 -0
  531. sqlspec-0.36.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,1172 @@
1
+ """Migration execution engine for SQLSpec."""
2
+
3
+ import ast
4
+ import hashlib
5
+ import inspect
6
+ import logging
7
+ import re
8
+ import time
9
+ from abc import ABC, abstractmethod
10
+ from pathlib import Path
11
+ from typing import TYPE_CHECKING, Any, Literal, Union, cast, overload
12
+
13
+ from sqlspec.core import SQL
14
+ from sqlspec.loader import SQLFileLoader
15
+ from sqlspec.migrations.context import MigrationContext
16
+ from sqlspec.migrations.loaders import get_migration_loader
17
+ from sqlspec.migrations.templates import TemplateDescriptionHints
18
+ from sqlspec.migrations.version import parse_version
19
+ from sqlspec.observability import resolve_db_system
20
+ from sqlspec.utils.logging import get_logger, log_with_context
21
+ from sqlspec.utils.sync_tools import async_, await_
22
+
23
+ if TYPE_CHECKING:
24
+ from collections.abc import Awaitable, Callable, Coroutine
25
+
26
+ from sqlspec.config import DatabaseConfigProtocol
27
+ from sqlspec.driver import AsyncDriverAdapterBase, SyncDriverAdapterBase
28
+ from sqlspec.observability import ObservabilityRuntime
29
+
30
+ __all__ = ("AsyncMigrationRunner", "SyncMigrationRunner", "create_migration_runner")
31
+
32
+ logger = get_logger("sqlspec.migrations.runner")
33
+
34
+
35
+ class _CachedMigrationMetadata:
36
+ """Cached migration metadata keyed by file path."""
37
+
38
+ __slots__ = ("metadata", "mtime_ns", "size")
39
+
40
+ def __init__(self, metadata: "dict[str, Any]", mtime_ns: int, size: int) -> None:
41
+ self.metadata = metadata
42
+ self.mtime_ns = mtime_ns
43
+ self.size = size
44
+
45
+ def clone(self) -> "dict[str, Any]":
46
+ return dict(self.metadata)
47
+
48
+
49
+ class _MigrationFileEntry:
50
+ """Represents a migration file discovered during directory scanning."""
51
+
52
+ __slots__ = ("extension_name", "path")
53
+
54
+ def __init__(self, path: Path, extension_name: "str | None") -> None:
55
+ self.path = path
56
+ self.extension_name = extension_name
57
+
58
+
59
+ class BaseMigrationRunner(ABC):
60
+ """Base migration runner with common functionality shared between sync and async implementations."""
61
+
62
+ def __init__(
63
+ self,
64
+ migrations_path: Path,
65
+ extension_migrations: "dict[str, Path] | None" = None,
66
+ context: "MigrationContext | None" = None,
67
+ extension_configs: "dict[str, dict[str, Any]] | None" = None,
68
+ runtime: "ObservabilityRuntime | None" = None,
69
+ description_hints: "TemplateDescriptionHints | None" = None,
70
+ ) -> None:
71
+ """Initialize the migration runner.
72
+
73
+ Args:
74
+ migrations_path: Path to the directory containing migration files.
75
+ extension_migrations: Optional mapping of extension names to their migration paths.
76
+ context: Optional migration context for Python migrations.
77
+ extension_configs: Optional mapping of extension names to their configurations.
78
+ runtime: Observability runtime shared with command/context consumers.
79
+ description_hints: Hints for extracting migration descriptions.
80
+ """
81
+ self.migrations_path = migrations_path
82
+ self.extension_migrations = extension_migrations or {}
83
+ self.runtime = runtime
84
+ self.loader = SQLFileLoader(runtime=runtime)
85
+ self.project_root: Path | None = None
86
+ self.context = context
87
+ self.extension_configs = extension_configs or {}
88
+ self._listing_digest: str | None = None
89
+ self._listing_cache: list[tuple[str, Path]] | None = None
90
+ self._listing_signatures: dict[str, tuple[int, int]] = {}
91
+ self._metadata_cache: dict[str, _CachedMigrationMetadata] = {}
92
+ self.description_hints = description_hints or TemplateDescriptionHints()
93
+
94
+ def _metric(self, name: str, amount: float = 1.0) -> None:
95
+ if self.runtime is None:
96
+ return
97
+ self.runtime.increment_metric(name, amount)
98
+
99
+ def _iter_directory_entries(self, base_path: Path, extension_name: "str | None") -> "list[_MigrationFileEntry]":
100
+ """Collect migration files discovered under a base path."""
101
+
102
+ if not base_path.exists():
103
+ return []
104
+
105
+ entries: list[_MigrationFileEntry] = []
106
+ for pattern in ("*.sql", "*.py"):
107
+ for file_path in sorted(base_path.glob(pattern)):
108
+ if file_path.name.startswith("."):
109
+ continue
110
+ entries.append(_MigrationFileEntry(path=file_path, extension_name=extension_name))
111
+ return entries
112
+
113
+ def _collect_listing_entries(self) -> "tuple[list[_MigrationFileEntry], dict[str, tuple[int, int]], str]":
114
+ """Gather migration files, stat signatures, and digest for cache validation."""
115
+
116
+ entries: list[_MigrationFileEntry] = []
117
+ signatures: dict[str, tuple[int, int]] = {}
118
+ digest_source = hashlib.md5(usedforsecurity=False)
119
+
120
+ for entry in self._iter_directory_entries(self.migrations_path, None):
121
+ self._record_entry(entry, entries, signatures, digest_source)
122
+
123
+ for ext_name, ext_path in self.extension_migrations.items():
124
+ for entry in self._iter_directory_entries(ext_path, ext_name):
125
+ self._record_entry(entry, entries, signatures, digest_source)
126
+
127
+ return entries, signatures, digest_source.hexdigest()
128
+
129
+ def _record_entry(
130
+ self,
131
+ entry: _MigrationFileEntry,
132
+ entries: "list[_MigrationFileEntry]",
133
+ signatures: "dict[str, tuple[int, int]]",
134
+ digest_source: Any,
135
+ ) -> None:
136
+ """Record entry metadata for cache decisions."""
137
+
138
+ try:
139
+ stat_result = entry.path.stat()
140
+ except FileNotFoundError:
141
+ return
142
+
143
+ path_str = str(entry.path)
144
+ token = (stat_result.st_mtime_ns, stat_result.st_size)
145
+ signatures[path_str] = token
146
+ digest_source.update(path_str.encode("utf-8"))
147
+ digest_source.update(f"{token[0]}:{token[1]}".encode())
148
+ entries.append(entry)
149
+
150
+ def _build_sorted_listing(self, entries: "list[_MigrationFileEntry]") -> "list[tuple[str, Path]]":
151
+ """Construct sorted migration listing from directory entries."""
152
+
153
+ migrations: list[tuple[str, Path]] = []
154
+
155
+ for entry in entries:
156
+ version = self._extract_version(entry.path.name)
157
+ if not version:
158
+ continue
159
+ if entry.extension_name:
160
+ version = f"ext_{entry.extension_name}_{version}"
161
+ migrations.append((version, entry.path))
162
+
163
+ def version_sort_key(migration_tuple: "tuple[str, Path]") -> "Any":
164
+ version_str = migration_tuple[0]
165
+ try:
166
+ return parse_version(version_str)
167
+ except ValueError:
168
+ return version_str
169
+
170
+ return sorted(migrations, key=version_sort_key)
171
+
172
+ def _log_listing_invalidation(
173
+ self, previous: "dict[str, tuple[int, int]]", current: "dict[str, tuple[int, int]]"
174
+ ) -> None:
175
+ """Log cache invalidation details at INFO level."""
176
+
177
+ prev_keys = set(previous)
178
+ curr_keys = set(current)
179
+ added = curr_keys - prev_keys
180
+ removed = prev_keys - curr_keys
181
+ modified = {key for key in prev_keys & curr_keys if previous[key] != current[key]}
182
+ logger.info(
183
+ "Migration listing cache invalidated (added=%d, removed=%d, modified=%d)",
184
+ len(added),
185
+ len(removed),
186
+ len(modified),
187
+ )
188
+ self._metric("migrations.listing.cache_invalidations")
189
+ if added:
190
+ self._metric("migrations.listing.added", float(len(added)))
191
+ if removed:
192
+ self._metric("migrations.listing.removed", float(len(removed)))
193
+ if modified:
194
+ self._metric("migrations.listing.modified", float(len(modified)))
195
+
196
+ def _extract_version(self, filename: str) -> "str | None":
197
+ """Extract version from filename.
198
+
199
+ Supports sequential (0001), timestamp (20251011120000), and extension-prefixed
200
+ (ext_litestar_0001) version formats.
201
+
202
+ Args:
203
+ filename: The migration filename.
204
+
205
+ Returns:
206
+ The extracted version string or None.
207
+ """
208
+ extension_version_parts = 3
209
+ timestamp_min_length = 4
210
+
211
+ name_without_ext = filename.rsplit(".", 1)[0]
212
+
213
+ if name_without_ext.startswith("ext_"):
214
+ parts = name_without_ext.split("_", 3)
215
+ if len(parts) >= extension_version_parts:
216
+ return f"{parts[0]}_{parts[1]}_{parts[2]}"
217
+ return None
218
+
219
+ parts = name_without_ext.split("_", 1)
220
+ if parts and parts[0].isdigit():
221
+ return parts[0] if len(parts[0]) > timestamp_min_length else parts[0].zfill(4)
222
+
223
+ return None
224
+
225
+ def _calculate_checksum(self, content: str) -> str:
226
+ """Calculate MD5 checksum of migration content.
227
+
228
+ Canonicalizes content by excluding query name headers that change during
229
+ fix command (migrate-{version}-up/down). This ensures checksums remain
230
+ stable when converting timestamp versions to sequential format.
231
+
232
+ Args:
233
+ content: The migration file content.
234
+
235
+ Returns:
236
+ MD5 checksum hex string.
237
+ """
238
+ canonical_content = re.sub(r"^--\s*name:\s*migrate-[^-]+-(?:up|down)\s*$", "", content, flags=re.MULTILINE)
239
+
240
+ return hashlib.md5(canonical_content.encode()).hexdigest() # noqa: S324
241
+
242
+ @abstractmethod
243
+ def load_migration(self, file_path: Path) -> Union["dict[str, Any]", "Coroutine[Any, Any, dict[str, Any]]"]:
244
+ """Load a migration file and extract its components.
245
+
246
+ Args:
247
+ file_path: Path to the migration file.
248
+
249
+ Returns:
250
+ Dictionary containing migration metadata and queries.
251
+ For async implementations, returns a coroutine.
252
+ """
253
+
254
+ def _load_migration_listing(self) -> "list[tuple[str, Path]]":
255
+ """Build the cached migration listing shared by sync/async runners."""
256
+ entries, signatures, digest = self._collect_listing_entries()
257
+ cached_listing = self._listing_cache
258
+
259
+ if cached_listing is not None and self._listing_digest == digest:
260
+ self._metric("migrations.listing.cache_hit")
261
+ self._metric("migrations.listing.files_cached", float(len(cached_listing)))
262
+ logger.debug("Migration listing cache hit (%d files)", len(cached_listing))
263
+ return cached_listing
264
+
265
+ files = self._build_sorted_listing(entries)
266
+ previous_digest = self._listing_digest
267
+ previous_signatures = self._listing_signatures
268
+
269
+ self._metric("migrations.listing.cache_miss")
270
+ self._metric("migrations.listing.files_scanned", float(len(files)))
271
+
272
+ self._listing_cache = files
273
+ self._listing_signatures = signatures
274
+ self._listing_digest = digest
275
+
276
+ if previous_digest is None:
277
+ logger.debug("Primed migration listing cache with %d files", len(files))
278
+ else:
279
+ self._log_listing_invalidation(previous_signatures, signatures)
280
+
281
+ return files
282
+
283
+ @abstractmethod
284
+ def get_migration_files(self) -> "list[tuple[str, Path]] | Awaitable[list[tuple[str, Path]]]":
285
+ """Get all migration files sorted by version."""
286
+
287
+ def _load_migration_metadata_common(self, file_path: Path, version: "str | None" = None) -> "dict[str, Any]":
288
+ """Load common migration metadata that doesn't require async operations.
289
+
290
+ Args:
291
+ file_path: Path to the migration file.
292
+ version: Optional pre-extracted version (preserves prefixes like ext_adk_0001).
293
+
294
+ Returns:
295
+ Partial migration metadata dictionary.
296
+ """
297
+ cache_key = str(file_path)
298
+ stat_result = file_path.stat()
299
+ cached_metadata = self._metadata_cache.get(cache_key)
300
+ if (
301
+ cached_metadata
302
+ and cached_metadata.mtime_ns == stat_result.st_mtime_ns
303
+ and cached_metadata.size == stat_result.st_size
304
+ ):
305
+ self._metric("migrations.metadata.cache_hit")
306
+ logger.debug("Migration metadata cache hit: %s", cache_key)
307
+ metadata = cached_metadata.clone()
308
+ metadata["file_path"] = file_path
309
+ return metadata
310
+
311
+ self._metric("migrations.metadata.cache_miss")
312
+ self._metric("migrations.metadata.bytes", float(stat_result.st_size))
313
+
314
+ content = file_path.read_text(encoding="utf-8")
315
+ checksum = self._calculate_checksum(content)
316
+ if version is None:
317
+ version = self._extract_version(file_path.name)
318
+ description = self._extract_description(content, file_path)
319
+ if not description:
320
+ description = file_path.stem.split("_", 1)[1] if "_" in file_path.stem else ""
321
+
322
+ transactional_match = re.search(
323
+ r"^--\s*transactional:\s*(true|false)\s*$", content, re.MULTILINE | re.IGNORECASE
324
+ )
325
+ transactional = None
326
+ if transactional_match:
327
+ transactional = transactional_match.group(1).lower() == "true"
328
+
329
+ metadata = {
330
+ "version": version,
331
+ "description": description,
332
+ "file_path": file_path,
333
+ "checksum": checksum,
334
+ "content": content,
335
+ "transactional": transactional,
336
+ }
337
+ self._metadata_cache[cache_key] = _CachedMigrationMetadata(
338
+ metadata=dict(metadata), mtime_ns=stat_result.st_mtime_ns, size=stat_result.st_size
339
+ )
340
+ if cached_metadata:
341
+ logger.debug("Migration metadata cache invalidated: %s", cache_key)
342
+ else:
343
+ logger.debug("Cached migration metadata: %s", cache_key)
344
+ return metadata
345
+
346
+ def _extract_description(self, content: str, file_path: Path) -> str:
347
+ if file_path.suffix == ".sql":
348
+ return self._extract_sql_description(content)
349
+ if file_path.suffix == ".py":
350
+ return self._extract_python_description(content)
351
+ return ""
352
+
353
+ def _extract_sql_description(self, content: str) -> str:
354
+ keys = self.description_hints.sql_keys
355
+ for line in content.splitlines():
356
+ stripped = line.strip()
357
+ if not stripped:
358
+ continue
359
+ if stripped.startswith("--"):
360
+ body = stripped.lstrip("-").strip()
361
+ if not body:
362
+ continue
363
+ if ":" in body:
364
+ key, value = body.split(":", 1)
365
+ if key.strip() in keys:
366
+ return value.strip()
367
+ continue
368
+ break
369
+ return ""
370
+
371
+ def _extract_python_description(self, content: str) -> str:
372
+ try:
373
+ module = ast.parse(content)
374
+ except SyntaxError:
375
+ return ""
376
+ docstring = ast.get_docstring(module) or ""
377
+ keys = self.description_hints.python_keys
378
+ for line in docstring.splitlines():
379
+ stripped = line.strip()
380
+ if not stripped:
381
+ continue
382
+ if ":" in stripped:
383
+ key, value = stripped.split(":", 1)
384
+ if key.strip() in keys:
385
+ return value.strip()
386
+ return stripped
387
+ return ""
388
+
389
+ def _get_context_for_migration(self, file_path: Path) -> "MigrationContext | None":
390
+ """Get the appropriate context for a migration file.
391
+
392
+ Args:
393
+ file_path: Path to the migration file.
394
+
395
+ Returns:
396
+ Migration context to use, or None to use default.
397
+ """
398
+ context_to_use = self.context
399
+ if context_to_use and file_path.name.startswith("ext_"):
400
+ version = self._extract_version(file_path.name)
401
+ if version and version.startswith("ext_"):
402
+ min_extension_version_parts = 3
403
+ parts = version.split("_", 2)
404
+ if len(parts) >= min_extension_version_parts:
405
+ ext_name = parts[1]
406
+ if ext_name in self.extension_configs:
407
+ context_to_use = MigrationContext(
408
+ dialect=self.context.dialect if self.context else None,
409
+ config=self.context.config if self.context else None,
410
+ driver=self.context.driver if self.context else None,
411
+ metadata=self.context.metadata.copy() if self.context and self.context.metadata else {},
412
+ extension_config=self.extension_configs[ext_name],
413
+ )
414
+
415
+ for ext_name, ext_path in self.extension_migrations.items():
416
+ if file_path.parent == ext_path:
417
+ if ext_name in self.extension_configs and self.context:
418
+ context_to_use = MigrationContext(
419
+ config=self.context.config,
420
+ dialect=self.context.dialect,
421
+ driver=self.context.driver,
422
+ metadata=self.context.metadata.copy() if self.context.metadata else {},
423
+ extension_config=self.extension_configs[ext_name],
424
+ )
425
+ break
426
+
427
+ return context_to_use
428
+
429
+ def should_use_transaction(
430
+ self, migration: "dict[str, Any]", config: "DatabaseConfigProtocol[Any, Any, Any]"
431
+ ) -> bool:
432
+ """Determine if migration should run in a transaction.
433
+
434
+ Args:
435
+ migration: Migration metadata dictionary.
436
+ config: The database configuration instance.
437
+
438
+ Returns:
439
+ True if migration should be wrapped in a transaction.
440
+ """
441
+ if not config.supports_transactional_ddl:
442
+ return False
443
+
444
+ if migration.get("transactional") is not None:
445
+ return bool(migration["transactional"])
446
+
447
+ migration_config = cast("dict[str, Any]", config.migration_config) or {}
448
+ return bool(migration_config.get("transactional", True))
449
+
450
+
451
+ class SyncMigrationRunner(BaseMigrationRunner):
452
+ """Synchronous migration runner with pure sync methods."""
453
+
454
+ def get_migration_files(self) -> "list[tuple[str, Path]]":
455
+ """Get all migration files sorted by version.
456
+
457
+ Returns:
458
+ List of (version, path) tuples sorted by version.
459
+ """
460
+ return self._load_migration_listing()
461
+
462
+ def load_migration(self, file_path: Path, version: "str | None" = None) -> "dict[str, Any]":
463
+ """Load a migration file and extract its components.
464
+
465
+ Args:
466
+ file_path: Path to the migration file.
467
+ version: Optional pre-extracted version (preserves prefixes like ext_adk_0001).
468
+
469
+ Returns:
470
+ Dictionary containing migration metadata and queries.
471
+ """
472
+ metadata = self._load_migration_metadata_common(file_path, version)
473
+ context_to_use = self._get_context_for_migration(file_path)
474
+
475
+ loader = get_migration_loader(file_path, self.migrations_path, self.project_root, context_to_use, self.loader)
476
+ loader.validate_migration_file(file_path)
477
+
478
+ has_upgrade, has_downgrade = True, False
479
+
480
+ if file_path.suffix == ".sql":
481
+ version = metadata["version"]
482
+ up_query, down_query = f"migrate-{version}-up", f"migrate-{version}-down"
483
+ has_upgrade, has_downgrade = self.loader.has_query(up_query), self.loader.has_query(down_query)
484
+ else:
485
+ try:
486
+ has_downgrade = bool(self._get_migration_sql({"loader": loader, "file_path": file_path}, "down"))
487
+ except Exception:
488
+ has_downgrade = False
489
+
490
+ metadata.update({"has_upgrade": has_upgrade, "has_downgrade": has_downgrade, "loader": loader})
491
+ return metadata
492
+
493
+ def execute_upgrade(
494
+ self,
495
+ driver: "SyncDriverAdapterBase",
496
+ migration: "dict[str, Any]",
497
+ *,
498
+ use_transaction: "bool | None" = None,
499
+ on_success: "Callable[[int], None] | None" = None,
500
+ ) -> "tuple[str | None, int]":
501
+ """Execute an upgrade migration.
502
+
503
+ Args:
504
+ driver: The sync database driver to use.
505
+ migration: Migration metadata dictionary.
506
+ use_transaction: Override transaction behavior. If None, uses should_use_transaction logic.
507
+ on_success: Callback invoked with execution_time_ms before commit (for version tracking).
508
+
509
+ Returns:
510
+ Tuple of (sql_content, execution_time_ms).
511
+ """
512
+ upgrade_sql_list = self._get_migration_sql(migration, "up")
513
+ if upgrade_sql_list is None:
514
+ self._metric("migrations.upgrade.skipped")
515
+ log_with_context(
516
+ logger,
517
+ logging.WARNING,
518
+ "migration.apply",
519
+ db_system=resolve_db_system(type(driver).__name__),
520
+ version=migration.get("version"),
521
+ status="missing",
522
+ )
523
+ return None, 0
524
+
525
+ if use_transaction is None:
526
+ config = self.context.config if self.context else None
527
+ use_transaction = self.should_use_transaction(migration, config) if config else False
528
+
529
+ runtime = self.runtime
530
+ span = None
531
+ if runtime is not None:
532
+ version = cast("str | None", migration.get("version"))
533
+ span = runtime.start_migration_span("upgrade", version=version)
534
+ runtime.increment_metric("migrations.upgrade.invocations")
535
+ log_with_context(
536
+ logger,
537
+ logging.INFO,
538
+ "migration.apply",
539
+ db_system=resolve_db_system(type(driver).__name__),
540
+ version=migration.get("version"),
541
+ use_transaction=use_transaction,
542
+ status="start",
543
+ )
544
+
545
+ start_time = time.perf_counter()
546
+ execution_time = 0
547
+
548
+ try:
549
+ if use_transaction:
550
+ driver.begin()
551
+ for sql_statement in upgrade_sql_list:
552
+ if sql_statement.strip():
553
+ driver.execute_script(sql_statement)
554
+ execution_time = int((time.perf_counter() - start_time) * 1000)
555
+ if on_success:
556
+ on_success(execution_time)
557
+ driver.commit()
558
+ else:
559
+ for sql_statement in upgrade_sql_list:
560
+ if sql_statement.strip():
561
+ driver.execute_script(sql_statement)
562
+ execution_time = int((time.perf_counter() - start_time) * 1000)
563
+ if on_success:
564
+ on_success(execution_time)
565
+ except Exception as exc:
566
+ if use_transaction:
567
+ driver.rollback()
568
+ if runtime is not None:
569
+ duration_ms = int((time.perf_counter() - start_time) * 1000)
570
+ runtime.increment_metric("migrations.upgrade.errors")
571
+ runtime.end_migration_span(span, duration_ms=duration_ms, error=exc)
572
+ log_with_context(
573
+ logger,
574
+ logging.ERROR,
575
+ "migration.apply",
576
+ db_system=resolve_db_system(type(driver).__name__),
577
+ version=migration.get("version"),
578
+ duration_ms=int((time.perf_counter() - start_time) * 1000),
579
+ error_type=type(exc).__name__,
580
+ status="failed",
581
+ )
582
+ raise
583
+
584
+ if runtime is not None:
585
+ runtime.increment_metric("migrations.upgrade.applied")
586
+ runtime.increment_metric("migrations.upgrade.duration_ms", float(execution_time))
587
+ runtime.end_migration_span(span, duration_ms=execution_time)
588
+ log_with_context(
589
+ logger,
590
+ logging.INFO,
591
+ "migration.apply",
592
+ db_system=resolve_db_system(type(driver).__name__),
593
+ version=migration.get("version"),
594
+ duration_ms=execution_time,
595
+ status="complete",
596
+ )
597
+
598
+ return None, execution_time
599
+
600
+ def execute_downgrade(
601
+ self,
602
+ driver: "SyncDriverAdapterBase",
603
+ migration: "dict[str, Any]",
604
+ *,
605
+ use_transaction: "bool | None" = None,
606
+ on_success: "Callable[[int], None] | None" = None,
607
+ ) -> "tuple[str | None, int]":
608
+ """Execute a downgrade migration.
609
+
610
+ Args:
611
+ driver: The sync database driver to use.
612
+ migration: Migration metadata dictionary.
613
+ use_transaction: Override transaction behavior. If None, uses should_use_transaction logic.
614
+ on_success: Callback invoked with execution_time_ms before commit (for version tracking).
615
+
616
+ Returns:
617
+ Tuple of (sql_content, execution_time_ms).
618
+ """
619
+ downgrade_sql_list = self._get_migration_sql(migration, "down")
620
+ if downgrade_sql_list is None:
621
+ self._metric("migrations.downgrade.skipped")
622
+ log_with_context(
623
+ logger,
624
+ logging.WARNING,
625
+ "migration.rollback",
626
+ db_system=resolve_db_system(type(driver).__name__),
627
+ version=migration.get("version"),
628
+ status="missing",
629
+ )
630
+ return None, 0
631
+
632
+ if use_transaction is None:
633
+ config = self.context.config if self.context else None
634
+ use_transaction = self.should_use_transaction(migration, config) if config else False
635
+
636
+ runtime = self.runtime
637
+ span = None
638
+ if runtime is not None:
639
+ version = cast("str | None", migration.get("version"))
640
+ span = runtime.start_migration_span("downgrade", version=version)
641
+ runtime.increment_metric("migrations.downgrade.invocations")
642
+ log_with_context(
643
+ logger,
644
+ logging.INFO,
645
+ "migration.rollback",
646
+ db_system=resolve_db_system(type(driver).__name__),
647
+ version=migration.get("version"),
648
+ use_transaction=use_transaction,
649
+ status="start",
650
+ )
651
+
652
+ start_time = time.perf_counter()
653
+ execution_time = 0
654
+
655
+ try:
656
+ if use_transaction:
657
+ driver.begin()
658
+ for sql_statement in downgrade_sql_list:
659
+ if sql_statement.strip():
660
+ driver.execute_script(sql_statement)
661
+ execution_time = int((time.perf_counter() - start_time) * 1000)
662
+ if on_success:
663
+ on_success(execution_time)
664
+ driver.commit()
665
+ else:
666
+ for sql_statement in downgrade_sql_list:
667
+ if sql_statement.strip():
668
+ driver.execute_script(sql_statement)
669
+ execution_time = int((time.perf_counter() - start_time) * 1000)
670
+ if on_success:
671
+ on_success(execution_time)
672
+ except Exception as exc:
673
+ if use_transaction:
674
+ driver.rollback()
675
+ if runtime is not None:
676
+ duration_ms = int((time.perf_counter() - start_time) * 1000)
677
+ runtime.increment_metric("migrations.downgrade.errors")
678
+ runtime.end_migration_span(span, duration_ms=duration_ms, error=exc)
679
+ log_with_context(
680
+ logger,
681
+ logging.ERROR,
682
+ "migration.rollback",
683
+ db_system=resolve_db_system(type(driver).__name__),
684
+ version=migration.get("version"),
685
+ duration_ms=int((time.perf_counter() - start_time) * 1000),
686
+ error_type=type(exc).__name__,
687
+ status="failed",
688
+ )
689
+ raise
690
+
691
+ if runtime is not None:
692
+ runtime.increment_metric("migrations.downgrade.applied")
693
+ runtime.increment_metric("migrations.downgrade.duration_ms", float(execution_time))
694
+ runtime.end_migration_span(span, duration_ms=execution_time)
695
+ log_with_context(
696
+ logger,
697
+ logging.INFO,
698
+ "migration.rollback",
699
+ db_system=resolve_db_system(type(driver).__name__),
700
+ version=migration.get("version"),
701
+ duration_ms=execution_time,
702
+ status="complete",
703
+ )
704
+
705
+ return None, execution_time
706
+
707
+ def _get_migration_sql(self, migration: "dict[str, Any]", direction: str) -> "list[str] | None":
708
+ """Get migration SQL for given direction (sync version).
709
+
710
+ Args:
711
+ migration: Migration metadata.
712
+ direction: Either 'up' or 'down'.
713
+
714
+ Returns:
715
+ SQL statements for the migration.
716
+ """
717
+ # If this is being called during migration loading (no has_*grade field yet),
718
+ # don't raise/warn - just proceed to check if the method exists
719
+ if f"has_{direction}grade" in migration and not migration.get(f"has_{direction}grade"):
720
+ if direction == "down":
721
+ logger.warning("Migration %s has no downgrade query", migration.get("version"))
722
+ return None
723
+ msg = f"Migration {migration.get('version')} has no upgrade query"
724
+ raise ValueError(msg)
725
+
726
+ file_path, loader = migration["file_path"], migration["loader"]
727
+
728
+ try:
729
+ method = loader.get_up_sql if direction == "up" else loader.get_down_sql
730
+ sql_statements = (
731
+ await_(method, raise_sync_error=False)(file_path)
732
+ if inspect.iscoroutinefunction(method)
733
+ else method(file_path)
734
+ )
735
+
736
+ except Exception as e:
737
+ if direction == "down":
738
+ logger.warning("Failed to load downgrade for migration %s: %s", migration.get("version"), e)
739
+ return None
740
+ msg = f"Failed to load upgrade for migration {migration.get('version')}: {e}"
741
+ raise ValueError(msg) from e
742
+ else:
743
+ if sql_statements:
744
+ return cast("list[str]", sql_statements)
745
+ return None
746
+
747
+ def load_all_migrations(self) -> "dict[str, SQL]":
748
+ """Load all migrations into a single namespace for bulk operations.
749
+
750
+ Returns:
751
+ Dictionary mapping query names to SQL objects.
752
+ """
753
+ all_queries = {}
754
+ migrations = self.get_migration_files()
755
+
756
+ for version, file_path in migrations:
757
+ if file_path.suffix == ".sql":
758
+ self.loader.load_sql(file_path)
759
+ for query_name in self.loader.list_queries():
760
+ all_queries[query_name] = self.loader.get_sql(query_name)
761
+ else:
762
+ loader = get_migration_loader(
763
+ file_path, self.migrations_path, self.project_root, self.context, self.loader
764
+ )
765
+
766
+ try:
767
+ up_sql = await_(loader.get_up_sql, raise_sync_error=False)(file_path)
768
+ down_sql = await_(loader.get_down_sql, raise_sync_error=False)(file_path)
769
+
770
+ if up_sql:
771
+ all_queries[f"migrate-{version}-up"] = SQL(up_sql[0])
772
+ if down_sql:
773
+ all_queries[f"migrate-{version}-down"] = SQL(down_sql[0])
774
+
775
+ except Exception as e:
776
+ logger.debug("Failed to load Python migration %s: %s", file_path, e)
777
+
778
+ return all_queries
779
+
780
+
781
+ class AsyncMigrationRunner(BaseMigrationRunner):
782
+ """Asynchronous migration runner with pure async methods."""
783
+
784
+ async def get_migration_files(self) -> "list[tuple[str, Path]]":
785
+ """Get all migration files sorted by version.
786
+
787
+ Returns:
788
+ List of (version, path) tuples sorted by version.
789
+ """
790
+ return await async_(self._load_migration_listing)()
791
+
792
+ async def load_migration(self, file_path: Path, version: "str | None" = None) -> "dict[str, Any]":
793
+ """Load a migration file and extract its components.
794
+
795
+ Args:
796
+ file_path: Path to the migration file.
797
+ version: Optional pre-extracted version (preserves prefixes like ext_adk_0001).
798
+
799
+ Returns:
800
+ Dictionary containing migration metadata and queries.
801
+ """
802
+ metadata = self._load_migration_metadata_common(file_path, version)
803
+ context_to_use = self._get_context_for_migration(file_path)
804
+
805
+ loader = get_migration_loader(file_path, self.migrations_path, self.project_root, context_to_use, self.loader)
806
+ loader.validate_migration_file(file_path)
807
+
808
+ has_upgrade, has_downgrade = True, False
809
+
810
+ if file_path.suffix == ".sql":
811
+ version = metadata["version"]
812
+ up_query, down_query = f"migrate-{version}-up", f"migrate-{version}-down"
813
+ has_upgrade, has_downgrade = self.loader.has_query(up_query), self.loader.has_query(down_query)
814
+ else:
815
+ try:
816
+ has_downgrade = bool(
817
+ await self._get_migration_sql_async({"loader": loader, "file_path": file_path}, "down")
818
+ )
819
+ except Exception:
820
+ has_downgrade = False
821
+
822
+ metadata.update({"has_upgrade": has_upgrade, "has_downgrade": has_downgrade, "loader": loader})
823
+ return metadata
824
+
825
+ async def execute_upgrade(
826
+ self,
827
+ driver: "AsyncDriverAdapterBase",
828
+ migration: "dict[str, Any]",
829
+ *,
830
+ use_transaction: "bool | None" = None,
831
+ on_success: "Callable[[int], Awaitable[None]] | None" = None,
832
+ ) -> "tuple[str | None, int]":
833
+ """Execute an upgrade migration.
834
+
835
+ Args:
836
+ driver: The async database driver to use.
837
+ migration: Migration metadata dictionary.
838
+ use_transaction: Override transaction behavior. If None, uses should_use_transaction logic.
839
+ on_success: Async callback invoked with execution_time_ms before commit (for version tracking).
840
+
841
+ Returns:
842
+ Tuple of (sql_content, execution_time_ms).
843
+ """
844
+ upgrade_sql_list = await self._get_migration_sql_async(migration, "up")
845
+ if upgrade_sql_list is None:
846
+ self._metric("migrations.upgrade.skipped")
847
+ log_with_context(
848
+ logger,
849
+ logging.WARNING,
850
+ "migration.apply",
851
+ db_system=resolve_db_system(type(driver).__name__),
852
+ version=migration.get("version"),
853
+ status="missing",
854
+ )
855
+ return None, 0
856
+
857
+ if use_transaction is None:
858
+ config = self.context.config if self.context else None
859
+ use_transaction = self.should_use_transaction(migration, config) if config else False
860
+
861
+ runtime = self.runtime
862
+ span = None
863
+ if runtime is not None:
864
+ version = cast("str | None", migration.get("version"))
865
+ span = runtime.start_migration_span("upgrade", version=version)
866
+ runtime.increment_metric("migrations.upgrade.invocations")
867
+ log_with_context(
868
+ logger,
869
+ logging.INFO,
870
+ "migration.apply",
871
+ db_system=resolve_db_system(type(driver).__name__),
872
+ version=migration.get("version"),
873
+ use_transaction=use_transaction,
874
+ status="start",
875
+ )
876
+
877
+ start_time = time.perf_counter()
878
+ execution_time = 0
879
+
880
+ try:
881
+ if use_transaction:
882
+ await driver.begin()
883
+ for sql_statement in upgrade_sql_list:
884
+ if sql_statement.strip():
885
+ await driver.execute_script(sql_statement)
886
+ execution_time = int((time.perf_counter() - start_time) * 1000)
887
+ if on_success:
888
+ await on_success(execution_time)
889
+ await driver.commit()
890
+ else:
891
+ for sql_statement in upgrade_sql_list:
892
+ if sql_statement.strip():
893
+ await driver.execute_script(sql_statement)
894
+ execution_time = int((time.perf_counter() - start_time) * 1000)
895
+ if on_success:
896
+ await on_success(execution_time)
897
+ except Exception as exc:
898
+ if use_transaction:
899
+ await driver.rollback()
900
+ if runtime is not None:
901
+ duration_ms = int((time.perf_counter() - start_time) * 1000)
902
+ runtime.increment_metric("migrations.upgrade.errors")
903
+ runtime.end_migration_span(span, duration_ms=duration_ms, error=exc)
904
+ log_with_context(
905
+ logger,
906
+ logging.ERROR,
907
+ "migration.apply",
908
+ db_system=resolve_db_system(type(driver).__name__),
909
+ version=migration.get("version"),
910
+ duration_ms=int((time.perf_counter() - start_time) * 1000),
911
+ error_type=type(exc).__name__,
912
+ status="failed",
913
+ )
914
+ raise
915
+
916
+ if runtime is not None:
917
+ runtime.increment_metric("migrations.upgrade.applied")
918
+ runtime.increment_metric("migrations.upgrade.duration_ms", float(execution_time))
919
+ runtime.end_migration_span(span, duration_ms=execution_time)
920
+ log_with_context(
921
+ logger,
922
+ logging.INFO,
923
+ "migration.apply",
924
+ db_system=resolve_db_system(type(driver).__name__),
925
+ version=migration.get("version"),
926
+ duration_ms=execution_time,
927
+ status="complete",
928
+ )
929
+
930
+ return None, execution_time
931
+
932
+ async def execute_downgrade(
933
+ self,
934
+ driver: "AsyncDriverAdapterBase",
935
+ migration: "dict[str, Any]",
936
+ *,
937
+ use_transaction: "bool | None" = None,
938
+ on_success: "Callable[[int], Awaitable[None]] | None" = None,
939
+ ) -> "tuple[str | None, int]":
940
+ """Execute a downgrade migration.
941
+
942
+ Args:
943
+ driver: The async database driver to use.
944
+ migration: Migration metadata dictionary.
945
+ use_transaction: Override transaction behavior. If None, uses should_use_transaction logic.
946
+ on_success: Async callback invoked with execution_time_ms before commit (for version tracking).
947
+
948
+ Returns:
949
+ Tuple of (sql_content, execution_time_ms).
950
+ """
951
+ downgrade_sql_list = await self._get_migration_sql_async(migration, "down")
952
+ if downgrade_sql_list is None:
953
+ self._metric("migrations.downgrade.skipped")
954
+ log_with_context(
955
+ logger,
956
+ logging.WARNING,
957
+ "migration.rollback",
958
+ db_system=resolve_db_system(type(driver).__name__),
959
+ version=migration.get("version"),
960
+ status="missing",
961
+ )
962
+ return None, 0
963
+
964
+ if use_transaction is None:
965
+ config = self.context.config if self.context else None
966
+ use_transaction = self.should_use_transaction(migration, config) if config else False
967
+
968
+ runtime = self.runtime
969
+ span = None
970
+ if runtime is not None:
971
+ version = cast("str | None", migration.get("version"))
972
+ span = runtime.start_migration_span("downgrade", version=version)
973
+ runtime.increment_metric("migrations.downgrade.invocations")
974
+ log_with_context(
975
+ logger,
976
+ logging.INFO,
977
+ "migration.rollback",
978
+ db_system=resolve_db_system(type(driver).__name__),
979
+ version=migration.get("version"),
980
+ use_transaction=use_transaction,
981
+ status="start",
982
+ )
983
+
984
+ start_time = time.perf_counter()
985
+ execution_time = 0
986
+
987
+ try:
988
+ if use_transaction:
989
+ await driver.begin()
990
+ for sql_statement in downgrade_sql_list:
991
+ if sql_statement.strip():
992
+ await driver.execute_script(sql_statement)
993
+ execution_time = int((time.perf_counter() - start_time) * 1000)
994
+ if on_success:
995
+ await on_success(execution_time)
996
+ await driver.commit()
997
+ else:
998
+ for sql_statement in downgrade_sql_list:
999
+ if sql_statement.strip():
1000
+ await driver.execute_script(sql_statement)
1001
+ execution_time = int((time.perf_counter() - start_time) * 1000)
1002
+ if on_success:
1003
+ await on_success(execution_time)
1004
+ except Exception as exc:
1005
+ if use_transaction:
1006
+ await driver.rollback()
1007
+ if runtime is not None:
1008
+ duration_ms = int((time.perf_counter() - start_time) * 1000)
1009
+ runtime.increment_metric("migrations.downgrade.errors")
1010
+ runtime.end_migration_span(span, duration_ms=duration_ms, error=exc)
1011
+ log_with_context(
1012
+ logger,
1013
+ logging.ERROR,
1014
+ "migration.rollback",
1015
+ db_system=resolve_db_system(type(driver).__name__),
1016
+ version=migration.get("version"),
1017
+ duration_ms=int((time.perf_counter() - start_time) * 1000),
1018
+ error_type=type(exc).__name__,
1019
+ status="failed",
1020
+ )
1021
+ raise
1022
+
1023
+ if runtime is not None:
1024
+ runtime.increment_metric("migrations.downgrade.applied")
1025
+ runtime.increment_metric("migrations.downgrade.duration_ms", float(execution_time))
1026
+ runtime.end_migration_span(span, duration_ms=execution_time)
1027
+ log_with_context(
1028
+ logger,
1029
+ logging.INFO,
1030
+ "migration.rollback",
1031
+ db_system=resolve_db_system(type(driver).__name__),
1032
+ version=migration.get("version"),
1033
+ duration_ms=execution_time,
1034
+ status="complete",
1035
+ )
1036
+
1037
+ return None, execution_time
1038
+
1039
+ async def _get_migration_sql_async(self, migration: "dict[str, Any]", direction: str) -> "list[str] | None":
1040
+ """Get migration SQL for given direction (async version).
1041
+
1042
+ Args:
1043
+ migration: Migration metadata.
1044
+ direction: Either 'up' or 'down'.
1045
+
1046
+ Returns:
1047
+ SQL statements for the migration.
1048
+ """
1049
+ # If this is being called during migration loading (no has_*grade field yet),
1050
+ # don't raise/warn - just proceed to check if the method exists
1051
+ if f"has_{direction}grade" in migration and not migration.get(f"has_{direction}grade"):
1052
+ if direction == "down":
1053
+ logger.warning("Migration %s has no downgrade query", migration.get("version"))
1054
+ return None
1055
+ msg = f"Migration {migration.get('version')} has no upgrade query"
1056
+ raise ValueError(msg)
1057
+
1058
+ file_path, loader = migration["file_path"], migration["loader"]
1059
+
1060
+ try:
1061
+ method = loader.get_up_sql if direction == "up" else loader.get_down_sql
1062
+ sql_statements = await method(file_path)
1063
+
1064
+ except Exception as e:
1065
+ if direction == "down":
1066
+ logger.warning("Failed to load downgrade for migration %s: %s", migration.get("version"), e)
1067
+ return None
1068
+ msg = f"Failed to load upgrade for migration {migration.get('version')}: {e}"
1069
+ raise ValueError(msg) from e
1070
+ else:
1071
+ if sql_statements:
1072
+ return cast("list[str]", sql_statements)
1073
+ return None
1074
+
1075
+ async def load_all_migrations(self) -> "dict[str, SQL]":
1076
+ """Load all migrations into a single namespace for bulk operations.
1077
+
1078
+ Returns:
1079
+ Dictionary mapping query names to SQL objects.
1080
+ """
1081
+ all_queries = {}
1082
+ migrations = await self.get_migration_files()
1083
+
1084
+ for version, file_path in migrations:
1085
+ if file_path.suffix == ".sql":
1086
+ await async_(self.loader.load_sql)(file_path)
1087
+ for query_name in self.loader.list_queries():
1088
+ all_queries[query_name] = self.loader.get_sql(query_name)
1089
+ else:
1090
+ loader = get_migration_loader(
1091
+ file_path, self.migrations_path, self.project_root, self.context, self.loader
1092
+ )
1093
+
1094
+ try:
1095
+ up_sql = await loader.get_up_sql(file_path)
1096
+ down_sql = await loader.get_down_sql(file_path)
1097
+
1098
+ if up_sql:
1099
+ all_queries[f"migrate-{version}-up"] = SQL(up_sql[0])
1100
+ if down_sql:
1101
+ all_queries[f"migrate-{version}-down"] = SQL(down_sql[0])
1102
+
1103
+ except Exception as e:
1104
+ logger.debug("Failed to load Python migration %s: %s", file_path, e)
1105
+
1106
+ return all_queries
1107
+
1108
+
1109
+ @overload
1110
+ def create_migration_runner(
1111
+ migrations_path: Path,
1112
+ extension_migrations: "dict[str, Path]",
1113
+ context: "MigrationContext | None",
1114
+ extension_configs: "dict[str, Any]",
1115
+ is_async: "Literal[False]" = False,
1116
+ runtime: "ObservabilityRuntime | None" = None,
1117
+ description_hints: "TemplateDescriptionHints | None" = None,
1118
+ ) -> SyncMigrationRunner: ...
1119
+
1120
+
1121
+ @overload
1122
+ def create_migration_runner(
1123
+ migrations_path: Path,
1124
+ extension_migrations: "dict[str, Path]",
1125
+ context: "MigrationContext | None",
1126
+ extension_configs: "dict[str, Any]",
1127
+ is_async: "Literal[True]",
1128
+ runtime: "ObservabilityRuntime | None" = None,
1129
+ description_hints: "TemplateDescriptionHints | None" = None,
1130
+ ) -> AsyncMigrationRunner: ...
1131
+
1132
+
1133
+ def create_migration_runner(
1134
+ migrations_path: Path,
1135
+ extension_migrations: "dict[str, Path]",
1136
+ context: "MigrationContext | None",
1137
+ extension_configs: "dict[str, Any]",
1138
+ is_async: bool = False,
1139
+ runtime: "ObservabilityRuntime | None" = None,
1140
+ description_hints: "TemplateDescriptionHints | None" = None,
1141
+ ) -> "SyncMigrationRunner | AsyncMigrationRunner":
1142
+ """Factory function to create the appropriate migration runner.
1143
+
1144
+ Args:
1145
+ migrations_path: Path to migrations directory.
1146
+ extension_migrations: Extension migration paths.
1147
+ context: Migration context.
1148
+ extension_configs: Extension configurations.
1149
+ is_async: Whether to create async or sync runner.
1150
+ runtime: Observability runtime shared with loaders and execution steps.
1151
+ description_hints: Optional description extraction hints from template profiles.
1152
+
1153
+ Returns:
1154
+ Appropriate migration runner instance.
1155
+ """
1156
+ if is_async:
1157
+ return AsyncMigrationRunner(
1158
+ migrations_path,
1159
+ extension_migrations,
1160
+ context,
1161
+ extension_configs,
1162
+ runtime=runtime,
1163
+ description_hints=description_hints,
1164
+ )
1165
+ return SyncMigrationRunner(
1166
+ migrations_path,
1167
+ extension_migrations,
1168
+ context,
1169
+ extension_configs,
1170
+ runtime=runtime,
1171
+ description_hints=description_hints,
1172
+ )