dblift 2.0.1__py3-none-any.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 (408) hide show
  1. api/__init__.py +12 -0
  2. api/_cli_support.py +27 -0
  3. api/_client_factory.py +387 -0
  4. api/_client_operations.py +258 -0
  5. api/_engine_config.py +70 -0
  6. api/async_client.py +138 -0
  7. api/client.py +974 -0
  8. api/events.py +579 -0
  9. api/migrations.py +5 -0
  10. api/py.typed +0 -0
  11. cli/__init__.py +10 -0
  12. cli/_command_handlers.py +94 -0
  13. cli/_config_helpers.py +599 -0
  14. cli/_constants.py +33 -0
  15. cli/_output.py +179 -0
  16. cli/_parser_setup.py +416 -0
  17. cli/db_utils.py +518 -0
  18. cli/extensions.py +56 -0
  19. cli/handlers/__init__.py +11 -0
  20. cli/handlers/_shared.py +132 -0
  21. cli/handlers/baseline.py +18 -0
  22. cli/handlers/clean.py +17 -0
  23. cli/handlers/import_flyway.py +17 -0
  24. cli/handlers/info.py +147 -0
  25. cli/handlers/migrate.py +46 -0
  26. cli/handlers/repair.py +18 -0
  27. cli/handlers/undo.py +32 -0
  28. cli/handlers/validate.py +29 -0
  29. cli/main.py +442 -0
  30. config/__init__.py +10 -0
  31. config/_credential_masking.py +71 -0
  32. config/_subclasses/__init__.py +12 -0
  33. config/_subclasses/cosmosdb_config.py +94 -0
  34. config/_subclasses/db2_config.py +56 -0
  35. config/_subclasses/dummy_config.py +19 -0
  36. config/_subclasses/mysql_config.py +91 -0
  37. config/_subclasses/oracle_config.py +48 -0
  38. config/_subclasses/postgresql_config.py +56 -0
  39. config/_subclasses/sqlite_config.py +88 -0
  40. config/_subclasses/sqlserver_config.py +116 -0
  41. config/_url_builder_mixin.py +119 -0
  42. config/config_builder.py +335 -0
  43. config/database_config.py +615 -0
  44. config/dblift_config.py +1139 -0
  45. config/errors.py +5 -0
  46. config/secrets/__init__.py +16 -0
  47. config/secrets/_cache.py +37 -0
  48. config/secrets/_provider_base.py +27 -0
  49. config/secrets/_registry.py +117 -0
  50. config/secrets/_resolver.py +83 -0
  51. config/secrets/_secrets_config.py +60 -0
  52. config/validation_config.py +124 -0
  53. core/__init__.py +59 -0
  54. core/constants.py +72 -0
  55. core/dialect_boundary.py +275 -0
  56. core/exceptions.py +55 -0
  57. core/features.py +28 -0
  58. core/introspection/__init__.py +23 -0
  59. core/introspection/_column_enricher.py +279 -0
  60. core/introspection/_partition_enricher.py +184 -0
  61. core/introspection/_schema_orchestrator.py +241 -0
  62. core/introspection/_utils.py +22 -0
  63. core/introspection/_vendor_property_applier.py +70 -0
  64. core/introspection/base_introspector.py +889 -0
  65. core/introspection/capability_matrix.py +276 -0
  66. core/introspection/extractors/__init__.py +25 -0
  67. core/introspection/extractors/base_extractor.py +142 -0
  68. core/introspection/extractors/column_extractor.py +230 -0
  69. core/introspection/extractors/constraint_extractor.py +598 -0
  70. core/introspection/extractors/index_extractor.py +344 -0
  71. core/introspection/extractors/misc_extractor.py +730 -0
  72. core/introspection/extractors/procedure_extractor.py +821 -0
  73. core/introspection/extractors/sequence_extractor.py +138 -0
  74. core/introspection/extractors/table_extractor.py +467 -0
  75. core/introspection/extractors/trigger_extractor.py +170 -0
  76. core/introspection/extractors/view_extractor.py +323 -0
  77. core/introspection/introspector_factory.py +136 -0
  78. core/introspection/result.py +204 -0
  79. core/introspection/schema_introspector.py +16 -0
  80. core/introspection/vendor_queries_base.py +924 -0
  81. core/introspection/vendor_queries_factory.py +153 -0
  82. core/introspection/vendor_queries_protocols.py +225 -0
  83. core/introspection/version_detector.py +279 -0
  84. core/logger/__init__.py +448 -0
  85. core/logger/_base.py +145 -0
  86. core/logger/_factory.py +239 -0
  87. core/logger/_formatters.py +120 -0
  88. core/logger/_levels.py +112 -0
  89. core/logger/_multi.py +135 -0
  90. core/logger/_null.py +66 -0
  91. core/logger/console.py +195 -0
  92. core/logger/formatters/__init__.py +30 -0
  93. core/logger/formatters/_format_diff_meta.py +117 -0
  94. core/logger/formatters/_format_diff_object.py +195 -0
  95. core/logger/formatters/_format_diff_routine.py +208 -0
  96. core/logger/formatters/_format_diff_table.py +250 -0
  97. core/logger/formatters/_formatter_impl.py +567 -0
  98. core/logger/formatters/diff_utils.py +396 -0
  99. core/logger/formatters/factory.py +104 -0
  100. core/logger/formatters/formatter.py +12 -0
  101. core/logger/formatters/htmlformatter.py +876 -0
  102. core/logger/formatters/jsonformatter.py +978 -0
  103. core/logger/log.py +782 -0
  104. core/logger/results.py +904 -0
  105. core/migration/__init__.py +33 -0
  106. core/migration/_type_match.py +111 -0
  107. core/migration/clean_summary.py +86 -0
  108. core/migration/commands/__init__.py +6 -0
  109. core/migration/commands/_script_events.py +18 -0
  110. core/migration/commands/base_command.py +1025 -0
  111. core/migration/commands/baseline_command.py +96 -0
  112. core/migration/commands/clean_command.py +342 -0
  113. core/migration/commands/import_flyway_command.py +190 -0
  114. core/migration/commands/info_command.py +208 -0
  115. core/migration/commands/migrate_command.py +853 -0
  116. core/migration/commands/repair_command.py +769 -0
  117. core/migration/commands/undo_command.py +869 -0
  118. core/migration/commands/validate_command.py +90 -0
  119. core/migration/encoding.py +90 -0
  120. core/migration/executor/__init__.py +12 -0
  121. core/migration/executor/execution_engine.py +1196 -0
  122. core/migration/executor/migration_executor.py +439 -0
  123. core/migration/executor/migration_helpers.py +142 -0
  124. core/migration/executor/placeholder_manager.py +77 -0
  125. core/migration/executor/transaction_policy.py +63 -0
  126. core/migration/executors/__init__.py +20 -0
  127. core/migration/executors/base_executor.py +179 -0
  128. core/migration/executors/executor_factory.py +251 -0
  129. core/migration/executors/python_executor.py +379 -0
  130. core/migration/executors/sql_executor.py +273 -0
  131. core/migration/formats/__init__.py +14 -0
  132. core/migration/formats/format_detector.py +204 -0
  133. core/migration/formats/migration_format.py +67 -0
  134. core/migration/history/__init__.py +1 -0
  135. core/migration/history/migration_history_manager.py +384 -0
  136. core/migration/journals/__init__.py +14 -0
  137. core/migration/journals/migration_journal.py +618 -0
  138. core/migration/migration.py +775 -0
  139. core/migration/placeholders/__init__.py +1 -0
  140. core/migration/placeholders/placeholder_service.py +84 -0
  141. core/migration/rules/__init__.py +1 -0
  142. core/migration/rules/migration_rules.py +145 -0
  143. core/migration/scripting/__init__.py +1 -0
  144. core/migration/scripting/migration_script_manager.py +721 -0
  145. core/migration/scripting/undo_script_generator/__init__.py +24 -0
  146. core/migration/scripting/undo_script_generator/_ddl_reversers.py +485 -0
  147. core/migration/scripting/undo_script_generator/_dml_reversers.py +319 -0
  148. core/migration/scripting/undo_script_generator/_extractors.py +511 -0
  149. core/migration/scripting/undo_script_generator/_generator.py +310 -0
  150. core/migration/scripting/undo_script_generator/_helpers.py +532 -0
  151. core/migration/scripting/undo_script_generator/_models.py +18 -0
  152. core/migration/scripting/undo_script_generator/_reversers.py +864 -0
  153. core/migration/sql/__init__.py +1 -0
  154. core/migration/sql/execution_statement.py +38 -0
  155. core/migration/sql/sql_analyzer.py +806 -0
  156. core/migration/sql/sql_execution_service.py +431 -0
  157. core/migration/sql/statement_splitter.py +74 -0
  158. core/migration/state/__init__.py +5 -0
  159. core/migration/state/migration_data_service.py +603 -0
  160. core/migration/state/migration_display_state.py +36 -0
  161. core/migration/state/migration_formatter.py +215 -0
  162. core/migration/state/migration_state.py +166 -0
  163. core/migration/state/migration_state_manager.py +830 -0
  164. core/migration/state/migration_state_service.py +244 -0
  165. core/migration/ui/__init__.py +15 -0
  166. core/migration/ui/data_collector.py +926 -0
  167. core/migration/ui/display_formatters.py +251 -0
  168. core/migration/ui/migration_analyzer.py +259 -0
  169. core/migration/ui/migration_ui.py +362 -0
  170. core/migration/ui/table_renderer.py +250 -0
  171. core/migration/version_utils.py +94 -0
  172. core/normalization/__init__.py +35 -0
  173. core/normalization/data_type_normalizer.py +5 -0
  174. core/normalization/dependency_resolver.py +435 -0
  175. core/normalization/identifier_normalizer.py +297 -0
  176. core/normalization/object_orderer.py +230 -0
  177. core/normalization/type_constants.py +154 -0
  178. core/normalization/type_mapper.py +220 -0
  179. core/normalization/type_mappings.py +89 -0
  180. core/normalization/type_normalizer.py +245 -0
  181. core/seams/__init__.py +1 -0
  182. core/seams/event_listeners.py +26 -0
  183. core/seams/introspection.py +18 -0
  184. core/sql_model/__init__.py +60 -0
  185. core/sql_model/_base_parse_result.py +491 -0
  186. core/sql_model/_base_sql_column.py +194 -0
  187. core/sql_model/_base_sql_constraint.py +223 -0
  188. core/sql_model/_base_sql_object.py +233 -0
  189. core/sql_model/_base_sql_statement.py +100 -0
  190. core/sql_model/base.py +43 -0
  191. core/sql_model/constraint_validator.py +469 -0
  192. core/sql_model/database_link.py +151 -0
  193. core/sql_model/dialect.py +482 -0
  194. core/sql_model/event.py +155 -0
  195. core/sql_model/extension.py +109 -0
  196. core/sql_model/foreign_data_wrapper.py +113 -0
  197. core/sql_model/foreign_server.py +138 -0
  198. core/sql_model/index.py +279 -0
  199. core/sql_model/linked_server.py +170 -0
  200. core/sql_model/module.py +112 -0
  201. core/sql_model/package.py +134 -0
  202. core/sql_model/partition.py +180 -0
  203. core/sql_model/procedure.py +321 -0
  204. core/sql_model/sequence.py +208 -0
  205. core/sql_model/synonym.py +143 -0
  206. core/sql_model/table.py +1106 -0
  207. core/sql_model/table_canonicalizer.py +45 -0
  208. core/sql_model/table_options.py +73 -0
  209. core/sql_model/trigger.py +310 -0
  210. core/sql_model/user_defined_type.py +151 -0
  211. core/sql_model/view.py +429 -0
  212. core/sql_model/view_options.py +95 -0
  213. core/sql_parser/__init__.py +5 -0
  214. core/sql_parser/_partition_handler.py +113 -0
  215. core/sql_parser/_sqlglot_builders.py +634 -0
  216. core/sql_parser/base_statement_parser.py +356 -0
  217. core/sql_parser/base_tokenizer.py +473 -0
  218. core/sql_parser/common/__init__.py +1 -0
  219. core/sql_parser/common/base_parser.py +598 -0
  220. core/sql_parser/dialects/__init__.py +5 -0
  221. core/sql_parser/dialects/base_config.py +125 -0
  222. core/sql_parser/enhanced_regex_parser.py +453 -0
  223. core/sql_parser/hybrid_parser.py +1106 -0
  224. core/sql_parser/parser_context.py +89 -0
  225. core/sql_parser/parser_factory.py +215 -0
  226. core/sql_parser/parser_interface.py +93 -0
  227. core/sql_parser/sqlglot_parser.py +544 -0
  228. core/sql_parser/tokens.py +48 -0
  229. core/sql_parser/unified_regex_parser.py +644 -0
  230. core/sql_validator/__init__.py +5 -0
  231. core/sql_validator/_checksum_validator.py +341 -0
  232. core/sql_validator/_flyway_compatibility.py +301 -0
  233. core/sql_validator/_migration_filter.py +221 -0
  234. core/sql_validator/_sql_syntax_validator.py +168 -0
  235. core/sql_validator/_strict_mode_validator.py +136 -0
  236. core/sql_validator/migration_validator.py +923 -0
  237. core/state/sql_script_formatter.py +144 -0
  238. core/state/sql_statement.py +41 -0
  239. core/utils/__init__.py +1 -0
  240. core/utils/database_url_parser.py +104 -0
  241. core/utils/metadata_helpers.py +83 -0
  242. core/utils/row_access.py +192 -0
  243. core/utils/string_utils.py +21 -0
  244. core/utils/url_masking.py +24 -0
  245. db/__init__.py +21 -0
  246. db/base_provider.py +266 -0
  247. db/base_quirks.py +1620 -0
  248. db/constants.py +5 -0
  249. db/error.py +344 -0
  250. db/error_handler.py +349 -0
  251. db/exceptions.py +11 -0
  252. db/native_connection_manager.py +70 -0
  253. db/object_naming.py +39 -0
  254. db/plugins/__init__.py +9 -0
  255. db/plugins/base_history_manager.py +616 -0
  256. db/plugins/base_locking_manager.py +154 -0
  257. db/plugins/base_query_executor.py +253 -0
  258. db/plugins/base_schema_operations.py +288 -0
  259. db/plugins/base_snapshot_manager.py +172 -0
  260. db/plugins/base_undo_manager.py +126 -0
  261. db/plugins/cosmosdb/__init__.py +12 -0
  262. db/plugins/cosmosdb/cosmosdb/__init__.py +19 -0
  263. db/plugins/cosmosdb/cosmosdb/_sdk.py +28 -0
  264. db/plugins/cosmosdb/cosmosdb/connection_manager.py +247 -0
  265. db/plugins/cosmosdb/cosmosdb/history_manager.py +427 -0
  266. db/plugins/cosmosdb/cosmosdb/locking_manager.py +465 -0
  267. db/plugins/cosmosdb/cosmosdb/query_executor.py +1518 -0
  268. db/plugins/cosmosdb/cosmosdb/schema_operations.py +416 -0
  269. db/plugins/cosmosdb/introspection/__init__.py +3 -0
  270. db/plugins/cosmosdb/parser/__init__.py +5 -0
  271. db/plugins/cosmosdb/parser/cosmosdb_regex_parser.py +64 -0
  272. db/plugins/cosmosdb/plugin.py +19 -0
  273. db/plugins/cosmosdb/provider.py +364 -0
  274. db/plugins/cosmosdb/quirks.py +240 -0
  275. db/plugins/cosmosdb/sdk_translator/__init__.py +39 -0
  276. db/plugins/cosmosdb/sdk_translator/_executor.py +422 -0
  277. db/plugins/cosmosdb/sdk_translator/_executors.py +356 -0
  278. db/plugins/cosmosdb/sdk_translator/_parsing.py +53 -0
  279. db/plugins/cosmosdb/sdk_translator/_script_generation.py +141 -0
  280. db/plugins/cosmosdb/sdk_translator/_translator.py +129 -0
  281. db/plugins/cosmosdb/sdk_translator/_translators.py +795 -0
  282. db/plugins/db2/__init__.py +11 -0
  283. db/plugins/db2/db2/__init__.py +11 -0
  284. db/plugins/db2/db2/history_manager.py +317 -0
  285. db/plugins/db2/db2/locking_manager.py +558 -0
  286. db/plugins/db2/db2/schema_operations.py +985 -0
  287. db/plugins/db2/introspection/__init__.py +3 -0
  288. db/plugins/db2/parser/__init__.py +9 -0
  289. db/plugins/db2/parser/db2_regex_parser.py +668 -0
  290. db/plugins/db2/parser/parser_config.py +1147 -0
  291. db/plugins/db2/plugin.py +22 -0
  292. db/plugins/db2/provider.py +420 -0
  293. db/plugins/db2/quirks.py +382 -0
  294. db/plugins/db2/sqlalchemy_url.py +62 -0
  295. db/plugins/mariadb/__init__.py +13 -0
  296. db/plugins/mariadb/plugin.py +21 -0
  297. db/plugins/mariadb/provider.py +40 -0
  298. db/plugins/mariadb/quirks.py +50 -0
  299. db/plugins/mysql/__init__.py +11 -0
  300. db/plugins/mysql/introspection/__init__.py +1 -0
  301. db/plugins/mysql/mysql/__init__.py +11 -0
  302. db/plugins/mysql/mysql/history_manager.py +384 -0
  303. db/plugins/mysql/mysql/locking_manager.py +371 -0
  304. db/plugins/mysql/mysql/schema_operations.py +555 -0
  305. db/plugins/mysql/parser/__init__.py +1 -0
  306. db/plugins/mysql/parser/mysql_regex_parser.py +531 -0
  307. db/plugins/mysql/parser/mysql_statement_parser.py +255 -0
  308. db/plugins/mysql/parser/mysql_tokenizer.py +460 -0
  309. db/plugins/mysql/parser/parser_config.py +670 -0
  310. db/plugins/mysql/plugin.py +22 -0
  311. db/plugins/mysql/provider.py +329 -0
  312. db/plugins/mysql/quirks.py +540 -0
  313. db/plugins/mysql/sqlalchemy_url.py +67 -0
  314. db/plugins/oracle/__init__.py +11 -0
  315. db/plugins/oracle/introspection/__init__.py +3 -0
  316. db/plugins/oracle/introspection/oracle_utils.py +86 -0
  317. db/plugins/oracle/oracle/__init__.py +14 -0
  318. db/plugins/oracle/oracle/dbms_output.py +49 -0
  319. db/plugins/oracle/oracle/history_manager.py +444 -0
  320. db/plugins/oracle/oracle/locking_manager.py +436 -0
  321. db/plugins/oracle/oracle/schema_operations.py +1134 -0
  322. db/plugins/oracle/parser/__init__.py +13 -0
  323. db/plugins/oracle/parser/_comments.py +57 -0
  324. db/plugins/oracle/parser/_object_extractor.py +203 -0
  325. db/plugins/oracle/parser/_plsql_block.py +727 -0
  326. db/plugins/oracle/parser/_sqlplus.py +312 -0
  327. db/plugins/oracle/parser/_statement_splitter.py +233 -0
  328. db/plugins/oracle/parser/oracle_parser.py +297 -0
  329. db/plugins/oracle/parser/oracle_statement_parser.py +434 -0
  330. db/plugins/oracle/parser/oracle_tokenizer.py +251 -0
  331. db/plugins/oracle/parser/parser_config.py +330 -0
  332. db/plugins/oracle/parser/sqlplus_context.py +179 -0
  333. db/plugins/oracle/plugin.py +22 -0
  334. db/plugins/oracle/provider.py +736 -0
  335. db/plugins/oracle/quirks.py +743 -0
  336. db/plugins/oracle/sqlalchemy_url.py +65 -0
  337. db/plugins/postgresql/__init__.py +12 -0
  338. db/plugins/postgresql/_provider_query_executor.py +25 -0
  339. db/plugins/postgresql/introspection/__init__.py +3 -0
  340. db/plugins/postgresql/parser/__init__.py +10 -0
  341. db/plugins/postgresql/parser/parser_config.py +876 -0
  342. db/plugins/postgresql/parser/postgresql_regex_parser.py +691 -0
  343. db/plugins/postgresql/parser/postgresql_statement_parser.py +118 -0
  344. db/plugins/postgresql/parser/postgresql_tokenizer.py +325 -0
  345. db/plugins/postgresql/plugin.py +29 -0
  346. db/plugins/postgresql/postgresql/__init__.py +11 -0
  347. db/plugins/postgresql/postgresql/history_manager.py +231 -0
  348. db/plugins/postgresql/postgresql/locking_manager.py +310 -0
  349. db/plugins/postgresql/postgresql/schema_operations.py +690 -0
  350. db/plugins/postgresql/provider.py +248 -0
  351. db/plugins/postgresql/quirks.py +722 -0
  352. db/plugins/postgresql/sqlalchemy_url.py +65 -0
  353. db/plugins/sqlite/__init__.py +12 -0
  354. db/plugins/sqlite/introspection/__init__.py +3 -0
  355. db/plugins/sqlite/parser/__init__.py +9 -0
  356. db/plugins/sqlite/parser/parser_config.py +377 -0
  357. db/plugins/sqlite/parser/sqlite_regex_parser.py +338 -0
  358. db/plugins/sqlite/plugin.py +21 -0
  359. db/plugins/sqlite/provider.py +504 -0
  360. db/plugins/sqlite/quirks.py +96 -0
  361. db/plugins/sqlite/sqlalchemy_url.py +16 -0
  362. db/plugins/sqlite/sqlite/__init__.py +24 -0
  363. db/plugins/sqlite/sqlite/connection_manager.py +162 -0
  364. db/plugins/sqlite/sqlite/history_manager.py +233 -0
  365. db/plugins/sqlite/sqlite/locking_manager.py +216 -0
  366. db/plugins/sqlite/sqlite/query_executor.py +243 -0
  367. db/plugins/sqlite/sqlite/schema_operations.py +303 -0
  368. db/plugins/sqlserver/__init__.py +11 -0
  369. db/plugins/sqlserver/introspection/__init__.py +3 -0
  370. db/plugins/sqlserver/parser/__init__.py +9 -0
  371. db/plugins/sqlserver/parser/parser_config.py +604 -0
  372. db/plugins/sqlserver/parser/sqlserver_regex_parser.py +368 -0
  373. db/plugins/sqlserver/parser/sqlserver_statement_parser.py +173 -0
  374. db/plugins/sqlserver/parser/sqlserver_tokenizer.py +238 -0
  375. db/plugins/sqlserver/parser/tsql_batch_separator.py +14 -0
  376. db/plugins/sqlserver/plugin.py +22 -0
  377. db/plugins/sqlserver/provider.py +615 -0
  378. db/plugins/sqlserver/quirks.py +489 -0
  379. db/plugins/sqlserver/sqlalchemy_url.py +120 -0
  380. db/plugins/sqlserver/sqlserver/__init__.py +11 -0
  381. db/plugins/sqlserver/sqlserver/history_manager.py +382 -0
  382. db/plugins/sqlserver/sqlserver/locking_manager.py +328 -0
  383. db/plugins/sqlserver/sqlserver/schema_operations.py +524 -0
  384. db/provider_capabilities.py +85 -0
  385. db/provider_interfaces.py +347 -0
  386. db/provider_registry.py +623 -0
  387. db/sqlalchemy_provider.py +340 -0
  388. db/value_utils.py +10 -0
  389. dblift-2.0.1.dist-info/METADATA +1075 -0
  390. dblift-2.0.1.dist-info/RECORD +408 -0
  391. dblift-2.0.1.dist-info/WHEEL +5 -0
  392. dblift-2.0.1.dist-info/entry_points.txt +12 -0
  393. dblift-2.0.1.dist-info/licenses/LICENSE +201 -0
  394. dblift-2.0.1.dist-info/top_level.txt +6 -0
  395. integrations/__init__.py +30 -0
  396. integrations/django/__init__.py +1 -0
  397. integrations/django/_client.py +54 -0
  398. integrations/django/_engine.py +53 -0
  399. integrations/django/apps.py +14 -0
  400. integrations/django/checks.py +35 -0
  401. integrations/django/management/__init__.py +1 -0
  402. integrations/django/management/commands/__init__.py +1 -0
  403. integrations/django/management/commands/dblift_info.py +25 -0
  404. integrations/django/management/commands/dblift_migrate.py +24 -0
  405. integrations/django/management/commands/dblift_validate.py +24 -0
  406. integrations/fastapi.py +153 -0
  407. integrations/flask.py +82 -0
  408. integrations/opentelemetry.py +155 -0
api/__init__.py ADDED
@@ -0,0 +1,12 @@
1
+ """Public API for DBLift library integration (OSS)."""
2
+
3
+ from api.client import DBLiftClient
4
+ from api.events import EventEmitter, EventType
5
+ from api.migrations import MigrationContext
6
+
7
+ __all__ = [
8
+ "DBLiftClient",
9
+ "EventEmitter",
10
+ "EventType",
11
+ "MigrationContext",
12
+ ]
api/_cli_support.py ADDED
@@ -0,0 +1,27 @@
1
+ """Internal re-export shim for ``cli/`` consumers.
2
+
3
+ The ``cli/`` package historically imported a handful of symbols directly
4
+ from ``db.*`` (provider registry, provider internals, etc.). The flake8 rule
5
+ ``banned-modules`` (configured in ``.flake8``) now forbids that pattern:
6
+ ``cli/`` must reach the database layer only through ``api.*``.
7
+
8
+ This module exists solely to centralize those low-level handles in a
9
+ single place under ``api/`` so the architectural rule holds. It is
10
+ deliberately leading-underscored: these symbols are not part of the
11
+ public ``api/`` surface (which is reserved for ``DBLiftClient``, events,
12
+ etc.). Library users should not depend on this module.
13
+
14
+ If you find yourself adding a new re-export here, ask whether the
15
+ underlying need can instead be expressed through ``DBLiftClient`` or a
16
+ new typed entry point in ``api/``.
17
+ """
18
+
19
+ from db.provider_capabilities import get_provider_display_url
20
+ from db.provider_interfaces import ConnectionProvider
21
+ from db.provider_registry import ProviderRegistry
22
+
23
+ __all__ = [
24
+ "ConnectionProvider",
25
+ "ProviderRegistry",
26
+ "get_provider_display_url",
27
+ ]
api/_client_factory.py ADDED
@@ -0,0 +1,387 @@
1
+ """Factory functions for creating DBLiftClient instances.
2
+
3
+ Extracted from api/client.py (story 20-16) to reduce file size.
4
+ Contains the logic behind the from_config, from_config_file, and from_sqlalchemy classmethods.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from copy import deepcopy
10
+ from pathlib import Path
11
+ from typing import Any, Dict, List, Optional, Protocol, Union
12
+
13
+ from api._engine_config import config_from_engine
14
+ from config import DbliftConfig
15
+ from config.config_builder import ConfigBuilder
16
+ from config.errors import ConfigurationError
17
+ from core.logger import DbliftLogger, LogFormat, LogLevel
18
+ from db.native_connection_manager import NativeConnectionManager
19
+ from db.provider_registry import ProviderRegistry
20
+
21
+
22
+ class _EnumWithFromString(Protocol):
23
+ """Enum-like type with ``from_string`` (e.g. LogFormat, LogLevel)."""
24
+
25
+ @classmethod
26
+ def from_string(cls, value: str) -> Any: ... # noqa: E704
27
+
28
+
29
+ def _resolve_enum_value(raw: Any, enum_class: type[_EnumWithFromString], default: Any) -> Any:
30
+ """Resolve a raw config value (string, enum, or None) to an enum instance."""
31
+ if raw is None:
32
+ return default
33
+ if isinstance(raw, enum_class):
34
+ return raw
35
+ return enum_class.from_string(str(raw).upper())
36
+
37
+
38
+ def _configured_log_directory(config: Any) -> Optional[str]:
39
+ """Return explicit log directory from flat ``log_dir`` or ``logging.directory``."""
40
+ flat = getattr(config, "log_dir", None)
41
+ nested = getattr(getattr(config, "logging", None), "directory", None)
42
+ chosen = flat or nested
43
+ if chosen is None:
44
+ return None
45
+ s = str(chosen).strip()
46
+ return s if s else None
47
+
48
+
49
+ def effective_log_file_from_config(config: Any) -> Optional[str]:
50
+ """Flat ``log_file`` or nested ``logging.file``, mirroring ``client_from_config`` logic."""
51
+ return getattr(config, "log_file", None) or getattr(
52
+ getattr(config, "logging", None), "file", None
53
+ )
54
+
55
+
56
+ def resolve_client_logfile_dir(config: Any, eff_log_file: Optional[str]) -> Optional[Path]:
57
+ """Resolve ``DbliftLogger``'s ``logfile_dir`` from config and optional log file path.
58
+
59
+ Absolute log file paths define the directory via their parent. Relative paths that
60
+ are only a file name (parent is ``.``) use :func:`_configured_log_directory` when set.
61
+ When no log file is configured, an explicit log directory still applies.
62
+ """
63
+ eff_log_dir = _configured_log_directory(config)
64
+ if not eff_log_file:
65
+ return Path(eff_log_dir) if eff_log_dir else None
66
+ p = Path(eff_log_file)
67
+ if p.is_absolute():
68
+ return p.parent
69
+ if p.parent != Path("."):
70
+ return p.parent
71
+ if eff_log_dir:
72
+ return Path(eff_log_dir)
73
+ return None
74
+
75
+
76
+ def resolve_config_or_raise(
77
+ provider: Any, explicit_config: Optional["DbliftConfig"]
78
+ ) -> "DbliftConfig":
79
+ """Return *explicit_config* if non-None, else ``provider.config``, else raise.
80
+
81
+ Extracted from ``DBLiftClient.__init__`` to keep the ctor focused on
82
+ wiring. Raising ``ConfigurationError`` here yields the same surface as
83
+ the previous inline check (callers expecting it still pass).
84
+ """
85
+ if explicit_config is not None:
86
+ return explicit_config
87
+ if provider.config is not None:
88
+ return provider.config # type: ignore[no-any-return]
89
+ raise ConfigurationError("DBLiftClient requires an explicit config or a provider with config")
90
+
91
+
92
+ def build_default_logger(
93
+ config: Any,
94
+ log_level: Optional[str],
95
+ log_format: Optional[str],
96
+ log_file: Optional[str],
97
+ ) -> DbliftLogger:
98
+ """Construct a ``DbliftLogger`` mirroring ``DBLiftClient.__init__`` defaults.
99
+
100
+ Ctor overrides (``log_level`` / ``log_format`` / ``log_file``) take
101
+ precedence over ``config``; ``None`` means "fall back to config".
102
+ """
103
+ raw_fmt = log_format if log_format is not None else getattr(config, "log_format", None)
104
+ log_format_value = _resolve_enum_value(raw_fmt, LogFormat, LogFormat.TEXT)
105
+
106
+ raw_lvl = log_level if log_level is not None else getattr(config, "log_level", None)
107
+ log_level_value = _resolve_enum_value(raw_lvl, LogLevel, LogLevel.INFO)
108
+
109
+ eff_log_file = log_file if log_file is not None else effective_log_file_from_config(config)
110
+ return DbliftLogger(
111
+ format=log_format_value,
112
+ level=log_level_value,
113
+ logfile_dir=resolve_client_logfile_dir(config, eff_log_file),
114
+ )
115
+
116
+
117
+ def normalize_migrations_dirs(config: Any, migrations_dir: Union[str, Path, List[Any]]) -> None:
118
+ """Normalize ``migrations_dir`` (str/Path/list) and apply to ``config.migrations``.
119
+
120
+ First entry becomes ``config.migrations.directory``; remaining entries (if any)
121
+ populate ``config.migrations.directories``. Caller passes the original
122
+ user-facing value; this function performs in-place mutation.
123
+ """
124
+ if isinstance(migrations_dir, (str, Path)):
125
+ migrations_dir = [migrations_dir]
126
+ paths = [_migration_directory_path(d) for d in migrations_dir]
127
+ if paths:
128
+ config.migrations.directory = str(paths[0])
129
+ if len(paths) > 1:
130
+ config.migrations.directories = [str(d) for d in paths[1:]]
131
+ else:
132
+ config.migrations.directories = []
133
+
134
+
135
+ def _migration_directory_path(directory: Any) -> Path:
136
+ if isinstance(directory, (str, Path)):
137
+ return Path(directory)
138
+ if isinstance(directory, dict):
139
+ return Path(directory.get("path", ""))
140
+ path = getattr(directory, "path", None)
141
+ if path is not None:
142
+ return Path(path)
143
+ return Path(directory)
144
+
145
+
146
+ def apply_ctor_overrides(
147
+ config: Any,
148
+ kwargs: Dict[str, Any],
149
+ log_level: Optional[str],
150
+ log_format: Optional[str],
151
+ log_file: Optional[str],
152
+ ) -> None:
153
+ """Apply ``**kwargs`` config setattr + explicit log_* overrides (in-place).
154
+
155
+ ``setattr`` is used so duck-typed configs without declared ``log_*`` fields
156
+ keep working — matches the prior inline logic in ``DBLiftClient.__init__``.
157
+ """
158
+ for key, value in kwargs.items():
159
+ if hasattr(config, key):
160
+ setattr(config, key, value)
161
+ if log_level is not None:
162
+ setattr(config, "log_level", log_level)
163
+ if log_format is not None:
164
+ setattr(config, "log_format", log_format)
165
+ if log_file is not None:
166
+ setattr(config, "log_file", log_file)
167
+
168
+
169
+ def client_from_config(
170
+ config: "DbliftConfig",
171
+ logger: Optional[Any] = None,
172
+ *,
173
+ client_cls: Optional[type] = None,
174
+ **kwargs: Any,
175
+ ) -> Any:
176
+ """Create a client instance from existing configuration.
177
+
178
+ This is the primary factory function for creating a client from an
179
+ existing configuration object. Used by CLI and other entry points.
180
+
181
+ Args:
182
+ config: DbliftConfig instance
183
+ logger: Optional logger instance
184
+ client_cls: Concrete client class (defaults to :class:`~api.client.DBLiftClient`)
185
+ **kwargs: Additional options passed to the client constructor
186
+
187
+ Returns:
188
+ An instance of ``client_cls`` (or ``DBLiftClient`` when ``client_cls`` is omitted)
189
+ """
190
+ client_config = deepcopy(config)
191
+
192
+ # Create logger if not provided
193
+ if logger is None:
194
+ raw_fmt = getattr(config, "log_format", None)
195
+ log_format_value = _resolve_enum_value(raw_fmt, LogFormat, LogFormat.TEXT)
196
+ raw_lvl = getattr(config, "log_level", None)
197
+ log_level = _resolve_enum_value(raw_lvl, LogLevel, LogLevel.INFO)
198
+ eff_log_file = effective_log_file_from_config(config)
199
+ logger = DbliftLogger(
200
+ format=log_format_value,
201
+ level=log_level,
202
+ logfile_dir=resolve_client_logfile_dir(config, eff_log_file),
203
+ )
204
+
205
+ provider = ProviderRegistry.create_provider(client_config, logger)
206
+
207
+ # Caller-supplied migrations_dir takes priority over config.migrations.directory.
208
+ # Pop it from kwargs now so it is not passed twice to the constructor.
209
+ caller_migrations_dir = kwargs.pop("migrations_dir", None)
210
+
211
+ # Get migrations directory from config (used only when caller did not supply one)
212
+ migrations_dir: Union[str, Path, List[Any]] = (
213
+ caller_migrations_dir
214
+ if caller_migrations_dir is not None
215
+ else client_config.migrations.directory
216
+ )
217
+ if caller_migrations_dir is None and hasattr(client_config.migrations, "get_directory_configs"):
218
+ dir_configs = client_config.migrations.get_directory_configs()
219
+ if dir_configs:
220
+ configured_dirs = [dir_config.path for dir_config in dir_configs]
221
+ if configured_dirs:
222
+ migrations_dir = configured_dirs
223
+
224
+ # Import here to avoid circular import: api.client imports this module at load time.
225
+ from api.client import DBLiftClient
226
+
227
+ ctor = client_cls if client_cls is not None else DBLiftClient
228
+
229
+ # Create client using provider (API-first pattern)
230
+ return ctor(
231
+ provider=provider,
232
+ migrations_dir=migrations_dir,
233
+ config=client_config,
234
+ logger=logger,
235
+ **kwargs,
236
+ )
237
+
238
+
239
+ def client_from_config_file(
240
+ config_path: str,
241
+ logger: Optional[Any] = None,
242
+ *,
243
+ client_cls: Optional[type] = None,
244
+ **overrides: Any,
245
+ ) -> Any:
246
+ """Create a client instance from config file path.
247
+
248
+ Args:
249
+ config_path: Path to configuration file
250
+ logger: Optional logger instance
251
+ client_cls: Concrete client class (defaults to :class:`~api.client.DBLiftClient`)
252
+ **overrides: Configuration overrides (database_url, database_schema, etc.)
253
+
254
+ Returns:
255
+ An instance of ``client_cls`` (or ``DBLiftClient`` when ``client_cls`` is omitted)
256
+ """
257
+ config = ConfigBuilder.build(file_path=config_path, **overrides)
258
+ # Keys already merged into ``config`` by ConfigBuilder.build must not be passed again
259
+ # to DBLiftClient (would re-apply or confuse nested database_* aliases).
260
+ passthrough = {
261
+ k: v for k, v in overrides.items() if k not in ConfigBuilder.CONFIG_BUILD_KWARG_KEYS
262
+ }
263
+ return client_from_config(config, logger, client_cls=client_cls, **passthrough)
264
+
265
+
266
+ def _attach_external_sqlite_connection(provider: Any, engine: Any, connection: Any) -> None:
267
+ """Bind a caller-owned SQLAlchemy engine/connection to a sqlite3 provider.
268
+
269
+ The native ``SQLiteProvider`` extracts the underlying DBAPI
270
+ ``sqlite3.Connection`` from the SQLAlchemy Engine (or Connection) and
271
+ operates on the *same* database as the caller. It also retains the
272
+ engine/connection so it can re-bind on reconnect, and flags the connection
273
+ as caller-owned so ``close()`` never disposes it.
274
+ """
275
+ provider.attach_external_sqlalchemy(engine, connection)
276
+
277
+
278
+ def client_from_sqlalchemy(
279
+ engine: Any = None,
280
+ migrations_dir: Optional[Union[str, Path, List[Union[str, Path]]]] = None,
281
+ schema: Optional[str] = None,
282
+ logger: Optional[Any] = None,
283
+ log_level: str = "INFO",
284
+ log_format: str = "text",
285
+ log_file: Optional[str] = None,
286
+ *,
287
+ connection: Any = None,
288
+ config: Optional[DbliftConfig] = None,
289
+ client_cls: Optional[type[Any]] = None,
290
+ **kwargs: Any,
291
+ ) -> Any:
292
+ """Create DBLiftClient from an existing SQLAlchemy Engine or Connection.
293
+
294
+ Primary integration point for Python application runtimes (FastAPI lifespan,
295
+ pytest fixtures, Flask, etc.). The caller retains ownership of the engine;
296
+ DBLiftClient.close() will not dispose it.
297
+
298
+ Accepts either ``engine=`` or ``connection=`` (mutually exclusive).
299
+ """
300
+ if engine is not None and connection is not None:
301
+ raise ConfigurationError("Pass engine or connection, not both")
302
+ if connection is not None:
303
+ engine = getattr(connection, "engine", connection)
304
+ if engine is None:
305
+ raise ConfigurationError("from_sqlalchemy requires engine= or connection=")
306
+
307
+ derived = config_from_engine(engine, schema=schema, migrations_dir=migrations_dir)
308
+ if config is not None:
309
+ # Overlay the caller's config without stomping connection identity
310
+ # derived from the injected engine.
311
+ merged = deepcopy(derived)
312
+ database_identity_fields = {
313
+ "url",
314
+ "type",
315
+ "host",
316
+ "port",
317
+ "database",
318
+ "username",
319
+ "password",
320
+ "driver",
321
+ "schema",
322
+ }
323
+ for attr in ("database", "migrations", "logging"):
324
+ if hasattr(config, attr):
325
+ override = getattr(config, attr)
326
+ target = getattr(merged, attr)
327
+ for f in dir(override):
328
+ if attr == "database" and f in database_identity_fields:
329
+ continue
330
+ if not f.startswith("_") and hasattr(override, f):
331
+ val = getattr(override, f)
332
+ if val is not None and not callable(val):
333
+ setattr(target, f, val)
334
+ # Also bring in top-level fields from the override (e.g. placeholders)
335
+ for f in dir(config):
336
+ if not f.startswith("_") and hasattr(config, f):
337
+ val = getattr(config, f)
338
+ if (
339
+ val is not None
340
+ and f not in ("database", "migrations", "logging")
341
+ and not callable(val)
342
+ ): # noqa: E501
343
+ setattr(merged, f, val)
344
+ derived = merged
345
+
346
+ # Respect explicit logger if provided (matches client_from_config contract).
347
+ # Only fall back to building from log_* params when logger is None.
348
+ if logger is None:
349
+ logger = build_default_logger(derived, log_level, log_format, log_file)
350
+
351
+ provider = ProviderRegistry.create_provider(derived, logger)
352
+ dialect_name = str(getattr(engine.dialect, "name", ""))
353
+
354
+ # Inject external engine so provider re-uses caller's Engine/Connection
355
+ # (ownership=False prevents dispose on client/provider close).
356
+ if hasattr(provider, "_conn_mgr"):
357
+ provider._conn_mgr = NativeConnectionManager(
358
+ derived, logger, engine=engine, owns_engine=False
359
+ ) # noqa: E501
360
+ elif dialect_name == "sqlite": # lint: allow-dialect-string: stdlib sqlite3 provider path
361
+ # The native SQLite provider talks to ``sqlite3`` directly instead of
362
+ # through a NativeConnectionManager, so the branch above never fires for
363
+ # it. Without this, the provider would open its *own* ``sqlite3``
364
+ # connection and migrate a different database than the caller's engine —
365
+ # fatal for ``sqlite:///:memory:`` where every connection is a separate
366
+ # in-memory DB. Reach through the SQLAlchemy engine/connection to its
367
+ # underlying DBAPI ``sqlite3.Connection`` and hand that to the provider.
368
+ _attach_external_sqlite_connection(provider, engine, connection)
369
+
370
+ # When a specific Connection was passed, bind it directly so that
371
+ # immediate provider operations (and thus migrations) run against the
372
+ # caller's live connection/session rather than opening a fresh one from
373
+ # the engine pool. This makes the `connection=` path actually useful.
374
+ # We also set a flag so the provider skips its auto-commit logic
375
+ # (the caller owns the session/tx and is responsible for commit/rollback).
376
+ if connection is not None:
377
+ setattr(provider, "_connection", connection)
378
+ setattr(provider, "_external_connection", True)
379
+
380
+ ctor = client_cls or __import__("api.client", fromlist=["DBLiftClient"]).DBLiftClient
381
+ return ctor(
382
+ provider=provider,
383
+ migrations_dir=migrations_dir or getattr(derived.migrations, "directory", None),
384
+ config=derived,
385
+ logger=logger,
386
+ **kwargs,
387
+ )
@@ -0,0 +1,258 @@
1
+ """Private operation helpers for OSS :mod:`api.client`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any, List, Optional, Union
7
+
8
+ from api.events import EventType
9
+ from core.logger.results import GenerateUndoScriptResult
10
+
11
+
12
+ def _heuristic_statement_count_from_sql(sql_text: str) -> int:
13
+ """Count lines that look like standalone SQL statements (heuristic)."""
14
+ return sum(
15
+ 1
16
+ for line in sql_text.split("\n")
17
+ if line.strip() and not line.strip().startswith("--") and line.strip().endswith(";")
18
+ )
19
+
20
+
21
+ def _apply_sql_script_warning_scan(
22
+ result: GenerateUndoScriptResult,
23
+ sql_text: str,
24
+ ) -> None:
25
+ """Set manual-review flag and collect per-line warnings from generated SQL text."""
26
+ sql_lower = sql_text.lower()
27
+ if "warning" in sql_lower or "requires manual review" in sql_lower:
28
+ result.requires_manual_review = True
29
+ for line in sql_text.split("\n"):
30
+ if "warning" in line.lower():
31
+ warning_msg = line.strip().lstrip("--").strip()
32
+ if warning_msg:
33
+ result.add_warning(warning_msg)
34
+
35
+
36
+ def generate_undo_script_operation(
37
+ client: Any,
38
+ *,
39
+ migration_path: Union[str, Path],
40
+ output_dir: Optional[Union[str, Path]] = None,
41
+ overwrite: bool = False,
42
+ ) -> GenerateUndoScriptResult:
43
+ """Generate one undo script for ``DBLiftClient.generate_undo_script``."""
44
+ result = GenerateUndoScriptResult()
45
+ migration_path = Path(migration_path)
46
+ result.migration_path = str(migration_path)
47
+ if output_dir:
48
+ output_dir = Path(output_dir)
49
+
50
+ client.events.emit(
51
+ EventType.MIGRATION_STARTED,
52
+ {"operation": "generate_undo_script", "migration_path": str(migration_path)},
53
+ )
54
+
55
+ try:
56
+ migration = _prepare_undo_generation_migration(client, migration_path)
57
+ result = _generate_undo_script_for_migration(
58
+ client,
59
+ migration_path=migration_path,
60
+ migration=migration,
61
+ output_dir=output_dir,
62
+ overwrite=overwrite,
63
+ )
64
+ client.events.emit(
65
+ EventType.MIGRATION_COMPLETED,
66
+ {"result": result, "operation": "generate_undo_script"},
67
+ )
68
+ return result
69
+ except FileNotFoundError as e:
70
+ _emit_undo_generation_failure(client, result, str(e))
71
+ raise
72
+ except (FileExistsError, ValueError) as e:
73
+ _emit_undo_generation_failure(client, result, str(e))
74
+ return result
75
+ except Exception as e:
76
+ _emit_undo_generation_failure(client, result, f"Failed to generate undo script: {str(e)}")
77
+ raise
78
+
79
+
80
+ def generate_undo_scripts_operation(
81
+ client: Any,
82
+ *,
83
+ migration_paths: Optional[List[Union[str, Path]]] = None,
84
+ migrations_dir: Optional[Union[str, Path]] = None,
85
+ overwrite: bool = False,
86
+ recursive: bool = True,
87
+ **kwargs: Any,
88
+ ) -> List[GenerateUndoScriptResult]:
89
+ """Generate many undo scripts for ``DBLiftClient.generate_undo_scripts``."""
90
+ results: List[GenerateUndoScriptResult] = []
91
+
92
+ if migration_paths is None:
93
+ migrations_dir = (
94
+ client._get_scripts_dir() if migrations_dir is None else Path(migrations_dir)
95
+ )
96
+ pattern = "**/V*.sql" if recursive else "V*.sql"
97
+ migration_paths = [f for f in migrations_dir.glob(pattern) if f.is_file()]
98
+ else:
99
+ migration_paths = [Path(p) for p in migration_paths]
100
+
101
+ client.events.emit(
102
+ EventType.MIGRATION_STARTED,
103
+ {"operation": "generate_undo_scripts", "count": len(migration_paths)},
104
+ )
105
+
106
+ for migration_path in migration_paths:
107
+ try:
108
+ migration_path_typed = (
109
+ Path(migration_path) if isinstance(migration_path, str) else migration_path
110
+ )
111
+ client.events.emit(
112
+ EventType.MIGRATION_STARTED,
113
+ {
114
+ "operation": "generate_undo_script",
115
+ "migration_path": str(migration_path_typed),
116
+ },
117
+ )
118
+ migration = _prepare_undo_generation_migration(client, migration_path_typed)
119
+ result = _generate_undo_script_for_migration(
120
+ client,
121
+ migration_path=migration_path_typed,
122
+ migration=migration,
123
+ output_dir=kwargs.get("output_dir"),
124
+ overwrite=overwrite,
125
+ )
126
+ client.events.emit(
127
+ EventType.MIGRATION_COMPLETED,
128
+ {"result": result, "operation": "generate_undo_script"},
129
+ )
130
+ results.append(result)
131
+ except (FileNotFoundError, FileExistsError, ValueError) as e:
132
+ error_result = _undo_script_error_result(migration_path, str(e))
133
+ results.append(error_result)
134
+ client.events.emit(
135
+ EventType.MIGRATION_FAILED,
136
+ {"error": str(e), "operation": "generate_undo_script"},
137
+ )
138
+ except Exception as e:
139
+ error_msg = f"Failed to generate undo script: {str(e)}"
140
+ results.append(_undo_script_error_result(migration_path, error_msg))
141
+ client.events.emit(
142
+ EventType.MIGRATION_FAILED,
143
+ {"error": error_msg, "operation": "generate_undo_script"},
144
+ )
145
+
146
+ client.events.emit(
147
+ EventType.MIGRATION_COMPLETED,
148
+ {
149
+ "operation": "generate_undo_scripts",
150
+ "results": results,
151
+ "success_count": sum(1 for r in results if r.success),
152
+ "failure_count": sum(1 for r in results if not r.success),
153
+ },
154
+ )
155
+ return results
156
+
157
+
158
+ def _prepare_undo_generation_migration(client: Any, migration_path: Path) -> Any:
159
+ """Validate a path once and return the parsed SQL versioned migration.
160
+
161
+ Returns a ``core.migration.migration.Migration`` — typed as ``Any``
162
+ here to avoid a top-level import cycle (``Migration`` transitively
163
+ pulls in ``api`` via the executor).
164
+ """
165
+ from core.migration.formats import MigrationFormat
166
+ from core.migration.migration import Migration
167
+ from core.migration.scripting.migration_script_manager import MigrationScriptManager
168
+
169
+ if not migration_path.exists():
170
+ raise FileNotFoundError(f"Migration file not found: {migration_path}")
171
+
172
+ script_manager = MigrationScriptManager(client.logger)
173
+ if not script_manager.is_versioned_script_name(migration_path.name):
174
+ raise ValueError(
175
+ f"File is not a versioned migration: {migration_path.name}. "
176
+ "Expected a versioned migration filename (V*__description.<ext>)."
177
+ )
178
+
179
+ migration = Migration(script_path=migration_path, logger=client.logger)
180
+ if not migration.version:
181
+ raise ValueError(f"Could not extract version from: {migration_path.name}")
182
+ if migration.format != MigrationFormat.SQL:
183
+ raise ValueError(
184
+ "Automatic undo script generation supports SQL migrations (V*__.sql) only. "
185
+ f"{migration_path.name} uses format {migration.format.value}; add a hand-written "
186
+ "U*__.sql undo script instead."
187
+ )
188
+ return migration
189
+
190
+
191
+ def _generate_undo_script_for_migration(
192
+ client: Any,
193
+ *,
194
+ migration_path: Path,
195
+ migration: Any,
196
+ output_dir: Optional[Union[str, Path]],
197
+ overwrite: bool,
198
+ ) -> GenerateUndoScriptResult:
199
+ """Generate an undo script for an already validated Migration."""
200
+ from core.migration.scripting.undo_script_generator import UndoScriptGenerator
201
+
202
+ result = GenerateUndoScriptResult()
203
+
204
+ output_dir_path: Optional[Path] = None
205
+ if output_dir and output_dir != "":
206
+ output_dir_path = Path(output_dir) if isinstance(output_dir, str) else output_dir
207
+ if output_dir_path is None:
208
+ output_dir_path = migration_path.parent
209
+
210
+ generator = UndoScriptGenerator(dialect=client.dialect, logger=client.logger)
211
+ expected_undo_path = generator.get_undo_script_path_for_migration(
212
+ migration,
213
+ output_dir=output_dir_path,
214
+ )
215
+ file_existed_before = expected_undo_path.exists()
216
+ # Use the pre-parsed-migration entry point so the file isn't re-parsed
217
+ # (we already validated + constructed ``migration`` in
218
+ # ``_prepare_undo_generation_migration``). Bugbot review on PR #382.
219
+ undo_path = generator.generate_undo_script_for_migration(
220
+ migration,
221
+ output_dir=output_dir_path,
222
+ overwrite=overwrite,
223
+ )
224
+
225
+ if undo_path.exists():
226
+ content = undo_path.read_text()
227
+ result.statements_generated = _heuristic_statement_count_from_sql(content)
228
+ _apply_sql_script_warning_scan(result, content)
229
+
230
+ if overwrite and file_existed_before:
231
+ result.overwritten = True
232
+
233
+ result.migration_path = str(migration_path)
234
+ result.undo_script_path = str(undo_path)
235
+ result.success = True
236
+ result.complete()
237
+ return result
238
+
239
+
240
+ def _emit_undo_generation_failure(
241
+ client: Any, result: GenerateUndoScriptResult, error_msg: str
242
+ ) -> None:
243
+ result.set_error(error_msg)
244
+ result.complete()
245
+ client.events.emit(
246
+ EventType.MIGRATION_FAILED,
247
+ {"error": error_msg, "operation": "generate_undo_script"},
248
+ )
249
+
250
+
251
+ def _undo_script_error_result(
252
+ migration_path: Union[str, Path], error_message: str
253
+ ) -> GenerateUndoScriptResult:
254
+ result = GenerateUndoScriptResult()
255
+ result.migration_path = str(migration_path)
256
+ result.set_error(error_message)
257
+ result.complete()
258
+ return result