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.
Files changed (186) hide show
  1. {velocity_python-0.1.10/src/velocity_python.egg-info → velocity_python-0.1.12}/PKG-INFO +1 -1
  2. {velocity_python-0.1.10 → velocity_python-0.1.12}/pyproject.toml +1 -1
  3. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/__init__.py +1 -1
  4. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/core/row.py +65 -4
  5. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/migrations.py +26 -38
  6. {velocity_python-0.1.10 → velocity_python-0.1.12/src/velocity_python.egg-info}/PKG-INFO +1 -1
  7. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity_python.egg-info/SOURCES.txt +1 -0
  8. {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_row_batch_update.py +2 -0
  9. velocity_python-0.1.12/tests/test_row_dirty_tracking.py +193 -0
  10. {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_schema_migrations.py +37 -38
  11. {velocity_python-0.1.10 → velocity_python-0.1.12}/LICENSE +0 -0
  12. {velocity_python-0.1.10 → velocity_python-0.1.12}/README.md +0 -0
  13. {velocity_python-0.1.10 → velocity_python-0.1.12}/setup.cfg +0 -0
  14. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/app/__init__.py +0 -0
  15. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/app/formbuilder/__init__.py +0 -0
  16. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/app/formbuilder/reshaper.py +0 -0
  17. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/app/invoices.py +0 -0
  18. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/app/orders.py +0 -0
  19. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/app/payments.py +0 -0
  20. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/app/purchase_orders.py +0 -0
  21. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/app/tests/__init__.py +0 -0
  22. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/app/tests/test_email_processing.py +0 -0
  23. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/app/tests/test_payment_profile_sorting.py +0 -0
  24. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/app/tests/test_spreadsheet_functions.py +0 -0
  25. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/app/validators/__init__.py +0 -0
  26. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/app/validators/formbuilder_template.py +0 -0
  27. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/aws/__init__.py +0 -0
  28. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/aws/amplify.py +0 -0
  29. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/aws/amplify_build.py +0 -0
  30. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/aws/handlers/__init__.py +0 -0
  31. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/aws/handlers/base_handler.py +0 -0
  32. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/aws/handlers/context.py +0 -0
  33. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/aws/handlers/context_factory.py +0 -0
  34. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/aws/handlers/exceptions.py +0 -0
  35. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/aws/handlers/lambda_handler.py +0 -0
  36. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
  37. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/aws/handlers/mixins/data_service.py +0 -0
  38. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
  39. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/aws/handlers/perf.py +0 -0
  40. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/aws/handlers/response.py +0 -0
  41. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/aws/handlers/sqs_handler.py +0 -0
  42. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/aws/tests/__init__.py +0 -0
  43. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
  44. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
  45. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/aws/tests/test_response.py +0 -0
  46. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/__init__.py +0 -0
  47. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/core/__init__.py +0 -0
  48. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/core/async_support.py +0 -0
  49. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/core/column.py +0 -0
  50. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/core/database.py +0 -0
  51. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/core/decorators.py +0 -0
  52. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/core/engine.py +0 -0
  53. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/core/result.py +0 -0
  54. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/core/sequence.py +0 -0
  55. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/core/table.py +0 -0
  56. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/core/transaction.py +0 -0
  57. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/core/view.py +0 -0
  58. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/exceptions.py +0 -0
  59. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/__init__.py +0 -0
  60. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/base/__init__.py +0 -0
  61. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/base/initializer.py +0 -0
  62. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/base/operators.py +0 -0
  63. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/base/sql.py +0 -0
  64. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/base/types.py +0 -0
  65. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/mysql/__init__.py +0 -0
  66. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/mysql/operators.py +0 -0
  67. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/mysql/reserved.py +0 -0
  68. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/mysql/sql.py +0 -0
  69. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/mysql/types.py +0 -0
  70. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/postgres/__init__.py +0 -0
  71. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/postgres/operators.py +0 -0
  72. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/postgres/reserved.py +0 -0
  73. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/postgres/sql.py +0 -0
  74. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/postgres/types.py +0 -0
  75. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/sqlite/__init__.py +0 -0
  76. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/sqlite/operators.py +0 -0
  77. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/sqlite/reserved.py +0 -0
  78. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/sqlite/sql.py +0 -0
  79. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/sqlite/types.py +0 -0
  80. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
  81. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/sqlserver/operators.py +0 -0
  82. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
  83. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/sqlserver/sql.py +0 -0
  84. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/sqlserver/types.py +0 -0
  85. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/servers/tablehelper.py +0 -0
  86. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/__init__.py +0 -0
  87. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/common_db_test.py +0 -0
  88. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/postgres/__init__.py +0 -0
  89. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/postgres/common.py +0 -0
  90. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/postgres/test_column.py +0 -0
  91. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/postgres/test_connections.py +0 -0
  92. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/postgres/test_database.py +0 -0
  93. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/postgres/test_engine.py +0 -0
  94. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
  95. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/postgres/test_imports.py +0 -0
  96. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/postgres/test_result.py +0 -0
  97. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/postgres/test_row.py +0 -0
  98. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
  99. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
  100. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
  101. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
  102. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
  103. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/postgres/test_table.py +0 -0
  104. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
  105. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
  106. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/sql/__init__.py +0 -0
  107. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/sql/common.py +0 -0
  108. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
  109. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
  110. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
  111. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/test_db_utils.py +0 -0
  112. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/test_postgres.py +0 -0
  113. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
  114. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
  115. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/test_result_caching.py +0 -0
  116. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
  117. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
  118. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
  119. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
  120. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/test_sql_builder.py +0 -0
  121. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/test_tablehelper.py +0 -0
  122. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/tests/test_view_helper.py +0 -0
  123. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/db/utils.py +0 -0
  124. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/logging.py +0 -0
  125. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/misc/__init__.py +0 -0
  126. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/misc/conv/__init__.py +0 -0
  127. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/misc/conv/iconv.py +0 -0
  128. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/misc/conv/oconv.py +0 -0
  129. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/misc/db.py +0 -0
  130. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/misc/export.py +0 -0
  131. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/misc/format.py +0 -0
  132. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/misc/mail.py +0 -0
  133. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/misc/merge.py +0 -0
  134. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/misc/tests/__init__.py +0 -0
  135. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/misc/tests/test_db.py +0 -0
  136. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/misc/tests/test_fix.py +0 -0
  137. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/misc/tests/test_format.py +0 -0
  138. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/misc/tests/test_iconv.py +0 -0
  139. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/misc/tests/test_merge.py +0 -0
  140. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/misc/tests/test_oconv.py +0 -0
  141. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/misc/tests/test_original_error.py +0 -0
  142. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/misc/tests/test_timer.py +0 -0
  143. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/misc/timer.py +0 -0
  144. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/misc/tools.py +0 -0
  145. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/payment/__init__.py +0 -0
  146. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/payment/authorizenet_adapter.py +0 -0
  147. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/payment/base_adapter.py +0 -0
  148. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/payment/braintree_adapter.py +0 -0
  149. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/payment/charge_rules.py +0 -0
  150. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/payment/demo_profiles.py +0 -0
  151. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/payment/profiles.py +0 -0
  152. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/payment/router.py +0 -0
  153. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity/payment/stripe_adapter.py +0 -0
  154. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity_python.egg-info/dependency_links.txt +0 -0
  155. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity_python.egg-info/entry_points.txt +0 -0
  156. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity_python.egg-info/requires.txt +0 -0
  157. {velocity_python-0.1.10 → velocity_python-0.1.12}/src/velocity_python.egg-info/top_level.txt +0 -0
  158. {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_amplify_build.py +0 -0
  159. {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_async_support.py +0 -0
  160. {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_batch_operations.py +0 -0
  161. {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_concurrency_safety.py +0 -0
  162. {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_connection_pool.py +0 -0
  163. {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_connection_resilience.py +0 -0
  164. {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_decorators.py +0 -0
  165. {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_formbuilder_reshaper.py +0 -0
  166. {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_formbuilder_template_validator.py +0 -0
  167. {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_iconv_money_to_cents.py +0 -0
  168. {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_lambda_handler.py +0 -0
  169. {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_lambda_handler_auth.py +0 -0
  170. {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_mixins_import.py +0 -0
  171. {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_n_plus_one.py +0 -0
  172. {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_observability.py +0 -0
  173. {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_payment_braintree_adapter.py +0 -0
  174. {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_payment_demo_profiles.py +0 -0
  175. {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_payment_profiles.py +0 -0
  176. {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_payment_router.py +0 -0
  177. {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_payment_stripe_adapter.py +0 -0
  178. {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_prepared_statements.py +0 -0
  179. {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_psycopg3_upgrade.py +0 -0
  180. {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_query_cache.py +0 -0
  181. {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_row_cache_staleness.py +0 -0
  182. {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_security_hardening.py +0 -0
  183. {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_sqs_per_record_transactions.py +0 -0
  184. {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_sys_modified_count_postgres_demo.py +0 -0
  185. {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_table_alter.py +0 -0
  186. {velocity_python-0.1.10 → velocity_python-0.1.12}/tests/test_where_clause_validation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: velocity-python
3
- Version: 0.1.10
3
+ Version: 0.1.12
4
4
  Summary: A rapid application development library for interfacing with data storage
5
5
  Author-email: Velocity Team <info@codeclubs.org>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "velocity-python"
7
- version = "0.1.10"
7
+ version = "0.1.12"
8
8
  authors = [
9
9
  { name="Velocity Team", email="info@codeclubs.org" },
10
10
  ]
@@ -1,4 +1,4 @@
1
- __version__ = version = "0.1.10"
1
+ __version__ = version = "0.1.12"
2
2
 
3
3
  import importlib as _importlib
4
4
 
@@ -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
- # Cache was already updated optimistically in __setitem__
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, Tuple
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 with columns:
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, PRIMARY KEY) — migration version number
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
- # Use raw SQL to avoid velocity system columns — we manage our own schema
151
- tx.execute(
152
- f"""
153
- CREATE TABLE IF NOT EXISTS {TRACKING_TABLE} (
154
- version INTEGER PRIMARY KEY,
155
- description TEXT NOT NULL DEFAULT '',
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
- result = tx.execute(
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 result.all():
164
+ for row in table.select(orderby="version").all():
175
165
  applied[row["version"]] = {
176
166
  "description": row["description"],
177
- "applied_at": row["applied_at"],
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.execute(
186
- f"INSERT INTO {TRACKING_TABLE} (version, description, checksum, execution_ms) "
187
- f"VALUES (%s, %s, %s, %s)",
188
- (version, description, checksum, execution_ms),
189
- cursor=tx.cursor(),
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.execute(
195
- f"DELETE FROM {TRACKING_TABLE} WHERE version = %s",
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: velocity-python
3
- Version: 0.1.10
3
+ Version: 0.1.12
4
4
  Summary: A rapid application development library for interfacing with data storage
5
5
  Author-email: Velocity Team <info@codeclubs.org>
6
6
  License-Expression: MIT
@@ -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()