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