velocity-python 0.0.109__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 -839
  28. velocity/db/core/result.py +30 -24
  29. velocity/db/core/row.py +15 -3
  30. velocity/db/core/table.py +279 -40
  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.109.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.109.dist-info/RECORD +0 -56
  118. {velocity_python-0.0.109.dist-info → velocity_python-0.0.155.dist-info}/WHEEL +0 -0
  119. {velocity_python-0.0.109.dist-info → velocity_python-0.0.155.dist-info}/licenses/LICENSE +0 -0
  120. {velocity_python-0.0.109.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,43 +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
-
28
- # Set up error code mappings from the SQL class
29
- self._setup_error_mappings()
30
-
31
- def _setup_error_mappings(self):
32
- """
33
- Set up error code to exception class mappings from the SQL driver.
34
- """
35
- self.error_codes = {}
36
-
37
- # Map error codes to exception class names
38
- sql_attrs = [
39
- ('ApplicationErrorCodes', 'DbApplicationError'),
40
- ('DatabaseMissingErrorCodes', 'DbDatabaseMissingError'),
41
- ('TableMissingErrorCodes', 'DbTableMissingError'),
42
- ('ColumnMissingErrorCodes', 'DbColumnMissingError'),
43
- ('ForeignKeyMissingErrorCodes', 'DbForeignKeyMissingError'),
44
- ('ConnectionErrorCodes', 'DbConnectionError'),
45
- ('DuplicateKeyErrorCodes', 'DbDuplicateKeyError'),
46
- ('RetryTransactionCodes', 'DbRetryTransaction'),
47
- ('TruncationErrorCodes', 'DbTruncationError'),
48
- ('LockTimeoutErrorCodes', 'DbLockTimeoutError'),
49
- ('DatabaseObjectExistsErrorCodes', 'DbObjectExistsError'),
50
- ('DataIntegrityErrorCodes', 'DbDataIntegrityError')
51
- ]
52
-
53
- for attr_name, exception_class in sql_attrs:
54
- if hasattr(self.sql, attr_name):
55
- codes = getattr(self.sql, attr_name)
56
- if codes: # Only add non-empty lists
57
- for code in codes:
58
- self.error_codes[str(code)] = exception_class
27
+ self.__schema_locked = schema_locked
59
28
 
60
29
  def __str__(self):
61
30
  return f"[{self.sql.server}] engine({self.config})"
@@ -208,18 +177,18 @@ class Engine:
208
177
  while True:
209
178
  try:
210
179
  return function(*args, **kwds)
211
- except exceptions.DbRetryTransaction as e:
180
+ except exceptions.DbRetryTransaction:
212
181
  retry_count += 1
213
182
  if retry_count > self.MAX_RETRIES:
214
183
  raise
215
184
  _tx.rollback()
216
- except exceptions.DbLockTimeoutError as e:
185
+ except exceptions.DbLockTimeoutError:
217
186
  lock_timeout_count += 1
218
187
  if lock_timeout_count > self.MAX_RETRIES:
219
188
  raise
220
189
  _tx.rollback()
221
190
  continue
222
- except:
191
+ except Exception:
223
192
  raise
224
193
  finally:
225
194
  setattr(_tx, "_exec_function_depth", depth)
@@ -239,6 +208,29 @@ class Engine:
239
208
  def sql(self):
240
209
  return self.__sql
241
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
+
242
234
  @property
243
235
  def version(self):
244
236
  """
@@ -359,833 +351,187 @@ class Engine:
359
351
 
360
352
  def process_error(self, exception, sql=None, parameters=None):
361
353
  """
362
- Process database errors and raise appropriate velocity exceptions.
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
354
+ Central method to parse driver exceptions and re-raise them as our custom exceptions.
372
355
  """
373
356
  logger = logging.getLogger(__name__)
374
-
375
- # Enhanced logging with context - more readable format
376
- sql_preview = sql[:100] + "..." if sql and len(sql) > 100 else sql or "None"
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
- )
384
-
385
- # Safely get error code and message with fallbacks
386
- try:
387
- # Try PostgreSQL-specific error code first, then use SQL driver's get_error method
388
- error_code = getattr(exception, 'pgcode', None)
389
- if not error_code and hasattr(self.sql, 'get_error'):
390
- try:
391
- error_code, error_message_from_driver = self.sql.get_error(exception)
392
- if error_message_from_driver:
393
- error_message = error_message_from_driver
394
- except Exception as get_error_exception:
395
- logger.warning(f"⚠️ SQL driver get_error failed: {get_error_exception}")
396
- error_code = None
397
- except Exception as e:
398
- logger.warning(f"⚠️ Unable to extract database error code: {e}")
399
- error_code = None
400
-
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
401
363
  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
- ]
477
-
478
- # Comprehensive pattern matching with error class mapping
479
- pattern_mappings = [
480
- (connection_patterns, 'DbConnectionError'),
481
- (duplicate_patterns, 'DbDuplicateKeyError'),
482
- (permission_patterns, 'DbPermissionError'),
483
- (not_found_patterns, 'DbTableMissingError'),
484
- (column_missing_patterns, 'DbColumnMissingError'),
485
- (syntax_patterns, 'DbSyntaxError'),
486
- (deadlock_patterns, 'DbDeadlockError')
487
- ]
488
-
489
- # Apply pattern matching
490
- for patterns, error_class in pattern_mappings:
491
- for pattern in patterns:
492
- try:
493
- if re.search(pattern, error_message_lower):
494
- logger.info(f"✅ Classified error by pattern match: '{pattern}' → {error_class}")
495
- raise self._create_exception_with_chaining(
496
- error_class, error_message, exception, sql, parameters
497
- )
498
- except re.error as regex_error:
499
- logger.warning(f"⚠️ Regex pattern error for '{pattern}': {regex_error}")
500
- continue
501
- except Exception as pattern_error:
502
- logger.error(f"❌ Error applying pattern '{pattern}': {pattern_error}")
503
- continue
504
-
505
- # Fallback: return generic database error with full context
506
- available_codes = list(getattr(self, 'error_codes', {}).keys()) if hasattr(self, 'error_codes') else []
507
- logger.warning(
508
- f"⚠️ Unable to classify database error automatically\n"
509
- f" → Falling back to generic DatabaseError\n"
510
- f" → Error Code: {error_code or 'Unknown'}\n"
511
- f" → Available Classifications: {available_codes or 'None configured'}"
512
- )
513
-
514
- raise self._create_exception_with_chaining(
515
- 'DatabaseError', error_message, exception, sql, parameters
516
- )
517
-
518
- def _format_human_readable_error(self, error_class, message, original_exception, sql=None, parameters=None, format_type='console'):
519
- """
520
- Format a human-readable error message with proper context and formatting.
521
-
522
- Args:
523
- error_class: The name of the exception class
524
- message: The raw error message
525
- original_exception: The original exception
526
- sql: The SQL statement (optional)
527
- parameters: The SQL parameters (optional)
528
- format_type: 'console' for terminal output, 'email' for HTML email format
529
-
530
- Returns:
531
- A nicely formatted, human-readable error message
532
- """
533
- if format_type == 'email':
534
- return self._format_email_error(error_class, message, original_exception, sql, parameters)
535
- else:
536
- return self._format_console_error(error_class, message, original_exception, sql, parameters)
537
-
538
- def _format_console_error(self, error_class, message, original_exception, sql=None, parameters=None):
539
- """
540
- Format error message for console/terminal output with Unicode box drawing.
541
- """
542
- # Map error classes to user-friendly descriptions
543
- error_descriptions = {
544
- 'DbColumnMissingError': 'Column Not Found',
545
- 'DbTableMissingError': 'Table Not Found',
546
- 'DbDatabaseMissingError': 'Database Not Found',
547
- 'DbForeignKeyMissingError': 'Foreign Key Constraint Violation',
548
- 'DbDuplicateKeyError': 'Duplicate Key Violation',
549
- 'DbConnectionError': 'Database Connection Failed',
550
- 'DbDataIntegrityError': 'Data Integrity Violation',
551
- 'DbQueryError': 'Query Execution Error',
552
- 'DbTransactionError': 'Transaction Error',
553
- 'DbTruncationError': 'Data Truncation Error',
554
- 'DatabaseError': 'Database Error'
555
- }
556
-
557
- # Get user-friendly error type
558
- friendly_type = error_descriptions.get(error_class, error_class.replace('Db', '').replace('Error', ' Error'))
559
-
560
- # Clean up the original message
561
- clean_message = str(message).strip()
562
-
563
- # Extract specific details from PostgreSQL errors
564
- details = self._extract_error_details(original_exception, clean_message)
565
-
566
- # Build the formatted message
567
- lines = []
568
- lines.append(f"╭─ {friendly_type} ─" + "─" * max(0, 60 - len(friendly_type)))
569
- lines.append("│")
570
-
571
- # Add the main error description
572
- if details.get('description'):
573
- lines.append(f"│ {details['description']}")
574
- else:
575
- lines.append(f"│ {clean_message}")
576
- lines.append("│")
577
-
578
- # Add error code if available
579
- error_code = getattr(original_exception, 'pgcode', None)
580
- if error_code:
581
- lines.append(f"│ Error Code: {error_code}")
582
-
583
- # Add specific details if available
584
- if details.get('column'):
585
- lines.append(f"│ Column: {details['column']}")
586
- if details.get('table'):
587
- lines.append(f"│ Table: {details['table']}")
588
- if details.get('constraint'):
589
- lines.append(f"│ Constraint: {details['constraint']}")
590
- if details.get('hint'):
591
- lines.append(f"│ Hint: {details['hint']}")
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
+ )
592
385
 
593
- # Add SQL context if available
594
386
  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}")
387
+ enhanced_message += (
388
+ f"\n\nSQL Query:\n{self._format_sql_with_params(sql, parameters)}"
389
+ )
683
390
 
684
- lines.append("│ │")
685
- lines.append("│ └─────────────────────────────────────────────────────────")
686
-
687
- lines.append("│")
688
- lines.append("╰" + "─" * 70)
689
-
690
- 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
691
462
 
692
- def _format_email_error(self, error_class, message, original_exception, sql=None, parameters=None):
693
- """
694
- Format error message for email delivery with HTML formatting.
695
- """
696
- # Map error classes to user-friendly descriptions
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('&', '&amp;')
842
- .replace('<', '&lt;')
843
- .replace('>', '&gt;')
844
- .replace('"', '&quot;')
845
- .replace("'", '&#x27;'))
846
-
847
- 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):
848
478
  """
849
- Format SQL with parameters substituted for easy copy-paste debugging.
850
-
851
- Args:
852
- sql: The SQL statement with placeholders
853
- parameters: The parameters to substitute
854
-
855
- Returns:
856
- SQL statement with parameters properly formatted for execution
479
+ Format SQL query with parameters merged for easy copy-paste debugging.
857
480
  """
481
+ if not sql:
482
+ return "No SQL provided"
483
+
858
484
  if not parameters:
859
485
  return sql
860
-
486
+
861
487
  try:
862
488
  # Handle different parameter formats
863
489
  if isinstance(parameters, (list, tuple)):
864
- # For positional parameters (%s style)
865
- formatted_sql = sql
866
-
867
- # Replace %s placeholders with properly formatted values
490
+ # Convert parameters to strings and handle None values
491
+ formatted_params = []
868
492
  for param in parameters:
869
- if isinstance(param, str):
493
+ if param is None:
494
+ formatted_params.append("NULL")
495
+ elif isinstance(param, str):
870
496
  # Escape single quotes and wrap in quotes
871
- formatted_param = f"'{param.replace(chr(39), chr(39)+chr(39))}'"
872
- elif isinstance(param, (int, float)):
873
- formatted_param = str(param)
874
- elif param is None:
875
- formatted_param = 'NULL'
497
+ escaped = param.replace("'", "''")
498
+ formatted_params.append(f"'{escaped}'")
876
499
  elif isinstance(param, bool):
877
- formatted_param = 'TRUE' if param else 'FALSE'
500
+ formatted_params.append("TRUE" if param else "FALSE")
878
501
  else:
879
- # For other types, try to convert to string and quote
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)
884
-
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
+
885
509
  return formatted_sql
886
-
510
+
887
511
  elif isinstance(parameters, dict):
888
- # For named parameters (%(name)s style)
512
+ # Handle named parameters
889
513
  formatted_sql = sql
890
514
  for key, value in parameters.items():
891
- placeholder = f'%({key})s'
892
- if isinstance(value, str):
893
- formatted_value = f"'{value.replace(chr(39), chr(39)+chr(39))}'"
894
- elif isinstance(value, (int, float)):
895
- formatted_value = str(value)
896
- elif value is None:
897
- formatted_value = 'NULL'
515
+ if value is None:
516
+ replacement = "NULL"
517
+ elif isinstance(value, str):
518
+ escaped = value.replace("'", "''")
519
+ replacement = f"'{escaped}'"
898
520
  elif isinstance(value, bool):
899
- formatted_value = 'TRUE' if value else 'FALSE'
521
+ replacement = "TRUE" if value else "FALSE"
900
522
  else:
901
- formatted_value = f"'{str(value).replace(chr(39), chr(39)+chr(39))}'"
902
-
903
- formatted_sql = formatted_sql.replace(placeholder, formatted_value)
904
-
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
+
905
529
  return formatted_sql
906
-
907
530
  else:
908
- # Fallback: just append parameters as comment
909
531
  return f"{sql}\n-- Parameters: {parameters}"
910
-
911
- except Exception:
912
- # If formatting fails, return original with parameters as comment
913
- return f"{sql}\n-- Parameters: {parameters}"
914
532
 
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
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}"
1115
537
  )
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'