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,418 @@
1
+ """PostgreSQL version detection and feature flags.
2
+
3
+ Provides utilities for detecting PostgreSQL version and checking
4
+ feature availability across versions 12-17.
5
+ """
6
+
7
+ import logging
8
+ import re
9
+ from dataclasses import dataclass
10
+ from enum import Enum
11
+ from typing import Any
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class PGFeature(Enum):
17
+ """PostgreSQL features by minimum required version.
18
+
19
+ Each feature is associated with the minimum PostgreSQL major version
20
+ that supports it.
21
+ """
22
+
23
+ # PostgreSQL 12 features
24
+ GENERATED_COLUMNS = 12
25
+ REINDEX_CONCURRENTLY = 12
26
+ JSON_PATH = 12
27
+ CTE_MATERIALIZATION_HINT = 12
28
+
29
+ # PostgreSQL 13 features
30
+ VACUUM_PARALLEL = 13
31
+ INCREMENTAL_SORT = 13
32
+ TRUSTED_EXTENSIONS = 13
33
+ HASH_AGGREGATE_MEMORY = 13
34
+
35
+ # PostgreSQL 14 features
36
+ MULTIRANGE_TYPES = 14
37
+ OUT_PARAMS_IN_PROCEDURES = 14
38
+ DETACH_PARTITION_CONCURRENTLY = 14
39
+ COMPRESSION_LZ4 = 14
40
+
41
+ # PostgreSQL 15 features
42
+ LOGICAL_REPLICATION_ROW_FILTER = 15
43
+ MERGE_STATEMENT = 15
44
+ SECURITY_INVOKER_VIEWS = 15
45
+ UNIQUE_NULLS_NOT_DISTINCT = 15
46
+ JSON_LOGS = 15
47
+
48
+ # PostgreSQL 16 features
49
+ JSON_IS_JSON = 16
50
+ PARALLEL_FULL_OUTER_JOIN = 16
51
+ LOGICAL_REPLICATION_FROM_STANDBY = 16
52
+ EXTENSION_SET_SCHEMA = 16
53
+
54
+ # PostgreSQL 17 features (upcoming/current)
55
+ INCREMENTAL_BACKUP = 17
56
+ LOGICAL_REPLICATION_FAILOVER = 17
57
+
58
+
59
+ @dataclass
60
+ class PGVersionInfo:
61
+ """PostgreSQL version information.
62
+
63
+ Example:
64
+ >>> info = PGVersionInfo(major=15, minor=4, full_version="PostgreSQL 15.4")
65
+ >>> info.supports(PGFeature.MERGE_STATEMENT)
66
+ True
67
+ >>> info.supports(PGFeature.JSON_IS_JSON)
68
+ False
69
+ """
70
+
71
+ major: int
72
+ minor: int
73
+ patch: int = 0
74
+ full_version: str = ""
75
+
76
+ def supports(self, feature: PGFeature) -> bool:
77
+ """Check if this version supports a feature.
78
+
79
+ Args:
80
+ feature: Feature to check
81
+
82
+ Returns:
83
+ True if feature is supported
84
+ """
85
+ return self.major >= feature.value
86
+
87
+ def is_at_least(self, major: int, minor: int = 0) -> bool:
88
+ """Check if version is at least the specified version.
89
+
90
+ Args:
91
+ major: Minimum major version
92
+ minor: Minimum minor version
93
+
94
+ Returns:
95
+ True if version meets requirement
96
+ """
97
+ if self.major > major:
98
+ return True
99
+ if self.major == major:
100
+ return self.minor >= minor
101
+ return False
102
+
103
+ @property
104
+ def version_tuple(self) -> tuple[int, int, int]:
105
+ """Get version as tuple."""
106
+ return (self.major, self.minor, self.patch)
107
+
108
+ def __str__(self) -> str:
109
+ """String representation."""
110
+ return f"{self.major}.{self.minor}.{self.patch}"
111
+
112
+
113
+ def detect_version(connection: Any) -> PGVersionInfo:
114
+ """Detect PostgreSQL version from connection.
115
+
116
+ Args:
117
+ connection: Database connection
118
+
119
+ Returns:
120
+ PGVersionInfo with detected version
121
+
122
+ Example:
123
+ >>> info = detect_version(conn)
124
+ >>> print(f"PostgreSQL {info.major}.{info.minor}")
125
+ PostgreSQL 15.4
126
+ """
127
+ with connection.cursor() as cur:
128
+ # Get full version string
129
+ cur.execute("SELECT version()")
130
+ version_str = cur.fetchone()[0]
131
+
132
+ # Get numeric version
133
+ cur.execute("SHOW server_version_num")
134
+ version_num = int(cur.fetchone()[0])
135
+
136
+ # Parse version number (e.g., 150004 = 15.0.4)
137
+ major = version_num // 10000
138
+ minor = (version_num % 10000) // 100
139
+ patch = version_num % 100
140
+
141
+ return PGVersionInfo(
142
+ major=major,
143
+ minor=minor,
144
+ patch=patch,
145
+ full_version=version_str,
146
+ )
147
+
148
+
149
+ def parse_version_string(version_str: str) -> PGVersionInfo:
150
+ """Parse a PostgreSQL version string.
151
+
152
+ Args:
153
+ version_str: Version string like "PostgreSQL 15.4" or "15.4"
154
+
155
+ Returns:
156
+ PGVersionInfo parsed from string
157
+
158
+ Example:
159
+ >>> info = parse_version_string("PostgreSQL 15.4.2")
160
+ >>> info.major
161
+ 15
162
+ """
163
+ # Extract version numbers
164
+ match = re.search(r"(\d+)\.(\d+)(?:\.(\d+))?", version_str)
165
+ if not match:
166
+ raise ValueError(f"Could not parse version from: {version_str}")
167
+
168
+ major = int(match.group(1))
169
+ minor = int(match.group(2))
170
+ patch = int(match.group(3)) if match.group(3) else 0
171
+
172
+ return PGVersionInfo(
173
+ major=major,
174
+ minor=minor,
175
+ patch=patch,
176
+ full_version=version_str,
177
+ )
178
+
179
+
180
+ class VersionAwareSQL:
181
+ """Generate version-specific SQL.
182
+
183
+ Provides utilities for generating SQL that adapts to the
184
+ PostgreSQL version.
185
+
186
+ Example:
187
+ >>> sql = VersionAwareSQL(version_info)
188
+ >>> sql.create_index_concurrently("idx_users_email", "users", ["email"])
189
+ 'CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_users_email ON users (email)'
190
+ """
191
+
192
+ def __init__(self, version: PGVersionInfo):
193
+ """Initialize with version info.
194
+
195
+ Args:
196
+ version: PostgreSQL version information
197
+ """
198
+ self.version = version
199
+
200
+ def reindex_concurrently(self, index_name: str) -> str:
201
+ """Generate REINDEX CONCURRENTLY statement.
202
+
203
+ Note: REINDEX CONCURRENTLY is only available in PG 12+.
204
+
205
+ Args:
206
+ index_name: Name of index to rebuild
207
+
208
+ Returns:
209
+ SQL statement
210
+ """
211
+ if self.version.supports(PGFeature.REINDEX_CONCURRENTLY):
212
+ return f"REINDEX INDEX CONCURRENTLY {index_name}"
213
+ else:
214
+ logger.warning(
215
+ f"REINDEX CONCURRENTLY not available in PG {self.version.major}, "
216
+ "using regular REINDEX (will block writes)"
217
+ )
218
+ return f"REINDEX INDEX {index_name}"
219
+
220
+ def vacuum_parallel(self, table: str, parallel_workers: int = 2) -> str:
221
+ """Generate VACUUM with parallel workers.
222
+
223
+ Note: VACUUM (PARALLEL n) is only available in PG 13+.
224
+
225
+ Args:
226
+ table: Table to vacuum
227
+ parallel_workers: Number of parallel workers
228
+
229
+ Returns:
230
+ SQL statement
231
+ """
232
+ if self.version.supports(PGFeature.VACUUM_PARALLEL):
233
+ return f"VACUUM (PARALLEL {parallel_workers}) {table}"
234
+ else:
235
+ logger.warning(
236
+ f"VACUUM PARALLEL not available in PG {self.version.major}, "
237
+ "using single-threaded VACUUM"
238
+ )
239
+ return f"VACUUM {table}"
240
+
241
+ def create_index_concurrently(
242
+ self,
243
+ index_name: str,
244
+ table: str,
245
+ columns: list[str],
246
+ unique: bool = False,
247
+ where: str | None = None,
248
+ ) -> str:
249
+ """Generate CREATE INDEX CONCURRENTLY statement.
250
+
251
+ Args:
252
+ index_name: Name for the new index
253
+ table: Table to create index on
254
+ columns: Columns to include in index
255
+ unique: Whether index should be unique
256
+ where: Optional WHERE clause for partial index
257
+
258
+ Returns:
259
+ SQL statement
260
+ """
261
+ unique_str = "UNIQUE " if unique else ""
262
+ columns_str = ", ".join(columns)
263
+ where_str = f" WHERE {where}" if where else ""
264
+
265
+ return (
266
+ f"CREATE {unique_str}INDEX CONCURRENTLY IF NOT EXISTS "
267
+ f"{index_name} ON {table} ({columns_str}){where_str}"
268
+ )
269
+
270
+ def merge_statement(
271
+ self,
272
+ target: str,
273
+ source: str,
274
+ on_condition: str,
275
+ when_matched: str | None = None,
276
+ when_not_matched: str | None = None,
277
+ ) -> str | None:
278
+ """Generate MERGE statement (PG 15+).
279
+
280
+ Args:
281
+ target: Target table
282
+ source: Source table or subquery
283
+ on_condition: Join condition
284
+ when_matched: Action when matched
285
+ when_not_matched: Action when not matched
286
+
287
+ Returns:
288
+ SQL statement or None if not supported
289
+ """
290
+ if not self.version.supports(PGFeature.MERGE_STATEMENT):
291
+ logger.warning(
292
+ f"MERGE not available in PG {self.version.major}, "
293
+ "use INSERT ON CONFLICT or manual upsert"
294
+ )
295
+ return None
296
+
297
+ parts = [f"MERGE INTO {target}", f"USING {source}", f"ON {on_condition}"]
298
+
299
+ if when_matched:
300
+ parts.append(f"WHEN MATCHED THEN {when_matched}")
301
+ if when_not_matched:
302
+ parts.append(f"WHEN NOT MATCHED THEN {when_not_matched}")
303
+
304
+ return " ".join(parts)
305
+
306
+ def add_column_with_default_fast(
307
+ self, table: str, column: str, column_type: str, default: str
308
+ ) -> str:
309
+ """Generate ADD COLUMN with DEFAULT.
310
+
311
+ In PG 11+, adding a column with a default is instant for
312
+ most data types (doesn't rewrite table).
313
+
314
+ Args:
315
+ table: Table name
316
+ column: Column name
317
+ column_type: Column type
318
+ default: Default value expression
319
+
320
+ Returns:
321
+ SQL statement
322
+ """
323
+ # PG 11+ supports instant add column with default
324
+ return (
325
+ f"ALTER TABLE {table} ADD COLUMN IF NOT EXISTS {column} {column_type} DEFAULT {default}"
326
+ )
327
+
328
+ def unique_nulls_not_distinct(
329
+ self, table: str, column: str, constraint_name: str
330
+ ) -> str | None:
331
+ """Generate UNIQUE constraint treating NULLs as equal (PG 15+).
332
+
333
+ Args:
334
+ table: Table name
335
+ column: Column name
336
+ constraint_name: Constraint name
337
+
338
+ Returns:
339
+ SQL statement or None if not supported
340
+ """
341
+ if not self.version.supports(PGFeature.UNIQUE_NULLS_NOT_DISTINCT):
342
+ logger.warning(f"NULLS NOT DISTINCT not available in PG {self.version.major}")
343
+ return None
344
+
345
+ return (
346
+ f"ALTER TABLE {table} ADD CONSTRAINT {constraint_name} "
347
+ f"UNIQUE NULLS NOT DISTINCT ({column})"
348
+ )
349
+
350
+ def detach_partition_concurrently(self, parent: str, partition: str) -> str:
351
+ """Generate DETACH PARTITION CONCURRENTLY (PG 14+).
352
+
353
+ Args:
354
+ parent: Parent table
355
+ partition: Partition to detach
356
+
357
+ Returns:
358
+ SQL statement (may block if not supported)
359
+ """
360
+ if self.version.supports(PGFeature.DETACH_PARTITION_CONCURRENTLY):
361
+ return f"ALTER TABLE {parent} DETACH PARTITION {partition} CONCURRENTLY"
362
+ else:
363
+ logger.warning(
364
+ f"DETACH PARTITION CONCURRENTLY not available in PG {self.version.major}, "
365
+ "using blocking DETACH"
366
+ )
367
+ return f"ALTER TABLE {parent} DETACH PARTITION {partition}"
368
+
369
+
370
+ def get_recommended_settings(version: PGVersionInfo) -> dict[str, str]:
371
+ """Get recommended settings for a PostgreSQL version.
372
+
373
+ Args:
374
+ version: PostgreSQL version
375
+
376
+ Returns:
377
+ Dictionary of setting name to recommended value
378
+ """
379
+ settings = {
380
+ "statement_timeout": "30s",
381
+ "lock_timeout": "10s",
382
+ "idle_in_transaction_session_timeout": "60s",
383
+ }
384
+
385
+ # Version-specific settings
386
+ if version.is_at_least(13):
387
+ settings["hash_mem_multiplier"] = "2.0"
388
+
389
+ if version.is_at_least(14):
390
+ settings["client_connection_check_interval"] = "1s"
391
+
392
+ if version.is_at_least(15):
393
+ settings["log_min_duration_statement"] = "1s"
394
+
395
+ return settings
396
+
397
+
398
+ def check_version_compatibility(
399
+ version: PGVersionInfo, min_version: tuple[int, int] = (12, 0)
400
+ ) -> tuple[bool, str | None]:
401
+ """Check if version meets minimum requirements.
402
+
403
+ Args:
404
+ version: Detected version
405
+ min_version: Minimum required (major, minor)
406
+
407
+ Returns:
408
+ Tuple of (is_compatible, error_message)
409
+ """
410
+ min_major, min_minor = min_version
411
+
412
+ if version.is_at_least(min_major, min_minor):
413
+ return True, None
414
+
415
+ return False, (
416
+ f"PostgreSQL {version.major}.{version.minor} is not supported. "
417
+ f"Minimum required version is {min_major}.{min_minor}."
418
+ )