velocity-python 0.0.109__py3-none-any.whl → 0.0.161__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.
- velocity/__init__.py +3 -1
- velocity/app/orders.py +3 -4
- velocity/app/tests/__init__.py +1 -0
- velocity/app/tests/test_email_processing.py +112 -0
- velocity/app/tests/test_payment_profile_sorting.py +191 -0
- velocity/app/tests/test_spreadsheet_functions.py +124 -0
- 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 +248 -0
- velocity/aws/handlers/context.py +251 -2
- velocity/aws/handlers/exceptions.py +16 -0
- velocity/aws/handlers/lambda_handler.py +24 -85
- velocity/aws/handlers/mixins/__init__.py +16 -0
- velocity/aws/handlers/mixins/activity_tracker.py +181 -0
- velocity/aws/handlers/mixins/aws_session_mixin.py +192 -0
- velocity/aws/handlers/mixins/error_handler.py +192 -0
- velocity/aws/handlers/mixins/legacy_mixin.py +53 -0
- velocity/aws/handlers/mixins/standard_mixin.py +73 -0
- velocity/aws/handlers/response.py +1 -1
- velocity/aws/handlers/sqs_handler.py +28 -143
- velocity/aws/tests/__init__.py +1 -0
- velocity/aws/tests/test_lambda_handler_json_serialization.py +120 -0
- velocity/aws/tests/test_response.py +163 -0
- velocity/db/__init__.py +16 -4
- velocity/db/core/decorators.py +48 -13
- velocity/db/core/engine.py +187 -840
- velocity/db/core/result.py +33 -25
- velocity/db/core/row.py +15 -3
- velocity/db/core/table.py +493 -50
- velocity/db/core/transaction.py +28 -15
- velocity/db/exceptions.py +42 -18
- velocity/db/servers/base/__init__.py +9 -0
- velocity/db/servers/base/initializer.py +70 -0
- velocity/db/servers/base/operators.py +98 -0
- velocity/db/servers/base/sql.py +503 -0
- velocity/db/servers/base/types.py +135 -0
- velocity/db/servers/mysql/__init__.py +73 -0
- velocity/db/servers/mysql/operators.py +54 -0
- velocity/db/servers/{mysql_reserved.py → mysql/reserved.py} +2 -14
- velocity/db/servers/mysql/sql.py +718 -0
- velocity/db/servers/mysql/types.py +107 -0
- velocity/db/servers/postgres/__init__.py +59 -11
- velocity/db/servers/postgres/operators.py +34 -0
- velocity/db/servers/postgres/sql.py +474 -120
- velocity/db/servers/postgres/types.py +88 -2
- velocity/db/servers/sqlite/__init__.py +61 -0
- velocity/db/servers/sqlite/operators.py +52 -0
- velocity/db/servers/sqlite/reserved.py +20 -0
- velocity/db/servers/sqlite/sql.py +677 -0
- velocity/db/servers/sqlite/types.py +92 -0
- velocity/db/servers/sqlserver/__init__.py +73 -0
- velocity/db/servers/sqlserver/operators.py +47 -0
- velocity/db/servers/sqlserver/reserved.py +32 -0
- velocity/db/servers/sqlserver/sql.py +805 -0
- velocity/db/servers/sqlserver/types.py +114 -0
- velocity/db/servers/tablehelper.py +117 -91
- velocity/db/tests/__init__.py +1 -0
- velocity/db/tests/common_db_test.py +0 -0
- velocity/db/tests/postgres/__init__.py +1 -0
- velocity/db/tests/postgres/common.py +49 -0
- velocity/db/tests/postgres/test_column.py +29 -0
- velocity/db/tests/postgres/test_connections.py +25 -0
- velocity/db/tests/postgres/test_database.py +21 -0
- velocity/db/tests/postgres/test_engine.py +205 -0
- velocity/db/tests/postgres/test_general_usage.py +88 -0
- velocity/db/tests/postgres/test_imports.py +8 -0
- velocity/db/tests/postgres/test_result.py +19 -0
- velocity/db/tests/postgres/test_row.py +137 -0
- velocity/db/tests/postgres/test_row_comprehensive.py +720 -0
- velocity/db/tests/postgres/test_schema_locking.py +335 -0
- velocity/db/tests/postgres/test_schema_locking_unit.py +115 -0
- velocity/db/tests/postgres/test_sequence.py +34 -0
- velocity/db/tests/postgres/test_sql_comprehensive.py +462 -0
- velocity/db/tests/postgres/test_table.py +101 -0
- velocity/db/tests/postgres/test_table_comprehensive.py +646 -0
- velocity/db/tests/postgres/test_transaction.py +106 -0
- velocity/db/tests/sql/__init__.py +1 -0
- velocity/db/tests/sql/common.py +177 -0
- velocity/db/tests/sql/test_postgres_select_advanced.py +285 -0
- velocity/db/tests/sql/test_postgres_select_variances.py +517 -0
- velocity/db/tests/test_cursor_rowcount_fix.py +150 -0
- velocity/db/tests/test_db_utils.py +270 -0
- velocity/db/tests/test_postgres.py +448 -0
- velocity/db/tests/test_postgres_unchanged.py +81 -0
- velocity/db/tests/test_process_error_robustness.py +292 -0
- velocity/db/tests/test_result_caching.py +279 -0
- velocity/db/tests/test_result_sql_aware.py +117 -0
- velocity/db/tests/test_row_get_missing_column.py +72 -0
- velocity/db/tests/test_schema_locking_initializers.py +226 -0
- velocity/db/tests/test_schema_locking_simple.py +97 -0
- velocity/db/tests/test_sql_builder.py +165 -0
- velocity/db/tests/test_tablehelper.py +486 -0
- velocity/db/utils.py +129 -51
- 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/tests/__init__.py +1 -0
- velocity/misc/tests/test_db.py +90 -0
- velocity/misc/tests/test_fix.py +78 -0
- velocity/misc/tests/test_format.py +64 -0
- velocity/misc/tests/test_iconv.py +203 -0
- velocity/misc/tests/test_merge.py +82 -0
- velocity/misc/tests/test_oconv.py +144 -0
- velocity/misc/tests/test_original_error.py +52 -0
- velocity/misc/tests/test_timer.py +74 -0
- velocity/misc/tools.py +0 -1
- {velocity_python-0.0.109.dist-info → velocity_python-0.0.161.dist-info}/METADATA +2 -2
- velocity_python-0.0.161.dist-info/RECORD +129 -0
- velocity/db/core/exceptions.py +0 -70
- velocity/db/servers/mysql.py +0 -641
- velocity/db/servers/sqlite.py +0 -968
- velocity/db/servers/sqlite_reserved.py +0 -208
- velocity/db/servers/sqlserver.py +0 -921
- velocity/db/servers/sqlserver_reserved.py +0 -314
- velocity_python-0.0.109.dist-info/RECORD +0 -56
- {velocity_python-0.0.109.dist-info → velocity_python-0.0.161.dist-info}/WHEEL +0 -0
- {velocity_python-0.0.109.dist-info → velocity_python-0.0.161.dist-info}/licenses/LICENSE +0 -0
- {velocity_python-0.0.109.dist-info → velocity_python-0.0.161.dist-info}/top_level.txt +0 -0
velocity/db/core/engine.py
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import inspect
|
|
2
|
-
import sys
|
|
3
2
|
import re
|
|
4
|
-
import
|
|
3
|
+
from contextlib import contextmanager
|
|
5
4
|
from functools import wraps
|
|
6
5
|
from velocity.db import exceptions
|
|
7
6
|
from velocity.db.core.transaction import Transaction
|
|
7
|
+
from velocity.db.utils import mask_config_for_display
|
|
8
8
|
|
|
9
9
|
import logging
|
|
10
10
|
|
|
@@ -19,46 +19,16 @@ class Engine:
|
|
|
19
19
|
|
|
20
20
|
MAX_RETRIES = 100
|
|
21
21
|
|
|
22
|
-
def __init__(self, driver, config, sql, connect_timeout=5):
|
|
22
|
+
def __init__(self, driver, config, sql, connect_timeout=5, schema_locked=False):
|
|
23
23
|
self.__config = config
|
|
24
24
|
self.__sql = sql
|
|
25
25
|
self.__driver = driver
|
|
26
26
|
self.__connect_timeout = connect_timeout
|
|
27
|
-
|
|
28
|
-
# Set up error code mappings from the SQL class
|
|
29
|
-
self._setup_error_mappings()
|
|
30
|
-
|
|
31
|
-
def _setup_error_mappings(self):
|
|
32
|
-
"""
|
|
33
|
-
Set up error code to exception class mappings from the SQL driver.
|
|
34
|
-
"""
|
|
35
|
-
self.error_codes = {}
|
|
36
|
-
|
|
37
|
-
# Map error codes to exception class names
|
|
38
|
-
sql_attrs = [
|
|
39
|
-
('ApplicationErrorCodes', 'DbApplicationError'),
|
|
40
|
-
('DatabaseMissingErrorCodes', 'DbDatabaseMissingError'),
|
|
41
|
-
('TableMissingErrorCodes', 'DbTableMissingError'),
|
|
42
|
-
('ColumnMissingErrorCodes', 'DbColumnMissingError'),
|
|
43
|
-
('ForeignKeyMissingErrorCodes', 'DbForeignKeyMissingError'),
|
|
44
|
-
('ConnectionErrorCodes', 'DbConnectionError'),
|
|
45
|
-
('DuplicateKeyErrorCodes', 'DbDuplicateKeyError'),
|
|
46
|
-
('RetryTransactionCodes', 'DbRetryTransaction'),
|
|
47
|
-
('TruncationErrorCodes', 'DbTruncationError'),
|
|
48
|
-
('LockTimeoutErrorCodes', 'DbLockTimeoutError'),
|
|
49
|
-
('DatabaseObjectExistsErrorCodes', 'DbObjectExistsError'),
|
|
50
|
-
('DataIntegrityErrorCodes', 'DbDataIntegrityError')
|
|
51
|
-
]
|
|
52
|
-
|
|
53
|
-
for attr_name, exception_class in sql_attrs:
|
|
54
|
-
if hasattr(self.sql, attr_name):
|
|
55
|
-
codes = getattr(self.sql, attr_name)
|
|
56
|
-
if codes: # Only add non-empty lists
|
|
57
|
-
for code in codes:
|
|
58
|
-
self.error_codes[str(code)] = exception_class
|
|
27
|
+
self.__schema_locked = schema_locked
|
|
59
28
|
|
|
60
29
|
def __str__(self):
|
|
61
|
-
|
|
30
|
+
safe_config = mask_config_for_display(self.config)
|
|
31
|
+
return f"[{self.sql.server}] engine({safe_config})"
|
|
62
32
|
|
|
63
33
|
def connect(self):
|
|
64
34
|
"""
|
|
@@ -208,18 +178,18 @@ class Engine:
|
|
|
208
178
|
while True:
|
|
209
179
|
try:
|
|
210
180
|
return function(*args, **kwds)
|
|
211
|
-
except exceptions.DbRetryTransaction
|
|
181
|
+
except exceptions.DbRetryTransaction:
|
|
212
182
|
retry_count += 1
|
|
213
183
|
if retry_count > self.MAX_RETRIES:
|
|
214
184
|
raise
|
|
215
185
|
_tx.rollback()
|
|
216
|
-
except exceptions.DbLockTimeoutError
|
|
186
|
+
except exceptions.DbLockTimeoutError:
|
|
217
187
|
lock_timeout_count += 1
|
|
218
188
|
if lock_timeout_count > self.MAX_RETRIES:
|
|
219
189
|
raise
|
|
220
190
|
_tx.rollback()
|
|
221
191
|
continue
|
|
222
|
-
except:
|
|
192
|
+
except Exception:
|
|
223
193
|
raise
|
|
224
194
|
finally:
|
|
225
195
|
setattr(_tx, "_exec_function_depth", depth)
|
|
@@ -239,6 +209,29 @@ class Engine:
|
|
|
239
209
|
def sql(self):
|
|
240
210
|
return self.__sql
|
|
241
211
|
|
|
212
|
+
@property
|
|
213
|
+
def schema_locked(self):
|
|
214
|
+
"""Returns True if schema modifications are locked."""
|
|
215
|
+
return self.__schema_locked
|
|
216
|
+
|
|
217
|
+
def lock_schema(self):
|
|
218
|
+
"""Lock schema to prevent automatic modifications."""
|
|
219
|
+
self.__schema_locked = True
|
|
220
|
+
|
|
221
|
+
def unlock_schema(self):
|
|
222
|
+
"""Unlock schema to allow automatic modifications."""
|
|
223
|
+
self.__schema_locked = False
|
|
224
|
+
|
|
225
|
+
@contextmanager
|
|
226
|
+
def unlocked_schema(self):
|
|
227
|
+
"""Temporarily unlock schema for automatic creation."""
|
|
228
|
+
original_state = self.__schema_locked
|
|
229
|
+
self.__schema_locked = False
|
|
230
|
+
try:
|
|
231
|
+
yield self
|
|
232
|
+
finally:
|
|
233
|
+
self.__schema_locked = original_state
|
|
234
|
+
|
|
242
235
|
@property
|
|
243
236
|
def version(self):
|
|
244
237
|
"""
|
|
@@ -359,833 +352,187 @@ class Engine:
|
|
|
359
352
|
|
|
360
353
|
def process_error(self, exception, sql=None, parameters=None):
|
|
361
354
|
"""
|
|
362
|
-
|
|
363
|
-
Enhanced for robustness with exception chaining and comprehensive error handling.
|
|
364
|
-
|
|
365
|
-
Args:
|
|
366
|
-
exception: The original exception from the database driver
|
|
367
|
-
sql: The SQL statement that caused the error (optional)
|
|
368
|
-
parameters: The parameters passed to the SQL statement (optional)
|
|
369
|
-
|
|
370
|
-
Raises:
|
|
371
|
-
The appropriate velocity exception with proper chaining
|
|
355
|
+
Central method to parse driver exceptions and re-raise them as our custom exceptions.
|
|
372
356
|
"""
|
|
373
357
|
logger = logging.getLogger(__name__)
|
|
374
|
-
|
|
375
|
-
#
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
f" Exception Type: {type(exception).__name__}\n"
|
|
381
|
-
f" SQL Statement: {sql_preview}\n"
|
|
382
|
-
f" Processing error for classification..."
|
|
383
|
-
)
|
|
384
|
-
|
|
385
|
-
# Safely get error code and message with fallbacks
|
|
386
|
-
try:
|
|
387
|
-
# Try PostgreSQL-specific error code first, then use SQL driver's get_error method
|
|
388
|
-
error_code = getattr(exception, 'pgcode', None)
|
|
389
|
-
if not error_code and hasattr(self.sql, 'get_error'):
|
|
390
|
-
try:
|
|
391
|
-
error_code, error_message_from_driver = self.sql.get_error(exception)
|
|
392
|
-
if error_message_from_driver:
|
|
393
|
-
error_message = error_message_from_driver
|
|
394
|
-
except Exception as get_error_exception:
|
|
395
|
-
logger.warning(f"⚠️ SQL driver get_error failed: {get_error_exception}")
|
|
396
|
-
error_code = None
|
|
397
|
-
except Exception as e:
|
|
398
|
-
logger.warning(f"⚠️ Unable to extract database error code: {e}")
|
|
399
|
-
error_code = None
|
|
400
|
-
|
|
358
|
+
|
|
359
|
+
# If it's already a velocity exception, just re-raise it
|
|
360
|
+
if isinstance(exception, exceptions.DbException):
|
|
361
|
+
raise exception
|
|
362
|
+
|
|
363
|
+
# Get error code and message from the SQL driver
|
|
401
364
|
try:
|
|
402
|
-
error_message =
|
|
403
|
-
except Exception
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
connection_patterns = [
|
|
424
|
-
r'connection.*refused|could not connect',
|
|
425
|
-
r'network.*unreachable|network.*down',
|
|
426
|
-
r'broken pipe|connection.*broken',
|
|
427
|
-
r'timeout.*connection|connection.*timeout',
|
|
428
|
-
r'server.*closed.*connection|connection.*lost',
|
|
429
|
-
r'no route to host|host.*unreachable',
|
|
430
|
-
r'connection.*reset|reset.*connection'
|
|
431
|
-
]
|
|
432
|
-
|
|
433
|
-
# Enhanced duplicate key patterns
|
|
434
|
-
duplicate_patterns = [
|
|
435
|
-
r'duplicate.*key.*value|unique.*constraint.*violated',
|
|
436
|
-
r'duplicate.*entry|key.*already.*exists',
|
|
437
|
-
r'violates.*unique.*constraint',
|
|
438
|
-
r'unique.*violation|constraint.*unique'
|
|
439
|
-
]
|
|
440
|
-
|
|
441
|
-
# Enhanced permission/authorization patterns
|
|
442
|
-
permission_patterns = [
|
|
443
|
-
r'permission.*denied|access.*denied|authorization.*failed',
|
|
444
|
-
r'insufficient.*privileges|privilege.*denied',
|
|
445
|
-
r'not.*authorized|unauthorized.*access',
|
|
446
|
-
r'authentication.*failed|login.*failed'
|
|
447
|
-
]
|
|
448
|
-
|
|
449
|
-
# Enhanced database/table not found patterns
|
|
450
|
-
not_found_patterns = [
|
|
451
|
-
r'database.*does.*not.*exist|unknown.*database',
|
|
452
|
-
r'table.*does.*not.*exist|relation.*does.*not.*exist',
|
|
453
|
-
r'no.*such.*database|database.*not.*found',
|
|
454
|
-
r'schema.*does.*not.*exist|unknown.*table'
|
|
455
|
-
]
|
|
456
|
-
|
|
457
|
-
# Enhanced column missing patterns
|
|
458
|
-
column_missing_patterns = [
|
|
459
|
-
r'column.*does.*not.*exist',
|
|
460
|
-
r'unknown.*column|column.*not.*found',
|
|
461
|
-
r'no.*such.*column|invalid.*column.*name'
|
|
462
|
-
]
|
|
463
|
-
|
|
464
|
-
# Enhanced syntax error patterns
|
|
465
|
-
syntax_patterns = [
|
|
466
|
-
r'syntax.*error|invalid.*syntax',
|
|
467
|
-
r'malformed.*query|bad.*sql.*grammar',
|
|
468
|
-
r'unexpected.*token|parse.*error'
|
|
469
|
-
]
|
|
470
|
-
|
|
471
|
-
# Enhanced deadlock/timeout patterns
|
|
472
|
-
deadlock_patterns = [
|
|
473
|
-
r'deadlock.*detected|lock.*timeout',
|
|
474
|
-
r'timeout.*waiting.*for.*lock|query.*timeout',
|
|
475
|
-
r'lock.*wait.*timeout|deadlock.*found'
|
|
476
|
-
]
|
|
477
|
-
|
|
478
|
-
# Comprehensive pattern matching with error class mapping
|
|
479
|
-
pattern_mappings = [
|
|
480
|
-
(connection_patterns, 'DbConnectionError'),
|
|
481
|
-
(duplicate_patterns, 'DbDuplicateKeyError'),
|
|
482
|
-
(permission_patterns, 'DbPermissionError'),
|
|
483
|
-
(not_found_patterns, 'DbTableMissingError'),
|
|
484
|
-
(column_missing_patterns, 'DbColumnMissingError'),
|
|
485
|
-
(syntax_patterns, 'DbSyntaxError'),
|
|
486
|
-
(deadlock_patterns, 'DbDeadlockError')
|
|
487
|
-
]
|
|
488
|
-
|
|
489
|
-
# Apply pattern matching
|
|
490
|
-
for patterns, error_class in pattern_mappings:
|
|
491
|
-
for pattern in patterns:
|
|
492
|
-
try:
|
|
493
|
-
if re.search(pattern, error_message_lower):
|
|
494
|
-
logger.info(f"✅ Classified error by pattern match: '{pattern}' → {error_class}")
|
|
495
|
-
raise self._create_exception_with_chaining(
|
|
496
|
-
error_class, error_message, exception, sql, parameters
|
|
497
|
-
)
|
|
498
|
-
except re.error as regex_error:
|
|
499
|
-
logger.warning(f"⚠️ Regex pattern error for '{pattern}': {regex_error}")
|
|
500
|
-
continue
|
|
501
|
-
except Exception as pattern_error:
|
|
502
|
-
logger.error(f"❌ Error applying pattern '{pattern}': {pattern_error}")
|
|
503
|
-
continue
|
|
504
|
-
|
|
505
|
-
# Fallback: return generic database error with full context
|
|
506
|
-
available_codes = list(getattr(self, 'error_codes', {}).keys()) if hasattr(self, 'error_codes') else []
|
|
507
|
-
logger.warning(
|
|
508
|
-
f"⚠️ Unable to classify database error automatically\n"
|
|
509
|
-
f" → Falling back to generic DatabaseError\n"
|
|
510
|
-
f" → Error Code: {error_code or 'Unknown'}\n"
|
|
511
|
-
f" → Available Classifications: {available_codes or 'None configured'}"
|
|
512
|
-
)
|
|
513
|
-
|
|
514
|
-
raise self._create_exception_with_chaining(
|
|
515
|
-
'DatabaseError', error_message, exception, sql, parameters
|
|
516
|
-
)
|
|
517
|
-
|
|
518
|
-
def _format_human_readable_error(self, error_class, message, original_exception, sql=None, parameters=None, format_type='console'):
|
|
519
|
-
"""
|
|
520
|
-
Format a human-readable error message with proper context and formatting.
|
|
521
|
-
|
|
522
|
-
Args:
|
|
523
|
-
error_class: The name of the exception class
|
|
524
|
-
message: The raw error message
|
|
525
|
-
original_exception: The original exception
|
|
526
|
-
sql: The SQL statement (optional)
|
|
527
|
-
parameters: The SQL parameters (optional)
|
|
528
|
-
format_type: 'console' for terminal output, 'email' for HTML email format
|
|
529
|
-
|
|
530
|
-
Returns:
|
|
531
|
-
A nicely formatted, human-readable error message
|
|
532
|
-
"""
|
|
533
|
-
if format_type == 'email':
|
|
534
|
-
return self._format_email_error(error_class, message, original_exception, sql, parameters)
|
|
535
|
-
else:
|
|
536
|
-
return self._format_console_error(error_class, message, original_exception, sql, parameters)
|
|
537
|
-
|
|
538
|
-
def _format_console_error(self, error_class, message, original_exception, sql=None, parameters=None):
|
|
539
|
-
"""
|
|
540
|
-
Format error message for console/terminal output with Unicode box drawing.
|
|
541
|
-
"""
|
|
542
|
-
# Map error classes to user-friendly descriptions
|
|
543
|
-
error_descriptions = {
|
|
544
|
-
'DbColumnMissingError': 'Column Not Found',
|
|
545
|
-
'DbTableMissingError': 'Table Not Found',
|
|
546
|
-
'DbDatabaseMissingError': 'Database Not Found',
|
|
547
|
-
'DbForeignKeyMissingError': 'Foreign Key Constraint Violation',
|
|
548
|
-
'DbDuplicateKeyError': 'Duplicate Key Violation',
|
|
549
|
-
'DbConnectionError': 'Database Connection Failed',
|
|
550
|
-
'DbDataIntegrityError': 'Data Integrity Violation',
|
|
551
|
-
'DbQueryError': 'Query Execution Error',
|
|
552
|
-
'DbTransactionError': 'Transaction Error',
|
|
553
|
-
'DbTruncationError': 'Data Truncation Error',
|
|
554
|
-
'DatabaseError': 'Database Error'
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
# Get user-friendly error type
|
|
558
|
-
friendly_type = error_descriptions.get(error_class, error_class.replace('Db', '').replace('Error', ' Error'))
|
|
559
|
-
|
|
560
|
-
# Clean up the original message
|
|
561
|
-
clean_message = str(message).strip()
|
|
562
|
-
|
|
563
|
-
# Extract specific details from PostgreSQL errors
|
|
564
|
-
details = self._extract_error_details(original_exception, clean_message)
|
|
565
|
-
|
|
566
|
-
# Build the formatted message
|
|
567
|
-
lines = []
|
|
568
|
-
lines.append(f"╭─ {friendly_type} ─" + "─" * max(0, 60 - len(friendly_type)))
|
|
569
|
-
lines.append("│")
|
|
570
|
-
|
|
571
|
-
# Add the main error description
|
|
572
|
-
if details.get('description'):
|
|
573
|
-
lines.append(f"│ {details['description']}")
|
|
574
|
-
else:
|
|
575
|
-
lines.append(f"│ {clean_message}")
|
|
576
|
-
lines.append("│")
|
|
577
|
-
|
|
578
|
-
# Add error code if available
|
|
579
|
-
error_code = getattr(original_exception, 'pgcode', None)
|
|
580
|
-
if error_code:
|
|
581
|
-
lines.append(f"│ Error Code: {error_code}")
|
|
582
|
-
|
|
583
|
-
# Add specific details if available
|
|
584
|
-
if details.get('column'):
|
|
585
|
-
lines.append(f"│ Column: {details['column']}")
|
|
586
|
-
if details.get('table'):
|
|
587
|
-
lines.append(f"│ Table: {details['table']}")
|
|
588
|
-
if details.get('constraint'):
|
|
589
|
-
lines.append(f"│ Constraint: {details['constraint']}")
|
|
590
|
-
if details.get('hint'):
|
|
591
|
-
lines.append(f"│ Hint: {details['hint']}")
|
|
365
|
+
error_code, error_message = self.sql.get_error(exception)
|
|
366
|
+
except Exception:
|
|
367
|
+
error_code, error_message = None, str(exception)
|
|
368
|
+
|
|
369
|
+
msg = str(exception).strip().lower()
|
|
370
|
+
|
|
371
|
+
# Create enhanced error message with SQL query and context
|
|
372
|
+
enhanced_message = str(exception)
|
|
373
|
+
|
|
374
|
+
# Add specific guidance for common WHERE clause errors
|
|
375
|
+
exception_str_lower = str(exception).lower()
|
|
376
|
+
if "argument of where must be type boolean" in exception_str_lower:
|
|
377
|
+
enhanced_message += (
|
|
378
|
+
"\n\n*** WHERE CLAUSE ERROR ***\n"
|
|
379
|
+
"This error typically occurs when a WHERE clause contains a bare value "
|
|
380
|
+
"instead of a proper boolean expression.\n"
|
|
381
|
+
"Common fixes:\n"
|
|
382
|
+
" - Change WHERE 1001 to WHERE sys_id = 1001\n"
|
|
383
|
+
" - Change WHERE {'column': value} format in dictionaries\n"
|
|
384
|
+
" - Ensure string WHERE clauses are complete SQL expressions"
|
|
385
|
+
)
|
|
592
386
|
|
|
593
|
-
# Add SQL context if available
|
|
594
387
|
if sql:
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
for line in sql.split('\n'):
|
|
599
|
-
lines.append(f"│ {line.strip()}")
|
|
600
|
-
|
|
601
|
-
# Add parameters if available
|
|
602
|
-
if parameters is not None:
|
|
603
|
-
lines.append("│")
|
|
604
|
-
lines.append(f"│ Parameters: {parameters}")
|
|
605
|
-
|
|
606
|
-
# Add debugging section with copy-paste ready format
|
|
607
|
-
if sql or parameters is not None:
|
|
608
|
-
lines.append("│")
|
|
609
|
-
lines.append("│ ┌─ DEBUG COPY-PASTE SECTION ─────────────────────────────")
|
|
610
|
-
|
|
611
|
-
if sql and parameters is not None:
|
|
612
|
-
# Format for direct copy-paste into database console
|
|
613
|
-
lines.append("│ │")
|
|
614
|
-
lines.append("│ │ Complete SQL with Parameters:")
|
|
615
|
-
lines.append("│ │ " + "─" * 45)
|
|
616
|
-
|
|
617
|
-
# Show the raw SQL
|
|
618
|
-
lines.append("│ │ Raw SQL:")
|
|
619
|
-
for line in sql.split('\n'):
|
|
620
|
-
lines.append(f"│ │ {line}")
|
|
621
|
-
|
|
622
|
-
lines.append("│ │")
|
|
623
|
-
lines.append(f"│ │ Raw Parameters: {parameters}")
|
|
624
|
-
|
|
625
|
-
# Try to create an executable version
|
|
626
|
-
lines.append("│ │")
|
|
627
|
-
lines.append("│ │ Executable Format (for PostgreSQL):")
|
|
628
|
-
lines.append("│ │ " + "─" * 35)
|
|
629
|
-
|
|
630
|
-
try:
|
|
631
|
-
# Create a version with parameters substituted for testing
|
|
632
|
-
executable_sql = self._format_executable_sql(sql, parameters)
|
|
633
|
-
for line in executable_sql.split('\n'):
|
|
634
|
-
lines.append(f"│ │ {line}")
|
|
635
|
-
except Exception:
|
|
636
|
-
lines.append("│ │ [Unable to format executable SQL]")
|
|
637
|
-
for line in sql.split('\n'):
|
|
638
|
-
lines.append(f"│ │ {line}")
|
|
639
|
-
lines.append(f"│ │ -- Parameters: {parameters}")
|
|
640
|
-
|
|
641
|
-
elif sql:
|
|
642
|
-
lines.append("│ │")
|
|
643
|
-
lines.append("│ │ SQL Statement (no parameters):")
|
|
644
|
-
lines.append("│ │ " + "─" * 30)
|
|
645
|
-
for line in sql.split('\n'):
|
|
646
|
-
lines.append(f"│ │ {line}")
|
|
647
|
-
|
|
648
|
-
lines.append("│ │")
|
|
649
|
-
lines.append("│ └─────────────────────────────────────────────────────────")
|
|
650
|
-
|
|
651
|
-
# Add detailed call stack information for debugging
|
|
652
|
-
stack_info = self._extract_call_stack_info()
|
|
653
|
-
if stack_info:
|
|
654
|
-
lines.append("│")
|
|
655
|
-
lines.append("│ ┌─ CALL STACK ANALYSIS ──────────────────────────────────")
|
|
656
|
-
lines.append("│ │")
|
|
657
|
-
|
|
658
|
-
if stack_info.get('top_level_call'):
|
|
659
|
-
lines.append("│ │ Top-Level Function (most helpful for debugging):")
|
|
660
|
-
lines.append("│ │ " + "─" * 48)
|
|
661
|
-
call = stack_info['top_level_call']
|
|
662
|
-
lines.append(f"│ │ Function: {call['function']}")
|
|
663
|
-
lines.append(f"│ │ File: {call['file']}")
|
|
664
|
-
lines.append(f"│ │ Line: {call['line']}")
|
|
665
|
-
if call.get('code'):
|
|
666
|
-
lines.append(f"│ │ Code: {call['code'].strip()}")
|
|
667
|
-
|
|
668
|
-
if stack_info.get('relevant_calls'):
|
|
669
|
-
lines.append("│ │")
|
|
670
|
-
lines.append("│ │ Relevant Call Chain (excluding middleware):")
|
|
671
|
-
lines.append("│ │ " + "─" * 44)
|
|
672
|
-
for i, call in enumerate(stack_info['relevant_calls'][:5], 1): # Show top 5
|
|
673
|
-
lines.append(f"│ │ {i}. {call['function']} in {call['file']}:{call['line']}")
|
|
674
|
-
if call.get('code'):
|
|
675
|
-
lines.append(f"│ │ → {call['code'].strip()}")
|
|
676
|
-
|
|
677
|
-
if stack_info.get('lambda_context'):
|
|
678
|
-
lines.append("│ │")
|
|
679
|
-
lines.append("│ │ AWS Lambda Context:")
|
|
680
|
-
lines.append("│ │ " + "─" * 19)
|
|
681
|
-
for key, value in stack_info['lambda_context'].items():
|
|
682
|
-
lines.append(f"│ │ {key}: {value}")
|
|
388
|
+
enhanced_message += (
|
|
389
|
+
f"\n\nSQL Query:\n{self._format_sql_with_params(sql, parameters)}"
|
|
390
|
+
)
|
|
683
391
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
392
|
+
# Add call stack context for better debugging
|
|
393
|
+
import traceback
|
|
394
|
+
stack_trace = traceback.format_stack()
|
|
395
|
+
# Get the last few frames that aren't in the error handling itself
|
|
396
|
+
relevant_frames = [frame for frame in stack_trace if 'process_error' not in frame and 'logging' not in frame][-3:]
|
|
397
|
+
if relevant_frames:
|
|
398
|
+
enhanced_message += "\n\nCall Context:\n" + "".join(relevant_frames)
|
|
399
|
+
|
|
400
|
+
# Format SQL for logging
|
|
401
|
+
formatted_sql_info = ""
|
|
402
|
+
if sql:
|
|
403
|
+
formatted_sql_info = f" sql={self._format_sql_with_params(sql, parameters)}"
|
|
404
|
+
|
|
405
|
+
# logger.warning(
|
|
406
|
+
# "Database error caught. Attempting to transform: code=%s message=%s%s",
|
|
407
|
+
# error_code,
|
|
408
|
+
# error_message,
|
|
409
|
+
# formatted_sql_info,
|
|
410
|
+
# )
|
|
411
|
+
|
|
412
|
+
# Direct error code mapping
|
|
413
|
+
if error_code in self.sql.ApplicationErrorCodes:
|
|
414
|
+
raise exceptions.DbApplicationError(enhanced_message) from exception
|
|
415
|
+
if error_code in self.sql.ColumnMissingErrorCodes:
|
|
416
|
+
raise exceptions.DbColumnMissingError(enhanced_message) from exception
|
|
417
|
+
if error_code in self.sql.TableMissingErrorCodes:
|
|
418
|
+
raise exceptions.DbTableMissingError(enhanced_message) from exception
|
|
419
|
+
if error_code in self.sql.DatabaseMissingErrorCodes:
|
|
420
|
+
raise exceptions.DbDatabaseMissingError(enhanced_message) from exception
|
|
421
|
+
if error_code in self.sql.ForeignKeyMissingErrorCodes:
|
|
422
|
+
raise exceptions.DbForeignKeyMissingError(enhanced_message) from exception
|
|
423
|
+
if error_code in self.sql.TruncationErrorCodes:
|
|
424
|
+
raise exceptions.DbTruncationError(enhanced_message) from exception
|
|
425
|
+
if error_code in self.sql.DataIntegrityErrorCodes:
|
|
426
|
+
raise exceptions.DbDataIntegrityError(enhanced_message) from exception
|
|
427
|
+
if error_code in self.sql.ConnectionErrorCodes:
|
|
428
|
+
raise exceptions.DbConnectionError(enhanced_message) from exception
|
|
429
|
+
if error_code in self.sql.DuplicateKeyErrorCodes:
|
|
430
|
+
raise exceptions.DbDuplicateKeyError(enhanced_message) from exception
|
|
431
|
+
if error_code in self.sql.DatabaseObjectExistsErrorCodes:
|
|
432
|
+
raise exceptions.DbObjectExistsError(enhanced_message) from exception
|
|
433
|
+
if error_code in self.sql.LockTimeoutErrorCodes:
|
|
434
|
+
raise exceptions.DbLockTimeoutError(enhanced_message) from exception
|
|
435
|
+
if error_code in self.sql.RetryTransactionCodes:
|
|
436
|
+
raise exceptions.DbRetryTransaction(enhanced_message) from exception
|
|
437
|
+
|
|
438
|
+
# Regex-based fallback patterns
|
|
439
|
+
if re.search(r"key \(sys_id\)=\(\d+\) already exists.", msg, re.M):
|
|
440
|
+
raise exceptions.DbDuplicateKeyError(enhanced_message) from exception
|
|
441
|
+
if re.findall(r"database.*does not exist", msg, re.M):
|
|
442
|
+
raise exceptions.DbDatabaseMissingError(enhanced_message) from exception
|
|
443
|
+
if re.findall(r"no such database", msg, re.M):
|
|
444
|
+
raise exceptions.DbDatabaseMissingError(enhanced_message) from exception
|
|
445
|
+
if re.findall(r"already exists", msg, re.M):
|
|
446
|
+
raise exceptions.DbObjectExistsError(enhanced_message) from exception
|
|
447
|
+
if re.findall(r"server closed the connection unexpectedly", msg, re.M):
|
|
448
|
+
raise exceptions.DbConnectionError(enhanced_message) from exception
|
|
449
|
+
if re.findall(r"no connection to the server", msg, re.M):
|
|
450
|
+
raise exceptions.DbConnectionError(enhanced_message) from exception
|
|
451
|
+
if re.findall(r"connection timed out", msg, re.M):
|
|
452
|
+
raise exceptions.DbConnectionError(enhanced_message) from exception
|
|
453
|
+
if re.findall(r"could not connect to server", msg, re.M):
|
|
454
|
+
raise exceptions.DbConnectionError(enhanced_message) from exception
|
|
455
|
+
if re.findall(r"cannot connect to server", msg, re.M):
|
|
456
|
+
raise exceptions.DbConnectionError(enhanced_message) from exception
|
|
457
|
+
if re.findall(r"connection already closed", msg, re.M):
|
|
458
|
+
raise exceptions.DbConnectionError(enhanced_message) from exception
|
|
459
|
+
if re.findall(r"cursor already closed", msg, re.M):
|
|
460
|
+
raise exceptions.DbConnectionError(enhanced_message) from exception
|
|
461
|
+
if "no such table:" in msg:
|
|
462
|
+
raise exceptions.DbTableMissingError(enhanced_message) from exception
|
|
691
463
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
'DbTruncationError': 'Data Truncation Error',
|
|
708
|
-
'DatabaseError': 'Database Error'
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
# Get user-friendly error type
|
|
712
|
-
friendly_type = error_descriptions.get(error_class, error_class.replace('Db', '').replace('Error', ' Error'))
|
|
713
|
-
|
|
714
|
-
# Clean up the original message
|
|
715
|
-
clean_message = str(message).strip()
|
|
716
|
-
|
|
717
|
-
# Extract specific details from PostgreSQL errors
|
|
718
|
-
details = self._extract_error_details(original_exception, clean_message)
|
|
719
|
-
|
|
720
|
-
# Get error code
|
|
721
|
-
error_code = getattr(original_exception, 'pgcode', None)
|
|
722
|
-
|
|
723
|
-
# Get stack info
|
|
724
|
-
stack_info = self._extract_call_stack_info()
|
|
725
|
-
|
|
726
|
-
# Build HTML email format
|
|
727
|
-
html_parts = []
|
|
728
|
-
|
|
729
|
-
# Email header
|
|
730
|
-
html_parts.append("""
|
|
731
|
-
<html>
|
|
732
|
-
<head>
|
|
733
|
-
<style>
|
|
734
|
-
body { font-family: 'Courier New', monospace; margin: 20px; }
|
|
735
|
-
.error-container { border: 2px solid #dc3545; border-radius: 8px; padding: 20px; background-color: #f8f9fa; }
|
|
736
|
-
.error-header { background-color: #dc3545; color: white; padding: 10px; border-radius: 5px; font-weight: bold; font-size: 16px; margin-bottom: 15px; }
|
|
737
|
-
.error-section { margin: 15px 0; padding: 10px; background-color: #ffffff; border-left: 4px solid #007bff; }
|
|
738
|
-
.section-title { font-weight: bold; color: #007bff; margin-bottom: 8px; }
|
|
739
|
-
.code-block { background-color: #f1f3f4; padding: 10px; border-radius: 4px; font-family: 'Courier New', monospace; margin: 5px 0; white-space: pre-wrap; }
|
|
740
|
-
.highlight { background-color: #fff3cd; padding: 2px 4px; border-radius: 3px; }
|
|
741
|
-
.stack-call { margin: 5px 0; padding: 5px; background-color: #e9ecef; border-radius: 3px; }
|
|
742
|
-
.copy-section { background-color: #d1ecf1; border: 1px solid #bee5eb; padding: 15px; border-radius: 5px; margin: 10px 0; }
|
|
743
|
-
</style>
|
|
744
|
-
</head>
|
|
745
|
-
<body>
|
|
746
|
-
<div class="error-container">
|
|
747
|
-
""")
|
|
748
|
-
|
|
749
|
-
# Error header
|
|
750
|
-
html_parts.append(f' <div class="error-header">🚨 {friendly_type}</div>')
|
|
751
|
-
|
|
752
|
-
# Main error description
|
|
753
|
-
description = details.get('description', clean_message)
|
|
754
|
-
html_parts.append(f' <div class="error-section"><strong>{description}</strong></div>')
|
|
755
|
-
|
|
756
|
-
# Error details section
|
|
757
|
-
if error_code or details.get('column') or details.get('table') or details.get('constraint'):
|
|
758
|
-
html_parts.append(' <div class="error-section">')
|
|
759
|
-
html_parts.append(' <div class="section-title">Error Details:</div>')
|
|
760
|
-
if error_code:
|
|
761
|
-
html_parts.append(f' <div><strong>Error Code:</strong> <span class="highlight">{error_code}</span></div>')
|
|
762
|
-
if details.get('column'):
|
|
763
|
-
html_parts.append(f' <div><strong>Column:</strong> <span class="highlight">{details["column"]}</span></div>')
|
|
764
|
-
if details.get('table'):
|
|
765
|
-
html_parts.append(f' <div><strong>Table:</strong> <span class="highlight">{details["table"]}</span></div>')
|
|
766
|
-
if details.get('constraint'):
|
|
767
|
-
html_parts.append(f' <div><strong>Constraint:</strong> <span class="highlight">{details["constraint"]}</span></div>')
|
|
768
|
-
if details.get('hint'):
|
|
769
|
-
html_parts.append(f' <div><strong>Hint:</strong> {details["hint"]}</div>')
|
|
770
|
-
html_parts.append(' </div>')
|
|
771
|
-
|
|
772
|
-
# SQL and Parameters section
|
|
773
|
-
if sql or parameters is not None:
|
|
774
|
-
html_parts.append(' <div class="copy-section">')
|
|
775
|
-
html_parts.append(' <div class="section-title">📋 Debug Information (Copy-Paste Ready)</div>')
|
|
776
|
-
|
|
777
|
-
if sql:
|
|
778
|
-
html_parts.append(' <div><strong>SQL Statement:</strong></div>')
|
|
779
|
-
html_parts.append(f' <div class="code-block">{self._html_escape(sql)}</div>')
|
|
780
|
-
|
|
781
|
-
if parameters is not None:
|
|
782
|
-
html_parts.append(f' <div><strong>Parameters:</strong> <code>{self._html_escape(str(parameters))}</code></div>')
|
|
783
|
-
|
|
784
|
-
# Executable SQL
|
|
785
|
-
if sql and parameters is not None:
|
|
786
|
-
try:
|
|
787
|
-
executable_sql = self._format_executable_sql(sql, parameters)
|
|
788
|
-
html_parts.append(' <div><strong>Executable SQL (for testing):</strong></div>')
|
|
789
|
-
html_parts.append(f' <div class="code-block">{self._html_escape(executable_sql)}</div>')
|
|
790
|
-
except Exception:
|
|
791
|
-
pass
|
|
792
|
-
|
|
793
|
-
html_parts.append(' </div>')
|
|
794
|
-
|
|
795
|
-
# Call stack section
|
|
796
|
-
if stack_info and stack_info.get('top_level_call'):
|
|
797
|
-
html_parts.append(' <div class="error-section">')
|
|
798
|
-
html_parts.append(' <div class="section-title">🔍 Source Code Location</div>')
|
|
799
|
-
|
|
800
|
-
call = stack_info['top_level_call']
|
|
801
|
-
html_parts.append(' <div class="stack-call">')
|
|
802
|
-
html_parts.append(f' <strong>Function:</strong> {call["function"]}<br>')
|
|
803
|
-
html_parts.append(f' <strong>File:</strong> {call["file"]}<br>')
|
|
804
|
-
html_parts.append(f' <strong>Line:</strong> {call["line"]}')
|
|
805
|
-
if call.get('code'):
|
|
806
|
-
html_parts.append(f'<br> <strong>Code:</strong> <code>{self._html_escape(call["code"].strip())}</code>')
|
|
807
|
-
html_parts.append(' </div>')
|
|
808
|
-
|
|
809
|
-
# Show relevant call chain
|
|
810
|
-
if stack_info.get('relevant_calls') and len(stack_info['relevant_calls']) > 1:
|
|
811
|
-
html_parts.append(' <div><strong>Call Chain:</strong></div>')
|
|
812
|
-
for i, call in enumerate(stack_info['relevant_calls'][:4], 1):
|
|
813
|
-
html_parts.append(' <div class="stack-call">')
|
|
814
|
-
html_parts.append(f' {i}. <strong>{call["function"]}</strong> in {call["file"]}:{call["line"]}')
|
|
815
|
-
html_parts.append(' </div>')
|
|
816
|
-
|
|
817
|
-
html_parts.append(' </div>')
|
|
818
|
-
|
|
819
|
-
# Lambda context
|
|
820
|
-
if stack_info and stack_info.get('lambda_context'):
|
|
821
|
-
html_parts.append(' <div class="error-section">')
|
|
822
|
-
html_parts.append(' <div class="section-title">⚡ AWS Lambda Context</div>')
|
|
823
|
-
for key, value in stack_info['lambda_context'].items():
|
|
824
|
-
html_parts.append(f' <div><strong>{key.title()}:</strong> {value}</div>')
|
|
825
|
-
html_parts.append(' </div>')
|
|
826
|
-
|
|
827
|
-
# Email footer
|
|
828
|
-
html_parts.append("""
|
|
829
|
-
</div>
|
|
830
|
-
</body>
|
|
831
|
-
</html>
|
|
832
|
-
""")
|
|
833
|
-
|
|
834
|
-
return ''.join(html_parts)
|
|
835
|
-
|
|
836
|
-
def _html_escape(self, text):
|
|
837
|
-
"""Escape HTML special characters."""
|
|
838
|
-
if not text:
|
|
839
|
-
return ""
|
|
840
|
-
return (str(text)
|
|
841
|
-
.replace('&', '&')
|
|
842
|
-
.replace('<', '<')
|
|
843
|
-
.replace('>', '>')
|
|
844
|
-
.replace('"', '"')
|
|
845
|
-
.replace("'", '''))
|
|
846
|
-
|
|
847
|
-
def _format_executable_sql(self, sql, parameters):
|
|
464
|
+
logger.error(
|
|
465
|
+
"Unhandled/Unknown Error in engine.process_error",
|
|
466
|
+
exc_info=True,
|
|
467
|
+
extra={
|
|
468
|
+
"error_code": error_code,
|
|
469
|
+
"error_msg": error_message,
|
|
470
|
+
"sql_stmt": sql,
|
|
471
|
+
"sql_params": parameters,
|
|
472
|
+
},
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
# If we can't classify it, re-raise with enhanced message
|
|
476
|
+
raise type(exception)(enhanced_message) from exception
|
|
477
|
+
|
|
478
|
+
def _format_sql_with_params(self, sql, parameters):
|
|
848
479
|
"""
|
|
849
|
-
Format SQL with parameters
|
|
850
|
-
|
|
851
|
-
Args:
|
|
852
|
-
sql: The SQL statement with placeholders
|
|
853
|
-
parameters: The parameters to substitute
|
|
854
|
-
|
|
855
|
-
Returns:
|
|
856
|
-
SQL statement with parameters properly formatted for execution
|
|
480
|
+
Format SQL query with parameters merged for easy copy-paste debugging.
|
|
857
481
|
"""
|
|
482
|
+
if not sql:
|
|
483
|
+
return "No SQL provided"
|
|
484
|
+
|
|
858
485
|
if not parameters:
|
|
859
486
|
return sql
|
|
860
|
-
|
|
487
|
+
|
|
861
488
|
try:
|
|
862
489
|
# Handle different parameter formats
|
|
863
490
|
if isinstance(parameters, (list, tuple)):
|
|
864
|
-
#
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
# Replace %s placeholders with properly formatted values
|
|
491
|
+
# Convert parameters to strings and handle None values
|
|
492
|
+
formatted_params = []
|
|
868
493
|
for param in parameters:
|
|
869
|
-
if
|
|
494
|
+
if param is None:
|
|
495
|
+
formatted_params.append("NULL")
|
|
496
|
+
elif isinstance(param, str):
|
|
870
497
|
# Escape single quotes and wrap in quotes
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
formatted_param = str(param)
|
|
874
|
-
elif param is None:
|
|
875
|
-
formatted_param = 'NULL'
|
|
498
|
+
escaped = param.replace("'", "''")
|
|
499
|
+
formatted_params.append(f"'{escaped}'")
|
|
876
500
|
elif isinstance(param, bool):
|
|
877
|
-
|
|
501
|
+
formatted_params.append("TRUE" if param else "FALSE")
|
|
878
502
|
else:
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
503
|
+
formatted_params.append(str(param))
|
|
504
|
+
|
|
505
|
+
# Replace %s placeholders with actual values
|
|
506
|
+
formatted_sql = sql
|
|
507
|
+
for param in formatted_params:
|
|
508
|
+
formatted_sql = formatted_sql.replace("%s", param, 1)
|
|
509
|
+
|
|
885
510
|
return formatted_sql
|
|
886
|
-
|
|
511
|
+
|
|
887
512
|
elif isinstance(parameters, dict):
|
|
888
|
-
#
|
|
513
|
+
# Handle named parameters
|
|
889
514
|
formatted_sql = sql
|
|
890
515
|
for key, value in parameters.items():
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
elif value is None:
|
|
897
|
-
formatted_value = 'NULL'
|
|
516
|
+
if value is None:
|
|
517
|
+
replacement = "NULL"
|
|
518
|
+
elif isinstance(value, str):
|
|
519
|
+
escaped = value.replace("'", "''")
|
|
520
|
+
replacement = f"'{escaped}'"
|
|
898
521
|
elif isinstance(value, bool):
|
|
899
|
-
|
|
522
|
+
replacement = "TRUE" if value else "FALSE"
|
|
900
523
|
else:
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
524
|
+
replacement = str(value)
|
|
525
|
+
|
|
526
|
+
# Replace %(key)s or :key patterns
|
|
527
|
+
formatted_sql = formatted_sql.replace(f"%({key})s", replacement)
|
|
528
|
+
formatted_sql = formatted_sql.replace(f":{key}", replacement)
|
|
529
|
+
|
|
905
530
|
return formatted_sql
|
|
906
|
-
|
|
907
531
|
else:
|
|
908
|
-
# Fallback: just append parameters as comment
|
|
909
532
|
return f"{sql}\n-- Parameters: {parameters}"
|
|
910
|
-
|
|
911
|
-
except Exception:
|
|
912
|
-
# If formatting fails, return original with parameters as comment
|
|
913
|
-
return f"{sql}\n-- Parameters: {parameters}"
|
|
914
533
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
Returns:
|
|
921
|
-
Dictionary with call stack analysis
|
|
922
|
-
"""
|
|
923
|
-
import traceback
|
|
924
|
-
import os
|
|
925
|
-
|
|
926
|
-
try:
|
|
927
|
-
# Get the current stack
|
|
928
|
-
stack = traceback.extract_stack()
|
|
929
|
-
|
|
930
|
-
# Common middleware/decorator patterns to filter out
|
|
931
|
-
middleware_patterns = [
|
|
932
|
-
'decorator',
|
|
933
|
-
'wrapper',
|
|
934
|
-
'new_function',
|
|
935
|
-
'exec_function',
|
|
936
|
-
'_execute',
|
|
937
|
-
'process_error',
|
|
938
|
-
'_create_exception',
|
|
939
|
-
'_format_human_readable',
|
|
940
|
-
'__enter__',
|
|
941
|
-
'__exit__',
|
|
942
|
-
'contextlib',
|
|
943
|
-
'functools'
|
|
944
|
-
]
|
|
945
|
-
|
|
946
|
-
# Lambda/AWS specific patterns
|
|
947
|
-
lambda_patterns = [
|
|
948
|
-
'lambda_handler',
|
|
949
|
-
'handler',
|
|
950
|
-
'bootstrap',
|
|
951
|
-
'runtime'
|
|
952
|
-
]
|
|
953
|
-
|
|
954
|
-
relevant_calls = []
|
|
955
|
-
top_level_call = None
|
|
956
|
-
lambda_context = {}
|
|
957
|
-
|
|
958
|
-
# Analyze stack frames from top to bottom (most recent first)
|
|
959
|
-
for frame in reversed(stack[:-4]): # Skip the last few frames (this method, etc.)
|
|
960
|
-
file_path = frame.filename
|
|
961
|
-
function_name = frame.name
|
|
962
|
-
line_number = frame.lineno
|
|
963
|
-
code_line = frame.line or ""
|
|
964
|
-
|
|
965
|
-
# Extract just the filename
|
|
966
|
-
filename = os.path.basename(file_path)
|
|
967
|
-
|
|
968
|
-
# Skip internal Python and library frames
|
|
969
|
-
if any(skip in file_path.lower() for skip in [
|
|
970
|
-
'python3', 'site-packages', '/usr/', '/opt/python',
|
|
971
|
-
'psycopg2', 'boto3', 'botocore'
|
|
972
|
-
]):
|
|
973
|
-
continue
|
|
974
|
-
|
|
975
|
-
# Capture Lambda context if found
|
|
976
|
-
if any(pattern in function_name.lower() for pattern in lambda_patterns):
|
|
977
|
-
lambda_context.update({
|
|
978
|
-
'handler': function_name,
|
|
979
|
-
'file': filename,
|
|
980
|
-
'line': line_number
|
|
981
|
-
})
|
|
982
|
-
|
|
983
|
-
# Skip middleware/decorator frames but keep track of meaningful ones
|
|
984
|
-
is_middleware = any(pattern in function_name.lower() for pattern in middleware_patterns)
|
|
985
|
-
|
|
986
|
-
if not is_middleware:
|
|
987
|
-
call_info = {
|
|
988
|
-
'function': function_name,
|
|
989
|
-
'file': filename,
|
|
990
|
-
'line': line_number,
|
|
991
|
-
'code': code_line,
|
|
992
|
-
'full_path': file_path
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
relevant_calls.append(call_info)
|
|
996
|
-
|
|
997
|
-
# The first non-middleware call is likely the most important
|
|
998
|
-
if not top_level_call:
|
|
999
|
-
# Look for application-level calls (not in velocity internals)
|
|
1000
|
-
if not any(internal in file_path for internal in [
|
|
1001
|
-
'velocity/db/', 'velocity/aws/', 'velocity/misc/'
|
|
1002
|
-
]):
|
|
1003
|
-
top_level_call = call_info
|
|
1004
|
-
|
|
1005
|
-
# If no clear top-level call found, use the first relevant call
|
|
1006
|
-
if not top_level_call and relevant_calls:
|
|
1007
|
-
top_level_call = relevant_calls[0]
|
|
1008
|
-
|
|
1009
|
-
# Look for handler functions in the stack
|
|
1010
|
-
handler_calls = [call for call in relevant_calls
|
|
1011
|
-
if any(pattern in call['function'].lower()
|
|
1012
|
-
for pattern in ['handler', 'main', 'process', 'action'])]
|
|
1013
|
-
|
|
1014
|
-
if handler_calls and not top_level_call:
|
|
1015
|
-
top_level_call = handler_calls[0]
|
|
1016
|
-
|
|
1017
|
-
return {
|
|
1018
|
-
'top_level_call': top_level_call,
|
|
1019
|
-
'relevant_calls': relevant_calls,
|
|
1020
|
-
'lambda_context': lambda_context if lambda_context else None
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
except Exception:
|
|
1024
|
-
# If stack analysis fails, return minimal info
|
|
1025
|
-
return None
|
|
1026
|
-
|
|
1027
|
-
def _extract_error_details(self, exception, message):
|
|
1028
|
-
"""
|
|
1029
|
-
Extract specific details from database errors for better formatting.
|
|
1030
|
-
|
|
1031
|
-
Args:
|
|
1032
|
-
exception: The original database exception
|
|
1033
|
-
message: The error message string
|
|
1034
|
-
|
|
1035
|
-
Returns:
|
|
1036
|
-
Dictionary with extracted details
|
|
1037
|
-
"""
|
|
1038
|
-
import re
|
|
1039
|
-
|
|
1040
|
-
details = {}
|
|
1041
|
-
|
|
1042
|
-
# PostgreSQL specific error parsing
|
|
1043
|
-
if hasattr(exception, 'pgcode'):
|
|
1044
|
-
# Column does not exist
|
|
1045
|
-
if 'column' in message.lower() and 'does not exist' in message.lower():
|
|
1046
|
-
match = re.search(r'column "([^"]+)" does not exist', message, re.IGNORECASE)
|
|
1047
|
-
if match:
|
|
1048
|
-
details['column'] = match.group(1)
|
|
1049
|
-
details['description'] = f'The column "{match.group(1)}" was not found in the table.'
|
|
1050
|
-
|
|
1051
|
-
# Table does not exist
|
|
1052
|
-
elif 'relation' in message.lower() and 'does not exist' in message.lower():
|
|
1053
|
-
match = re.search(r'relation "([^"]+)" does not exist', message, re.IGNORECASE)
|
|
1054
|
-
if match:
|
|
1055
|
-
details['table'] = match.group(1)
|
|
1056
|
-
details['description'] = f'The table "{match.group(1)}" was not found in the database.'
|
|
1057
|
-
|
|
1058
|
-
# Foreign key violation
|
|
1059
|
-
elif 'foreign key constraint' in message.lower():
|
|
1060
|
-
match = re.search(r'violates foreign key constraint "([^"]+)"', message, re.IGNORECASE)
|
|
1061
|
-
if match:
|
|
1062
|
-
details['constraint'] = match.group(1)
|
|
1063
|
-
details['description'] = 'A foreign key constraint was violated.'
|
|
1064
|
-
details['hint'] = 'Make sure the referenced record exists.'
|
|
1065
|
-
|
|
1066
|
-
# Unique constraint violation
|
|
1067
|
-
elif 'unique constraint' in message.lower() or 'duplicate key' in message.lower():
|
|
1068
|
-
match = re.search(r'violates unique constraint "([^"]+)"', message, re.IGNORECASE)
|
|
1069
|
-
if match:
|
|
1070
|
-
details['constraint'] = match.group(1)
|
|
1071
|
-
details['description'] = 'A unique constraint was violated (duplicate key).'
|
|
1072
|
-
details['hint'] = 'The value you are trying to insert already exists.'
|
|
1073
|
-
|
|
1074
|
-
# Connection errors
|
|
1075
|
-
elif any(term in message.lower() for term in ['connection', 'connect', 'server']):
|
|
1076
|
-
details['description'] = 'Failed to connect to the database server.'
|
|
1077
|
-
details['hint'] = 'Check your database connection settings and network connectivity.'
|
|
1078
|
-
|
|
1079
|
-
# Data type errors
|
|
1080
|
-
elif 'invalid input syntax' in message.lower():
|
|
1081
|
-
details['description'] = 'Invalid data format provided.'
|
|
1082
|
-
details['hint'] = 'Check that your data matches the expected format for the column type.'
|
|
1083
|
-
|
|
1084
|
-
return details
|
|
1085
|
-
|
|
1086
|
-
def _create_exception_with_chaining(self, error_class, message, original_exception, sql=None, parameters=None, format_type=None):
|
|
1087
|
-
"""
|
|
1088
|
-
Create a velocity exception with proper exception chaining and human-readable formatting.
|
|
1089
|
-
|
|
1090
|
-
Args:
|
|
1091
|
-
error_class: The name of the exception class to create
|
|
1092
|
-
message: The error message
|
|
1093
|
-
original_exception: The original exception to chain
|
|
1094
|
-
sql: The SQL statement (optional)
|
|
1095
|
-
parameters: The SQL parameters (optional)
|
|
1096
|
-
format_type: 'console', 'email', or None (auto-detect)
|
|
1097
|
-
|
|
1098
|
-
Returns:
|
|
1099
|
-
The created exception with proper chaining and formatting
|
|
1100
|
-
"""
|
|
1101
|
-
logger = logging.getLogger(__name__)
|
|
1102
|
-
|
|
1103
|
-
try:
|
|
1104
|
-
# Import the exception class dynamically
|
|
1105
|
-
exception_module = __import__('velocity.db.exceptions', fromlist=[error_class])
|
|
1106
|
-
ExceptionClass = getattr(exception_module, error_class)
|
|
1107
|
-
|
|
1108
|
-
# Auto-detect format if not specified
|
|
1109
|
-
if format_type is None:
|
|
1110
|
-
format_type = self._detect_output_format()
|
|
1111
|
-
|
|
1112
|
-
# Create human-readable, formatted message
|
|
1113
|
-
formatted_message = self._format_human_readable_error(
|
|
1114
|
-
error_class, message, original_exception, sql, parameters, format_type
|
|
534
|
+
except Exception as e:
|
|
535
|
+
# If formatting fails, return original SQL with parameters shown separately
|
|
536
|
+
return (
|
|
537
|
+
f"{sql}\n-- Parameters (formatting failed): {parameters}\n-- Error: {e}"
|
|
1115
538
|
)
|
|
1116
|
-
|
|
1117
|
-
# For email format, also create a console version for logging
|
|
1118
|
-
if format_type == 'email':
|
|
1119
|
-
console_message = self._format_human_readable_error(
|
|
1120
|
-
error_class, message, original_exception, sql, parameters, 'console'
|
|
1121
|
-
)
|
|
1122
|
-
# Log the console version for server logs
|
|
1123
|
-
logger.error(f"Database Error (Console Format):\n{console_message}")
|
|
1124
|
-
|
|
1125
|
-
# Create custom exception with both formats
|
|
1126
|
-
new_exception = ExceptionClass(formatted_message)
|
|
1127
|
-
new_exception.console_format = console_message
|
|
1128
|
-
new_exception.email_format = formatted_message
|
|
1129
|
-
else:
|
|
1130
|
-
new_exception = ExceptionClass(formatted_message)
|
|
1131
|
-
|
|
1132
|
-
# Only set __cause__ if original_exception is not None and derives from BaseException
|
|
1133
|
-
if isinstance(original_exception, BaseException):
|
|
1134
|
-
new_exception.__cause__ = original_exception # Preserve exception chain
|
|
1135
|
-
|
|
1136
|
-
return new_exception
|
|
1137
|
-
|
|
1138
|
-
except (ImportError, AttributeError) as e:
|
|
1139
|
-
logger.error(f"Could not import exception class {error_class}: {e}")
|
|
1140
|
-
# Fallback to generic database error
|
|
1141
|
-
try:
|
|
1142
|
-
exception_module = __import__('velocity.db.exceptions', fromlist=['DatabaseError'])
|
|
1143
|
-
DatabaseError = getattr(exception_module, 'DatabaseError')
|
|
1144
|
-
|
|
1145
|
-
# Auto-detect format if not specified for fallback too
|
|
1146
|
-
if format_type is None:
|
|
1147
|
-
format_type = self._detect_output_format()
|
|
1148
|
-
|
|
1149
|
-
# Still format the fallback nicely
|
|
1150
|
-
formatted_message = self._format_human_readable_error(
|
|
1151
|
-
'DatabaseError', message, original_exception, sql, parameters, format_type
|
|
1152
|
-
)
|
|
1153
|
-
|
|
1154
|
-
fallback_exception = DatabaseError(formatted_message)
|
|
1155
|
-
# Only set __cause__ if original_exception is not None and derives from BaseException
|
|
1156
|
-
if isinstance(original_exception, BaseException):
|
|
1157
|
-
fallback_exception.__cause__ = original_exception
|
|
1158
|
-
return fallback_exception
|
|
1159
|
-
except Exception as fallback_error:
|
|
1160
|
-
logger.critical(f"Failed to create fallback exception: {fallback_error}")
|
|
1161
|
-
# Last resort: return the original exception
|
|
1162
|
-
return original_exception
|
|
1163
|
-
|
|
1164
|
-
def _detect_output_format(self):
|
|
1165
|
-
"""
|
|
1166
|
-
Detect whether we should use console or email formatting based on context.
|
|
1167
|
-
|
|
1168
|
-
Returns:
|
|
1169
|
-
'email' if in email/notification context, 'console' otherwise
|
|
1170
|
-
"""
|
|
1171
|
-
import inspect
|
|
1172
|
-
|
|
1173
|
-
# Look at the call stack for email-related functions
|
|
1174
|
-
stack = inspect.stack()
|
|
1175
|
-
|
|
1176
|
-
email_indicators = [
|
|
1177
|
-
'email', 'mail', 'notification', 'alert', 'send', 'notify',
|
|
1178
|
-
'smtp', 'message', 'recipient', 'subject', 'body'
|
|
1179
|
-
]
|
|
1180
|
-
|
|
1181
|
-
for frame_info in stack:
|
|
1182
|
-
function_name = frame_info.function.lower()
|
|
1183
|
-
filename = frame_info.filename.lower()
|
|
1184
|
-
|
|
1185
|
-
# Check if we're in an email-related context
|
|
1186
|
-
if any(indicator in function_name or indicator in filename
|
|
1187
|
-
for indicator in email_indicators):
|
|
1188
|
-
return 'email'
|
|
1189
|
-
|
|
1190
|
-
# Default to console format
|
|
1191
|
-
return 'console'
|