velocity-python 0.1.2__tar.gz → 0.1.3__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 (178) hide show
  1. {velocity_python-0.1.2 → velocity_python-0.1.3}/PKG-INFO +1 -1
  2. {velocity_python-0.1.2 → velocity_python-0.1.3}/pyproject.toml +1 -1
  3. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/__init__.py +1 -1
  4. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/core/column.py +2 -1
  5. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/core/engine.py +5 -1
  6. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/core/table.py +24 -3
  7. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/servers/base/sql.py +10 -0
  8. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/servers/mysql/sql.py +5 -0
  9. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/servers/sqlserver/sql.py +5 -0
  10. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity_python.egg-info/PKG-INFO +1 -1
  11. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity_python.egg-info/SOURCES.txt +1 -0
  12. velocity_python-0.1.3/tests/test_security_hardening.py +256 -0
  13. {velocity_python-0.1.2 → velocity_python-0.1.3}/LICENSE +0 -0
  14. {velocity_python-0.1.2 → velocity_python-0.1.3}/README.md +0 -0
  15. {velocity_python-0.1.2 → velocity_python-0.1.3}/setup.cfg +0 -0
  16. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/app/__init__.py +0 -0
  17. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/app/formbuilder/__init__.py +0 -0
  18. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/app/formbuilder/reshaper.py +0 -0
  19. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/app/invoices.py +0 -0
  20. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/app/orders.py +0 -0
  21. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/app/payments.py +0 -0
  22. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/app/purchase_orders.py +0 -0
  23. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/app/tests/__init__.py +0 -0
  24. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/app/tests/test_email_processing.py +0 -0
  25. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/app/tests/test_payment_profile_sorting.py +0 -0
  26. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/app/tests/test_spreadsheet_functions.py +0 -0
  27. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/app/validators/__init__.py +0 -0
  28. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/app/validators/formbuilder_template.py +0 -0
  29. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/aws/__init__.py +0 -0
  30. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/aws/amplify.py +0 -0
  31. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/aws/amplify_build.py +0 -0
  32. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/aws/handlers/__init__.py +0 -0
  33. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/aws/handlers/base_handler.py +0 -0
  34. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/aws/handlers/context.py +0 -0
  35. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/aws/handlers/context_factory.py +0 -0
  36. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/aws/handlers/exceptions.py +0 -0
  37. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/aws/handlers/lambda_handler.py +0 -0
  38. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
  39. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/aws/handlers/mixins/data_service.py +0 -0
  40. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
  41. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/aws/handlers/perf.py +0 -0
  42. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/aws/handlers/response.py +0 -0
  43. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/aws/handlers/sqs_handler.py +0 -0
  44. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/aws/tests/__init__.py +0 -0
  45. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
  46. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
  47. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/aws/tests/test_response.py +0 -0
  48. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/__init__.py +0 -0
  49. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/core/__init__.py +0 -0
  50. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/core/database.py +0 -0
  51. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/core/decorators.py +0 -0
  52. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/core/result.py +0 -0
  53. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/core/row.py +0 -0
  54. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/core/sequence.py +0 -0
  55. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/core/transaction.py +0 -0
  56. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/core/view.py +0 -0
  57. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/exceptions.py +0 -0
  58. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/servers/__init__.py +0 -0
  59. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/servers/base/__init__.py +0 -0
  60. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/servers/base/initializer.py +0 -0
  61. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/servers/base/operators.py +0 -0
  62. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/servers/base/types.py +0 -0
  63. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/servers/mysql/__init__.py +0 -0
  64. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/servers/mysql/operators.py +0 -0
  65. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/servers/mysql/reserved.py +0 -0
  66. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/servers/mysql/types.py +0 -0
  67. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/servers/postgres/__init__.py +0 -0
  68. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/servers/postgres/operators.py +0 -0
  69. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/servers/postgres/reserved.py +0 -0
  70. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/servers/postgres/sql.py +0 -0
  71. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/servers/postgres/types.py +0 -0
  72. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/servers/sqlite/__init__.py +0 -0
  73. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/servers/sqlite/operators.py +0 -0
  74. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/servers/sqlite/reserved.py +0 -0
  75. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/servers/sqlite/sql.py +0 -0
  76. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/servers/sqlite/types.py +0 -0
  77. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
  78. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/servers/sqlserver/operators.py +0 -0
  79. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
  80. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/servers/sqlserver/types.py +0 -0
  81. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/servers/tablehelper.py +0 -0
  82. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/tests/__init__.py +0 -0
  83. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/tests/common_db_test.py +0 -0
  84. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/tests/postgres/__init__.py +0 -0
  85. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/tests/postgres/common.py +0 -0
  86. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/tests/postgres/test_column.py +0 -0
  87. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/tests/postgres/test_connections.py +0 -0
  88. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/tests/postgres/test_database.py +0 -0
  89. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/tests/postgres/test_engine.py +0 -0
  90. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
  91. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/tests/postgres/test_imports.py +0 -0
  92. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/tests/postgres/test_result.py +0 -0
  93. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/tests/postgres/test_row.py +0 -0
  94. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
  95. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
  96. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
  97. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
  98. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
  99. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/tests/postgres/test_table.py +0 -0
  100. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
  101. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
  102. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/tests/sql/__init__.py +0 -0
  103. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/tests/sql/common.py +0 -0
  104. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
  105. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
  106. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
  107. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/tests/test_db_utils.py +0 -0
  108. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/tests/test_postgres.py +0 -0
  109. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
  110. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
  111. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/tests/test_result_caching.py +0 -0
  112. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
  113. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
  114. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
  115. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
  116. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/tests/test_sql_builder.py +0 -0
  117. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/tests/test_tablehelper.py +0 -0
  118. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/tests/test_view_helper.py +0 -0
  119. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/db/utils.py +0 -0
  120. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/logging.py +0 -0
  121. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/misc/__init__.py +0 -0
  122. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/misc/conv/__init__.py +0 -0
  123. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/misc/conv/iconv.py +0 -0
  124. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/misc/conv/oconv.py +0 -0
  125. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/misc/db.py +0 -0
  126. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/misc/export.py +0 -0
  127. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/misc/format.py +0 -0
  128. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/misc/mail.py +0 -0
  129. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/misc/merge.py +0 -0
  130. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/misc/tests/__init__.py +0 -0
  131. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/misc/tests/test_db.py +0 -0
  132. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/misc/tests/test_fix.py +0 -0
  133. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/misc/tests/test_format.py +0 -0
  134. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/misc/tests/test_iconv.py +0 -0
  135. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/misc/tests/test_merge.py +0 -0
  136. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/misc/tests/test_oconv.py +0 -0
  137. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/misc/tests/test_original_error.py +0 -0
  138. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/misc/tests/test_timer.py +0 -0
  139. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/misc/timer.py +0 -0
  140. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/misc/tools.py +0 -0
  141. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/payment/__init__.py +0 -0
  142. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/payment/authorizenet_adapter.py +0 -0
  143. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/payment/base_adapter.py +0 -0
  144. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/payment/braintree_adapter.py +0 -0
  145. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/payment/charge_rules.py +0 -0
  146. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/payment/demo_profiles.py +0 -0
  147. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/payment/profiles.py +0 -0
  148. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/payment/router.py +0 -0
  149. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity/payment/stripe_adapter.py +0 -0
  150. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity_python.egg-info/dependency_links.txt +0 -0
  151. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity_python.egg-info/requires.txt +0 -0
  152. {velocity_python-0.1.2 → velocity_python-0.1.3}/src/velocity_python.egg-info/top_level.txt +0 -0
  153. {velocity_python-0.1.2 → velocity_python-0.1.3}/tests/test_amplify_build.py +0 -0
  154. {velocity_python-0.1.2 → velocity_python-0.1.3}/tests/test_batch_operations.py +0 -0
  155. {velocity_python-0.1.2 → velocity_python-0.1.3}/tests/test_concurrency_safety.py +0 -0
  156. {velocity_python-0.1.2 → velocity_python-0.1.3}/tests/test_connection_pool.py +0 -0
  157. {velocity_python-0.1.2 → velocity_python-0.1.3}/tests/test_connection_resilience.py +0 -0
  158. {velocity_python-0.1.2 → velocity_python-0.1.3}/tests/test_decorators.py +0 -0
  159. {velocity_python-0.1.2 → velocity_python-0.1.3}/tests/test_formbuilder_reshaper.py +0 -0
  160. {velocity_python-0.1.2 → velocity_python-0.1.3}/tests/test_formbuilder_template_validator.py +0 -0
  161. {velocity_python-0.1.2 → velocity_python-0.1.3}/tests/test_iconv_money_to_cents.py +0 -0
  162. {velocity_python-0.1.2 → velocity_python-0.1.3}/tests/test_lambda_handler.py +0 -0
  163. {velocity_python-0.1.2 → velocity_python-0.1.3}/tests/test_lambda_handler_auth.py +0 -0
  164. {velocity_python-0.1.2 → velocity_python-0.1.3}/tests/test_mixins_import.py +0 -0
  165. {velocity_python-0.1.2 → velocity_python-0.1.3}/tests/test_payment_braintree_adapter.py +0 -0
  166. {velocity_python-0.1.2 → velocity_python-0.1.3}/tests/test_payment_demo_profiles.py +0 -0
  167. {velocity_python-0.1.2 → velocity_python-0.1.3}/tests/test_payment_profiles.py +0 -0
  168. {velocity_python-0.1.2 → velocity_python-0.1.3}/tests/test_payment_router.py +0 -0
  169. {velocity_python-0.1.2 → velocity_python-0.1.3}/tests/test_payment_stripe_adapter.py +0 -0
  170. {velocity_python-0.1.2 → velocity_python-0.1.3}/tests/test_prepared_statements.py +0 -0
  171. {velocity_python-0.1.2 → velocity_python-0.1.3}/tests/test_psycopg3_upgrade.py +0 -0
  172. {velocity_python-0.1.2 → velocity_python-0.1.3}/tests/test_query_cache.py +0 -0
  173. {velocity_python-0.1.2 → velocity_python-0.1.3}/tests/test_row_batch_update.py +0 -0
  174. {velocity_python-0.1.2 → velocity_python-0.1.3}/tests/test_row_cache_staleness.py +0 -0
  175. {velocity_python-0.1.2 → velocity_python-0.1.3}/tests/test_sqs_per_record_transactions.py +0 -0
  176. {velocity_python-0.1.2 → velocity_python-0.1.3}/tests/test_sys_modified_count_postgres_demo.py +0 -0
  177. {velocity_python-0.1.2 → velocity_python-0.1.3}/tests/test_table_alter.py +0 -0
  178. {velocity_python-0.1.2 → velocity_python-0.1.3}/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.3
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.3"
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.3"
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):
@@ -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
 
@@ -714,6 +714,10 @@ class Engine:
714
714
  if relevant_frames:
715
715
  enhanced_message += "\n\nCall Context:\n" + "".join(relevant_frames)
716
716
 
717
+ # Mask any credentials that may have leaked into driver error messages
718
+ # (e.g. connection strings containing password=...).
719
+ enhanced_message = mask_sensitive_in_string(enhanced_message)
720
+
717
721
  # Note: SQL formatting for logging is available via _format_sql_with_params,
718
722
  # but we intentionally avoid eager logging here.
719
723
 
@@ -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
@@ -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.3
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
@@ -169,6 +169,7 @@ tests/test_psycopg3_upgrade.py
169
169
  tests/test_query_cache.py
170
170
  tests/test_row_batch_update.py
171
171
  tests/test_row_cache_staleness.py
172
+ tests/test_security_hardening.py
172
173
  tests/test_sqs_per_record_transactions.py
173
174
  tests/test_sys_modified_count_postgres_demo.py
174
175
  tests/test_table_alter.py
@@ -0,0 +1,256 @@
1
+ """
2
+ Tests for R10 — Security Hardening: column quoting, DDL logging, credential masking.
3
+ """
4
+
5
+ import logging
6
+ import re
7
+ import pytest
8
+ from unittest.mock import MagicMock, patch, call
9
+
10
+ from velocity.db.servers.base.sql import BaseSQLDialect
11
+
12
+
13
+ # ──────────────────────────────────────────────────────────────────────
14
+ # quote_identifier tests
15
+ # ──────────────────────────────────────────────────────────────────────
16
+
17
+
18
+ class TestQuoteIdentifier:
19
+ """Verify quote_identifier on each dialect."""
20
+
21
+ def test_base_dialect_double_quotes(self):
22
+ assert BaseSQLDialect.quote_identifier("amount") == '"amount"'
23
+
24
+ def test_base_dialect_escapes_internal_quotes(self):
25
+ assert BaseSQLDialect.quote_identifier('col"name') == '"col""name"'
26
+
27
+ def test_base_dialect_prevents_injection(self):
28
+ malicious = 'sys_id); DROP TABLE users; --'
29
+ quoted = BaseSQLDialect.quote_identifier(malicious)
30
+ # Must be a single quoted identifier — entire payload wrapped in quotes
31
+ assert quoted.startswith('"')
32
+ assert quoted.endswith('"')
33
+ assert quoted == '"sys_id); DROP TABLE users; --"'
34
+ # When embedded in SQL like max("sys_id); DROP TABLE users; --"),
35
+ # the DB treats the whole thing as one (non-existent) column name
36
+
37
+ def test_postgres_inherits_default(self):
38
+ from velocity.db.servers.postgres.sql import SQL as PgSQL
39
+ assert PgSQL.quote_identifier("balance") == '"balance"'
40
+
41
+ def test_mysql_uses_backticks(self):
42
+ from velocity.db.servers.mysql.sql import SQL as MySqlSQL
43
+ assert MySqlSQL.quote_identifier("balance") == "`balance`"
44
+
45
+ def test_mysql_escapes_backtick(self):
46
+ from velocity.db.servers.mysql.sql import SQL as MySqlSQL
47
+ assert MySqlSQL.quote_identifier("col`name") == "`col``name`"
48
+
49
+ def test_sqlserver_uses_brackets(self):
50
+ from velocity.db.servers.sqlserver.sql import SQL as SsSQL
51
+ assert SsSQL.quote_identifier("balance") == "[balance]"
52
+
53
+ def test_sqlserver_escapes_bracket(self):
54
+ from velocity.db.servers.sqlserver.sql import SQL as SsSQL
55
+ assert SsSQL.quote_identifier("col]name") == "[col]]name]"
56
+
57
+
58
+ # ──────────────────────────────────────────────────────────────────────
59
+ # Aggregate column quoting integration tests
60
+ # ──────────────────────────────────────────────────────────────────────
61
+
62
+
63
+ def _make_table():
64
+ """Create a Table with a mocked transaction and Postgres SQL dialect."""
65
+ from velocity.db.core.table import Table
66
+ from velocity.db.servers.postgres.sql import SQL as PgSQL
67
+
68
+ tx = MagicMock()
69
+ tx.engine.sql = PgSQL
70
+ table = Table(tx, "accounts")
71
+ table._cursor_obj = MagicMock()
72
+ return table
73
+
74
+
75
+ class TestAggregateQuoting:
76
+ """Verify aggregate SQL uses quoted column names."""
77
+
78
+ def test_sum_quotes_column(self):
79
+ table = _make_table()
80
+ sql, _ = table.sum("balance", sql_only=True)
81
+ # Column should be double-quoted inside the aggregate
82
+ assert '"balance"' in sql
83
+ assert 'sum(coalesce("balance"' in sql
84
+
85
+ def test_max_quotes_column(self):
86
+ table = _make_table()
87
+ sql, _ = table.max("amount", sql_only=True)
88
+ assert 'max("amount")' in sql
89
+
90
+ def test_min_quotes_column(self):
91
+ table = _make_table()
92
+ sql, _ = table.min("amount", sql_only=True)
93
+ assert 'min("amount")' in sql
94
+
95
+ def test_sum_injection_prevented(self):
96
+ table = _make_table()
97
+ 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
101
+
102
+ def test_max_injection_prevented(self):
103
+ table = _make_table()
104
+ malicious = "x); DELETE FROM accounts; --"
105
+ sql, _ = table.max(malicious, sql_only=True)
106
+ assert "DELETE" not in sql.replace('"', "").split(")")[0]
107
+
108
+
109
+ # ──────────────────────────────────────────────────────────────────────
110
+ # DDL audit logging tests
111
+ # ──────────────────────────────────────────────────────────────────────
112
+
113
+
114
+ class TestDDLAuditLogging:
115
+ """Verify DDL operations emit WARNING log messages."""
116
+
117
+ def _make_table_with_sql(self):
118
+ from velocity.db.core.table import Table
119
+ tx = MagicMock()
120
+ sql_dialect = MagicMock()
121
+ sql_dialect.create_table.return_value = ("CREATE TABLE ...", [])
122
+ sql_dialect.drop_table.return_value = ("DROP TABLE ...", [])
123
+ sql_dialect.create_index.return_value = ("CREATE INDEX ...", [])
124
+ sql_dialect.drop_index.return_value = ("DROP INDEX ...", [])
125
+ sql_dialect.drop_column.return_value = ("ALTER TABLE DROP COLUMN ...", [])
126
+ sql_dialect.create_foreign_key.return_value = ("ALTER TABLE ADD FK ...", [])
127
+ sql_dialect.rename_table.return_value = ("ALTER TABLE RENAME ...", [])
128
+ sql_dialect.rename_column.return_value = ("ALTER TABLE RENAME COLUMN ...", [])
129
+ sql_dialect.alter_trigger.return_value = ("ALTER TABLE TRIGGER ...", [])
130
+ sql_dialect.create_view.return_value = ("CREATE VIEW ...", [])
131
+ sql_dialect.drop_view.return_value = ("DROP VIEW ...", [])
132
+ sql_dialect.set_sequence.return_value = ("ALTER SEQUENCE ...", [])
133
+ sql_dialect.alter_column_by_type.return_value = ("ALTER COLUMN ...", [])
134
+ tx.engine.sql = sql_dialect
135
+ tx.execute.return_value = MagicMock(cursor=MagicMock(), scalar=MagicMock(return_value=0))
136
+ table = Table(tx, "test_table")
137
+ table._cursor_obj = MagicMock()
138
+ return table
139
+
140
+ def test_create_table_logs_warning(self, caplog):
141
+ table = self._make_table_with_sql()
142
+ with caplog.at_level(logging.WARNING, logger="velocity.db.core.table"):
143
+ table.create(columns={"name": "text", "age": "int"})
144
+ assert any("DDL CREATE TABLE test_table" in r.message for r in caplog.records)
145
+ assert any("name" in r.message and "age" in r.message for r in caplog.records)
146
+
147
+ def test_drop_table_logs_warning(self, caplog):
148
+ table = self._make_table_with_sql()
149
+ with caplog.at_level(logging.WARNING, logger="velocity.db.core.table"):
150
+ table.drop()
151
+ assert any("DDL DROP TABLE test_table" in r.message for r in caplog.records)
152
+
153
+ def test_create_index_logs_warning(self, caplog):
154
+ table = self._make_table_with_sql()
155
+ with caplog.at_level(logging.WARNING, logger="velocity.db.core.table"):
156
+ table.create_index(columns=["email"], unique=True)
157
+ assert any("DDL CREATE INDEX" in r.message and "unique=True" in r.message for r in caplog.records)
158
+
159
+ def test_drop_column_logs_warning(self, caplog):
160
+ table = self._make_table_with_sql()
161
+ with caplog.at_level(logging.WARNING, logger="velocity.db.core.table"):
162
+ table.drop_column("old_col")
163
+ assert any("DDL DROP COLUMN old_col" in r.message for r in caplog.records)
164
+
165
+ def test_rename_column_logs_warning(self, caplog):
166
+ table = self._make_table_with_sql()
167
+ with caplog.at_level(logging.WARNING, logger="velocity.db.core.table"):
168
+ table.rename_column("old", "new")
169
+ assert any("DDL RENAME COLUMN" in r.message and "old -> new" in r.message for r in caplog.records)
170
+
171
+ def test_sql_only_does_not_log(self, caplog):
172
+ """sql_only=True should return SQL without logging."""
173
+ table = self._make_table_with_sql()
174
+ with caplog.at_level(logging.WARNING, logger="velocity.db.core.table"):
175
+ result = table.create_index(columns=["email"], sql_only=True)
176
+ assert not any("DDL" in r.message for r in caplog.records)
177
+ assert result is not None # Should return the SQL tuple
178
+
179
+
180
+ # ──────────────────────────────────────────────────────────────────────
181
+ # Credential masking in error messages
182
+ # ──────────────────────────────────────────────────────────────────────
183
+
184
+
185
+ class TestCredentialMasking:
186
+ """Verify credentials are masked in error messages raised by process_error."""
187
+
188
+ def test_password_masked_in_connection_error(self):
189
+ from velocity.db.core.engine import Engine
190
+ from velocity.db import exceptions
191
+
192
+ engine = MagicMock(spec=Engine)
193
+ engine.sql = MagicMock()
194
+ engine.sql.get_error.return_value = (None, "connection refused")
195
+ engine.sql.ConnectionErrorCodes = []
196
+ engine.sql.ApplicationErrorCodes = []
197
+ engine.sql.ColumnMissingErrorCodes = []
198
+ engine.sql.TableMissingErrorCodes = []
199
+ engine.sql.DatabaseMissingErrorCodes = []
200
+ engine.sql.ForeignKeyMissingErrorCodes = []
201
+ engine.sql.TruncationErrorCodes = []
202
+ engine.sql.DataIntegrityErrorCodes = []
203
+ engine.sql.DuplicateKeyErrorCodes = []
204
+ engine.sql.DatabaseObjectExistsErrorCodes = []
205
+ engine.sql.LockTimeoutErrorCodes = []
206
+ engine.sql.RetryTransactionCodes = []
207
+ engine.sql.is_connection_error_message.return_value = False
208
+
209
+ # Simulate a driver error that includes a password in its message
210
+ exc = Exception("connection to host=db.example.com password=s3cret123 failed")
211
+
212
+ with pytest.raises(Exception) as exc_info:
213
+ Engine.process_error(engine, exc)
214
+
215
+ msg = str(exc_info.value)
216
+ assert "s3cret123" not in msg
217
+ assert "*****" in msg
218
+
219
+ def test_url_credentials_masked(self):
220
+ from velocity.db.core.engine import Engine
221
+
222
+ engine = MagicMock(spec=Engine)
223
+ engine.sql = MagicMock()
224
+ engine.sql.get_error.return_value = (None, "fail")
225
+ engine.sql.ConnectionErrorCodes = []
226
+ engine.sql.ApplicationErrorCodes = []
227
+ engine.sql.ColumnMissingErrorCodes = []
228
+ engine.sql.TableMissingErrorCodes = []
229
+ engine.sql.DatabaseMissingErrorCodes = []
230
+ engine.sql.ForeignKeyMissingErrorCodes = []
231
+ engine.sql.TruncationErrorCodes = []
232
+ engine.sql.DataIntegrityErrorCodes = []
233
+ engine.sql.DuplicateKeyErrorCodes = []
234
+ engine.sql.DatabaseObjectExistsErrorCodes = []
235
+ engine.sql.LockTimeoutErrorCodes = []
236
+ engine.sql.RetryTransactionCodes = []
237
+ engine.sql.is_connection_error_message.return_value = False
238
+
239
+ exc = Exception("could not connect to postgresql://admin:supersecret@db:5432/mydb")
240
+
241
+ with pytest.raises(Exception) as exc_info:
242
+ Engine.process_error(engine, exc)
243
+
244
+ msg = str(exc_info.value)
245
+ assert "supersecret" not in msg
246
+ assert "*****" in msg
247
+
248
+ def test_mask_sensitive_in_string_function(self):
249
+ from velocity.db.utils import mask_sensitive_in_string
250
+
251
+ # password= pattern
252
+ assert "s3cret" not in mask_sensitive_in_string("password=s3cret host=db")
253
+ # URL pattern
254
+ assert "mypass" not in mask_sensitive_in_string("postgresql://user:mypass@host/db")
255
+ # No credentials — unchanged
256
+ assert mask_sensitive_in_string("normal error text") == "normal error text"
File without changes