velocity-python 0.0.206__tar.gz → 0.0.208__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.206 → velocity_python-0.0.208}/PKG-INFO +1 -1
  2. {velocity_python-0.0.206 → velocity_python-0.0.208}/pyproject.toml +1 -1
  3. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/__init__.py +1 -1
  4. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/aws/handlers/base_handler.py +25 -0
  5. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/aws/handlers/mixins/data_service.py +137 -3
  6. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/core/decorators.py +14 -1
  7. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity_python.egg-info/PKG-INFO +1 -1
  8. {velocity_python-0.0.206 → velocity_python-0.0.208}/LICENSE +0 -0
  9. {velocity_python-0.0.206 → velocity_python-0.0.208}/README.md +0 -0
  10. {velocity_python-0.0.206 → velocity_python-0.0.208}/setup.cfg +0 -0
  11. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/app/__init__.py +0 -0
  12. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/app/invoices.py +0 -0
  13. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/app/orders.py +0 -0
  14. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/app/payments.py +0 -0
  15. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/app/purchase_orders.py +0 -0
  16. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/app/tests/__init__.py +0 -0
  17. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/app/tests/test_email_processing.py +0 -0
  18. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/app/tests/test_payment_profile_sorting.py +0 -0
  19. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/app/tests/test_spreadsheet_functions.py +0 -0
  20. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/aws/__init__.py +0 -0
  21. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/aws/amplify.py +0 -0
  22. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/aws/handlers/__init__.py +0 -0
  23. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/aws/handlers/context.py +0 -0
  24. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/aws/handlers/context_factory.py +0 -0
  25. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/aws/handlers/exceptions.py +0 -0
  26. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/aws/handlers/lambda_handler.py +0 -0
  27. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
  28. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
  29. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/aws/handlers/perf.py +0 -0
  30. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/aws/handlers/response.py +0 -0
  31. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/aws/handlers/sqs_handler.py +0 -0
  32. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/aws/tests/__init__.py +0 -0
  33. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
  34. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/aws/tests/test_response.py +0 -0
  35. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/__init__.py +0 -0
  36. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/core/__init__.py +0 -0
  37. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/core/column.py +0 -0
  38. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/core/database.py +0 -0
  39. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/core/engine.py +0 -0
  40. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/core/result.py +0 -0
  41. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/core/row.py +0 -0
  42. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/core/sequence.py +0 -0
  43. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/core/table.py +0 -0
  44. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/core/transaction.py +0 -0
  45. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/core/view.py +0 -0
  46. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/exceptions.py +0 -0
  47. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/servers/__init__.py +0 -0
  48. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/servers/base/__init__.py +0 -0
  49. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/servers/base/initializer.py +0 -0
  50. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/servers/base/operators.py +0 -0
  51. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/servers/base/sql.py +0 -0
  52. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/servers/base/types.py +0 -0
  53. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/servers/mysql/__init__.py +0 -0
  54. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/servers/mysql/operators.py +0 -0
  55. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/servers/mysql/reserved.py +0 -0
  56. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/servers/mysql/sql.py +0 -0
  57. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/servers/mysql/types.py +0 -0
  58. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/servers/postgres/__init__.py +0 -0
  59. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/servers/postgres/operators.py +0 -0
  60. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/servers/postgres/reserved.py +0 -0
  61. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/servers/postgres/sql.py +0 -0
  62. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/servers/postgres/types.py +0 -0
  63. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/servers/sqlite/__init__.py +0 -0
  64. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/servers/sqlite/operators.py +0 -0
  65. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/servers/sqlite/reserved.py +0 -0
  66. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/servers/sqlite/sql.py +0 -0
  67. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/servers/sqlite/types.py +0 -0
  68. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
  69. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/servers/sqlserver/operators.py +0 -0
  70. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
  71. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/servers/sqlserver/sql.py +0 -0
  72. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/servers/sqlserver/types.py +0 -0
  73. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/servers/tablehelper.py +0 -0
  74. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/tests/__init__.py +0 -0
  75. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/tests/common_db_test.py +0 -0
  76. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/tests/postgres/__init__.py +0 -0
  77. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/tests/postgres/common.py +0 -0
  78. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/tests/postgres/test_column.py +0 -0
  79. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/tests/postgres/test_connections.py +0 -0
  80. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/tests/postgres/test_database.py +0 -0
  81. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/tests/postgres/test_engine.py +0 -0
  82. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
  83. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/tests/postgres/test_imports.py +0 -0
  84. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/tests/postgres/test_result.py +0 -0
  85. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/tests/postgres/test_row.py +0 -0
  86. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
  87. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
  88. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
  89. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
  90. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
  91. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/tests/postgres/test_table.py +0 -0
  92. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
  93. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
  94. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/tests/sql/__init__.py +0 -0
  95. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/tests/sql/common.py +0 -0
  96. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
  97. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
  98. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
  99. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/tests/test_db_utils.py +0 -0
  100. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/tests/test_postgres.py +0 -0
  101. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
  102. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
  103. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/tests/test_result_caching.py +0 -0
  104. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
  105. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
  106. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
  107. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
  108. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/tests/test_sql_builder.py +0 -0
  109. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/tests/test_tablehelper.py +0 -0
  110. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/tests/test_view_helper.py +0 -0
  111. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/db/utils.py +0 -0
  112. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/logging.py +0 -0
  113. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/misc/__init__.py +0 -0
  114. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/misc/conv/__init__.py +0 -0
  115. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/misc/conv/iconv.py +0 -0
  116. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/misc/conv/oconv.py +0 -0
  117. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/misc/db.py +0 -0
  118. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/misc/export.py +0 -0
  119. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/misc/format.py +0 -0
  120. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/misc/mail.py +0 -0
  121. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/misc/merge.py +0 -0
  122. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/misc/tests/__init__.py +0 -0
  123. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/misc/tests/test_db.py +0 -0
  124. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/misc/tests/test_fix.py +0 -0
  125. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/misc/tests/test_format.py +0 -0
  126. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/misc/tests/test_iconv.py +0 -0
  127. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/misc/tests/test_merge.py +0 -0
  128. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/misc/tests/test_oconv.py +0 -0
  129. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/misc/tests/test_original_error.py +0 -0
  130. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/misc/tests/test_timer.py +0 -0
  131. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/misc/timer.py +0 -0
  132. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/misc/tools.py +0 -0
  133. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/payment/__init__.py +0 -0
  134. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/payment/base_adapter.py +0 -0
  135. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/payment/braintree_adapter.py +0 -0
  136. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/payment/router.py +0 -0
  137. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity/payment/stripe_adapter.py +0 -0
  138. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity_python.egg-info/SOURCES.txt +0 -0
  139. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity_python.egg-info/dependency_links.txt +0 -0
  140. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity_python.egg-info/requires.txt +0 -0
  141. {velocity_python-0.0.206 → velocity_python-0.0.208}/src/velocity_python.egg-info/top_level.txt +0 -0
  142. {velocity_python-0.0.206 → velocity_python-0.0.208}/tests/test_decorators.py +0 -0
  143. {velocity_python-0.0.206 → velocity_python-0.0.208}/tests/test_iconv_money_to_cents.py +0 -0
  144. {velocity_python-0.0.206 → velocity_python-0.0.208}/tests/test_lambda_handler.py +0 -0
  145. {velocity_python-0.0.206 → velocity_python-0.0.208}/tests/test_lambda_handler_auth.py +0 -0
  146. {velocity_python-0.0.206 → velocity_python-0.0.208}/tests/test_mixins_import.py +0 -0
  147. {velocity_python-0.0.206 → velocity_python-0.0.208}/tests/test_sys_modified_count_postgres_demo.py +0 -0
  148. {velocity_python-0.0.206 → velocity_python-0.0.208}/tests/test_table_alter.py +0 -0
  149. {velocity_python-0.0.206 → velocity_python-0.0.208}/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.206
3
+ Version: 0.0.208
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.206"
7
+ version = "0.0.208"
8
8
  authors = [
9
9
  { name="Velocity Team", email="info@codeclubs.org" },
10
10
  ]
@@ -1,4 +1,4 @@
1
- __version__ = version = "0.0.206"
1
+ __version__ = version = "0.0.208"
2
2
 
3
3
  from . import aws
4
4
  from . import db
@@ -13,6 +13,7 @@ from typing import Any, Dict, List, Optional
13
13
 
14
14
  from velocity.aws.handlers import context as VelocityContext
15
15
  from velocity.aws.handlers.context_factory import ContextFactory
16
+ from velocity.aws.handlers.exceptions import AlertError
16
17
 
17
18
  logger = logging.getLogger(__name__)
18
19
 
@@ -221,6 +222,30 @@ class BaseHandler:
221
222
  local_context: The context object
222
223
  exception: The exception that occurred
223
224
  """
225
+ if isinstance(exception, AlertError):
226
+ payload = {}
227
+ try:
228
+ payload = exception.get_payload() or {}
229
+ except Exception:
230
+ payload = {"message": str(exception)}
231
+
232
+ title = payload.get("title") or "Notification"
233
+ message = payload.get("message") or str(exception)
234
+
235
+ logger.warning(
236
+ "AlertError during action execution",
237
+ exc_info=True,
238
+ extra={
239
+ "handler": self.__class__.__name__,
240
+ "action": getattr(local_context, "action", lambda: None)(),
241
+ "tx_present": tx is not None,
242
+ },
243
+ )
244
+
245
+ # Historical behavior: raising AlertError should surface a UI alert.
246
+ local_context.response().alert(message=message, title=title)
247
+ return
248
+
224
249
  # Always log the original exception so it isn't lost if the error-path
225
250
  # (e.g. job-status updates) fails due to a transient DB disconnect.
226
251
  logger.exception(
@@ -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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: velocity-python
3
- Version: 0.0.206
3
+ Version: 0.0.208
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