velocity-python 0.0.116__py3-none-any.whl → 0.0.118__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 +3 -1
- velocity/aws/__init__.py +3 -0
- velocity/aws/amplify.py +10 -6
- velocity/aws/handlers/__init__.py +2 -0
- velocity/aws/handlers/base_handler.py +245 -0
- velocity/aws/handlers/context.py +58 -41
- velocity/aws/handlers/exceptions.py +16 -0
- velocity/aws/handlers/lambda_handler.py +15 -84
- velocity/aws/handlers/response.py +1 -1
- velocity/aws/handlers/sqs_handler.py +23 -144
- velocity/db/__init__.py +16 -1
- velocity/db/core/decorators.py +0 -1
- velocity/db/core/engine.py +33 -31
- velocity/db/core/exceptions.py +3 -0
- velocity/db/core/result.py +30 -24
- velocity/db/core/row.py +3 -1
- velocity/db/core/table.py +6 -5
- velocity/db/exceptions.py +35 -18
- velocity/db/servers/mysql.py +2 -3
- velocity/db/servers/postgres/__init__.py +10 -12
- velocity/db/servers/postgres/sql.py +36 -17
- velocity/db/servers/sqlite.py +2 -2
- velocity/db/servers/sqlserver.py +3 -3
- velocity/db/servers/tablehelper.py +117 -91
- velocity/db/utils.py +62 -47
- velocity/misc/conv/__init__.py +2 -0
- velocity/misc/conv/iconv.py +5 -4
- velocity/misc/export.py +1 -4
- velocity/misc/merge.py +1 -1
- velocity/misc/tools.py +0 -1
- {velocity_python-0.0.116.dist-info → velocity_python-0.0.118.dist-info}/METADATA +1 -1
- velocity_python-0.0.118.dist-info/RECORD +58 -0
- velocity_python-0.0.116.dist-info/RECORD +0 -56
- {velocity_python-0.0.116.dist-info → velocity_python-0.0.118.dist-info}/WHEEL +0 -0
- {velocity_python-0.0.116.dist-info → velocity_python-0.0.118.dist-info}/licenses/LICENSE +0 -0
- {velocity_python-0.0.116.dist-info → velocity_python-0.0.118.dist-info}/top_level.txt +0 -0
|
@@ -6,109 +6,59 @@ It includes logging capabilities, action routing, and error handling.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import json
|
|
9
|
-
import
|
|
10
|
-
import sys
|
|
11
|
-
import traceback
|
|
12
|
-
from typing import Any, Dict, Optional
|
|
9
|
+
from typing import Any, Dict
|
|
13
10
|
|
|
14
|
-
from velocity.aws import DEBUG
|
|
15
11
|
from velocity.aws.handlers import context as VelocityContext
|
|
16
|
-
from velocity.
|
|
12
|
+
from velocity.aws.handlers.base_handler import BaseHandler
|
|
17
13
|
|
|
18
14
|
|
|
19
|
-
class SqsHandler:
|
|
15
|
+
class SqsHandler(BaseHandler):
|
|
20
16
|
"""
|
|
21
17
|
Base class for handling SQS events in AWS Lambda functions.
|
|
22
|
-
|
|
18
|
+
|
|
23
19
|
Provides structured processing of SQS records with automatic action routing,
|
|
24
20
|
logging capabilities, and error handling hooks.
|
|
25
21
|
"""
|
|
26
22
|
|
|
27
|
-
def __init__(
|
|
28
|
-
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
aws_event: Dict[str, Any],
|
|
26
|
+
aws_context: Any,
|
|
27
|
+
context_class=VelocityContext.Context,
|
|
28
|
+
):
|
|
29
29
|
"""
|
|
30
30
|
Initialize the SQS handler.
|
|
31
|
-
|
|
31
|
+
|
|
32
32
|
Args:
|
|
33
33
|
aws_event: The AWS Lambda event containing SQS records
|
|
34
34
|
aws_context: The AWS Lambda context object
|
|
35
35
|
context_class: The context class to use for processing
|
|
36
36
|
"""
|
|
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>"
|
|
37
|
+
super().__init__(aws_event, aws_context, context_class)
|
|
88
38
|
|
|
89
39
|
def serve(self, tx):
|
|
90
40
|
"""
|
|
91
41
|
Process all SQS records in the event.
|
|
92
|
-
|
|
42
|
+
|
|
93
43
|
Args:
|
|
94
44
|
tx: Database transaction object
|
|
95
45
|
"""
|
|
96
46
|
records = self.aws_event.get("Records", [])
|
|
97
|
-
|
|
47
|
+
|
|
98
48
|
for record in records:
|
|
99
49
|
self._process_record(tx, record)
|
|
100
|
-
|
|
50
|
+
|
|
101
51
|
def _process_record(self, tx, record: Dict[str, Any]):
|
|
102
52
|
"""
|
|
103
53
|
Process a single SQS record.
|
|
104
|
-
|
|
54
|
+
|
|
105
55
|
Args:
|
|
106
56
|
tx: Database transaction object
|
|
107
57
|
record: Individual SQS record to process
|
|
108
58
|
"""
|
|
109
59
|
attrs = record.get("attributes", {})
|
|
110
60
|
postdata = {}
|
|
111
|
-
|
|
61
|
+
|
|
112
62
|
# Parse message body if present
|
|
113
63
|
body = record.get("body")
|
|
114
64
|
if body:
|
|
@@ -127,93 +77,22 @@ class SqsHandler:
|
|
|
127
77
|
response=None,
|
|
128
78
|
session=None,
|
|
129
79
|
)
|
|
130
|
-
|
|
80
|
+
|
|
81
|
+
# Use BaseHandler's execute_actions method
|
|
131
82
|
try:
|
|
132
|
-
self.
|
|
83
|
+
self.execute_actions(tx, local_context, postdata, attrs)
|
|
133
84
|
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(" ", "")
|
|
85
|
+
self.handle_error(local_context, e)
|
|
207
86
|
|
|
208
87
|
def OnActionDefault(self, tx, context):
|
|
209
88
|
"""
|
|
210
89
|
Default action handler when no specific action is found.
|
|
211
|
-
|
|
90
|
+
|
|
212
91
|
Args:
|
|
213
92
|
tx: Database transaction object
|
|
214
93
|
context: The context object for this record
|
|
215
94
|
"""
|
|
216
|
-
action = context.action() if hasattr(context,
|
|
95
|
+
action = context.action() if hasattr(context, "action") else "unknown"
|
|
217
96
|
warning_message = (
|
|
218
97
|
f"[Warn] Action handler not found. Calling default action "
|
|
219
98
|
f"`SqsHandler.OnActionDefault` with the following parameters:\n"
|
velocity/db/__init__.py
CHANGED
|
@@ -15,5 +15,20 @@ from velocity.db.utils import (
|
|
|
15
15
|
safe_sort_key_none_first,
|
|
16
16
|
safe_sort_key_with_default,
|
|
17
17
|
group_by_fields,
|
|
18
|
-
safe_sort_grouped_rows
|
|
18
|
+
safe_sort_grouped_rows,
|
|
19
19
|
)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"exceptions",
|
|
23
|
+
"postgres",
|
|
24
|
+
"mysql",
|
|
25
|
+
"sqlite",
|
|
26
|
+
"sqlserver",
|
|
27
|
+
"utils",
|
|
28
|
+
"safe_sort_rows",
|
|
29
|
+
"safe_sort_key_none_last",
|
|
30
|
+
"safe_sort_key_none_first",
|
|
31
|
+
"safe_sort_key_with_default",
|
|
32
|
+
"group_by_fields",
|
|
33
|
+
"safe_sort_grouped_rows",
|
|
34
|
+
]
|
velocity/db/core/decorators.py
CHANGED
velocity/db/core/engine.py
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import inspect
|
|
2
|
-
import sys
|
|
3
2
|
import re
|
|
4
|
-
import traceback
|
|
5
3
|
from functools import wraps
|
|
6
4
|
from velocity.db import exceptions
|
|
7
5
|
from velocity.db.core.transaction import Transaction
|
|
@@ -176,18 +174,18 @@ class Engine:
|
|
|
176
174
|
while True:
|
|
177
175
|
try:
|
|
178
176
|
return function(*args, **kwds)
|
|
179
|
-
except exceptions.DbRetryTransaction
|
|
177
|
+
except exceptions.DbRetryTransaction:
|
|
180
178
|
retry_count += 1
|
|
181
179
|
if retry_count > self.MAX_RETRIES:
|
|
182
180
|
raise
|
|
183
181
|
_tx.rollback()
|
|
184
|
-
except exceptions.DbLockTimeoutError
|
|
182
|
+
except exceptions.DbLockTimeoutError:
|
|
185
183
|
lock_timeout_count += 1
|
|
186
184
|
if lock_timeout_count > self.MAX_RETRIES:
|
|
187
185
|
raise
|
|
188
186
|
_tx.rollback()
|
|
189
187
|
continue
|
|
190
|
-
except:
|
|
188
|
+
except Exception:
|
|
191
189
|
raise
|
|
192
190
|
finally:
|
|
193
191
|
setattr(_tx, "_exec_function_depth", depth)
|
|
@@ -330,30 +328,32 @@ class Engine:
|
|
|
330
328
|
Central method to parse driver exceptions and re-raise them as our custom exceptions.
|
|
331
329
|
"""
|
|
332
330
|
logger = logging.getLogger(__name__)
|
|
333
|
-
|
|
331
|
+
|
|
334
332
|
# If it's already a velocity exception, just re-raise it
|
|
335
333
|
if isinstance(exception, exceptions.DbException):
|
|
336
334
|
raise exception
|
|
337
|
-
|
|
335
|
+
|
|
338
336
|
# Get error code and message from the SQL driver
|
|
339
337
|
try:
|
|
340
338
|
error_code, error_message = self.sql.get_error(exception)
|
|
341
339
|
except Exception:
|
|
342
340
|
error_code, error_message = None, str(exception)
|
|
343
|
-
|
|
341
|
+
|
|
344
342
|
msg = str(exception).strip().lower()
|
|
345
|
-
|
|
343
|
+
|
|
346
344
|
# Create enhanced error message with SQL query
|
|
347
345
|
enhanced_message = str(exception)
|
|
348
346
|
if sql:
|
|
349
|
-
enhanced_message +=
|
|
350
|
-
|
|
347
|
+
enhanced_message += (
|
|
348
|
+
f"\n\nSQL Query:\n{self._format_sql_with_params(sql, parameters)}"
|
|
349
|
+
)
|
|
350
|
+
|
|
351
351
|
logger.warning(
|
|
352
352
|
"Database error caught. Attempting to transform: code=%s message=%s",
|
|
353
353
|
error_code,
|
|
354
354
|
error_message,
|
|
355
355
|
)
|
|
356
|
-
|
|
356
|
+
|
|
357
357
|
# Direct error code mapping
|
|
358
358
|
if error_code in self.sql.ApplicationErrorCodes:
|
|
359
359
|
raise exceptions.DbApplicationError(enhanced_message) from exception
|
|
@@ -379,7 +379,7 @@ class Engine:
|
|
|
379
379
|
raise exceptions.DbLockTimeoutError(enhanced_message) from exception
|
|
380
380
|
if error_code in self.sql.RetryTransactionCodes:
|
|
381
381
|
raise exceptions.DbRetryTransaction(enhanced_message) from exception
|
|
382
|
-
|
|
382
|
+
|
|
383
383
|
# Regex-based fallback patterns
|
|
384
384
|
if re.search(r"key \(sys_id\)=\(\d+\) already exists.", msg, re.M):
|
|
385
385
|
raise exceptions.DbDuplicateKeyError(enhanced_message) from exception
|
|
@@ -405,7 +405,7 @@ class Engine:
|
|
|
405
405
|
raise exceptions.DbConnectionError(enhanced_message) from exception
|
|
406
406
|
if "no such table:" in msg:
|
|
407
407
|
raise exceptions.DbTableMissingError(enhanced_message) from exception
|
|
408
|
-
|
|
408
|
+
|
|
409
409
|
logger.error(
|
|
410
410
|
"Unhandled/Unknown Error in engine.process_error",
|
|
411
411
|
exc_info=True,
|
|
@@ -416,7 +416,7 @@ class Engine:
|
|
|
416
416
|
"sql_params": parameters,
|
|
417
417
|
},
|
|
418
418
|
)
|
|
419
|
-
|
|
419
|
+
|
|
420
420
|
# If we can't classify it, re-raise with enhanced message
|
|
421
421
|
raise type(exception)(enhanced_message) from exception
|
|
422
422
|
|
|
@@ -426,10 +426,10 @@ class Engine:
|
|
|
426
426
|
"""
|
|
427
427
|
if not sql:
|
|
428
428
|
return "No SQL provided"
|
|
429
|
-
|
|
429
|
+
|
|
430
430
|
if not parameters:
|
|
431
431
|
return sql
|
|
432
|
-
|
|
432
|
+
|
|
433
433
|
try:
|
|
434
434
|
# Handle different parameter formats
|
|
435
435
|
if isinstance(parameters, (list, tuple)):
|
|
@@ -437,45 +437,47 @@ class Engine:
|
|
|
437
437
|
formatted_params = []
|
|
438
438
|
for param in parameters:
|
|
439
439
|
if param is None:
|
|
440
|
-
formatted_params.append(
|
|
440
|
+
formatted_params.append("NULL")
|
|
441
441
|
elif isinstance(param, str):
|
|
442
442
|
# Escape single quotes and wrap in quotes
|
|
443
443
|
escaped = param.replace("'", "''")
|
|
444
444
|
formatted_params.append(f"'{escaped}'")
|
|
445
445
|
elif isinstance(param, bool):
|
|
446
|
-
formatted_params.append(
|
|
446
|
+
formatted_params.append("TRUE" if param else "FALSE")
|
|
447
447
|
else:
|
|
448
448
|
formatted_params.append(str(param))
|
|
449
|
-
|
|
449
|
+
|
|
450
450
|
# Replace %s placeholders with actual values
|
|
451
451
|
formatted_sql = sql
|
|
452
452
|
for param in formatted_params:
|
|
453
|
-
formatted_sql = formatted_sql.replace(
|
|
454
|
-
|
|
453
|
+
formatted_sql = formatted_sql.replace("%s", param, 1)
|
|
454
|
+
|
|
455
455
|
return formatted_sql
|
|
456
|
-
|
|
456
|
+
|
|
457
457
|
elif isinstance(parameters, dict):
|
|
458
458
|
# Handle named parameters
|
|
459
459
|
formatted_sql = sql
|
|
460
460
|
for key, value in parameters.items():
|
|
461
461
|
if value is None:
|
|
462
|
-
replacement =
|
|
462
|
+
replacement = "NULL"
|
|
463
463
|
elif isinstance(value, str):
|
|
464
464
|
escaped = value.replace("'", "''")
|
|
465
465
|
replacement = f"'{escaped}'"
|
|
466
466
|
elif isinstance(value, bool):
|
|
467
|
-
replacement =
|
|
467
|
+
replacement = "TRUE" if value else "FALSE"
|
|
468
468
|
else:
|
|
469
469
|
replacement = str(value)
|
|
470
|
-
|
|
470
|
+
|
|
471
471
|
# Replace %(key)s or :key patterns
|
|
472
|
-
formatted_sql = formatted_sql.replace(f
|
|
473
|
-
formatted_sql = formatted_sql.replace(f
|
|
474
|
-
|
|
472
|
+
formatted_sql = formatted_sql.replace(f"%({key})s", replacement)
|
|
473
|
+
formatted_sql = formatted_sql.replace(f":{key}", replacement)
|
|
474
|
+
|
|
475
475
|
return formatted_sql
|
|
476
476
|
else:
|
|
477
477
|
return f"{sql}\n-- Parameters: {parameters}"
|
|
478
|
-
|
|
478
|
+
|
|
479
479
|
except Exception as e:
|
|
480
480
|
# If formatting fails, return original SQL with parameters shown separately
|
|
481
|
-
return
|
|
481
|
+
return (
|
|
482
|
+
f"{sql}\n-- Parameters (formatting failed): {parameters}\n-- Error: {e}"
|
|
483
|
+
)
|
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
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import datetime
|
|
2
|
-
import decimal
|
|
3
1
|
from velocity.misc.format import to_json
|
|
4
2
|
|
|
5
3
|
|
|
@@ -7,14 +5,14 @@ class Result:
|
|
|
7
5
|
"""
|
|
8
6
|
Wraps a database cursor to provide various convenience transformations
|
|
9
7
|
(dict, list, tuple, etc.) and helps iterate over query results.
|
|
10
|
-
|
|
8
|
+
|
|
11
9
|
Features:
|
|
12
10
|
- Pre-fetches first row for immediate boolean evaluation
|
|
13
11
|
- Boolean state changes as rows are consumed: bool(result) tells you if MORE rows are available
|
|
14
12
|
- Supports __bool__, is_empty(), has_results() for checking remaining results
|
|
15
13
|
- Efficient iteration without unnecessary fetchall() calls
|
|
16
14
|
- Caches next row to maintain accurate state without redundant database calls
|
|
17
|
-
|
|
15
|
+
|
|
18
16
|
Boolean Behavior:
|
|
19
17
|
- Initially: bool(result) = True if query returned any rows
|
|
20
18
|
- After each row: bool(result) = True if more rows are available to fetch
|
|
@@ -29,15 +27,15 @@ class Result:
|
|
|
29
27
|
description = getattr(cursor, "description", []) or []
|
|
30
28
|
self._headers = []
|
|
31
29
|
for col in description:
|
|
32
|
-
if hasattr(col,
|
|
30
|
+
if hasattr(col, "__getitem__"): # Tuple-like (col[0])
|
|
33
31
|
self._headers.append(col[0].lower())
|
|
34
|
-
elif hasattr(col,
|
|
32
|
+
elif hasattr(col, "name"): # Object with name attribute
|
|
35
33
|
self._headers.append(col.name.lower())
|
|
36
34
|
else:
|
|
37
|
-
self._headers.append(f
|
|
35
|
+
self._headers.append(f"column_{len(self._headers)}")
|
|
38
36
|
except (AttributeError, TypeError, IndexError):
|
|
39
37
|
self._headers = []
|
|
40
|
-
|
|
38
|
+
|
|
41
39
|
self.__as_strings = False
|
|
42
40
|
self.__enumerate = False
|
|
43
41
|
self.__count = -1
|
|
@@ -49,7 +47,7 @@ class Result:
|
|
|
49
47
|
self._cached_first_row = None
|
|
50
48
|
self._first_row_fetched = False
|
|
51
49
|
self._exhausted = False
|
|
52
|
-
|
|
50
|
+
|
|
53
51
|
# Pre-fetch the first row to enable immediate boolean evaluation
|
|
54
52
|
self._fetch_first_row()
|
|
55
53
|
|
|
@@ -60,14 +58,16 @@ class Result:
|
|
|
60
58
|
"""
|
|
61
59
|
if self._first_row_fetched or not self._cursor:
|
|
62
60
|
return
|
|
63
|
-
|
|
61
|
+
|
|
64
62
|
# Don't try to fetch from INSERT/UPDATE/DELETE operations
|
|
65
63
|
# These operations don't return rows, only rowcount
|
|
66
|
-
if self.__sql and self.__sql.strip().upper().startswith(
|
|
64
|
+
if self.__sql and self.__sql.strip().upper().startswith(
|
|
65
|
+
("INSERT", "UPDATE", "DELETE", "TRUNCATE")
|
|
66
|
+
):
|
|
67
67
|
self._exhausted = True
|
|
68
68
|
self._first_row_fetched = True
|
|
69
69
|
return
|
|
70
|
-
|
|
70
|
+
|
|
71
71
|
try:
|
|
72
72
|
raw_row = self._cursor.fetchone()
|
|
73
73
|
if raw_row:
|
|
@@ -109,7 +109,9 @@ class Result:
|
|
|
109
109
|
Return True if there are more rows available to fetch.
|
|
110
110
|
This is based on whether we have a cached row or the cursor isn't exhausted.
|
|
111
111
|
"""
|
|
112
|
-
return self._cached_first_row is not None or (
|
|
112
|
+
return self._cached_first_row is not None or (
|
|
113
|
+
not self._exhausted and self._cursor
|
|
114
|
+
)
|
|
113
115
|
|
|
114
116
|
def __next__(self):
|
|
115
117
|
"""
|
|
@@ -127,7 +129,7 @@ class Result:
|
|
|
127
129
|
if not row:
|
|
128
130
|
self._exhausted = True
|
|
129
131
|
raise StopIteration
|
|
130
|
-
# Try to pre-fetch the next row to update our state
|
|
132
|
+
# Try to pre-fetch the next row to update our state
|
|
131
133
|
self._try_cache_next_row()
|
|
132
134
|
except Exception as e:
|
|
133
135
|
# Handle cursor errors (e.g., closed cursor)
|
|
@@ -154,7 +156,7 @@ class Result:
|
|
|
154
156
|
"""
|
|
155
157
|
if not self._cursor or self._cached_first_row is not None:
|
|
156
158
|
return
|
|
157
|
-
|
|
159
|
+
|
|
158
160
|
try:
|
|
159
161
|
next_row = self._cursor.fetchone()
|
|
160
162
|
if next_row:
|
|
@@ -214,18 +216,22 @@ class Result:
|
|
|
214
216
|
"""
|
|
215
217
|
if not self.__columns and self._cursor and hasattr(self._cursor, "description"):
|
|
216
218
|
for column in self._cursor.description:
|
|
217
|
-
data = {
|
|
218
|
-
|
|
219
|
-
}
|
|
220
|
-
|
|
219
|
+
data = {"type_name": "unknown"} # Default value
|
|
220
|
+
|
|
221
221
|
# Try to get type information (PostgreSQL specific)
|
|
222
222
|
try:
|
|
223
|
-
if
|
|
224
|
-
|
|
223
|
+
if (
|
|
224
|
+
hasattr(column, "type_code")
|
|
225
|
+
and self.__tx
|
|
226
|
+
and hasattr(self.__tx, "pg_types")
|
|
227
|
+
):
|
|
228
|
+
data["type_name"] = self.__tx.pg_types.get(
|
|
229
|
+
column.type_code, "unknown"
|
|
230
|
+
)
|
|
225
231
|
except (AttributeError, KeyError):
|
|
226
232
|
# Keep default value
|
|
227
233
|
pass
|
|
228
|
-
|
|
234
|
+
|
|
229
235
|
# Get all other column attributes safely
|
|
230
236
|
for key in dir(column):
|
|
231
237
|
if not key.startswith("__"):
|
|
@@ -234,8 +240,8 @@ class Result:
|
|
|
234
240
|
except (AttributeError, TypeError):
|
|
235
241
|
# Skip attributes that can't be accessed
|
|
236
242
|
continue
|
|
237
|
-
|
|
238
|
-
column_name = getattr(column,
|
|
243
|
+
|
|
244
|
+
column_name = getattr(column, "name", f"column_{len(self.__columns)}")
|
|
239
245
|
self.__columns[column_name] = data
|
|
240
246
|
return self.__columns
|
|
241
247
|
|
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
|
velocity/db/core/table.py
CHANGED
|
@@ -54,13 +54,13 @@ class Table:
|
|
|
54
54
|
"""
|
|
55
55
|
try:
|
|
56
56
|
self._cursor.close()
|
|
57
|
-
except:
|
|
57
|
+
except Exception:
|
|
58
58
|
pass
|
|
59
59
|
|
|
60
60
|
def cursor(self):
|
|
61
61
|
try:
|
|
62
62
|
return self._cursor
|
|
63
|
-
except:
|
|
63
|
+
except AttributeError:
|
|
64
64
|
pass
|
|
65
65
|
self._cursor = self.tx.cursor()
|
|
66
66
|
return self._cursor
|
|
@@ -287,7 +287,8 @@ class Table:
|
|
|
287
287
|
if use_where:
|
|
288
288
|
return Row(self, where, lock=lock)
|
|
289
289
|
return Row(self, result[0]["sys_id"], lock=lock)
|
|
290
|
-
|
|
290
|
+
|
|
291
|
+
one = find
|
|
291
292
|
|
|
292
293
|
@return_default(None)
|
|
293
294
|
def first(
|
|
@@ -886,8 +887,8 @@ class Table:
|
|
|
886
887
|
# Return a descriptive string of differences.
|
|
887
888
|
if differences:
|
|
888
889
|
differences.insert(0, f"Comparing {self.name}: {pk1} vs {pk2}")
|
|
889
|
-
differences.insert(0,
|
|
890
|
-
differences.append(
|
|
890
|
+
differences.insert(0, "--------------------------------------")
|
|
891
|
+
differences.append("--------------------------------------")
|
|
891
892
|
return "\n".join(differences)
|
|
892
893
|
else:
|
|
893
894
|
return f"{self.name} rows {pk1} and {pk2} are identical."
|