velocity-python 0.1.10__tar.gz → 0.1.12__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.10/src/velocity_python.egg-info → velocity_python-0.1.12}/PKG-INFO +1 -1
- {velocity_python-0.1.10 → velocity_python-0.1.12}/pyproject.toml +1 -1
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/__init__.py +1 -1
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/core/row.py +65 -4
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/migrations.py +26 -38
- {velocity_python-0.1.10 → velocity_python-0.1.12/src/velocity_python.egg-info}/PKG-INFO +1 -1
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity_python.egg-info/SOURCES.txt +1 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_row_batch_update.py +2 -0
- velocity_python-0.1.12/tests/test_row_dirty_tracking.py +193 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_schema_migrations.py +37 -38
- {velocity_python-0.1.10 → velocity_python-0.1.12}/LICENSE +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/README.md +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/setup.cfg +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/app/__init__.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/app/formbuilder/__init__.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/app/formbuilder/reshaper.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/app/invoices.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/app/orders.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/app/payments.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/app/purchase_orders.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/app/tests/__init__.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/app/tests/test_email_processing.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/app/tests/test_payment_profile_sorting.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/app/tests/test_spreadsheet_functions.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/app/validators/__init__.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/app/validators/formbuilder_template.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/aws/__init__.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/aws/amplify.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/aws/amplify_build.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/aws/handlers/__init__.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/aws/handlers/base_handler.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/aws/handlers/context.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/aws/handlers/context_factory.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/aws/handlers/exceptions.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/aws/handlers/lambda_handler.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/aws/handlers/mixins/data_service.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/aws/handlers/perf.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/aws/handlers/response.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/aws/handlers/sqs_handler.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/aws/tests/__init__.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/aws/tests/test_response.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/__init__.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/core/__init__.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/core/async_support.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/core/column.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/core/database.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/core/decorators.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/core/engine.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/core/result.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/core/sequence.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/core/table.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/core/transaction.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/core/view.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/exceptions.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/__init__.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/base/__init__.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/base/initializer.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/base/operators.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/base/sql.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/base/types.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/mysql/__init__.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/mysql/operators.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/mysql/reserved.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/mysql/sql.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/mysql/types.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/postgres/__init__.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/postgres/operators.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/postgres/reserved.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/postgres/sql.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/postgres/types.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/sqlite/__init__.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/sqlite/operators.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/sqlite/reserved.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/sqlite/sql.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/sqlite/types.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/sqlserver/operators.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/sqlserver/sql.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/sqlserver/types.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/tablehelper.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/__init__.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/common_db_test.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/postgres/__init__.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/postgres/common.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/postgres/test_column.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/postgres/test_connections.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/postgres/test_database.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/postgres/test_engine.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/postgres/test_imports.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/postgres/test_result.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/postgres/test_row.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/postgres/test_table.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/sql/__init__.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/sql/common.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/test_db_utils.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/test_postgres.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/test_result_caching.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/test_sql_builder.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/test_tablehelper.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/test_view_helper.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/utils.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/logging.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/misc/__init__.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/misc/conv/__init__.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/misc/conv/iconv.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/misc/conv/oconv.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/misc/db.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/misc/export.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/misc/format.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/misc/mail.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/misc/merge.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/misc/tests/__init__.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/misc/tests/test_db.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/misc/tests/test_fix.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/misc/tests/test_format.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/misc/tests/test_iconv.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/misc/tests/test_merge.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/misc/tests/test_oconv.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/misc/tests/test_original_error.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/misc/tests/test_timer.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/misc/timer.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/misc/tools.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/payment/__init__.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/payment/authorizenet_adapter.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/payment/base_adapter.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/payment/braintree_adapter.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/payment/charge_rules.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/payment/demo_profiles.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/payment/profiles.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/payment/router.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/payment/stripe_adapter.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity_python.egg-info/dependency_links.txt +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity_python.egg-info/entry_points.txt +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity_python.egg-info/requires.txt +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity_python.egg-info/top_level.txt +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_amplify_build.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_async_support.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_batch_operations.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_concurrency_safety.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_connection_pool.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_connection_resilience.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_decorators.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_formbuilder_reshaper.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_formbuilder_template_validator.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_iconv_money_to_cents.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_lambda_handler.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_lambda_handler_auth.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_mixins_import.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_n_plus_one.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_observability.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_payment_braintree_adapter.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_payment_demo_profiles.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_payment_profiles.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_payment_router.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_payment_stripe_adapter.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_prepared_statements.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_psycopg3_upgrade.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_query_cache.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_row_cache_staleness.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_security_hardening.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_sqs_per_record_transactions.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_sys_modified_count_postgres_demo.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_table_alter.py +0 -0
- {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_where_clause_validation.py +0 -0
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import pprint
|
|
2
2
|
import time as _time
|
|
3
|
-
import warnings
|
|
4
3
|
from collections.abc import MutableMapping
|
|
5
4
|
from velocity.db.exceptions import DbColumnMissingError
|
|
6
5
|
|
|
@@ -9,7 +8,7 @@ from velocity.db.exceptions import DbColumnMissingError
|
|
|
9
8
|
# intercepted by __getattr__ / __setattr__.
|
|
10
9
|
_INTERNAL_ATTRS = frozenset({
|
|
11
10
|
"table", "pk", "_cache", "_column_set", "_batching", "_pending",
|
|
12
|
-
"_cache_ttl", "_cache_time", "_no_cache",
|
|
11
|
+
"_cache_ttl", "_cache_time", "_no_cache", "_dirty_tracking", "_dirty",
|
|
13
12
|
})
|
|
14
13
|
|
|
15
14
|
|
|
@@ -25,7 +24,7 @@ class Row(MutableMapping):
|
|
|
25
24
|
write-through (immediate UPDATE) and also update the local cache.
|
|
26
25
|
"""
|
|
27
26
|
|
|
28
|
-
def __init__(self, table, key, lock=None, cache_ttl=None, no_cache=False):
|
|
27
|
+
def __init__(self, table, key, lock=None, cache_ttl=None, no_cache=False, dirty_tracking=False):
|
|
29
28
|
if isinstance(table, str):
|
|
30
29
|
raise Exception("Table parameter must be a `table` instance.")
|
|
31
30
|
object.__setattr__(self, "table", table)
|
|
@@ -48,6 +47,8 @@ class Row(MutableMapping):
|
|
|
48
47
|
object.__setattr__(self, "_cache_ttl", cache_ttl)
|
|
49
48
|
object.__setattr__(self, "_cache_time", None)
|
|
50
49
|
object.__setattr__(self, "_no_cache", no_cache)
|
|
50
|
+
object.__setattr__(self, "_dirty_tracking", dirty_tracking)
|
|
51
|
+
object.__setattr__(self, "_dirty", {})
|
|
51
52
|
if lock:
|
|
52
53
|
self.lock()
|
|
53
54
|
|
|
@@ -78,6 +79,8 @@ class Row(MutableMapping):
|
|
|
78
79
|
object.__setattr__(row, "_cache_ttl", None)
|
|
79
80
|
object.__setattr__(row, "_cache_time", _time.monotonic())
|
|
80
81
|
object.__setattr__(row, "_no_cache", False)
|
|
82
|
+
object.__setattr__(row, "_dirty_tracking", False)
|
|
83
|
+
object.__setattr__(row, "_dirty", {})
|
|
81
84
|
return row
|
|
82
85
|
|
|
83
86
|
# ------------------------------------------------------------------
|
|
@@ -144,6 +147,13 @@ class Row(MutableMapping):
|
|
|
144
147
|
self._cache[key] = val
|
|
145
148
|
object.__setattr__(self, "_column_set", None)
|
|
146
149
|
return
|
|
150
|
+
if self._dirty_tracking:
|
|
151
|
+
self._dirty[key] = val
|
|
152
|
+
# Update local cache optimistically so reads see the dirty value
|
|
153
|
+
if self._cache is not None:
|
|
154
|
+
self._cache[key] = val
|
|
155
|
+
object.__setattr__(self, "_column_set", None)
|
|
156
|
+
return
|
|
147
157
|
self.table.update_or_insert({key: val}, pk=self.pk)
|
|
148
158
|
# Invalidate cache so trigger-computed columns are re-fetched
|
|
149
159
|
object.__setattr__(self, "_cache", None)
|
|
@@ -394,6 +404,55 @@ class Row(MutableMapping):
|
|
|
394
404
|
"""
|
|
395
405
|
return _BatchContext(self)
|
|
396
406
|
|
|
407
|
+
def save(self):
|
|
408
|
+
"""Flush accumulated dirty-tracking changes to the database.
|
|
409
|
+
|
|
410
|
+
When ``dirty_tracking=True``, assignments to the row accumulate
|
|
411
|
+
in memory instead of writing through immediately. Call
|
|
412
|
+
``.save()`` to flush them all in a single UPDATE::
|
|
413
|
+
|
|
414
|
+
row = Row(table, 1, dirty_tracking=True)
|
|
415
|
+
row["name"] = "John"
|
|
416
|
+
row["email"] = "john@example.com"
|
|
417
|
+
row.save() # Single UPDATE with both columns
|
|
418
|
+
|
|
419
|
+
Returns ``self`` for chaining.
|
|
420
|
+
|
|
421
|
+
Raises:
|
|
422
|
+
RuntimeError: If dirty tracking is not enabled on this row.
|
|
423
|
+
"""
|
|
424
|
+
if not self._dirty_tracking:
|
|
425
|
+
raise RuntimeError(
|
|
426
|
+
"save() requires dirty_tracking=True. "
|
|
427
|
+
"Use Row(table, key, dirty_tracking=True) or row.update({...})."
|
|
428
|
+
)
|
|
429
|
+
dirty = self._dirty
|
|
430
|
+
object.__setattr__(self, "_dirty", {})
|
|
431
|
+
if dirty:
|
|
432
|
+
self.table.update_or_insert(dirty, pk=self.pk)
|
|
433
|
+
# Invalidate cache so trigger-computed columns are re-fetched
|
|
434
|
+
object.__setattr__(self, "_cache", None)
|
|
435
|
+
object.__setattr__(self, "_column_set", None)
|
|
436
|
+
return self
|
|
437
|
+
|
|
438
|
+
@property
|
|
439
|
+
def is_dirty(self):
|
|
440
|
+
"""Return True if there are unsaved dirty-tracking changes."""
|
|
441
|
+
return bool(self._dirty)
|
|
442
|
+
|
|
443
|
+
def discard(self):
|
|
444
|
+
"""Discard accumulated dirty-tracking changes without writing to DB.
|
|
445
|
+
|
|
446
|
+
Also invalidates the cache so the next read re-fetches from the
|
|
447
|
+
database, undoing any optimistic cache updates.
|
|
448
|
+
|
|
449
|
+
Returns ``self`` for chaining.
|
|
450
|
+
"""
|
|
451
|
+
object.__setattr__(self, "_dirty", {})
|
|
452
|
+
object.__setattr__(self, "_cache", None)
|
|
453
|
+
object.__setattr__(self, "_column_set", None)
|
|
454
|
+
return self
|
|
455
|
+
|
|
397
456
|
def touch(self):
|
|
398
457
|
"""
|
|
399
458
|
Update sys_modified to current timestamp.
|
|
@@ -473,5 +532,7 @@ class _BatchContext:
|
|
|
473
532
|
|
|
474
533
|
if pending:
|
|
475
534
|
row.table.update_or_insert(pending, pk=row.pk)
|
|
476
|
-
#
|
|
535
|
+
# Invalidate cache so trigger-computed columns are re-fetched
|
|
536
|
+
object.__setattr__(row, "_cache", None)
|
|
537
|
+
object.__setattr__(row, "_column_set", None)
|
|
477
538
|
return False
|
|
@@ -25,14 +25,12 @@ from __future__ import annotations
|
|
|
25
25
|
|
|
26
26
|
import hashlib
|
|
27
27
|
import inspect
|
|
28
|
-
import logging
|
|
29
|
-
import os
|
|
30
28
|
import importlib
|
|
31
29
|
import importlib.util
|
|
30
|
+
import logging
|
|
32
31
|
import time
|
|
33
|
-
from datetime import datetime, timezone
|
|
34
32
|
from pathlib import Path
|
|
35
|
-
from typing import Any, Callable, Dict, List, Optional
|
|
33
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
36
34
|
|
|
37
35
|
logger = logging.getLogger("velocity.db.migrations")
|
|
38
36
|
|
|
@@ -102,14 +100,17 @@ class MigrationRunner:
|
|
|
102
100
|
"""
|
|
103
101
|
Discovers, applies, and rolls back schema migrations.
|
|
104
102
|
|
|
105
|
-
Migrations are tracked in a ``velocity_migrations`` table
|
|
103
|
+
Migrations are tracked in a ``velocity_migrations`` table managed by
|
|
104
|
+
velocity (with standard system columns like ``sys_id``, ``sys_created``,
|
|
105
|
+
etc.). User columns:
|
|
106
106
|
|
|
107
|
-
- ``version`` (INT
|
|
107
|
+
- ``version`` (INT) — migration version number
|
|
108
108
|
- ``description`` (TEXT) — human-readable description
|
|
109
|
-
- ``applied_at`` (TIMESTAMPTZ) — when the migration was applied
|
|
110
109
|
- ``checksum`` (TEXT) — SHA-256 of the migration function source
|
|
111
110
|
- ``execution_ms`` (INT) — how long the migration took in milliseconds
|
|
112
111
|
|
|
112
|
+
The ``sys_created`` system column serves as the applied-at timestamp.
|
|
113
|
+
|
|
113
114
|
Args:
|
|
114
115
|
engine: A velocity Engine instance.
|
|
115
116
|
migrations_dir: Optional path to a directory containing migration .py files.
|
|
@@ -147,34 +148,23 @@ class MigrationRunner:
|
|
|
147
148
|
"""Create the velocity_migrations tracking table if it doesn't exist."""
|
|
148
149
|
table = tx.table(TRACKING_TABLE)
|
|
149
150
|
if not table.exists():
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
157
|
-
checksum TEXT NOT NULL DEFAULT '',
|
|
158
|
-
execution_ms INTEGER NOT NULL DEFAULT 0
|
|
159
|
-
)
|
|
160
|
-
""",
|
|
161
|
-
cursor=tx.cursor(),
|
|
162
|
-
)
|
|
151
|
+
table.create(columns={
|
|
152
|
+
"version": int,
|
|
153
|
+
"description": str,
|
|
154
|
+
"checksum": str,
|
|
155
|
+
"execution_ms": int,
|
|
156
|
+
})
|
|
163
157
|
logger.info("Created tracking table: %s", TRACKING_TABLE)
|
|
164
158
|
|
|
165
159
|
def _get_applied_versions(self, tx) -> Dict[int, Dict[str, Any]]:
|
|
166
160
|
"""Return dict of {version: {description, applied_at, checksum, execution_ms}}."""
|
|
167
161
|
self._ensure_tracking_table(tx)
|
|
168
|
-
|
|
169
|
-
f"SELECT version, description, applied_at, checksum, execution_ms "
|
|
170
|
-
f"FROM {TRACKING_TABLE} ORDER BY version",
|
|
171
|
-
cursor=tx.cursor(),
|
|
172
|
-
)
|
|
162
|
+
table = tx.table(TRACKING_TABLE)
|
|
173
163
|
applied = {}
|
|
174
|
-
for row in
|
|
164
|
+
for row in table.select(orderby="version").all():
|
|
175
165
|
applied[row["version"]] = {
|
|
176
166
|
"description": row["description"],
|
|
177
|
-
"applied_at": row["
|
|
167
|
+
"applied_at": row["sys_created"],
|
|
178
168
|
"checksum": row["checksum"],
|
|
179
169
|
"execution_ms": row["execution_ms"],
|
|
180
170
|
}
|
|
@@ -182,20 +172,18 @@ class MigrationRunner:
|
|
|
182
172
|
|
|
183
173
|
def _record_migration(self, tx, version: int, description: str, checksum: str, execution_ms: int) -> None:
|
|
184
174
|
"""Insert a record into the tracking table."""
|
|
185
|
-
tx.
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
175
|
+
table = tx.table(TRACKING_TABLE)
|
|
176
|
+
table.insert({
|
|
177
|
+
"version": version,
|
|
178
|
+
"description": description,
|
|
179
|
+
"checksum": checksum,
|
|
180
|
+
"execution_ms": execution_ms,
|
|
181
|
+
})
|
|
191
182
|
|
|
192
183
|
def _remove_migration_record(self, tx, version: int) -> None:
|
|
193
184
|
"""Delete a record from the tracking table."""
|
|
194
|
-
tx.
|
|
195
|
-
|
|
196
|
-
(version,),
|
|
197
|
-
cursor=tx.cursor(),
|
|
198
|
-
)
|
|
185
|
+
table = tx.table(TRACKING_TABLE)
|
|
186
|
+
table.delete(where={"version": version})
|
|
199
187
|
|
|
200
188
|
# ── Checksum ────────────────────────────────────────────────
|
|
201
189
|
|
|
@@ -175,6 +175,7 @@ tests/test_psycopg3_upgrade.py
|
|
|
175
175
|
tests/test_query_cache.py
|
|
176
176
|
tests/test_row_batch_update.py
|
|
177
177
|
tests/test_row_cache_staleness.py
|
|
178
|
+
tests/test_row_dirty_tracking.py
|
|
178
179
|
tests/test_schema_migrations.py
|
|
179
180
|
tests/test_security_hardening.py
|
|
180
181
|
tests/test_sqs_per_record_transactions.py
|
|
@@ -29,6 +29,8 @@ def _make_row(cache=None, pk=None):
|
|
|
29
29
|
object.__setattr__(row, "_cache_ttl", None)
|
|
30
30
|
object.__setattr__(row, "_cache_time", None)
|
|
31
31
|
object.__setattr__(row, "_no_cache", False)
|
|
32
|
+
object.__setattr__(row, "_dirty_tracking", False)
|
|
33
|
+
object.__setattr__(row, "_dirty", {})
|
|
32
34
|
return row
|
|
33
35
|
|
|
34
36
|
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for R3 — Row dirty-tracking mode with .save().
|
|
3
|
+
|
|
4
|
+
When ``dirty_tracking=True``, writes accumulate in memory and are flushed
|
|
5
|
+
to the database only on explicit ``.save()``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
from unittest.mock import MagicMock
|
|
10
|
+
|
|
11
|
+
from velocity.db.core.row import Row
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _make_dirty_row(cache=None, pk=None):
|
|
15
|
+
"""Create a Row with dirty_tracking=True and a mocked table."""
|
|
16
|
+
table = MagicMock()
|
|
17
|
+
table.name = "test_table"
|
|
18
|
+
table.select.return_value.as_dict.return_value.one.return_value = cache or {
|
|
19
|
+
"name": "Alice",
|
|
20
|
+
"email": "alice@example.com",
|
|
21
|
+
"sys_id": 1,
|
|
22
|
+
}
|
|
23
|
+
table.update_or_insert = MagicMock()
|
|
24
|
+
|
|
25
|
+
row = Row.__new__(Row)
|
|
26
|
+
object.__setattr__(row, "table", table)
|
|
27
|
+
object.__setattr__(row, "pk", pk or {"sys_id": 1})
|
|
28
|
+
object.__setattr__(row, "_cache", dict(cache) if cache else None)
|
|
29
|
+
object.__setattr__(row, "_column_set", None)
|
|
30
|
+
object.__setattr__(row, "_batching", False)
|
|
31
|
+
object.__setattr__(row, "_pending", {})
|
|
32
|
+
object.__setattr__(row, "_cache_ttl", None)
|
|
33
|
+
object.__setattr__(row, "_cache_time", None)
|
|
34
|
+
object.__setattr__(row, "_no_cache", False)
|
|
35
|
+
object.__setattr__(row, "_dirty_tracking", True)
|
|
36
|
+
object.__setattr__(row, "_dirty", {})
|
|
37
|
+
return row
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _make_normal_row(cache=None, pk=None):
|
|
41
|
+
"""Create a Row with dirty_tracking=False (default)."""
|
|
42
|
+
table = MagicMock()
|
|
43
|
+
table.name = "test_table"
|
|
44
|
+
table.select.return_value.as_dict.return_value.one.return_value = cache or {
|
|
45
|
+
"name": "Alice",
|
|
46
|
+
"email": "alice@example.com",
|
|
47
|
+
"sys_id": 1,
|
|
48
|
+
}
|
|
49
|
+
table.update_or_insert = MagicMock()
|
|
50
|
+
|
|
51
|
+
row = Row.__new__(Row)
|
|
52
|
+
object.__setattr__(row, "table", table)
|
|
53
|
+
object.__setattr__(row, "pk", pk or {"sys_id": 1})
|
|
54
|
+
object.__setattr__(row, "_cache", dict(cache) if cache else None)
|
|
55
|
+
object.__setattr__(row, "_column_set", None)
|
|
56
|
+
object.__setattr__(row, "_batching", False)
|
|
57
|
+
object.__setattr__(row, "_pending", {})
|
|
58
|
+
object.__setattr__(row, "_cache_ttl", None)
|
|
59
|
+
object.__setattr__(row, "_cache_time", None)
|
|
60
|
+
object.__setattr__(row, "_no_cache", False)
|
|
61
|
+
object.__setattr__(row, "_dirty_tracking", False)
|
|
62
|
+
object.__setattr__(row, "_dirty", {})
|
|
63
|
+
return row
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class TestDirtyTracking:
|
|
67
|
+
|
|
68
|
+
def test_no_db_write_on_assignment(self):
|
|
69
|
+
"""Assignments should NOT trigger DB writes when dirty tracking is on."""
|
|
70
|
+
row = _make_dirty_row({"name": "Alice", "sys_id": 1})
|
|
71
|
+
row["name"] = "Bob"
|
|
72
|
+
row.table.update_or_insert.assert_not_called()
|
|
73
|
+
|
|
74
|
+
def test_save_flushes_all_changes(self):
|
|
75
|
+
"""save() should issue a single UPDATE with all accumulated changes."""
|
|
76
|
+
row = _make_dirty_row({"name": "Alice", "email": "old@test.com", "sys_id": 1})
|
|
77
|
+
row["name"] = "Bob"
|
|
78
|
+
row["email"] = "bob@test.com"
|
|
79
|
+
row.save()
|
|
80
|
+
|
|
81
|
+
row.table.update_or_insert.assert_called_once_with(
|
|
82
|
+
{"name": "Bob", "email": "bob@test.com"}, pk={"sys_id": 1}
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def test_save_clears_dirty_dict(self):
|
|
86
|
+
"""After save(), the dirty dict should be empty."""
|
|
87
|
+
row = _make_dirty_row({"name": "Alice", "sys_id": 1})
|
|
88
|
+
row["name"] = "Bob"
|
|
89
|
+
assert row.is_dirty
|
|
90
|
+
row.save()
|
|
91
|
+
assert not row.is_dirty
|
|
92
|
+
|
|
93
|
+
def test_save_no_changes_is_noop(self):
|
|
94
|
+
"""save() with no changes should not issue a DB call."""
|
|
95
|
+
row = _make_dirty_row({"name": "Alice", "sys_id": 1})
|
|
96
|
+
row.save()
|
|
97
|
+
row.table.update_or_insert.assert_not_called()
|
|
98
|
+
|
|
99
|
+
def test_reads_see_dirty_values(self):
|
|
100
|
+
"""Reads should return the dirty (not-yet-saved) value."""
|
|
101
|
+
row = _make_dirty_row({"name": "Alice", "sys_id": 1})
|
|
102
|
+
row["name"] = "Bob"
|
|
103
|
+
assert row["name"] == "Bob"
|
|
104
|
+
|
|
105
|
+
def test_is_dirty_property(self):
|
|
106
|
+
"""is_dirty should reflect whether unsaved changes exist."""
|
|
107
|
+
row = _make_dirty_row({"name": "Alice", "sys_id": 1})
|
|
108
|
+
assert not row.is_dirty
|
|
109
|
+
row["name"] = "Bob"
|
|
110
|
+
assert row.is_dirty
|
|
111
|
+
|
|
112
|
+
def test_discard_clears_changes(self):
|
|
113
|
+
"""discard() should drop pending changes and invalidate cache."""
|
|
114
|
+
row = _make_dirty_row({"name": "Alice", "sys_id": 1})
|
|
115
|
+
row["name"] = "Bob"
|
|
116
|
+
assert row.is_dirty
|
|
117
|
+
row.discard()
|
|
118
|
+
assert not row.is_dirty
|
|
119
|
+
|
|
120
|
+
def test_discard_invalidates_cache(self):
|
|
121
|
+
"""After discard(), cache should be None so next read re-fetches."""
|
|
122
|
+
row = _make_dirty_row({"name": "Alice", "sys_id": 1})
|
|
123
|
+
row["name"] = "Bob"
|
|
124
|
+
row.discard()
|
|
125
|
+
assert row._cache is None
|
|
126
|
+
|
|
127
|
+
def test_save_invalidates_cache(self):
|
|
128
|
+
"""After save(), cache should be invalidated for trigger-computed columns."""
|
|
129
|
+
row = _make_dirty_row({"name": "Alice", "sys_id": 1})
|
|
130
|
+
row["name"] = "Bob"
|
|
131
|
+
row.save()
|
|
132
|
+
assert row._cache is None
|
|
133
|
+
|
|
134
|
+
def test_last_write_wins(self):
|
|
135
|
+
"""Multiple assignments to the same column keep the last value."""
|
|
136
|
+
row = _make_dirty_row({"name": "Alice", "sys_id": 1})
|
|
137
|
+
row["name"] = "Bob"
|
|
138
|
+
row["name"] = "Charlie"
|
|
139
|
+
row.save()
|
|
140
|
+
|
|
141
|
+
row.table.update_or_insert.assert_called_once_with(
|
|
142
|
+
{"name": "Charlie"}, pk={"sys_id": 1}
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def test_pk_write_raises(self):
|
|
146
|
+
"""Writing to a PK column should raise even with dirty tracking."""
|
|
147
|
+
row = _make_dirty_row({"name": "Alice", "sys_id": 1})
|
|
148
|
+
with pytest.raises(Exception, match="Cannot update a primary key"):
|
|
149
|
+
row["sys_id"] = 999
|
|
150
|
+
|
|
151
|
+
def test_attr_style_is_deferred(self):
|
|
152
|
+
"""Attribute-style writes should also be deferred."""
|
|
153
|
+
row = _make_dirty_row({"name": "Alice", "sys_id": 1})
|
|
154
|
+
row.name = "Bob"
|
|
155
|
+
row.table.update_or_insert.assert_not_called()
|
|
156
|
+
row.save()
|
|
157
|
+
row.table.update_or_insert.assert_called_once()
|
|
158
|
+
|
|
159
|
+
def test_save_without_dirty_tracking_raises(self):
|
|
160
|
+
"""save() on a normal row should raise RuntimeError."""
|
|
161
|
+
row = _make_normal_row({"name": "Alice", "sys_id": 1})
|
|
162
|
+
with pytest.raises(RuntimeError, match="dirty_tracking=True"):
|
|
163
|
+
row.save()
|
|
164
|
+
|
|
165
|
+
def test_save_returns_self(self):
|
|
166
|
+
"""save() should return self for chaining."""
|
|
167
|
+
row = _make_dirty_row({"name": "Alice", "sys_id": 1})
|
|
168
|
+
row["name"] = "Bob"
|
|
169
|
+
result = row.save()
|
|
170
|
+
assert result is row
|
|
171
|
+
|
|
172
|
+
def test_discard_returns_self(self):
|
|
173
|
+
"""discard() should return self for chaining."""
|
|
174
|
+
row = _make_dirty_row({"name": "Alice", "sys_id": 1})
|
|
175
|
+
result = row.discard()
|
|
176
|
+
assert result is row
|
|
177
|
+
|
|
178
|
+
def test_constructor_parameter(self):
|
|
179
|
+
"""Row can be created with dirty_tracking=True via constructor."""
|
|
180
|
+
table = MagicMock()
|
|
181
|
+
table.name = "test_table"
|
|
182
|
+
table.select.return_value.as_dict.return_value.one.return_value = {
|
|
183
|
+
"name": "Alice", "sys_id": 1,
|
|
184
|
+
}
|
|
185
|
+
row = Row(table, 1, dirty_tracking=True)
|
|
186
|
+
assert row._dirty_tracking is True
|
|
187
|
+
assert row._dirty == {}
|
|
188
|
+
|
|
189
|
+
def test_normal_row_writes_through(self):
|
|
190
|
+
"""Without dirty tracking, writes still go through immediately."""
|
|
191
|
+
row = _make_normal_row({"name": "Alice", "sys_id": 1})
|
|
192
|
+
row["name"] = "Bob"
|
|
193
|
+
row.table.update_or_insert.assert_called_once()
|