velocity-python 0.1.123__tar.gz → 0.1.125__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.123 → velocity_python-0.1.125}/PKG-INFO +1 -1
  2. {velocity_python-0.1.123 → velocity_python-0.1.125}/pyproject.toml +1 -1
  3. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/__init__.py +1 -1
  4. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/facets.py +26 -1
  5. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/handlers/mixins/data_service.py +38 -0
  6. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/core/table.py +30 -0
  7. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/servers/postgres/sql.py +22 -0
  8. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity_python.egg-info/PKG-INFO +1 -1
  9. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity_python.egg-info/SOURCES.txt +2 -1
  10. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_facets.py +32 -1
  11. velocity_python-0.1.125/tests/test_write_hook_empty_strings.py +146 -0
  12. {velocity_python-0.1.123 → velocity_python-0.1.125}/LICENSE +0 -0
  13. {velocity_python-0.1.123 → velocity_python-0.1.125}/README.md +0 -0
  14. {velocity_python-0.1.123 → velocity_python-0.1.125}/setup.cfg +0 -0
  15. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/__init__.py +0 -0
  16. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/amplify.py +0 -0
  17. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/amplify_build.py +0 -0
  18. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/assets/__init__.py +0 -0
  19. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/assets/backfill.py +0 -0
  20. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/assets/indexing.py +0 -0
  21. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/assets/references.py +0 -0
  22. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/assets/service.py +0 -0
  23. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/assets/usage_index.py +0 -0
  24. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/blobs.py +0 -0
  25. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/capabilities/__init__.py +0 -0
  26. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/capabilities/base.py +0 -0
  27. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/capabilities/dispatch.py +0 -0
  28. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/capabilities/infra.py +0 -0
  29. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/capabilities/jobs.py +0 -0
  30. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/capabilities/provisioning.py +0 -0
  31. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/capabilities/retention.py +0 -0
  32. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/capabilities/security.py +0 -0
  33. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/capabilities/single_session.py +0 -0
  34. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/deploy/__init__.py +0 -0
  35. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/deploy/config.py +0 -0
  36. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/deploy/operations.py +0 -0
  37. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/deploy/policies.py +0 -0
  38. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/deploy/reconcile.py +0 -0
  39. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/deploy/targets.py +0 -0
  40. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/dirty_pipeline.py +0 -0
  41. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/email.py +0 -0
  42. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/feature_flags.py +0 -0
  43. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/handlers/__init__.py +0 -0
  44. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/handlers/base_handler.py +0 -0
  45. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/handlers/context.py +0 -0
  46. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/handlers/context_factory.py +0 -0
  47. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/handlers/exceptions.py +0 -0
  48. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/handlers/lambda_handler.py +0 -0
  49. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/handlers/masquerade.py +0 -0
  50. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
  51. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/handlers/mixins/job_handler.py +0 -0
  52. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/handlers/mixins/session_control.py +0 -0
  53. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
  54. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/handlers/perf.py +0 -0
  55. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/handlers/response.py +0 -0
  56. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/handlers/sqs_handler.py +0 -0
  57. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/job_lifecycle.py +0 -0
  58. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/job_results.py +0 -0
  59. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/mirror.py +0 -0
  60. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/ops.py +0 -0
  61. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/s3.py +0 -0
  62. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/sms.py +0 -0
  63. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/ssm_config.py +0 -0
  64. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/tests/__init__.py +0 -0
  65. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/tests/test_amplify_build_policy_size.py +0 -0
  66. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/tests/test_amplify_build_shim.py +0 -0
  67. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
  68. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/tests/test_deploy_policies.py +0 -0
  69. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/tests/test_deploy_reconcile.py +0 -0
  70. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/tests/test_email.py +0 -0
  71. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
  72. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/tests/test_response.py +0 -0
  73. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/aws/tests/test_sms.py +0 -0
  74. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/cache.py +0 -0
  75. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/__init__.py +0 -0
  76. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/core/__init__.py +0 -0
  77. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/core/async_support.py +0 -0
  78. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/core/column.py +0 -0
  79. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/core/database.py +0 -0
  80. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/core/decorators.py +0 -0
  81. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/core/engine.py +0 -0
  82. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/core/jsonproxy.py +0 -0
  83. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/core/lazy_engine.py +0 -0
  84. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/core/result.py +0 -0
  85. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/core/row.py +0 -0
  86. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/core/sequence.py +0 -0
  87. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/core/transaction.py +0 -0
  88. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/core/view.py +0 -0
  89. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/exceptions.py +0 -0
  90. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/maintenance.py +0 -0
  91. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/migrations.py +0 -0
  92. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/servers/__init__.py +0 -0
  93. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/servers/base/__init__.py +0 -0
  94. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/servers/base/initializer.py +0 -0
  95. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/servers/base/operators.py +0 -0
  96. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/servers/base/sql.py +0 -0
  97. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/servers/base/types.py +0 -0
  98. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/servers/mysql/__init__.py +0 -0
  99. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/servers/mysql/operators.py +0 -0
  100. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/servers/mysql/reserved.py +0 -0
  101. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/servers/mysql/sql.py +0 -0
  102. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/servers/mysql/types.py +0 -0
  103. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/servers/postgres/__init__.py +0 -0
  104. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/servers/postgres/operators.py +0 -0
  105. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/servers/postgres/reserved.py +0 -0
  106. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/servers/postgres/types.py +0 -0
  107. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/servers/sqlite/__init__.py +0 -0
  108. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/servers/sqlite/operators.py +0 -0
  109. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/servers/sqlite/reserved.py +0 -0
  110. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/servers/sqlite/sql.py +0 -0
  111. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/servers/sqlite/types.py +0 -0
  112. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
  113. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/servers/sqlserver/operators.py +0 -0
  114. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
  115. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/servers/sqlserver/sql.py +0 -0
  116. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/servers/sqlserver/types.py +0 -0
  117. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/servers/tablehelper.py +0 -0
  118. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/tests/__init__.py +0 -0
  119. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/tests/common_db_test.py +0 -0
  120. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/tests/postgres/__init__.py +0 -0
  121. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/tests/postgres/common.py +0 -0
  122. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/tests/postgres/conftest.py +0 -0
  123. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/tests/postgres/test_column.py +0 -0
  124. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/tests/postgres/test_connections.py +0 -0
  125. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/tests/postgres/test_database.py +0 -0
  126. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/tests/postgres/test_engine.py +0 -0
  127. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
  128. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/tests/postgres/test_imports.py +0 -0
  129. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/tests/postgres/test_result.py +0 -0
  130. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/tests/postgres/test_row.py +0 -0
  131. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
  132. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
  133. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
  134. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
  135. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
  136. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/tests/postgres/test_table.py +0 -0
  137. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
  138. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
  139. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/tests/sql/__init__.py +0 -0
  140. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/tests/sql/common.py +0 -0
  141. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
  142. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
  143. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
  144. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/tests/test_db_utils.py +0 -0
  145. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/tests/test_postgres.py +0 -0
  146. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
  147. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
  148. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/tests/test_result_caching.py +0 -0
  149. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
  150. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
  151. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
  152. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
  153. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/tests/test_sql_builder.py +0 -0
  154. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/tests/test_tablehelper.py +0 -0
  155. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/tests/test_view_helper.py +0 -0
  156. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/db/utils.py +0 -0
  157. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/logging.py +0 -0
  158. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/misc/__init__.py +0 -0
  159. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/misc/conv/__init__.py +0 -0
  160. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/misc/conv/iconv.py +0 -0
  161. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/misc/conv/oconv.py +0 -0
  162. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/misc/db.py +0 -0
  163. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/misc/export.py +0 -0
  164. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/misc/format.py +0 -0
  165. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/misc/mail.py +0 -0
  166. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/misc/merge.py +0 -0
  167. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/misc/pdf.py +0 -0
  168. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/misc/tests/__init__.py +0 -0
  169. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/misc/tests/test_db.py +0 -0
  170. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/misc/tests/test_fix.py +0 -0
  171. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/misc/tests/test_format.py +0 -0
  172. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/misc/tests/test_iconv.py +0 -0
  173. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/misc/tests/test_mail_send.py +0 -0
  174. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/misc/tests/test_merge.py +0 -0
  175. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/misc/tests/test_oconv.py +0 -0
  176. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/misc/tests/test_original_error.py +0 -0
  177. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/misc/tests/test_timer.py +0 -0
  178. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/misc/timer.py +0 -0
  179. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/misc/tools.py +0 -0
  180. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/nosql/__init__.py +0 -0
  181. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/nosql/core/__init__.py +0 -0
  182. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/nosql/core/capabilities.py +0 -0
  183. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/nosql/core/store.py +0 -0
  184. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/nosql/core/where.py +0 -0
  185. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/nosql/exceptions.py +0 -0
  186. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/nosql/servers/__init__.py +0 -0
  187. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/nosql/servers/base/__init__.py +0 -0
  188. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/nosql/servers/cassandra/__init__.py +0 -0
  189. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/nosql/servers/cassandra/translation.py +0 -0
  190. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/nosql/servers/couchbase/__init__.py +0 -0
  191. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/nosql/servers/couchbase/translation.py +0 -0
  192. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/nosql/servers/dynamodb/__init__.py +0 -0
  193. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/nosql/servers/dynamodb/translation.py +0 -0
  194. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/nosql/servers/elasticsearch/__init__.py +0 -0
  195. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/nosql/servers/elasticsearch/translation.py +0 -0
  196. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/nosql/servers/memory/__init__.py +0 -0
  197. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/nosql/servers/mongodb/__init__.py +0 -0
  198. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/nosql/servers/mongodb/translation.py +0 -0
  199. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/nosql/servers/redis/__init__.py +0 -0
  200. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/payment/__init__.py +0 -0
  201. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/payment/authorizenet_adapter.py +0 -0
  202. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/payment/authorizenet_mirror.py +0 -0
  203. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/payment/base_adapter.py +0 -0
  204. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/payment/braintree_adapter.py +0 -0
  205. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/payment/braintree_mirror.py +0 -0
  206. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/payment/charge_rules.py +0 -0
  207. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/payment/stripe_adapter.py +0 -0
  208. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity/payment/stripe_mirror.py +0 -0
  209. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity_python.egg-info/dependency_links.txt +0 -0
  210. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity_python.egg-info/entry_points.txt +0 -0
  211. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity_python.egg-info/requires.txt +0 -0
  212. {velocity_python-0.1.123 → velocity_python-0.1.125}/src/velocity_python.egg-info/top_level.txt +0 -0
  213. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_access_error_403.py +0 -0
  214. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_activity_tracking_aborted_tx.py +0 -0
  215. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_amplify_build.py +0 -0
  216. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_asset_indexing.py +0 -0
  217. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_asset_references.py +0 -0
  218. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_assets_service.py +0 -0
  219. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_async_support.py +0 -0
  220. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_batch_operations.py +0 -0
  221. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_blob_store.py +0 -0
  222. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_capabilities.py +0 -0
  223. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_capability_catalog.py +0 -0
  224. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_cognito_user_cache.py +0 -0
  225. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_column_tx_arg.py +0 -0
  226. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_concurrency_safety.py +0 -0
  227. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_connection_pool.py +0 -0
  228. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_connection_resilience.py +0 -0
  229. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_context_flags_loader.py +0 -0
  230. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_context_job_descriptions.py +0 -0
  231. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_data_service_query_cap.py +0 -0
  232. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_data_service_renumber.py +0 -0
  233. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_data_service_save_as.py +0 -0
  234. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_db_credentials_ssm_cascade.py +0 -0
  235. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_db_maintenance.py +0 -0
  236. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_decorators.py +0 -0
  237. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_delete_hook_fk_blocked.py +0 -0
  238. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_dirty_pipeline_fast_path.py +0 -0
  239. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_email_processing.py +0 -0
  240. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_enqueue_metric_namespace.py +0 -0
  241. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_enqueue_send_failures.py +0 -0
  242. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_feature_flags.py +0 -0
  243. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_get_cognito_user_provider.py +0 -0
  244. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_http_handler_rollback.py +0 -0
  245. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_iconv_money_to_cents.py +0 -0
  246. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_identifier_injection_guard.py +0 -0
  247. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_job_handler.py +0 -0
  248. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_job_lifecycle.py +0 -0
  249. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_job_results.py +0 -0
  250. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_json_columns.py +0 -0
  251. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_jsonb_dict_adapter.py +0 -0
  252. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_lambda_handler.py +0 -0
  253. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_lambda_handler_auth.py +0 -0
  254. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_lambda_handler_masquerade.py +0 -0
  255. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_lambda_handler_user_cache.py +0 -0
  256. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_lazy_engine.py +0 -0
  257. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_masquerade_grant.py +0 -0
  258. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_mirror.py +0 -0
  259. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_mixins_import.py +0 -0
  260. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_n_plus_one.py +0 -0
  261. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_nosql_dynamodb.py +0 -0
  262. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_nosql_memory.py +0 -0
  263. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_nosql_mongodb.py +0 -0
  264. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_nosql_redis.py +0 -0
  265. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_nosql_translation.py +0 -0
  266. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_nosql_where.py +0 -0
  267. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_observability.py +0 -0
  268. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_on_commit.py +0 -0
  269. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_ops.py +0 -0
  270. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_payment_authorizenet_adapter.py +0 -0
  271. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_payment_braintree_adapter.py +0 -0
  272. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_payment_braintree_mirror.py +0 -0
  273. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_payment_mirror_empty_table.py +0 -0
  274. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_payment_profile_sorting.py +0 -0
  275. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_payment_stripe_adapter.py +0 -0
  276. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_pdf.py +0 -0
  277. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_prepared_statements.py +0 -0
  278. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_psycopg3_upgrade.py +0 -0
  279. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_query_cache.py +0 -0
  280. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_query_missing_table_unlocked.py +0 -0
  281. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_read_cache.py +0 -0
  282. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_response.py +0 -0
  283. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_restricted_direct_tables.py +0 -0
  284. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_result_returning.py +0 -0
  285. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_retry_side_effect_guard.py +0 -0
  286. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_return_default_safety.py +0 -0
  287. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_row_batch_update.py +0 -0
  288. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_row_cache_staleness.py +0 -0
  289. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_s3_bucket_config.py +0 -0
  290. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_schema_migrations.py +0 -0
  291. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_security_capability.py +0 -0
  292. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_security_hardening.py +0 -0
  293. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_server_cursor.py +0 -0
  294. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_session_control.py +0 -0
  295. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_single_autocommit_safety.py +0 -0
  296. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_single_session_capability.py +0 -0
  297. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_spreadsheet_functions.py +0 -0
  298. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_sqlite_backend.py +0 -0
  299. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_sqs_per_record_transactions.py +0 -0
  300. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_ssm_config.py +0 -0
  301. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_store_user_data.py +0 -0
  302. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_sys_modified_count_postgres_demo.py +0 -0
  303. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_table_alter.py +0 -0
  304. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_table_find_cache.py +0 -0
  305. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_table_purge.py +0 -0
  306. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_transaction_class_wrapping.py +0 -0
  307. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_transaction_commit_and_ownership.py +0 -0
  308. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_transaction_edge_cases.py +0 -0
  309. {velocity_python-0.1.123 → velocity_python-0.1.125}/tests/test_where_clause_validation.py +0 -0
  310. {velocity_python-0.1.123 → velocity_python-0.1.125}/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.123
3
+ Version: 0.1.125
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.123"
7
+ version = "0.1.125"
8
8
  authors = [
9
9
  { name="Velocity Team", email="info@codeclubs.org" },
10
10
  ]
@@ -1,4 +1,4 @@
1
- __version__ = version = "0.1.123"
1
+ __version__ = version = "0.1.125"
2
2
 
3
3
  import importlib as _importlib
4
4
 
@@ -34,6 +34,9 @@ Facets AND together; the values of one facet OR together. Kinds:
34
34
  ``since`` a period ending now: ``today``, ``Nd`` (``7d``, ``30d`` …, up to
35
35
  3660 days), ``year`` (this calendar year). Evaluated in the
36
36
  database's session time zone.
37
+ ``before`` strictly before a day (``YYYY-MM-DD``) or older than a period
38
+ (``Nd``): ``{"field": "sys_created", "kind": "before", "value":
39
+ "2026-09-01"}``. With ``since`` it brackets a range.
37
40
  ``text`` a write-in, case-insensitive, against ``field::text``:
38
41
  ``match`` is ``contains`` (default), ``equals``, ``starts`` or
39
42
  ``words`` — every whitespace-separated word of the term must
@@ -72,7 +75,7 @@ from velocity.db.servers.tablehelper import TableHelper
72
75
 
73
76
  PARAM_KEY = "facets"
74
77
 
75
- KINDS = ("values", "contains", "presence", "since", "text", "domain")
78
+ KINDS = ("values", "contains", "presence", "since", "before", "text", "domain")
76
79
  MATCHES = ("contains", "equals", "starts", "words")
77
80
  PRESENCE = ("present", "absent")
78
81
 
@@ -199,6 +202,27 @@ def _kind_since(column: str, entry: Dict[str, Any], field: str) -> Optional[Pred
199
202
  return f"{column} >= now() - make_interval(days => %s)", (days,)
200
203
 
201
204
 
205
+ _ISO_DATE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
206
+
207
+
208
+ def _kind_before(column: str, entry: Dict[str, Any], field: str) -> Optional[Predicate]:
209
+ """Rows strictly before a day (``YYYY-MM-DD``: anything earlier than that
210
+ day's midnight) or older than a period (``Nd``: before N days ago). The
211
+ complement of ``since`` — together they bracket a date range."""
212
+ value = _one(entry, field).lower()
213
+ if not value:
214
+ return None
215
+ if _ISO_DATE.match(value):
216
+ return f"{column} < %s::date", (value,)
217
+ match = _DAYS.match(value)
218
+ if not match:
219
+ raise FacetError(f"facet {field}: before must be YYYY-MM-DD or <N>d")
220
+ days = int(match.group(1))
221
+ if not 1 <= days <= MAX_DAYS:
222
+ raise FacetError(f"facet {field}: period must be 1 to {MAX_DAYS} days")
223
+ return f"{column} < now() - make_interval(days => %s)", (days,)
224
+
225
+
202
226
  def _columns(entry: Dict[str, Any], column: str, field: str) -> List[str]:
203
227
  """The column(s) a text facet matches: ``fields`` when given, else the field."""
204
228
  fields = entry.get("fields")
@@ -282,6 +306,7 @@ _BUILDERS = {
282
306
  "contains": _kind_contains,
283
307
  "presence": _kind_presence,
284
308
  "since": _kind_since,
309
+ "before": _kind_before,
285
310
  "text": _kind_text,
286
311
  "domain": _kind_domain,
287
312
  }
@@ -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
@@ -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.123
3
+ Version: 0.1.125
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
@@ -47,6 +47,34 @@ def test_presence_treats_null_and_empty_alike_for_any_column_type():
47
47
  facets.predicate({"field": "x", "kind": "presence", "value": "maybe"})
48
48
 
49
49
 
50
+ def test_before_day_or_period():
51
+ assert facets.predicate(
52
+ {"field": "sys_created", "kind": "before", "value": "2026-09-01"}
53
+ ) == ("sys_created < %s::date", ("2026-09-01",))
54
+ assert facets.predicate(
55
+ {"field": "sys_created", "kind": "before", "value": "30d"}
56
+ ) == ("sys_created < now() - make_interval(days => %s)", (30,))
57
+ assert (
58
+ facets.predicate({"field": "sys_created", "kind": "before", "value": ""})
59
+ is None
60
+ )
61
+ for bad in ("2026-9-1", "yesterday", "0d", "9999d", "2026-09-01; drop"):
62
+ with pytest.raises(facets.FacetError):
63
+ facets.predicate({"field": "sys_created", "kind": "before", "value": bad})
64
+
65
+
66
+ def test_since_and_before_bracket_a_range():
67
+ sql, params = facets.build_where(
68
+ [
69
+ {"field": "sys_created", "kind": "since", "value": "60d"},
70
+ {"field": "sys_created", "kind": "before", "value": "2026-09-01"},
71
+ ]
72
+ )
73
+ assert "sys_created >= now() - make_interval(days => %s)" in sql
74
+ assert "sys_created < %s::date" in sql
75
+ assert params == (60, "2026-09-01")
76
+
77
+
50
78
  def test_since_periods():
51
79
  assert facets.predicate(
52
80
  {"field": "internal_date", "kind": "since", "value": "today"}
@@ -173,7 +201,10 @@ def test_domain_refuses_an_address_or_whitespace_and_ignores_blanks():
173
201
  for bad in ("user@example.com", "two words.com", "a\tb.com"):
174
202
  with pytest.raises(facets.FacetError):
175
203
  facets.predicate({"field": "d", "kind": "domain", "values": [bad]})
176
- assert facets.predicate({"field": "d", "kind": "domain", "values": ["", ".", " "]}) is None
204
+ assert (
205
+ facets.predicate({"field": "d", "kind": "domain", "values": ["", ".", " "]})
206
+ is None
207
+ )
177
208
 
178
209
 
179
210
  def test_exclude_negates_null_safely_for_any_kind():
@@ -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()