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