velocity-python 0.1.2__tar.gz → 0.1.4__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 (179) hide show
  1. {velocity_python-0.1.2 → velocity_python-0.1.4}/PKG-INFO +1 -1
  2. {velocity_python-0.1.2 → velocity_python-0.1.4}/pyproject.toml +1 -1
  3. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/__init__.py +1 -1
  4. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/core/column.py +2 -1
  5. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/core/decorators.py +6 -0
  6. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/core/engine.py +68 -5
  7. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/core/table.py +24 -3
  8. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/core/transaction.py +85 -0
  9. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/servers/base/sql.py +10 -0
  10. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/servers/mysql/sql.py +5 -0
  11. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/servers/sqlserver/sql.py +5 -0
  12. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity_python.egg-info/PKG-INFO +1 -1
  13. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity_python.egg-info/SOURCES.txt +2 -0
  14. velocity_python-0.1.4/tests/test_observability.py +443 -0
  15. velocity_python-0.1.4/tests/test_security_hardening.py +256 -0
  16. {velocity_python-0.1.2 → velocity_python-0.1.4}/LICENSE +0 -0
  17. {velocity_python-0.1.2 → velocity_python-0.1.4}/README.md +0 -0
  18. {velocity_python-0.1.2 → velocity_python-0.1.4}/setup.cfg +0 -0
  19. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/app/__init__.py +0 -0
  20. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/app/formbuilder/__init__.py +0 -0
  21. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/app/formbuilder/reshaper.py +0 -0
  22. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/app/invoices.py +0 -0
  23. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/app/orders.py +0 -0
  24. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/app/payments.py +0 -0
  25. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/app/purchase_orders.py +0 -0
  26. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/app/tests/__init__.py +0 -0
  27. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/app/tests/test_email_processing.py +0 -0
  28. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/app/tests/test_payment_profile_sorting.py +0 -0
  29. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/app/tests/test_spreadsheet_functions.py +0 -0
  30. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/app/validators/__init__.py +0 -0
  31. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/app/validators/formbuilder_template.py +0 -0
  32. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/aws/__init__.py +0 -0
  33. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/aws/amplify.py +0 -0
  34. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/aws/amplify_build.py +0 -0
  35. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/aws/handlers/__init__.py +0 -0
  36. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/aws/handlers/base_handler.py +0 -0
  37. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/aws/handlers/context.py +0 -0
  38. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/aws/handlers/context_factory.py +0 -0
  39. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/aws/handlers/exceptions.py +0 -0
  40. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/aws/handlers/lambda_handler.py +0 -0
  41. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
  42. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/aws/handlers/mixins/data_service.py +0 -0
  43. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
  44. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/aws/handlers/perf.py +0 -0
  45. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/aws/handlers/response.py +0 -0
  46. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/aws/handlers/sqs_handler.py +0 -0
  47. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/aws/tests/__init__.py +0 -0
  48. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
  49. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
  50. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/aws/tests/test_response.py +0 -0
  51. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/__init__.py +0 -0
  52. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/core/__init__.py +0 -0
  53. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/core/database.py +0 -0
  54. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/core/result.py +0 -0
  55. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/core/row.py +0 -0
  56. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/core/sequence.py +0 -0
  57. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/core/view.py +0 -0
  58. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/exceptions.py +0 -0
  59. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/servers/__init__.py +0 -0
  60. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/servers/base/__init__.py +0 -0
  61. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/servers/base/initializer.py +0 -0
  62. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/servers/base/operators.py +0 -0
  63. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/servers/base/types.py +0 -0
  64. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/servers/mysql/__init__.py +0 -0
  65. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/servers/mysql/operators.py +0 -0
  66. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/servers/mysql/reserved.py +0 -0
  67. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/servers/mysql/types.py +0 -0
  68. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/servers/postgres/__init__.py +0 -0
  69. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/servers/postgres/operators.py +0 -0
  70. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/servers/postgres/reserved.py +0 -0
  71. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/servers/postgres/sql.py +0 -0
  72. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/servers/postgres/types.py +0 -0
  73. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/servers/sqlite/__init__.py +0 -0
  74. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/servers/sqlite/operators.py +0 -0
  75. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/servers/sqlite/reserved.py +0 -0
  76. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/servers/sqlite/sql.py +0 -0
  77. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/servers/sqlite/types.py +0 -0
  78. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
  79. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/servers/sqlserver/operators.py +0 -0
  80. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
  81. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/servers/sqlserver/types.py +0 -0
  82. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/servers/tablehelper.py +0 -0
  83. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/tests/__init__.py +0 -0
  84. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/tests/common_db_test.py +0 -0
  85. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/tests/postgres/__init__.py +0 -0
  86. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/tests/postgres/common.py +0 -0
  87. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/tests/postgres/test_column.py +0 -0
  88. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/tests/postgres/test_connections.py +0 -0
  89. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/tests/postgres/test_database.py +0 -0
  90. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/tests/postgres/test_engine.py +0 -0
  91. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
  92. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/tests/postgres/test_imports.py +0 -0
  93. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/tests/postgres/test_result.py +0 -0
  94. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/tests/postgres/test_row.py +0 -0
  95. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
  96. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
  97. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
  98. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
  99. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
  100. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/tests/postgres/test_table.py +0 -0
  101. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
  102. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
  103. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/tests/sql/__init__.py +0 -0
  104. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/tests/sql/common.py +0 -0
  105. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
  106. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
  107. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
  108. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/tests/test_db_utils.py +0 -0
  109. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/tests/test_postgres.py +0 -0
  110. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
  111. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
  112. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/tests/test_result_caching.py +0 -0
  113. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
  114. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
  115. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
  116. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
  117. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/tests/test_sql_builder.py +0 -0
  118. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/tests/test_tablehelper.py +0 -0
  119. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/tests/test_view_helper.py +0 -0
  120. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/db/utils.py +0 -0
  121. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/logging.py +0 -0
  122. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/misc/__init__.py +0 -0
  123. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/misc/conv/__init__.py +0 -0
  124. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/misc/conv/iconv.py +0 -0
  125. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/misc/conv/oconv.py +0 -0
  126. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/misc/db.py +0 -0
  127. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/misc/export.py +0 -0
  128. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/misc/format.py +0 -0
  129. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/misc/mail.py +0 -0
  130. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/misc/merge.py +0 -0
  131. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/misc/tests/__init__.py +0 -0
  132. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/misc/tests/test_db.py +0 -0
  133. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/misc/tests/test_fix.py +0 -0
  134. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/misc/tests/test_format.py +0 -0
  135. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/misc/tests/test_iconv.py +0 -0
  136. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/misc/tests/test_merge.py +0 -0
  137. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/misc/tests/test_oconv.py +0 -0
  138. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/misc/tests/test_original_error.py +0 -0
  139. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/misc/tests/test_timer.py +0 -0
  140. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/misc/timer.py +0 -0
  141. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/misc/tools.py +0 -0
  142. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/payment/__init__.py +0 -0
  143. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/payment/authorizenet_adapter.py +0 -0
  144. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/payment/base_adapter.py +0 -0
  145. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/payment/braintree_adapter.py +0 -0
  146. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/payment/charge_rules.py +0 -0
  147. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/payment/demo_profiles.py +0 -0
  148. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/payment/profiles.py +0 -0
  149. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/payment/router.py +0 -0
  150. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity/payment/stripe_adapter.py +0 -0
  151. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity_python.egg-info/dependency_links.txt +0 -0
  152. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity_python.egg-info/requires.txt +0 -0
  153. {velocity_python-0.1.2 → velocity_python-0.1.4}/src/velocity_python.egg-info/top_level.txt +0 -0
  154. {velocity_python-0.1.2 → velocity_python-0.1.4}/tests/test_amplify_build.py +0 -0
  155. {velocity_python-0.1.2 → velocity_python-0.1.4}/tests/test_batch_operations.py +0 -0
  156. {velocity_python-0.1.2 → velocity_python-0.1.4}/tests/test_concurrency_safety.py +0 -0
  157. {velocity_python-0.1.2 → velocity_python-0.1.4}/tests/test_connection_pool.py +0 -0
  158. {velocity_python-0.1.2 → velocity_python-0.1.4}/tests/test_connection_resilience.py +0 -0
  159. {velocity_python-0.1.2 → velocity_python-0.1.4}/tests/test_decorators.py +0 -0
  160. {velocity_python-0.1.2 → velocity_python-0.1.4}/tests/test_formbuilder_reshaper.py +0 -0
  161. {velocity_python-0.1.2 → velocity_python-0.1.4}/tests/test_formbuilder_template_validator.py +0 -0
  162. {velocity_python-0.1.2 → velocity_python-0.1.4}/tests/test_iconv_money_to_cents.py +0 -0
  163. {velocity_python-0.1.2 → velocity_python-0.1.4}/tests/test_lambda_handler.py +0 -0
  164. {velocity_python-0.1.2 → velocity_python-0.1.4}/tests/test_lambda_handler_auth.py +0 -0
  165. {velocity_python-0.1.2 → velocity_python-0.1.4}/tests/test_mixins_import.py +0 -0
  166. {velocity_python-0.1.2 → velocity_python-0.1.4}/tests/test_payment_braintree_adapter.py +0 -0
  167. {velocity_python-0.1.2 → velocity_python-0.1.4}/tests/test_payment_demo_profiles.py +0 -0
  168. {velocity_python-0.1.2 → velocity_python-0.1.4}/tests/test_payment_profiles.py +0 -0
  169. {velocity_python-0.1.2 → velocity_python-0.1.4}/tests/test_payment_router.py +0 -0
  170. {velocity_python-0.1.2 → velocity_python-0.1.4}/tests/test_payment_stripe_adapter.py +0 -0
  171. {velocity_python-0.1.2 → velocity_python-0.1.4}/tests/test_prepared_statements.py +0 -0
  172. {velocity_python-0.1.2 → velocity_python-0.1.4}/tests/test_psycopg3_upgrade.py +0 -0
  173. {velocity_python-0.1.2 → velocity_python-0.1.4}/tests/test_query_cache.py +0 -0
  174. {velocity_python-0.1.2 → velocity_python-0.1.4}/tests/test_row_batch_update.py +0 -0
  175. {velocity_python-0.1.2 → velocity_python-0.1.4}/tests/test_row_cache_staleness.py +0 -0
  176. {velocity_python-0.1.2 → velocity_python-0.1.4}/tests/test_sqs_per_record_transactions.py +0 -0
  177. {velocity_python-0.1.2 → velocity_python-0.1.4}/tests/test_sys_modified_count_postgres_demo.py +0 -0
  178. {velocity_python-0.1.2 → velocity_python-0.1.4}/tests/test_table_alter.py +0 -0
  179. {velocity_python-0.1.2 → velocity_python-0.1.4}/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.2
3
+ Version: 0.1.4
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.2"
7
+ version = "0.1.4"
8
8
  authors = [
9
9
  { name="Velocity Team", email="info@codeclubs.org" },
10
10
  ]
@@ -1,4 +1,4 @@
1
- __version__ = version = "0.1.2"
1
+ __version__ = version = "0.1.4"
2
2
 
3
3
  import importlib as _importlib
4
4
 
@@ -125,8 +125,9 @@ class Column:
125
125
  Returns the MAX() of this column, or 0 if table/column is missing.
126
126
  """
127
127
  try:
128
+ qcol = self.sql.quote_identifier(self.name)
128
129
  sql, vals = self.sql.select(
129
- columns=f"max({self.name})", table=self.table.name, where=where
130
+ columns=f"max({qcol})", table=self.table.name, where=where
130
131
  )
131
132
  return self.tx.execute(sql, vals).scalar()
132
133
  except (exceptions.DbTableMissingError, exceptions.DbColumnMissingError):
@@ -121,6 +121,12 @@ def return_default(
121
121
  e,
122
122
  )
123
123
 
124
+ # R12 — Increment per-transaction counter for swallowed exceptions.
125
+ try:
126
+ self.tx._return_default_count = getattr(self.tx, "_return_default_count", 0) + 1
127
+ except Exception:
128
+ pass
129
+
124
130
  # Capture swallowed exceptions for upstream diagnostics.
125
131
  # This decorator intentionally returns a default value instead of
126
132
  # raising, but consumers (e.g. API handlers) may still want to
@@ -7,7 +7,7 @@ from contextlib import contextmanager
7
7
  from functools import wraps
8
8
  from velocity.db import exceptions
9
9
  from velocity.db.core.transaction import Transaction
10
- from velocity.db.utils import mask_config_for_display
10
+ from velocity.db.utils import mask_config_for_display, mask_sensitive_in_string
11
11
 
12
12
  import logging
13
13
 
@@ -77,6 +77,7 @@ class ConnectionPool:
77
77
  if self._closed:
78
78
  raise exceptions.DbConnectionError("Connection pool is closed")
79
79
 
80
+ t0 = time.perf_counter()
80
81
  self._available.acquire() # blocks when maxconn reached
81
82
 
82
83
  with self._lock:
@@ -84,7 +85,11 @@ class ConnectionPool:
84
85
  while self._pool:
85
86
  conn = self._pool.pop()
86
87
  if self._is_alive(conn):
87
- logger.debug("Pool: reusing idle connection (pool_size=%d)", len(self._pool))
88
+ elapsed_ms = (time.perf_counter() - t0) * 1000
89
+ logger.debug(
90
+ "Pool: reusing idle connection (pool_size=%d, wait=%.1f ms)",
91
+ len(self._pool), elapsed_ms,
92
+ )
88
93
  return conn
89
94
  # Dead connection — close it silently and create a fresh one.
90
95
  logger.debug("Pool: discarding dead idle connection")
@@ -95,7 +100,11 @@ class ConnectionPool:
95
100
  try:
96
101
  conn = self._connect_fn()
97
102
  self._total_created += 1
98
- logger.debug("Pool: created new connection (total=%d)", self._total_created)
103
+ elapsed_ms = (time.perf_counter() - t0) * 1000
104
+ logger.debug(
105
+ "Pool: created new connection (total=%d, wait=%.1f ms)",
106
+ self._total_created, elapsed_ms,
107
+ )
99
108
  return conn
100
109
  except Exception:
101
110
  self._available.release()
@@ -277,10 +286,12 @@ class Engine:
277
286
  If pooling is enabled, borrows from the pool.
278
287
  If the database is missing, tries to create it, then reconnect.
279
288
  """
289
+ t0 = time.perf_counter()
280
290
  if self.__pool:
281
291
  try:
282
292
  conn = self.__pool.getconn()
283
- logger.debug("Engine.connect: obtained pooled connection")
293
+ elapsed_ms = (time.perf_counter() - t0) * 1000
294
+ logger.debug("Engine.connect: obtained pooled connection (%.1f ms)", elapsed_ms)
284
295
  return conn
285
296
  except exceptions.DbDatabaseMissingError:
286
297
  self.create_database()
@@ -291,7 +302,8 @@ class Engine:
291
302
 
292
303
  try:
293
304
  conn = self._raw_connect()
294
- logger.debug("Engine.connect: created direct connection")
305
+ elapsed_ms = (time.perf_counter() - t0) * 1000
306
+ logger.debug("Engine.connect: created direct connection (%.1f ms)", elapsed_ms)
295
307
  except exceptions.DbDatabaseMissingError:
296
308
  self.create_database()
297
309
  conn = self._raw_connect()
@@ -496,6 +508,53 @@ class Engine:
496
508
  # if depth == 0:
497
509
  # delattr(_tx, "_exec_function_depth")
498
510
 
511
+ def perf_log(self, func):
512
+ """
513
+ Decorator that logs wall time, query count, and total query time
514
+ for a ``@engine.transaction``-wrapped function.
515
+
516
+ Usage::
517
+
518
+ @engine.perf_log
519
+ @engine.transaction
520
+ def some_work(tx):
521
+ ...
522
+
523
+ The ``tx`` parameter **must** be a keyword argument or the first
524
+ positional ``Transaction`` so we can read its counters.
525
+ """
526
+
527
+ @wraps(func)
528
+ def wrapper(*args, **kwds):
529
+ t0 = time.perf_counter()
530
+ result = func(*args, **kwds)
531
+ wall_ms = (time.perf_counter() - t0) * 1000
532
+
533
+ # Find the Transaction among the arguments.
534
+ tx = kwds.get("tx")
535
+ if tx is None:
536
+ for a in args:
537
+ if isinstance(a, Transaction):
538
+ tx = a
539
+ break
540
+
541
+ qcount = getattr(tx, "_query_count", 0) if tx else 0
542
+ qtime = getattr(tx, "_query_time_ms", 0.0) if tx else 0.0
543
+
544
+ logger.info(
545
+ "perf_log %s: wall=%.1f ms, queries=%d, query_time=%.1f ms",
546
+ getattr(func, "__qualname__", func.__name__),
547
+ wall_ms, qcount, qtime,
548
+ extra={
549
+ "wall_ms": round(wall_ms, 1),
550
+ "query_count": qcount,
551
+ "query_time_ms": round(qtime, 1),
552
+ },
553
+ )
554
+ return result
555
+
556
+ return wrapper
557
+
499
558
  @property
500
559
  def driver(self):
501
560
  return self.__driver
@@ -714,6 +773,10 @@ class Engine:
714
773
  if relevant_frames:
715
774
  enhanced_message += "\n\nCall Context:\n" + "".join(relevant_frames)
716
775
 
776
+ # Mask any credentials that may have leaked into driver error messages
777
+ # (e.g. connection strings containing password=...).
778
+ enhanced_message = mask_sensitive_in_string(enhanced_message)
779
+
717
780
  # Note: SQL formatting for logging is available via _format_sql_with_params,
718
781
  # but we intentionally avoid eager logging here.
719
782
 
@@ -1,3 +1,4 @@
1
+ import logging
1
2
  import re
2
3
  import warnings
3
4
  import sqlparse
@@ -12,6 +13,8 @@ from velocity.db.core.decorators import (
12
13
  reset_id_on_dup_key,
13
14
  )
14
15
 
16
+ _ddl_logger = logging.getLogger(__name__)
17
+
15
18
 
16
19
  class Query:
17
20
  """
@@ -252,6 +255,7 @@ class Table:
252
255
  )
253
256
  if kwds.get("sql_only", False):
254
257
  return sql, vals
258
+ _ddl_logger.warning("DDL CREATE INDEX on %s columns=%s unique=%s", self.name, columns, unique)
255
259
  self.tx.execute(sql, vals, cursor=self.cursor())
256
260
 
257
261
  def create_indexes(self, indexes, **kwds):
@@ -317,6 +321,7 @@ class Table:
317
321
  sql, vals = self.sql.drop_index(self.name, columns)
318
322
  if kwds.get("sql_only", False):
319
323
  return sql, vals
324
+ _ddl_logger.warning("DDL DROP INDEX on %s columns=%s", self.name, columns)
320
325
  self.tx.execute(sql, vals, cursor=self.cursor())
321
326
 
322
327
  @return_default(None)
@@ -325,6 +330,7 @@ class Table:
325
330
  Drops a column from this table.
326
331
  """
327
332
  sql, vals = self.sql.drop_column(self.name, column)
333
+ _ddl_logger.warning("DDL DROP COLUMN %s on %s", column, self.name)
328
334
  self.tx.execute(sql, vals, cursor=self.cursor())
329
335
 
330
336
  def create(self, columns=None, drop=False):
@@ -334,6 +340,7 @@ class Table:
334
340
  """
335
341
  columns = columns or {}
336
342
  sql, vals = self.sql.create_table(self.name, columns, drop)
343
+ _ddl_logger.warning("DDL CREATE TABLE %s columns=%s drop=%s", self.name, list(columns.keys()), drop)
337
344
  self.tx.execute(sql, vals, cursor=self.cursor())
338
345
 
339
346
  def drop(self):
@@ -341,6 +348,7 @@ class Table:
341
348
  Drops this table if it exists.
342
349
  """
343
350
  sql, vals = self.sql.drop_table(self.name)
351
+ _ddl_logger.warning("DDL DROP TABLE %s", self.name)
344
352
  self.tx.execute(sql, vals, cursor=self.cursor())
345
353
 
346
354
  def exists(self):
@@ -625,6 +633,7 @@ class Table:
625
633
  )
626
634
  if kwds.get("sql_only", False):
627
635
  return sql, vals
636
+ _ddl_logger.warning("DDL CREATE FOREIGN KEY on %s columns=%s ref=%s(%s)", self.name, columns, key_to_table, key_to_columns)
628
637
  return self.tx.execute(sql, vals, cursor=self.cursor())
629
638
 
630
639
  def drop_foreign_key(self, columns, key_to_table, key_to_columns="sys_id", **kwds):
@@ -636,6 +645,7 @@ class Table:
636
645
  )
637
646
  if kwds.get("sql_only", False):
638
647
  return sql, vals
648
+ _ddl_logger.warning("DDL DROP FOREIGN KEY on %s columns=%s ref=%s(%s)", self.name, columns, key_to_table, key_to_columns)
639
649
  return self.tx.execute(sql, vals, cursor=self.cursor())
640
650
 
641
651
  def rename(self, name, **kwds):
@@ -645,6 +655,7 @@ class Table:
645
655
  sql, vals = self.sql.rename_table(self.name, name)
646
656
  if kwds.get("sql_only", False):
647
657
  return sql, vals
658
+ _ddl_logger.warning("DDL RENAME TABLE %s to %s", self.name, name)
648
659
  self.tx.execute(sql, vals, cursor=self.cursor())
649
660
  self.name = name
650
661
 
@@ -784,6 +795,7 @@ class Table:
784
795
  return statements[0]
785
796
  return statements
786
797
 
798
+ _ddl_logger.warning("DDL ALTER TABLE %s columns=%s mode=%s", self.name, list(columns.keys()), mode)
787
799
  for sql, vals in statements:
788
800
  if not sql:
789
801
  continue
@@ -808,6 +820,7 @@ class Table:
808
820
  )
809
821
  if kwds.get("sql_only", False):
810
822
  return sql, vals
823
+ _ddl_logger.warning("DDL ALTER COLUMN TYPE on %s column=%s", self.name, column)
811
824
  self.tx.execute(sql, vals, cursor=self.cursor())
812
825
 
813
826
  @create_missing
@@ -1069,9 +1082,10 @@ class Table:
1069
1082
  """
1070
1083
  Returns the sum of the given column across rows matching `where`.
1071
1084
  """
1085
+ qcol = self.sql.quote_identifier(column)
1072
1086
  sql, vals = self.sql.select(
1073
1087
  self.tx,
1074
- columns=f"coalesce(sum(coalesce({column},0)),0)",
1088
+ columns=f"coalesce(sum(coalesce({qcol},0)),0)",
1075
1089
  table=self.name,
1076
1090
  where=where,
1077
1091
  )
@@ -1322,6 +1336,7 @@ class Table:
1322
1336
  )
1323
1337
  if kwds.get("sql_only", False):
1324
1338
  return sql, vals
1339
+ _ddl_logger.warning("DDL CREATE VIEW %s temp=%s", name, temp)
1325
1340
  return self.tx.execute(sql, vals)
1326
1341
 
1327
1342
  def drop_view(self, name, silent=True, **kwds):
@@ -1331,6 +1346,7 @@ class Table:
1331
1346
  sql, vals = self.sql.drop_view(name=name, silent=silent)
1332
1347
  if kwds.get("sql_only", False):
1333
1348
  return sql, vals
1349
+ _ddl_logger.warning("DDL DROP VIEW %s", name)
1334
1350
  return self.tx.execute(sql, vals)
1335
1351
 
1336
1352
  def alter_trigger(self, name="USER", state="ENABLE", **kwds):
@@ -1340,6 +1356,7 @@ class Table:
1340
1356
  sql, vals = self.sql.alter_trigger(table=self.name, state=state, name=name)
1341
1357
  if kwds.get("sql_only", False):
1342
1358
  return sql, vals
1359
+ _ddl_logger.warning("DDL ALTER TRIGGER %s on %s state=%s", name, self.name, state)
1343
1360
  return self.tx.execute(sql, vals)
1344
1361
 
1345
1362
  def rename_column(self, orig, new, **kwds):
@@ -1349,6 +1366,7 @@ class Table:
1349
1366
  sql, vals = self.sql.rename_column(table=self.name, orig=orig, new=new)
1350
1367
  if kwds.get("sql_only", False):
1351
1368
  return sql, vals
1369
+ _ddl_logger.warning("DDL RENAME COLUMN on %s %s -> %s", self.name, orig, new)
1352
1370
  return self.tx.execute(sql, vals)
1353
1371
 
1354
1372
  def set_sequence(self, next_value=1000, **kwds):
@@ -1358,6 +1376,7 @@ class Table:
1358
1376
  sql, vals = self.sql.set_sequence(table=self.name, next_value=next_value)
1359
1377
  if kwds.get("sql_only", False):
1360
1378
  return sql, vals
1379
+ _ddl_logger.warning("DDL SET SEQUENCE on %s next_value=%s", self.name, next_value)
1361
1380
  return self.tx.execute(sql, vals).scalar()
1362
1381
 
1363
1382
  def get_sequence(self, **kwds):
@@ -1396,8 +1415,9 @@ class Table:
1396
1415
  """
1397
1416
  Returns the MAX() of the specified column.
1398
1417
  """
1418
+ qcol = self.sql.quote_identifier(column)
1399
1419
  sql, vals = self.sql.select(
1400
- self.tx, columns=f"max({column})", table=self.name, where=where
1420
+ self.tx, columns=f"max({qcol})", table=self.name, where=where
1401
1421
  )
1402
1422
  if kwds.get("sql_only", False):
1403
1423
  return sql, vals
@@ -1408,8 +1428,9 @@ class Table:
1408
1428
  """
1409
1429
  Returns the MIN() of the specified column.
1410
1430
  """
1431
+ qcol = self.sql.quote_identifier(column)
1411
1432
  sql, vals = self.sql.select(
1412
- self.tx, columns=f"min({column})", table=self.name, where=where
1433
+ self.tx, columns=f"min({qcol})", table=self.name, where=where
1413
1434
  )
1414
1435
  if kwds.get("sql_only", False):
1415
1436
  return sql, vals
@@ -1,4 +1,6 @@
1
+ import logging
1
2
  import os
3
+ import time as _time
2
4
  import traceback
3
5
  from collections import OrderedDict
4
6
 
@@ -14,9 +16,60 @@ from velocity.misc.db import randomword
14
16
 
15
17
  debug = False
16
18
 
19
+ _logger = logging.getLogger("velocity.db.transaction")
20
+
17
21
  # Default maximum number of cached query results per transaction.
18
22
  _DEFAULT_QUERY_CACHE_SIZE = int(os.environ.get("VELOCITY_QUERY_CACHE_SIZE", "100"))
19
23
 
24
+ # Slow-query threshold in milliseconds (0 = disabled).
25
+ _SLOW_QUERY_MS = int(os.environ.get("VELOCITY_SLOW_QUERY_MS", "500"))
26
+
27
+ _SQL_OP_PREFIXES = {
28
+ "select": "SELECT",
29
+ "insert": "INSERT",
30
+ "update": "UPDATE",
31
+ "delete": "DELETE",
32
+ "create": "DDL",
33
+ "alter": "DDL",
34
+ "drop": "DDL",
35
+ "set": "SET",
36
+ }
37
+
38
+
39
+ def _classify_sql(sql):
40
+ """Return a short operation label (SELECT, INSERT, …) from a SQL string."""
41
+ if not sql:
42
+ return "OTHER"
43
+ first = sql.lstrip().split(None, 1)[0].lower() if sql.strip() else ""
44
+ return _SQL_OP_PREFIXES.get(first, "OTHER")
45
+
46
+
47
+ def _extract_table_name(sql):
48
+ """Best-effort extraction of the main table name from a SQL statement."""
49
+ if not sql:
50
+ return None
51
+ upper = sql.strip().upper()
52
+ lowered = sql.strip()
53
+ try:
54
+ if upper.startswith("SELECT"):
55
+ idx = upper.find(" FROM ")
56
+ if idx != -1:
57
+ rest = lowered[idx + 6:].strip()
58
+ return rest.split()[0].strip('"').strip("'") if rest else None
59
+ elif upper.startswith("INSERT"):
60
+ idx = upper.find(" INTO ")
61
+ if idx != -1:
62
+ rest = lowered[idx + 6:].strip()
63
+ return rest.split()[0].strip('"').strip("'") if rest else None
64
+ elif upper.startswith(("UPDATE", "DELETE FROM")):
65
+ parts = lowered.split()
66
+ if len(parts) >= 2:
67
+ token = parts[2] if upper.startswith("DELETE") and len(parts) > 2 else parts[1]
68
+ return token.strip('"').strip("'")
69
+ except (IndexError, ValueError):
70
+ pass
71
+ return None
72
+
20
73
 
21
74
  class Transaction:
22
75
  """
@@ -32,6 +85,10 @@ class Transaction:
32
85
  # R5 — Transaction-scoped query cache (opt-in via cache=True on select).
33
86
  self.__query_cache: OrderedDict = OrderedDict()
34
87
  self.__query_cache_max = _DEFAULT_QUERY_CACHE_SIZE
88
+ # R12 — Observability counters.
89
+ self._query_count = 0
90
+ self._query_time_ms = 0.0
91
+ self._return_default_count = 0
35
92
 
36
93
  def __str__(self):
37
94
  config = mask_config_for_display(self.engine.config)
@@ -119,6 +176,7 @@ class Transaction:
119
176
  if prepare is None:
120
177
  prepare = getattr(self.engine, "prepare_enabled", False)
121
178
 
179
+ t0 = _time.perf_counter()
122
180
  try:
123
181
  if parms:
124
182
  cursor.execute(sql, parms, prepare=prepare)
@@ -136,6 +194,24 @@ class Transaction:
136
194
  except Exception as e:
137
195
  raise self.engine.process_error(e, sql, parms)
138
196
 
197
+ elapsed_ms = (_time.perf_counter() - t0) * 1000
198
+ self._query_count += 1
199
+ self._query_time_ms += elapsed_ms
200
+
201
+ # R12 — Slow query logging.
202
+ if _SLOW_QUERY_MS and elapsed_ms > _SLOW_QUERY_MS:
203
+ op = _classify_sql(sql)
204
+ tbl = _extract_table_name(sql)
205
+ _logger.warning(
206
+ "Slow query (%s): %.1f ms table=%s",
207
+ op, elapsed_ms, tbl,
208
+ extra={
209
+ "query_duration_ms": round(elapsed_ms, 1),
210
+ "table_name": tbl,
211
+ "operation": op,
212
+ },
213
+ )
214
+
139
215
  if single:
140
216
  self.connection.autocommit = False
141
217
 
@@ -195,6 +271,15 @@ class Transaction:
195
271
  if debug:
196
272
  print(f"{id(self)} --- connection commit.")
197
273
  self.connection.commit()
274
+ if self._query_count:
275
+ _logger.debug(
276
+ "Transaction commit: %d queries in %.1f ms",
277
+ self._query_count, self._query_time_ms,
278
+ extra={
279
+ "query_count": self._query_count,
280
+ "query_time_ms": round(self._query_time_ms, 1),
281
+ },
282
+ )
198
283
 
199
284
  def rollback(self):
200
285
  """
@@ -38,6 +38,16 @@ class BaseSQLDialect(ABC):
38
38
  DatabaseObjectExistsErrorCodes: List[str] = []
39
39
  DataIntegrityErrorCodes: List[str] = []
40
40
 
41
+ @classmethod
42
+ def quote_identifier(cls, name: str) -> str:
43
+ """Always-quote a single SQL identifier to prevent injection.
44
+
45
+ Uses standard SQL double-quoting. Dialect subclasses override for
46
+ MySQL (backticks) and SQL Server (brackets).
47
+ """
48
+ escaped = name.replace('"', '""')
49
+ return f'"{escaped}"'
50
+
41
51
  @classmethod
42
52
  @abstractmethod
43
53
  def get_error(cls, e: Exception) -> Optional[str]:
@@ -48,6 +48,11 @@ class SQL(BaseSQLDialect):
48
48
  type_column_identifier = "DATA_TYPE"
49
49
  is_nullable = "IS_NULLABLE"
50
50
 
51
+ @classmethod
52
+ def quote_identifier(cls, name: str) -> str:
53
+ escaped = name.replace('`', '``')
54
+ return f'`{escaped}`'
55
+
51
56
  default_schema = ""
52
57
 
53
58
  ApplicationErrorCodes = []
@@ -48,6 +48,11 @@ class SQL(BaseSQLDialect):
48
48
  type_column_identifier = "DATA_TYPE"
49
49
  is_nullable = "IS_NULLABLE"
50
50
 
51
+ @classmethod
52
+ def quote_identifier(cls, name: str) -> str:
53
+ escaped = name.replace(']', ']]')
54
+ return f'[{escaped}]'
55
+
51
56
  default_schema = "dbo"
52
57
 
53
58
  # SQL Server error numbers
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: velocity-python
3
- Version: 0.1.2
3
+ Version: 0.1.4
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
@@ -159,6 +159,7 @@ tests/test_iconv_money_to_cents.py
159
159
  tests/test_lambda_handler.py
160
160
  tests/test_lambda_handler_auth.py
161
161
  tests/test_mixins_import.py
162
+ tests/test_observability.py
162
163
  tests/test_payment_braintree_adapter.py
163
164
  tests/test_payment_demo_profiles.py
164
165
  tests/test_payment_profiles.py
@@ -169,6 +170,7 @@ tests/test_psycopg3_upgrade.py
169
170
  tests/test_query_cache.py
170
171
  tests/test_row_batch_update.py
171
172
  tests/test_row_cache_staleness.py
173
+ tests/test_security_hardening.py
172
174
  tests/test_sqs_per_record_transactions.py
173
175
  tests/test_sys_modified_count_postgres_demo.py
174
176
  tests/test_table_alter.py