fraiseql-confiture 0.3.7__cp311-cp311-macosx_11_0_arm64.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 (124) hide show
  1. confiture/__init__.py +48 -0
  2. confiture/_core.cpython-311-darwin.so +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 +1893 -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 +184 -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 +882 -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 +24 -0
  95. confiture/models/lint.py +193 -0
  96. confiture/models/migration.py +265 -0
  97. confiture/models/schema.py +203 -0
  98. confiture/models/sql_file_migration.py +225 -0
  99. confiture/scenarios/__init__.py +36 -0
  100. confiture/scenarios/compliance.py +586 -0
  101. confiture/scenarios/ecommerce.py +199 -0
  102. confiture/scenarios/financial.py +253 -0
  103. confiture/scenarios/healthcare.py +315 -0
  104. confiture/scenarios/multi_tenant.py +340 -0
  105. confiture/scenarios/saas.py +295 -0
  106. confiture/testing/FRAMEWORK_API.md +722 -0
  107. confiture/testing/__init__.py +100 -0
  108. confiture/testing/fixtures/__init__.py +11 -0
  109. confiture/testing/fixtures/data_validator.py +229 -0
  110. confiture/testing/fixtures/migration_runner.py +167 -0
  111. confiture/testing/fixtures/schema_snapshotter.py +352 -0
  112. confiture/testing/frameworks/__init__.py +10 -0
  113. confiture/testing/frameworks/mutation.py +587 -0
  114. confiture/testing/frameworks/performance.py +479 -0
  115. confiture/testing/loader.py +225 -0
  116. confiture/testing/pytest/__init__.py +38 -0
  117. confiture/testing/pytest_plugin.py +190 -0
  118. confiture/testing/sandbox.py +304 -0
  119. confiture/testing/utils/__init__.py +0 -0
  120. fraiseql_confiture-0.3.7.dist-info/METADATA +438 -0
  121. fraiseql_confiture-0.3.7.dist-info/RECORD +124 -0
  122. fraiseql_confiture-0.3.7.dist-info/WHEEL +4 -0
  123. fraiseql_confiture-0.3.7.dist-info/entry_points.txt +4 -0
  124. fraiseql_confiture-0.3.7.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,38 @@
1
+ """Pytest integration for confiture migration testing.
2
+
3
+ This module provides both the pytest plugin and the migration_test decorator.
4
+
5
+ Usage:
6
+ # Enable the plugin in conftest.py
7
+ pytest_plugins = ["confiture.testing.pytest"]
8
+
9
+ # Use the decorator for migration-specific tests
10
+ from confiture.testing.pytest import migration_test
11
+
12
+ @migration_test("003_move_catalog_tables")
13
+ class TestMigration003:
14
+ def test_up_preserves_data(self, confiture_sandbox, migration):
15
+ migration.up()
16
+ assert confiture_sandbox.validator.constraints_valid()
17
+ """
18
+
19
+ # Re-export from the plugin module
20
+ from confiture.testing.pytest_plugin import (
21
+ confiture_db_url,
22
+ confiture_migrations_dir,
23
+ confiture_sandbox,
24
+ confiture_snapshotter,
25
+ confiture_validator,
26
+ migration_test,
27
+ )
28
+
29
+ __all__ = [
30
+ # Decorator
31
+ "migration_test",
32
+ # Fixtures (for documentation, actual fixtures registered via plugin)
33
+ "confiture_db_url",
34
+ "confiture_migrations_dir",
35
+ "confiture_sandbox",
36
+ "confiture_validator",
37
+ "confiture_snapshotter",
38
+ ]
@@ -0,0 +1,190 @@
1
+ """Pytest plugin for confiture migration testing.
2
+
3
+ This module provides pytest fixtures for migration testing with automatic
4
+ transaction rollback.
5
+
6
+ Usage:
7
+ # In conftest.py
8
+ pytest_plugins = ["confiture.testing.pytest"]
9
+
10
+ # Or enable automatically when confiture is installed (via entry point)
11
+
12
+ Available fixtures:
13
+ - confiture_db_url: Database URL (override to customize)
14
+ - confiture_migrations_dir: Migrations directory (override to customize)
15
+ - confiture_sandbox: MigrationSandbox with automatic rollback
16
+ - confiture_validator: DataValidator from sandbox
17
+ - confiture_snapshotter: SchemaSnapshotter from sandbox
18
+
19
+ Example test file:
20
+ >>> def test_migration(confiture_sandbox):
21
+ ... migration = confiture_sandbox.load("003_move_tables")
22
+ ... migration.up()
23
+ ... assert confiture_sandbox.validator.constraints_valid()
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import os
29
+ from collections.abc import Generator
30
+ from pathlib import Path
31
+ from typing import TYPE_CHECKING
32
+
33
+ import pytest
34
+
35
+ if TYPE_CHECKING:
36
+ from confiture.testing.fixtures.data_validator import DataValidator
37
+ from confiture.testing.fixtures.schema_snapshotter import SchemaSnapshotter
38
+ from confiture.testing.sandbox import MigrationSandbox
39
+
40
+
41
+ @pytest.fixture
42
+ def confiture_db_url() -> str:
43
+ """Provide database URL for migration testing.
44
+
45
+ Override this fixture to use a different database:
46
+
47
+ @pytest.fixture
48
+ def confiture_db_url():
49
+ return "postgresql://localhost/my_test_db"
50
+
51
+ Environment variable CONFITURE_TEST_DB_URL takes precedence.
52
+
53
+ Returns:
54
+ Database connection URL
55
+ """
56
+ return os.getenv(
57
+ "CONFITURE_TEST_DB_URL",
58
+ "postgresql://localhost/confiture_test",
59
+ )
60
+
61
+
62
+ @pytest.fixture
63
+ def confiture_migrations_dir() -> Path:
64
+ """Provide migrations directory for testing.
65
+
66
+ Override this fixture to use a custom migrations directory:
67
+
68
+ @pytest.fixture
69
+ def confiture_migrations_dir():
70
+ return Path("custom/migrations")
71
+
72
+ Returns:
73
+ Path to migrations directory
74
+ """
75
+ return Path("db/migrations")
76
+
77
+
78
+ @pytest.fixture
79
+ def confiture_sandbox(
80
+ confiture_db_url: str,
81
+ confiture_migrations_dir: Path,
82
+ ) -> Generator[MigrationSandbox, None, None]:
83
+ """Provide a migration sandbox with automatic rollback.
84
+
85
+ Creates a MigrationSandbox that automatically rolls back all changes
86
+ at the end of the test.
87
+
88
+ Usage:
89
+ def test_migration(confiture_sandbox):
90
+ migration = confiture_sandbox.load("003_move_tables")
91
+ baseline = confiture_sandbox.capture_baseline()
92
+ migration.up()
93
+ confiture_sandbox.assert_no_data_loss(baseline)
94
+
95
+ Yields:
96
+ MigrationSandbox instance
97
+
98
+ Note:
99
+ This fixture requires a running PostgreSQL database.
100
+ Tests will be skipped if the database is not available.
101
+ """
102
+ from confiture.testing.sandbox import MigrationSandbox
103
+
104
+ try:
105
+ with MigrationSandbox(
106
+ db_url=confiture_db_url,
107
+ migrations_dir=confiture_migrations_dir,
108
+ ) as sandbox:
109
+ yield sandbox
110
+ except Exception as e:
111
+ pytest.skip(f"Database not available: {e}")
112
+
113
+
114
+ @pytest.fixture
115
+ def confiture_validator(confiture_sandbox: MigrationSandbox) -> DataValidator:
116
+ """Provide data validator from sandbox.
117
+
118
+ Convenience fixture that extracts the validator from the sandbox.
119
+
120
+ Usage:
121
+ def test_constraints(confiture_sandbox, confiture_validator):
122
+ confiture_sandbox.load("003").up()
123
+ assert confiture_validator.constraints_valid()
124
+
125
+ Returns:
126
+ DataValidator instance
127
+ """
128
+ return confiture_sandbox.validator
129
+
130
+
131
+ @pytest.fixture
132
+ def confiture_snapshotter(confiture_sandbox: MigrationSandbox) -> SchemaSnapshotter:
133
+ """Provide schema snapshotter from sandbox.
134
+
135
+ Convenience fixture that extracts the snapshotter from the sandbox.
136
+
137
+ Usage:
138
+ def test_schema_changes(confiture_sandbox, confiture_snapshotter):
139
+ before = confiture_snapshotter.capture()
140
+ confiture_sandbox.load("003").up()
141
+ after = confiture_snapshotter.capture()
142
+ changes = confiture_snapshotter.compare(before, after)
143
+ assert "products" in changes["tables_added"]
144
+
145
+ Returns:
146
+ SchemaSnapshotter instance
147
+ """
148
+ return confiture_sandbox.snapshotter
149
+
150
+
151
+ def migration_test(migration_name: str):
152
+ """Decorator to inject migration fixture for specific migration.
153
+
154
+ Use this decorator on test classes to automatically inject a `migration`
155
+ fixture that loads the specified migration.
156
+
157
+ Usage:
158
+ from confiture.testing.pytest import migration_test
159
+
160
+ @migration_test("003_move_catalog_tables")
161
+ class TestMigration003:
162
+ def test_up_preserves_data(self, confiture_sandbox, migration):
163
+ baseline = confiture_sandbox.capture_baseline()
164
+ migration.up()
165
+ confiture_sandbox.assert_no_data_loss(baseline)
166
+
167
+ def test_down_reverses_changes(self, confiture_sandbox, migration):
168
+ migration.up()
169
+ migration.down()
170
+ # Assert schema is back to original state
171
+
172
+ Args:
173
+ migration_name: Name of the migration to load (e.g., "003_move_catalog_tables")
174
+
175
+ Returns:
176
+ Class decorator that adds migration fixture
177
+ """
178
+
179
+ def decorator(cls):
180
+ # Create a migration fixture for this specific test class
181
+ @pytest.fixture
182
+ def migration(self, confiture_sandbox: MigrationSandbox): # noqa: ARG001
183
+ """Migration fixture injected by @migration_test decorator."""
184
+ return confiture_sandbox.load(migration_name)
185
+
186
+ # Add the fixture to the class
187
+ cls.migration = migration
188
+ return cls
189
+
190
+ return decorator
@@ -0,0 +1,304 @@
1
+ """Migration testing sandbox.
2
+
3
+ Provides an isolated environment for testing migrations with automatic
4
+ rollback and pre-loaded testing utilities.
5
+
6
+ Example:
7
+ >>> from confiture.testing import MigrationSandbox
8
+ >>>
9
+ >>> with MigrationSandbox(db_url) as sandbox:
10
+ ... migration = sandbox.load("003_move_tables")
11
+ ... migration.up()
12
+ ... assert sandbox.validator.constraints_valid()
13
+ ...
14
+ >>> # Auto-rollback at end of context
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from pathlib import Path
20
+ from typing import TYPE_CHECKING
21
+
22
+ import psycopg
23
+
24
+ if TYPE_CHECKING:
25
+ from confiture.models.migration import Migration
26
+ from confiture.testing.fixtures.data_validator import DataBaseline, DataValidator
27
+ from confiture.testing.fixtures.schema_snapshotter import SchemaSnapshotter
28
+
29
+
30
+ class MigrationSandbox:
31
+ """Test migrations in isolation with automatic rollback.
32
+
33
+ A context manager that provides a sandboxed environment for migration testing:
34
+ - Automatic transaction management (rollback on exit)
35
+ - Pre-loaded testing utilities (validator, snapshotter)
36
+ - Migration loading via load_migration()
37
+
38
+ The sandbox can work in two modes:
39
+ 1. **URL mode**: Creates a new connection, uses transaction with rollback
40
+ 2. **Connection mode**: Uses existing connection, creates a savepoint for rollback
41
+
42
+ Attributes:
43
+ connection: The database connection being used
44
+ validator: DataValidator for data integrity checks
45
+ snapshotter: SchemaSnapshotter for schema comparison
46
+ migrations_dir: Directory where migrations are located
47
+
48
+ Example with URL:
49
+ >>> with MigrationSandbox("postgresql://localhost/test_db") as sandbox:
50
+ ... migration = sandbox.load("003_move_tables")
51
+ ... baseline = sandbox.capture_baseline()
52
+ ... migration.up()
53
+ ... assert sandbox.validator.no_data_loss(baseline)
54
+
55
+ Example with existing connection:
56
+ >>> with MigrationSandbox(connection=existing_conn) as sandbox:
57
+ ... # Uses savepoint instead of full transaction
58
+ ... migration = sandbox.load("003")
59
+ ... migration.up()
60
+
61
+ Example with custom migrations directory:
62
+ >>> with MigrationSandbox(db_url, migrations_dir=Path("/custom/migrations")) as sandbox:
63
+ ... migration = sandbox.load("001_initial")
64
+ """
65
+
66
+ def __init__(
67
+ self,
68
+ db_url: str | None = None,
69
+ *,
70
+ connection: psycopg.Connection | None = None,
71
+ migrations_dir: Path | None = None,
72
+ ):
73
+ """Initialize the migration sandbox.
74
+
75
+ Args:
76
+ db_url: Database connection URL. Creates a new connection.
77
+ connection: Existing database connection. Uses savepoint for rollback.
78
+ migrations_dir: Custom migrations directory. Defaults to "db/migrations".
79
+
80
+ Raises:
81
+ ValueError: If neither db_url nor connection is provided,
82
+ or if both are provided.
83
+
84
+ Note:
85
+ When using an existing connection, the sandbox creates a savepoint
86
+ that will be rolled back on exit. This preserves the connection's
87
+ transaction state.
88
+ """
89
+ if db_url is None and connection is None:
90
+ raise ValueError("Either 'db_url' or 'connection' must be provided")
91
+ if db_url is not None and connection is not None:
92
+ raise ValueError("Provide either 'db_url' or 'connection', not both")
93
+
94
+ self._db_url = db_url
95
+ self._external_connection = connection
96
+ self._owns_connection = db_url is not None
97
+ self._savepoint_name = "confiture_sandbox"
98
+ self._active = False
99
+
100
+ self.migrations_dir = migrations_dir or Path("db/migrations")
101
+ self.connection: psycopg.Connection = None # type: ignore[assignment]
102
+ self._validator: DataValidator | None = None
103
+ self._snapshotter: SchemaSnapshotter | None = None
104
+
105
+ def __enter__(self) -> MigrationSandbox:
106
+ """Enter the sandbox context.
107
+
108
+ Creates connection (if URL provided) and starts transaction/savepoint.
109
+
110
+ Returns:
111
+ Self for use in with statement
112
+ """
113
+ if self._owns_connection:
114
+ # Create new connection with autocommit=False for transaction control
115
+ assert self._db_url is not None
116
+ self.connection = psycopg.connect(self._db_url, autocommit=False)
117
+ else:
118
+ # Use provided connection, create savepoint
119
+ assert self._external_connection is not None
120
+ self.connection = self._external_connection
121
+ with self.connection.cursor() as cursor:
122
+ cursor.execute(f"SAVEPOINT {self._savepoint_name}")
123
+
124
+ self._active = True
125
+ return self
126
+
127
+ def __exit__(
128
+ self,
129
+ exc_type: type[BaseException] | None,
130
+ exc_val: BaseException | None,
131
+ exc_tb: object,
132
+ ) -> None:
133
+ """Exit the sandbox context.
134
+
135
+ Rolls back all changes and closes connection (if we created it).
136
+
137
+ Args:
138
+ exc_type: Exception type if an error occurred
139
+ exc_val: Exception value
140
+ exc_tb: Exception traceback
141
+ """
142
+ if not self._active:
143
+ return
144
+
145
+ try:
146
+ if self._owns_connection:
147
+ # Rollback entire transaction
148
+ self.connection.rollback()
149
+ self.connection.close()
150
+ else:
151
+ # Rollback to savepoint
152
+ with self.connection.cursor() as cursor:
153
+ cursor.execute(f"ROLLBACK TO SAVEPOINT {self._savepoint_name}")
154
+ cursor.execute(f"RELEASE SAVEPOINT {self._savepoint_name}")
155
+ finally:
156
+ self._active = False
157
+
158
+ def load(self, name: str) -> Migration:
159
+ """Load and instantiate a migration.
160
+
161
+ Args:
162
+ name: Migration name without .py extension (e.g., "003_move_tables")
163
+ or version prefix (e.g., "003")
164
+
165
+ Returns:
166
+ Instantiated Migration object ready to execute
167
+
168
+ Raises:
169
+ MigrationNotFoundError: If migration not found
170
+ MigrationLoadError: If migration cannot be loaded
171
+
172
+ Example:
173
+ >>> migration = sandbox.load("003_move_catalog_tables")
174
+ >>> migration.up()
175
+
176
+ >>> # Also works with version prefix
177
+ >>> migration = sandbox.load("003")
178
+ """
179
+ from confiture.testing.loader import load_migration
180
+
181
+ # Determine if name is a version prefix or full name
182
+ if "_" in name:
183
+ # Full name provided
184
+ migration_class = load_migration(name, migrations_dir=self.migrations_dir)
185
+ else:
186
+ # Version prefix provided
187
+ migration_class = load_migration(version=name, migrations_dir=self.migrations_dir)
188
+
189
+ return migration_class(connection=self.connection)
190
+
191
+ @property
192
+ def validator(self) -> DataValidator:
193
+ """Get data validator for this sandbox.
194
+
195
+ Returns:
196
+ DataValidator configured with the sandbox's connection
197
+
198
+ Example:
199
+ >>> assert sandbox.validator.constraints_valid()
200
+ """
201
+ if self._validator is None:
202
+ from confiture.testing.fixtures.data_validator import DataValidator
203
+
204
+ self._validator = DataValidator(self.connection)
205
+ return self._validator
206
+
207
+ @property
208
+ def snapshotter(self) -> SchemaSnapshotter:
209
+ """Get schema snapshotter for this sandbox.
210
+
211
+ Returns:
212
+ SchemaSnapshotter configured with the sandbox's connection
213
+
214
+ Example:
215
+ >>> before = sandbox.snapshotter.capture()
216
+ >>> migration.up()
217
+ >>> after = sandbox.snapshotter.capture()
218
+ >>> changes = sandbox.snapshotter.compare(before, after)
219
+ """
220
+ if self._snapshotter is None:
221
+ from confiture.testing.fixtures.schema_snapshotter import SchemaSnapshotter
222
+
223
+ self._snapshotter = SchemaSnapshotter(self.connection)
224
+ return self._snapshotter
225
+
226
+ def capture_baseline(self) -> DataBaseline:
227
+ """Capture data baseline before migration.
228
+
229
+ Convenience method that wraps validator.capture_baseline().
230
+
231
+ Returns:
232
+ DataBaseline snapshot for later comparison
233
+
234
+ Example:
235
+ >>> baseline = sandbox.capture_baseline()
236
+ >>> migration.up()
237
+ >>> sandbox.assert_no_data_loss(baseline)
238
+ """
239
+ return self.validator.capture_baseline()
240
+
241
+ def assert_no_data_loss(self, baseline: DataBaseline) -> None:
242
+ """Assert no data was lost since baseline.
243
+
244
+ Convenience method that wraps validator.no_data_loss() with assertion.
245
+
246
+ Args:
247
+ baseline: Baseline captured before migration
248
+
249
+ Raises:
250
+ AssertionError: If data loss is detected
251
+
252
+ Example:
253
+ >>> baseline = sandbox.capture_baseline()
254
+ >>> migration.up()
255
+ >>> sandbox.assert_no_data_loss(baseline) # Raises if data lost
256
+ """
257
+ if not self.validator.no_data_loss(baseline):
258
+ raise AssertionError("Data loss detected after migration")
259
+
260
+ def assert_constraints_valid(self) -> None:
261
+ """Assert all database constraints are valid.
262
+
263
+ Convenience method that wraps validator.constraints_valid() with assertion.
264
+
265
+ Raises:
266
+ AssertionError: If constraint violations are detected
267
+
268
+ Example:
269
+ >>> migration.up()
270
+ >>> sandbox.assert_constraints_valid() # Raises if violations found
271
+ """
272
+ if not self.validator.constraints_valid():
273
+ raise AssertionError("Constraint violations detected after migration")
274
+
275
+ def execute(self, sql: str) -> None:
276
+ """Execute raw SQL in the sandbox.
277
+
278
+ Useful for setting up test data or making assertions.
279
+
280
+ Args:
281
+ sql: SQL to execute
282
+
283
+ Example:
284
+ >>> sandbox.execute("INSERT INTO users (name) VALUES ('test')")
285
+ """
286
+ with self.connection.cursor() as cursor:
287
+ cursor.execute(sql)
288
+
289
+ def query(self, sql: str) -> list[tuple]:
290
+ """Execute a query and return results.
291
+
292
+ Args:
293
+ sql: SQL query to execute
294
+
295
+ Returns:
296
+ List of result rows as tuples
297
+
298
+ Example:
299
+ >>> rows = sandbox.query("SELECT COUNT(*) FROM users")
300
+ >>> assert rows[0][0] > 0
301
+ """
302
+ with self.connection.cursor() as cursor:
303
+ cursor.execute(sql)
304
+ return cursor.fetchall()
File without changes