kailash 0.3.2__py3-none-any.whl → 0.4.1__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.
Files changed (151) hide show
  1. kailash/__init__.py +33 -1
  2. kailash/access_control/__init__.py +129 -0
  3. kailash/access_control/managers.py +461 -0
  4. kailash/access_control/rule_evaluators.py +467 -0
  5. kailash/access_control_abac.py +825 -0
  6. kailash/config/__init__.py +27 -0
  7. kailash/config/database_config.py +359 -0
  8. kailash/database/__init__.py +28 -0
  9. kailash/database/execution_pipeline.py +499 -0
  10. kailash/middleware/__init__.py +306 -0
  11. kailash/middleware/auth/__init__.py +33 -0
  12. kailash/middleware/auth/access_control.py +436 -0
  13. kailash/middleware/auth/auth_manager.py +422 -0
  14. kailash/middleware/auth/jwt_auth.py +477 -0
  15. kailash/middleware/auth/kailash_jwt_auth.py +616 -0
  16. kailash/middleware/communication/__init__.py +37 -0
  17. kailash/middleware/communication/ai_chat.py +989 -0
  18. kailash/middleware/communication/api_gateway.py +802 -0
  19. kailash/middleware/communication/events.py +470 -0
  20. kailash/middleware/communication/realtime.py +710 -0
  21. kailash/middleware/core/__init__.py +21 -0
  22. kailash/middleware/core/agent_ui.py +890 -0
  23. kailash/middleware/core/schema.py +643 -0
  24. kailash/middleware/core/workflows.py +396 -0
  25. kailash/middleware/database/__init__.py +63 -0
  26. kailash/middleware/database/base.py +113 -0
  27. kailash/middleware/database/base_models.py +525 -0
  28. kailash/middleware/database/enums.py +106 -0
  29. kailash/middleware/database/migrations.py +12 -0
  30. kailash/{api/database.py → middleware/database/models.py} +183 -291
  31. kailash/middleware/database/repositories.py +685 -0
  32. kailash/middleware/database/session_manager.py +19 -0
  33. kailash/middleware/mcp/__init__.py +38 -0
  34. kailash/middleware/mcp/client_integration.py +585 -0
  35. kailash/middleware/mcp/enhanced_server.py +576 -0
  36. kailash/nodes/__init__.py +27 -3
  37. kailash/nodes/admin/__init__.py +42 -0
  38. kailash/nodes/admin/audit_log.py +794 -0
  39. kailash/nodes/admin/permission_check.py +864 -0
  40. kailash/nodes/admin/role_management.py +823 -0
  41. kailash/nodes/admin/security_event.py +1523 -0
  42. kailash/nodes/admin/user_management.py +944 -0
  43. kailash/nodes/ai/a2a.py +24 -7
  44. kailash/nodes/ai/ai_providers.py +248 -40
  45. kailash/nodes/ai/embedding_generator.py +11 -11
  46. kailash/nodes/ai/intelligent_agent_orchestrator.py +99 -11
  47. kailash/nodes/ai/llm_agent.py +436 -5
  48. kailash/nodes/ai/self_organizing.py +85 -10
  49. kailash/nodes/ai/vision_utils.py +148 -0
  50. kailash/nodes/alerts/__init__.py +26 -0
  51. kailash/nodes/alerts/base.py +234 -0
  52. kailash/nodes/alerts/discord.py +499 -0
  53. kailash/nodes/api/auth.py +287 -6
  54. kailash/nodes/api/rest.py +151 -0
  55. kailash/nodes/auth/__init__.py +17 -0
  56. kailash/nodes/auth/directory_integration.py +1228 -0
  57. kailash/nodes/auth/enterprise_auth_provider.py +1328 -0
  58. kailash/nodes/auth/mfa.py +2338 -0
  59. kailash/nodes/auth/risk_assessment.py +872 -0
  60. kailash/nodes/auth/session_management.py +1093 -0
  61. kailash/nodes/auth/sso.py +1040 -0
  62. kailash/nodes/base.py +344 -13
  63. kailash/nodes/base_cycle_aware.py +4 -2
  64. kailash/nodes/base_with_acl.py +1 -1
  65. kailash/nodes/code/python.py +283 -10
  66. kailash/nodes/compliance/__init__.py +9 -0
  67. kailash/nodes/compliance/data_retention.py +1888 -0
  68. kailash/nodes/compliance/gdpr.py +2004 -0
  69. kailash/nodes/data/__init__.py +22 -2
  70. kailash/nodes/data/async_connection.py +469 -0
  71. kailash/nodes/data/async_sql.py +757 -0
  72. kailash/nodes/data/async_vector.py +598 -0
  73. kailash/nodes/data/readers.py +767 -0
  74. kailash/nodes/data/retrieval.py +360 -1
  75. kailash/nodes/data/sharepoint_graph.py +397 -21
  76. kailash/nodes/data/sql.py +94 -5
  77. kailash/nodes/data/streaming.py +68 -8
  78. kailash/nodes/data/vector_db.py +54 -4
  79. kailash/nodes/enterprise/__init__.py +13 -0
  80. kailash/nodes/enterprise/batch_processor.py +741 -0
  81. kailash/nodes/enterprise/data_lineage.py +497 -0
  82. kailash/nodes/logic/convergence.py +31 -9
  83. kailash/nodes/logic/operations.py +14 -3
  84. kailash/nodes/mixins/__init__.py +8 -0
  85. kailash/nodes/mixins/event_emitter.py +201 -0
  86. kailash/nodes/mixins/mcp.py +9 -4
  87. kailash/nodes/mixins/security.py +165 -0
  88. kailash/nodes/monitoring/__init__.py +7 -0
  89. kailash/nodes/monitoring/performance_benchmark.py +2497 -0
  90. kailash/nodes/rag/__init__.py +284 -0
  91. kailash/nodes/rag/advanced.py +1615 -0
  92. kailash/nodes/rag/agentic.py +773 -0
  93. kailash/nodes/rag/conversational.py +999 -0
  94. kailash/nodes/rag/evaluation.py +875 -0
  95. kailash/nodes/rag/federated.py +1188 -0
  96. kailash/nodes/rag/graph.py +721 -0
  97. kailash/nodes/rag/multimodal.py +671 -0
  98. kailash/nodes/rag/optimized.py +933 -0
  99. kailash/nodes/rag/privacy.py +1059 -0
  100. kailash/nodes/rag/query_processing.py +1335 -0
  101. kailash/nodes/rag/realtime.py +764 -0
  102. kailash/nodes/rag/registry.py +547 -0
  103. kailash/nodes/rag/router.py +837 -0
  104. kailash/nodes/rag/similarity.py +1854 -0
  105. kailash/nodes/rag/strategies.py +566 -0
  106. kailash/nodes/rag/workflows.py +575 -0
  107. kailash/nodes/security/__init__.py +19 -0
  108. kailash/nodes/security/abac_evaluator.py +1411 -0
  109. kailash/nodes/security/audit_log.py +103 -0
  110. kailash/nodes/security/behavior_analysis.py +1893 -0
  111. kailash/nodes/security/credential_manager.py +401 -0
  112. kailash/nodes/security/rotating_credentials.py +760 -0
  113. kailash/nodes/security/security_event.py +133 -0
  114. kailash/nodes/security/threat_detection.py +1103 -0
  115. kailash/nodes/testing/__init__.py +9 -0
  116. kailash/nodes/testing/credential_testing.py +499 -0
  117. kailash/nodes/transform/__init__.py +10 -2
  118. kailash/nodes/transform/chunkers.py +592 -1
  119. kailash/nodes/transform/processors.py +484 -14
  120. kailash/nodes/validation.py +321 -0
  121. kailash/runtime/access_controlled.py +1 -1
  122. kailash/runtime/async_local.py +41 -7
  123. kailash/runtime/docker.py +1 -1
  124. kailash/runtime/local.py +474 -55
  125. kailash/runtime/parallel.py +1 -1
  126. kailash/runtime/parallel_cyclic.py +1 -1
  127. kailash/runtime/testing.py +210 -2
  128. kailash/security.py +1 -1
  129. kailash/utils/migrations/__init__.py +25 -0
  130. kailash/utils/migrations/generator.py +433 -0
  131. kailash/utils/migrations/models.py +231 -0
  132. kailash/utils/migrations/runner.py +489 -0
  133. kailash/utils/secure_logging.py +342 -0
  134. kailash/workflow/__init__.py +16 -0
  135. kailash/workflow/cyclic_runner.py +3 -4
  136. kailash/workflow/graph.py +70 -2
  137. kailash/workflow/resilience.py +249 -0
  138. kailash/workflow/templates.py +726 -0
  139. {kailash-0.3.2.dist-info → kailash-0.4.1.dist-info}/METADATA +256 -20
  140. kailash-0.4.1.dist-info/RECORD +227 -0
  141. kailash/api/__init__.py +0 -17
  142. kailash/api/__main__.py +0 -6
  143. kailash/api/studio_secure.py +0 -893
  144. kailash/mcp/__main__.py +0 -13
  145. kailash/mcp/server_new.py +0 -336
  146. kailash/mcp/servers/__init__.py +0 -12
  147. kailash-0.3.2.dist-info/RECORD +0 -136
  148. {kailash-0.3.2.dist-info → kailash-0.4.1.dist-info}/WHEEL +0 -0
  149. {kailash-0.3.2.dist-info → kailash-0.4.1.dist-info}/entry_points.txt +0 -0
  150. {kailash-0.3.2.dist-info → kailash-0.4.1.dist-info}/licenses/LICENSE +0 -0
  151. {kailash-0.3.2.dist-info → kailash-0.4.1.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)