velocity-python 0.0.109__py3-none-any.whl → 0.0.112__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of velocity-python might be problematic. Click here for more details.
- velocity/__init__.py +1 -1
- velocity/db/core/engine.py +112 -822
- {velocity_python-0.0.109.dist-info → velocity_python-0.0.112.dist-info}/METADATA +1 -1
- {velocity_python-0.0.109.dist-info → velocity_python-0.0.112.dist-info}/RECORD +7 -7
- {velocity_python-0.0.109.dist-info → velocity_python-0.0.112.dist-info}/WHEEL +0 -0
- {velocity_python-0.0.109.dist-info → velocity_python-0.0.112.dist-info}/licenses/LICENSE +0 -0
- {velocity_python-0.0.109.dist-info → velocity_python-0.0.112.dist-info}/top_level.txt +0 -0
velocity/__init__.py
CHANGED
velocity/db/core/engine.py
CHANGED
|
@@ -24,38 +24,6 @@ class Engine:
|
|
|
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
|
|
59
27
|
|
|
60
28
|
def __str__(self):
|
|
61
29
|
return f"[{self.sql.server}] engine({self.config})"
|
|
@@ -359,833 +327,155 @@ class Engine:
|
|
|
359
327
|
|
|
360
328
|
def process_error(self, exception, sql=None, parameters=None):
|
|
361
329
|
"""
|
|
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
|
|
330
|
+
Central method to parse driver exceptions and re-raise them as our custom exceptions.
|
|
372
331
|
"""
|
|
373
332
|
logger = logging.getLogger(__name__)
|
|
374
333
|
|
|
375
|
-
#
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
logger.error(
|
|
379
|
-
f"🔴 Database Error Detected\n"
|
|
380
|
-
f" Exception Type: {type(exception).__name__}\n"
|
|
381
|
-
f" SQL Statement: {sql_preview}\n"
|
|
382
|
-
f" Processing error for classification..."
|
|
383
|
-
)
|
|
334
|
+
# If it's already a velocity exception, just re-raise it
|
|
335
|
+
if isinstance(exception, exceptions.DbException):
|
|
336
|
+
raise exception
|
|
384
337
|
|
|
385
|
-
#
|
|
338
|
+
# Get error code and message from the SQL driver
|
|
386
339
|
try:
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
-
|
|
401
|
-
try:
|
|
402
|
-
error_message = str(exception)
|
|
403
|
-
except Exception as e:
|
|
404
|
-
logger.warning(f"⚠️ Unable to convert exception to string: {e}")
|
|
405
|
-
error_message = f"<Error converting exception: {type(exception).__name__}>"
|
|
406
|
-
|
|
407
|
-
# Primary error classification by error code
|
|
408
|
-
if error_code and hasattr(self, 'error_codes') and str(error_code) in self.error_codes:
|
|
409
|
-
error_class = self.error_codes[str(error_code)]
|
|
410
|
-
logger.info(f"✅ Successfully classified error: {error_code} → {error_class}")
|
|
411
|
-
try:
|
|
412
|
-
raise self._create_exception_with_chaining(
|
|
413
|
-
error_class, error_message, exception, sql, parameters
|
|
414
|
-
)
|
|
415
|
-
except Exception as creation_error:
|
|
416
|
-
logger.error(f"❌ Failed to create {error_class} exception: {creation_error}")
|
|
417
|
-
# Fall through to regex classification
|
|
418
|
-
|
|
419
|
-
# Secondary error classification by message patterns (regex fallback)
|
|
420
|
-
error_message_lower = error_message.lower()
|
|
421
|
-
|
|
422
|
-
# Enhanced connection error patterns
|
|
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
|
-
]
|
|
340
|
+
error_code, error_message = self.sql.get_error(exception)
|
|
341
|
+
except Exception:
|
|
342
|
+
error_code, error_message = None, str(exception)
|
|
477
343
|
|
|
478
|
-
|
|
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
|
-
]
|
|
344
|
+
msg = str(exception).strip().lower()
|
|
488
345
|
|
|
489
|
-
#
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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
|
|
346
|
+
# Create enhanced error message with SQL query
|
|
347
|
+
enhanced_message = str(exception)
|
|
348
|
+
if sql:
|
|
349
|
+
enhanced_message += f"\n\nSQL Query:\n{self._format_sql_with_params(sql, parameters)}"
|
|
504
350
|
|
|
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
351
|
logger.warning(
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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
|
|
352
|
+
"Database error caught. Attempting to transform: code=%s message=%s",
|
|
353
|
+
error_code,
|
|
354
|
+
error_message,
|
|
516
355
|
)
|
|
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
356
|
|
|
571
|
-
#
|
|
572
|
-
if
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
357
|
+
# Direct error code mapping
|
|
358
|
+
if error_code in self.sql.ApplicationErrorCodes:
|
|
359
|
+
raise exceptions.DbApplicationError(enhanced_message) from exception
|
|
360
|
+
if error_code in self.sql.ColumnMissingErrorCodes:
|
|
361
|
+
raise exceptions.DbColumnMissingError(enhanced_message) from exception
|
|
362
|
+
if error_code in self.sql.TableMissingErrorCodes:
|
|
363
|
+
raise exceptions.DbTableMissingError(enhanced_message) from exception
|
|
364
|
+
if error_code in self.sql.DatabaseMissingErrorCodes:
|
|
365
|
+
raise exceptions.DbDatabaseMissingError(enhanced_message) from exception
|
|
366
|
+
if error_code in self.sql.ForeignKeyMissingErrorCodes:
|
|
367
|
+
raise exceptions.DbForeignKeyMissingError(enhanced_message) from exception
|
|
368
|
+
if error_code in self.sql.TruncationErrorCodes:
|
|
369
|
+
raise exceptions.DbTruncationError(enhanced_message) from exception
|
|
370
|
+
if error_code in self.sql.DataIntegrityErrorCodes:
|
|
371
|
+
raise exceptions.DbDataIntegrityError(enhanced_message) from exception
|
|
372
|
+
if error_code in self.sql.ConnectionErrorCodes:
|
|
373
|
+
raise exceptions.DbConnectionError(enhanced_message) from exception
|
|
374
|
+
if error_code in self.sql.DuplicateKeyErrorCodes:
|
|
375
|
+
raise exceptions.DbDuplicateKeyError(enhanced_message) from exception
|
|
376
|
+
if error_code in self.sql.DatabaseObjectExistsErrorCodes:
|
|
377
|
+
raise exceptions.DbObjectExistsError(enhanced_message) from exception
|
|
378
|
+
if error_code in self.sql.LockTimeoutErrorCodes:
|
|
379
|
+
raise exceptions.DbLockTimeoutError(enhanced_message) from exception
|
|
380
|
+
if error_code in self.sql.RetryTransactionCodes:
|
|
381
|
+
raise exceptions.DbRetryTransaction(enhanced_message) from exception
|
|
382
|
+
|
|
383
|
+
# Regex-based fallback patterns
|
|
384
|
+
if re.search(r"key \(sys_id\)=\(\d+\) already exists.", msg, re.M):
|
|
385
|
+
raise exceptions.DbDuplicateKeyError(enhanced_message) from exception
|
|
386
|
+
if re.findall(r"database.*does not exist", msg, re.M):
|
|
387
|
+
raise exceptions.DbDatabaseMissingError(enhanced_message) from exception
|
|
388
|
+
if re.findall(r"no such database", msg, re.M):
|
|
389
|
+
raise exceptions.DbDatabaseMissingError(enhanced_message) from exception
|
|
390
|
+
if re.findall(r"already exists", msg, re.M):
|
|
391
|
+
raise exceptions.DbObjectExistsError(enhanced_message) from exception
|
|
392
|
+
if re.findall(r"server closed the connection unexpectedly", msg, re.M):
|
|
393
|
+
raise exceptions.DbConnectionError(enhanced_message) from exception
|
|
394
|
+
if re.findall(r"no connection to the server", msg, re.M):
|
|
395
|
+
raise exceptions.DbConnectionError(enhanced_message) from exception
|
|
396
|
+
if re.findall(r"connection timed out", msg, re.M):
|
|
397
|
+
raise exceptions.DbConnectionError(enhanced_message) from exception
|
|
398
|
+
if re.findall(r"could not connect to server", msg, re.M):
|
|
399
|
+
raise exceptions.DbConnectionError(enhanced_message) from exception
|
|
400
|
+
if re.findall(r"cannot connect to server", msg, re.M):
|
|
401
|
+
raise exceptions.DbConnectionError(enhanced_message) from exception
|
|
402
|
+
if re.findall(r"connection already closed", msg, re.M):
|
|
403
|
+
raise exceptions.DbConnectionError(enhanced_message) from exception
|
|
404
|
+
if re.findall(r"cursor already closed", msg, re.M):
|
|
405
|
+
raise exceptions.DbConnectionError(enhanced_message) from exception
|
|
406
|
+
if "no such table:" in msg:
|
|
407
|
+
raise exceptions.DbTableMissingError(enhanced_message) from exception
|
|
577
408
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
if details.get('constraint'):
|
|
589
|
-
lines.append(f"│ Constraint: {details['constraint']}")
|
|
590
|
-
if details.get('hint'):
|
|
591
|
-
lines.append(f"│ Hint: {details['hint']}")
|
|
592
|
-
|
|
593
|
-
# Add SQL context if available
|
|
594
|
-
if sql:
|
|
595
|
-
lines.append("│")
|
|
596
|
-
lines.append("│ SQL Statement:")
|
|
597
|
-
# Show complete SQL without truncation for debugging
|
|
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}")
|
|
683
|
-
|
|
684
|
-
lines.append("│ │")
|
|
685
|
-
lines.append("│ └─────────────────────────────────────────────────────────")
|
|
686
|
-
|
|
687
|
-
lines.append("│")
|
|
688
|
-
lines.append("╰" + "─" * 70)
|
|
409
|
+
logger.error(
|
|
410
|
+
"Unhandled/Unknown Error in engine.process_error",
|
|
411
|
+
exc_info=True,
|
|
412
|
+
extra={
|
|
413
|
+
"error_code": error_code,
|
|
414
|
+
"error_msg": error_message,
|
|
415
|
+
"sql_stmt": sql,
|
|
416
|
+
"sql_params": parameters,
|
|
417
|
+
},
|
|
418
|
+
)
|
|
689
419
|
|
|
690
|
-
|
|
420
|
+
# If we can't classify it, re-raise with enhanced message
|
|
421
|
+
raise type(exception)(enhanced_message) from exception
|
|
691
422
|
|
|
692
|
-
def
|
|
693
|
-
"""
|
|
694
|
-
Format error message for email delivery with HTML formatting.
|
|
423
|
+
def _format_sql_with_params(self, sql, parameters):
|
|
695
424
|
"""
|
|
696
|
-
|
|
697
|
-
error_descriptions = {
|
|
698
|
-
'DbColumnMissingError': 'Column Not Found',
|
|
699
|
-
'DbTableMissingError': 'Table Not Found',
|
|
700
|
-
'DbDatabaseMissingError': 'Database Not Found',
|
|
701
|
-
'DbForeignKeyMissingError': 'Foreign Key Constraint Violation',
|
|
702
|
-
'DbDuplicateKeyError': 'Duplicate Key Violation',
|
|
703
|
-
'DbConnectionError': 'Database Connection Failed',
|
|
704
|
-
'DbDataIntegrityError': 'Data Integrity Violation',
|
|
705
|
-
'DbQueryError': 'Query Execution Error',
|
|
706
|
-
'DbTransactionError': 'Transaction Error',
|
|
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):
|
|
425
|
+
Format SQL query with parameters merged for easy copy-paste debugging.
|
|
848
426
|
"""
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
Args:
|
|
852
|
-
sql: The SQL statement with placeholders
|
|
853
|
-
parameters: The parameters to substitute
|
|
427
|
+
if not sql:
|
|
428
|
+
return "No SQL provided"
|
|
854
429
|
|
|
855
|
-
Returns:
|
|
856
|
-
SQL statement with parameters properly formatted for execution
|
|
857
|
-
"""
|
|
858
430
|
if not parameters:
|
|
859
431
|
return sql
|
|
860
432
|
|
|
861
433
|
try:
|
|
862
434
|
# Handle different parameter formats
|
|
863
435
|
if isinstance(parameters, (list, tuple)):
|
|
864
|
-
#
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
# Replace %s placeholders with properly formatted values
|
|
436
|
+
# Convert parameters to strings and handle None values
|
|
437
|
+
formatted_params = []
|
|
868
438
|
for param in parameters:
|
|
869
|
-
if
|
|
439
|
+
if param is None:
|
|
440
|
+
formatted_params.append('NULL')
|
|
441
|
+
elif isinstance(param, str):
|
|
870
442
|
# Escape single quotes and wrap in quotes
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
formatted_param = str(param)
|
|
874
|
-
elif param is None:
|
|
875
|
-
formatted_param = 'NULL'
|
|
443
|
+
escaped = param.replace("'", "''")
|
|
444
|
+
formatted_params.append(f"'{escaped}'")
|
|
876
445
|
elif isinstance(param, bool):
|
|
877
|
-
|
|
446
|
+
formatted_params.append('TRUE' if param else 'FALSE')
|
|
878
447
|
else:
|
|
879
|
-
|
|
880
|
-
formatted_param = f"'{str(param).replace(chr(39), chr(39)+chr(39))}'"
|
|
881
|
-
|
|
882
|
-
# Replace first occurrence of %s
|
|
883
|
-
formatted_sql = formatted_sql.replace('%s', formatted_param, 1)
|
|
448
|
+
formatted_params.append(str(param))
|
|
884
449
|
|
|
450
|
+
# Replace %s placeholders with actual values
|
|
451
|
+
formatted_sql = sql
|
|
452
|
+
for param in formatted_params:
|
|
453
|
+
formatted_sql = formatted_sql.replace('%s', param, 1)
|
|
454
|
+
|
|
885
455
|
return formatted_sql
|
|
886
456
|
|
|
887
457
|
elif isinstance(parameters, dict):
|
|
888
|
-
#
|
|
458
|
+
# Handle named parameters
|
|
889
459
|
formatted_sql = sql
|
|
890
460
|
for key, value in parameters.items():
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
elif value is None:
|
|
897
|
-
formatted_value = 'NULL'
|
|
461
|
+
if value is None:
|
|
462
|
+
replacement = 'NULL'
|
|
463
|
+
elif isinstance(value, str):
|
|
464
|
+
escaped = value.replace("'", "''")
|
|
465
|
+
replacement = f"'{escaped}'"
|
|
898
466
|
elif isinstance(value, bool):
|
|
899
|
-
|
|
467
|
+
replacement = 'TRUE' if value else 'FALSE'
|
|
900
468
|
else:
|
|
901
|
-
|
|
469
|
+
replacement = str(value)
|
|
470
|
+
|
|
471
|
+
# Replace %(key)s or :key patterns
|
|
472
|
+
formatted_sql = formatted_sql.replace(f'%({key})s', replacement)
|
|
473
|
+
formatted_sql = formatted_sql.replace(f':{key}', replacement)
|
|
902
474
|
|
|
903
|
-
formatted_sql = formatted_sql.replace(placeholder, formatted_value)
|
|
904
|
-
|
|
905
475
|
return formatted_sql
|
|
906
|
-
|
|
907
476
|
else:
|
|
908
|
-
# Fallback: just append parameters as comment
|
|
909
477
|
return f"{sql}\n-- Parameters: {parameters}"
|
|
910
478
|
|
|
911
|
-
except Exception:
|
|
912
|
-
# If formatting fails, return original with parameters
|
|
913
|
-
return f"{sql}\n-- Parameters: {parameters}"
|
|
914
|
-
|
|
915
|
-
def _extract_call_stack_info(self):
|
|
916
|
-
"""
|
|
917
|
-
Extract relevant call stack information for debugging, filtering out
|
|
918
|
-
middleware and focusing on the most useful frames.
|
|
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
|
|
1115
|
-
)
|
|
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'
|
|
479
|
+
except Exception as e:
|
|
480
|
+
# If formatting fails, return original SQL with parameters shown separately
|
|
481
|
+
return f"{sql}\n-- Parameters (formatting failed): {parameters}\n-- Error: {e}"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
velocity/__init__.py,sha256=
|
|
1
|
+
velocity/__init__.py,sha256=iH2m31ZNvp-oPV3zuzJRMhEbgHm_d-Gs5k6ljPudBlg,107
|
|
2
2
|
velocity/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
3
|
velocity/app/invoices.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
4
|
velocity/app/orders.py,sha256=W-HAXEwY8-IFXbKh82HnMeRVZM7P-TWGEQOWtkLIzI4,6298
|
|
@@ -18,7 +18,7 @@ velocity/db/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,
|
|
|
18
18
|
velocity/db/core/column.py,sha256=tAr8tL3a2nyaYpNHhGl508FrY_pGZTzyYgjAV5CEBv4,4092
|
|
19
19
|
velocity/db/core/database.py,sha256=3zNGItklu9tZCKsbx2T2vCcU1so8AL9PPL0DLjvaz6s,3554
|
|
20
20
|
velocity/db/core/decorators.py,sha256=76Jkr9XptXt8cvcgp1zbHfuL8uHzWy8lwfR29u-DVu4,4574
|
|
21
|
-
velocity/db/core/engine.py,sha256=
|
|
21
|
+
velocity/db/core/engine.py,sha256=Yy6uwxT44a4oEHrFF312ChzFO01cR2W79BTryFgTe-s,18635
|
|
22
22
|
velocity/db/core/exceptions.py,sha256=tuDniRqTX8Opc2d033LPJOI3Ux4NSwUcHqW729n-HXA,1027
|
|
23
23
|
velocity/db/core/result.py,sha256=dgiOXH-iJXuDH4PbSTWVkn-heAkJQcXCC-gs0ZuqF94,12814
|
|
24
24
|
velocity/db/core/row.py,sha256=zZ3zZbWjZkZfYAYuZJLHFJ8jdXc7dYv8Iyv9Ut8W8tE,7261
|
|
@@ -49,8 +49,8 @@ velocity/misc/tools.py,sha256=_bGneHHA_BV-kUonzw5H3hdJ5AOJRCKfzhgpkFbGqIo,1502
|
|
|
49
49
|
velocity/misc/conv/__init__.py,sha256=MLYF58QHjzfDSxb1rdnmLnuEQCa3gnhzzZ30CwZVvQo,40
|
|
50
50
|
velocity/misc/conv/iconv.py,sha256=d4_BucW8HTIkGNurJ7GWrtuptqUf-9t79ObzjJ5N76U,10603
|
|
51
51
|
velocity/misc/conv/oconv.py,sha256=h5Lo05DqOQnxoD3y6Px_MQP_V-pBbWf8Hkgkb9Xp1jk,6032
|
|
52
|
-
velocity_python-0.0.
|
|
53
|
-
velocity_python-0.0.
|
|
54
|
-
velocity_python-0.0.
|
|
55
|
-
velocity_python-0.0.
|
|
56
|
-
velocity_python-0.0.
|
|
52
|
+
velocity_python-0.0.112.dist-info/licenses/LICENSE,sha256=aoN245GG8s9oRUU89KNiGTU4_4OtnNmVi4hQeChg6rM,1076
|
|
53
|
+
velocity_python-0.0.112.dist-info/METADATA,sha256=LEPElQOJU4yBn0Zwdw4VzpLRNhC4M6MM1_6Qa28VBDU,34262
|
|
54
|
+
velocity_python-0.0.112.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
55
|
+
velocity_python-0.0.112.dist-info/top_level.txt,sha256=JW2vJPmodgdgSz7H6yoZvnxF8S3fTMIv-YJWCT1sNW0,9
|
|
56
|
+
velocity_python-0.0.112.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|