kailash 0.3.2__py3-none-any.whl → 0.4.0__py3-none-any.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.
- kailash/__init__.py +33 -1
- kailash/access_control/__init__.py +129 -0
- kailash/access_control/managers.py +461 -0
- kailash/access_control/rule_evaluators.py +467 -0
- kailash/access_control_abac.py +825 -0
- kailash/config/__init__.py +27 -0
- kailash/config/database_config.py +359 -0
- kailash/database/__init__.py +28 -0
- kailash/database/execution_pipeline.py +499 -0
- kailash/middleware/__init__.py +306 -0
- kailash/middleware/auth/__init__.py +33 -0
- kailash/middleware/auth/access_control.py +436 -0
- kailash/middleware/auth/auth_manager.py +422 -0
- kailash/middleware/auth/jwt_auth.py +477 -0
- kailash/middleware/auth/kailash_jwt_auth.py +616 -0
- kailash/middleware/communication/__init__.py +37 -0
- kailash/middleware/communication/ai_chat.py +989 -0
- kailash/middleware/communication/api_gateway.py +802 -0
- kailash/middleware/communication/events.py +470 -0
- kailash/middleware/communication/realtime.py +710 -0
- kailash/middleware/core/__init__.py +21 -0
- kailash/middleware/core/agent_ui.py +890 -0
- kailash/middleware/core/schema.py +643 -0
- kailash/middleware/core/workflows.py +396 -0
- kailash/middleware/database/__init__.py +63 -0
- kailash/middleware/database/base.py +113 -0
- kailash/middleware/database/base_models.py +525 -0
- kailash/middleware/database/enums.py +106 -0
- kailash/middleware/database/migrations.py +12 -0
- kailash/{api/database.py → middleware/database/models.py} +183 -291
- kailash/middleware/database/repositories.py +685 -0
- kailash/middleware/database/session_manager.py +19 -0
- kailash/middleware/mcp/__init__.py +38 -0
- kailash/middleware/mcp/client_integration.py +585 -0
- kailash/middleware/mcp/enhanced_server.py +576 -0
- kailash/nodes/__init__.py +25 -3
- kailash/nodes/admin/__init__.py +35 -0
- kailash/nodes/admin/audit_log.py +794 -0
- kailash/nodes/admin/permission_check.py +864 -0
- kailash/nodes/admin/role_management.py +823 -0
- kailash/nodes/admin/security_event.py +1519 -0
- kailash/nodes/admin/user_management.py +944 -0
- kailash/nodes/ai/a2a.py +24 -7
- kailash/nodes/ai/ai_providers.py +1 -0
- kailash/nodes/ai/embedding_generator.py +11 -11
- kailash/nodes/ai/intelligent_agent_orchestrator.py +99 -11
- kailash/nodes/ai/llm_agent.py +407 -2
- kailash/nodes/ai/self_organizing.py +85 -10
- kailash/nodes/api/auth.py +287 -6
- kailash/nodes/api/rest.py +151 -0
- kailash/nodes/auth/__init__.py +17 -0
- kailash/nodes/auth/directory_integration.py +1228 -0
- kailash/nodes/auth/enterprise_auth_provider.py +1328 -0
- kailash/nodes/auth/mfa.py +2338 -0
- kailash/nodes/auth/risk_assessment.py +872 -0
- kailash/nodes/auth/session_management.py +1093 -0
- kailash/nodes/auth/sso.py +1040 -0
- kailash/nodes/base.py +344 -13
- kailash/nodes/base_cycle_aware.py +4 -2
- kailash/nodes/base_with_acl.py +1 -1
- kailash/nodes/code/python.py +283 -10
- kailash/nodes/compliance/__init__.py +9 -0
- kailash/nodes/compliance/data_retention.py +1888 -0
- kailash/nodes/compliance/gdpr.py +2004 -0
- kailash/nodes/data/__init__.py +22 -2
- kailash/nodes/data/async_connection.py +469 -0
- kailash/nodes/data/async_sql.py +757 -0
- kailash/nodes/data/async_vector.py +598 -0
- kailash/nodes/data/readers.py +767 -0
- kailash/nodes/data/retrieval.py +360 -1
- kailash/nodes/data/sharepoint_graph.py +397 -21
- kailash/nodes/data/sql.py +94 -5
- kailash/nodes/data/streaming.py +68 -8
- kailash/nodes/data/vector_db.py +54 -4
- kailash/nodes/enterprise/__init__.py +13 -0
- kailash/nodes/enterprise/batch_processor.py +741 -0
- kailash/nodes/enterprise/data_lineage.py +497 -0
- kailash/nodes/logic/convergence.py +31 -9
- kailash/nodes/logic/operations.py +14 -3
- kailash/nodes/mixins/__init__.py +8 -0
- kailash/nodes/mixins/event_emitter.py +201 -0
- kailash/nodes/mixins/mcp.py +9 -4
- kailash/nodes/mixins/security.py +165 -0
- kailash/nodes/monitoring/__init__.py +7 -0
- kailash/nodes/monitoring/performance_benchmark.py +2497 -0
- kailash/nodes/rag/__init__.py +284 -0
- kailash/nodes/rag/advanced.py +1615 -0
- kailash/nodes/rag/agentic.py +773 -0
- kailash/nodes/rag/conversational.py +999 -0
- kailash/nodes/rag/evaluation.py +875 -0
- kailash/nodes/rag/federated.py +1188 -0
- kailash/nodes/rag/graph.py +721 -0
- kailash/nodes/rag/multimodal.py +671 -0
- kailash/nodes/rag/optimized.py +933 -0
- kailash/nodes/rag/privacy.py +1059 -0
- kailash/nodes/rag/query_processing.py +1335 -0
- kailash/nodes/rag/realtime.py +764 -0
- kailash/nodes/rag/registry.py +547 -0
- kailash/nodes/rag/router.py +837 -0
- kailash/nodes/rag/similarity.py +1854 -0
- kailash/nodes/rag/strategies.py +566 -0
- kailash/nodes/rag/workflows.py +575 -0
- kailash/nodes/security/__init__.py +19 -0
- kailash/nodes/security/abac_evaluator.py +1411 -0
- kailash/nodes/security/audit_log.py +91 -0
- kailash/nodes/security/behavior_analysis.py +1893 -0
- kailash/nodes/security/credential_manager.py +401 -0
- kailash/nodes/security/rotating_credentials.py +760 -0
- kailash/nodes/security/security_event.py +132 -0
- kailash/nodes/security/threat_detection.py +1103 -0
- kailash/nodes/testing/__init__.py +9 -0
- kailash/nodes/testing/credential_testing.py +499 -0
- kailash/nodes/transform/__init__.py +10 -2
- kailash/nodes/transform/chunkers.py +592 -1
- kailash/nodes/transform/processors.py +484 -14
- kailash/nodes/validation.py +321 -0
- kailash/runtime/access_controlled.py +1 -1
- kailash/runtime/async_local.py +41 -7
- kailash/runtime/docker.py +1 -1
- kailash/runtime/local.py +474 -55
- kailash/runtime/parallel.py +1 -1
- kailash/runtime/parallel_cyclic.py +1 -1
- kailash/runtime/testing.py +210 -2
- kailash/utils/migrations/__init__.py +25 -0
- kailash/utils/migrations/generator.py +433 -0
- kailash/utils/migrations/models.py +231 -0
- kailash/utils/migrations/runner.py +489 -0
- kailash/utils/secure_logging.py +342 -0
- kailash/workflow/__init__.py +16 -0
- kailash/workflow/cyclic_runner.py +3 -4
- kailash/workflow/graph.py +70 -2
- kailash/workflow/resilience.py +249 -0
- kailash/workflow/templates.py +726 -0
- {kailash-0.3.2.dist-info → kailash-0.4.0.dist-info}/METADATA +253 -20
- kailash-0.4.0.dist-info/RECORD +223 -0
- kailash/api/__init__.py +0 -17
- kailash/api/__main__.py +0 -6
- kailash/api/studio_secure.py +0 -893
- kailash/mcp/__main__.py +0 -13
- kailash/mcp/server_new.py +0 -336
- kailash/mcp/servers/__init__.py +0 -12
- kailash-0.3.2.dist-info/RECORD +0 -136
- {kailash-0.3.2.dist-info → kailash-0.4.0.dist-info}/WHEEL +0 -0
- {kailash-0.3.2.dist-info → kailash-0.4.0.dist-info}/entry_points.txt +0 -0
- {kailash-0.3.2.dist-info → kailash-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.3.2.dist-info → kailash-0.4.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,489 @@
|
|
1
|
+
"""Migration runner for executing database migrations."""
|
2
|
+
|
3
|
+
import asyncio
|
4
|
+
import logging
|
5
|
+
import time
|
6
|
+
from datetime import UTC, datetime
|
7
|
+
from typing import Any, Dict, List, Optional, Set, Type
|
8
|
+
|
9
|
+
from kailash.nodes.data.async_connection import get_connection_manager
|
10
|
+
from kailash.utils.migrations.models import Migration, MigrationHistory, MigrationPlan
|
11
|
+
|
12
|
+
logger = logging.getLogger(__name__)
|
13
|
+
|
14
|
+
|
15
|
+
class MigrationRunner:
|
16
|
+
"""Executes database migrations with dependency management.
|
17
|
+
|
18
|
+
This class handles the execution of migrations, tracking their
|
19
|
+
status, managing dependencies, and providing rollback capabilities.
|
20
|
+
|
21
|
+
Example:
|
22
|
+
>>> runner = MigrationRunner(db_config)
|
23
|
+
>>> await runner.initialize()
|
24
|
+
>>>
|
25
|
+
>>> # Apply all pending migrations
|
26
|
+
>>> plan = await runner.create_plan()
|
27
|
+
>>> await runner.execute_plan(plan)
|
28
|
+
>>>
|
29
|
+
>>> # Rollback specific migration
|
30
|
+
>>> await runner.rollback_migration("001_add_user_table")
|
31
|
+
"""
|
32
|
+
|
33
|
+
def __init__(
|
34
|
+
self,
|
35
|
+
db_config: Dict[str, Any],
|
36
|
+
tenant_id: str = "default",
|
37
|
+
migration_table: str = "kailash_migrations",
|
38
|
+
):
|
39
|
+
"""Initialize migration runner.
|
40
|
+
|
41
|
+
Args:
|
42
|
+
db_config: Database configuration
|
43
|
+
tenant_id: Tenant identifier for multi-tenant systems
|
44
|
+
migration_table: Table name for tracking migrations
|
45
|
+
"""
|
46
|
+
self.db_config = db_config
|
47
|
+
self.tenant_id = tenant_id
|
48
|
+
self.migration_table = migration_table
|
49
|
+
self.connection_manager = get_connection_manager()
|
50
|
+
self.registered_migrations: Dict[str, Type[Migration]] = {}
|
51
|
+
self._initialized = False
|
52
|
+
|
53
|
+
async def initialize(self):
|
54
|
+
"""Initialize migration tracking table."""
|
55
|
+
if self._initialized:
|
56
|
+
return
|
57
|
+
|
58
|
+
async with self.connection_manager.get_connection(
|
59
|
+
self.tenant_id, self.db_config
|
60
|
+
) as conn:
|
61
|
+
# Create migration tracking table
|
62
|
+
await self._create_migration_table(conn)
|
63
|
+
|
64
|
+
self._initialized = True
|
65
|
+
logger.info(f"Migration runner initialized for tenant {self.tenant_id}")
|
66
|
+
|
67
|
+
async def _create_migration_table(self, conn: Any):
|
68
|
+
"""Create migration tracking table if not exists."""
|
69
|
+
db_type = self.db_config.get("type", "postgresql")
|
70
|
+
|
71
|
+
if db_type == "postgresql":
|
72
|
+
query = f"""
|
73
|
+
CREATE TABLE IF NOT EXISTS {self.migration_table} (
|
74
|
+
migration_id VARCHAR(255) PRIMARY KEY,
|
75
|
+
applied_at TIMESTAMP NOT NULL,
|
76
|
+
applied_by VARCHAR(255) NOT NULL,
|
77
|
+
execution_time FLOAT NOT NULL,
|
78
|
+
success BOOLEAN NOT NULL,
|
79
|
+
error_message TEXT,
|
80
|
+
rollback_at TIMESTAMP,
|
81
|
+
rollback_by VARCHAR(255),
|
82
|
+
migration_hash VARCHAR(32)
|
83
|
+
)
|
84
|
+
"""
|
85
|
+
elif db_type == "mysql":
|
86
|
+
query = f"""
|
87
|
+
CREATE TABLE IF NOT EXISTS {self.migration_table} (
|
88
|
+
migration_id VARCHAR(255) PRIMARY KEY,
|
89
|
+
applied_at TIMESTAMP NOT NULL,
|
90
|
+
applied_by VARCHAR(255) NOT NULL,
|
91
|
+
execution_time FLOAT NOT NULL,
|
92
|
+
success BOOLEAN NOT NULL,
|
93
|
+
error_message TEXT,
|
94
|
+
rollback_at TIMESTAMP NULL,
|
95
|
+
rollback_by VARCHAR(255),
|
96
|
+
migration_hash VARCHAR(32)
|
97
|
+
)
|
98
|
+
"""
|
99
|
+
elif db_type == "sqlite":
|
100
|
+
query = f"""
|
101
|
+
CREATE TABLE IF NOT EXISTS {self.migration_table} (
|
102
|
+
migration_id TEXT PRIMARY KEY,
|
103
|
+
applied_at TEXT NOT NULL,
|
104
|
+
applied_by TEXT NOT NULL,
|
105
|
+
execution_time REAL NOT NULL,
|
106
|
+
success INTEGER NOT NULL,
|
107
|
+
error_message TEXT,
|
108
|
+
rollback_at TEXT,
|
109
|
+
rollback_by TEXT,
|
110
|
+
migration_hash TEXT
|
111
|
+
)
|
112
|
+
"""
|
113
|
+
else:
|
114
|
+
raise ValueError(f"Unsupported database type: {db_type}")
|
115
|
+
|
116
|
+
await conn.execute(query)
|
117
|
+
|
118
|
+
def register_migration(self, migration_class: Type[Migration]):
|
119
|
+
"""Register a migration class.
|
120
|
+
|
121
|
+
Args:
|
122
|
+
migration_class: Migration class to register
|
123
|
+
"""
|
124
|
+
instance = migration_class()
|
125
|
+
if instance.id in self.registered_migrations:
|
126
|
+
raise ValueError(f"Migration {instance.id} already registered")
|
127
|
+
|
128
|
+
self.registered_migrations[instance.id] = migration_class
|
129
|
+
logger.debug(f"Registered migration: {instance.id}")
|
130
|
+
|
131
|
+
def register_migrations_from_module(self, module: Any):
|
132
|
+
"""Register all migrations from a module.
|
133
|
+
|
134
|
+
Args:
|
135
|
+
module: Python module containing Migration subclasses
|
136
|
+
"""
|
137
|
+
for attr_name in dir(module):
|
138
|
+
attr = getattr(module, attr_name)
|
139
|
+
if (
|
140
|
+
isinstance(attr, type)
|
141
|
+
and issubclass(attr, Migration)
|
142
|
+
and attr != Migration
|
143
|
+
):
|
144
|
+
self.register_migration(attr)
|
145
|
+
|
146
|
+
async def get_applied_migrations(self) -> Set[str]:
|
147
|
+
"""Get set of applied migration IDs."""
|
148
|
+
if not self._initialized:
|
149
|
+
await self.initialize()
|
150
|
+
|
151
|
+
async with self.connection_manager.get_connection(
|
152
|
+
self.tenant_id, self.db_config
|
153
|
+
) as conn:
|
154
|
+
query = f"""
|
155
|
+
SELECT migration_id
|
156
|
+
FROM {self.migration_table}
|
157
|
+
WHERE success = true AND rollback_at IS NULL
|
158
|
+
"""
|
159
|
+
|
160
|
+
rows = await conn.fetch(query)
|
161
|
+
return {row["migration_id"] for row in rows}
|
162
|
+
|
163
|
+
async def get_migration_history(
|
164
|
+
self, migration_id: Optional[str] = None
|
165
|
+
) -> List[MigrationHistory]:
|
166
|
+
"""Get migration history.
|
167
|
+
|
168
|
+
Args:
|
169
|
+
migration_id: Optional specific migration to get history for
|
170
|
+
|
171
|
+
Returns:
|
172
|
+
List of migration history records
|
173
|
+
"""
|
174
|
+
if not self._initialized:
|
175
|
+
await self.initialize()
|
176
|
+
|
177
|
+
async with self.connection_manager.get_connection(
|
178
|
+
self.tenant_id, self.db_config
|
179
|
+
) as conn:
|
180
|
+
if migration_id:
|
181
|
+
query = f"""
|
182
|
+
SELECT * FROM {self.migration_table}
|
183
|
+
WHERE migration_id = $1
|
184
|
+
ORDER BY applied_at DESC
|
185
|
+
"""
|
186
|
+
rows = await conn.fetch(query, migration_id)
|
187
|
+
else:
|
188
|
+
query = f"""
|
189
|
+
SELECT * FROM {self.migration_table}
|
190
|
+
ORDER BY applied_at DESC
|
191
|
+
"""
|
192
|
+
rows = await conn.fetch(query)
|
193
|
+
|
194
|
+
history = []
|
195
|
+
for row in rows:
|
196
|
+
history.append(
|
197
|
+
MigrationHistory(
|
198
|
+
migration_id=row["migration_id"],
|
199
|
+
applied_at=row["applied_at"],
|
200
|
+
applied_by=row["applied_by"],
|
201
|
+
execution_time=row["execution_time"],
|
202
|
+
success=row["success"],
|
203
|
+
error_message=row.get("error_message"),
|
204
|
+
rollback_at=row.get("rollback_at"),
|
205
|
+
rollback_by=row.get("rollback_by"),
|
206
|
+
)
|
207
|
+
)
|
208
|
+
|
209
|
+
return history
|
210
|
+
|
211
|
+
async def create_plan(
|
212
|
+
self, target_migration: Optional[str] = None, rollback: bool = False
|
213
|
+
) -> MigrationPlan:
|
214
|
+
"""Create execution plan for migrations.
|
215
|
+
|
216
|
+
Args:
|
217
|
+
target_migration: Optional specific migration to target
|
218
|
+
rollback: Whether to create rollback plan
|
219
|
+
|
220
|
+
Returns:
|
221
|
+
Migration execution plan
|
222
|
+
"""
|
223
|
+
if not self._initialized:
|
224
|
+
await self.initialize()
|
225
|
+
|
226
|
+
plan = MigrationPlan()
|
227
|
+
applied = await self.get_applied_migrations()
|
228
|
+
|
229
|
+
if rollback:
|
230
|
+
# Create rollback plan
|
231
|
+
if not target_migration:
|
232
|
+
raise ValueError("target_migration required for rollback")
|
233
|
+
|
234
|
+
if target_migration not in applied:
|
235
|
+
plan.add_warning(f"Migration {target_migration} not applied")
|
236
|
+
return plan
|
237
|
+
|
238
|
+
# TODO: Implement dependency checking for rollback
|
239
|
+
migration_class = self.registered_migrations.get(target_migration)
|
240
|
+
if migration_class:
|
241
|
+
plan.migrations_to_rollback.append(migration_class())
|
242
|
+
else:
|
243
|
+
# Create forward migration plan
|
244
|
+
pending = []
|
245
|
+
|
246
|
+
for migration_id, migration_class in self.registered_migrations.items():
|
247
|
+
if migration_id not in applied:
|
248
|
+
instance = migration_class()
|
249
|
+
|
250
|
+
# Check dependencies
|
251
|
+
missing_deps = [
|
252
|
+
dep
|
253
|
+
for dep in instance.dependencies
|
254
|
+
if dep not in applied and dep not in self.registered_migrations
|
255
|
+
]
|
256
|
+
|
257
|
+
if missing_deps:
|
258
|
+
plan.add_warning(
|
259
|
+
f"Migration {migration_id} has missing dependencies: "
|
260
|
+
f"{', '.join(missing_deps)}"
|
261
|
+
)
|
262
|
+
else:
|
263
|
+
pending.append(instance)
|
264
|
+
|
265
|
+
# Sort by dependencies
|
266
|
+
plan.migrations_to_apply = self._topological_sort(pending)
|
267
|
+
plan.dependency_order = [m.id for m in plan.migrations_to_apply]
|
268
|
+
|
269
|
+
# Stop at target if specified
|
270
|
+
if target_migration and target_migration in plan.dependency_order:
|
271
|
+
idx = plan.dependency_order.index(target_migration)
|
272
|
+
plan.migrations_to_apply = plan.migrations_to_apply[: idx + 1]
|
273
|
+
plan.dependency_order = plan.dependency_order[: idx + 1]
|
274
|
+
|
275
|
+
# Estimate execution time (rough estimate)
|
276
|
+
plan.estimated_time = len(plan.migrations_to_apply) * 2.0
|
277
|
+
|
278
|
+
return plan
|
279
|
+
|
280
|
+
def _topological_sort(self, migrations: List[Migration]) -> List[Migration]:
|
281
|
+
"""Sort migrations by dependencies."""
|
282
|
+
# Build dependency graph
|
283
|
+
graph: Dict[str, Set[str]] = {}
|
284
|
+
migration_map = {m.id: m for m in migrations}
|
285
|
+
|
286
|
+
for migration in migrations:
|
287
|
+
graph[migration.id] = set(
|
288
|
+
dep for dep in migration.dependencies if dep in migration_map
|
289
|
+
)
|
290
|
+
|
291
|
+
# Kahn's algorithm
|
292
|
+
sorted_migrations = []
|
293
|
+
no_deps = [m_id for m_id, deps in graph.items() if not deps]
|
294
|
+
|
295
|
+
while no_deps:
|
296
|
+
current = no_deps.pop(0)
|
297
|
+
sorted_migrations.append(migration_map[current])
|
298
|
+
|
299
|
+
# Remove current from dependencies
|
300
|
+
for m_id, deps in graph.items():
|
301
|
+
if current in deps:
|
302
|
+
deps.remove(current)
|
303
|
+
if not deps and m_id not in [m.id for m in sorted_migrations]:
|
304
|
+
no_deps.append(m_id)
|
305
|
+
|
306
|
+
# Check for cycles
|
307
|
+
if len(sorted_migrations) != len(migrations):
|
308
|
+
remaining = set(m.id for m in migrations) - set(
|
309
|
+
m.id for m in sorted_migrations
|
310
|
+
)
|
311
|
+
raise ValueError(f"Circular dependencies detected: {remaining}")
|
312
|
+
|
313
|
+
return sorted_migrations
|
314
|
+
|
315
|
+
async def execute_plan(
|
316
|
+
self, plan: MigrationPlan, dry_run: bool = False, user: str = "system"
|
317
|
+
) -> List[MigrationHistory]:
|
318
|
+
"""Execute migration plan.
|
319
|
+
|
320
|
+
Args:
|
321
|
+
plan: Migration plan to execute
|
322
|
+
dry_run: If True, validate but don't apply changes
|
323
|
+
user: User executing migrations
|
324
|
+
|
325
|
+
Returns:
|
326
|
+
List of migration history records
|
327
|
+
"""
|
328
|
+
if not plan.is_safe():
|
329
|
+
raise ValueError("Migration plan is not safe to execute")
|
330
|
+
|
331
|
+
history = []
|
332
|
+
|
333
|
+
if plan.migrations_to_rollback:
|
334
|
+
# Execute rollbacks
|
335
|
+
for migration in plan.migrations_to_rollback:
|
336
|
+
record = await self._rollback_migration(migration, user, dry_run)
|
337
|
+
history.append(record)
|
338
|
+
else:
|
339
|
+
# Execute forward migrations
|
340
|
+
for migration in plan.migrations_to_apply:
|
341
|
+
record = await self._apply_migration(migration, user, dry_run)
|
342
|
+
history.append(record)
|
343
|
+
|
344
|
+
if not record.success:
|
345
|
+
logger.error(f"Migration {migration.id} failed, stopping execution")
|
346
|
+
break
|
347
|
+
|
348
|
+
return history
|
349
|
+
|
350
|
+
async def _apply_migration(
|
351
|
+
self, migration: Migration, user: str, dry_run: bool
|
352
|
+
) -> MigrationHistory:
|
353
|
+
"""Apply a single migration."""
|
354
|
+
logger.info(f"Applying migration: {migration.id}")
|
355
|
+
|
356
|
+
start_time = time.time()
|
357
|
+
success = True
|
358
|
+
error_message = None
|
359
|
+
|
360
|
+
try:
|
361
|
+
async with self.connection_manager.get_connection(
|
362
|
+
self.tenant_id, self.db_config
|
363
|
+
) as conn:
|
364
|
+
# Validate migration
|
365
|
+
if not await migration.validate(conn):
|
366
|
+
raise ValueError("Migration validation failed")
|
367
|
+
|
368
|
+
if not dry_run:
|
369
|
+
# Begin transaction
|
370
|
+
if hasattr(conn, "transaction"):
|
371
|
+
async with conn.transaction():
|
372
|
+
await migration.forward(conn)
|
373
|
+
else:
|
374
|
+
await migration.forward(conn)
|
375
|
+
|
376
|
+
# Record success
|
377
|
+
await self._record_migration(
|
378
|
+
conn, migration, user, time.time() - start_time, True, None
|
379
|
+
)
|
380
|
+
|
381
|
+
except Exception as e:
|
382
|
+
success = False
|
383
|
+
error_message = str(e)
|
384
|
+
logger.error(f"Migration {migration.id} failed: {e}")
|
385
|
+
|
386
|
+
if not dry_run:
|
387
|
+
# Record failure
|
388
|
+
try:
|
389
|
+
async with self.connection_manager.get_connection(
|
390
|
+
self.tenant_id, self.db_config
|
391
|
+
) as conn:
|
392
|
+
await self._record_migration(
|
393
|
+
conn,
|
394
|
+
migration,
|
395
|
+
user,
|
396
|
+
time.time() - start_time,
|
397
|
+
False,
|
398
|
+
error_message,
|
399
|
+
)
|
400
|
+
except Exception as record_error:
|
401
|
+
logger.error(f"Failed to record migration failure: {record_error}")
|
402
|
+
|
403
|
+
return MigrationHistory(
|
404
|
+
migration_id=migration.id,
|
405
|
+
applied_at=datetime.now(UTC),
|
406
|
+
applied_by=user,
|
407
|
+
execution_time=time.time() - start_time,
|
408
|
+
success=success,
|
409
|
+
error_message=error_message,
|
410
|
+
)
|
411
|
+
|
412
|
+
async def _rollback_migration(
|
413
|
+
self, migration: Migration, user: str, dry_run: bool
|
414
|
+
) -> MigrationHistory:
|
415
|
+
"""Rollback a single migration."""
|
416
|
+
logger.info(f"Rolling back migration: {migration.id}")
|
417
|
+
|
418
|
+
start_time = time.time()
|
419
|
+
success = True
|
420
|
+
error_message = None
|
421
|
+
|
422
|
+
try:
|
423
|
+
async with self.connection_manager.get_connection(
|
424
|
+
self.tenant_id, self.db_config
|
425
|
+
) as conn:
|
426
|
+
if not dry_run:
|
427
|
+
# Begin transaction
|
428
|
+
if hasattr(conn, "transaction"):
|
429
|
+
async with conn.transaction():
|
430
|
+
await migration.backward(conn)
|
431
|
+
else:
|
432
|
+
await migration.backward(conn)
|
433
|
+
|
434
|
+
# Update migration record
|
435
|
+
await self._update_migration_rollback(conn, migration.id, user)
|
436
|
+
|
437
|
+
except Exception as e:
|
438
|
+
success = False
|
439
|
+
error_message = str(e)
|
440
|
+
logger.error(f"Rollback of {migration.id} failed: {e}")
|
441
|
+
|
442
|
+
return MigrationHistory(
|
443
|
+
migration_id=migration.id,
|
444
|
+
applied_at=datetime.now(UTC),
|
445
|
+
applied_by=user,
|
446
|
+
execution_time=time.time() - start_time,
|
447
|
+
success=success,
|
448
|
+
error_message=error_message,
|
449
|
+
rollback_at=datetime.now(UTC),
|
450
|
+
rollback_by=user,
|
451
|
+
)
|
452
|
+
|
453
|
+
async def _record_migration(
|
454
|
+
self,
|
455
|
+
conn: Any,
|
456
|
+
migration: Migration,
|
457
|
+
user: str,
|
458
|
+
execution_time: float,
|
459
|
+
success: bool,
|
460
|
+
error_message: Optional[str],
|
461
|
+
):
|
462
|
+
"""Record migration execution."""
|
463
|
+
query = f"""
|
464
|
+
INSERT INTO {self.migration_table} (
|
465
|
+
migration_id, applied_at, applied_by, execution_time,
|
466
|
+
success, error_message, migration_hash
|
467
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7)
|
468
|
+
"""
|
469
|
+
|
470
|
+
await conn.execute(
|
471
|
+
query,
|
472
|
+
migration.id,
|
473
|
+
datetime.now(UTC),
|
474
|
+
user,
|
475
|
+
execution_time,
|
476
|
+
success,
|
477
|
+
error_message,
|
478
|
+
migration.get_hash(),
|
479
|
+
)
|
480
|
+
|
481
|
+
async def _update_migration_rollback(self, conn: Any, migration_id: str, user: str):
|
482
|
+
"""Update migration record for rollback."""
|
483
|
+
query = f"""
|
484
|
+
UPDATE {self.migration_table}
|
485
|
+
SET rollback_at = $1, rollback_by = $2
|
486
|
+
WHERE migration_id = $3 AND rollback_at IS NULL
|
487
|
+
"""
|
488
|
+
|
489
|
+
await conn.execute(query, datetime.now(UTC), user, migration_id)
|