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.
Files changed (120) hide show
  1. velocity/__init__.py +3 -1
  2. velocity/app/orders.py +3 -4
  3. velocity/app/tests/__init__.py +1 -0
  4. velocity/app/tests/test_email_processing.py +112 -0
  5. velocity/app/tests/test_payment_profile_sorting.py +191 -0
  6. velocity/app/tests/test_spreadsheet_functions.py +124 -0
  7. velocity/aws/__init__.py +3 -0
  8. velocity/aws/amplify.py +10 -6
  9. velocity/aws/handlers/__init__.py +2 -0
  10. velocity/aws/handlers/base_handler.py +248 -0
  11. velocity/aws/handlers/context.py +167 -2
  12. velocity/aws/handlers/exceptions.py +16 -0
  13. velocity/aws/handlers/lambda_handler.py +24 -85
  14. velocity/aws/handlers/mixins/__init__.py +16 -0
  15. velocity/aws/handlers/mixins/activity_tracker.py +181 -0
  16. velocity/aws/handlers/mixins/aws_session_mixin.py +192 -0
  17. velocity/aws/handlers/mixins/error_handler.py +192 -0
  18. velocity/aws/handlers/mixins/legacy_mixin.py +53 -0
  19. velocity/aws/handlers/mixins/standard_mixin.py +73 -0
  20. velocity/aws/handlers/response.py +1 -1
  21. velocity/aws/handlers/sqs_handler.py +28 -143
  22. velocity/aws/tests/__init__.py +1 -0
  23. velocity/aws/tests/test_lambda_handler_json_serialization.py +120 -0
  24. velocity/aws/tests/test_response.py +163 -0
  25. velocity/db/__init__.py +16 -4
  26. velocity/db/core/decorators.py +20 -4
  27. velocity/db/core/engine.py +185 -792
  28. velocity/db/core/result.py +36 -22
  29. velocity/db/core/row.py +15 -3
  30. velocity/db/core/table.py +283 -44
  31. velocity/db/core/transaction.py +19 -11
  32. velocity/db/exceptions.py +42 -18
  33. velocity/db/servers/base/__init__.py +9 -0
  34. velocity/db/servers/base/initializer.py +70 -0
  35. velocity/db/servers/base/operators.py +98 -0
  36. velocity/db/servers/base/sql.py +503 -0
  37. velocity/db/servers/base/types.py +135 -0
  38. velocity/db/servers/mysql/__init__.py +73 -0
  39. velocity/db/servers/mysql/operators.py +54 -0
  40. velocity/db/servers/{mysql_reserved.py → mysql/reserved.py} +2 -14
  41. velocity/db/servers/mysql/sql.py +718 -0
  42. velocity/db/servers/mysql/types.py +107 -0
  43. velocity/db/servers/postgres/__init__.py +59 -11
  44. velocity/db/servers/postgres/operators.py +34 -0
  45. velocity/db/servers/postgres/sql.py +474 -120
  46. velocity/db/servers/postgres/types.py +88 -2
  47. velocity/db/servers/sqlite/__init__.py +61 -0
  48. velocity/db/servers/sqlite/operators.py +52 -0
  49. velocity/db/servers/sqlite/reserved.py +20 -0
  50. velocity/db/servers/sqlite/sql.py +677 -0
  51. velocity/db/servers/sqlite/types.py +92 -0
  52. velocity/db/servers/sqlserver/__init__.py +73 -0
  53. velocity/db/servers/sqlserver/operators.py +47 -0
  54. velocity/db/servers/sqlserver/reserved.py +32 -0
  55. velocity/db/servers/sqlserver/sql.py +805 -0
  56. velocity/db/servers/sqlserver/types.py +114 -0
  57. velocity/db/servers/tablehelper.py +117 -91
  58. velocity/db/tests/__init__.py +1 -0
  59. velocity/db/tests/common_db_test.py +0 -0
  60. velocity/db/tests/postgres/__init__.py +1 -0
  61. velocity/db/tests/postgres/common.py +49 -0
  62. velocity/db/tests/postgres/test_column.py +29 -0
  63. velocity/db/tests/postgres/test_connections.py +25 -0
  64. velocity/db/tests/postgres/test_database.py +21 -0
  65. velocity/db/tests/postgres/test_engine.py +205 -0
  66. velocity/db/tests/postgres/test_general_usage.py +88 -0
  67. velocity/db/tests/postgres/test_imports.py +8 -0
  68. velocity/db/tests/postgres/test_result.py +19 -0
  69. velocity/db/tests/postgres/test_row.py +137 -0
  70. velocity/db/tests/postgres/test_row_comprehensive.py +720 -0
  71. velocity/db/tests/postgres/test_schema_locking.py +335 -0
  72. velocity/db/tests/postgres/test_schema_locking_unit.py +115 -0
  73. velocity/db/tests/postgres/test_sequence.py +34 -0
  74. velocity/db/tests/postgres/test_sql_comprehensive.py +462 -0
  75. velocity/db/tests/postgres/test_table.py +101 -0
  76. velocity/db/tests/postgres/test_table_comprehensive.py +646 -0
  77. velocity/db/tests/postgres/test_transaction.py +106 -0
  78. velocity/db/tests/sql/__init__.py +1 -0
  79. velocity/db/tests/sql/common.py +177 -0
  80. velocity/db/tests/sql/test_postgres_select_advanced.py +285 -0
  81. velocity/db/tests/sql/test_postgres_select_variances.py +517 -0
  82. velocity/db/tests/test_cursor_rowcount_fix.py +150 -0
  83. velocity/db/tests/test_db_utils.py +221 -0
  84. velocity/db/tests/test_postgres.py +448 -0
  85. velocity/db/tests/test_postgres_unchanged.py +81 -0
  86. velocity/db/tests/test_process_error_robustness.py +292 -0
  87. velocity/db/tests/test_result_caching.py +279 -0
  88. velocity/db/tests/test_result_sql_aware.py +117 -0
  89. velocity/db/tests/test_row_get_missing_column.py +72 -0
  90. velocity/db/tests/test_schema_locking_initializers.py +226 -0
  91. velocity/db/tests/test_schema_locking_simple.py +97 -0
  92. velocity/db/tests/test_sql_builder.py +165 -0
  93. velocity/db/tests/test_tablehelper.py +486 -0
  94. velocity/db/utils.py +62 -47
  95. velocity/misc/conv/__init__.py +2 -0
  96. velocity/misc/conv/iconv.py +5 -4
  97. velocity/misc/export.py +1 -4
  98. velocity/misc/merge.py +1 -1
  99. velocity/misc/tests/__init__.py +1 -0
  100. velocity/misc/tests/test_db.py +90 -0
  101. velocity/misc/tests/test_fix.py +78 -0
  102. velocity/misc/tests/test_format.py +64 -0
  103. velocity/misc/tests/test_iconv.py +203 -0
  104. velocity/misc/tests/test_merge.py +82 -0
  105. velocity/misc/tests/test_oconv.py +144 -0
  106. velocity/misc/tests/test_original_error.py +52 -0
  107. velocity/misc/tests/test_timer.py +74 -0
  108. velocity/misc/tools.py +0 -1
  109. {velocity_python-0.0.105.dist-info → velocity_python-0.0.155.dist-info}/METADATA +2 -2
  110. velocity_python-0.0.155.dist-info/RECORD +129 -0
  111. velocity/db/core/exceptions.py +0 -70
  112. velocity/db/servers/mysql.py +0 -641
  113. velocity/db/servers/sqlite.py +0 -968
  114. velocity/db/servers/sqlite_reserved.py +0 -208
  115. velocity/db/servers/sqlserver.py +0 -921
  116. velocity/db/servers/sqlserver_reserved.py +0 -314
  117. velocity_python-0.0.105.dist-info/RECORD +0 -56
  118. {velocity_python-0.0.105.dist-info → velocity_python-0.0.155.dist-info}/WHEEL +0 -0
  119. {velocity_python-0.0.105.dist-info → velocity_python-0.0.155.dist-info}/licenses/LICENSE +0 -0
  120. {velocity_python-0.0.105.dist-info → velocity_python-0.0.155.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,7 @@
1
1
  import inspect
2
- import sys
3
2
  import re
4
- import traceback
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 as e:
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 as e:
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
- Process database errors and raise appropriate velocity exceptions.
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
- # Enhanced logging with context - more readable format
344
- sql_preview = sql[:100] + "..." if sql and len(sql) > 100 else sql or "None"
345
-
346
- logger.error(
347
- f"🔴 Database Error Detected\n"
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 = str(exception)
362
- except Exception as e:
363
- logger.warning(f"⚠️ Unable to convert exception to string: {e}")
364
- error_message = f"<Error converting exception: {type(exception).__name__}>"
365
-
366
- # Primary error classification by error code
367
- if error_code and hasattr(self, 'error_codes'):
368
- for error_class, codes in self.error_codes.items():
369
- if error_code in codes:
370
- logger.info(f"✅ Successfully classified error: {error_code} {error_class}")
371
- try:
372
- raise self._create_exception_with_chaining(
373
- error_class, error_message, exception, sql, parameters
374
- )
375
- except Exception as creation_error:
376
- logger.error(f" Failed to create {error_class} exception: {creation_error}")
377
- # Fall through to regex classification
378
- break
379
-
380
- # Secondary error classification by message patterns (regex fallback)
381
- error_message_lower = error_message.lower()
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
- lines.append("│")
549
- lines.append("│ SQL Statement:")
550
- # Show complete SQL without truncation for debugging
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
- lines.append("│ │")
638
- lines.append("│ └─────────────────────────────────────────────────────────")
639
-
640
- lines.append("│")
641
- lines.append("╰" + "─" * 70)
642
-
643
- return '\n'.join(lines)
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
- def _format_email_error(self, error_class, message, original_exception, sql=None, parameters=None):
646
- """
647
- Format error message for email delivery with HTML formatting.
648
- """
649
- # Map error classes to user-friendly descriptions
650
- error_descriptions = {
651
- 'DbColumnMissingError': 'Column Not Found',
652
- 'DbTableMissingError': 'Table Not Found',
653
- 'DbDatabaseMissingError': 'Database Not Found',
654
- 'DbForeignKeyMissingError': 'Foreign Key Constraint Violation',
655
- 'DbDuplicateKeyError': 'Duplicate Key Violation',
656
- 'DbConnectionError': 'Database Connection Failed',
657
- 'DbDataIntegrityError': 'Data Integrity Violation',
658
- 'DbQueryError': 'Query Execution Error',
659
- 'DbTransactionError': 'Transaction Error',
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('&', '&amp;')
795
- .replace('<', '&lt;')
796
- .replace('>', '&gt;')
797
- .replace('"', '&quot;')
798
- .replace("'", '&#x27;'))
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 substituted for easy copy-paste debugging.
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
- # For positional parameters (%s style)
818
- formatted_sql = sql
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 isinstance(param, str):
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
- formatted_param = f"'{param.replace(chr(39), chr(39)+chr(39))}'"
825
- elif isinstance(param, (int, float)):
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
- formatted_param = 'TRUE' if param else 'FALSE'
500
+ formatted_params.append("TRUE" if param else "FALSE")
831
501
  else:
832
- # For other types, try to convert to string and quote
833
- formatted_param = f"'{str(param).replace(chr(39), chr(39)+chr(39))}'"
834
-
835
- # Replace first occurrence of %s
836
- formatted_sql = formatted_sql.replace('%s', formatted_param, 1)
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
- # For named parameters (%(name)s style)
512
+ # Handle named parameters
842
513
  formatted_sql = sql
843
514
  for key, value in parameters.items():
844
- placeholder = f'%({key})s'
845
- if isinstance(value, str):
846
- formatted_value = f"'{value.replace(chr(39), chr(39)+chr(39))}'"
847
- elif isinstance(value, (int, float)):
848
- formatted_value = str(value)
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
- formatted_value = 'TRUE' if value else 'FALSE'
521
+ replacement = "TRUE" if value else "FALSE"
853
522
  else:
854
- formatted_value = f"'{str(value).replace(chr(39), chr(39)+chr(39))}'"
855
-
856
- formatted_sql = formatted_sql.replace(placeholder, formatted_value)
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
- def _create_exception_with_chaining(self, error_class, message, original_exception, sql=None, parameters=None, format_type=None):
1040
- """
1041
- Create a velocity exception with proper exception chaining and human-readable formatting.
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'