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.
- api/__init__.py +12 -0
- api/_cli_support.py +27 -0
- api/_client_factory.py +387 -0
- api/_client_operations.py +258 -0
- api/_engine_config.py +70 -0
- api/async_client.py +138 -0
- api/client.py +974 -0
- api/events.py +579 -0
- api/migrations.py +5 -0
- api/py.typed +0 -0
- cli/__init__.py +10 -0
- cli/_command_handlers.py +94 -0
- cli/_config_helpers.py +599 -0
- cli/_constants.py +33 -0
- cli/_output.py +179 -0
- cli/_parser_setup.py +416 -0
- cli/db_utils.py +518 -0
- cli/extensions.py +56 -0
- cli/handlers/__init__.py +11 -0
- cli/handlers/_shared.py +132 -0
- cli/handlers/baseline.py +18 -0
- cli/handlers/clean.py +17 -0
- cli/handlers/import_flyway.py +17 -0
- cli/handlers/info.py +147 -0
- cli/handlers/migrate.py +46 -0
- cli/handlers/repair.py +18 -0
- cli/handlers/undo.py +32 -0
- cli/handlers/validate.py +29 -0
- cli/main.py +442 -0
- config/__init__.py +10 -0
- config/_credential_masking.py +71 -0
- config/_subclasses/__init__.py +12 -0
- config/_subclasses/cosmosdb_config.py +94 -0
- config/_subclasses/db2_config.py +56 -0
- config/_subclasses/dummy_config.py +19 -0
- config/_subclasses/mysql_config.py +91 -0
- config/_subclasses/oracle_config.py +48 -0
- config/_subclasses/postgresql_config.py +56 -0
- config/_subclasses/sqlite_config.py +88 -0
- config/_subclasses/sqlserver_config.py +116 -0
- config/_url_builder_mixin.py +119 -0
- config/config_builder.py +335 -0
- config/database_config.py +615 -0
- config/dblift_config.py +1139 -0
- config/errors.py +5 -0
- config/secrets/__init__.py +16 -0
- config/secrets/_cache.py +37 -0
- config/secrets/_provider_base.py +27 -0
- config/secrets/_registry.py +117 -0
- config/secrets/_resolver.py +83 -0
- config/secrets/_secrets_config.py +60 -0
- config/validation_config.py +124 -0
- core/__init__.py +59 -0
- core/constants.py +72 -0
- core/dialect_boundary.py +275 -0
- core/exceptions.py +55 -0
- core/features.py +28 -0
- core/introspection/__init__.py +23 -0
- core/introspection/_column_enricher.py +279 -0
- core/introspection/_partition_enricher.py +184 -0
- core/introspection/_schema_orchestrator.py +241 -0
- core/introspection/_utils.py +22 -0
- core/introspection/_vendor_property_applier.py +70 -0
- core/introspection/base_introspector.py +889 -0
- core/introspection/capability_matrix.py +276 -0
- core/introspection/extractors/__init__.py +25 -0
- core/introspection/extractors/base_extractor.py +142 -0
- core/introspection/extractors/column_extractor.py +230 -0
- core/introspection/extractors/constraint_extractor.py +598 -0
- core/introspection/extractors/index_extractor.py +344 -0
- core/introspection/extractors/misc_extractor.py +730 -0
- core/introspection/extractors/procedure_extractor.py +821 -0
- core/introspection/extractors/sequence_extractor.py +138 -0
- core/introspection/extractors/table_extractor.py +467 -0
- core/introspection/extractors/trigger_extractor.py +170 -0
- core/introspection/extractors/view_extractor.py +323 -0
- core/introspection/introspector_factory.py +136 -0
- core/introspection/result.py +204 -0
- core/introspection/schema_introspector.py +16 -0
- core/introspection/vendor_queries_base.py +924 -0
- core/introspection/vendor_queries_factory.py +153 -0
- core/introspection/vendor_queries_protocols.py +225 -0
- core/introspection/version_detector.py +279 -0
- core/logger/__init__.py +448 -0
- core/logger/_base.py +145 -0
- core/logger/_factory.py +239 -0
- core/logger/_formatters.py +120 -0
- core/logger/_levels.py +112 -0
- core/logger/_multi.py +135 -0
- core/logger/_null.py +66 -0
- core/logger/console.py +195 -0
- core/logger/formatters/__init__.py +30 -0
- core/logger/formatters/_format_diff_meta.py +117 -0
- core/logger/formatters/_format_diff_object.py +195 -0
- core/logger/formatters/_format_diff_routine.py +208 -0
- core/logger/formatters/_format_diff_table.py +250 -0
- core/logger/formatters/_formatter_impl.py +567 -0
- core/logger/formatters/diff_utils.py +396 -0
- core/logger/formatters/factory.py +104 -0
- core/logger/formatters/formatter.py +12 -0
- core/logger/formatters/htmlformatter.py +876 -0
- core/logger/formatters/jsonformatter.py +978 -0
- core/logger/log.py +782 -0
- core/logger/results.py +904 -0
- core/migration/__init__.py +33 -0
- core/migration/_type_match.py +111 -0
- core/migration/clean_summary.py +86 -0
- core/migration/commands/__init__.py +6 -0
- core/migration/commands/_script_events.py +18 -0
- core/migration/commands/base_command.py +1025 -0
- core/migration/commands/baseline_command.py +96 -0
- core/migration/commands/clean_command.py +342 -0
- core/migration/commands/import_flyway_command.py +190 -0
- core/migration/commands/info_command.py +208 -0
- core/migration/commands/migrate_command.py +853 -0
- core/migration/commands/repair_command.py +769 -0
- core/migration/commands/undo_command.py +869 -0
- core/migration/commands/validate_command.py +90 -0
- core/migration/encoding.py +90 -0
- core/migration/executor/__init__.py +12 -0
- core/migration/executor/execution_engine.py +1196 -0
- core/migration/executor/migration_executor.py +439 -0
- core/migration/executor/migration_helpers.py +142 -0
- core/migration/executor/placeholder_manager.py +77 -0
- core/migration/executor/transaction_policy.py +63 -0
- core/migration/executors/__init__.py +20 -0
- core/migration/executors/base_executor.py +179 -0
- core/migration/executors/executor_factory.py +251 -0
- core/migration/executors/python_executor.py +379 -0
- core/migration/executors/sql_executor.py +273 -0
- core/migration/formats/__init__.py +14 -0
- core/migration/formats/format_detector.py +204 -0
- core/migration/formats/migration_format.py +67 -0
- core/migration/history/__init__.py +1 -0
- core/migration/history/migration_history_manager.py +384 -0
- core/migration/journals/__init__.py +14 -0
- core/migration/journals/migration_journal.py +618 -0
- core/migration/migration.py +775 -0
- core/migration/placeholders/__init__.py +1 -0
- core/migration/placeholders/placeholder_service.py +84 -0
- core/migration/rules/__init__.py +1 -0
- core/migration/rules/migration_rules.py +145 -0
- core/migration/scripting/__init__.py +1 -0
- core/migration/scripting/migration_script_manager.py +721 -0
- core/migration/scripting/undo_script_generator/__init__.py +24 -0
- core/migration/scripting/undo_script_generator/_ddl_reversers.py +485 -0
- core/migration/scripting/undo_script_generator/_dml_reversers.py +319 -0
- core/migration/scripting/undo_script_generator/_extractors.py +511 -0
- core/migration/scripting/undo_script_generator/_generator.py +310 -0
- core/migration/scripting/undo_script_generator/_helpers.py +532 -0
- core/migration/scripting/undo_script_generator/_models.py +18 -0
- core/migration/scripting/undo_script_generator/_reversers.py +864 -0
- core/migration/sql/__init__.py +1 -0
- core/migration/sql/execution_statement.py +38 -0
- core/migration/sql/sql_analyzer.py +806 -0
- core/migration/sql/sql_execution_service.py +431 -0
- core/migration/sql/statement_splitter.py +74 -0
- core/migration/state/__init__.py +5 -0
- core/migration/state/migration_data_service.py +603 -0
- core/migration/state/migration_display_state.py +36 -0
- core/migration/state/migration_formatter.py +215 -0
- core/migration/state/migration_state.py +166 -0
- core/migration/state/migration_state_manager.py +830 -0
- core/migration/state/migration_state_service.py +244 -0
- core/migration/ui/__init__.py +15 -0
- core/migration/ui/data_collector.py +926 -0
- core/migration/ui/display_formatters.py +251 -0
- core/migration/ui/migration_analyzer.py +259 -0
- core/migration/ui/migration_ui.py +362 -0
- core/migration/ui/table_renderer.py +250 -0
- core/migration/version_utils.py +94 -0
- core/normalization/__init__.py +35 -0
- core/normalization/data_type_normalizer.py +5 -0
- core/normalization/dependency_resolver.py +435 -0
- core/normalization/identifier_normalizer.py +297 -0
- core/normalization/object_orderer.py +230 -0
- core/normalization/type_constants.py +154 -0
- core/normalization/type_mapper.py +220 -0
- core/normalization/type_mappings.py +89 -0
- core/normalization/type_normalizer.py +245 -0
- core/seams/__init__.py +1 -0
- core/seams/event_listeners.py +26 -0
- core/seams/introspection.py +18 -0
- core/sql_model/__init__.py +60 -0
- core/sql_model/_base_parse_result.py +491 -0
- core/sql_model/_base_sql_column.py +194 -0
- core/sql_model/_base_sql_constraint.py +223 -0
- core/sql_model/_base_sql_object.py +233 -0
- core/sql_model/_base_sql_statement.py +100 -0
- core/sql_model/base.py +43 -0
- core/sql_model/constraint_validator.py +469 -0
- core/sql_model/database_link.py +151 -0
- core/sql_model/dialect.py +482 -0
- core/sql_model/event.py +155 -0
- core/sql_model/extension.py +109 -0
- core/sql_model/foreign_data_wrapper.py +113 -0
- core/sql_model/foreign_server.py +138 -0
- core/sql_model/index.py +279 -0
- core/sql_model/linked_server.py +170 -0
- core/sql_model/module.py +112 -0
- core/sql_model/package.py +134 -0
- core/sql_model/partition.py +180 -0
- core/sql_model/procedure.py +321 -0
- core/sql_model/sequence.py +208 -0
- core/sql_model/synonym.py +143 -0
- core/sql_model/table.py +1106 -0
- core/sql_model/table_canonicalizer.py +45 -0
- core/sql_model/table_options.py +73 -0
- core/sql_model/trigger.py +310 -0
- core/sql_model/user_defined_type.py +151 -0
- core/sql_model/view.py +429 -0
- core/sql_model/view_options.py +95 -0
- core/sql_parser/__init__.py +5 -0
- core/sql_parser/_partition_handler.py +113 -0
- core/sql_parser/_sqlglot_builders.py +634 -0
- core/sql_parser/base_statement_parser.py +356 -0
- core/sql_parser/base_tokenizer.py +473 -0
- core/sql_parser/common/__init__.py +1 -0
- core/sql_parser/common/base_parser.py +598 -0
- core/sql_parser/dialects/__init__.py +5 -0
- core/sql_parser/dialects/base_config.py +125 -0
- core/sql_parser/enhanced_regex_parser.py +453 -0
- core/sql_parser/hybrid_parser.py +1106 -0
- core/sql_parser/parser_context.py +89 -0
- core/sql_parser/parser_factory.py +215 -0
- core/sql_parser/parser_interface.py +93 -0
- core/sql_parser/sqlglot_parser.py +544 -0
- core/sql_parser/tokens.py +48 -0
- core/sql_parser/unified_regex_parser.py +644 -0
- core/sql_validator/__init__.py +5 -0
- core/sql_validator/_checksum_validator.py +341 -0
- core/sql_validator/_flyway_compatibility.py +301 -0
- core/sql_validator/_migration_filter.py +221 -0
- core/sql_validator/_sql_syntax_validator.py +168 -0
- core/sql_validator/_strict_mode_validator.py +136 -0
- core/sql_validator/migration_validator.py +923 -0
- core/state/sql_script_formatter.py +144 -0
- core/state/sql_statement.py +41 -0
- core/utils/__init__.py +1 -0
- core/utils/database_url_parser.py +104 -0
- core/utils/metadata_helpers.py +83 -0
- core/utils/row_access.py +192 -0
- core/utils/string_utils.py +21 -0
- core/utils/url_masking.py +24 -0
- db/__init__.py +21 -0
- db/base_provider.py +266 -0
- db/base_quirks.py +1620 -0
- db/constants.py +5 -0
- db/error.py +344 -0
- db/error_handler.py +349 -0
- db/exceptions.py +11 -0
- db/native_connection_manager.py +70 -0
- db/object_naming.py +39 -0
- db/plugins/__init__.py +9 -0
- db/plugins/base_history_manager.py +616 -0
- db/plugins/base_locking_manager.py +154 -0
- db/plugins/base_query_executor.py +253 -0
- db/plugins/base_schema_operations.py +288 -0
- db/plugins/base_snapshot_manager.py +172 -0
- db/plugins/base_undo_manager.py +126 -0
- db/plugins/cosmosdb/__init__.py +12 -0
- db/plugins/cosmosdb/cosmosdb/__init__.py +19 -0
- db/plugins/cosmosdb/cosmosdb/_sdk.py +28 -0
- db/plugins/cosmosdb/cosmosdb/connection_manager.py +247 -0
- db/plugins/cosmosdb/cosmosdb/history_manager.py +427 -0
- db/plugins/cosmosdb/cosmosdb/locking_manager.py +465 -0
- db/plugins/cosmosdb/cosmosdb/query_executor.py +1518 -0
- db/plugins/cosmosdb/cosmosdb/schema_operations.py +416 -0
- db/plugins/cosmosdb/introspection/__init__.py +3 -0
- db/plugins/cosmosdb/parser/__init__.py +5 -0
- db/plugins/cosmosdb/parser/cosmosdb_regex_parser.py +64 -0
- db/plugins/cosmosdb/plugin.py +19 -0
- db/plugins/cosmosdb/provider.py +364 -0
- db/plugins/cosmosdb/quirks.py +240 -0
- db/plugins/cosmosdb/sdk_translator/__init__.py +39 -0
- db/plugins/cosmosdb/sdk_translator/_executor.py +422 -0
- db/plugins/cosmosdb/sdk_translator/_executors.py +356 -0
- db/plugins/cosmosdb/sdk_translator/_parsing.py +53 -0
- db/plugins/cosmosdb/sdk_translator/_script_generation.py +141 -0
- db/plugins/cosmosdb/sdk_translator/_translator.py +129 -0
- db/plugins/cosmosdb/sdk_translator/_translators.py +795 -0
- db/plugins/db2/__init__.py +11 -0
- db/plugins/db2/db2/__init__.py +11 -0
- db/plugins/db2/db2/history_manager.py +317 -0
- db/plugins/db2/db2/locking_manager.py +558 -0
- db/plugins/db2/db2/schema_operations.py +985 -0
- db/plugins/db2/introspection/__init__.py +3 -0
- db/plugins/db2/parser/__init__.py +9 -0
- db/plugins/db2/parser/db2_regex_parser.py +668 -0
- db/plugins/db2/parser/parser_config.py +1147 -0
- db/plugins/db2/plugin.py +22 -0
- db/plugins/db2/provider.py +420 -0
- db/plugins/db2/quirks.py +382 -0
- db/plugins/db2/sqlalchemy_url.py +62 -0
- db/plugins/mariadb/__init__.py +13 -0
- db/plugins/mariadb/plugin.py +21 -0
- db/plugins/mariadb/provider.py +40 -0
- db/plugins/mariadb/quirks.py +50 -0
- db/plugins/mysql/__init__.py +11 -0
- db/plugins/mysql/introspection/__init__.py +1 -0
- db/plugins/mysql/mysql/__init__.py +11 -0
- db/plugins/mysql/mysql/history_manager.py +384 -0
- db/plugins/mysql/mysql/locking_manager.py +371 -0
- db/plugins/mysql/mysql/schema_operations.py +555 -0
- db/plugins/mysql/parser/__init__.py +1 -0
- db/plugins/mysql/parser/mysql_regex_parser.py +531 -0
- db/plugins/mysql/parser/mysql_statement_parser.py +255 -0
- db/plugins/mysql/parser/mysql_tokenizer.py +460 -0
- db/plugins/mysql/parser/parser_config.py +670 -0
- db/plugins/mysql/plugin.py +22 -0
- db/plugins/mysql/provider.py +329 -0
- db/plugins/mysql/quirks.py +540 -0
- db/plugins/mysql/sqlalchemy_url.py +67 -0
- db/plugins/oracle/__init__.py +11 -0
- db/plugins/oracle/introspection/__init__.py +3 -0
- db/plugins/oracle/introspection/oracle_utils.py +86 -0
- db/plugins/oracle/oracle/__init__.py +14 -0
- db/plugins/oracle/oracle/dbms_output.py +49 -0
- db/plugins/oracle/oracle/history_manager.py +444 -0
- db/plugins/oracle/oracle/locking_manager.py +436 -0
- db/plugins/oracle/oracle/schema_operations.py +1134 -0
- db/plugins/oracle/parser/__init__.py +13 -0
- db/plugins/oracle/parser/_comments.py +57 -0
- db/plugins/oracle/parser/_object_extractor.py +203 -0
- db/plugins/oracle/parser/_plsql_block.py +727 -0
- db/plugins/oracle/parser/_sqlplus.py +312 -0
- db/plugins/oracle/parser/_statement_splitter.py +233 -0
- db/plugins/oracle/parser/oracle_parser.py +297 -0
- db/plugins/oracle/parser/oracle_statement_parser.py +434 -0
- db/plugins/oracle/parser/oracle_tokenizer.py +251 -0
- db/plugins/oracle/parser/parser_config.py +330 -0
- db/plugins/oracle/parser/sqlplus_context.py +179 -0
- db/plugins/oracle/plugin.py +22 -0
- db/plugins/oracle/provider.py +736 -0
- db/plugins/oracle/quirks.py +743 -0
- db/plugins/oracle/sqlalchemy_url.py +65 -0
- db/plugins/postgresql/__init__.py +12 -0
- db/plugins/postgresql/_provider_query_executor.py +25 -0
- db/plugins/postgresql/introspection/__init__.py +3 -0
- db/plugins/postgresql/parser/__init__.py +10 -0
- db/plugins/postgresql/parser/parser_config.py +876 -0
- db/plugins/postgresql/parser/postgresql_regex_parser.py +691 -0
- db/plugins/postgresql/parser/postgresql_statement_parser.py +118 -0
- db/plugins/postgresql/parser/postgresql_tokenizer.py +325 -0
- db/plugins/postgresql/plugin.py +29 -0
- db/plugins/postgresql/postgresql/__init__.py +11 -0
- db/plugins/postgresql/postgresql/history_manager.py +231 -0
- db/plugins/postgresql/postgresql/locking_manager.py +310 -0
- db/plugins/postgresql/postgresql/schema_operations.py +690 -0
- db/plugins/postgresql/provider.py +248 -0
- db/plugins/postgresql/quirks.py +722 -0
- db/plugins/postgresql/sqlalchemy_url.py +65 -0
- db/plugins/sqlite/__init__.py +12 -0
- db/plugins/sqlite/introspection/__init__.py +3 -0
- db/plugins/sqlite/parser/__init__.py +9 -0
- db/plugins/sqlite/parser/parser_config.py +377 -0
- db/plugins/sqlite/parser/sqlite_regex_parser.py +338 -0
- db/plugins/sqlite/plugin.py +21 -0
- db/plugins/sqlite/provider.py +504 -0
- db/plugins/sqlite/quirks.py +96 -0
- db/plugins/sqlite/sqlalchemy_url.py +16 -0
- db/plugins/sqlite/sqlite/__init__.py +24 -0
- db/plugins/sqlite/sqlite/connection_manager.py +162 -0
- db/plugins/sqlite/sqlite/history_manager.py +233 -0
- db/plugins/sqlite/sqlite/locking_manager.py +216 -0
- db/plugins/sqlite/sqlite/query_executor.py +243 -0
- db/plugins/sqlite/sqlite/schema_operations.py +303 -0
- db/plugins/sqlserver/__init__.py +11 -0
- db/plugins/sqlserver/introspection/__init__.py +3 -0
- db/plugins/sqlserver/parser/__init__.py +9 -0
- db/plugins/sqlserver/parser/parser_config.py +604 -0
- db/plugins/sqlserver/parser/sqlserver_regex_parser.py +368 -0
- db/plugins/sqlserver/parser/sqlserver_statement_parser.py +173 -0
- db/plugins/sqlserver/parser/sqlserver_tokenizer.py +238 -0
- db/plugins/sqlserver/parser/tsql_batch_separator.py +14 -0
- db/plugins/sqlserver/plugin.py +22 -0
- db/plugins/sqlserver/provider.py +615 -0
- db/plugins/sqlserver/quirks.py +489 -0
- db/plugins/sqlserver/sqlalchemy_url.py +120 -0
- db/plugins/sqlserver/sqlserver/__init__.py +11 -0
- db/plugins/sqlserver/sqlserver/history_manager.py +382 -0
- db/plugins/sqlserver/sqlserver/locking_manager.py +328 -0
- db/plugins/sqlserver/sqlserver/schema_operations.py +524 -0
- db/provider_capabilities.py +85 -0
- db/provider_interfaces.py +347 -0
- db/provider_registry.py +623 -0
- db/sqlalchemy_provider.py +340 -0
- db/value_utils.py +10 -0
- dblift-2.0.1.dist-info/METADATA +1075 -0
- dblift-2.0.1.dist-info/RECORD +408 -0
- dblift-2.0.1.dist-info/WHEEL +5 -0
- dblift-2.0.1.dist-info/entry_points.txt +12 -0
- dblift-2.0.1.dist-info/licenses/LICENSE +201 -0
- dblift-2.0.1.dist-info/top_level.txt +6 -0
- integrations/__init__.py +30 -0
- integrations/django/__init__.py +1 -0
- integrations/django/_client.py +54 -0
- integrations/django/_engine.py +53 -0
- integrations/django/apps.py +14 -0
- integrations/django/checks.py +35 -0
- integrations/django/management/__init__.py +1 -0
- integrations/django/management/commands/__init__.py +1 -0
- integrations/django/management/commands/dblift_info.py +25 -0
- integrations/django/management/commands/dblift_migrate.py +24 -0
- integrations/django/management/commands/dblift_validate.py +24 -0
- integrations/fastapi.py +153 -0
- integrations/flask.py +82 -0
- 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
|