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,352 @@
|
|
|
1
|
+
"""Schema snapshot utility for migration testing.
|
|
2
|
+
|
|
3
|
+
Captures and compares database schema states to validate migrations work correctly.
|
|
4
|
+
Can be extracted to confiture-testing package in the future.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import psycopg
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class ColumnInfo:
|
|
16
|
+
"""Information about a database column."""
|
|
17
|
+
|
|
18
|
+
name: str
|
|
19
|
+
data_type: str
|
|
20
|
+
is_nullable: bool
|
|
21
|
+
column_default: str | None = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class ConstraintInfo:
|
|
26
|
+
"""Information about a table constraint."""
|
|
27
|
+
|
|
28
|
+
name: str
|
|
29
|
+
constraint_type: str # PRIMARY KEY, FOREIGN KEY, UNIQUE, CHECK, etc.
|
|
30
|
+
columns: list[str] = field(default_factory=list)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class IndexInfo:
|
|
35
|
+
"""Information about a database index."""
|
|
36
|
+
|
|
37
|
+
name: str
|
|
38
|
+
table_name: str
|
|
39
|
+
is_unique: bool
|
|
40
|
+
columns: list[str] = field(default_factory=list)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class ForeignKeyInfo:
|
|
45
|
+
"""Information about a foreign key relationship."""
|
|
46
|
+
|
|
47
|
+
constraint_name: str
|
|
48
|
+
column_name: str
|
|
49
|
+
referenced_table: str
|
|
50
|
+
referenced_column: str
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class TableSchema:
|
|
55
|
+
"""Complete schema information for a single table."""
|
|
56
|
+
|
|
57
|
+
name: str
|
|
58
|
+
schema_name: str
|
|
59
|
+
columns: dict[str, ColumnInfo] = field(default_factory=dict)
|
|
60
|
+
constraints: list[ConstraintInfo] = field(default_factory=list)
|
|
61
|
+
indexes: list[IndexInfo] = field(default_factory=list)
|
|
62
|
+
foreign_keys: list[ForeignKeyInfo] = field(default_factory=list)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class SchemaSnapshot:
|
|
67
|
+
"""Complete snapshot of database schema at a point in time."""
|
|
68
|
+
|
|
69
|
+
tables: dict[str, TableSchema] = field(default_factory=dict)
|
|
70
|
+
views: set[str] = field(default_factory=set)
|
|
71
|
+
materialized_views: set[str] = field(default_factory=set)
|
|
72
|
+
functions: set[str] = field(default_factory=set)
|
|
73
|
+
timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class SchemaChange:
|
|
78
|
+
"""Represents a detected schema change."""
|
|
79
|
+
|
|
80
|
+
change_type: str # added, removed, modified
|
|
81
|
+
object_type: str # table, column, index, constraint, etc.
|
|
82
|
+
object_name: str
|
|
83
|
+
details: dict[str, Any] = field(default_factory=dict)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class SchemaSnapshotter:
|
|
87
|
+
"""Capture and compare database schema states.
|
|
88
|
+
|
|
89
|
+
Generic schema introspection that can be extracted to confiture-testing.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
def __init__(self, connection: psycopg.Connection):
|
|
93
|
+
"""Initialize schema snapshotter.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
connection: PostgreSQL connection for schema introspection
|
|
97
|
+
"""
|
|
98
|
+
self.connection = connection
|
|
99
|
+
|
|
100
|
+
def capture(self) -> SchemaSnapshot:
|
|
101
|
+
"""Capture current schema state.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
SchemaSnapshot with complete schema information
|
|
105
|
+
"""
|
|
106
|
+
snapshot = SchemaSnapshot()
|
|
107
|
+
|
|
108
|
+
with self.connection.cursor() as cur:
|
|
109
|
+
# Get all tables
|
|
110
|
+
cur.execute(
|
|
111
|
+
"""
|
|
112
|
+
SELECT table_schema, table_name
|
|
113
|
+
FROM information_schema.tables
|
|
114
|
+
WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
|
|
115
|
+
ORDER BY table_schema, table_name
|
|
116
|
+
"""
|
|
117
|
+
)
|
|
118
|
+
tables = cur.fetchall()
|
|
119
|
+
|
|
120
|
+
# Capture each table's schema
|
|
121
|
+
for schema_name, table_name in tables:
|
|
122
|
+
table_key = f"{schema_name}.{table_name}"
|
|
123
|
+
snapshot.tables[table_key] = self._capture_table_schema(
|
|
124
|
+
cur, schema_name, table_name
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Get views
|
|
128
|
+
cur.execute(
|
|
129
|
+
"""
|
|
130
|
+
SELECT table_name
|
|
131
|
+
FROM information_schema.views
|
|
132
|
+
WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
|
|
133
|
+
ORDER BY table_name
|
|
134
|
+
"""
|
|
135
|
+
)
|
|
136
|
+
snapshot.views = {row[0] for row in cur.fetchall()}
|
|
137
|
+
|
|
138
|
+
# Get materialized views
|
|
139
|
+
cur.execute("SELECT matviewname FROM pg_matviews WHERE schemaname != 'pg_catalog'")
|
|
140
|
+
snapshot.materialized_views = {row[0] for row in cur.fetchall()}
|
|
141
|
+
|
|
142
|
+
# Get functions
|
|
143
|
+
cur.execute(
|
|
144
|
+
"""
|
|
145
|
+
SELECT routine_name
|
|
146
|
+
FROM information_schema.routines
|
|
147
|
+
WHERE routine_schema NOT IN ('pg_catalog', 'information_schema')
|
|
148
|
+
ORDER BY routine_name
|
|
149
|
+
"""
|
|
150
|
+
)
|
|
151
|
+
snapshot.functions = {row[0] for row in cur.fetchall()}
|
|
152
|
+
|
|
153
|
+
return snapshot
|
|
154
|
+
|
|
155
|
+
def _capture_table_schema(
|
|
156
|
+
self, cur: psycopg.Cursor, schema_name: str, table_name: str
|
|
157
|
+
) -> TableSchema:
|
|
158
|
+
"""Capture schema for a single table.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
cur: Database cursor
|
|
162
|
+
schema_name: Schema name
|
|
163
|
+
table_name: Table name
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
TableSchema with complete table information
|
|
167
|
+
"""
|
|
168
|
+
table = TableSchema(name=table_name, schema_name=schema_name)
|
|
169
|
+
|
|
170
|
+
# Get columns
|
|
171
|
+
cur.execute(
|
|
172
|
+
"""
|
|
173
|
+
SELECT column_name, data_type, is_nullable, column_default
|
|
174
|
+
FROM information_schema.columns
|
|
175
|
+
WHERE table_schema = %s AND table_name = %s
|
|
176
|
+
ORDER BY ordinal_position
|
|
177
|
+
""",
|
|
178
|
+
(schema_name, table_name),
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
for col_name, data_type, is_nullable, col_default in cur.fetchall():
|
|
182
|
+
table.columns[col_name] = ColumnInfo(
|
|
183
|
+
name=col_name,
|
|
184
|
+
data_type=data_type,
|
|
185
|
+
is_nullable=is_nullable == "YES",
|
|
186
|
+
column_default=col_default,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Get constraints
|
|
190
|
+
cur.execute(
|
|
191
|
+
"""
|
|
192
|
+
SELECT constraint_name, constraint_type
|
|
193
|
+
FROM information_schema.table_constraints
|
|
194
|
+
WHERE table_schema = %s AND table_name = %s
|
|
195
|
+
ORDER BY constraint_name
|
|
196
|
+
""",
|
|
197
|
+
(schema_name, table_name),
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
for constraint_name, constraint_type in cur.fetchall():
|
|
201
|
+
# Get columns for this constraint
|
|
202
|
+
cur.execute(
|
|
203
|
+
"""
|
|
204
|
+
SELECT column_name
|
|
205
|
+
FROM information_schema.constraint_column_usage
|
|
206
|
+
WHERE constraint_schema = %s
|
|
207
|
+
AND constraint_name = %s
|
|
208
|
+
ORDER BY column_name
|
|
209
|
+
""",
|
|
210
|
+
(schema_name, constraint_name),
|
|
211
|
+
)
|
|
212
|
+
columns = [row[0] for row in cur.fetchall()]
|
|
213
|
+
|
|
214
|
+
table.constraints.append(
|
|
215
|
+
ConstraintInfo(
|
|
216
|
+
name=constraint_name, constraint_type=constraint_type, columns=columns
|
|
217
|
+
)
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
# Get indexes
|
|
221
|
+
cur.execute(
|
|
222
|
+
"""
|
|
223
|
+
SELECT indexname, indexdef
|
|
224
|
+
FROM pg_indexes
|
|
225
|
+
WHERE schemaname = %s AND tablename = %s
|
|
226
|
+
ORDER BY indexname
|
|
227
|
+
""",
|
|
228
|
+
(schema_name, table_name),
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
for index_name, index_def in cur.fetchall():
|
|
232
|
+
is_unique = "UNIQUE" in (index_def or "").upper()
|
|
233
|
+
table.indexes.append(
|
|
234
|
+
IndexInfo(
|
|
235
|
+
name=index_name,
|
|
236
|
+
table_name=table_name,
|
|
237
|
+
is_unique=is_unique,
|
|
238
|
+
columns=[], # Would need to parse index_def to get columns
|
|
239
|
+
)
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# Get foreign keys
|
|
243
|
+
cur.execute(
|
|
244
|
+
"""
|
|
245
|
+
SELECT
|
|
246
|
+
kcu.constraint_name,
|
|
247
|
+
kcu.column_name,
|
|
248
|
+
ccu.table_name,
|
|
249
|
+
ccu.column_name
|
|
250
|
+
FROM information_schema.key_column_usage kcu
|
|
251
|
+
JOIN information_schema.constraint_column_usage ccu
|
|
252
|
+
ON kcu.constraint_name = ccu.constraint_name
|
|
253
|
+
AND kcu.table_schema = ccu.table_schema
|
|
254
|
+
WHERE kcu.table_schema = %s
|
|
255
|
+
AND kcu.table_name = %s
|
|
256
|
+
AND kcu.constraint_name IN (
|
|
257
|
+
SELECT constraint_name
|
|
258
|
+
FROM information_schema.table_constraints
|
|
259
|
+
WHERE table_schema = %s
|
|
260
|
+
AND table_name = %s
|
|
261
|
+
AND constraint_type = 'FOREIGN KEY'
|
|
262
|
+
)
|
|
263
|
+
""",
|
|
264
|
+
(schema_name, table_name, schema_name, table_name),
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
for constraint_name, col_name, ref_table, ref_col in cur.fetchall():
|
|
268
|
+
table.foreign_keys.append(
|
|
269
|
+
ForeignKeyInfo(
|
|
270
|
+
constraint_name=constraint_name,
|
|
271
|
+
column_name=col_name,
|
|
272
|
+
referenced_table=ref_table,
|
|
273
|
+
referenced_column=ref_col,
|
|
274
|
+
)
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
return table
|
|
278
|
+
|
|
279
|
+
def compare(self, before: SchemaSnapshot, after: SchemaSnapshot) -> dict[str, Any]:
|
|
280
|
+
"""Compare two schema snapshots.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
before: Schema snapshot before migration
|
|
284
|
+
after: Schema snapshot after migration
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
Dictionary of detected changes
|
|
288
|
+
"""
|
|
289
|
+
changes = {
|
|
290
|
+
"tables_added": set(after.tables.keys()) - set(before.tables.keys()),
|
|
291
|
+
"tables_removed": set(before.tables.keys()) - set(after.tables.keys()),
|
|
292
|
+
"tables_modified": [],
|
|
293
|
+
"views_added": after.views - before.views,
|
|
294
|
+
"views_removed": before.views - after.views,
|
|
295
|
+
"mat_views_added": after.materialized_views - before.materialized_views,
|
|
296
|
+
"mat_views_removed": before.materialized_views - after.materialized_views,
|
|
297
|
+
"functions_added": after.functions - before.functions,
|
|
298
|
+
"functions_removed": before.functions - after.functions,
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
# Check for modified tables
|
|
302
|
+
common_tables = set(before.tables.keys()) & set(after.tables.keys())
|
|
303
|
+
for table_name in common_tables:
|
|
304
|
+
before_table = before.tables[table_name]
|
|
305
|
+
after_table = after.tables[table_name]
|
|
306
|
+
|
|
307
|
+
table_changes = {
|
|
308
|
+
"table": table_name,
|
|
309
|
+
"columns_added": set(after_table.columns.keys()) - set(before_table.columns.keys()),
|
|
310
|
+
"columns_removed": set(before_table.columns.keys())
|
|
311
|
+
- set(after_table.columns.keys()),
|
|
312
|
+
"columns_modified": [],
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
# Check for modified columns
|
|
316
|
+
common_cols = set(before_table.columns.keys()) & set(after_table.columns.keys())
|
|
317
|
+
for col_name in common_cols:
|
|
318
|
+
before_col = before_table.columns[col_name]
|
|
319
|
+
after_col = after_table.columns[col_name]
|
|
320
|
+
|
|
321
|
+
if (
|
|
322
|
+
before_col.data_type != after_col.data_type
|
|
323
|
+
or before_col.is_nullable != after_col.is_nullable
|
|
324
|
+
):
|
|
325
|
+
table_changes["columns_modified"].append(
|
|
326
|
+
{
|
|
327
|
+
"column": col_name,
|
|
328
|
+
"before_type": before_col.data_type,
|
|
329
|
+
"after_type": after_col.data_type,
|
|
330
|
+
"before_nullable": before_col.is_nullable,
|
|
331
|
+
"after_nullable": after_col.is_nullable,
|
|
332
|
+
}
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
# Check for constraint changes
|
|
336
|
+
before_constraint_names = {c.name for c in before_table.constraints}
|
|
337
|
+
after_constraint_names = {c.name for c in after_table.constraints}
|
|
338
|
+
|
|
339
|
+
table_changes["constraints_added"] = after_constraint_names - before_constraint_names
|
|
340
|
+
table_changes["constraints_removed"] = before_constraint_names - after_constraint_names
|
|
341
|
+
|
|
342
|
+
# Only add to modified list if there are actual changes
|
|
343
|
+
if (
|
|
344
|
+
table_changes["columns_added"]
|
|
345
|
+
or table_changes["columns_removed"]
|
|
346
|
+
or table_changes["columns_modified"]
|
|
347
|
+
or table_changes["constraints_added"]
|
|
348
|
+
or table_changes["constraints_removed"]
|
|
349
|
+
):
|
|
350
|
+
changes["tables_modified"].append(table_changes)
|
|
351
|
+
|
|
352
|
+
return changes
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Testing frameworks for Confiture migration validation."""
|
|
2
|
+
|
|
3
|
+
from confiture.testing.frameworks.mutation import MutationRegistry, MutationRunner
|
|
4
|
+
from confiture.testing.frameworks.performance import MigrationPerformanceProfiler
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"MutationRegistry",
|
|
8
|
+
"MutationRunner",
|
|
9
|
+
"MigrationPerformanceProfiler",
|
|
10
|
+
]
|