velocity-python 0.1.36__tar.gz → 0.1.38__tar.gz
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.
- {velocity_python-0.1.36/src/velocity_python.egg-info → velocity_python-0.1.38}/PKG-INFO +1 -1
- {velocity_python-0.1.36 → velocity_python-0.1.38}/pyproject.toml +1 -1
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/__init__.py +1 -1
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/core/decorators.py +103 -17
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/core/table.py +476 -1
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/exceptions.py +7 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/tests/postgres/test_schema_locking.py +232 -14
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/payment/braintree_mirror.py +32 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38/src/velocity_python.egg-info}/PKG-INFO +1 -1
- velocity_python-0.1.38/tests/test_payment_braintree_mirror.py +130 -0
- velocity_python-0.1.36/tests/test_payment_braintree_mirror.py +0 -50
- {velocity_python-0.1.36 → velocity_python-0.1.38}/LICENSE +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/README.md +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/setup.cfg +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/aws/__init__.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/aws/amplify.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/aws/amplify_build.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/aws/assets/__init__.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/aws/assets/backfill.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/aws/assets/indexing.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/aws/assets/references.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/aws/assets/service.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/aws/assets/usage_index.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/aws/dirty_pipeline.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/aws/handlers/__init__.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/aws/handlers/base_handler.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/aws/handlers/context.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/aws/handlers/context_factory.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/aws/handlers/exceptions.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/aws/handlers/lambda_handler.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/aws/handlers/mixins/data_service.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/aws/handlers/perf.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/aws/handlers/response.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/aws/handlers/sqs_handler.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/aws/s3.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/aws/ssm_config.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/aws/tests/__init__.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/aws/tests/test_response.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/__init__.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/core/__init__.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/core/async_support.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/core/column.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/core/database.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/core/engine.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/core/result.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/core/row.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/core/sequence.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/core/transaction.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/core/view.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/migrations.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/servers/__init__.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/servers/base/__init__.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/servers/base/initializer.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/servers/base/operators.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/servers/base/sql.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/servers/base/types.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/servers/mysql/__init__.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/servers/mysql/operators.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/servers/mysql/reserved.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/servers/mysql/sql.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/servers/mysql/types.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/servers/postgres/__init__.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/servers/postgres/operators.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/servers/postgres/reserved.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/servers/postgres/sql.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/servers/postgres/types.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/servers/sqlite/__init__.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/servers/sqlite/operators.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/servers/sqlite/reserved.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/servers/sqlite/sql.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/servers/sqlite/types.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/servers/sqlserver/operators.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/servers/sqlserver/sql.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/servers/sqlserver/types.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/servers/tablehelper.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/tests/__init__.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/tests/common_db_test.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/tests/postgres/__init__.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/tests/postgres/common.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/tests/postgres/test_column.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/tests/postgres/test_connections.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/tests/postgres/test_database.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/tests/postgres/test_engine.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/tests/postgres/test_imports.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/tests/postgres/test_result.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/tests/postgres/test_row.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/tests/postgres/test_table.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/tests/sql/__init__.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/tests/sql/common.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/tests/test_db_utils.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/tests/test_postgres.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/tests/test_result_caching.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/tests/test_sql_builder.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/tests/test_tablehelper.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/tests/test_view_helper.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/db/utils.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/logging.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/misc/__init__.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/misc/conv/__init__.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/misc/conv/iconv.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/misc/conv/oconv.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/misc/db.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/misc/export.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/misc/format.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/misc/mail.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/misc/merge.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/misc/pdf.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/misc/tests/__init__.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/misc/tests/test_db.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/misc/tests/test_fix.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/misc/tests/test_format.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/misc/tests/test_iconv.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/misc/tests/test_merge.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/misc/tests/test_oconv.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/misc/tests/test_original_error.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/misc/tests/test_timer.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/misc/timer.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/misc/tools.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/payment/__init__.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/payment/authorizenet_adapter.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/payment/authorizenet_mirror.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/payment/base_adapter.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/payment/braintree_adapter.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/payment/charge_rules.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/payment/stripe_adapter.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity/payment/stripe_mirror.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity_python.egg-info/SOURCES.txt +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity_python.egg-info/dependency_links.txt +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity_python.egg-info/entry_points.txt +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity_python.egg-info/requires.txt +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/src/velocity_python.egg-info/top_level.txt +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/tests/test_amplify_build.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/tests/test_asset_indexing.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/tests/test_asset_references.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/tests/test_assets_service.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/tests/test_async_support.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/tests/test_batch_operations.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/tests/test_concurrency_safety.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/tests/test_connection_pool.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/tests/test_connection_resilience.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/tests/test_decorators.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/tests/test_dirty_pipeline_fast_path.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/tests/test_email_processing.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/tests/test_iconv_money_to_cents.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/tests/test_lambda_handler.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/tests/test_lambda_handler_auth.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/tests/test_mixins_import.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/tests/test_n_plus_one.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/tests/test_observability.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/tests/test_payment_authorizenet_adapter.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/tests/test_payment_braintree_adapter.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/tests/test_payment_profile_sorting.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/tests/test_payment_stripe_adapter.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/tests/test_pdf.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/tests/test_prepared_statements.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/tests/test_psycopg3_upgrade.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/tests/test_query_cache.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/tests/test_row_batch_update.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/tests/test_row_cache_staleness.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/tests/test_row_dirty_tracking.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/tests/test_schema_migrations.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/tests/test_security_hardening.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/tests/test_server_cursor.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/tests/test_spreadsheet_functions.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/tests/test_sqs_per_record_transactions.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/tests/test_sys_modified_count_postgres_demo.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/tests/test_table_alter.py +0 -0
- {velocity_python-0.1.36 → velocity_python-0.1.38}/tests/test_where_clause_validation.py +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import time
|
|
3
3
|
import random
|
|
4
|
+
from collections.abc import Mapping, Sequence
|
|
4
5
|
from functools import wraps
|
|
5
6
|
from velocity.db import exceptions
|
|
6
7
|
|
|
@@ -148,6 +149,73 @@ def return_default(
|
|
|
148
149
|
return decorator
|
|
149
150
|
|
|
150
151
|
|
|
152
|
+
def _merge_schema_seed(target, value):
|
|
153
|
+
"""Collect representative columns from dict payloads or lists of dict rows."""
|
|
154
|
+
|
|
155
|
+
if isinstance(value, Mapping):
|
|
156
|
+
for key, val in value.items():
|
|
157
|
+
if isinstance(key, str) and key not in target:
|
|
158
|
+
target[key] = val
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)):
|
|
162
|
+
for item in value:
|
|
163
|
+
if isinstance(item, Mapping):
|
|
164
|
+
_merge_schema_seed(target, item)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _collect_schema_seed(args, kwds):
|
|
168
|
+
data = {}
|
|
169
|
+
|
|
170
|
+
for key in ("pk", "data"):
|
|
171
|
+
_merge_schema_seed(data, kwds.get(key))
|
|
172
|
+
|
|
173
|
+
for arg in args:
|
|
174
|
+
_merge_schema_seed(data, arg)
|
|
175
|
+
|
|
176
|
+
return data
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _normalize_conflict_columns(pk):
|
|
180
|
+
if pk is None:
|
|
181
|
+
return []
|
|
182
|
+
if isinstance(pk, str):
|
|
183
|
+
return [pk]
|
|
184
|
+
if isinstance(pk, Mapping):
|
|
185
|
+
return [str(key) for key in pk.keys()]
|
|
186
|
+
if isinstance(pk, Sequence) and not isinstance(pk, (str, bytes, bytearray)):
|
|
187
|
+
return [str(column) for column in pk]
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
return [str(column) for column in pk]
|
|
191
|
+
except TypeError:
|
|
192
|
+
return [str(pk)]
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _infer_conflict_columns(func, args, kwds):
|
|
196
|
+
"""Infer explicit ON CONFLICT columns for upsert operations."""
|
|
197
|
+
|
|
198
|
+
if func.__name__ not in {"merge", "upsert_many"}:
|
|
199
|
+
return []
|
|
200
|
+
|
|
201
|
+
pk = kwds.get("pk")
|
|
202
|
+
if pk is None and len(args) >= 2:
|
|
203
|
+
pk = args[1]
|
|
204
|
+
|
|
205
|
+
columns = [column.lower() for column in _normalize_conflict_columns(pk) if column]
|
|
206
|
+
if len(columns) == 1 and columns[0] == "sys_id":
|
|
207
|
+
return []
|
|
208
|
+
return columns
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _missing_conflict_constraint_error(exc):
|
|
212
|
+
message = str(exc or "").lower()
|
|
213
|
+
return (
|
|
214
|
+
"no unique or exclusion constraint matching the on conflict specification"
|
|
215
|
+
in message
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
151
219
|
def create_missing(func):
|
|
152
220
|
"""
|
|
153
221
|
If the function call fails with DbColumnMissingError or DbTableMissingError,
|
|
@@ -161,6 +229,8 @@ def create_missing(func):
|
|
|
161
229
|
|
|
162
230
|
@wraps(func)
|
|
163
231
|
def wrapper(self, *args, **kwds):
|
|
232
|
+
conflict_columns = _infer_conflict_columns(func, args, kwds)
|
|
233
|
+
|
|
164
234
|
sp = self.tx.create_savepoint(cursor=self.cursor())
|
|
165
235
|
try:
|
|
166
236
|
result = func(self, *args, **kwds)
|
|
@@ -181,14 +251,7 @@ def create_missing(func):
|
|
|
181
251
|
) from e
|
|
182
252
|
|
|
183
253
|
# Existing logic for automatic creation
|
|
184
|
-
data =
|
|
185
|
-
if "pk" in kwds:
|
|
186
|
-
data.update(kwds["pk"])
|
|
187
|
-
if "data" in kwds:
|
|
188
|
-
data.update(kwds["data"])
|
|
189
|
-
for i, arg in enumerate(args):
|
|
190
|
-
if isinstance(arg, dict):
|
|
191
|
-
data.update(arg)
|
|
254
|
+
data = _collect_schema_seed(args, kwds)
|
|
192
255
|
|
|
193
256
|
# ALTER TABLE ADD COLUMN IF NOT EXISTS — acquire an advisory
|
|
194
257
|
# lock to serialize DDL across concurrent Lambda containers,
|
|
@@ -220,15 +283,7 @@ def create_missing(func):
|
|
|
220
283
|
) from e
|
|
221
284
|
|
|
222
285
|
# Existing logic for automatic creation
|
|
223
|
-
data =
|
|
224
|
-
if "pk" in kwds:
|
|
225
|
-
data.update(kwds["pk"])
|
|
226
|
-
if "data" in kwds:
|
|
227
|
-
data.update(kwds["data"])
|
|
228
|
-
for i, arg in enumerate(args):
|
|
229
|
-
if isinstance(arg, dict):
|
|
230
|
-
data.update(arg)
|
|
231
|
-
|
|
286
|
+
data = _collect_schema_seed(args, kwds)
|
|
232
287
|
# CREATE TABLE IF NOT EXISTS — acquire advisory lock then
|
|
233
288
|
# guard against concurrent creation with a savepoint.
|
|
234
289
|
try:
|
|
@@ -242,6 +297,37 @@ def create_missing(func):
|
|
|
242
297
|
except exceptions.DbObjectExistsError:
|
|
243
298
|
self.tx.rollback_savepoint(sp2, cursor=self.cursor())
|
|
244
299
|
|
|
300
|
+
if conflict_columns:
|
|
301
|
+
self.create_index(conflict_columns, unique=True)
|
|
302
|
+
|
|
303
|
+
return func(self, *args, **kwds)
|
|
304
|
+
except exceptions.DbException as e:
|
|
305
|
+
self.tx.rollback_savepoint(sp, cursor=self.cursor())
|
|
306
|
+
|
|
307
|
+
if not conflict_columns or not _missing_conflict_constraint_error(e):
|
|
308
|
+
raise
|
|
309
|
+
|
|
310
|
+
if self.tx.engine.schema_locked:
|
|
311
|
+
logger.warning(
|
|
312
|
+
"@create_missing triggered on locked schema: table=%s error=%s",
|
|
313
|
+
self.name, e,
|
|
314
|
+
extra={
|
|
315
|
+
"table_name": self.name,
|
|
316
|
+
"operation": "create_missing_conflict_index",
|
|
317
|
+
"schema_locked": True,
|
|
318
|
+
},
|
|
319
|
+
)
|
|
320
|
+
raise exceptions.DbSchemaLockedError(
|
|
321
|
+
"Cannot create missing unique index: schema is locked. "
|
|
322
|
+
f"Original error: {e}"
|
|
323
|
+
) from e
|
|
324
|
+
|
|
325
|
+
try:
|
|
326
|
+
self.tx.advisory_lock(f"velocity_ddl_{self.name}")
|
|
327
|
+
except Exception:
|
|
328
|
+
pass
|
|
329
|
+
|
|
330
|
+
self.create_index(conflict_columns, unique=True)
|
|
245
331
|
return func(self, *args, **kwds)
|
|
246
332
|
|
|
247
333
|
return wrapper
|
|
@@ -2,7 +2,8 @@ import logging
|
|
|
2
2
|
import re
|
|
3
3
|
import warnings
|
|
4
4
|
import sqlparse
|
|
5
|
-
from collections
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
from collections.abc import Iterable, Mapping, Sequence
|
|
6
7
|
from velocity.db import exceptions
|
|
7
8
|
from velocity.db.core.row import Row
|
|
8
9
|
from velocity.db.core.result import Result
|
|
@@ -150,6 +151,198 @@ def _parse_column_spec(spec, default_nullable):
|
|
|
150
151
|
}
|
|
151
152
|
|
|
152
153
|
|
|
154
|
+
def _normalize_identifier(value):
|
|
155
|
+
return str(value or "").strip().strip('"').lower()
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _split_sql_expressions(text):
|
|
159
|
+
expressions = []
|
|
160
|
+
current = []
|
|
161
|
+
depth = 0
|
|
162
|
+
|
|
163
|
+
for char in str(text or ""):
|
|
164
|
+
if char == "(":
|
|
165
|
+
depth += 1
|
|
166
|
+
elif char == ")" and depth > 0:
|
|
167
|
+
depth -= 1
|
|
168
|
+
|
|
169
|
+
if char == "," and depth == 0:
|
|
170
|
+
expression = "".join(current).strip()
|
|
171
|
+
if expression:
|
|
172
|
+
expressions.append(expression)
|
|
173
|
+
current = []
|
|
174
|
+
continue
|
|
175
|
+
|
|
176
|
+
current.append(char)
|
|
177
|
+
|
|
178
|
+
tail = "".join(current).strip()
|
|
179
|
+
if tail:
|
|
180
|
+
expressions.append(tail)
|
|
181
|
+
|
|
182
|
+
return expressions
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _extract_parenthesized(text, start_index):
|
|
186
|
+
depth = 0
|
|
187
|
+
current = []
|
|
188
|
+
|
|
189
|
+
for index in range(start_index, len(text)):
|
|
190
|
+
char = text[index]
|
|
191
|
+
if char == "(":
|
|
192
|
+
if depth > 0:
|
|
193
|
+
current.append(char)
|
|
194
|
+
depth += 1
|
|
195
|
+
continue
|
|
196
|
+
if char == ")":
|
|
197
|
+
depth -= 1
|
|
198
|
+
if depth == 0:
|
|
199
|
+
return "".join(current), index
|
|
200
|
+
current.append(char)
|
|
201
|
+
continue
|
|
202
|
+
if depth > 0:
|
|
203
|
+
current.append(char)
|
|
204
|
+
|
|
205
|
+
raise ValueError(f"Unbalanced parentheses in SQL fragment: {text}")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _normalize_index_expression(expression):
|
|
209
|
+
normalized = str(expression or "").strip().replace('"', "")
|
|
210
|
+
normalized = re.sub(r"::[a-zA-Z0-9_\[\]\s\.]+", "", normalized)
|
|
211
|
+
normalized = re.sub(r"\s+", " ", normalized)
|
|
212
|
+
normalized = re.sub(r"\(\s+", "(", normalized)
|
|
213
|
+
normalized = re.sub(r"\s+\)", ")", normalized)
|
|
214
|
+
return normalized.strip().lower()
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _parse_index_signature(index_sql):
|
|
218
|
+
normalized_sql = " ".join(str(index_sql or "").split())
|
|
219
|
+
upper_sql = normalized_sql.upper()
|
|
220
|
+
|
|
221
|
+
name_match = re.search(
|
|
222
|
+
r"CREATE(?:\s+UNIQUE)?\s+INDEX\s+([^\s]+)\s+ON\s+",
|
|
223
|
+
normalized_sql,
|
|
224
|
+
flags=re.IGNORECASE,
|
|
225
|
+
)
|
|
226
|
+
index_name = _normalize_identifier(name_match.group(1)) if name_match else None
|
|
227
|
+
|
|
228
|
+
on_pos = upper_sql.find(" ON ")
|
|
229
|
+
if on_pos == -1:
|
|
230
|
+
raise ValueError(f"Unable to parse index definition: {index_sql}")
|
|
231
|
+
|
|
232
|
+
open_paren = normalized_sql.find("(", on_pos)
|
|
233
|
+
if open_paren == -1:
|
|
234
|
+
raise ValueError(f"Unable to parse index columns: {index_sql}")
|
|
235
|
+
|
|
236
|
+
columns_text, closing_index = _extract_parenthesized(normalized_sql, open_paren)
|
|
237
|
+
where_clause = None
|
|
238
|
+
where_pos = upper_sql.find(" WHERE ", closing_index)
|
|
239
|
+
if where_pos != -1:
|
|
240
|
+
where_clause = _normalize_index_expression(normalized_sql[where_pos + 7 :])
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
"name": index_name,
|
|
244
|
+
"unique": "CREATE UNIQUE INDEX" in upper_sql,
|
|
245
|
+
"columns": tuple(
|
|
246
|
+
_normalize_index_expression(expression)
|
|
247
|
+
for expression in _split_sql_expressions(columns_text)
|
|
248
|
+
),
|
|
249
|
+
"where": where_clause,
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _normalize_index_specs(indexes, *, unique_default=False):
|
|
254
|
+
if indexes is None:
|
|
255
|
+
return []
|
|
256
|
+
|
|
257
|
+
if isinstance(indexes, (str, bytes, Mapping)):
|
|
258
|
+
indexes = [indexes]
|
|
259
|
+
|
|
260
|
+
normalized = []
|
|
261
|
+
for definition in indexes:
|
|
262
|
+
if isinstance(definition, Mapping):
|
|
263
|
+
columns = definition.get("columns")
|
|
264
|
+
if not columns:
|
|
265
|
+
raise ValueError("Index definition requires non-empty columns")
|
|
266
|
+
unique = bool(definition.get("unique", unique_default))
|
|
267
|
+
direction = definition.get("direction")
|
|
268
|
+
where = definition.get("where")
|
|
269
|
+
lower = definition.get("lower")
|
|
270
|
+
else:
|
|
271
|
+
columns = definition
|
|
272
|
+
unique = unique_default
|
|
273
|
+
direction = None
|
|
274
|
+
where = None
|
|
275
|
+
lower = None
|
|
276
|
+
|
|
277
|
+
if isinstance(columns, str):
|
|
278
|
+
columns = [column.strip() for column in columns.split(",") if column.strip()]
|
|
279
|
+
elif isinstance(columns, Sequence):
|
|
280
|
+
columns = list(columns)
|
|
281
|
+
else:
|
|
282
|
+
columns = [columns]
|
|
283
|
+
|
|
284
|
+
normalized.append(
|
|
285
|
+
{
|
|
286
|
+
"columns": [_normalize_identifier(column) for column in columns],
|
|
287
|
+
"unique": unique,
|
|
288
|
+
"direction": direction,
|
|
289
|
+
"where": where,
|
|
290
|
+
"lower": lower,
|
|
291
|
+
}
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
return normalized
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _normalize_foreign_key_specs(foreign_keys):
|
|
298
|
+
if foreign_keys is None:
|
|
299
|
+
return []
|
|
300
|
+
|
|
301
|
+
if isinstance(foreign_keys, Mapping):
|
|
302
|
+
foreign_keys = [foreign_keys]
|
|
303
|
+
|
|
304
|
+
normalized = []
|
|
305
|
+
for definition in foreign_keys:
|
|
306
|
+
if not isinstance(definition, Mapping):
|
|
307
|
+
raise TypeError("foreign_keys entries must be mappings")
|
|
308
|
+
|
|
309
|
+
columns = definition.get("columns") or definition.get("column")
|
|
310
|
+
if not columns:
|
|
311
|
+
raise ValueError("Foreign key definition requires columns")
|
|
312
|
+
|
|
313
|
+
ref_table = definition.get("ref_table") or definition.get("key_to_table")
|
|
314
|
+
if not ref_table:
|
|
315
|
+
raise ValueError("Foreign key definition requires ref_table")
|
|
316
|
+
|
|
317
|
+
ref_columns = definition.get("ref_columns") or definition.get("key_to_columns") or "sys_id"
|
|
318
|
+
|
|
319
|
+
if isinstance(columns, str):
|
|
320
|
+
columns = [column.strip() for column in columns.split(",") if column.strip()]
|
|
321
|
+
elif isinstance(columns, Sequence):
|
|
322
|
+
columns = list(columns)
|
|
323
|
+
else:
|
|
324
|
+
columns = [columns]
|
|
325
|
+
|
|
326
|
+
if isinstance(ref_columns, str):
|
|
327
|
+
ref_columns = [column.strip() for column in ref_columns.split(",") if column.strip()]
|
|
328
|
+
elif isinstance(ref_columns, Sequence):
|
|
329
|
+
ref_columns = list(ref_columns)
|
|
330
|
+
else:
|
|
331
|
+
ref_columns = [ref_columns]
|
|
332
|
+
|
|
333
|
+
normalized.append(
|
|
334
|
+
{
|
|
335
|
+
"columns": tuple(_normalize_identifier(column) for column in columns),
|
|
336
|
+
"ref_table": _normalize_identifier(ref_table),
|
|
337
|
+
"ref_columns": tuple(
|
|
338
|
+
_normalize_identifier(column) for column in ref_columns
|
|
339
|
+
),
|
|
340
|
+
}
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
return normalized
|
|
344
|
+
|
|
345
|
+
|
|
153
346
|
class Table:
|
|
154
347
|
SYSTEM_COLUMNS = SYSTEM_COLUMN_NAMES
|
|
155
348
|
|
|
@@ -343,6 +536,288 @@ class Table:
|
|
|
343
536
|
_ddl_logger.warning("DDL CREATE TABLE %s columns=%s drop=%s", self.name, list(columns.keys()), drop)
|
|
344
537
|
self.tx.execute(sql, vals, cursor=self.cursor())
|
|
345
538
|
|
|
539
|
+
def _ensure_schema_unlocked(self, operation):
|
|
540
|
+
if self.tx.engine.schema_locked:
|
|
541
|
+
raise exceptions.DbSchemaLockedError(
|
|
542
|
+
f"Cannot {operation}: schema is locked for table '{self.name}'."
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
def _existing_index_signatures(self):
|
|
546
|
+
signatures = []
|
|
547
|
+
for name, _table_name, _schema_name, indexdef in self.indexes().as_tuple().all():
|
|
548
|
+
signature = _parse_index_signature(indexdef)
|
|
549
|
+
signature["name"] = _normalize_identifier(name)
|
|
550
|
+
signature["sql"] = indexdef
|
|
551
|
+
signatures.append(signature)
|
|
552
|
+
return signatures
|
|
553
|
+
|
|
554
|
+
def _desired_index_signature(self, spec):
|
|
555
|
+
sql, _vals = self.create_index(
|
|
556
|
+
spec["columns"],
|
|
557
|
+
unique=spec["unique"],
|
|
558
|
+
direction=spec["direction"],
|
|
559
|
+
where=spec["where"],
|
|
560
|
+
lower=spec["lower"],
|
|
561
|
+
sql_only=True,
|
|
562
|
+
)
|
|
563
|
+
return _parse_index_signature(sql)
|
|
564
|
+
|
|
565
|
+
def _existing_foreign_key_signatures(self):
|
|
566
|
+
sql, vals = self.sql.foreign_key_info(table=self.name, column=None)
|
|
567
|
+
rows = self.tx.execute(sql, vals, cursor=self.cursor()).as_dict().all()
|
|
568
|
+
grouped = defaultdict(list)
|
|
569
|
+
for row in rows:
|
|
570
|
+
grouped[_normalize_identifier(row["fk_constraint_name"])].append(row)
|
|
571
|
+
|
|
572
|
+
signatures = []
|
|
573
|
+
for constraint_name, constraint_rows in grouped.items():
|
|
574
|
+
ordered = sorted(
|
|
575
|
+
constraint_rows,
|
|
576
|
+
key=lambda item: item.get("fk_ordinal_position") or 0,
|
|
577
|
+
)
|
|
578
|
+
first = ordered[0]
|
|
579
|
+
ref_schema = _normalize_identifier(first.get("referenced_table_schema"))
|
|
580
|
+
ref_table = _normalize_identifier(first.get("referenced_table_name"))
|
|
581
|
+
full_ref_table = (
|
|
582
|
+
f"{ref_schema}.{ref_table}"
|
|
583
|
+
if ref_schema and ref_schema not in {"public", ""}
|
|
584
|
+
else ref_table
|
|
585
|
+
)
|
|
586
|
+
signatures.append(
|
|
587
|
+
{
|
|
588
|
+
"constraint_name": constraint_name,
|
|
589
|
+
"columns": tuple(
|
|
590
|
+
_normalize_identifier(item["fk_column_name"]) for item in ordered
|
|
591
|
+
),
|
|
592
|
+
"ref_table": full_ref_table,
|
|
593
|
+
"ref_columns": tuple(
|
|
594
|
+
_normalize_identifier(item["referenced_column_name"])
|
|
595
|
+
for item in ordered
|
|
596
|
+
),
|
|
597
|
+
}
|
|
598
|
+
)
|
|
599
|
+
return signatures
|
|
600
|
+
|
|
601
|
+
def ensure_schema(
|
|
602
|
+
self,
|
|
603
|
+
columns=None,
|
|
604
|
+
unique_indexes=None,
|
|
605
|
+
indexes=None,
|
|
606
|
+
foreign_keys=None,
|
|
607
|
+
create_missing=True,
|
|
608
|
+
alter_missing_columns=True,
|
|
609
|
+
create_missing_indexes=True,
|
|
610
|
+
create_missing_foreign_keys=False,
|
|
611
|
+
ensure_system_columns=False,
|
|
612
|
+
on_existing_conflicts="raise",
|
|
613
|
+
):
|
|
614
|
+
"""Create or align safe schema objects for this table.
|
|
615
|
+
|
|
616
|
+
This is an idempotent helper intended for deploy-time or startup schema
|
|
617
|
+
sync. By default it will create missing tables, add missing columns, and
|
|
618
|
+
create declared indexes. It does not drop objects or rewrite existing
|
|
619
|
+
column types implicitly.
|
|
620
|
+
"""
|
|
621
|
+
|
|
622
|
+
if on_existing_conflicts not in {"raise", "ignore"}:
|
|
623
|
+
raise ValueError(
|
|
624
|
+
"on_existing_conflicts must be either 'raise' or 'ignore'."
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
if columns is not None and not isinstance(columns, Mapping):
|
|
628
|
+
raise TypeError("columns must be a mapping when provided.")
|
|
629
|
+
|
|
630
|
+
normalized_columns = self.lower_keys(columns or {})
|
|
631
|
+
normalized_indexes = _normalize_index_specs(indexes)
|
|
632
|
+
normalized_unique_indexes = _normalize_index_specs(
|
|
633
|
+
unique_indexes, unique_default=True
|
|
634
|
+
)
|
|
635
|
+
normalized_foreign_keys = _normalize_foreign_key_specs(foreign_keys)
|
|
636
|
+
|
|
637
|
+
summary = {
|
|
638
|
+
"created_table": False,
|
|
639
|
+
"added_columns": [],
|
|
640
|
+
"created_indexes": [],
|
|
641
|
+
"skipped_indexes": [],
|
|
642
|
+
"created_foreign_keys": [],
|
|
643
|
+
"skipped_foreign_keys": [],
|
|
644
|
+
"ensured_system_columns": False,
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
table_exists = self.exists()
|
|
648
|
+
if not table_exists:
|
|
649
|
+
if not create_missing:
|
|
650
|
+
raise exceptions.DbTableMissingError(
|
|
651
|
+
f"Table '{self.name}' does not exist and create_missing=False."
|
|
652
|
+
)
|
|
653
|
+
self._ensure_schema_unlocked("create missing table")
|
|
654
|
+
self.create(normalized_columns)
|
|
655
|
+
table_exists = True
|
|
656
|
+
summary["created_table"] = True
|
|
657
|
+
|
|
658
|
+
existing_columns = {column.lower() for column in self.sys_columns()}
|
|
659
|
+
if normalized_columns and alter_missing_columns:
|
|
660
|
+
missing_columns = {
|
|
661
|
+
key: value
|
|
662
|
+
for key, value in normalized_columns.items()
|
|
663
|
+
if key.lower() not in existing_columns
|
|
664
|
+
}
|
|
665
|
+
if missing_columns:
|
|
666
|
+
self._ensure_schema_unlocked("add missing columns")
|
|
667
|
+
self.alter(missing_columns, mode="add")
|
|
668
|
+
summary["added_columns"] = list(missing_columns.keys())
|
|
669
|
+
existing_columns.update(missing_columns.keys())
|
|
670
|
+
|
|
671
|
+
if ensure_system_columns:
|
|
672
|
+
required_system_columns = {name.lower() for name in self.SYSTEM_COLUMNS}
|
|
673
|
+
if not required_system_columns.issubset(existing_columns):
|
|
674
|
+
self._ensure_schema_unlocked("ensure system columns")
|
|
675
|
+
self.ensure_system_columns()
|
|
676
|
+
summary["ensured_system_columns"] = True
|
|
677
|
+
|
|
678
|
+
existing_indexes = self._existing_index_signatures()
|
|
679
|
+
index_specs = normalized_unique_indexes + normalized_indexes
|
|
680
|
+
for spec in index_specs:
|
|
681
|
+
desired_signature = self._desired_index_signature(spec)
|
|
682
|
+
exact_match = next(
|
|
683
|
+
(
|
|
684
|
+
existing
|
|
685
|
+
for existing in existing_indexes
|
|
686
|
+
if existing["unique"] == desired_signature["unique"]
|
|
687
|
+
and existing["columns"] == desired_signature["columns"]
|
|
688
|
+
and existing["where"] == desired_signature["where"]
|
|
689
|
+
),
|
|
690
|
+
None,
|
|
691
|
+
)
|
|
692
|
+
if exact_match:
|
|
693
|
+
continue
|
|
694
|
+
|
|
695
|
+
name_conflict = next(
|
|
696
|
+
(
|
|
697
|
+
existing
|
|
698
|
+
for existing in existing_indexes
|
|
699
|
+
if existing["name"] == desired_signature["name"]
|
|
700
|
+
),
|
|
701
|
+
None,
|
|
702
|
+
)
|
|
703
|
+
if name_conflict:
|
|
704
|
+
message = (
|
|
705
|
+
f"Index '{desired_signature['name']}' on table '{self.name}' exists "
|
|
706
|
+
"with a different definition."
|
|
707
|
+
)
|
|
708
|
+
if on_existing_conflicts == "raise":
|
|
709
|
+
raise exceptions.DbSchemaConflictError(message)
|
|
710
|
+
summary["skipped_indexes"].append(
|
|
711
|
+
{
|
|
712
|
+
"columns": list(spec["columns"]),
|
|
713
|
+
"reason": message,
|
|
714
|
+
}
|
|
715
|
+
)
|
|
716
|
+
continue
|
|
717
|
+
|
|
718
|
+
if not create_missing_indexes:
|
|
719
|
+
summary["skipped_indexes"].append(
|
|
720
|
+
{
|
|
721
|
+
"columns": list(spec["columns"]),
|
|
722
|
+
"reason": "create_missing_indexes is disabled",
|
|
723
|
+
}
|
|
724
|
+
)
|
|
725
|
+
continue
|
|
726
|
+
|
|
727
|
+
self._ensure_schema_unlocked("create missing indexes")
|
|
728
|
+
try:
|
|
729
|
+
self.create_index(
|
|
730
|
+
spec["columns"],
|
|
731
|
+
unique=spec["unique"],
|
|
732
|
+
direction=spec["direction"],
|
|
733
|
+
where=spec["where"],
|
|
734
|
+
lower=spec["lower"],
|
|
735
|
+
)
|
|
736
|
+
summary["created_indexes"].append(
|
|
737
|
+
{
|
|
738
|
+
"columns": list(spec["columns"]),
|
|
739
|
+
"unique": spec["unique"],
|
|
740
|
+
}
|
|
741
|
+
)
|
|
742
|
+
existing_indexes = self._existing_index_signatures()
|
|
743
|
+
except exceptions.DbDuplicateKeyError as exc:
|
|
744
|
+
message = (
|
|
745
|
+
f"Cannot create unique index on table '{self.name}' for columns "
|
|
746
|
+
f"{spec['columns']}: existing rows violate uniqueness."
|
|
747
|
+
)
|
|
748
|
+
if on_existing_conflicts == "raise":
|
|
749
|
+
raise exceptions.DbSchemaConflictError(message) from exc
|
|
750
|
+
summary["skipped_indexes"].append(
|
|
751
|
+
{
|
|
752
|
+
"columns": list(spec["columns"]),
|
|
753
|
+
"reason": message,
|
|
754
|
+
}
|
|
755
|
+
)
|
|
756
|
+
|
|
757
|
+
existing_foreign_keys = self._existing_foreign_key_signatures()
|
|
758
|
+
for spec in normalized_foreign_keys:
|
|
759
|
+
exact_match = next(
|
|
760
|
+
(
|
|
761
|
+
existing
|
|
762
|
+
for existing in existing_foreign_keys
|
|
763
|
+
if existing["columns"] == spec["columns"]
|
|
764
|
+
and existing["ref_table"] == spec["ref_table"]
|
|
765
|
+
and existing["ref_columns"] == spec["ref_columns"]
|
|
766
|
+
),
|
|
767
|
+
None,
|
|
768
|
+
)
|
|
769
|
+
if exact_match:
|
|
770
|
+
continue
|
|
771
|
+
|
|
772
|
+
column_conflict = next(
|
|
773
|
+
(
|
|
774
|
+
existing
|
|
775
|
+
for existing in existing_foreign_keys
|
|
776
|
+
if existing["columns"] == spec["columns"]
|
|
777
|
+
),
|
|
778
|
+
None,
|
|
779
|
+
)
|
|
780
|
+
if column_conflict:
|
|
781
|
+
message = (
|
|
782
|
+
f"Foreign key on table '{self.name}' for columns {spec['columns']} "
|
|
783
|
+
"exists with a different target."
|
|
784
|
+
)
|
|
785
|
+
if on_existing_conflicts == "raise":
|
|
786
|
+
raise exceptions.DbSchemaConflictError(message)
|
|
787
|
+
summary["skipped_foreign_keys"].append(
|
|
788
|
+
{
|
|
789
|
+
"columns": list(spec["columns"]),
|
|
790
|
+
"reason": message,
|
|
791
|
+
}
|
|
792
|
+
)
|
|
793
|
+
continue
|
|
794
|
+
|
|
795
|
+
if not create_missing_foreign_keys:
|
|
796
|
+
summary["skipped_foreign_keys"].append(
|
|
797
|
+
{
|
|
798
|
+
"columns": list(spec["columns"]),
|
|
799
|
+
"reason": "create_missing_foreign_keys is disabled",
|
|
800
|
+
}
|
|
801
|
+
)
|
|
802
|
+
continue
|
|
803
|
+
|
|
804
|
+
self._ensure_schema_unlocked("create missing foreign keys")
|
|
805
|
+
self.create_foreign_key(
|
|
806
|
+
list(spec["columns"]),
|
|
807
|
+
spec["ref_table"],
|
|
808
|
+
list(spec["ref_columns"]),
|
|
809
|
+
)
|
|
810
|
+
summary["created_foreign_keys"].append(
|
|
811
|
+
{
|
|
812
|
+
"columns": list(spec["columns"]),
|
|
813
|
+
"ref_table": spec["ref_table"],
|
|
814
|
+
"ref_columns": list(spec["ref_columns"]),
|
|
815
|
+
}
|
|
816
|
+
)
|
|
817
|
+
existing_foreign_keys = self._existing_foreign_key_signatures()
|
|
818
|
+
|
|
819
|
+
return summary
|
|
820
|
+
|
|
346
821
|
def drop(self):
|
|
347
822
|
"""
|
|
348
823
|
Drops this table if it exists.
|
|
@@ -99,6 +99,12 @@ class DbSchemaLockedError(DbApplicationError):
|
|
|
99
99
|
pass
|
|
100
100
|
|
|
101
101
|
|
|
102
|
+
class DbSchemaConflictError(DbApplicationError):
|
|
103
|
+
"""Raised when declared schema cannot be reconciled safely."""
|
|
104
|
+
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
|
|
102
108
|
class DuplicateRowsFoundError(Exception):
|
|
103
109
|
"""Multiple rows found when expecting single result."""
|
|
104
110
|
|
|
@@ -132,5 +138,6 @@ __all__ = [
|
|
|
132
138
|
"DbQueryError",
|
|
133
139
|
"DbTransactionError",
|
|
134
140
|
"DbSchemaLockedError",
|
|
141
|
+
"DbSchemaConflictError",
|
|
135
142
|
"DuplicateRowsFoundError",
|
|
136
143
|
]
|