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.
- confiture/__init__.py +48 -0
- confiture/_core.cpython-311-darwin.so +0 -0
- confiture/cli/__init__.py +0 -0
- confiture/cli/dry_run.py +116 -0
- confiture/cli/lint_formatter.py +193 -0
- confiture/cli/main.py +1893 -0
- confiture/config/__init__.py +0 -0
- confiture/config/environment.py +263 -0
- confiture/core/__init__.py +51 -0
- confiture/core/anonymization/__init__.py +0 -0
- confiture/core/anonymization/audit.py +485 -0
- confiture/core/anonymization/benchmarking.py +372 -0
- confiture/core/anonymization/breach_notification.py +652 -0
- confiture/core/anonymization/compliance.py +617 -0
- confiture/core/anonymization/composer.py +298 -0
- confiture/core/anonymization/data_subject_rights.py +669 -0
- confiture/core/anonymization/factory.py +319 -0
- confiture/core/anonymization/governance.py +737 -0
- confiture/core/anonymization/performance.py +1092 -0
- confiture/core/anonymization/profile.py +284 -0
- confiture/core/anonymization/registry.py +195 -0
- confiture/core/anonymization/security/kms_manager.py +547 -0
- confiture/core/anonymization/security/lineage.py +888 -0
- confiture/core/anonymization/security/token_store.py +686 -0
- confiture/core/anonymization/strategies/__init__.py +41 -0
- confiture/core/anonymization/strategies/address.py +359 -0
- confiture/core/anonymization/strategies/credit_card.py +374 -0
- confiture/core/anonymization/strategies/custom.py +161 -0
- confiture/core/anonymization/strategies/date.py +218 -0
- confiture/core/anonymization/strategies/differential_privacy.py +398 -0
- confiture/core/anonymization/strategies/email.py +141 -0
- confiture/core/anonymization/strategies/format_preserving_encryption.py +310 -0
- confiture/core/anonymization/strategies/hash.py +150 -0
- confiture/core/anonymization/strategies/ip_address.py +235 -0
- confiture/core/anonymization/strategies/masking_retention.py +252 -0
- confiture/core/anonymization/strategies/name.py +298 -0
- confiture/core/anonymization/strategies/phone.py +119 -0
- confiture/core/anonymization/strategies/preserve.py +85 -0
- confiture/core/anonymization/strategies/redact.py +101 -0
- confiture/core/anonymization/strategies/salted_hashing.py +322 -0
- confiture/core/anonymization/strategies/text_redaction.py +183 -0
- confiture/core/anonymization/strategies/tokenization.py +334 -0
- confiture/core/anonymization/strategy.py +241 -0
- confiture/core/anonymization/syncer_audit.py +357 -0
- confiture/core/blue_green.py +683 -0
- confiture/core/builder.py +500 -0
- confiture/core/checksum.py +358 -0
- confiture/core/connection.py +184 -0
- confiture/core/differ.py +522 -0
- confiture/core/drift.py +564 -0
- confiture/core/dry_run.py +182 -0
- confiture/core/health.py +313 -0
- confiture/core/hooks/__init__.py +87 -0
- confiture/core/hooks/base.py +232 -0
- confiture/core/hooks/context.py +146 -0
- confiture/core/hooks/execution_strategies.py +57 -0
- confiture/core/hooks/observability.py +220 -0
- confiture/core/hooks/phases.py +53 -0
- confiture/core/hooks/registry.py +295 -0
- confiture/core/large_tables.py +775 -0
- confiture/core/linting/__init__.py +70 -0
- confiture/core/linting/composer.py +192 -0
- confiture/core/linting/libraries/__init__.py +17 -0
- confiture/core/linting/libraries/gdpr.py +168 -0
- confiture/core/linting/libraries/general.py +184 -0
- confiture/core/linting/libraries/hipaa.py +144 -0
- confiture/core/linting/libraries/pci_dss.py +104 -0
- confiture/core/linting/libraries/sox.py +120 -0
- confiture/core/linting/schema_linter.py +491 -0
- confiture/core/linting/versioning.py +151 -0
- confiture/core/locking.py +389 -0
- confiture/core/migration_generator.py +298 -0
- confiture/core/migrator.py +882 -0
- confiture/core/observability/__init__.py +44 -0
- confiture/core/observability/audit.py +323 -0
- confiture/core/observability/logging.py +187 -0
- confiture/core/observability/metrics.py +174 -0
- confiture/core/observability/tracing.py +192 -0
- confiture/core/pg_version.py +418 -0
- confiture/core/pool.py +406 -0
- confiture/core/risk/__init__.py +39 -0
- confiture/core/risk/predictor.py +188 -0
- confiture/core/risk/scoring.py +248 -0
- confiture/core/rollback_generator.py +388 -0
- confiture/core/schema_analyzer.py +769 -0
- confiture/core/schema_to_schema.py +590 -0
- confiture/core/security/__init__.py +32 -0
- confiture/core/security/logging.py +201 -0
- confiture/core/security/validation.py +416 -0
- confiture/core/signals.py +371 -0
- confiture/core/syncer.py +540 -0
- confiture/exceptions.py +192 -0
- confiture/integrations/__init__.py +0 -0
- confiture/models/__init__.py +24 -0
- confiture/models/lint.py +193 -0
- confiture/models/migration.py +265 -0
- confiture/models/schema.py +203 -0
- confiture/models/sql_file_migration.py +225 -0
- confiture/scenarios/__init__.py +36 -0
- confiture/scenarios/compliance.py +586 -0
- confiture/scenarios/ecommerce.py +199 -0
- confiture/scenarios/financial.py +253 -0
- confiture/scenarios/healthcare.py +315 -0
- confiture/scenarios/multi_tenant.py +340 -0
- confiture/scenarios/saas.py +295 -0
- confiture/testing/FRAMEWORK_API.md +722 -0
- confiture/testing/__init__.py +100 -0
- confiture/testing/fixtures/__init__.py +11 -0
- confiture/testing/fixtures/data_validator.py +229 -0
- confiture/testing/fixtures/migration_runner.py +167 -0
- confiture/testing/fixtures/schema_snapshotter.py +352 -0
- confiture/testing/frameworks/__init__.py +10 -0
- confiture/testing/frameworks/mutation.py +587 -0
- confiture/testing/frameworks/performance.py +479 -0
- confiture/testing/loader.py +225 -0
- confiture/testing/pytest/__init__.py +38 -0
- confiture/testing/pytest_plugin.py +190 -0
- confiture/testing/sandbox.py +304 -0
- confiture/testing/utils/__init__.py +0 -0
- fraiseql_confiture-0.3.7.dist-info/METADATA +438 -0
- fraiseql_confiture-0.3.7.dist-info/RECORD +124 -0
- fraiseql_confiture-0.3.7.dist-info/WHEEL +4 -0
- fraiseql_confiture-0.3.7.dist-info/entry_points.txt +4 -0
- 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
|
+
)
|