velocity-python 0.1.68__tar.gz → 0.1.69__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 (208) hide show
  1. {velocity_python-0.1.68 → velocity_python-0.1.69}/PKG-INFO +1 -1
  2. {velocity_python-0.1.68 → velocity_python-0.1.69}/pyproject.toml +1 -1
  3. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/__init__.py +1 -1
  4. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/core/result.py +0 -10
  5. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/core/row.py +35 -77
  6. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/core/table.py +48 -37
  7. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/core/transaction.py +36 -9
  8. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/servers/base/sql.py +56 -0
  9. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/servers/mysql/sql.py +3 -2
  10. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/servers/postgres/sql.py +8 -3
  11. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/servers/sqlite/sql.py +57 -14
  12. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/servers/sqlite/types.py +10 -0
  13. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/servers/sqlserver/sql.py +2 -2
  14. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/tests/test_postgres.py +7 -24
  15. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity_python.egg-info/PKG-INFO +1 -1
  16. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity_python.egg-info/SOURCES.txt +1 -1
  17. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_json_columns.py +131 -21
  18. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_row_batch_update.py +0 -2
  19. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_row_cache_staleness.py +0 -2
  20. velocity_python-0.1.69/tests/test_sqlite_backend.py +182 -0
  21. velocity_python-0.1.68/tests/test_row_dirty_tracking.py +0 -193
  22. {velocity_python-0.1.68 → velocity_python-0.1.69}/LICENSE +0 -0
  23. {velocity_python-0.1.68 → velocity_python-0.1.69}/README.md +0 -0
  24. {velocity_python-0.1.68 → velocity_python-0.1.69}/setup.cfg +0 -0
  25. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/aws/__init__.py +0 -0
  26. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/aws/amplify.py +0 -0
  27. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/aws/amplify_build.py +0 -0
  28. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/aws/assets/__init__.py +0 -0
  29. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/aws/assets/backfill.py +0 -0
  30. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/aws/assets/indexing.py +0 -0
  31. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/aws/assets/references.py +0 -0
  32. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/aws/assets/service.py +0 -0
  33. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/aws/assets/usage_index.py +0 -0
  34. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/aws/dirty_pipeline.py +0 -0
  35. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/aws/handlers/__init__.py +0 -0
  36. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/aws/handlers/base_handler.py +0 -0
  37. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/aws/handlers/context.py +0 -0
  38. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/aws/handlers/context_factory.py +0 -0
  39. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/aws/handlers/exceptions.py +0 -0
  40. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/aws/handlers/lambda_handler.py +0 -0
  41. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/aws/handlers/masquerade.py +0 -0
  42. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
  43. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/aws/handlers/mixins/data_service.py +0 -0
  44. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
  45. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/aws/handlers/perf.py +0 -0
  46. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/aws/handlers/response.py +0 -0
  47. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/aws/handlers/sqs_handler.py +0 -0
  48. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/aws/s3.py +0 -0
  49. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/aws/ssm_config.py +0 -0
  50. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/aws/tests/__init__.py +0 -0
  51. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
  52. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
  53. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/aws/tests/test_response.py +0 -0
  54. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/__init__.py +0 -0
  55. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/core/__init__.py +0 -0
  56. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/core/async_support.py +0 -0
  57. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/core/column.py +0 -0
  58. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/core/database.py +0 -0
  59. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/core/decorators.py +0 -0
  60. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/core/engine.py +0 -0
  61. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/core/jsonproxy.py +0 -0
  62. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/core/sequence.py +0 -0
  63. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/core/view.py +0 -0
  64. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/exceptions.py +0 -0
  65. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/migrations.py +0 -0
  66. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/servers/__init__.py +0 -0
  67. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/servers/base/__init__.py +0 -0
  68. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/servers/base/initializer.py +0 -0
  69. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/servers/base/operators.py +0 -0
  70. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/servers/base/types.py +0 -0
  71. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/servers/mysql/__init__.py +0 -0
  72. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/servers/mysql/operators.py +0 -0
  73. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/servers/mysql/reserved.py +0 -0
  74. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/servers/mysql/types.py +0 -0
  75. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/servers/postgres/__init__.py +0 -0
  76. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/servers/postgres/operators.py +0 -0
  77. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/servers/postgres/reserved.py +0 -0
  78. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/servers/postgres/types.py +0 -0
  79. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/servers/sqlite/__init__.py +0 -0
  80. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/servers/sqlite/operators.py +0 -0
  81. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/servers/sqlite/reserved.py +0 -0
  82. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
  83. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/servers/sqlserver/operators.py +0 -0
  84. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
  85. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/servers/sqlserver/types.py +0 -0
  86. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/servers/tablehelper.py +0 -0
  87. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/tests/__init__.py +0 -0
  88. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/tests/common_db_test.py +0 -0
  89. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/tests/postgres/__init__.py +0 -0
  90. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/tests/postgres/common.py +0 -0
  91. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/tests/postgres/conftest.py +0 -0
  92. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/tests/postgres/test_column.py +0 -0
  93. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/tests/postgres/test_connections.py +0 -0
  94. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/tests/postgres/test_database.py +0 -0
  95. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/tests/postgres/test_engine.py +0 -0
  96. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
  97. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/tests/postgres/test_imports.py +0 -0
  98. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/tests/postgres/test_result.py +0 -0
  99. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/tests/postgres/test_row.py +0 -0
  100. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
  101. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
  102. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
  103. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
  104. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
  105. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/tests/postgres/test_table.py +0 -0
  106. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
  107. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
  108. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/tests/sql/__init__.py +0 -0
  109. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/tests/sql/common.py +0 -0
  110. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
  111. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
  112. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
  113. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/tests/test_db_utils.py +0 -0
  114. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
  115. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
  116. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/tests/test_result_caching.py +0 -0
  117. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
  118. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
  119. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
  120. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
  121. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/tests/test_sql_builder.py +0 -0
  122. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/tests/test_tablehelper.py +0 -0
  123. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/tests/test_view_helper.py +0 -0
  124. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/db/utils.py +0 -0
  125. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/logging.py +0 -0
  126. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/misc/__init__.py +0 -0
  127. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/misc/conv/__init__.py +0 -0
  128. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/misc/conv/iconv.py +0 -0
  129. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/misc/conv/oconv.py +0 -0
  130. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/misc/db.py +0 -0
  131. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/misc/export.py +0 -0
  132. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/misc/format.py +0 -0
  133. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/misc/mail.py +0 -0
  134. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/misc/merge.py +0 -0
  135. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/misc/pdf.py +0 -0
  136. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/misc/tests/__init__.py +0 -0
  137. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/misc/tests/test_db.py +0 -0
  138. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/misc/tests/test_fix.py +0 -0
  139. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/misc/tests/test_format.py +0 -0
  140. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/misc/tests/test_iconv.py +0 -0
  141. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/misc/tests/test_merge.py +0 -0
  142. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/misc/tests/test_oconv.py +0 -0
  143. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/misc/tests/test_original_error.py +0 -0
  144. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/misc/tests/test_timer.py +0 -0
  145. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/misc/timer.py +0 -0
  146. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/misc/tools.py +0 -0
  147. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/payment/__init__.py +0 -0
  148. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/payment/authorizenet_adapter.py +0 -0
  149. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/payment/authorizenet_mirror.py +0 -0
  150. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/payment/base_adapter.py +0 -0
  151. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/payment/braintree_adapter.py +0 -0
  152. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/payment/braintree_mirror.py +0 -0
  153. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/payment/charge_rules.py +0 -0
  154. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/payment/stripe_adapter.py +0 -0
  155. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity/payment/stripe_mirror.py +0 -0
  156. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity_python.egg-info/dependency_links.txt +0 -0
  157. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity_python.egg-info/entry_points.txt +0 -0
  158. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity_python.egg-info/requires.txt +0 -0
  159. {velocity_python-0.1.68 → velocity_python-0.1.69}/src/velocity_python.egg-info/top_level.txt +0 -0
  160. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_amplify_build.py +0 -0
  161. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_asset_indexing.py +0 -0
  162. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_asset_references.py +0 -0
  163. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_assets_service.py +0 -0
  164. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_async_support.py +0 -0
  165. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_batch_operations.py +0 -0
  166. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_concurrency_safety.py +0 -0
  167. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_connection_pool.py +0 -0
  168. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_connection_resilience.py +0 -0
  169. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_context_job_descriptions.py +0 -0
  170. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_db_credentials_ssm_cascade.py +0 -0
  171. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_decorators.py +0 -0
  172. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_dirty_pipeline_fast_path.py +0 -0
  173. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_email_processing.py +0 -0
  174. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_get_cognito_user_provider.py +0 -0
  175. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_http_handler_rollback.py +0 -0
  176. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_iconv_money_to_cents.py +0 -0
  177. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_identifier_injection_guard.py +0 -0
  178. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_jsonb_dict_adapter.py +0 -0
  179. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_lambda_handler.py +0 -0
  180. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_lambda_handler_auth.py +0 -0
  181. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_lambda_handler_masquerade.py +0 -0
  182. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_masquerade_grant.py +0 -0
  183. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_mixins_import.py +0 -0
  184. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_n_plus_one.py +0 -0
  185. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_observability.py +0 -0
  186. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_payment_authorizenet_adapter.py +0 -0
  187. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_payment_braintree_adapter.py +0 -0
  188. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_payment_braintree_mirror.py +0 -0
  189. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_payment_profile_sorting.py +0 -0
  190. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_payment_stripe_adapter.py +0 -0
  191. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_pdf.py +0 -0
  192. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_prepared_statements.py +0 -0
  193. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_psycopg3_upgrade.py +0 -0
  194. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_query_cache.py +0 -0
  195. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_restricted_direct_tables.py +0 -0
  196. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_retry_side_effect_guard.py +0 -0
  197. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_return_default_safety.py +0 -0
  198. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_schema_migrations.py +0 -0
  199. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_security_hardening.py +0 -0
  200. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_server_cursor.py +0 -0
  201. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_single_autocommit_safety.py +0 -0
  202. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_spreadsheet_functions.py +0 -0
  203. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_sqs_per_record_transactions.py +0 -0
  204. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_ssm_config.py +0 -0
  205. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_sys_modified_count_postgres_demo.py +0 -0
  206. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_table_alter.py +0 -0
  207. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_where_clause_validation.py +0 -0
  208. {velocity_python-0.1.68 → velocity_python-0.1.69}/tests/test_write_hook_create_flow.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: velocity-python
3
- Version: 0.1.68
3
+ Version: 0.1.69
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.68"
7
+ version = "0.1.69"
8
8
  authors = [
9
9
  { name="Velocity Team", email="info@codeclubs.org" },
10
10
  ]
@@ -1,4 +1,4 @@
1
- __version__ = version = "0.1.68"
1
+ __version__ = version = "0.1.69"
2
2
 
3
3
  import importlib as _importlib
4
4
 
@@ -1,4 +1,3 @@
1
- import warnings
2
1
  from velocity.misc.format import to_json
3
2
  from velocity.db.core.row import Row
4
3
 
@@ -288,15 +287,6 @@ class Result:
288
287
  self.transform = lambda row: list(zip(self.headers, row))
289
288
  return self
290
289
 
291
- def as_named_tuple(self):
292
- """Deprecated: use as_pairs() instead."""
293
- warnings.warn(
294
- "Result.as_named_tuple() is deprecated, use Result.as_pairs() instead.",
295
- DeprecationWarning,
296
- stacklevel=2,
297
- )
298
- return self.as_pairs()
299
-
300
290
  def as_list(self):
301
291
  """
302
292
  Transform each row into a list of values.
@@ -1,3 +1,4 @@
1
+ import json as _json
1
2
  import pprint
2
3
  import time as _time
3
4
  from collections.abc import MutableMapping
@@ -9,7 +10,7 @@ from velocity.db.core import jsonproxy
9
10
  # intercepted by __getattr__ / __setattr__.
10
11
  _INTERNAL_ATTRS = frozenset({
11
12
  "table", "pk", "_cache", "_column_set", "_batching", "_pending",
12
- "_cache_ttl", "_cache_time", "_no_cache", "_dirty_tracking", "_dirty",
13
+ "_cache_ttl", "_cache_time", "_no_cache",
13
14
  })
14
15
 
15
16
 
@@ -25,7 +26,7 @@ class Row(MutableMapping):
25
26
  write-through (immediate UPDATE) and also update the local cache.
26
27
  """
27
28
 
28
- def __init__(self, table, key, lock=None, cache_ttl=None, no_cache=False, dirty_tracking=False):
29
+ def __init__(self, table, key, lock=None, cache_ttl=None, no_cache=False):
29
30
  if isinstance(table, str):
30
31
  raise Exception("Table parameter must be a `table` instance.")
31
32
  object.__setattr__(self, "table", table)
@@ -48,8 +49,6 @@ class Row(MutableMapping):
48
49
  object.__setattr__(self, "_cache_ttl", cache_ttl)
49
50
  object.__setattr__(self, "_cache_time", None)
50
51
  object.__setattr__(self, "_no_cache", no_cache)
51
- object.__setattr__(self, "_dirty_tracking", dirty_tracking)
52
- object.__setattr__(self, "_dirty", {})
53
52
  if lock:
54
53
  self.lock()
55
54
 
@@ -80,8 +79,6 @@ class Row(MutableMapping):
80
79
  object.__setattr__(row, "_cache_ttl", None)
81
80
  object.__setattr__(row, "_cache_time", _time.monotonic())
82
81
  object.__setattr__(row, "_no_cache", False)
83
- object.__setattr__(row, "_dirty_tracking", False)
84
- object.__setattr__(row, "_dirty", {})
85
82
  return row
86
83
 
87
84
  # ------------------------------------------------------------------
@@ -129,29 +126,46 @@ class Row(MutableMapping):
129
126
  # MutableMapping protocol
130
127
  # ------------------------------------------------------------------
131
128
 
129
+ def _bind_json(self, key, val):
130
+ """
131
+ Wrap a JSON column value in a live proxy so in-place mutation writes
132
+ the whole value back. On backends whose driver returns JSON as text
133
+ (everything except PostgreSQL), decode the string first when *key* is a
134
+ known JSON column. Non-JSON values pass through unchanged.
135
+ """
136
+ if isinstance(val, (jsonproxy.BoundDict, jsonproxy.BoundList)):
137
+ return val
138
+ if isinstance(val, str):
139
+ # Possible JSON text from a non-native backend. Decode only for
140
+ # columns the table reports as JSON; tables that don't implement
141
+ # json_columns() (mocks, external Table-likes) skip decoding.
142
+ json_cols = getattr(self.table, "json_columns", None)
143
+ if json_cols is None:
144
+ return val
145
+ try:
146
+ if key in json_cols():
147
+ val = _json.loads(val)
148
+ else:
149
+ return val
150
+ except (ValueError, TypeError):
151
+ return val
152
+ if isinstance(val, (dict, list)):
153
+ return jsonproxy.bind(
154
+ val, lambda plain, _k=key: self.__setitem__(_k, plain)
155
+ )
156
+ return val
157
+
132
158
  def __getitem__(self, key):
133
159
  if key in self.pk:
134
160
  return self.pk[key]
135
161
  cache = self._ensure_cache()
136
162
  if key in cache:
137
- val = cache[key]
138
- # JSON columns come back as plain dict/list — wrap them in live
139
- # proxies so in-place mutation writes the whole value back.
140
- if isinstance(val, (dict, list)) and not isinstance(
141
- val, (jsonproxy.BoundDict, jsonproxy.BoundList)
142
- ):
143
- val = jsonproxy.bind(
144
- val, lambda plain, _k=key: self.__setitem__(_k, plain)
145
- )
163
+ val = self._bind_json(key, cache[key])
164
+ if isinstance(val, (jsonproxy.BoundDict, jsonproxy.BoundList)):
146
165
  cache[key] = val
147
166
  return val
148
167
  # Fall back to a direct DB fetch for columns not in the initial SELECT
149
- val = self.table.get_value(key, self.pk)
150
- if isinstance(val, (dict, list)):
151
- val = jsonproxy.bind(
152
- val, lambda plain, _k=key: self.__setitem__(_k, plain)
153
- )
154
- return val
168
+ return self._bind_json(key, self.table.get_value(key, self.pk))
155
169
 
156
170
  def __setitem__(self, key, val):
157
171
  if key in self.pk:
@@ -163,13 +177,6 @@ class Row(MutableMapping):
163
177
  self._cache[key] = val
164
178
  object.__setattr__(self, "_column_set", None)
165
179
  return
166
- if self._dirty_tracking:
167
- self._dirty[key] = val
168
- # Update local cache optimistically so reads see the dirty value
169
- if self._cache is not None:
170
- self._cache[key] = val
171
- object.__setattr__(self, "_column_set", None)
172
- return
173
180
  self.table.update_or_insert({key: val}, pk=self.pk)
174
181
  # Invalidate cache so trigger-computed columns are re-fetched
175
182
  object.__setattr__(self, "_cache", None)
@@ -420,55 +427,6 @@ class Row(MutableMapping):
420
427
  """
421
428
  return _BatchContext(self)
422
429
 
423
- def save(self):
424
- """Flush accumulated dirty-tracking changes to the database.
425
-
426
- When ``dirty_tracking=True``, assignments to the row accumulate
427
- in memory instead of writing through immediately. Call
428
- ``.save()`` to flush them all in a single UPDATE::
429
-
430
- row = Row(table, 1, dirty_tracking=True)
431
- row["name"] = "John"
432
- row["email"] = "john@example.com"
433
- row.save() # Single UPDATE with both columns
434
-
435
- Returns ``self`` for chaining.
436
-
437
- Raises:
438
- RuntimeError: If dirty tracking is not enabled on this row.
439
- """
440
- if not self._dirty_tracking:
441
- raise RuntimeError(
442
- "save() requires dirty_tracking=True. "
443
- "Use Row(table, key, dirty_tracking=True) or row.update({...})."
444
- )
445
- dirty = self._dirty
446
- object.__setattr__(self, "_dirty", {})
447
- if dirty:
448
- self.table.update_or_insert(dirty, pk=self.pk)
449
- # Invalidate cache so trigger-computed columns are re-fetched
450
- object.__setattr__(self, "_cache", None)
451
- object.__setattr__(self, "_column_set", None)
452
- return self
453
-
454
- @property
455
- def is_dirty(self):
456
- """Return True if there are unsaved dirty-tracking changes."""
457
- return bool(self._dirty)
458
-
459
- def discard(self):
460
- """Discard accumulated dirty-tracking changes without writing to DB.
461
-
462
- Also invalidates the cache so the next read re-fetches from the
463
- database, undoing any optimistic cache updates.
464
-
465
- Returns ``self`` for chaining.
466
- """
467
- object.__setattr__(self, "_dirty", {})
468
- object.__setattr__(self, "_cache", None)
469
- object.__setattr__(self, "_column_set", None)
470
- return self
471
-
472
430
  def touch(self):
473
431
  """
474
432
  Update sys_modified to current timestamp.
@@ -1,6 +1,5 @@
1
1
  import logging
2
2
  import re
3
- import warnings
4
3
  import sqlparse
5
4
  from collections import defaultdict
6
5
  from collections.abc import Iterable, Mapping, Sequence
@@ -106,6 +105,11 @@ def _is_nullable_flag(value):
106
105
  return None
107
106
 
108
107
 
108
+ _COLUMN_SPEC_OPTION_KEYS = frozenset(
109
+ {"type", "value", "add_value", "nullable", "sql", "using"}
110
+ )
111
+
112
+
109
113
  def _parse_column_spec(spec, default_nullable):
110
114
  """Normalize user-provided column specification into a common structure."""
111
115
 
@@ -121,7 +125,11 @@ def _parse_column_spec(spec, default_nullable):
121
125
  base, override_options = spec
122
126
 
123
127
  options = {}
124
- if isinstance(base, Mapping):
128
+ # A Mapping base is an options spec (e.g. {"type": "TEXT", "nullable": False})
129
+ # only when every key is a recognized option keyword. A plain JSON data
130
+ # value that happens to be a dict (e.g. {"k": 1}) must be kept as the value
131
+ # so its column types as JSON/JSONB — not silently mis-read as options.
132
+ if isinstance(base, Mapping) and base and set(base) <= _COLUMN_SPEC_OPTION_KEYS:
125
133
  options.update(base)
126
134
  base = options.get("type", options.get("value"))
127
135
 
@@ -590,7 +598,8 @@ class Table:
590
598
  if kwds.get("sql_only", False):
591
599
  return sql, vals
592
600
  result = self.tx.execute(sql, vals, cursor=self.cursor())
593
- return [x[0] for x in result.as_tuple()]
601
+ name_idx = self.sql.columns_name_index
602
+ return [x[name_idx] for x in result.as_tuple()]
594
603
 
595
604
  def columns(self):
596
605
  """
@@ -723,9 +732,12 @@ class Table:
723
732
  Optionally drops any existing table first.
724
733
  """
725
734
  columns = columns or {}
726
- sql, vals = self.sql.create_table(self.name, columns, drop)
727
735
  _ddl_logger.warning("DDL CREATE TABLE %s columns=%s drop=%s", self.name, list(columns.keys()), drop)
728
- self.tx.execute(sql, vals, cursor=self.cursor())
736
+ # Execute statement-by-statement: SQLite's driver rejects
737
+ # multi-statement execute() calls. Backends whose CREATE TABLE is a
738
+ # single (possibly multi-statement) string return a one-element list.
739
+ for stmt in self.sql.create_table_statements(self.name, columns, drop):
740
+ self.tx.execute(stmt, tuple(), cursor=self.cursor())
729
741
 
730
742
  def _ensure_schema_unlocked(self, operation):
731
743
  if self.tx.engine.schema_locked:
@@ -1921,8 +1933,16 @@ class Table:
1921
1933
  null_statements.append((sql, vals))
1922
1934
 
1923
1935
  if to_add:
1924
- sql, vals = self.sql.alter_add(self.name, to_add, default_nullable)
1925
- statements.append((sql, vals))
1936
+ # alter_add_statements returns one (sql, vals) per column so SQLite
1937
+ # (single-statement execute) works; other dialects return a single
1938
+ # combined statement. Fall back to alter_add for SQL implementations
1939
+ # that predate the per-statement contract.
1940
+ add_stmts_fn = getattr(self.sql, "alter_add_statements", None)
1941
+ if add_stmts_fn is not None:
1942
+ statements.extend(add_stmts_fn(self.name, to_add, default_nullable))
1943
+ else:
1944
+ sql, vals = self.sql.alter_add(self.name, to_add, default_nullable)
1945
+ statements.append((sql, vals))
1926
1946
 
1927
1947
  for column_name, spec in add_specs.items():
1928
1948
  if (
@@ -1984,6 +2004,26 @@ class Table:
1984
2004
  _ddl_logger.warning("DDL ALTER COLUMN TYPE on %s column=%s", self.name, column)
1985
2005
  self.tx.execute(sql, vals, cursor=self.cursor())
1986
2006
 
2007
+ def json_columns(self):
2008
+ """
2009
+ Return the (cached) set of column names on this table that hold JSON
2010
+ values and need Row-layer decoding on read.
2011
+
2012
+ Empty on backends whose driver round-trips JSON natively (PostgreSQL),
2013
+ so the Row read path stays zero-cost there.
2014
+ """
2015
+ if self.sql.json_round_trips_natively:
2016
+ return frozenset()
2017
+ cache = self.tx._json_columns_cache
2018
+ names = cache.get(self.name)
2019
+ if names is None:
2020
+ try:
2021
+ names = self.sql.json_column_names(self.tx, self.name)
2022
+ except Exception:
2023
+ names = frozenset()
2024
+ cache[self.name] = names
2025
+ return names
2026
+
1987
2027
  def _adapt_data(self, data):
1988
2028
  """
1989
2029
  Apply the dialect's data-value adaptation (e.g. dict/list -> JSONB on
@@ -2177,9 +2217,7 @@ class Table:
2177
2217
  else:
2178
2218
  exists_where = pk
2179
2219
 
2180
- ins_builder = getattr(self.sql, "insert_if_not_exists", None) or getattr(
2181
- self.sql, "insnx", None
2182
- )
2220
+ ins_builder = getattr(self.sql, "insert_if_not_exists", None)
2183
2221
  if ins_builder is None:
2184
2222
  raise NotImplementedError(
2185
2223
  "Current SQL dialect does not support insert-if-not-exists operations."
@@ -2193,15 +2231,6 @@ class Table:
2193
2231
  result = self.tx.execute(sql, vals, cursor=self.cursor())
2194
2232
  return result.cursor.rowcount if result.cursor else 0
2195
2233
 
2196
- @property
2197
- def updins(self):
2198
- warnings.warn(
2199
- "Table.updins is deprecated, use Table.update_or_insert instead.",
2200
- DeprecationWarning,
2201
- stacklevel=2,
2202
- )
2203
- return self.update_or_insert
2204
-
2205
2234
  @create_missing
2206
2235
  def insert_if_not_exists(self, data, where=None, **kwds):
2207
2236
  """
@@ -2223,26 +2252,8 @@ class Table:
2223
2252
  result = self.tx.execute(sql, vals, cursor=self.cursor())
2224
2253
  return result.cursor.rowcount if result.cursor else 0
2225
2254
 
2226
- @property
2227
- def insnx(self):
2228
- warnings.warn(
2229
- "Table.insnx is deprecated, use Table.insert_if_not_exists instead.",
2230
- DeprecationWarning,
2231
- stacklevel=2,
2232
- )
2233
- return self.insert_if_not_exists
2234
-
2235
2255
  upsert = merge
2236
2256
 
2237
- @property
2238
- def indate(self):
2239
- warnings.warn(
2240
- "Table.indate is deprecated, use Table.merge (or Table.upsert) instead.",
2241
- DeprecationWarning,
2242
- stacklevel=2,
2243
- )
2244
- return self.merge
2245
-
2246
2257
  @return_default(0)
2247
2258
  def count(self, where=None, **kwds):
2248
2259
  """
@@ -176,6 +176,10 @@ class Transaction:
176
176
  # R14 — N+1 detection: per-table SELECT counts.
177
177
  self._table_select_counts: dict[str, int] = {}
178
178
  self._n1_warned: set[str] = set()
179
+ # JSON-column name cache (per table) for backends whose driver returns
180
+ # JSON columns as text and need Row-layer decoding. None = not yet
181
+ # looked up; invalidated by invalidate_cache().
182
+ self._json_columns_cache: dict[str, frozenset] = {}
179
183
  # R21 — set once an irreversible external side effect (e.g. a payment
180
184
  # charge/capture) has happened in this transaction. The engine's
181
185
  # auto-retry envelope must NOT re-run a function after this, or a
@@ -268,15 +272,32 @@ class Transaction:
268
272
  if prepare is None:
269
273
  prepare = getattr(self.engine, "prepare_enabled", False)
270
274
 
275
+ # Only psycopg3 accepts the 'prepare' keyword. Once a driver rejects
276
+ # it we remember that on the engine so we don't raise+catch a TypeError
277
+ # on every subsequent statement (sqlite/mysql/sqlserver).
278
+ supports_prepare_kw = getattr(self.engine, "_supports_prepare_kw", True)
279
+
271
280
  t0 = _time.perf_counter()
272
281
  try:
273
- try:
274
- if parms:
275
- cursor.execute(sql, parms, prepare=prepare)
276
- else:
277
- cursor.execute(sql, prepare=prepare)
278
- except TypeError:
279
- # Driver doesn't support 'prepare' keyword (non-psycopg3).
282
+ if supports_prepare_kw:
283
+ try:
284
+ if parms:
285
+ cursor.execute(sql, parms, prepare=prepare)
286
+ else:
287
+ cursor.execute(sql, prepare=prepare)
288
+ except TypeError:
289
+ # Driver doesn't support 'prepare' keyword (non-psycopg3).
290
+ self.engine._supports_prepare_kw = False
291
+ try:
292
+ if parms:
293
+ cursor.execute(sql, parms)
294
+ else:
295
+ cursor.execute(sql)
296
+ except Exception as e:
297
+ raise self.engine.process_error(e, sql, parms)
298
+ except Exception as e:
299
+ raise self.engine.process_error(e, sql, parms)
300
+ else:
280
301
  try:
281
302
  if parms:
282
303
  cursor.execute(sql, parms)
@@ -284,8 +305,6 @@ class Transaction:
284
305
  cursor.execute(sql)
285
306
  except Exception as e:
286
307
  raise self.engine.process_error(e, sql, parms)
287
- except Exception as e:
288
- raise self.engine.process_error(e, sql, parms)
289
308
 
290
309
  elapsed_ms = (_time.perf_counter() - t0) * 1000
291
310
  self._query_count += 1
@@ -481,10 +500,16 @@ class Transaction:
481
500
  rolls back. Use this to serialize DDL or other operations across
482
501
  concurrent Lambda containers.
483
502
 
503
+ No-op on non-PostgreSQL backends (advisory locks are a PostgreSQL
504
+ feature) — emitting the ``pg_advisory_xact_lock`` statement elsewhere
505
+ would raise a syntax error that callers must then swallow.
506
+
484
507
  Args:
485
508
  key: An integer lock key, or a string that will be hashed to
486
509
  a 64-bit integer.
487
510
  """
511
+ if not getattr(self.engine.sql, "supports_advisory_lock", False):
512
+ return
488
513
  if isinstance(key, str):
489
514
  import hashlib
490
515
  # Use first 8 bytes of SHA-256 as a signed 64-bit int.
@@ -531,7 +556,9 @@ class Transaction:
531
556
  """
532
557
  if table_name is None:
533
558
  self.__query_cache.clear()
559
+ self._json_columns_cache.clear()
534
560
  return
561
+ self._json_columns_cache.pop(table_name, None)
535
562
  # Simple substring match on the SQL text (first element of the key).
536
563
  tbl_lower = table_name.lower()
537
564
  to_remove = [
@@ -22,9 +22,19 @@ class BaseSQLDialect(ABC):
22
22
  type_column_identifier: str = ""
23
23
  is_nullable: str = ""
24
24
 
25
+ # Position of the column NAME within each row returned by columns().
26
+ # Most dialects put it first; SQLite's PRAGMA table_info has cid at 0 and
27
+ # the name at 1.
28
+ columns_name_index: int = 0
29
+
25
30
  # Default schema name for this database
26
31
  default_schema: str = ""
27
32
 
33
+ # PostgreSQL-style transaction advisory locks (pg_advisory_xact_lock).
34
+ # False elsewhere so Transaction.advisory_lock() is a clean no-op instead
35
+ # of emitting a statement the backend can't parse.
36
+ supports_advisory_lock: bool = False
37
+
28
38
  # Error code classifications - must be set by subclasses
29
39
  ApplicationErrorCodes: List[str] = []
30
40
  DatabaseMissingErrorCodes: List[str] = []
@@ -54,6 +64,52 @@ class BaseSQLDialect(ABC):
54
64
  return json.dumps(val)
55
65
  return val
56
66
 
67
+ # ------------------------------------------------------------------
68
+ # JSON-column read support
69
+ # ------------------------------------------------------------------
70
+ # When True, the driver returns JSON columns already decoded into
71
+ # Python dict/list (PostgreSQL/psycopg). When False, JSON columns come
72
+ # back as text and the Row layer decodes them — see ``json_column_names``.
73
+ json_round_trips_natively: bool = False
74
+
75
+ @classmethod
76
+ def json_column_names(cls, tx, table):
77
+ """
78
+ Return the set of column names on *table* that hold JSON values, used
79
+ by the Row layer to decode JSON text on backends where the driver does
80
+ not round-trip JSON natively.
81
+
82
+ Default: empty set (no decoding). Backends that store JSON in a
83
+ distinguishable declared type override this.
84
+ """
85
+ return frozenset()
86
+
87
+ @classmethod
88
+ def create_table_statements(cls, name, columns=None, drop=False):
89
+ """
90
+ Return CREATE TABLE as a list of individual SQL statements.
91
+
92
+ Most dialects build one (possibly multi-statement) string that the
93
+ driver executes in a single call; the default wraps that string as a
94
+ one-element list. SQLite overrides this because ``sqlite3`` rejects
95
+ multi-statement ``execute()`` calls, so each statement must run
96
+ separately.
97
+ """
98
+ sql, _vals = cls.create_table(name, columns or {}, drop)
99
+ return [sql]
100
+
101
+ @classmethod
102
+ def alter_add_statements(cls, table, columns, null_allowed=True):
103
+ """
104
+ Return ADD COLUMN DDL as a list of ``(sql, params)`` statements.
105
+
106
+ Default wraps the dialect's single ``alter_add`` string as one element;
107
+ SQLite overrides this so each column is its own statement (its driver
108
+ rejects multi-statement ``execute()``).
109
+ """
110
+ sql, vals = cls.alter_add(table, columns, null_allowed)
111
+ return [(sql, vals)]
112
+
57
113
  @classmethod
58
114
  def quote_identifier(cls, name: str) -> str:
59
115
  """Always-quote a single SQL identifier to prevent injection.
@@ -588,11 +588,12 @@ END;
588
588
 
589
589
  @classmethod
590
590
  def columns(cls, name):
591
- return f"SHOW COLUMNS FROM {quote(name)}"
591
+ # SHOW COLUMNS rows: (Field, Type, Null, Key, Default, Extra) — name at 0.
592
+ return f"SHOW COLUMNS FROM {quote(name)}", tuple()
592
593
 
593
594
  @classmethod
594
595
  def column_info(cls, table, name):
595
- return f"SHOW COLUMNS FROM {quote(table)} LIKE '{name}'"
596
+ return f"SHOW COLUMNS FROM {quote(table)} LIKE '{name}'", tuple()
596
597
 
597
598
  @classmethod
598
599
  def drop_column(cls, table, name, cascade=True):
@@ -37,6 +37,13 @@ class SQL(BaseSQLDialect):
37
37
 
38
38
  default_schema = "public"
39
39
 
40
+ # psycopg decodes JSONB/JSON into dict/list automatically — no Row-layer
41
+ # decode needed (and the Row layer skips all JSON metadata lookups).
42
+ json_round_trips_natively = True
43
+
44
+ # PostgreSQL supports pg_advisory_xact_lock (see Transaction.advisory_lock).
45
+ supports_advisory_lock = True
46
+
40
47
  ApplicationErrorCodes = [
41
48
  "22P02",
42
49
  "42883",
@@ -907,7 +914,7 @@ class SQL(BaseSQLDialect):
907
914
  return sql, args_list, values_template
908
915
 
909
916
  @classmethod
910
- def insnx(cls, tx, table, data, where=None):
917
+ def insert_if_not_exists(cls, tx, table, data, where=None):
911
918
  """Insert only when the supplied predicate finds no existing row."""
912
919
  if not table:
913
920
  raise ValueError("Table name is required.")
@@ -1001,8 +1008,6 @@ class SQL(BaseSQLDialect):
1001
1008
  )
1002
1009
  return final_sql, tuple(vals)
1003
1010
 
1004
- insert_if_not_exists = insnx
1005
-
1006
1011
  @classmethod
1007
1012
  def version(cls):
1008
1013
  return "select version()", tuple()