velocity-python 0.0.205__tar.gz → 0.0.207__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 (149) hide show
  1. {velocity_python-0.0.205 → velocity_python-0.0.207}/PKG-INFO +1 -1
  2. {velocity_python-0.0.205 → velocity_python-0.0.207}/pyproject.toml +1 -1
  3. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/__init__.py +1 -1
  4. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/aws/handlers/mixins/data_service.py +137 -3
  5. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/core/decorators.py +14 -1
  6. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/servers/base/sql.py +3 -23
  7. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/servers/mysql/sql.py +39 -0
  8. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/servers/postgres/sql.py +31 -0
  9. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/servers/sqlite/sql.py +18 -0
  10. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/servers/sqlserver/sql.py +34 -0
  11. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity_python.egg-info/PKG-INFO +1 -1
  12. {velocity_python-0.0.205 → velocity_python-0.0.207}/LICENSE +0 -0
  13. {velocity_python-0.0.205 → velocity_python-0.0.207}/README.md +0 -0
  14. {velocity_python-0.0.205 → velocity_python-0.0.207}/setup.cfg +0 -0
  15. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/app/__init__.py +0 -0
  16. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/app/invoices.py +0 -0
  17. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/app/orders.py +0 -0
  18. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/app/payments.py +0 -0
  19. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/app/purchase_orders.py +0 -0
  20. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/app/tests/__init__.py +0 -0
  21. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/app/tests/test_email_processing.py +0 -0
  22. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/app/tests/test_payment_profile_sorting.py +0 -0
  23. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/app/tests/test_spreadsheet_functions.py +0 -0
  24. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/aws/__init__.py +0 -0
  25. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/aws/amplify.py +0 -0
  26. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/aws/handlers/__init__.py +0 -0
  27. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/aws/handlers/base_handler.py +0 -0
  28. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/aws/handlers/context.py +0 -0
  29. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/aws/handlers/context_factory.py +0 -0
  30. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/aws/handlers/exceptions.py +0 -0
  31. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/aws/handlers/lambda_handler.py +0 -0
  32. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
  33. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
  34. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/aws/handlers/perf.py +0 -0
  35. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/aws/handlers/response.py +0 -0
  36. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/aws/handlers/sqs_handler.py +0 -0
  37. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/aws/tests/__init__.py +0 -0
  38. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
  39. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/aws/tests/test_response.py +0 -0
  40. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/__init__.py +0 -0
  41. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/core/__init__.py +0 -0
  42. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/core/column.py +0 -0
  43. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/core/database.py +0 -0
  44. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/core/engine.py +0 -0
  45. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/core/result.py +0 -0
  46. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/core/row.py +0 -0
  47. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/core/sequence.py +0 -0
  48. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/core/table.py +0 -0
  49. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/core/transaction.py +0 -0
  50. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/core/view.py +0 -0
  51. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/exceptions.py +0 -0
  52. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/servers/__init__.py +0 -0
  53. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/servers/base/__init__.py +0 -0
  54. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/servers/base/initializer.py +0 -0
  55. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/servers/base/operators.py +0 -0
  56. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/servers/base/types.py +0 -0
  57. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/servers/mysql/__init__.py +0 -0
  58. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/servers/mysql/operators.py +0 -0
  59. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/servers/mysql/reserved.py +0 -0
  60. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/servers/mysql/types.py +0 -0
  61. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/servers/postgres/__init__.py +0 -0
  62. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/servers/postgres/operators.py +0 -0
  63. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/servers/postgres/reserved.py +0 -0
  64. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/servers/postgres/types.py +0 -0
  65. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/servers/sqlite/__init__.py +0 -0
  66. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/servers/sqlite/operators.py +0 -0
  67. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/servers/sqlite/reserved.py +0 -0
  68. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/servers/sqlite/types.py +0 -0
  69. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
  70. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/servers/sqlserver/operators.py +0 -0
  71. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
  72. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/servers/sqlserver/types.py +0 -0
  73. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/servers/tablehelper.py +0 -0
  74. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/tests/__init__.py +0 -0
  75. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/tests/common_db_test.py +0 -0
  76. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/tests/postgres/__init__.py +0 -0
  77. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/tests/postgres/common.py +0 -0
  78. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/tests/postgres/test_column.py +0 -0
  79. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/tests/postgres/test_connections.py +0 -0
  80. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/tests/postgres/test_database.py +0 -0
  81. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/tests/postgres/test_engine.py +0 -0
  82. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
  83. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/tests/postgres/test_imports.py +0 -0
  84. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/tests/postgres/test_result.py +0 -0
  85. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/tests/postgres/test_row.py +0 -0
  86. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
  87. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
  88. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
  89. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
  90. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
  91. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/tests/postgres/test_table.py +0 -0
  92. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
  93. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
  94. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/tests/sql/__init__.py +0 -0
  95. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/tests/sql/common.py +0 -0
  96. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
  97. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
  98. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
  99. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/tests/test_db_utils.py +0 -0
  100. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/tests/test_postgres.py +0 -0
  101. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
  102. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
  103. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/tests/test_result_caching.py +0 -0
  104. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
  105. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
  106. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
  107. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
  108. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/tests/test_sql_builder.py +0 -0
  109. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/tests/test_tablehelper.py +0 -0
  110. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/tests/test_view_helper.py +0 -0
  111. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/db/utils.py +0 -0
  112. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/logging.py +0 -0
  113. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/misc/__init__.py +0 -0
  114. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/misc/conv/__init__.py +0 -0
  115. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/misc/conv/iconv.py +0 -0
  116. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/misc/conv/oconv.py +0 -0
  117. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/misc/db.py +0 -0
  118. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/misc/export.py +0 -0
  119. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/misc/format.py +0 -0
  120. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/misc/mail.py +0 -0
  121. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/misc/merge.py +0 -0
  122. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/misc/tests/__init__.py +0 -0
  123. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/misc/tests/test_db.py +0 -0
  124. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/misc/tests/test_fix.py +0 -0
  125. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/misc/tests/test_format.py +0 -0
  126. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/misc/tests/test_iconv.py +0 -0
  127. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/misc/tests/test_merge.py +0 -0
  128. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/misc/tests/test_oconv.py +0 -0
  129. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/misc/tests/test_original_error.py +0 -0
  130. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/misc/tests/test_timer.py +0 -0
  131. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/misc/timer.py +0 -0
  132. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/misc/tools.py +0 -0
  133. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/payment/__init__.py +0 -0
  134. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/payment/base_adapter.py +0 -0
  135. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/payment/braintree_adapter.py +0 -0
  136. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/payment/router.py +0 -0
  137. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity/payment/stripe_adapter.py +0 -0
  138. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity_python.egg-info/SOURCES.txt +0 -0
  139. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity_python.egg-info/dependency_links.txt +0 -0
  140. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity_python.egg-info/requires.txt +0 -0
  141. {velocity_python-0.0.205 → velocity_python-0.0.207}/src/velocity_python.egg-info/top_level.txt +0 -0
  142. {velocity_python-0.0.205 → velocity_python-0.0.207}/tests/test_decorators.py +0 -0
  143. {velocity_python-0.0.205 → velocity_python-0.0.207}/tests/test_iconv_money_to_cents.py +0 -0
  144. {velocity_python-0.0.205 → velocity_python-0.0.207}/tests/test_lambda_handler.py +0 -0
  145. {velocity_python-0.0.205 → velocity_python-0.0.207}/tests/test_lambda_handler_auth.py +0 -0
  146. {velocity_python-0.0.205 → velocity_python-0.0.207}/tests/test_mixins_import.py +0 -0
  147. {velocity_python-0.0.205 → velocity_python-0.0.207}/tests/test_sys_modified_count_postgres_demo.py +0 -0
  148. {velocity_python-0.0.205 → velocity_python-0.0.207}/tests/test_table_alter.py +0 -0
  149. {velocity_python-0.0.205 → velocity_python-0.0.207}/tests/test_where_clause_validation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: velocity-python
3
- Version: 0.0.205
3
+ Version: 0.0.207
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.0.205"
7
+ version = "0.0.207"
8
8
  authors = [
9
9
  { name="Velocity Team", email="info@codeclubs.org" },
10
10
  ]
@@ -1,4 +1,4 @@
1
- __version__ = version = "0.0.205"
1
+ __version__ = version = "0.0.207"
2
2
 
3
3
  from . import aws
4
4
  from . import db
@@ -9,6 +9,7 @@ import base64
9
9
  import datetime
10
10
  import importlib
11
11
  import logging
12
+ import re
12
13
  from io import BytesIO
13
14
 
14
15
  from velocity.misc import export
@@ -152,8 +153,116 @@ class DataServiceMixin:
152
153
  self._call_rwx_hook("before_query", "common", tx, table, payload, context)
153
154
  self._call_rwx_hook("before_query", table, tx, table, payload, context)
154
155
  params = payload.get("params", {})
155
- result = tx.table(payload["obj"]).select(**params)
156
- if payload.get("result_format") == "excel":
156
+ result_format = payload.get("result_format")
157
+
158
+ # Clear any previous swallowed-error details so we only report errors
159
+ # related to this query.
160
+ if hasattr(tx, "_last_return_default_error"):
161
+ tx._last_return_default_error = None
162
+
163
+ obj = payload["obj"]
164
+
165
+ def _normalize_identifier(identifier: str):
166
+ if identifier is None:
167
+ return None
168
+ identifier = str(identifier).strip()
169
+ if not identifier:
170
+ return None
171
+ if identifier[0] == identifier[-1] and identifier[0] in {'"', "'"}:
172
+ identifier = identifier[1:-1]
173
+ return identifier
174
+
175
+ def _extract_requested_column_names(columns_spec):
176
+ if not columns_spec:
177
+ return []
178
+ if isinstance(columns_spec, str):
179
+ columns_spec = [columns_spec]
180
+ if not isinstance(columns_spec, (list, tuple)):
181
+ return []
182
+
183
+ requested = []
184
+ simple_identifier = re.compile(
185
+ r"^([A-Za-z_][A-Za-z0-9_]*)(\\.([A-Za-z_][A-Za-z0-9_]*))?$"
186
+ )
187
+
188
+ for col in columns_spec:
189
+ if not isinstance(col, str):
190
+ continue
191
+
192
+ raw = col.strip()
193
+ if not raw or raw == "*":
194
+ continue
195
+
196
+ # If this looks like an expression (functions, casts, aliases, etc.),
197
+ # skip strict validation and let the DB error (which we will surface).
198
+ lowered = raw.lower()
199
+ if (
200
+ "(" in raw
201
+ or ")" in raw
202
+ or " over " in lowered
203
+ or "::" in raw
204
+ or " as " in lowered
205
+ or "+" in raw
206
+ or "-" in raw
207
+ or "/" in raw
208
+ or "*" in raw
209
+ or "||" in raw
210
+ ):
211
+ continue
212
+
213
+ # Allow simple identifiers, optionally qualified (table.column).
214
+ match = simple_identifier.match(raw)
215
+ if not match:
216
+ continue
217
+
218
+ colname = match.group(3) or match.group(1)
219
+ colname = _normalize_identifier(colname)
220
+ if colname:
221
+ requested.append(colname)
222
+
223
+ return requested
224
+
225
+ # Optional: detect missing columns before running the query.
226
+ requested_cols = _extract_requested_column_names(params.get("columns"))
227
+ if requested_cols:
228
+ available_cols = {c.lower() for c in tx.table(obj).sys_columns()}
229
+ missing = sorted(
230
+ {c for c in requested_cols if c.lower() not in available_cols},
231
+ key=lambda v: v.lower(),
232
+ )
233
+ if missing:
234
+ message = (
235
+ f"Query failed for '{obj}': requested columns not present in the database: "
236
+ + ", ".join(missing)
237
+ )
238
+ context.response().toast(message, "error")
239
+ error_payload = {
240
+ "type": "missing-columns",
241
+ "message": message,
242
+ "missing": missing,
243
+ }
244
+ if result_format == "excel":
245
+ return {
246
+ "headers": payload.get("headers", []),
247
+ "rows": [],
248
+ "error": error_payload,
249
+ }
250
+ return {
251
+ "rows": [],
252
+ "config": {
253
+ "lastFetch": datetime.datetime.now(),
254
+ "query": None,
255
+ "format": result_format,
256
+ "error": error_payload,
257
+ },
258
+ }
259
+
260
+ result = tx.table(obj).select(**params)
261
+
262
+ swallowed_error = getattr(tx, "_last_return_default_error", None)
263
+ if swallowed_error:
264
+ tx._last_return_default_error = None
265
+ if result_format == "excel":
157
266
  data = {
158
267
  "headers": payload.get(
159
268
  "headers", [x.replace("_", " ").title() for x in result.headers]
@@ -166,9 +275,34 @@ class DataServiceMixin:
166
275
  "config": {
167
276
  "lastFetch": datetime.datetime.now(),
168
277
  "query": result.sql,
169
- "format": payload.get("result_format"),
278
+ "format": result_format,
170
279
  },
171
280
  }
281
+
282
+ # If the DB call failed but was swallowed (return_default), surface a reason.
283
+ # Common symptoms are empty rows + null SQL.
284
+ if swallowed_error and result_format == "excel":
285
+ error_message = swallowed_error.get("message") or "Unknown database error"
286
+ context.response().toast(
287
+ f"Query failed for '{obj}': {error_message.splitlines()[0]}",
288
+ "error",
289
+ )
290
+ data["error"] = {"type": "db-error", **swallowed_error}
291
+ elif swallowed_error and isinstance(data, dict) and data.get("config") is not None:
292
+ error_message = swallowed_error.get("message") or "Unknown database error"
293
+ context.response().toast(
294
+ f"Query failed for '{obj}': {error_message.splitlines()[0]}",
295
+ "error",
296
+ )
297
+ data["config"].update(
298
+ {
299
+ "query": data["config"].get("query") or None,
300
+ "error": {
301
+ "type": "db-error",
302
+ **swallowed_error,
303
+ },
304
+ }
305
+ )
172
306
  if payload.get("count"):
173
307
  data["count"] = tx.table(payload["obj"]).count(
174
308
  where=params.get("where", None)
@@ -105,8 +105,21 @@ def return_default(
105
105
  result = func(self, *args, **kwds)
106
106
  if result is None:
107
107
  result = default
108
- except func.exceptions:
108
+ except func.exceptions as e:
109
109
  self.tx.rollback_savepoint(sp, cursor=self.cursor())
110
+
111
+ # Capture swallowed exceptions for upstream diagnostics.
112
+ # This decorator intentionally returns a default value instead of
113
+ # raising, but consumers (e.g. API handlers) may still want to
114
+ # surface a reason to the caller.
115
+ try:
116
+ self.tx._last_return_default_error = {
117
+ "type": e.__class__.__name__,
118
+ "message": str(e),
119
+ "function": f"{func.__module__}.{getattr(func, '__qualname__', func.__name__)}",
120
+ }
121
+ except Exception:
122
+ pass
110
123
  return default
111
124
  self.tx.release_savepoint(sp, cursor=self.cursor())
112
125
  return result
@@ -59,35 +59,15 @@ class BaseSQLDialect(ABC):
59
59
  Dialects should override/extend this if they can do better than message matching.
60
60
  Engine uses this only as a fallback when no/unknown error code is available.
61
61
  """
62
- if not msg:
63
- return False
64
- m = str(msg).strip().lower()
65
- needles = (
66
- "server closed the connection unexpectedly",
67
- "no connection to the server",
68
- "connection timed out",
69
- "could not connect to server",
70
- "cannot connect to server",
71
- "connection already closed",
72
- "cursor already closed",
73
- "ssl syscall error",
74
- "eof detected",
75
- "connection reset by peer",
76
- "broken pipe",
77
- "terminating connection due to administrator command",
78
- "could not receive data from server",
79
- "could not send data to server",
80
- )
81
- return any(n in m for n in needles)
62
+ return False
82
63
 
83
64
  @classmethod
84
65
  def is_transient_connection_error_message(cls, msg: str) -> bool:
85
66
  """Return True if a connection error looks transient/retryable.
86
67
 
87
- Default implementation treats most low-level disconnects as transient.
88
- Dialects may override to be stricter.
68
+ Default implementation is disabled; dialects must implement this.
89
69
  """
90
- return cls.is_connection_error_message(msg)
70
+ return False
91
71
 
92
72
  # Core CRUD Operations
93
73
  @classmethod
@@ -72,6 +72,45 @@ class SQL(BaseSQLDialect):
72
72
  error_msg = getattr(e, "msg", None)
73
73
  return error_code, error_msg
74
74
 
75
+ @classmethod
76
+ def is_connection_error_message(cls, msg: str) -> bool:
77
+ if not msg:
78
+ return False
79
+ m = str(msg).strip().lower()
80
+
81
+ # Common MySQL connector / server disconnects.
82
+ needles = (
83
+ "mysql server has gone away",
84
+ "lost connection to mysql server",
85
+ "can't connect to mysql server",
86
+ "connection refused",
87
+ "connection reset by peer",
88
+ "broken pipe",
89
+ "connection timed out",
90
+ "read timed out",
91
+ "write timed out",
92
+ "server shutdown",
93
+ "too many connections",
94
+ "is dead or not enabled",
95
+ )
96
+ return any(n in m for n in needles)
97
+
98
+ @classmethod
99
+ def is_transient_connection_error_message(cls, msg: str) -> bool:
100
+ if not cls.is_connection_error_message(msg):
101
+ return False
102
+
103
+ # Do not treat auth/config problems as transient.
104
+ m = str(msg).strip().lower()
105
+ non_transient = (
106
+ "access denied",
107
+ "authentication",
108
+ "unknown database",
109
+ "unknown mysql server host",
110
+ "bad handshake",
111
+ )
112
+ return not any(n in m for n in non_transient)
113
+
75
114
  @classmethod
76
115
  def select(
77
116
  cls,
@@ -77,6 +77,37 @@ class SQL(BaseSQLDialect):
77
77
  error_mesg = getattr(e, "pgerror", None)
78
78
  return error_code, error_mesg
79
79
 
80
+ @classmethod
81
+ def is_connection_error_message(cls, msg: str) -> bool:
82
+ if not msg:
83
+ return False
84
+ m = str(msg).strip().lower()
85
+ needles = (
86
+ "server closed the connection unexpectedly",
87
+ "no connection to the server",
88
+ "connection timed out",
89
+ "could not connect to server",
90
+ "cannot connect to server",
91
+ "could not translate host name",
92
+ "connection already closed",
93
+ "cursor already closed",
94
+ "ssl syscall error",
95
+ "eof detected",
96
+ "connection reset by peer",
97
+ "broken pipe",
98
+ "terminating connection due to administrator command",
99
+ "could not receive data from server",
100
+ "could not send data to server",
101
+ "the database system is starting up",
102
+ "the database system is shutting down",
103
+ )
104
+ return any(n in m for n in needles)
105
+
106
+ @classmethod
107
+ def is_transient_connection_error_message(cls, msg: str) -> bool:
108
+ # For Postgres, low-level disconnects/restarts are typically transient.
109
+ return cls.is_connection_error_message(msg)
110
+
80
111
  @staticmethod
81
112
  def _validate_where_string(where):
82
113
  """
@@ -72,6 +72,24 @@ class SQL(BaseSQLDialect):
72
72
  # SQLite exceptions don't have error codes like other databases
73
73
  return None, str(e)
74
74
 
75
+ @classmethod
76
+ def is_connection_error_message(cls, msg: str) -> bool:
77
+ if not msg:
78
+ return False
79
+ m = str(msg).strip().lower()
80
+ needles = (
81
+ "unable to open database file",
82
+ "disk i/o error",
83
+ "readonly database",
84
+ "file is not a database",
85
+ )
86
+ return any(n in m for n in needles)
87
+
88
+ @classmethod
89
+ def is_transient_connection_error_message(cls, msg: str) -> bool:
90
+ # SQLite connection/file errors are typically not transient in-process.
91
+ return False
92
+
75
93
  @classmethod
76
94
  def select(
77
95
  cls,
@@ -74,6 +74,40 @@ class SQL(BaseSQLDialect):
74
74
  error_message = getattr(e, "message", None) or str(e)
75
75
  return error_number, error_message
76
76
 
77
+ @classmethod
78
+ def is_connection_error_message(cls, msg: str) -> bool:
79
+ if not msg:
80
+ return False
81
+ m = str(msg).strip().lower()
82
+
83
+ # pyodbc/pymssql/pytds typically surface these in message text.
84
+ needles = (
85
+ "communication link failure",
86
+ "transport-level error",
87
+ "tcp provider",
88
+ "a connection was successfully established with the server, but then an error occurred during the login process",
89
+ "connection is broken",
90
+ "connection was forcibly closed",
91
+ "connection reset",
92
+ "login timeout expired",
93
+ "server is not found or not accessible",
94
+ "could not open a connection",
95
+ "network-related",
96
+ "connection timed out",
97
+ "broken pipe",
98
+ )
99
+ return any(n in m for n in needles)
100
+
101
+ @classmethod
102
+ def is_transient_connection_error_message(cls, msg: str) -> bool:
103
+ if not cls.is_connection_error_message(msg):
104
+ return False
105
+ m = str(msg).strip().lower()
106
+ # "login failed" is usually credentials/permissions, not transient.
107
+ if "login failed" in m:
108
+ return False
109
+ return True
110
+
77
111
  @classmethod
78
112
  def select(
79
113
  cls,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: velocity-python
3
- Version: 0.0.205
3
+ Version: 0.0.207
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