velocity-python 0.1.37__tar.gz → 0.1.38__tar.gz

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