velocity-python 0.1.3__tar.gz → 0.1.5__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 (180) hide show
  1. {velocity_python-0.1.3 → velocity_python-0.1.5}/PKG-INFO +1 -1
  2. {velocity_python-0.1.3 → velocity_python-0.1.5}/pyproject.toml +1 -1
  3. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/__init__.py +1 -1
  4. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/core/decorators.py +6 -0
  5. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/core/engine.py +63 -4
  6. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/core/result.py +22 -0
  7. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/core/row.py +29 -0
  8. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/core/table.py +31 -5
  9. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/core/transaction.py +110 -0
  10. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity_python.egg-info/PKG-INFO +1 -1
  11. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity_python.egg-info/SOURCES.txt +2 -0
  12. velocity_python-0.1.5/tests/test_n_plus_one.py +359 -0
  13. velocity_python-0.1.5/tests/test_observability.py +443 -0
  14. {velocity_python-0.1.3 → velocity_python-0.1.5}/LICENSE +0 -0
  15. {velocity_python-0.1.3 → velocity_python-0.1.5}/README.md +0 -0
  16. {velocity_python-0.1.3 → velocity_python-0.1.5}/setup.cfg +0 -0
  17. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/app/__init__.py +0 -0
  18. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/app/formbuilder/__init__.py +0 -0
  19. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/app/formbuilder/reshaper.py +0 -0
  20. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/app/invoices.py +0 -0
  21. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/app/orders.py +0 -0
  22. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/app/payments.py +0 -0
  23. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/app/purchase_orders.py +0 -0
  24. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/app/tests/__init__.py +0 -0
  25. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/app/tests/test_email_processing.py +0 -0
  26. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/app/tests/test_payment_profile_sorting.py +0 -0
  27. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/app/tests/test_spreadsheet_functions.py +0 -0
  28. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/app/validators/__init__.py +0 -0
  29. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/app/validators/formbuilder_template.py +0 -0
  30. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/aws/__init__.py +0 -0
  31. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/aws/amplify.py +0 -0
  32. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/aws/amplify_build.py +0 -0
  33. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/aws/handlers/__init__.py +0 -0
  34. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/aws/handlers/base_handler.py +0 -0
  35. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/aws/handlers/context.py +0 -0
  36. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/aws/handlers/context_factory.py +0 -0
  37. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/aws/handlers/exceptions.py +0 -0
  38. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/aws/handlers/lambda_handler.py +0 -0
  39. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
  40. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/aws/handlers/mixins/data_service.py +0 -0
  41. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
  42. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/aws/handlers/perf.py +0 -0
  43. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/aws/handlers/response.py +0 -0
  44. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/aws/handlers/sqs_handler.py +0 -0
  45. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/aws/tests/__init__.py +0 -0
  46. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
  47. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
  48. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/aws/tests/test_response.py +0 -0
  49. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/__init__.py +0 -0
  50. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/core/__init__.py +0 -0
  51. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/core/column.py +0 -0
  52. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/core/database.py +0 -0
  53. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/core/sequence.py +0 -0
  54. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/core/view.py +0 -0
  55. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/exceptions.py +0 -0
  56. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/servers/__init__.py +0 -0
  57. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/servers/base/__init__.py +0 -0
  58. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/servers/base/initializer.py +0 -0
  59. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/servers/base/operators.py +0 -0
  60. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/servers/base/sql.py +0 -0
  61. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/servers/base/types.py +0 -0
  62. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/servers/mysql/__init__.py +0 -0
  63. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/servers/mysql/operators.py +0 -0
  64. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/servers/mysql/reserved.py +0 -0
  65. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/servers/mysql/sql.py +0 -0
  66. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/servers/mysql/types.py +0 -0
  67. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/servers/postgres/__init__.py +0 -0
  68. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/servers/postgres/operators.py +0 -0
  69. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/servers/postgres/reserved.py +0 -0
  70. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/servers/postgres/sql.py +0 -0
  71. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/servers/postgres/types.py +0 -0
  72. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/servers/sqlite/__init__.py +0 -0
  73. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/servers/sqlite/operators.py +0 -0
  74. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/servers/sqlite/reserved.py +0 -0
  75. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/servers/sqlite/sql.py +0 -0
  76. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/servers/sqlite/types.py +0 -0
  77. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
  78. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/servers/sqlserver/operators.py +0 -0
  79. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
  80. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/servers/sqlserver/sql.py +0 -0
  81. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/servers/sqlserver/types.py +0 -0
  82. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/servers/tablehelper.py +0 -0
  83. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/tests/__init__.py +0 -0
  84. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/tests/common_db_test.py +0 -0
  85. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/tests/postgres/__init__.py +0 -0
  86. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/tests/postgres/common.py +0 -0
  87. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/tests/postgres/test_column.py +0 -0
  88. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/tests/postgres/test_connections.py +0 -0
  89. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/tests/postgres/test_database.py +0 -0
  90. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/tests/postgres/test_engine.py +0 -0
  91. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
  92. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/tests/postgres/test_imports.py +0 -0
  93. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/tests/postgres/test_result.py +0 -0
  94. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/tests/postgres/test_row.py +0 -0
  95. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
  96. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
  97. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
  98. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
  99. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
  100. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/tests/postgres/test_table.py +0 -0
  101. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
  102. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
  103. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/tests/sql/__init__.py +0 -0
  104. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/tests/sql/common.py +0 -0
  105. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
  106. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
  107. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
  108. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/tests/test_db_utils.py +0 -0
  109. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/tests/test_postgres.py +0 -0
  110. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
  111. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
  112. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/tests/test_result_caching.py +0 -0
  113. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
  114. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
  115. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
  116. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
  117. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/tests/test_sql_builder.py +0 -0
  118. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/tests/test_tablehelper.py +0 -0
  119. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/tests/test_view_helper.py +0 -0
  120. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/db/utils.py +0 -0
  121. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/logging.py +0 -0
  122. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/misc/__init__.py +0 -0
  123. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/misc/conv/__init__.py +0 -0
  124. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/misc/conv/iconv.py +0 -0
  125. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/misc/conv/oconv.py +0 -0
  126. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/misc/db.py +0 -0
  127. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/misc/export.py +0 -0
  128. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/misc/format.py +0 -0
  129. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/misc/mail.py +0 -0
  130. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/misc/merge.py +0 -0
  131. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/misc/tests/__init__.py +0 -0
  132. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/misc/tests/test_db.py +0 -0
  133. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/misc/tests/test_fix.py +0 -0
  134. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/misc/tests/test_format.py +0 -0
  135. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/misc/tests/test_iconv.py +0 -0
  136. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/misc/tests/test_merge.py +0 -0
  137. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/misc/tests/test_oconv.py +0 -0
  138. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/misc/tests/test_original_error.py +0 -0
  139. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/misc/tests/test_timer.py +0 -0
  140. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/misc/timer.py +0 -0
  141. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/misc/tools.py +0 -0
  142. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/payment/__init__.py +0 -0
  143. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/payment/authorizenet_adapter.py +0 -0
  144. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/payment/base_adapter.py +0 -0
  145. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/payment/braintree_adapter.py +0 -0
  146. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/payment/charge_rules.py +0 -0
  147. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/payment/demo_profiles.py +0 -0
  148. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/payment/profiles.py +0 -0
  149. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/payment/router.py +0 -0
  150. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity/payment/stripe_adapter.py +0 -0
  151. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity_python.egg-info/dependency_links.txt +0 -0
  152. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity_python.egg-info/requires.txt +0 -0
  153. {velocity_python-0.1.3 → velocity_python-0.1.5}/src/velocity_python.egg-info/top_level.txt +0 -0
  154. {velocity_python-0.1.3 → velocity_python-0.1.5}/tests/test_amplify_build.py +0 -0
  155. {velocity_python-0.1.3 → velocity_python-0.1.5}/tests/test_batch_operations.py +0 -0
  156. {velocity_python-0.1.3 → velocity_python-0.1.5}/tests/test_concurrency_safety.py +0 -0
  157. {velocity_python-0.1.3 → velocity_python-0.1.5}/tests/test_connection_pool.py +0 -0
  158. {velocity_python-0.1.3 → velocity_python-0.1.5}/tests/test_connection_resilience.py +0 -0
  159. {velocity_python-0.1.3 → velocity_python-0.1.5}/tests/test_decorators.py +0 -0
  160. {velocity_python-0.1.3 → velocity_python-0.1.5}/tests/test_formbuilder_reshaper.py +0 -0
  161. {velocity_python-0.1.3 → velocity_python-0.1.5}/tests/test_formbuilder_template_validator.py +0 -0
  162. {velocity_python-0.1.3 → velocity_python-0.1.5}/tests/test_iconv_money_to_cents.py +0 -0
  163. {velocity_python-0.1.3 → velocity_python-0.1.5}/tests/test_lambda_handler.py +0 -0
  164. {velocity_python-0.1.3 → velocity_python-0.1.5}/tests/test_lambda_handler_auth.py +0 -0
  165. {velocity_python-0.1.3 → velocity_python-0.1.5}/tests/test_mixins_import.py +0 -0
  166. {velocity_python-0.1.3 → velocity_python-0.1.5}/tests/test_payment_braintree_adapter.py +0 -0
  167. {velocity_python-0.1.3 → velocity_python-0.1.5}/tests/test_payment_demo_profiles.py +0 -0
  168. {velocity_python-0.1.3 → velocity_python-0.1.5}/tests/test_payment_profiles.py +0 -0
  169. {velocity_python-0.1.3 → velocity_python-0.1.5}/tests/test_payment_router.py +0 -0
  170. {velocity_python-0.1.3 → velocity_python-0.1.5}/tests/test_payment_stripe_adapter.py +0 -0
  171. {velocity_python-0.1.3 → velocity_python-0.1.5}/tests/test_prepared_statements.py +0 -0
  172. {velocity_python-0.1.3 → velocity_python-0.1.5}/tests/test_psycopg3_upgrade.py +0 -0
  173. {velocity_python-0.1.3 → velocity_python-0.1.5}/tests/test_query_cache.py +0 -0
  174. {velocity_python-0.1.3 → velocity_python-0.1.5}/tests/test_row_batch_update.py +0 -0
  175. {velocity_python-0.1.3 → velocity_python-0.1.5}/tests/test_row_cache_staleness.py +0 -0
  176. {velocity_python-0.1.3 → velocity_python-0.1.5}/tests/test_security_hardening.py +0 -0
  177. {velocity_python-0.1.3 → velocity_python-0.1.5}/tests/test_sqs_per_record_transactions.py +0 -0
  178. {velocity_python-0.1.3 → velocity_python-0.1.5}/tests/test_sys_modified_count_postgres_demo.py +0 -0
  179. {velocity_python-0.1.3 → velocity_python-0.1.5}/tests/test_table_alter.py +0 -0
  180. {velocity_python-0.1.3 → velocity_python-0.1.5}/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.3
3
+ Version: 0.1.5
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.3"
7
+ version = "0.1.5"
8
8
  authors = [
9
9
  { name="Velocity Team", email="info@codeclubs.org" },
10
10
  ]
@@ -1,4 +1,4 @@
1
- __version__ = version = "0.1.3"
1
+ __version__ = version = "0.1.5"
2
2
 
3
3
  import importlib as _importlib
4
4
 
@@ -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
@@ -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
@@ -1,5 +1,6 @@
1
1
  import warnings
2
2
  from velocity.misc.format import to_json
3
+ from velocity.db.core.row import Row
3
4
 
4
5
 
5
6
  class Result:
@@ -310,6 +311,27 @@ class Result:
310
311
  self.transform = lambda row: row
311
312
  return self
312
313
 
314
+ def as_rows(self, table):
315
+ """Transform each row into a :class:`Row` with a pre-populated cache.
316
+
317
+ This converts a ``SELECT`` result set into Row objects without
318
+ triggering extra queries, preventing the N+1 pattern::
319
+
320
+ rows = table.select(where={"status": "active"}).as_rows(table)
321
+ for row in rows.all():
322
+ print(row["name"]) # no extra SELECT
323
+
324
+ Args:
325
+ table: The :class:`Table` the rows belong to.
326
+
327
+ Returns:
328
+ This ``Result`` (for chaining). Each iteration yields a :class:`Row`.
329
+ """
330
+ self.as_dict() # ensure we get dicts first
331
+ _original = self.transform
332
+ self.transform = lambda raw_row: Row._from_data(table, _original(raw_row))
333
+ return self
334
+
313
335
  def as_simple_list(self, pos=0):
314
336
  """
315
337
  Transform each row into the single value at position `pos`.
@@ -51,6 +51,35 @@ class Row(MutableMapping):
51
51
  if lock:
52
52
  self.lock()
53
53
 
54
+ @classmethod
55
+ def _from_data(cls, table, data):
56
+ """Create a Row with its cache pre-populated from *data*.
57
+
58
+ This avoids the lazy SELECT that normally fires on first attribute
59
+ access, eliminating the N+1 problem when you already have the full
60
+ row dict (e.g. from a bulk ``SELECT``).
61
+
62
+ Args:
63
+ table: The :class:`Table` instance this row belongs to.
64
+ data: A ``dict`` of ``{column_name: value}`` for the row.
65
+ Must include the primary-key column(s).
66
+
67
+ Returns:
68
+ A fully-initialised :class:`Row` whose local cache is *data*.
69
+ """
70
+ row = cls.__new__(cls)
71
+ pk = {k: data[k] for k in ("sys_id",) if k in data}
72
+ object.__setattr__(row, "table", table)
73
+ object.__setattr__(row, "pk", pk)
74
+ object.__setattr__(row, "_cache", dict(data))
75
+ object.__setattr__(row, "_column_set", None)
76
+ object.__setattr__(row, "_batching", False)
77
+ object.__setattr__(row, "_pending", {})
78
+ object.__setattr__(row, "_cache_ttl", None)
79
+ object.__setattr__(row, "_cache_time", _time.monotonic())
80
+ object.__setattr__(row, "_no_cache", False)
81
+ return row
82
+
54
83
  # ------------------------------------------------------------------
55
84
  # Cache management
56
85
  # ------------------------------------------------------------------
@@ -423,14 +423,40 @@ class Table:
423
423
  r = self.find(key)
424
424
  return r.to_dict() if r else {}
425
425
 
426
- def rows(self, where=None, orderby=None, qty=None, lock=None, skip_locked=None, cache_ttl=None, no_cache=False):
426
+ def rows(self, where=None, orderby=None, qty=None, lock=None, skip_locked=None, cache_ttl=None, no_cache=False, prefetch=False):
427
427
  """
428
428
  Generator that yields Row objects matching `where`, up to `qty`.
429
+
430
+ When *prefetch* is True a single SELECT fetches all matching data
431
+ and each yielded Row has its cache pre-populated, avoiding the
432
+ N+1 pattern where every Row triggers a lazy SELECT on first access.
429
433
  """
430
- for key in self.ids(
431
- where=where, orderby=orderby, qty=qty, lock=lock, skip_locked=skip_locked
432
- ):
433
- yield Row(self, key, lock=lock, cache_ttl=cache_ttl, no_cache=no_cache)
434
+ if prefetch:
435
+ result = self.select(
436
+ where=where, orderby=orderby, qty=qty, lock=lock, skip_locked=skip_locked,
437
+ )
438
+ for data in result:
439
+ yield Row._from_data(self, data)
440
+ else:
441
+ for key in self.ids(
442
+ where=where, orderby=orderby, qty=qty, lock=lock, skip_locked=skip_locked
443
+ ):
444
+ yield Row(self, key, lock=lock, cache_ttl=cache_ttl, no_cache=no_cache)
445
+
446
+ def select_rows(self, where=None, orderby=None, qty=None, lock=None, skip_locked=None):
447
+ """Return a list of Row objects with pre-populated caches from a single SELECT.
448
+
449
+ This is the recommended way to load multiple rows when you need
450
+ Row-level write-through behaviour but want to avoid the N+1
451
+ problem inherent in :meth:`rows`.
452
+
453
+ Equivalent to ``list(table.rows(where=..., prefetch=True))``
454
+ but expressed as a convenience method.
455
+ """
456
+ result = self.select(
457
+ where=where, orderby=orderby, qty=qty, lock=lock, skip_locked=skip_locked,
458
+ )
459
+ return [Row._from_data(self, data) for data in result]
434
460
 
435
461
  def ids(
436
462
  self,
@@ -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,65 @@ 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
+ # N+1 detection: warn when the same table is SELECTed more than this many
28
+ # times within a single transaction. 0 = disabled. Only active when the
29
+ # module-level ``debug`` flag is True.
30
+ _N_PLUS_1_THRESHOLD = int(os.environ.get("VELOCITY_N_PLUS_1_THRESHOLD", "10"))
31
+
32
+ _SQL_OP_PREFIXES = {
33
+ "select": "SELECT",
34
+ "insert": "INSERT",
35
+ "update": "UPDATE",
36
+ "delete": "DELETE",
37
+ "create": "DDL",
38
+ "alter": "DDL",
39
+ "drop": "DDL",
40
+ "set": "SET",
41
+ }
42
+
43
+
44
+ def _classify_sql(sql):
45
+ """Return a short operation label (SELECT, INSERT, …) from a SQL string."""
46
+ if not sql:
47
+ return "OTHER"
48
+ first = sql.lstrip().split(None, 1)[0].lower() if sql.strip() else ""
49
+ return _SQL_OP_PREFIXES.get(first, "OTHER")
50
+
51
+
52
+ def _extract_table_name(sql):
53
+ """Best-effort extraction of the main table name from a SQL statement."""
54
+ if not sql:
55
+ return None
56
+ upper = sql.strip().upper()
57
+ lowered = sql.strip()
58
+ try:
59
+ if upper.startswith("SELECT"):
60
+ idx = upper.find(" FROM ")
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("INSERT"):
65
+ idx = upper.find(" INTO ")
66
+ if idx != -1:
67
+ rest = lowered[idx + 6:].strip()
68
+ return rest.split()[0].strip('"').strip("'") if rest else None
69
+ elif upper.startswith(("UPDATE", "DELETE FROM")):
70
+ parts = lowered.split()
71
+ if len(parts) >= 2:
72
+ token = parts[2] if upper.startswith("DELETE") and len(parts) > 2 else parts[1]
73
+ return token.strip('"').strip("'")
74
+ except (IndexError, ValueError):
75
+ pass
76
+ return None
77
+
20
78
 
21
79
  class Transaction:
22
80
  """
@@ -32,6 +90,13 @@ class Transaction:
32
90
  # R5 — Transaction-scoped query cache (opt-in via cache=True on select).
33
91
  self.__query_cache: OrderedDict = OrderedDict()
34
92
  self.__query_cache_max = _DEFAULT_QUERY_CACHE_SIZE
93
+ # R12 — Observability counters.
94
+ self._query_count = 0
95
+ self._query_time_ms = 0.0
96
+ self._return_default_count = 0
97
+ # R14 — N+1 detection: per-table SELECT counts.
98
+ self._table_select_counts: dict[str, int] = {}
99
+ self._n1_warned: set[str] = set()
35
100
 
36
101
  def __str__(self):
37
102
  config = mask_config_for_display(self.engine.config)
@@ -119,6 +184,7 @@ class Transaction:
119
184
  if prepare is None:
120
185
  prepare = getattr(self.engine, "prepare_enabled", False)
121
186
 
187
+ t0 = _time.perf_counter()
122
188
  try:
123
189
  if parms:
124
190
  cursor.execute(sql, parms, prepare=prepare)
@@ -136,6 +202,41 @@ class Transaction:
136
202
  except Exception as e:
137
203
  raise self.engine.process_error(e, sql, parms)
138
204
 
205
+ elapsed_ms = (_time.perf_counter() - t0) * 1000
206
+ self._query_count += 1
207
+ self._query_time_ms += elapsed_ms
208
+
209
+ # R12 — Slow query logging.
210
+ if _SLOW_QUERY_MS and elapsed_ms > _SLOW_QUERY_MS:
211
+ op = _classify_sql(sql)
212
+ tbl = _extract_table_name(sql)
213
+ _logger.warning(
214
+ "Slow query (%s): %.1f ms table=%s",
215
+ op, elapsed_ms, tbl,
216
+ extra={
217
+ "query_duration_ms": round(elapsed_ms, 1),
218
+ "table_name": tbl,
219
+ "operation": op,
220
+ },
221
+ )
222
+
223
+ # R14 — N+1 detection (only when debug=True).
224
+ if debug and _N_PLUS_1_THRESHOLD:
225
+ op = _classify_sql(sql)
226
+ if op == "SELECT":
227
+ tbl = _extract_table_name(sql)
228
+ if tbl:
229
+ self._table_select_counts[tbl] = self._table_select_counts.get(tbl, 0) + 1
230
+ count = self._table_select_counts[tbl]
231
+ if count > _N_PLUS_1_THRESHOLD and tbl not in self._n1_warned:
232
+ self._n1_warned.add(tbl)
233
+ _logger.warning(
234
+ "Possible N+1: table %s queried %d times in this transaction "
235
+ "(threshold=%d). Consider using prefetch=True or select_rows().",
236
+ tbl, count, _N_PLUS_1_THRESHOLD,
237
+ extra={"table_name": tbl, "select_count": count},
238
+ )
239
+
139
240
  if single:
140
241
  self.connection.autocommit = False
141
242
 
@@ -195,6 +296,15 @@ class Transaction:
195
296
  if debug:
196
297
  print(f"{id(self)} --- connection commit.")
197
298
  self.connection.commit()
299
+ if self._query_count:
300
+ _logger.debug(
301
+ "Transaction commit: %d queries in %.1f ms",
302
+ self._query_count, self._query_time_ms,
303
+ extra={
304
+ "query_count": self._query_count,
305
+ "query_time_ms": round(self._query_time_ms, 1),
306
+ },
307
+ )
198
308
 
199
309
  def rollback(self):
200
310
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: velocity-python
3
- Version: 0.1.3
3
+ Version: 0.1.5
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,8 @@ 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_n_plus_one.py
163
+ tests/test_observability.py
162
164
  tests/test_payment_braintree_adapter.py
163
165
  tests/test_payment_demo_profiles.py
164
166
  tests/test_payment_profiles.py