fraiseql-confiture 0.3.4__cp311-cp311-win_amd64.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 (119) hide show
  1. confiture/__init__.py +48 -0
  2. confiture/_core.cp311-win_amd64.pyd +0 -0
  3. confiture/cli/__init__.py +0 -0
  4. confiture/cli/dry_run.py +116 -0
  5. confiture/cli/lint_formatter.py +193 -0
  6. confiture/cli/main.py +1656 -0
  7. confiture/config/__init__.py +0 -0
  8. confiture/config/environment.py +263 -0
  9. confiture/core/__init__.py +51 -0
  10. confiture/core/anonymization/__init__.py +0 -0
  11. confiture/core/anonymization/audit.py +485 -0
  12. confiture/core/anonymization/benchmarking.py +372 -0
  13. confiture/core/anonymization/breach_notification.py +652 -0
  14. confiture/core/anonymization/compliance.py +617 -0
  15. confiture/core/anonymization/composer.py +298 -0
  16. confiture/core/anonymization/data_subject_rights.py +669 -0
  17. confiture/core/anonymization/factory.py +319 -0
  18. confiture/core/anonymization/governance.py +737 -0
  19. confiture/core/anonymization/performance.py +1092 -0
  20. confiture/core/anonymization/profile.py +284 -0
  21. confiture/core/anonymization/registry.py +195 -0
  22. confiture/core/anonymization/security/kms_manager.py +547 -0
  23. confiture/core/anonymization/security/lineage.py +888 -0
  24. confiture/core/anonymization/security/token_store.py +686 -0
  25. confiture/core/anonymization/strategies/__init__.py +41 -0
  26. confiture/core/anonymization/strategies/address.py +359 -0
  27. confiture/core/anonymization/strategies/credit_card.py +374 -0
  28. confiture/core/anonymization/strategies/custom.py +161 -0
  29. confiture/core/anonymization/strategies/date.py +218 -0
  30. confiture/core/anonymization/strategies/differential_privacy.py +398 -0
  31. confiture/core/anonymization/strategies/email.py +141 -0
  32. confiture/core/anonymization/strategies/format_preserving_encryption.py +310 -0
  33. confiture/core/anonymization/strategies/hash.py +150 -0
  34. confiture/core/anonymization/strategies/ip_address.py +235 -0
  35. confiture/core/anonymization/strategies/masking_retention.py +252 -0
  36. confiture/core/anonymization/strategies/name.py +298 -0
  37. confiture/core/anonymization/strategies/phone.py +119 -0
  38. confiture/core/anonymization/strategies/preserve.py +85 -0
  39. confiture/core/anonymization/strategies/redact.py +101 -0
  40. confiture/core/anonymization/strategies/salted_hashing.py +322 -0
  41. confiture/core/anonymization/strategies/text_redaction.py +183 -0
  42. confiture/core/anonymization/strategies/tokenization.py +334 -0
  43. confiture/core/anonymization/strategy.py +241 -0
  44. confiture/core/anonymization/syncer_audit.py +357 -0
  45. confiture/core/blue_green.py +683 -0
  46. confiture/core/builder.py +500 -0
  47. confiture/core/checksum.py +358 -0
  48. confiture/core/connection.py +132 -0
  49. confiture/core/differ.py +522 -0
  50. confiture/core/drift.py +564 -0
  51. confiture/core/dry_run.py +182 -0
  52. confiture/core/health.py +313 -0
  53. confiture/core/hooks/__init__.py +87 -0
  54. confiture/core/hooks/base.py +232 -0
  55. confiture/core/hooks/context.py +146 -0
  56. confiture/core/hooks/execution_strategies.py +57 -0
  57. confiture/core/hooks/observability.py +220 -0
  58. confiture/core/hooks/phases.py +53 -0
  59. confiture/core/hooks/registry.py +295 -0
  60. confiture/core/large_tables.py +775 -0
  61. confiture/core/linting/__init__.py +70 -0
  62. confiture/core/linting/composer.py +192 -0
  63. confiture/core/linting/libraries/__init__.py +17 -0
  64. confiture/core/linting/libraries/gdpr.py +168 -0
  65. confiture/core/linting/libraries/general.py +184 -0
  66. confiture/core/linting/libraries/hipaa.py +144 -0
  67. confiture/core/linting/libraries/pci_dss.py +104 -0
  68. confiture/core/linting/libraries/sox.py +120 -0
  69. confiture/core/linting/schema_linter.py +491 -0
  70. confiture/core/linting/versioning.py +151 -0
  71. confiture/core/locking.py +389 -0
  72. confiture/core/migration_generator.py +298 -0
  73. confiture/core/migrator.py +793 -0
  74. confiture/core/observability/__init__.py +44 -0
  75. confiture/core/observability/audit.py +323 -0
  76. confiture/core/observability/logging.py +187 -0
  77. confiture/core/observability/metrics.py +174 -0
  78. confiture/core/observability/tracing.py +192 -0
  79. confiture/core/pg_version.py +418 -0
  80. confiture/core/pool.py +406 -0
  81. confiture/core/risk/__init__.py +39 -0
  82. confiture/core/risk/predictor.py +188 -0
  83. confiture/core/risk/scoring.py +248 -0
  84. confiture/core/rollback_generator.py +388 -0
  85. confiture/core/schema_analyzer.py +769 -0
  86. confiture/core/schema_to_schema.py +590 -0
  87. confiture/core/security/__init__.py +32 -0
  88. confiture/core/security/logging.py +201 -0
  89. confiture/core/security/validation.py +416 -0
  90. confiture/core/signals.py +371 -0
  91. confiture/core/syncer.py +540 -0
  92. confiture/exceptions.py +192 -0
  93. confiture/integrations/__init__.py +0 -0
  94. confiture/models/__init__.py +0 -0
  95. confiture/models/lint.py +193 -0
  96. confiture/models/migration.py +180 -0
  97. confiture/models/schema.py +203 -0
  98. confiture/scenarios/__init__.py +36 -0
  99. confiture/scenarios/compliance.py +586 -0
  100. confiture/scenarios/ecommerce.py +199 -0
  101. confiture/scenarios/financial.py +253 -0
  102. confiture/scenarios/healthcare.py +315 -0
  103. confiture/scenarios/multi_tenant.py +340 -0
  104. confiture/scenarios/saas.py +295 -0
  105. confiture/testing/FRAMEWORK_API.md +722 -0
  106. confiture/testing/__init__.py +38 -0
  107. confiture/testing/fixtures/__init__.py +11 -0
  108. confiture/testing/fixtures/data_validator.py +229 -0
  109. confiture/testing/fixtures/migration_runner.py +167 -0
  110. confiture/testing/fixtures/schema_snapshotter.py +352 -0
  111. confiture/testing/frameworks/__init__.py +10 -0
  112. confiture/testing/frameworks/mutation.py +587 -0
  113. confiture/testing/frameworks/performance.py +479 -0
  114. confiture/testing/utils/__init__.py +0 -0
  115. fraiseql_confiture-0.3.4.dist-info/METADATA +438 -0
  116. fraiseql_confiture-0.3.4.dist-info/RECORD +119 -0
  117. fraiseql_confiture-0.3.4.dist-info/WHEEL +4 -0
  118. fraiseql_confiture-0.3.4.dist-info/entry_points.txt +2 -0
  119. fraiseql_confiture-0.3.4.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,192 @@
1
+ """Confiture exception hierarchy
2
+
3
+ All exceptions raised by Confiture inherit from ConfiturError.
4
+ This allows users to catch all Confiture-specific errors with a single except clause.
5
+ """
6
+
7
+
8
+ class ConfiturError(Exception):
9
+ """Base exception for all Confiture errors
10
+
11
+ All Confiture-specific exceptions inherit from this base class.
12
+ This allows catching all Confiture errors with:
13
+
14
+ try:
15
+ confiture.build()
16
+ except ConfiturError as e:
17
+ # Handle any Confiture error
18
+ pass
19
+ """
20
+
21
+ pass
22
+
23
+
24
+ class ConfigurationError(ConfiturError):
25
+ """Invalid configuration (YAML, environment, database connection)
26
+
27
+ Raised when:
28
+ - Environment YAML file is malformed or missing
29
+ - Required configuration fields are missing
30
+ - Database connection string is invalid
31
+ - Include/exclude directory patterns are invalid
32
+
33
+ Example:
34
+ >>> raise ConfigurationError("Missing database_url in local.yaml")
35
+ """
36
+
37
+ pass
38
+
39
+
40
+ class MigrationError(ConfiturError):
41
+ """Migration execution failure
42
+
43
+ Raised when:
44
+ - Migration file cannot be loaded
45
+ - Migration up() or down() fails
46
+ - Migration has already been applied
47
+ - Migration rollback fails
48
+
49
+ Attributes:
50
+ version: Migration version that failed (e.g., "001")
51
+ migration_name: Human-readable migration name
52
+ """
53
+
54
+ def __init__(
55
+ self,
56
+ message: str,
57
+ version: str | None = None,
58
+ migration_name: str | None = None,
59
+ ):
60
+ super().__init__(message)
61
+ self.version = version
62
+ self.migration_name = migration_name
63
+
64
+
65
+ class SchemaError(ConfiturError):
66
+ """Invalid schema DDL or schema build failure
67
+
68
+ Raised when:
69
+ - SQL syntax error in DDL files
70
+ - Missing required schema directories
71
+ - Circular dependencies between schema files
72
+ - Schema hash computation fails
73
+
74
+ Example:
75
+ >>> raise SchemaError("Syntax error in 10_tables/users.sql at line 15")
76
+ """
77
+
78
+ pass
79
+
80
+
81
+ class SyncError(ConfiturError):
82
+ """Production data sync failure
83
+
84
+ Raised when:
85
+ - Cannot connect to source database
86
+ - Table does not exist in source or target
87
+ - Anonymization rule fails
88
+ - Data copy operation fails
89
+
90
+ Example:
91
+ >>> raise SyncError("Table 'users' not found in source database")
92
+ """
93
+
94
+ pass
95
+
96
+
97
+ class DifferError(ConfiturError):
98
+ """Schema diff detection error
99
+
100
+ Raised when:
101
+ - Cannot parse SQL DDL
102
+ - Schema comparison fails
103
+ - Ambiguous schema changes detected
104
+
105
+ Example:
106
+ >>> raise DifferError("Cannot parse CREATE TABLE statement")
107
+ """
108
+
109
+ pass
110
+
111
+
112
+ class ValidationError(ConfiturError):
113
+ """Data or schema validation error
114
+
115
+ Raised when:
116
+ - Row count mismatch after migration
117
+ - Foreign key constraints violated
118
+ - Custom validation rules fail
119
+
120
+ Example:
121
+ >>> raise ValidationError("Row count mismatch: expected 10000, got 9999")
122
+ """
123
+
124
+ pass
125
+
126
+
127
+ class RollbackError(ConfiturError):
128
+ """Migration rollback failure
129
+
130
+ Raised when:
131
+ - Cannot rollback migration (irreversible change)
132
+ - Rollback SQL fails
133
+ - Database state is inconsistent after rollback
134
+
135
+ This is a critical error that may require manual intervention.
136
+
137
+ Example:
138
+ >>> raise RollbackError("Cannot rollback: data already deleted")
139
+ """
140
+
141
+ pass
142
+
143
+
144
+ class SQLError(ConfiturError):
145
+ """SQL execution error with detailed context
146
+
147
+ Raised when:
148
+ - SQL statement fails during migration execution
149
+ - Provides context about which SQL statement failed
150
+ - Includes original SQL and error details
151
+
152
+ Attributes:
153
+ sql: The SQL statement that failed
154
+ params: Query parameters (if any)
155
+ original_error: The underlying database error
156
+
157
+ Example:
158
+ >>> raise SQLError(
159
+ ... "CREATE TABLE users (id INT PRIMARY KEY, name TEXT)",
160
+ ... None,
161
+ ... psycopg_error
162
+ ... )
163
+ """
164
+
165
+ def __init__(
166
+ self,
167
+ sql: str,
168
+ params: tuple[str, ...] | None,
169
+ original_error: Exception,
170
+ ):
171
+ self.sql = sql
172
+ self.params = params
173
+ self.original_error = original_error
174
+
175
+ # Create detailed error message
176
+ message_parts = ["SQL execution failed"]
177
+
178
+ # Add SQL snippet (first 100 chars)
179
+ sql_preview = sql.strip()[:100]
180
+ if len(sql.strip()) > 100:
181
+ sql_preview += "..."
182
+ message_parts.append(f"SQL: {sql_preview}")
183
+
184
+ # Add parameters if present
185
+ if params:
186
+ message_parts.append(f"Parameters: {params}")
187
+
188
+ # Add original error
189
+ message_parts.append(f"Error: {original_error}")
190
+
191
+ message = " | ".join(message_parts)
192
+ super().__init__(message)
File without changes
File without changes
@@ -0,0 +1,193 @@
1
+ """Linting models for schema validation.
2
+
3
+ This module provides data structures for schema linting including:
4
+ - Violation: A single schema quality issue
5
+ - LintSeverity: Severity level of violations
6
+ - LintConfig: Configuration for linting rules
7
+ - LintReport: Aggregated linting results
8
+ """
9
+
10
+ from dataclasses import dataclass, field
11
+ from enum import Enum
12
+ from typing import Any
13
+
14
+
15
+ class LintSeverity(str, Enum):
16
+ """Severity levels for linting violations.
17
+
18
+ Attributes:
19
+ ERROR: Blocking issue - must fix before migration
20
+ WARNING: Should fix but optional
21
+ INFO: Informational only
22
+ """
23
+
24
+ ERROR = "error"
25
+ WARNING = "warning"
26
+ INFO = "info"
27
+
28
+
29
+ @dataclass
30
+ class Violation:
31
+ """A single schema quality violation.
32
+
33
+ Attributes:
34
+ rule_name: Name of the rule that detected this violation
35
+ severity: Severity level (ERROR, WARNING, INFO)
36
+ message: Human-readable description of the issue
37
+ location: Where the violation occurred (table name, column, etc.)
38
+ suggested_fix: Optional suggestion on how to fix it
39
+ """
40
+
41
+ rule_name: str
42
+ severity: LintSeverity
43
+ message: str
44
+ location: str
45
+ suggested_fix: str | None = None
46
+
47
+ def __str__(self) -> str:
48
+ """Format violation for human consumption."""
49
+ return f"[{self.severity.upper()}] {self.location}: {self.message}"
50
+
51
+ def __repr__(self) -> str:
52
+ """Return repr for debugging."""
53
+ return (
54
+ f"Violation(rule={self.rule_name}, severity={self.severity}, location={self.location})"
55
+ )
56
+
57
+
58
+ @dataclass
59
+ class LintConfig:
60
+ """Configuration for schema linting.
61
+
62
+ Attributes:
63
+ enabled: Whether linting is enabled
64
+ rules: Dict mapping rule names to their configs
65
+ fail_on_error: Exit with error code if violations found
66
+ fail_on_warning: Exit with error code if warnings found (stricter)
67
+ exclude_tables: List of table name patterns to exclude from linting
68
+ """
69
+
70
+ enabled: bool = True
71
+ rules: dict[str, Any] = field(default_factory=dict)
72
+ fail_on_error: bool = True
73
+ fail_on_warning: bool = False
74
+ exclude_tables: list[str] = field(default_factory=list)
75
+
76
+ @classmethod
77
+ def default(cls) -> "LintConfig":
78
+ """Create LintConfig with sensible defaults for all rules.
79
+
80
+ Returns:
81
+ LintConfig with all 6 rules enabled with default settings
82
+
83
+ Example:
84
+ >>> config = LintConfig.default()
85
+ >>> config.rules.keys()
86
+ dict_keys(['naming_convention', 'primary_key', ...])
87
+ """
88
+ return cls(
89
+ enabled=True,
90
+ fail_on_error=True,
91
+ fail_on_warning=False,
92
+ rules={
93
+ "naming_convention": {
94
+ "enabled": True,
95
+ "style": "snake_case",
96
+ },
97
+ "primary_key": {
98
+ "enabled": True,
99
+ },
100
+ "documentation": {
101
+ "enabled": True,
102
+ },
103
+ "multi_tenant": {
104
+ "enabled": True,
105
+ "identifier": "tenant_id",
106
+ },
107
+ "missing_index": {
108
+ "enabled": True,
109
+ },
110
+ "security": {
111
+ "enabled": True,
112
+ },
113
+ },
114
+ )
115
+
116
+
117
+ @dataclass
118
+ class LintReport:
119
+ """Results of a complete linting pass.
120
+
121
+ Attributes:
122
+ violations: List of all violations found
123
+ schema_name: Name of schema that was linted
124
+ tables_checked: Total number of tables checked
125
+ columns_checked: Total number of columns checked
126
+ errors_count: Number of ERROR level violations
127
+ warnings_count: Number of WARNING level violations
128
+ info_count: Number of INFO level violations
129
+ execution_time_ms: Time taken to lint in milliseconds
130
+ """
131
+
132
+ violations: list[Violation]
133
+ schema_name: str
134
+ tables_checked: int
135
+ columns_checked: int
136
+ errors_count: int
137
+ warnings_count: int
138
+ info_count: int
139
+ execution_time_ms: int
140
+
141
+ @property
142
+ def has_errors(self) -> bool:
143
+ """Whether there are any ERROR level violations.
144
+
145
+ Returns:
146
+ True if errors_count > 0, False otherwise
147
+ """
148
+ return self.errors_count > 0
149
+
150
+ @property
151
+ def has_warnings(self) -> bool:
152
+ """Whether there are any WARNING level violations.
153
+
154
+ Returns:
155
+ True if warnings_count > 0, False otherwise
156
+ """
157
+ return self.warnings_count > 0
158
+
159
+ def violations_by_severity(self) -> dict[LintSeverity, list[Violation]]:
160
+ """Group violations by their severity level.
161
+
162
+ Returns:
163
+ Dict mapping LintSeverity to list of violations at that level
164
+
165
+ Example:
166
+ >>> report.violations_by_severity()
167
+ {
168
+ <LintSeverity.ERROR: 'error'>: [Violation(...), ...],
169
+ <LintSeverity.WARNING: 'warning'>: [...],
170
+ <LintSeverity.INFO: 'info'>: [...],
171
+ }
172
+ """
173
+ grouped: dict[LintSeverity, list[Violation]] = {}
174
+
175
+ for severity in LintSeverity:
176
+ grouped[severity] = [v for v in self.violations if v.severity == severity]
177
+
178
+ return grouped
179
+
180
+ def __str__(self) -> str:
181
+ """Format report for human consumption.
182
+
183
+ Returns:
184
+ Multi-line string with summary of linting results
185
+ """
186
+ tables_with_violations = {v.location.split(".")[0] for v in self.violations}
187
+ lines = [
188
+ f"Schema: {self.schema_name}",
189
+ f"Tables: {self.tables_checked} checked, {len(tables_with_violations)} with violations",
190
+ f"Violations: {self.errors_count} errors, {self.warnings_count} warnings, {self.info_count} info",
191
+ f"Time: {self.execution_time_ms}ms",
192
+ ]
193
+ return "\n".join(lines)
@@ -0,0 +1,180 @@
1
+ """Migration base class for database migrations."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ import psycopg
7
+
8
+ if TYPE_CHECKING:
9
+ from confiture.core.hooks import Hook
10
+
11
+
12
+ class Migration(ABC):
13
+ """Base class for all database migrations.
14
+
15
+ Each migration must:
16
+ - Define a version (e.g., "001", "002")
17
+ - Define a name (e.g., "create_users")
18
+ - Implement up() method for applying the migration
19
+ - Implement down() method for rolling back the migration
20
+
21
+ Migrations can optionally define hooks that execute before/after DDL:
22
+ - before_validation_hooks: Pre-flight checks before migration
23
+ - before_ddl_hooks: Data prep before structural changes
24
+ - after_ddl_hooks: Data backfill after structural changes
25
+ - after_validation_hooks: Verification after data operations
26
+ - cleanup_hooks: Final cleanup operations
27
+ - error_hooks: Error handlers during rollback
28
+
29
+ Transaction Control:
30
+ By default, migrations run inside a transaction with savepoints.
31
+ Set `transactional = False` for operations that cannot run in
32
+ a transaction, such as:
33
+ - CREATE INDEX CONCURRENTLY
34
+ - DROP INDEX CONCURRENTLY
35
+ - VACUUM
36
+ - REINDEX CONCURRENTLY
37
+
38
+ WARNING: Non-transactional migrations cannot be automatically
39
+ rolled back if they fail. Manual cleanup may be required.
40
+
41
+ Example:
42
+ >>> class CreateUsers(Migration):
43
+ ... version = "001"
44
+ ... name = "create_users"
45
+ ...
46
+ ... def up(self):
47
+ ... self.execute('''
48
+ ... CREATE TABLE users (
49
+ ... id SERIAL PRIMARY KEY,
50
+ ... username TEXT NOT NULL
51
+ ... )
52
+ ... ''')
53
+ ...
54
+ ... def down(self):
55
+ ... self.execute('DROP TABLE users')
56
+
57
+ Example with hooks:
58
+ >>> class AddAnalyticsTable(Migration):
59
+ ... version = "002"
60
+ ... name = "add_analytics_table"
61
+ ... after_ddl_hooks = [BackfillAnalyticsHook()]
62
+ ...
63
+ ... def up(self):
64
+ ... self.execute('CREATE TABLE analytics (...)')
65
+ ...
66
+ ... def down(self):
67
+ ... self.execute('DROP TABLE analytics')
68
+
69
+ Example non-transactional (CREATE INDEX CONCURRENTLY):
70
+ >>> class AddSearchIndex(Migration):
71
+ ... version = "015"
72
+ ... name = "add_search_index"
73
+ ... transactional = False # Required for CONCURRENTLY
74
+ ...
75
+ ... def up(self):
76
+ ... self.execute('CREATE INDEX CONCURRENTLY idx_search ON products(name)')
77
+ ...
78
+ ... def down(self):
79
+ ... self.execute('DROP INDEX CONCURRENTLY IF EXISTS idx_search')
80
+ """
81
+
82
+ # Subclasses must define these
83
+ version: str
84
+ name: str
85
+
86
+ # Configuration attributes
87
+ transactional: bool = True # Default: run in transaction with savepoints
88
+ strict_mode: bool = False # Default: lenient error handling
89
+
90
+ # Hook attributes (optional, default to empty lists)
91
+ before_validation_hooks: list["Hook"] = []
92
+ before_ddl_hooks: list["Hook"] = []
93
+ after_ddl_hooks: list["Hook"] = []
94
+ after_validation_hooks: list["Hook"] = []
95
+ cleanup_hooks: list["Hook"] = []
96
+ error_hooks: list["Hook"] = []
97
+
98
+ def __init__(self, connection: psycopg.Connection):
99
+ """Initialize migration with database connection.
100
+
101
+ Args:
102
+ connection: psycopg3 database connection
103
+
104
+ Raises:
105
+ TypeError: If version or name not defined in subclass
106
+ """
107
+ self.connection = connection
108
+
109
+ # Ensure subclass defined version and name
110
+ if not hasattr(self.__class__, "version") or self.__class__.version is None:
111
+ raise TypeError(f"{self.__class__.__name__} must define a 'version' class attribute")
112
+ if not hasattr(self.__class__, "name") or self.__class__.name is None:
113
+ raise TypeError(f"{self.__class__.__name__} must define a 'name' class attribute")
114
+
115
+ @abstractmethod
116
+ def up(self) -> None:
117
+ """Apply the migration.
118
+
119
+ This method must be implemented by subclasses to perform
120
+ the forward migration (e.g., CREATE TABLE, ALTER TABLE).
121
+
122
+ Raises:
123
+ NotImplementedError: If not implemented by subclass
124
+ """
125
+ raise NotImplementedError(f"{self.__class__.__name__}.up() must be implemented")
126
+
127
+ @abstractmethod
128
+ def down(self) -> None:
129
+ """Rollback the migration.
130
+
131
+ This method must be implemented by subclasses to perform
132
+ the reverse migration (e.g., DROP TABLE, revert ALTER).
133
+
134
+ Raises:
135
+ NotImplementedError: If not implemented by subclass
136
+ """
137
+ raise NotImplementedError(f"{self.__class__.__name__}.down() must be implemented")
138
+
139
+ def execute(self, sql: str, params: tuple[Any, ...] | None = None) -> None:
140
+ """Execute a SQL statement.
141
+
142
+ In strict mode:
143
+ - Validates statement success explicitly
144
+ - May check for PostgreSQL warnings (future enhancement)
145
+
146
+ In normal mode:
147
+ - Only fails on actual errors (default)
148
+ - Ignores notices and warnings
149
+
150
+ Args:
151
+ sql: SQL statement to execute
152
+ params: Optional query parameters (for parameterized queries)
153
+
154
+ Raises:
155
+ SQLError: If SQL execution fails, with detailed context
156
+
157
+ Example:
158
+ >>> self.execute("CREATE TABLE users (id INT)")
159
+ >>> self.execute("INSERT INTO users (name) VALUES (%s)", ("Alice",))
160
+ """
161
+ from confiture.exceptions import SQLError
162
+
163
+ try:
164
+ with self.connection.cursor() as cursor:
165
+ if params:
166
+ cursor.execute(sql, params)
167
+ else:
168
+ cursor.execute(sql)
169
+
170
+ # In strict mode, we could check for warnings here
171
+ # For now, this is a placeholder for future enhancement
172
+ if self.strict_mode:
173
+ # TODO: Implement warning detection
174
+ # PostgreSQL notices are sent via connection.notices
175
+ # or through a notice handler
176
+ pass
177
+
178
+ except Exception as e:
179
+ # Wrap the error with SQL context
180
+ raise SQLError(sql, params, e) from e