velocity-python 0.0.115__py3-none-any.whl → 0.0.117__py3-none-any.whl
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.
Potentially problematic release.
This version of velocity-python might be problematic. Click here for more details.
- velocity/__init__.py +1 -1
- velocity/aws/amplify.py +8 -3
- velocity/aws/handlers/base_handler.py +245 -0
- velocity/aws/handlers/context.py +66 -44
- velocity/aws/handlers/exceptions.py +16 -0
- velocity/aws/handlers/lambda_handler.py +12 -77
- velocity/aws/handlers/sqs_handler.py +22 -138
- velocity/db/__init__.py +1 -1
- velocity/db/core/engine.py +30 -26
- velocity/db/core/exceptions.py +3 -0
- velocity/db/core/result.py +30 -22
- velocity/db/core/row.py +3 -1
- velocity/db/core/table.py +2 -1
- velocity/db/exceptions.py +35 -18
- velocity/db/servers/postgres/__init__.py +10 -12
- velocity/db/servers/postgres/sql.py +32 -13
- velocity/db/servers/tablehelper.py +117 -91
- velocity/db/utils.py +61 -46
- {velocity_python-0.0.115.dist-info → velocity_python-0.0.117.dist-info}/METADATA +1 -1
- {velocity_python-0.0.115.dist-info → velocity_python-0.0.117.dist-info}/RECORD +23 -21
- {velocity_python-0.0.115.dist-info → velocity_python-0.0.117.dist-info}/WHEEL +0 -0
- {velocity_python-0.0.115.dist-info → velocity_python-0.0.117.dist-info}/licenses/LICENSE +0 -0
- {velocity_python-0.0.115.dist-info → velocity_python-0.0.117.dist-info}/top_level.txt +0 -0
|
@@ -13,102 +13,57 @@ from typing import Any, Dict, Optional
|
|
|
13
13
|
|
|
14
14
|
from velocity.aws import DEBUG
|
|
15
15
|
from velocity.aws.handlers import context as VelocityContext
|
|
16
|
+
from velocity.aws.handlers.base_handler import BaseHandler
|
|
16
17
|
from velocity.misc.format import to_json
|
|
17
18
|
|
|
18
19
|
|
|
19
|
-
class SqsHandler:
|
|
20
|
+
class SqsHandler(BaseHandler):
|
|
20
21
|
"""
|
|
21
22
|
Base class for handling SQS events in AWS Lambda functions.
|
|
22
|
-
|
|
23
|
+
|
|
23
24
|
Provides structured processing of SQS records with automatic action routing,
|
|
24
25
|
logging capabilities, and error handling hooks.
|
|
25
26
|
"""
|
|
26
27
|
|
|
27
|
-
def __init__(
|
|
28
|
-
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
aws_event: Dict[str, Any],
|
|
31
|
+
aws_context: Any,
|
|
32
|
+
context_class=VelocityContext.Context,
|
|
33
|
+
):
|
|
29
34
|
"""
|
|
30
35
|
Initialize the SQS handler.
|
|
31
|
-
|
|
36
|
+
|
|
32
37
|
Args:
|
|
33
38
|
aws_event: The AWS Lambda event containing SQS records
|
|
34
39
|
aws_context: The AWS Lambda context object
|
|
35
40
|
context_class: The context class to use for processing
|
|
36
41
|
"""
|
|
37
|
-
|
|
38
|
-
self.aws_context = aws_context
|
|
39
|
-
self.serve_action_default = True
|
|
40
|
-
self.skip_action = False
|
|
41
|
-
self.ContextClass = context_class
|
|
42
|
-
|
|
43
|
-
def log(self, tx, message: str, function: Optional[str] = None):
|
|
44
|
-
"""
|
|
45
|
-
Log a message to the system log table.
|
|
46
|
-
|
|
47
|
-
Args:
|
|
48
|
-
tx: Database transaction object
|
|
49
|
-
message: The message to log
|
|
50
|
-
function: Optional function name, auto-detected if not provided
|
|
51
|
-
"""
|
|
52
|
-
if not function:
|
|
53
|
-
function = self._get_calling_function()
|
|
54
|
-
|
|
55
|
-
data = {
|
|
56
|
-
"app_name": os.environ.get("ProjectName", "Unknown"),
|
|
57
|
-
"referer": "SQS",
|
|
58
|
-
"user_agent": "QueueHandler",
|
|
59
|
-
"device_type": "Lambda",
|
|
60
|
-
"function": function,
|
|
61
|
-
"message": message,
|
|
62
|
-
"sys_modified_by": "Lambda:BackOfficeQueueHandler",
|
|
63
|
-
}
|
|
64
|
-
tx.table("sys_log").insert(data)
|
|
65
|
-
|
|
66
|
-
def _get_calling_function(self) -> str:
|
|
67
|
-
"""
|
|
68
|
-
Get the name of the calling function by inspecting the call stack.
|
|
69
|
-
|
|
70
|
-
Returns:
|
|
71
|
-
The name of the calling function or "<Unknown>" if not found
|
|
72
|
-
"""
|
|
73
|
-
skip_functions = {"x", "log", "_transaction", "_get_calling_function"}
|
|
74
|
-
|
|
75
|
-
for idx in range(10): # Limit search to prevent infinite loops
|
|
76
|
-
try:
|
|
77
|
-
frame = sys._getframe(idx)
|
|
78
|
-
function_name = frame.f_code.co_name
|
|
79
|
-
|
|
80
|
-
if function_name not in skip_functions:
|
|
81
|
-
return function_name
|
|
82
|
-
|
|
83
|
-
except ValueError:
|
|
84
|
-
# No more frames in the stack
|
|
85
|
-
break
|
|
86
|
-
|
|
87
|
-
return "<Unknown>"
|
|
42
|
+
super().__init__(aws_event, aws_context, context_class)
|
|
88
43
|
|
|
89
44
|
def serve(self, tx):
|
|
90
45
|
"""
|
|
91
46
|
Process all SQS records in the event.
|
|
92
|
-
|
|
47
|
+
|
|
93
48
|
Args:
|
|
94
49
|
tx: Database transaction object
|
|
95
50
|
"""
|
|
96
51
|
records = self.aws_event.get("Records", [])
|
|
97
|
-
|
|
52
|
+
|
|
98
53
|
for record in records:
|
|
99
54
|
self._process_record(tx, record)
|
|
100
|
-
|
|
55
|
+
|
|
101
56
|
def _process_record(self, tx, record: Dict[str, Any]):
|
|
102
57
|
"""
|
|
103
58
|
Process a single SQS record.
|
|
104
|
-
|
|
59
|
+
|
|
105
60
|
Args:
|
|
106
61
|
tx: Database transaction object
|
|
107
62
|
record: Individual SQS record to process
|
|
108
63
|
"""
|
|
109
64
|
attrs = record.get("attributes", {})
|
|
110
65
|
postdata = {}
|
|
111
|
-
|
|
66
|
+
|
|
112
67
|
# Parse message body if present
|
|
113
68
|
body = record.get("body")
|
|
114
69
|
if body:
|
|
@@ -127,93 +82,22 @@ class SqsHandler:
|
|
|
127
82
|
response=None,
|
|
128
83
|
session=None,
|
|
129
84
|
)
|
|
130
|
-
|
|
85
|
+
|
|
86
|
+
# Use BaseHandler's execute_actions method
|
|
131
87
|
try:
|
|
132
|
-
self.
|
|
88
|
+
self.execute_actions(tx, local_context, postdata, attrs)
|
|
133
89
|
except Exception as e:
|
|
134
|
-
|
|
135
|
-
self.onError(
|
|
136
|
-
local_context,
|
|
137
|
-
exc=e.__class__.__name__,
|
|
138
|
-
tb=traceback.format_exc(),
|
|
139
|
-
)
|
|
140
|
-
else:
|
|
141
|
-
# Re-raise if no error handler is defined
|
|
142
|
-
raise
|
|
143
|
-
|
|
144
|
-
def _execute_actions(self, local_context):
|
|
145
|
-
"""
|
|
146
|
-
Execute the appropriate actions for the given context.
|
|
147
|
-
|
|
148
|
-
Args:
|
|
149
|
-
local_context: The context object for this record
|
|
150
|
-
"""
|
|
151
|
-
# Execute beforeAction hook if available
|
|
152
|
-
if hasattr(self, "beforeAction"):
|
|
153
|
-
self.beforeAction(local_context)
|
|
154
|
-
|
|
155
|
-
# Determine which actions to execute
|
|
156
|
-
actions = self._get_actions_to_execute(local_context)
|
|
157
|
-
|
|
158
|
-
# Execute the first matching action
|
|
159
|
-
for action in actions:
|
|
160
|
-
if self.skip_action:
|
|
161
|
-
return
|
|
162
|
-
|
|
163
|
-
if hasattr(self, action):
|
|
164
|
-
getattr(self, action)(local_context)
|
|
165
|
-
break
|
|
166
|
-
|
|
167
|
-
# Execute afterAction hook if available
|
|
168
|
-
if hasattr(self, "afterAction"):
|
|
169
|
-
self.afterAction(local_context)
|
|
170
|
-
|
|
171
|
-
def _get_actions_to_execute(self, local_context) -> list:
|
|
172
|
-
"""
|
|
173
|
-
Get the list of actions to execute for the given context.
|
|
174
|
-
|
|
175
|
-
Args:
|
|
176
|
-
local_context: The context object for this record
|
|
177
|
-
|
|
178
|
-
Returns:
|
|
179
|
-
List of action method names to try executing
|
|
180
|
-
"""
|
|
181
|
-
actions = []
|
|
182
|
-
|
|
183
|
-
# Add specific action if available
|
|
184
|
-
action = local_context.action()
|
|
185
|
-
if action:
|
|
186
|
-
action_method = self._format_action_name(action)
|
|
187
|
-
actions.append(action_method)
|
|
188
|
-
|
|
189
|
-
# Add default action if enabled
|
|
190
|
-
if self.serve_action_default:
|
|
191
|
-
actions.append("OnActionDefault")
|
|
192
|
-
|
|
193
|
-
return actions
|
|
194
|
-
|
|
195
|
-
def _format_action_name(self, action: str) -> str:
|
|
196
|
-
"""
|
|
197
|
-
Format an action string into a method name.
|
|
198
|
-
|
|
199
|
-
Args:
|
|
200
|
-
action: The raw action string
|
|
201
|
-
|
|
202
|
-
Returns:
|
|
203
|
-
Formatted method name
|
|
204
|
-
"""
|
|
205
|
-
formatted = action.replace('-', ' ').replace('_', ' ')
|
|
206
|
-
return f"on action {formatted}".title().replace(" ", "")
|
|
90
|
+
self.handle_error(local_context, e)
|
|
207
91
|
|
|
208
92
|
def OnActionDefault(self, tx, context):
|
|
209
93
|
"""
|
|
210
94
|
Default action handler when no specific action is found.
|
|
211
|
-
|
|
95
|
+
|
|
212
96
|
Args:
|
|
213
97
|
tx: Database transaction object
|
|
214
98
|
context: The context object for this record
|
|
215
99
|
"""
|
|
216
|
-
action = context.action() if hasattr(context,
|
|
100
|
+
action = context.action() if hasattr(context, "action") else "unknown"
|
|
217
101
|
warning_message = (
|
|
218
102
|
f"[Warn] Action handler not found. Calling default action "
|
|
219
103
|
f"`SqsHandler.OnActionDefault` with the following parameters:\n"
|
velocity/db/__init__.py
CHANGED
velocity/db/core/engine.py
CHANGED
|
@@ -330,30 +330,32 @@ class Engine:
|
|
|
330
330
|
Central method to parse driver exceptions and re-raise them as our custom exceptions.
|
|
331
331
|
"""
|
|
332
332
|
logger = logging.getLogger(__name__)
|
|
333
|
-
|
|
333
|
+
|
|
334
334
|
# If it's already a velocity exception, just re-raise it
|
|
335
335
|
if isinstance(exception, exceptions.DbException):
|
|
336
336
|
raise exception
|
|
337
|
-
|
|
337
|
+
|
|
338
338
|
# Get error code and message from the SQL driver
|
|
339
339
|
try:
|
|
340
340
|
error_code, error_message = self.sql.get_error(exception)
|
|
341
341
|
except Exception:
|
|
342
342
|
error_code, error_message = None, str(exception)
|
|
343
|
-
|
|
343
|
+
|
|
344
344
|
msg = str(exception).strip().lower()
|
|
345
|
-
|
|
345
|
+
|
|
346
346
|
# Create enhanced error message with SQL query
|
|
347
347
|
enhanced_message = str(exception)
|
|
348
348
|
if sql:
|
|
349
|
-
enhanced_message +=
|
|
350
|
-
|
|
349
|
+
enhanced_message += (
|
|
350
|
+
f"\n\nSQL Query:\n{self._format_sql_with_params(sql, parameters)}"
|
|
351
|
+
)
|
|
352
|
+
|
|
351
353
|
logger.warning(
|
|
352
354
|
"Database error caught. Attempting to transform: code=%s message=%s",
|
|
353
355
|
error_code,
|
|
354
356
|
error_message,
|
|
355
357
|
)
|
|
356
|
-
|
|
358
|
+
|
|
357
359
|
# Direct error code mapping
|
|
358
360
|
if error_code in self.sql.ApplicationErrorCodes:
|
|
359
361
|
raise exceptions.DbApplicationError(enhanced_message) from exception
|
|
@@ -379,7 +381,7 @@ class Engine:
|
|
|
379
381
|
raise exceptions.DbLockTimeoutError(enhanced_message) from exception
|
|
380
382
|
if error_code in self.sql.RetryTransactionCodes:
|
|
381
383
|
raise exceptions.DbRetryTransaction(enhanced_message) from exception
|
|
382
|
-
|
|
384
|
+
|
|
383
385
|
# Regex-based fallback patterns
|
|
384
386
|
if re.search(r"key \(sys_id\)=\(\d+\) already exists.", msg, re.M):
|
|
385
387
|
raise exceptions.DbDuplicateKeyError(enhanced_message) from exception
|
|
@@ -405,7 +407,7 @@ class Engine:
|
|
|
405
407
|
raise exceptions.DbConnectionError(enhanced_message) from exception
|
|
406
408
|
if "no such table:" in msg:
|
|
407
409
|
raise exceptions.DbTableMissingError(enhanced_message) from exception
|
|
408
|
-
|
|
410
|
+
|
|
409
411
|
logger.error(
|
|
410
412
|
"Unhandled/Unknown Error in engine.process_error",
|
|
411
413
|
exc_info=True,
|
|
@@ -416,7 +418,7 @@ class Engine:
|
|
|
416
418
|
"sql_params": parameters,
|
|
417
419
|
},
|
|
418
420
|
)
|
|
419
|
-
|
|
421
|
+
|
|
420
422
|
# If we can't classify it, re-raise with enhanced message
|
|
421
423
|
raise type(exception)(enhanced_message) from exception
|
|
422
424
|
|
|
@@ -426,10 +428,10 @@ class Engine:
|
|
|
426
428
|
"""
|
|
427
429
|
if not sql:
|
|
428
430
|
return "No SQL provided"
|
|
429
|
-
|
|
431
|
+
|
|
430
432
|
if not parameters:
|
|
431
433
|
return sql
|
|
432
|
-
|
|
434
|
+
|
|
433
435
|
try:
|
|
434
436
|
# Handle different parameter formats
|
|
435
437
|
if isinstance(parameters, (list, tuple)):
|
|
@@ -437,45 +439,47 @@ class Engine:
|
|
|
437
439
|
formatted_params = []
|
|
438
440
|
for param in parameters:
|
|
439
441
|
if param is None:
|
|
440
|
-
formatted_params.append(
|
|
442
|
+
formatted_params.append("NULL")
|
|
441
443
|
elif isinstance(param, str):
|
|
442
444
|
# Escape single quotes and wrap in quotes
|
|
443
445
|
escaped = param.replace("'", "''")
|
|
444
446
|
formatted_params.append(f"'{escaped}'")
|
|
445
447
|
elif isinstance(param, bool):
|
|
446
|
-
formatted_params.append(
|
|
448
|
+
formatted_params.append("TRUE" if param else "FALSE")
|
|
447
449
|
else:
|
|
448
450
|
formatted_params.append(str(param))
|
|
449
|
-
|
|
451
|
+
|
|
450
452
|
# Replace %s placeholders with actual values
|
|
451
453
|
formatted_sql = sql
|
|
452
454
|
for param in formatted_params:
|
|
453
|
-
formatted_sql = formatted_sql.replace(
|
|
454
|
-
|
|
455
|
+
formatted_sql = formatted_sql.replace("%s", param, 1)
|
|
456
|
+
|
|
455
457
|
return formatted_sql
|
|
456
|
-
|
|
458
|
+
|
|
457
459
|
elif isinstance(parameters, dict):
|
|
458
460
|
# Handle named parameters
|
|
459
461
|
formatted_sql = sql
|
|
460
462
|
for key, value in parameters.items():
|
|
461
463
|
if value is None:
|
|
462
|
-
replacement =
|
|
464
|
+
replacement = "NULL"
|
|
463
465
|
elif isinstance(value, str):
|
|
464
466
|
escaped = value.replace("'", "''")
|
|
465
467
|
replacement = f"'{escaped}'"
|
|
466
468
|
elif isinstance(value, bool):
|
|
467
|
-
replacement =
|
|
469
|
+
replacement = "TRUE" if value else "FALSE"
|
|
468
470
|
else:
|
|
469
471
|
replacement = str(value)
|
|
470
|
-
|
|
472
|
+
|
|
471
473
|
# Replace %(key)s or :key patterns
|
|
472
|
-
formatted_sql = formatted_sql.replace(f
|
|
473
|
-
formatted_sql = formatted_sql.replace(f
|
|
474
|
-
|
|
474
|
+
formatted_sql = formatted_sql.replace(f"%({key})s", replacement)
|
|
475
|
+
formatted_sql = formatted_sql.replace(f":{key}", replacement)
|
|
476
|
+
|
|
475
477
|
return formatted_sql
|
|
476
478
|
else:
|
|
477
479
|
return f"{sql}\n-- Parameters: {parameters}"
|
|
478
|
-
|
|
480
|
+
|
|
479
481
|
except Exception as e:
|
|
480
482
|
# If formatting fails, return original SQL with parameters shown separately
|
|
481
|
-
return
|
|
483
|
+
return (
|
|
484
|
+
f"{sql}\n-- Parameters (formatting failed): {parameters}\n-- Error: {e}"
|
|
485
|
+
)
|
velocity/db/core/exceptions.py
CHANGED
|
@@ -56,15 +56,18 @@ class DuplicateRowsFoundError(Exception):
|
|
|
56
56
|
|
|
57
57
|
class DbQueryError(DbException):
|
|
58
58
|
"""Database query error"""
|
|
59
|
+
|
|
59
60
|
pass
|
|
60
61
|
|
|
61
62
|
|
|
62
63
|
class DbTransactionError(DbException):
|
|
63
64
|
"""Database transaction error"""
|
|
65
|
+
|
|
64
66
|
pass
|
|
65
67
|
|
|
66
68
|
|
|
67
69
|
# Add aliases for backward compatibility with engine.py
|
|
68
70
|
class DatabaseError(DbException):
|
|
69
71
|
"""Generic database error - alias for DbException"""
|
|
72
|
+
|
|
70
73
|
pass
|
velocity/db/core/result.py
CHANGED
|
@@ -7,14 +7,14 @@ class Result:
|
|
|
7
7
|
"""
|
|
8
8
|
Wraps a database cursor to provide various convenience transformations
|
|
9
9
|
(dict, list, tuple, etc.) and helps iterate over query results.
|
|
10
|
-
|
|
10
|
+
|
|
11
11
|
Features:
|
|
12
12
|
- Pre-fetches first row for immediate boolean evaluation
|
|
13
13
|
- Boolean state changes as rows are consumed: bool(result) tells you if MORE rows are available
|
|
14
14
|
- Supports __bool__, is_empty(), has_results() for checking remaining results
|
|
15
15
|
- Efficient iteration without unnecessary fetchall() calls
|
|
16
16
|
- Caches next row to maintain accurate state without redundant database calls
|
|
17
|
-
|
|
17
|
+
|
|
18
18
|
Boolean Behavior:
|
|
19
19
|
- Initially: bool(result) = True if query returned any rows
|
|
20
20
|
- After each row: bool(result) = True if more rows are available to fetch
|
|
@@ -29,15 +29,15 @@ class Result:
|
|
|
29
29
|
description = getattr(cursor, "description", []) or []
|
|
30
30
|
self._headers = []
|
|
31
31
|
for col in description:
|
|
32
|
-
if hasattr(col,
|
|
32
|
+
if hasattr(col, "__getitem__"): # Tuple-like (col[0])
|
|
33
33
|
self._headers.append(col[0].lower())
|
|
34
|
-
elif hasattr(col,
|
|
34
|
+
elif hasattr(col, "name"): # Object with name attribute
|
|
35
35
|
self._headers.append(col.name.lower())
|
|
36
36
|
else:
|
|
37
|
-
self._headers.append(f
|
|
37
|
+
self._headers.append(f"column_{len(self._headers)}")
|
|
38
38
|
except (AttributeError, TypeError, IndexError):
|
|
39
39
|
self._headers = []
|
|
40
|
-
|
|
40
|
+
|
|
41
41
|
self.__as_strings = False
|
|
42
42
|
self.__enumerate = False
|
|
43
43
|
self.__count = -1
|
|
@@ -49,7 +49,7 @@ class Result:
|
|
|
49
49
|
self._cached_first_row = None
|
|
50
50
|
self._first_row_fetched = False
|
|
51
51
|
self._exhausted = False
|
|
52
|
-
|
|
52
|
+
|
|
53
53
|
# Pre-fetch the first row to enable immediate boolean evaluation
|
|
54
54
|
self._fetch_first_row()
|
|
55
55
|
|
|
@@ -60,14 +60,16 @@ class Result:
|
|
|
60
60
|
"""
|
|
61
61
|
if self._first_row_fetched or not self._cursor:
|
|
62
62
|
return
|
|
63
|
-
|
|
63
|
+
|
|
64
64
|
# Don't try to fetch from INSERT/UPDATE/DELETE operations
|
|
65
65
|
# These operations don't return rows, only rowcount
|
|
66
|
-
if self.__sql and self.__sql.strip().upper().startswith(
|
|
66
|
+
if self.__sql and self.__sql.strip().upper().startswith(
|
|
67
|
+
("INSERT", "UPDATE", "DELETE", "TRUNCATE")
|
|
68
|
+
):
|
|
67
69
|
self._exhausted = True
|
|
68
70
|
self._first_row_fetched = True
|
|
69
71
|
return
|
|
70
|
-
|
|
72
|
+
|
|
71
73
|
try:
|
|
72
74
|
raw_row = self._cursor.fetchone()
|
|
73
75
|
if raw_row:
|
|
@@ -109,7 +111,9 @@ class Result:
|
|
|
109
111
|
Return True if there are more rows available to fetch.
|
|
110
112
|
This is based on whether we have a cached row or the cursor isn't exhausted.
|
|
111
113
|
"""
|
|
112
|
-
return self._cached_first_row is not None or (
|
|
114
|
+
return self._cached_first_row is not None or (
|
|
115
|
+
not self._exhausted and self._cursor
|
|
116
|
+
)
|
|
113
117
|
|
|
114
118
|
def __next__(self):
|
|
115
119
|
"""
|
|
@@ -127,7 +131,7 @@ class Result:
|
|
|
127
131
|
if not row:
|
|
128
132
|
self._exhausted = True
|
|
129
133
|
raise StopIteration
|
|
130
|
-
# Try to pre-fetch the next row to update our state
|
|
134
|
+
# Try to pre-fetch the next row to update our state
|
|
131
135
|
self._try_cache_next_row()
|
|
132
136
|
except Exception as e:
|
|
133
137
|
# Handle cursor errors (e.g., closed cursor)
|
|
@@ -154,7 +158,7 @@ class Result:
|
|
|
154
158
|
"""
|
|
155
159
|
if not self._cursor or self._cached_first_row is not None:
|
|
156
160
|
return
|
|
157
|
-
|
|
161
|
+
|
|
158
162
|
try:
|
|
159
163
|
next_row = self._cursor.fetchone()
|
|
160
164
|
if next_row:
|
|
@@ -214,18 +218,22 @@ class Result:
|
|
|
214
218
|
"""
|
|
215
219
|
if not self.__columns and self._cursor and hasattr(self._cursor, "description"):
|
|
216
220
|
for column in self._cursor.description:
|
|
217
|
-
data = {
|
|
218
|
-
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
+
data = {"type_name": "unknown"} # Default value
|
|
222
|
+
|
|
221
223
|
# Try to get type information (PostgreSQL specific)
|
|
222
224
|
try:
|
|
223
|
-
if
|
|
224
|
-
|
|
225
|
+
if (
|
|
226
|
+
hasattr(column, "type_code")
|
|
227
|
+
and self.__tx
|
|
228
|
+
and hasattr(self.__tx, "pg_types")
|
|
229
|
+
):
|
|
230
|
+
data["type_name"] = self.__tx.pg_types.get(
|
|
231
|
+
column.type_code, "unknown"
|
|
232
|
+
)
|
|
225
233
|
except (AttributeError, KeyError):
|
|
226
234
|
# Keep default value
|
|
227
235
|
pass
|
|
228
|
-
|
|
236
|
+
|
|
229
237
|
# Get all other column attributes safely
|
|
230
238
|
for key in dir(column):
|
|
231
239
|
if not key.startswith("__"):
|
|
@@ -234,8 +242,8 @@ class Result:
|
|
|
234
242
|
except (AttributeError, TypeError):
|
|
235
243
|
# Skip attributes that can't be accessed
|
|
236
244
|
continue
|
|
237
|
-
|
|
238
|
-
column_name = getattr(column,
|
|
245
|
+
|
|
246
|
+
column_name = getattr(column, "name", f"column_{len(self.__columns)}")
|
|
239
247
|
self.__columns[column_name] = data
|
|
240
248
|
return self.__columns
|
|
241
249
|
|
velocity/db/core/row.py
CHANGED
|
@@ -97,7 +97,9 @@ class Row:
|
|
|
97
97
|
except Exception as e:
|
|
98
98
|
# Check if the error message indicates a missing column
|
|
99
99
|
error_msg = str(e).lower()
|
|
100
|
-
if
|
|
100
|
+
if "column" in error_msg and (
|
|
101
|
+
"does not exist" in error_msg or "not found" in error_msg
|
|
102
|
+
):
|
|
101
103
|
return failobj
|
|
102
104
|
# Re-raise other exceptions
|
|
103
105
|
raise
|