velocity-python 0.1.58__tar.gz → 0.1.61__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 (201) hide show
  1. {velocity_python-0.1.58 → velocity_python-0.1.61}/PKG-INFO +1 -1
  2. {velocity_python-0.1.58 → velocity_python-0.1.61}/pyproject.toml +1 -1
  3. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/__init__.py +1 -1
  4. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/aws/handlers/lambda_handler.py +10 -0
  5. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/aws/handlers/mixins/data_service.py +49 -6
  6. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/servers/tablehelper.py +56 -1
  7. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity_python.egg-info/PKG-INFO +1 -1
  8. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity_python.egg-info/SOURCES.txt +5 -1
  9. {velocity_python-0.1.58 → velocity_python-0.1.61}/tests/test_dirty_pipeline_fast_path.py +8 -0
  10. velocity_python-0.1.61/tests/test_http_handler_rollback.py +82 -0
  11. velocity_python-0.1.61/tests/test_identifier_injection_guard.py +116 -0
  12. velocity_python-0.1.61/tests/test_restricted_direct_tables.py +59 -0
  13. {velocity_python-0.1.58 → velocity_python-0.1.61}/tests/test_security_hardening.py +6 -5
  14. velocity_python-0.1.61/tests/test_write_hook_create_flow.py +138 -0
  15. {velocity_python-0.1.58 → velocity_python-0.1.61}/LICENSE +0 -0
  16. {velocity_python-0.1.58 → velocity_python-0.1.61}/README.md +0 -0
  17. {velocity_python-0.1.58 → velocity_python-0.1.61}/setup.cfg +0 -0
  18. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/aws/__init__.py +0 -0
  19. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/aws/amplify.py +0 -0
  20. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/aws/amplify_build.py +0 -0
  21. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/aws/assets/__init__.py +0 -0
  22. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/aws/assets/backfill.py +0 -0
  23. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/aws/assets/indexing.py +0 -0
  24. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/aws/assets/references.py +0 -0
  25. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/aws/assets/service.py +0 -0
  26. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/aws/assets/usage_index.py +0 -0
  27. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/aws/dirty_pipeline.py +0 -0
  28. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/aws/handlers/__init__.py +0 -0
  29. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/aws/handlers/base_handler.py +0 -0
  30. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/aws/handlers/context.py +0 -0
  31. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/aws/handlers/context_factory.py +0 -0
  32. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/aws/handlers/exceptions.py +0 -0
  33. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
  34. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
  35. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/aws/handlers/perf.py +0 -0
  36. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/aws/handlers/response.py +0 -0
  37. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/aws/handlers/sqs_handler.py +0 -0
  38. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/aws/s3.py +0 -0
  39. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/aws/ssm_config.py +0 -0
  40. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/aws/tests/__init__.py +0 -0
  41. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
  42. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
  43. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/aws/tests/test_response.py +0 -0
  44. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/__init__.py +0 -0
  45. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/core/__init__.py +0 -0
  46. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/core/async_support.py +0 -0
  47. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/core/column.py +0 -0
  48. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/core/database.py +0 -0
  49. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/core/decorators.py +0 -0
  50. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/core/engine.py +0 -0
  51. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/core/result.py +0 -0
  52. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/core/row.py +0 -0
  53. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/core/sequence.py +0 -0
  54. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/core/table.py +0 -0
  55. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/core/transaction.py +0 -0
  56. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/core/view.py +0 -0
  57. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/exceptions.py +0 -0
  58. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/migrations.py +0 -0
  59. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/servers/__init__.py +0 -0
  60. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/servers/base/__init__.py +0 -0
  61. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/servers/base/initializer.py +0 -0
  62. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/servers/base/operators.py +0 -0
  63. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/servers/base/sql.py +0 -0
  64. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/servers/base/types.py +0 -0
  65. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/servers/mysql/__init__.py +0 -0
  66. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/servers/mysql/operators.py +0 -0
  67. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/servers/mysql/reserved.py +0 -0
  68. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/servers/mysql/sql.py +0 -0
  69. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/servers/mysql/types.py +0 -0
  70. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/servers/postgres/__init__.py +0 -0
  71. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/servers/postgres/operators.py +0 -0
  72. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/servers/postgres/reserved.py +0 -0
  73. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/servers/postgres/sql.py +0 -0
  74. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/servers/postgres/types.py +0 -0
  75. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/servers/sqlite/__init__.py +0 -0
  76. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/servers/sqlite/operators.py +0 -0
  77. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/servers/sqlite/reserved.py +0 -0
  78. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/servers/sqlite/sql.py +0 -0
  79. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/servers/sqlite/types.py +0 -0
  80. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
  81. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/servers/sqlserver/operators.py +0 -0
  82. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
  83. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/servers/sqlserver/sql.py +0 -0
  84. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/servers/sqlserver/types.py +0 -0
  85. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/tests/__init__.py +0 -0
  86. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/tests/common_db_test.py +0 -0
  87. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/tests/postgres/__init__.py +0 -0
  88. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/tests/postgres/common.py +0 -0
  89. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/tests/postgres/conftest.py +0 -0
  90. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/tests/postgres/test_column.py +0 -0
  91. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/tests/postgres/test_connections.py +0 -0
  92. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/tests/postgres/test_database.py +0 -0
  93. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/tests/postgres/test_engine.py +0 -0
  94. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
  95. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/tests/postgres/test_imports.py +0 -0
  96. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/tests/postgres/test_result.py +0 -0
  97. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/tests/postgres/test_row.py +0 -0
  98. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
  99. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
  100. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
  101. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
  102. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
  103. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/tests/postgres/test_table.py +0 -0
  104. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
  105. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
  106. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/tests/sql/__init__.py +0 -0
  107. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/tests/sql/common.py +0 -0
  108. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
  109. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
  110. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
  111. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/tests/test_db_utils.py +0 -0
  112. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/tests/test_postgres.py +0 -0
  113. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
  114. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
  115. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/tests/test_result_caching.py +0 -0
  116. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
  117. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
  118. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
  119. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
  120. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/tests/test_sql_builder.py +0 -0
  121. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/tests/test_tablehelper.py +0 -0
  122. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/tests/test_view_helper.py +0 -0
  123. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/db/utils.py +0 -0
  124. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/logging.py +0 -0
  125. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/misc/__init__.py +0 -0
  126. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/misc/conv/__init__.py +0 -0
  127. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/misc/conv/iconv.py +0 -0
  128. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/misc/conv/oconv.py +0 -0
  129. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/misc/db.py +0 -0
  130. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/misc/export.py +0 -0
  131. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/misc/format.py +0 -0
  132. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/misc/mail.py +0 -0
  133. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/misc/merge.py +0 -0
  134. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/misc/pdf.py +0 -0
  135. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/misc/tests/__init__.py +0 -0
  136. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/misc/tests/test_db.py +0 -0
  137. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/misc/tests/test_fix.py +0 -0
  138. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/misc/tests/test_format.py +0 -0
  139. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/misc/tests/test_iconv.py +0 -0
  140. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/misc/tests/test_merge.py +0 -0
  141. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/misc/tests/test_oconv.py +0 -0
  142. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/misc/tests/test_original_error.py +0 -0
  143. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/misc/tests/test_timer.py +0 -0
  144. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/misc/timer.py +0 -0
  145. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/misc/tools.py +0 -0
  146. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/payment/__init__.py +0 -0
  147. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/payment/authorizenet_adapter.py +0 -0
  148. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/payment/authorizenet_mirror.py +0 -0
  149. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/payment/base_adapter.py +0 -0
  150. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/payment/braintree_adapter.py +0 -0
  151. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/payment/braintree_mirror.py +0 -0
  152. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/payment/charge_rules.py +0 -0
  153. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/payment/stripe_adapter.py +0 -0
  154. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity/payment/stripe_mirror.py +0 -0
  155. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity_python.egg-info/dependency_links.txt +0 -0
  156. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity_python.egg-info/entry_points.txt +0 -0
  157. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity_python.egg-info/requires.txt +0 -0
  158. {velocity_python-0.1.58 → velocity_python-0.1.61}/src/velocity_python.egg-info/top_level.txt +0 -0
  159. {velocity_python-0.1.58 → velocity_python-0.1.61}/tests/test_amplify_build.py +0 -0
  160. {velocity_python-0.1.58 → velocity_python-0.1.61}/tests/test_asset_indexing.py +0 -0
  161. {velocity_python-0.1.58 → velocity_python-0.1.61}/tests/test_asset_references.py +0 -0
  162. {velocity_python-0.1.58 → velocity_python-0.1.61}/tests/test_assets_service.py +0 -0
  163. {velocity_python-0.1.58 → velocity_python-0.1.61}/tests/test_async_support.py +0 -0
  164. {velocity_python-0.1.58 → velocity_python-0.1.61}/tests/test_batch_operations.py +0 -0
  165. {velocity_python-0.1.58 → velocity_python-0.1.61}/tests/test_concurrency_safety.py +0 -0
  166. {velocity_python-0.1.58 → velocity_python-0.1.61}/tests/test_connection_pool.py +0 -0
  167. {velocity_python-0.1.58 → velocity_python-0.1.61}/tests/test_connection_resilience.py +0 -0
  168. {velocity_python-0.1.58 → velocity_python-0.1.61}/tests/test_context_job_descriptions.py +0 -0
  169. {velocity_python-0.1.58 → velocity_python-0.1.61}/tests/test_db_credentials_ssm_cascade.py +0 -0
  170. {velocity_python-0.1.58 → velocity_python-0.1.61}/tests/test_decorators.py +0 -0
  171. {velocity_python-0.1.58 → velocity_python-0.1.61}/tests/test_email_processing.py +0 -0
  172. {velocity_python-0.1.58 → velocity_python-0.1.61}/tests/test_iconv_money_to_cents.py +0 -0
  173. {velocity_python-0.1.58 → velocity_python-0.1.61}/tests/test_jsonb_dict_adapter.py +0 -0
  174. {velocity_python-0.1.58 → velocity_python-0.1.61}/tests/test_lambda_handler.py +0 -0
  175. {velocity_python-0.1.58 → velocity_python-0.1.61}/tests/test_lambda_handler_auth.py +0 -0
  176. {velocity_python-0.1.58 → velocity_python-0.1.61}/tests/test_mixins_import.py +0 -0
  177. {velocity_python-0.1.58 → velocity_python-0.1.61}/tests/test_n_plus_one.py +0 -0
  178. {velocity_python-0.1.58 → velocity_python-0.1.61}/tests/test_observability.py +0 -0
  179. {velocity_python-0.1.58 → velocity_python-0.1.61}/tests/test_payment_authorizenet_adapter.py +0 -0
  180. {velocity_python-0.1.58 → velocity_python-0.1.61}/tests/test_payment_braintree_adapter.py +0 -0
  181. {velocity_python-0.1.58 → velocity_python-0.1.61}/tests/test_payment_braintree_mirror.py +0 -0
  182. {velocity_python-0.1.58 → velocity_python-0.1.61}/tests/test_payment_profile_sorting.py +0 -0
  183. {velocity_python-0.1.58 → velocity_python-0.1.61}/tests/test_payment_stripe_adapter.py +0 -0
  184. {velocity_python-0.1.58 → velocity_python-0.1.61}/tests/test_pdf.py +0 -0
  185. {velocity_python-0.1.58 → velocity_python-0.1.61}/tests/test_prepared_statements.py +0 -0
  186. {velocity_python-0.1.58 → velocity_python-0.1.61}/tests/test_psycopg3_upgrade.py +0 -0
  187. {velocity_python-0.1.58 → velocity_python-0.1.61}/tests/test_query_cache.py +0 -0
  188. {velocity_python-0.1.58 → velocity_python-0.1.61}/tests/test_retry_side_effect_guard.py +0 -0
  189. {velocity_python-0.1.58 → velocity_python-0.1.61}/tests/test_return_default_safety.py +0 -0
  190. {velocity_python-0.1.58 → velocity_python-0.1.61}/tests/test_row_batch_update.py +0 -0
  191. {velocity_python-0.1.58 → velocity_python-0.1.61}/tests/test_row_cache_staleness.py +0 -0
  192. {velocity_python-0.1.58 → velocity_python-0.1.61}/tests/test_row_dirty_tracking.py +0 -0
  193. {velocity_python-0.1.58 → velocity_python-0.1.61}/tests/test_schema_migrations.py +0 -0
  194. {velocity_python-0.1.58 → velocity_python-0.1.61}/tests/test_server_cursor.py +0 -0
  195. {velocity_python-0.1.58 → velocity_python-0.1.61}/tests/test_single_autocommit_safety.py +0 -0
  196. {velocity_python-0.1.58 → velocity_python-0.1.61}/tests/test_spreadsheet_functions.py +0 -0
  197. {velocity_python-0.1.58 → velocity_python-0.1.61}/tests/test_sqs_per_record_transactions.py +0 -0
  198. {velocity_python-0.1.58 → velocity_python-0.1.61}/tests/test_ssm_config.py +0 -0
  199. {velocity_python-0.1.58 → velocity_python-0.1.61}/tests/test_sys_modified_count_postgres_demo.py +0 -0
  200. {velocity_python-0.1.58 → velocity_python-0.1.61}/tests/test_table_alter.py +0 -0
  201. {velocity_python-0.1.58 → velocity_python-0.1.61}/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.58
3
+ Version: 0.1.61
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.58"
7
+ version = "0.1.61"
8
8
  authors = [
9
9
  { name="Velocity Team", email="info@codeclubs.org" },
10
10
  ]
@@ -1,4 +1,4 @@
1
- __version__ = version = "0.1.58"
1
+ __version__ = version = "0.1.61"
2
2
 
3
3
  import importlib as _importlib
4
4
 
@@ -209,6 +209,16 @@ class LambdaHandler(BaseHandler):
209
209
  try:
210
210
  self.execute_actions(tx, local_context, actions)
211
211
  except Exception as e:
212
+ # A single HTTP action is atomic: any exception (including a handled
213
+ # AlertError validation rejection) must discard partial writes so the
214
+ # surrounding @engine.transaction does not commit them on the normal
215
+ # return below. Without this, e.g. a write_hook that inserts a @new
216
+ # row and then raises a validation AlertError would leak a committed
217
+ # orphan row. Mirrors SqsHandler._process_record.
218
+ try:
219
+ tx.rollback()
220
+ except Exception:
221
+ pass
212
222
  self.handle_error(tx, local_context, e)
213
223
  local_context.perf.log("execute_actions total (serve)")
214
224
 
@@ -16,6 +16,7 @@ import xml.etree.ElementTree as ET
16
16
  from io import BytesIO, StringIO
17
17
 
18
18
  from velocity.aws import dirty_pipeline
19
+ from velocity.aws.handlers.exceptions import AlertError
19
20
  from velocity.misc import export
20
21
 
21
22
  logger = logging.getLogger(__name__)
@@ -41,6 +42,15 @@ class DataServiceMixin:
41
42
  Override read_hook, write_hook, etc. methods to add custom business logic.
42
43
  """
43
44
 
45
+ # Tables that must NOT be reachable through the hook-bypassing generic
46
+ # actions (query-direct, update-rows, get-table-schema). The generic actions
47
+ # do not run rwx hooks, so any per-table row-scoping or secret-redaction a
48
+ # table relies on would be bypassed. Apps populate this set (business policy
49
+ # lives in the app, not this business-agnostic mixin) with the names of such
50
+ # tables; the normal read-object/find-object/query/write-object actions —
51
+ # which DO run hooks — remain the supported path for them.
52
+ restricted_direct_tables = frozenset()
53
+
44
54
  # PostgreSQL type mappings for frontend display
45
55
  _pg_types = {
46
56
  "bool": "string",
@@ -58,6 +68,14 @@ class DataServiceMixin:
58
68
  "timestamp": "string",
59
69
  }
60
70
 
71
+ def _assert_direct_access_allowed(self, table):
72
+ """Reject hook-bypassing direct access to a restricted table."""
73
+ if table in self.restricted_direct_tables:
74
+ raise AlertError(
75
+ f"Direct access to '{table}' is not permitted; "
76
+ "use the standard data actions."
77
+ )
78
+
61
79
  def _get_field_type(self, column_info):
62
80
  """Convert database column type to frontend display type"""
63
81
  return (
@@ -149,20 +167,39 @@ class DataServiceMixin:
149
167
  self._call_rwx_hook(
150
168
  "before_new", table, tx, table, sys_id, incoming, context
151
169
  )
170
+ # Run before_write BEFORE inserting the row, passing the "@new"
171
+ # sentinel so table hooks can detect a create, validate required
172
+ # fields, and reject it without leaving an orphan row. (The serve()
173
+ # error path rolls the transaction back, but validating pre-insert
174
+ # avoids ever allocating the row in the first place.)
175
+ self._call_rwx_hook(
176
+ "before_write", "common", tx, table, "@new", incoming, context
177
+ )
178
+ self._call_rwx_hook(
179
+ "before_write", table, tx, table, "@new", incoming, context
180
+ )
152
181
  row = tx.table(table).new()
153
182
  sys_id = row["sys_id"]
154
183
  self._call_rwx_hook("after_new", "common", tx, table, sys_id, row, context)
155
184
  self._call_rwx_hook("after_new", table, tx, table, sys_id, row, context)
156
185
  elif sys_id:
157
186
  sys_id = int(sys_id)
187
+ self._call_rwx_hook(
188
+ "before_write", "common", tx, table, sys_id, incoming, context
189
+ )
190
+ self._call_rwx_hook(
191
+ "before_write", table, tx, table, sys_id, incoming, context
192
+ )
193
+ # Update path: the row must already exist. Use find (not get, which
194
+ # is get-or-create) so a write to a stale/unknown sys_id is a clear
195
+ # error rather than a phantom row inserted with a client-chosen PK.
196
+ row = tx.table(table).find(sys_id)
197
+ if not row:
198
+ raise AlertError(
199
+ f"Cannot update {table}: record {sys_id} no longer exists."
200
+ )
158
201
  else:
159
202
  raise Exception("Object sys_id was not supplied on write operation.")
160
- self._call_rwx_hook(
161
- "before_write", "common", tx, table, sys_id, incoming, context
162
- )
163
- self._call_rwx_hook("before_write", table, tx, table, sys_id, incoming, context)
164
- if not row:
165
- row = tx.table(table).get(sys_id)
166
203
  row.update(incoming)
167
204
  self._call_rwx_hook("after_write", "common", tx, table, sys_id, row, context)
168
205
  self._call_rwx_hook("after_write", table, tx, table, sys_id, row, context)
@@ -842,6 +879,8 @@ class DataServiceMixin:
842
879
  if not rows:
843
880
  raise ValueError("Parameter 'updateRows' cannot be empty")
844
881
 
882
+ self._assert_direct_access_allowed(table)
883
+
845
884
  t = tx.table(table)
846
885
  count = t.update(data, {"sys_id": rows})
847
886
  context.response().toast(f"Updated {count} item(s).", "success")
@@ -867,6 +906,8 @@ class DataServiceMixin:
867
906
  if not table_name:
868
907
  raise ValueError("Parameter 'obj' cannot be empty")
869
908
 
909
+ self._assert_direct_access_allowed(table_name)
910
+
870
911
  params = payload.get("params", {})
871
912
 
872
913
  if payload.get("result_format") == "excel":
@@ -932,6 +973,8 @@ class DataServiceMixin:
932
973
  if not table_name:
933
974
  raise ValueError("Parameter 'tableName' cannot be empty")
934
975
 
976
+ self._assert_direct_access_allowed(table_name)
977
+
935
978
  try:
936
979
  # Query information_schema to get table schema
937
980
  schema_query = """
@@ -158,6 +158,54 @@ class TableHelper:
158
158
  self.foreign_keys[key] = data
159
159
  return data
160
160
 
161
+ # Substrings that never legitimately appear inside a column/WHERE-key
162
+ # reference and are the signatures of identifier-level SQL injection. The
163
+ # values side of every predicate is parameterized; only the identifier side
164
+ # flows through here, so a placeholder, statement terminator, or comment in
165
+ # this position is always an attack, never a real column or expression.
166
+ _INJECTION_SIGNATURES: ClassVar[Tuple[str, ...]] = (
167
+ ";", # statement terminator / stacked query
168
+ "--", # line comment
169
+ "/*", # block comment open
170
+ "*/", # block comment close
171
+ "%s", # positional bind-parameter smuggling
172
+ "%(", # named bind-parameter smuggling
173
+ )
174
+
175
+ def _assert_safe_reference(self, key: str) -> None:
176
+ """
177
+ Reject identifier/expression references carrying SQL-injection signatures.
178
+
179
+ This is a defense-in-depth guard for the *structured* query path (WHERE
180
+ dict keys, ORDER BY / GROUP BY entries, projected columns). Values are
181
+ always parameterized; this guard protects the identifier side, which is
182
+ interpolated. It deliberately does not reject legitimate SQL expressions
183
+ (aggregates, CASE/CAST, window functions, correlated subqueries) — only
184
+ the unambiguous injection markers and unbalanced parentheses. Raw string
185
+ WHERE clauses are intentionally not routed through here and remain
186
+ caller-beware per the documented contract.
187
+
188
+ Raises:
189
+ ValueError: unconditionally (ignores bypass_on_error) when an
190
+ injection signature is present, because such input is never a
191
+ valid reference.
192
+ """
193
+ if not isinstance(key, str):
194
+ return
195
+ # Strip a leading operator prefix (e.g. '%', '%%', '>=') so that a LIKE
196
+ # key such as '%status' is not misread as the '%s' placeholder marker.
197
+ body = self.remove_operator(key)
198
+ lowered = body.lower()
199
+ for marker in self._INJECTION_SIGNATURES:
200
+ if marker in lowered:
201
+ raise ValueError(
202
+ f"Unsafe SQL reference detected (contains {marker!r}): {key!r}"
203
+ )
204
+ if not self.are_parentheses_balanced(body):
205
+ raise ValueError(
206
+ f"Unsafe SQL reference detected (unbalanced parentheses): {key!r}"
207
+ )
208
+
161
209
  def resolve_references(
162
210
  self, key: str, options: Optional[Dict[str, Any]] = None
163
211
  ) -> str:
@@ -176,13 +224,20 @@ class TableHelper:
176
224
  Resolved column reference with appropriate aliasing
177
225
 
178
226
  Raises:
179
- ValueError: If key is invalid and bypass_on_error is False
227
+ ValueError: If key is invalid and bypass_on_error is False, or if the
228
+ key carries an SQL-injection signature (always, regardless of
229
+ bypass_on_error)
180
230
  """
181
231
  if not key or not isinstance(key, str):
182
232
  if options and options.get("bypass_on_error"):
183
233
  return key or ""
184
234
  raise ValueError(f"Invalid key: {key}")
185
235
 
236
+ # Identifier-level injection guard runs before bypass_on_error so that a
237
+ # poisoned reference is a hard error on every path, not silently passed
238
+ # through.
239
+ self._assert_safe_reference(key)
240
+
186
241
  if options is None:
187
242
  options = {"alias_column": True, "alias_table": False, "alias_only": False}
188
243
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: velocity-python
3
- Version: 0.1.58
3
+ Version: 0.1.61
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
@@ -162,7 +162,9 @@ tests/test_db_credentials_ssm_cascade.py
162
162
  tests/test_decorators.py
163
163
  tests/test_dirty_pipeline_fast_path.py
164
164
  tests/test_email_processing.py
165
+ tests/test_http_handler_rollback.py
165
166
  tests/test_iconv_money_to_cents.py
167
+ tests/test_identifier_injection_guard.py
166
168
  tests/test_jsonb_dict_adapter.py
167
169
  tests/test_lambda_handler.py
168
170
  tests/test_lambda_handler_auth.py
@@ -178,6 +180,7 @@ tests/test_pdf.py
178
180
  tests/test_prepared_statements.py
179
181
  tests/test_psycopg3_upgrade.py
180
182
  tests/test_query_cache.py
183
+ tests/test_restricted_direct_tables.py
181
184
  tests/test_retry_side_effect_guard.py
182
185
  tests/test_return_default_safety.py
183
186
  tests/test_row_batch_update.py
@@ -192,4 +195,5 @@ tests/test_sqs_per_record_transactions.py
192
195
  tests/test_ssm_config.py
193
196
  tests/test_sys_modified_count_postgres_demo.py
194
197
  tests/test_table_alter.py
195
- tests/test_where_clause_validation.py
198
+ tests/test_where_clause_validation.py
199
+ tests/test_write_hook_create_flow.py
@@ -39,6 +39,14 @@ class FakeTable:
39
39
  def get(self, sys_id):
40
40
  return self.row
41
41
 
42
+ def find(self, sys_id):
43
+ # Update path now uses find() (not get, which is get-or-create) so a
44
+ # write to a stale sys_id is an error rather than a phantom row.
45
+ return self.row
46
+
47
+ def new(self):
48
+ return self.row
49
+
42
50
 
43
51
  class FakeTx:
44
52
  def __init__(self, row):
@@ -0,0 +1,82 @@
1
+ """V2 — the HTTP handler must roll back partial writes before handle_error.
2
+
3
+ A single HTTP action is atomic. Because the handler class is wrapped in
4
+ @engine.transaction, a normal return from serve() commits. Without an explicit
5
+ rollback on the error path, an action that writes and then raises — including a
6
+ write_hook that inserts a @new row and then raises a validation AlertError
7
+ (D2) — would leave a committed orphan row. These tests pin the rollback.
8
+ """
9
+
10
+ import unittest
11
+ from unittest.mock import MagicMock
12
+
13
+ from velocity.aws.handlers.exceptions import AlertError
14
+ from velocity.aws.handlers.lambda_handler import LambdaHandler
15
+ from velocity.aws.handlers.context_factory import ContextFactory
16
+
17
+
18
+ class _StubContext:
19
+ def __init__(self, response):
20
+ self._response = response
21
+ self.perf = MagicMock()
22
+
23
+ def parse_postdata(self):
24
+ return {"action": "do-thing", "payload": {}}
25
+
26
+ def update_postdata(self, postdata):
27
+ pass
28
+
29
+ def configure_perf(self, **kw):
30
+ pass
31
+
32
+ def action(self):
33
+ return "do-thing"
34
+
35
+ def postdata(self, keys=-1, default=None):
36
+ return {"action": "do-thing", "payload": {}}
37
+
38
+ def response(self):
39
+ return self._response
40
+
41
+
42
+ class _StubContextFactory(ContextFactory):
43
+ def create(self, *, aws_event, aws_context, args, postdata, response, session):
44
+ return _StubContext(response)
45
+
46
+
47
+ def _make_handler(raise_exc):
48
+ handler = LambdaHandler(
49
+ {"queryStringParameters": {}},
50
+ MagicMock(aws_request_id="req-123"),
51
+ context_factory=_StubContextFactory(),
52
+ )
53
+
54
+ def _boom(tx, local_context, actions):
55
+ raise raise_exc
56
+
57
+ handler.execute_actions = _boom
58
+ # Keep onError off the DB during the generic-exception path.
59
+ handler.onError = lambda tx, context, exc, tb: None
60
+ return handler
61
+
62
+
63
+ class HttpHandlerRollbackTest(unittest.TestCase):
64
+ def test_alert_error_rolls_back_before_handle_error(self):
65
+ # D2: a validation AlertError raised after a @new insert must not commit
66
+ # the orphan row.
67
+ handler = _make_handler(AlertError("required field missing"))
68
+ tx = MagicMock()
69
+ handler.serve(tx)
70
+ tx.rollback.assert_called_once()
71
+
72
+ def test_unhandled_exception_rolls_back_and_returns_500(self):
73
+ handler = _make_handler(RuntimeError("kaboom"))
74
+ tx = MagicMock()
75
+ rendered = handler.serve(tx)
76
+ tx.rollback.assert_called_once()
77
+ # _set_unhandled_error_response sets a 500 on the response.
78
+ self.assertIn("500", str(rendered.get("statusCode", "")) + str(rendered))
79
+
80
+
81
+ if __name__ == "__main__":
82
+ unittest.main()
@@ -0,0 +1,116 @@
1
+ """V1 — identifier-level SQL-injection guard for the structured query path.
2
+
3
+ The value side of every predicate is parameterized; the identifier side (WHERE
4
+ dict keys, ORDER BY / GROUP BY entries, projected columns) is interpolated and
5
+ flows through TableHelper.resolve_references / make_predicate. These tests pin
6
+ down that injection signatures on the identifier side are rejected while
7
+ legitimate column expressions (aggregates, CASE/CAST, window functions, pointer
8
+ syntax, operator prefixes including LIKE/ILIKE) still resolve.
9
+ """
10
+
11
+ import unittest
12
+
13
+ # Importing the postgres SQL module configures TableHelper.operators / .reserved
14
+ # as a side effect, which make_predicate / resolve_references rely on.
15
+ from velocity.db.servers.postgres.sql import SQL # noqa: F401
16
+ from velocity.db.servers.postgres.operators import OPERATORS
17
+ from velocity.db.servers.postgres.reserved import reserved_words
18
+ from velocity.db.servers.tablehelper import TableHelper
19
+
20
+
21
+ class IdentifierInjectionGuardTest(unittest.TestCase):
22
+ def setUp(self):
23
+ # Pin the postgres operator/reserved tables on the shared class attrs so
24
+ # this suite is independent of any other test that mutates them.
25
+ TableHelper.operators = OPERATORS
26
+ TableHelper.reserved = reserved_words
27
+ # tx is only consulted for pointer (foreign-key) resolution; the cases
28
+ # here are all local columns/expressions, so None is sufficient.
29
+ self.helper = TableHelper(tx=None, table="customers")
30
+
31
+ # --- the confirmed proof-of-concept vectors from the code review ---------
32
+
33
+ def test_where_key_placeholder_injection_rejected(self):
34
+ with self.assertRaises(ValueError):
35
+ self.helper.make_predicate("email = %s) OR (1=1", "x")
36
+
37
+ def test_orderby_style_stacked_query_rejected(self):
38
+ with self.assertRaises(ValueError):
39
+ self.helper.resolve_references(
40
+ "name; DROP TABLE users",
41
+ options={"alias_only": True, "bypass_on_error": True},
42
+ )
43
+
44
+ def test_column_comment_and_stacking_rejected(self):
45
+ with self.assertRaises(ValueError):
46
+ self.helper.resolve_references(
47
+ "1 FROM users; DELETE FROM users WHERE 1=1 --",
48
+ options={
49
+ "alias_column": True,
50
+ "alias_table": True,
51
+ "bypass_on_error": True,
52
+ },
53
+ )
54
+
55
+ # --- bypass_on_error must NOT swallow an injection ------------------------
56
+
57
+ def test_bypass_on_error_does_not_swallow_injection(self):
58
+ for poisoned in (
59
+ "col; SELECT 1",
60
+ "col /* x */",
61
+ "col -- comment",
62
+ "col = %s",
63
+ "col = %(name)s",
64
+ "col) OR (1=1",
65
+ ):
66
+ with self.subTest(poisoned=poisoned):
67
+ with self.assertRaises(ValueError):
68
+ self.helper.resolve_references(
69
+ poisoned, options={"bypass_on_error": True}
70
+ )
71
+
72
+ # --- legitimate references still resolve ---------------------------------
73
+
74
+ def test_plain_column_resolves(self):
75
+ sql, val = self.helper.make_predicate("email_address", "a@example.com")
76
+ self.assertEqual(sql, "email_address = %s")
77
+ self.assertEqual(val, "a@example.com")
78
+
79
+ def test_like_operator_prefix_not_flagged_as_placeholder(self):
80
+ # '%status' is a LIKE on the `status` column; the leading '%' is an
81
+ # operator prefix, not a '%s' bind placeholder.
82
+ sql, val = self.helper.make_predicate("%status", "active%")
83
+ self.assertIn("status", sql)
84
+ self.assertIn("LIKE", sql.upper())
85
+ self.assertEqual(val, "active%")
86
+
87
+ def test_ilike_double_percent_prefix_ok(self):
88
+ sql, _ = self.helper.make_predicate("%%name", "smith")
89
+ self.assertIn("ILIKE", sql.upper())
90
+
91
+ def test_aggregate_expression_column_ok(self):
92
+ result = self.helper.resolve_references(
93
+ "SUM(amount)",
94
+ options={"alias_column": True, "bypass_on_error": True},
95
+ )
96
+ self.assertIn("amount", result)
97
+
98
+ def test_balanced_function_expression_ok(self):
99
+ result = self.helper.resolve_references(
100
+ "COALESCE(amount, 0)",
101
+ options={"alias_column": True, "bypass_on_error": True},
102
+ )
103
+ self.assertIn("amount", result)
104
+
105
+ def test_in_list_predicate_ok(self):
106
+ sql, val = self.helper.make_predicate("sys_id", [1, 2, 3])
107
+ self.assertIn("IN", sql.upper())
108
+ self.assertEqual(tuple(val), (1, 2, 3))
109
+
110
+ def test_not_equal_operator_prefix_ok(self):
111
+ sql, _ = self.helper.make_predicate("!status", "deleted")
112
+ self.assertIn("status", sql)
113
+
114
+
115
+ if __name__ == "__main__":
116
+ unittest.main()
@@ -0,0 +1,59 @@
1
+ """D6 — hook-bypassing generic actions must refuse restricted tables.
2
+
3
+ query-direct / update-rows / get-table-schema run no rwx hooks, so a table
4
+ that relies on per-row scoping or secret redaction in its hooks must not be
5
+ reachable through them. Apps declare such tables in restricted_direct_tables.
6
+ """
7
+
8
+ import unittest
9
+ from unittest.mock import MagicMock
10
+
11
+ from velocity.aws.handlers.exceptions import AlertError
12
+ from velocity.aws.handlers.mixins.data_service import DataServiceMixin
13
+
14
+
15
+ class _Service(DataServiceMixin):
16
+ restricted_direct_tables = frozenset({"mail_accounts"})
17
+
18
+
19
+ def _ctx(payload):
20
+ ctx = MagicMock()
21
+ ctx.payload.return_value = payload
22
+ return ctx
23
+
24
+
25
+ class RestrictedDirectTablesTest(unittest.TestCase):
26
+ def setUp(self):
27
+ self.service = _Service()
28
+ self.tx = MagicMock()
29
+
30
+ def test_query_direct_blocks_restricted(self):
31
+ ctx = _ctx({"obj": "mail_accounts", "params": {}})
32
+ with self.assertRaises(AlertError):
33
+ self.service.OnActionQueryDirect(self.tx, ctx)
34
+
35
+ def test_update_rows_blocks_restricted(self):
36
+ ctx = _ctx(
37
+ {"table": "mail_accounts", "updateData": {"x": 1}, "updateRows": [1, 2]}
38
+ )
39
+ with self.assertRaises(AlertError):
40
+ self.service.OnActionUpdateRows(self.tx, ctx)
41
+
42
+ def test_get_table_schema_blocks_restricted(self):
43
+ ctx = _ctx({"tableName": "mail_accounts"})
44
+ with self.assertRaises(AlertError):
45
+ self.service.OnActionGetTableSchema(self.tx, ctx)
46
+
47
+ def test_unrestricted_table_not_blocked_by_guard(self):
48
+ # A non-restricted table passes the guard (it then proceeds to touch the
49
+ # mocked tx, which is fine for this assertion — no AlertError from the
50
+ # guard itself).
51
+ ctx = _ctx({"table": "customers", "updateData": {"x": 1}, "updateRows": [1]})
52
+ try:
53
+ self.service.OnActionUpdateRows(self.tx, ctx)
54
+ except AlertError:
55
+ self.fail("guard should not block an unrestricted table")
56
+
57
+
58
+ if __name__ == "__main__":
59
+ unittest.main()
@@ -93,17 +93,18 @@ class TestAggregateQuoting:
93
93
  assert 'min("amount")' in sql
94
94
 
95
95
  def test_sum_injection_prevented(self):
96
+ # The identifier-level injection guard (V1) rejects the poisoned column
97
+ # outright rather than relying on quoting to defang it.
96
98
  table = _make_table()
97
99
  malicious = "balance); DROP TABLE users; --"
98
- sql, _ = table.sum(malicious, sql_only=True)
99
- # The malicious payload should be inside quotes, not executable
100
- assert "DROP TABLE" not in sql.split('"')[-1] # not outside quotes
100
+ with pytest.raises(ValueError):
101
+ table.sum(malicious, sql_only=True)
101
102
 
102
103
  def test_max_injection_prevented(self):
103
104
  table = _make_table()
104
105
  malicious = "x); DELETE FROM accounts; --"
105
- sql, _ = table.max(malicious, sql_only=True)
106
- assert "DELETE" not in sql.replace('"', "").split(")")[0]
106
+ with pytest.raises(ValueError):
107
+ table.max(malicious, sql_only=True)
107
108
 
108
109
 
109
110
  # ──────────────────────────────────────────────────────────────────────