velocity-python 0.1.124__tar.gz → 0.1.126__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 (310) hide show
  1. {velocity_python-0.1.124 → velocity_python-0.1.126}/PKG-INFO +1 -1
  2. {velocity_python-0.1.124 → velocity_python-0.1.126}/pyproject.toml +1 -1
  3. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/__init__.py +1 -1
  4. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/handlers/mixins/data_service.py +38 -0
  5. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/core/jsonproxy.py +32 -0
  6. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/core/table.py +30 -0
  7. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/servers/postgres/sql.py +22 -0
  8. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity_python.egg-info/PKG-INFO +1 -1
  9. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity_python.egg-info/SOURCES.txt +2 -1
  10. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_json_columns.py +57 -0
  11. velocity_python-0.1.126/tests/test_write_hook_empty_strings.py +146 -0
  12. {velocity_python-0.1.124 → velocity_python-0.1.126}/LICENSE +0 -0
  13. {velocity_python-0.1.124 → velocity_python-0.1.126}/README.md +0 -0
  14. {velocity_python-0.1.124 → velocity_python-0.1.126}/setup.cfg +0 -0
  15. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/__init__.py +0 -0
  16. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/amplify.py +0 -0
  17. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/amplify_build.py +0 -0
  18. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/assets/__init__.py +0 -0
  19. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/assets/backfill.py +0 -0
  20. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/assets/indexing.py +0 -0
  21. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/assets/references.py +0 -0
  22. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/assets/service.py +0 -0
  23. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/assets/usage_index.py +0 -0
  24. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/blobs.py +0 -0
  25. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/capabilities/__init__.py +0 -0
  26. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/capabilities/base.py +0 -0
  27. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/capabilities/dispatch.py +0 -0
  28. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/capabilities/infra.py +0 -0
  29. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/capabilities/jobs.py +0 -0
  30. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/capabilities/provisioning.py +0 -0
  31. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/capabilities/retention.py +0 -0
  32. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/capabilities/security.py +0 -0
  33. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/capabilities/single_session.py +0 -0
  34. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/deploy/__init__.py +0 -0
  35. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/deploy/config.py +0 -0
  36. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/deploy/operations.py +0 -0
  37. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/deploy/policies.py +0 -0
  38. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/deploy/reconcile.py +0 -0
  39. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/deploy/targets.py +0 -0
  40. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/dirty_pipeline.py +0 -0
  41. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/email.py +0 -0
  42. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/facets.py +0 -0
  43. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/feature_flags.py +0 -0
  44. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/handlers/__init__.py +0 -0
  45. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/handlers/base_handler.py +0 -0
  46. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/handlers/context.py +0 -0
  47. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/handlers/context_factory.py +0 -0
  48. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/handlers/exceptions.py +0 -0
  49. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/handlers/lambda_handler.py +0 -0
  50. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/handlers/masquerade.py +0 -0
  51. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
  52. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/handlers/mixins/job_handler.py +0 -0
  53. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/handlers/mixins/session_control.py +0 -0
  54. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
  55. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/handlers/perf.py +0 -0
  56. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/handlers/response.py +0 -0
  57. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/handlers/sqs_handler.py +0 -0
  58. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/job_lifecycle.py +0 -0
  59. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/job_results.py +0 -0
  60. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/mirror.py +0 -0
  61. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/ops.py +0 -0
  62. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/s3.py +0 -0
  63. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/sms.py +0 -0
  64. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/ssm_config.py +0 -0
  65. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/tests/__init__.py +0 -0
  66. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/tests/test_amplify_build_policy_size.py +0 -0
  67. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/tests/test_amplify_build_shim.py +0 -0
  68. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
  69. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/tests/test_deploy_policies.py +0 -0
  70. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/tests/test_deploy_reconcile.py +0 -0
  71. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/tests/test_email.py +0 -0
  72. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
  73. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/tests/test_response.py +0 -0
  74. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/aws/tests/test_sms.py +0 -0
  75. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/cache.py +0 -0
  76. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/__init__.py +0 -0
  77. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/core/__init__.py +0 -0
  78. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/core/async_support.py +0 -0
  79. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/core/column.py +0 -0
  80. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/core/database.py +0 -0
  81. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/core/decorators.py +0 -0
  82. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/core/engine.py +0 -0
  83. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/core/lazy_engine.py +0 -0
  84. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/core/result.py +0 -0
  85. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/core/row.py +0 -0
  86. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/core/sequence.py +0 -0
  87. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/core/transaction.py +0 -0
  88. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/core/view.py +0 -0
  89. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/exceptions.py +0 -0
  90. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/maintenance.py +0 -0
  91. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/migrations.py +0 -0
  92. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/servers/__init__.py +0 -0
  93. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/servers/base/__init__.py +0 -0
  94. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/servers/base/initializer.py +0 -0
  95. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/servers/base/operators.py +0 -0
  96. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/servers/base/sql.py +0 -0
  97. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/servers/base/types.py +0 -0
  98. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/servers/mysql/__init__.py +0 -0
  99. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/servers/mysql/operators.py +0 -0
  100. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/servers/mysql/reserved.py +0 -0
  101. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/servers/mysql/sql.py +0 -0
  102. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/servers/mysql/types.py +0 -0
  103. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/servers/postgres/__init__.py +0 -0
  104. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/servers/postgres/operators.py +0 -0
  105. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/servers/postgres/reserved.py +0 -0
  106. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/servers/postgres/types.py +0 -0
  107. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/servers/sqlite/__init__.py +0 -0
  108. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/servers/sqlite/operators.py +0 -0
  109. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/servers/sqlite/reserved.py +0 -0
  110. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/servers/sqlite/sql.py +0 -0
  111. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/servers/sqlite/types.py +0 -0
  112. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
  113. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/servers/sqlserver/operators.py +0 -0
  114. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
  115. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/servers/sqlserver/sql.py +0 -0
  116. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/servers/sqlserver/types.py +0 -0
  117. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/servers/tablehelper.py +0 -0
  118. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/tests/__init__.py +0 -0
  119. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/tests/common_db_test.py +0 -0
  120. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/tests/postgres/__init__.py +0 -0
  121. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/tests/postgres/common.py +0 -0
  122. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/tests/postgres/conftest.py +0 -0
  123. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/tests/postgres/test_column.py +0 -0
  124. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/tests/postgres/test_connections.py +0 -0
  125. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/tests/postgres/test_database.py +0 -0
  126. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/tests/postgres/test_engine.py +0 -0
  127. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
  128. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/tests/postgres/test_imports.py +0 -0
  129. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/tests/postgres/test_result.py +0 -0
  130. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/tests/postgres/test_row.py +0 -0
  131. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
  132. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
  133. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
  134. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
  135. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
  136. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/tests/postgres/test_table.py +0 -0
  137. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
  138. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
  139. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/tests/sql/__init__.py +0 -0
  140. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/tests/sql/common.py +0 -0
  141. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
  142. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
  143. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
  144. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/tests/test_db_utils.py +0 -0
  145. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/tests/test_postgres.py +0 -0
  146. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
  147. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
  148. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/tests/test_result_caching.py +0 -0
  149. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
  150. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
  151. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
  152. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
  153. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/tests/test_sql_builder.py +0 -0
  154. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/tests/test_tablehelper.py +0 -0
  155. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/tests/test_view_helper.py +0 -0
  156. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/db/utils.py +0 -0
  157. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/logging.py +0 -0
  158. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/misc/__init__.py +0 -0
  159. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/misc/conv/__init__.py +0 -0
  160. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/misc/conv/iconv.py +0 -0
  161. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/misc/conv/oconv.py +0 -0
  162. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/misc/db.py +0 -0
  163. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/misc/export.py +0 -0
  164. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/misc/format.py +0 -0
  165. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/misc/mail.py +0 -0
  166. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/misc/merge.py +0 -0
  167. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/misc/pdf.py +0 -0
  168. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/misc/tests/__init__.py +0 -0
  169. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/misc/tests/test_db.py +0 -0
  170. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/misc/tests/test_fix.py +0 -0
  171. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/misc/tests/test_format.py +0 -0
  172. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/misc/tests/test_iconv.py +0 -0
  173. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/misc/tests/test_mail_send.py +0 -0
  174. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/misc/tests/test_merge.py +0 -0
  175. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/misc/tests/test_oconv.py +0 -0
  176. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/misc/tests/test_original_error.py +0 -0
  177. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/misc/tests/test_timer.py +0 -0
  178. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/misc/timer.py +0 -0
  179. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/misc/tools.py +0 -0
  180. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/nosql/__init__.py +0 -0
  181. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/nosql/core/__init__.py +0 -0
  182. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/nosql/core/capabilities.py +0 -0
  183. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/nosql/core/store.py +0 -0
  184. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/nosql/core/where.py +0 -0
  185. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/nosql/exceptions.py +0 -0
  186. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/nosql/servers/__init__.py +0 -0
  187. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/nosql/servers/base/__init__.py +0 -0
  188. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/nosql/servers/cassandra/__init__.py +0 -0
  189. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/nosql/servers/cassandra/translation.py +0 -0
  190. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/nosql/servers/couchbase/__init__.py +0 -0
  191. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/nosql/servers/couchbase/translation.py +0 -0
  192. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/nosql/servers/dynamodb/__init__.py +0 -0
  193. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/nosql/servers/dynamodb/translation.py +0 -0
  194. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/nosql/servers/elasticsearch/__init__.py +0 -0
  195. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/nosql/servers/elasticsearch/translation.py +0 -0
  196. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/nosql/servers/memory/__init__.py +0 -0
  197. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/nosql/servers/mongodb/__init__.py +0 -0
  198. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/nosql/servers/mongodb/translation.py +0 -0
  199. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/nosql/servers/redis/__init__.py +0 -0
  200. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/payment/__init__.py +0 -0
  201. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/payment/authorizenet_adapter.py +0 -0
  202. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/payment/authorizenet_mirror.py +0 -0
  203. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/payment/base_adapter.py +0 -0
  204. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/payment/braintree_adapter.py +0 -0
  205. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/payment/braintree_mirror.py +0 -0
  206. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/payment/charge_rules.py +0 -0
  207. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/payment/stripe_adapter.py +0 -0
  208. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity/payment/stripe_mirror.py +0 -0
  209. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity_python.egg-info/dependency_links.txt +0 -0
  210. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity_python.egg-info/entry_points.txt +0 -0
  211. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity_python.egg-info/requires.txt +0 -0
  212. {velocity_python-0.1.124 → velocity_python-0.1.126}/src/velocity_python.egg-info/top_level.txt +0 -0
  213. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_access_error_403.py +0 -0
  214. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_activity_tracking_aborted_tx.py +0 -0
  215. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_amplify_build.py +0 -0
  216. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_asset_indexing.py +0 -0
  217. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_asset_references.py +0 -0
  218. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_assets_service.py +0 -0
  219. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_async_support.py +0 -0
  220. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_batch_operations.py +0 -0
  221. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_blob_store.py +0 -0
  222. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_capabilities.py +0 -0
  223. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_capability_catalog.py +0 -0
  224. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_cognito_user_cache.py +0 -0
  225. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_column_tx_arg.py +0 -0
  226. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_concurrency_safety.py +0 -0
  227. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_connection_pool.py +0 -0
  228. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_connection_resilience.py +0 -0
  229. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_context_flags_loader.py +0 -0
  230. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_context_job_descriptions.py +0 -0
  231. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_data_service_query_cap.py +0 -0
  232. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_data_service_renumber.py +0 -0
  233. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_data_service_save_as.py +0 -0
  234. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_db_credentials_ssm_cascade.py +0 -0
  235. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_db_maintenance.py +0 -0
  236. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_decorators.py +0 -0
  237. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_delete_hook_fk_blocked.py +0 -0
  238. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_dirty_pipeline_fast_path.py +0 -0
  239. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_email_processing.py +0 -0
  240. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_enqueue_metric_namespace.py +0 -0
  241. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_enqueue_send_failures.py +0 -0
  242. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_facets.py +0 -0
  243. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_feature_flags.py +0 -0
  244. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_get_cognito_user_provider.py +0 -0
  245. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_http_handler_rollback.py +0 -0
  246. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_iconv_money_to_cents.py +0 -0
  247. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_identifier_injection_guard.py +0 -0
  248. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_job_handler.py +0 -0
  249. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_job_lifecycle.py +0 -0
  250. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_job_results.py +0 -0
  251. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_jsonb_dict_adapter.py +0 -0
  252. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_lambda_handler.py +0 -0
  253. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_lambda_handler_auth.py +0 -0
  254. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_lambda_handler_masquerade.py +0 -0
  255. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_lambda_handler_user_cache.py +0 -0
  256. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_lazy_engine.py +0 -0
  257. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_masquerade_grant.py +0 -0
  258. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_mirror.py +0 -0
  259. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_mixins_import.py +0 -0
  260. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_n_plus_one.py +0 -0
  261. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_nosql_dynamodb.py +0 -0
  262. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_nosql_memory.py +0 -0
  263. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_nosql_mongodb.py +0 -0
  264. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_nosql_redis.py +0 -0
  265. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_nosql_translation.py +0 -0
  266. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_nosql_where.py +0 -0
  267. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_observability.py +0 -0
  268. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_on_commit.py +0 -0
  269. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_ops.py +0 -0
  270. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_payment_authorizenet_adapter.py +0 -0
  271. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_payment_braintree_adapter.py +0 -0
  272. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_payment_braintree_mirror.py +0 -0
  273. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_payment_mirror_empty_table.py +0 -0
  274. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_payment_profile_sorting.py +0 -0
  275. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_payment_stripe_adapter.py +0 -0
  276. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_pdf.py +0 -0
  277. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_prepared_statements.py +0 -0
  278. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_psycopg3_upgrade.py +0 -0
  279. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_query_cache.py +0 -0
  280. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_query_missing_table_unlocked.py +0 -0
  281. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_read_cache.py +0 -0
  282. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_response.py +0 -0
  283. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_restricted_direct_tables.py +0 -0
  284. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_result_returning.py +0 -0
  285. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_retry_side_effect_guard.py +0 -0
  286. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_return_default_safety.py +0 -0
  287. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_row_batch_update.py +0 -0
  288. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_row_cache_staleness.py +0 -0
  289. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_s3_bucket_config.py +0 -0
  290. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_schema_migrations.py +0 -0
  291. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_security_capability.py +0 -0
  292. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_security_hardening.py +0 -0
  293. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_server_cursor.py +0 -0
  294. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_session_control.py +0 -0
  295. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_single_autocommit_safety.py +0 -0
  296. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_single_session_capability.py +0 -0
  297. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_spreadsheet_functions.py +0 -0
  298. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_sqlite_backend.py +0 -0
  299. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_sqs_per_record_transactions.py +0 -0
  300. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_ssm_config.py +0 -0
  301. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_store_user_data.py +0 -0
  302. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_sys_modified_count_postgres_demo.py +0 -0
  303. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_table_alter.py +0 -0
  304. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_table_find_cache.py +0 -0
  305. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_table_purge.py +0 -0
  306. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_transaction_class_wrapping.py +0 -0
  307. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_transaction_commit_and_ownership.py +0 -0
  308. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_transaction_edge_cases.py +0 -0
  309. {velocity_python-0.1.124 → velocity_python-0.1.126}/tests/test_where_clause_validation.py +0 -0
  310. {velocity_python-0.1.124 → velocity_python-0.1.126}/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.124
3
+ Version: 0.1.126
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.124"
7
+ version = "0.1.126"
8
8
  authors = [
9
9
  { name="Velocity Team", email="info@codeclubs.org" },
10
10
  ]
@@ -1,4 +1,4 @@
1
- __version__ = version = "0.1.124"
1
+ __version__ = version = "0.1.126"
2
2
 
3
3
  import importlib as _importlib
4
4
 
@@ -56,6 +56,39 @@ def _fk_blocked_delete_message(table, exc):
56
56
  _IDENTIFIER_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
57
57
 
58
58
 
59
+ TEXT_TYPES = frozenset(
60
+ {"text", "character varying", "character", "varchar", "char", "citext", "name"}
61
+ )
62
+
63
+
64
+ def coerce_empty_strings(column_types, incoming):
65
+ """The subset of ``incoming`` whose empty-string values must become
66
+ ``None`` because their column is not a text type — ``{}`` when nothing
67
+ needs changing or the types are unknown."""
68
+ if not column_types:
69
+ return {}
70
+ out = {}
71
+ for key, value in incoming.items():
72
+ if not isinstance(value, str) or value.strip() != "":
73
+ continue
74
+ dtype = column_types.get(key)
75
+ if dtype is None:
76
+ continue
77
+ if str(dtype).lower() in TEXT_TYPES:
78
+ continue
79
+ out[key] = None
80
+ return out
81
+
82
+
83
+ def _column_types(tx, table):
84
+ """The table's ``{column: data_type}`` map, ``{}`` when unavailable —
85
+ a lookup failure must never block a write."""
86
+ try:
87
+ return tx.table(table).column_types(cache_ttl=300) or {}
88
+ except Exception: # noqa: BLE001 — best effort, the write proceeds
89
+ return {}
90
+
91
+
59
92
  def _bump_sys_id_sequence(tx, table):
60
93
  """Keep the table's serial/identity sequence ahead of MAX(sys_id) so a
61
94
  future auto-generated id can never collide with an explicitly chosen one
@@ -338,6 +371,11 @@ class DataServiceMixin:
338
371
  def write_hook(self, tx, table, sys_id, incoming, context, save_as=False):
339
372
  row = {}
340
373
  incoming.pop("sys_id", None)
374
+ # A form posts every field it shows; untouched NULLs come back as ''.
375
+ # Postgres refuses '' for boolean/date/timestamp/numeric columns
376
+ # (found live: UPDATE studio SET inactive = '' — 2026-09-26), so
377
+ # empty strings become NULL wherever the column is not text.
378
+ incoming.update(coerce_empty_strings(_column_types(tx, table), incoming))
341
379
  if sys_id == "@new":
342
380
  self._call_rwx_hook(
343
381
  "before_new", "common", tx, table, sys_id, incoming, context
@@ -68,6 +68,22 @@ class BoundList(list):
68
68
  super().__init__(_wrap(v, ctrl) for v in iterable)
69
69
  self._ctrl = ctrl
70
70
 
71
+ # -- copies are plain values ---------------------------------------
72
+ # A copy of a bound value is a plain list with no link to the row.
73
+ # Without these, copy.deepcopy rebuilt the proxy through the pickle
74
+ # path and refilled it with __setitem__ — one flush (an UPDATE) per
75
+ # nested container for a read-only copy, or an AttributeError when the
76
+ # rebuilt controller had no flush (2026-09-28, found by a dance forms
77
+ # migration deep-copying a definition read from a JSONB column).
78
+ def __copy__(self):
79
+ return unwrap(self)
80
+
81
+ def __deepcopy__(self, memo):
82
+ return unwrap(self)
83
+
84
+ def __reduce__(self):
85
+ return (list, (unwrap(self),))
86
+
71
87
  # -- mutators ------------------------------------------------------
72
88
  def __setitem__(self, index, value):
73
89
  if isinstance(index, slice):
@@ -131,6 +147,22 @@ class BoundDict(dict):
131
147
  super().__init__({k: _wrap(v, ctrl) for k, v in mapping.items()})
132
148
  self._ctrl = ctrl
133
149
 
150
+ # -- copies are plain values ---------------------------------------
151
+ # A copy of a bound value is a plain dict with no link to the row.
152
+ # Without these, copy.deepcopy rebuilt the proxy through the pickle
153
+ # path and refilled it with __setitem__ — one flush (an UPDATE) per
154
+ # nested container for a read-only copy, or an AttributeError when the
155
+ # rebuilt controller had no flush (2026-09-28, found by a dance forms
156
+ # migration deep-copying a definition read from a JSONB column).
157
+ def __copy__(self):
158
+ return unwrap(self)
159
+
160
+ def __deepcopy__(self, memo):
161
+ return unwrap(self)
162
+
163
+ def __reduce__(self):
164
+ return (dict, (unwrap(self),))
165
+
134
166
  # -- mutators ------------------------------------------------------
135
167
  def __setitem__(self, key, value):
136
168
  super().__setitem__(key, _wrap(value, self._ctrl))
@@ -641,6 +641,36 @@ class Table:
641
641
  )
642
642
  return load()
643
643
 
644
+ def column_types(self, **kwds):
645
+ """``{column_name: data_type}`` (information_schema names, e.g.
646
+ ``boolean``, ``timestamp with time zone``, ``text``). ``{}`` when the
647
+ server module has no ``column_types`` SQL. ``cache_ttl=N`` as in
648
+ ``sys_columns``."""
649
+ builder = getattr(self.sql, "column_types", None)
650
+ if builder is None:
651
+ return {}
652
+ sql, vals = builder(self.name)
653
+ if kwds.get("sql_only", False):
654
+ return sql, vals
655
+
656
+ def load():
657
+ result = self.tx.execute(sql, vals, cursor=self.cursor())
658
+ return {name: dtype for name, dtype in result.as_tuple()}
659
+
660
+ cache_ttl = kwds.get("cache_ttl")
661
+ if cache_ttl:
662
+ from velocity.cache import SCHEMA_TAG, read_cache
663
+
664
+ return dict(
665
+ read_cache.memo(
666
+ ("column_types", self._cache_scope(), self.name),
667
+ load,
668
+ cache_ttl,
669
+ tables=(self.name, SCHEMA_TAG),
670
+ )
671
+ )
672
+ return load()
673
+
644
674
  def columns(self):
645
675
  """
646
676
  Returns non-system column names.
@@ -1392,6 +1392,28 @@ class SQL(BaseSQLDialect):
1392
1392
  ]
1393
1393
  )
1394
1394
 
1395
+ @classmethod
1396
+ def column_types(cls, name):
1397
+ """``(column_name, data_type)`` rows for a table — the type map the
1398
+ write path uses to turn a form's empty strings into NULL for
1399
+ non-text columns."""
1400
+ if "." in name:
1401
+ return """
1402
+ select column_name, data_type
1403
+ from information_schema.columns
1404
+ where UPPER(table_schema) = UPPER(%s)
1405
+ and UPPER(table_name) = UPPER(%s)
1406
+ """, tuple(
1407
+ name.split(".")
1408
+ )
1409
+ return """
1410
+ select column_name, data_type
1411
+ from information_schema.columns
1412
+ where UPPER(table_name) = UPPER(%s)
1413
+ """, (
1414
+ name,
1415
+ )
1416
+
1395
1417
  @classmethod
1396
1418
  def column_info(cls, table, name):
1397
1419
  params = table.split(".")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: velocity-python
3
- Version: 0.1.124
3
+ Version: 0.1.126
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
@@ -304,4 +304,5 @@ tests/test_transaction_class_wrapping.py
304
304
  tests/test_transaction_commit_and_ownership.py
305
305
  tests/test_transaction_edge_cases.py
306
306
  tests/test_where_clause_validation.py
307
- tests/test_write_hook_create_flow.py
307
+ tests/test_write_hook_create_flow.py
308
+ tests/test_write_hook_empty_strings.py
@@ -423,3 +423,60 @@ class TestRowJsonTextDecode(unittest.TestCase):
423
423
 
424
424
  if __name__ == "__main__":
425
425
  unittest.main()
426
+
427
+
428
+ class TestProxyCopies(unittest.TestCase):
429
+ """A copy of a bound JSON value is a plain value: copying never
430
+ touches the row (2026-09-28)."""
431
+
432
+ def _bound(self):
433
+ from velocity.db.core import jsonproxy
434
+
435
+ writes = []
436
+ value = {
437
+ "pages": [{"ui": [{"copy": "x"}], "skippable": {"label": "s"}}],
438
+ "tags": ["a", ["b"]],
439
+ }
440
+ return jsonproxy.bind(value, writes.append), value, writes
441
+
442
+ def _assert_plain(self, value):
443
+ self.assertIn(type(value), (dict, list))
444
+ items = value.values() if isinstance(value, dict) else value
445
+ for item in items:
446
+ if isinstance(item, (dict, list)):
447
+ self._assert_plain(item)
448
+
449
+ def test_deepcopy_and_copy_are_plain_and_write_nothing(self):
450
+ import copy
451
+
452
+ bound, original, writes = self._bound()
453
+ deep = copy.deepcopy(bound)
454
+ shallow = copy.copy(bound)
455
+ self.assertEqual(deep, original)
456
+ self.assertEqual(shallow, original)
457
+ self._assert_plain(deep)
458
+ self._assert_plain(shallow)
459
+ self.assertEqual(writes, [])
460
+ # the copy is independent of the row
461
+ deep["pages"][0]["skippable"]["label"] = "changed"
462
+ self.assertEqual(bound["pages"][0]["skippable"]["label"], "s")
463
+ self.assertEqual(writes, [])
464
+ # nested proxies copy plain too
465
+ self._assert_plain(copy.deepcopy(bound["pages"]))
466
+ self._assert_plain(copy.copy(bound["tags"]))
467
+ self.assertEqual(writes, [])
468
+
469
+ def test_pickle_round_trip_is_plain(self):
470
+ import pickle
471
+
472
+ bound, original, writes = self._bound()
473
+ back = pickle.loads(pickle.dumps(bound))
474
+ self.assertEqual(back, original)
475
+ self._assert_plain(back)
476
+ self.assertEqual(writes, [])
477
+
478
+ def test_mutation_still_flushes(self):
479
+ bound, _original, writes = self._bound()
480
+ bound["pages"][0]["skippable"]["label"] = "t"
481
+ self.assertEqual(len(writes), 1)
482
+ self.assertEqual(writes[0]["pages"][0]["skippable"]["label"], "t")
@@ -0,0 +1,146 @@
1
+ """write_hook turns a form's empty strings into NULL for non-text columns
2
+ (found live 2026-09-26: UPDATE studio SET inactive = '' — "invalid input
3
+ syntax for type boolean"). Text columns keep '' (a cleared note is a note
4
+ that is empty); unknown columns and unknown types are left alone.
5
+ """
6
+
7
+ import importlib.util
8
+ import unittest
9
+ from pathlib import Path
10
+
11
+ MODULE_PATH = (
12
+ Path(__file__).resolve().parents[1]
13
+ / "src"
14
+ / "velocity"
15
+ / "aws"
16
+ / "handlers"
17
+ / "mixins"
18
+ / "data_service.py"
19
+ )
20
+ _spec = importlib.util.spec_from_file_location("test_write_hook_empty", MODULE_PATH)
21
+ _mod = importlib.util.module_from_spec(_spec)
22
+ _spec.loader.exec_module(_mod)
23
+
24
+ TYPES = {
25
+ "description": "text",
26
+ "notes": "character varying",
27
+ "inactive": "boolean",
28
+ "inactivated_on": "timestamp with time zone",
29
+ "start_date": "date",
30
+ "amount": "numeric",
31
+ "payload": "jsonb",
32
+ }
33
+
34
+
35
+ class CoerceTests(unittest.TestCase):
36
+ def test_empty_strings_become_null_for_non_text_columns(self):
37
+ incoming = {
38
+ "description": "Magnify 1",
39
+ "notes": "",
40
+ "inactive": "",
41
+ "inactivated_on": " ",
42
+ "start_date": "",
43
+ "amount": "",
44
+ "payload": "",
45
+ "unknown_col": "",
46
+ }
47
+ self.assertEqual(
48
+ _mod.coerce_empty_strings(TYPES, incoming),
49
+ {
50
+ "inactive": None,
51
+ "inactivated_on": None,
52
+ "start_date": None,
53
+ "amount": None,
54
+ "payload": None,
55
+ },
56
+ )
57
+
58
+ def test_real_values_and_unknown_types_untouched(self):
59
+ self.assertEqual(
60
+ _mod.coerce_empty_strings(TYPES, {"inactive": False, "amount": "0"}), {}
61
+ )
62
+ self.assertEqual(_mod.coerce_empty_strings({}, {"inactive": ""}), {})
63
+ self.assertEqual(_mod.coerce_empty_strings(None, {"inactive": ""}), {})
64
+
65
+
66
+ class FakeRow:
67
+ def __init__(self, data):
68
+ self.data = dict(data)
69
+ self.updates = []
70
+
71
+ def update(self, payload):
72
+ self.updates.append(dict(payload))
73
+ self.data.update(payload)
74
+
75
+ def to_dict(self):
76
+ return dict(self.data)
77
+
78
+ def __getitem__(self, k):
79
+ return self.data[k]
80
+
81
+
82
+ class FakeTable:
83
+ def __init__(self, row, types):
84
+ self.row = row
85
+ self.types = types
86
+
87
+ def find(self, sys_id):
88
+ return self.row
89
+
90
+ def column_types(self, **kwds):
91
+ return self.types
92
+
93
+
94
+ class FakeTx:
95
+ def __init__(self, table):
96
+ self._table = table
97
+
98
+ def table(self, name):
99
+ return self._table
100
+
101
+
102
+ class FakeContext:
103
+ def get_feature_flags(self, tx):
104
+ return {}
105
+
106
+ def enqueue_dirty(self, table, sys_id):
107
+ pass
108
+
109
+
110
+ class Service(_mod.DataServiceMixin):
111
+ def _call_rwx_hook(self, *args, **kwargs):
112
+ pass
113
+
114
+ def _get_dirty_pipeline_excluded_tables(self, tx, context):
115
+ return set()
116
+
117
+ def _dirty_pipeline_enabled(self, *a, **k):
118
+ return False
119
+
120
+
121
+ class WriteHookTests(unittest.TestCase):
122
+ def test_update_writes_null_not_empty_string(self):
123
+ row = FakeRow({"sys_id": 6, "description": "Magnify", "inactive": None})
124
+ tx = FakeTx(FakeTable(row, TYPES))
125
+ incoming = {
126
+ "sys_id": 6,
127
+ "description": "Magnify 1",
128
+ "inactive": "",
129
+ "inactivated_on": "",
130
+ "inactivated_by": "",
131
+ }
132
+ try:
133
+ Service().write_hook(tx, "studio", 6, incoming, FakeContext())
134
+ except AttributeError:
135
+ # the tail of write_hook touches dirty-pipeline plumbing the fake
136
+ # does not model; the update itself has already happened
137
+ pass
138
+ self.assertEqual(row.updates[0]["inactive"], None)
139
+ self.assertEqual(row.updates[0]["inactivated_on"], None)
140
+ # a text column keeps its empty string
141
+ self.assertEqual(row.updates[0]["inactivated_by"], "")
142
+ self.assertEqual(row.updates[0]["description"], "Magnify 1")
143
+
144
+
145
+ if __name__ == "__main__":
146
+ unittest.main()