spatial-memory-mcp 1.9.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.
- spatial_memory/__init__.py +97 -0
- spatial_memory/__main__.py +271 -0
- spatial_memory/adapters/__init__.py +7 -0
- spatial_memory/adapters/lancedb_repository.py +880 -0
- spatial_memory/config.py +769 -0
- spatial_memory/core/__init__.py +118 -0
- spatial_memory/core/cache.py +317 -0
- spatial_memory/core/circuit_breaker.py +297 -0
- spatial_memory/core/connection_pool.py +220 -0
- spatial_memory/core/consolidation_strategies.py +401 -0
- spatial_memory/core/database.py +3072 -0
- spatial_memory/core/db_idempotency.py +242 -0
- spatial_memory/core/db_indexes.py +576 -0
- spatial_memory/core/db_migrations.py +588 -0
- spatial_memory/core/db_search.py +512 -0
- spatial_memory/core/db_versioning.py +178 -0
- spatial_memory/core/embeddings.py +558 -0
- spatial_memory/core/errors.py +317 -0
- spatial_memory/core/file_security.py +701 -0
- spatial_memory/core/filesystem.py +178 -0
- spatial_memory/core/health.py +289 -0
- spatial_memory/core/helpers.py +79 -0
- spatial_memory/core/import_security.py +433 -0
- spatial_memory/core/lifecycle_ops.py +1067 -0
- spatial_memory/core/logging.py +194 -0
- spatial_memory/core/metrics.py +192 -0
- spatial_memory/core/models.py +660 -0
- spatial_memory/core/rate_limiter.py +326 -0
- spatial_memory/core/response_types.py +500 -0
- spatial_memory/core/security.py +588 -0
- spatial_memory/core/spatial_ops.py +430 -0
- spatial_memory/core/tracing.py +300 -0
- spatial_memory/core/utils.py +110 -0
- spatial_memory/core/validation.py +406 -0
- spatial_memory/factory.py +444 -0
- spatial_memory/migrations/__init__.py +40 -0
- spatial_memory/ports/__init__.py +11 -0
- spatial_memory/ports/repositories.py +630 -0
- spatial_memory/py.typed +0 -0
- spatial_memory/server.py +1214 -0
- spatial_memory/services/__init__.py +70 -0
- spatial_memory/services/decay_manager.py +411 -0
- spatial_memory/services/export_import.py +1031 -0
- spatial_memory/services/lifecycle.py +1139 -0
- spatial_memory/services/memory.py +412 -0
- spatial_memory/services/spatial.py +1152 -0
- spatial_memory/services/utility.py +429 -0
- spatial_memory/tools/__init__.py +5 -0
- spatial_memory/tools/definitions.py +695 -0
- spatial_memory/verify.py +140 -0
- spatial_memory_mcp-1.9.1.dist-info/METADATA +509 -0
- spatial_memory_mcp-1.9.1.dist-info/RECORD +55 -0
- spatial_memory_mcp-1.9.1.dist-info/WHEEL +4 -0
- spatial_memory_mcp-1.9.1.dist-info/entry_points.txt +2 -0
- spatial_memory_mcp-1.9.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
"""Schema migration system for LanceDB database.
|
|
2
|
+
|
|
3
|
+
This module provides a migration framework for managing schema changes
|
|
4
|
+
over time. It supports:
|
|
5
|
+
- Forward migrations (up)
|
|
6
|
+
- Rollback migrations (down)
|
|
7
|
+
- Dry-run mode for previewing changes
|
|
8
|
+
- Automatic snapshot creation before migrations
|
|
9
|
+
|
|
10
|
+
Migrations are versioned using semantic versioning (e.g., "1.0.0", "1.1.0").
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
from spatial_memory.core.db_migrations import MigrationManager
|
|
14
|
+
|
|
15
|
+
manager = MigrationManager(database)
|
|
16
|
+
manager.register_builtin_migrations()
|
|
17
|
+
|
|
18
|
+
# Check pending migrations
|
|
19
|
+
pending = manager.get_pending_migrations()
|
|
20
|
+
|
|
21
|
+
# Run migrations
|
|
22
|
+
applied = manager.run_pending(dry_run=False)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import logging
|
|
28
|
+
from abc import ABC, abstractmethod
|
|
29
|
+
from dataclasses import dataclass, field
|
|
30
|
+
from datetime import datetime
|
|
31
|
+
from typing import TYPE_CHECKING, Any
|
|
32
|
+
|
|
33
|
+
import pyarrow as pa
|
|
34
|
+
|
|
35
|
+
from spatial_memory.core.errors import MigrationError, StorageError
|
|
36
|
+
from spatial_memory.core.utils import utc_now
|
|
37
|
+
|
|
38
|
+
if TYPE_CHECKING:
|
|
39
|
+
from spatial_memory.core.database import Database
|
|
40
|
+
from spatial_memory.ports.repositories import EmbeddingServiceProtocol
|
|
41
|
+
|
|
42
|
+
logger = logging.getLogger(__name__)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# Schema metadata table name
|
|
46
|
+
SCHEMA_VERSIONS_TABLE = "_schema_versions"
|
|
47
|
+
|
|
48
|
+
# Current schema version
|
|
49
|
+
CURRENT_SCHEMA_VERSION = "1.0.0"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# =============================================================================
|
|
53
|
+
# Migration Data Types
|
|
54
|
+
# =============================================================================
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class MigrationRecord:
|
|
59
|
+
"""Record of an applied migration.
|
|
60
|
+
|
|
61
|
+
Stored in the _schema_versions table to track migration history.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
version: str
|
|
65
|
+
description: str
|
|
66
|
+
applied_at: datetime
|
|
67
|
+
embedding_model: str | None = None
|
|
68
|
+
embedding_dimensions: int | None = None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class MigrationResult:
|
|
73
|
+
"""Result of running migrations.
|
|
74
|
+
|
|
75
|
+
Attributes:
|
|
76
|
+
migrations_applied: List of version strings that were applied.
|
|
77
|
+
dry_run: Whether this was a dry run.
|
|
78
|
+
current_version: Version after migrations.
|
|
79
|
+
errors: List of error messages if any migrations failed.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
migrations_applied: list[str] = field(default_factory=list)
|
|
83
|
+
dry_run: bool = True
|
|
84
|
+
current_version: str = "0.0.0"
|
|
85
|
+
errors: list[str] = field(default_factory=list)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# =============================================================================
|
|
89
|
+
# Migration Protocol/Base Class
|
|
90
|
+
# =============================================================================
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class Migration(ABC):
|
|
94
|
+
"""Abstract base class for schema migrations.
|
|
95
|
+
|
|
96
|
+
Each migration should:
|
|
97
|
+
1. Have a unique version string (semantic versioning)
|
|
98
|
+
2. Implement up() for forward migration
|
|
99
|
+
3. Optionally implement down() for rollback
|
|
100
|
+
4. Provide a description of what the migration does
|
|
101
|
+
|
|
102
|
+
Example:
|
|
103
|
+
class Migration001AddExpiresAt(Migration):
|
|
104
|
+
version = "1.1.0"
|
|
105
|
+
description = "Add expires_at column for TTL support"
|
|
106
|
+
|
|
107
|
+
def up(self, db: Database, embeddings: EmbeddingServiceProtocol | None) -> None:
|
|
108
|
+
# Add new column or modify schema
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
def down(self, db: Database) -> None:
|
|
112
|
+
# Rollback changes (optional)
|
|
113
|
+
raise NotImplementedError("Rollback not supported")
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
@abstractmethod
|
|
118
|
+
def version(self) -> str:
|
|
119
|
+
"""Semantic version string (e.g., '1.1.0')."""
|
|
120
|
+
...
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
@abstractmethod
|
|
124
|
+
def description(self) -> str:
|
|
125
|
+
"""Human-readable description of the migration."""
|
|
126
|
+
...
|
|
127
|
+
|
|
128
|
+
@abstractmethod
|
|
129
|
+
def up(
|
|
130
|
+
self,
|
|
131
|
+
db: Database,
|
|
132
|
+
embeddings: EmbeddingServiceProtocol | None = None,
|
|
133
|
+
) -> None:
|
|
134
|
+
"""Apply the migration forward.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
db: Database instance to migrate.
|
|
138
|
+
embeddings: Optional embedding service for re-embedding operations.
|
|
139
|
+
|
|
140
|
+
Raises:
|
|
141
|
+
MigrationError: If migration fails.
|
|
142
|
+
"""
|
|
143
|
+
...
|
|
144
|
+
|
|
145
|
+
def down(self, db: Database) -> None:
|
|
146
|
+
"""Rollback the migration (optional).
|
|
147
|
+
|
|
148
|
+
By default, rollback is not supported. Override this method
|
|
149
|
+
to enable rollback for a specific migration.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
db: Database instance to rollback.
|
|
153
|
+
|
|
154
|
+
Raises:
|
|
155
|
+
NotImplementedError: If rollback is not supported.
|
|
156
|
+
"""
|
|
157
|
+
raise NotImplementedError(
|
|
158
|
+
f"Rollback not supported for migration {self.version}"
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# =============================================================================
|
|
163
|
+
# Migration Manager
|
|
164
|
+
# =============================================================================
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class MigrationManager:
|
|
168
|
+
"""Manages database schema migrations.
|
|
169
|
+
|
|
170
|
+
The manager:
|
|
171
|
+
- Tracks applied migrations in a metadata table
|
|
172
|
+
- Supports forward migrations (up) and rollbacks (down)
|
|
173
|
+
- Creates snapshots before applying migrations for safety
|
|
174
|
+
- Supports dry-run mode for previewing changes
|
|
175
|
+
|
|
176
|
+
Example:
|
|
177
|
+
manager = MigrationManager(database)
|
|
178
|
+
manager.register_builtin_migrations()
|
|
179
|
+
|
|
180
|
+
# Preview pending migrations
|
|
181
|
+
pending = manager.get_pending_migrations()
|
|
182
|
+
for m in pending:
|
|
183
|
+
print(f"Pending: {m.version} - {m.description}")
|
|
184
|
+
|
|
185
|
+
# Apply migrations
|
|
186
|
+
result = manager.run_pending(dry_run=False)
|
|
187
|
+
print(f"Applied: {result.migrations_applied}")
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
def __init__(
|
|
191
|
+
self,
|
|
192
|
+
db: Database,
|
|
193
|
+
embeddings: EmbeddingServiceProtocol | None = None,
|
|
194
|
+
) -> None:
|
|
195
|
+
"""Initialize the migration manager.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
db: Database instance to manage.
|
|
199
|
+
embeddings: Optional embedding service for migrations that re-embed.
|
|
200
|
+
"""
|
|
201
|
+
self._db = db
|
|
202
|
+
self._embeddings = embeddings
|
|
203
|
+
self._migrations: dict[str, Migration] = {}
|
|
204
|
+
self._schema_table_checked = False
|
|
205
|
+
|
|
206
|
+
def _ensure_schema_table(self) -> None:
|
|
207
|
+
"""Ensure the schema versions table exists.
|
|
208
|
+
|
|
209
|
+
Creates the table if it doesn't exist. This is called lazily
|
|
210
|
+
on first access to avoid issues with fresh databases.
|
|
211
|
+
"""
|
|
212
|
+
if self._schema_table_checked:
|
|
213
|
+
return
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
lance_db = self._db._db
|
|
217
|
+
assert lance_db is not None, "Database connection not initialized"
|
|
218
|
+
table_names = lance_db.table_names()
|
|
219
|
+
|
|
220
|
+
if SCHEMA_VERSIONS_TABLE not in table_names:
|
|
221
|
+
# Create schema versions table
|
|
222
|
+
schema = pa.schema([
|
|
223
|
+
pa.field("version", pa.string()),
|
|
224
|
+
pa.field("description", pa.string()),
|
|
225
|
+
pa.field("applied_at", pa.timestamp("us")),
|
|
226
|
+
pa.field("embedding_model", pa.string()),
|
|
227
|
+
pa.field("embedding_dimensions", pa.int32()),
|
|
228
|
+
])
|
|
229
|
+
# Create empty table with schema
|
|
230
|
+
empty_table = pa.table(
|
|
231
|
+
{
|
|
232
|
+
"version": pa.array([], type=pa.string()),
|
|
233
|
+
"description": pa.array([], type=pa.string()),
|
|
234
|
+
"applied_at": pa.array([], type=pa.timestamp("us")),
|
|
235
|
+
"embedding_model": pa.array([], type=pa.string()),
|
|
236
|
+
"embedding_dimensions": pa.array([], type=pa.int32()),
|
|
237
|
+
},
|
|
238
|
+
schema=schema,
|
|
239
|
+
)
|
|
240
|
+
lance_db.create_table(SCHEMA_VERSIONS_TABLE, empty_table)
|
|
241
|
+
logger.info("Created schema versions table")
|
|
242
|
+
|
|
243
|
+
self._schema_table_checked = True
|
|
244
|
+
except Exception as e:
|
|
245
|
+
raise StorageError(f"Failed to create schema versions table: {e}") from e
|
|
246
|
+
|
|
247
|
+
def register(self, migration: Migration) -> None:
|
|
248
|
+
"""Register a migration.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
migration: Migration instance to register.
|
|
252
|
+
|
|
253
|
+
Raises:
|
|
254
|
+
ValueError: If a migration with the same version already exists.
|
|
255
|
+
"""
|
|
256
|
+
if migration.version in self._migrations:
|
|
257
|
+
raise ValueError(f"Migration {migration.version} already registered")
|
|
258
|
+
self._migrations[migration.version] = migration
|
|
259
|
+
|
|
260
|
+
def register_builtin_migrations(self) -> None:
|
|
261
|
+
"""Register all built-in migrations.
|
|
262
|
+
|
|
263
|
+
Called automatically to set up standard migrations.
|
|
264
|
+
"""
|
|
265
|
+
# Register the initial schema migration
|
|
266
|
+
self.register(InitialSchemaMigration())
|
|
267
|
+
# Future migrations would be registered here:
|
|
268
|
+
# self.register(Migration001AddExpiresAt())
|
|
269
|
+
|
|
270
|
+
def get_current_version(self) -> str:
|
|
271
|
+
"""Get the current schema version from the database.
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
Current version string, or "0.0.0" if no migrations applied.
|
|
275
|
+
"""
|
|
276
|
+
self._ensure_schema_table()
|
|
277
|
+
|
|
278
|
+
try:
|
|
279
|
+
lance_db = self._db._db
|
|
280
|
+
assert lance_db is not None, "Database connection not initialized"
|
|
281
|
+
table = lance_db.open_table(SCHEMA_VERSIONS_TABLE)
|
|
282
|
+
arrow_table = table.to_arrow()
|
|
283
|
+
|
|
284
|
+
if arrow_table.num_rows == 0:
|
|
285
|
+
return "0.0.0"
|
|
286
|
+
|
|
287
|
+
# Get the latest version by comparing all versions
|
|
288
|
+
versions: list[str] = arrow_table.column("version").to_pylist()
|
|
289
|
+
if not versions:
|
|
290
|
+
return "0.0.0"
|
|
291
|
+
|
|
292
|
+
# Find the maximum version using semantic comparison
|
|
293
|
+
return max(versions, key=lambda v: tuple(int(x) for x in v.split(".")))
|
|
294
|
+
except Exception as e:
|
|
295
|
+
logger.warning(f"Could not get current version: {e}")
|
|
296
|
+
return "0.0.0"
|
|
297
|
+
|
|
298
|
+
def get_applied_migrations(self) -> list[MigrationRecord]:
|
|
299
|
+
"""Get list of all applied migrations.
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
List of MigrationRecord for each applied migration.
|
|
303
|
+
"""
|
|
304
|
+
self._ensure_schema_table()
|
|
305
|
+
|
|
306
|
+
try:
|
|
307
|
+
lance_db = self._db._db
|
|
308
|
+
assert lance_db is not None, "Database connection not initialized"
|
|
309
|
+
table = lance_db.open_table(SCHEMA_VERSIONS_TABLE)
|
|
310
|
+
arrow_table = table.to_arrow()
|
|
311
|
+
|
|
312
|
+
if arrow_table.num_rows == 0:
|
|
313
|
+
return []
|
|
314
|
+
|
|
315
|
+
records = []
|
|
316
|
+
versions = arrow_table.column("version").to_pylist()
|
|
317
|
+
descriptions = arrow_table.column("description").to_pylist()
|
|
318
|
+
applied_ats = arrow_table.column("applied_at").to_pylist()
|
|
319
|
+
embedding_models = arrow_table.column("embedding_model").to_pylist()
|
|
320
|
+
embedding_dims = arrow_table.column("embedding_dimensions").to_pylist()
|
|
321
|
+
|
|
322
|
+
for i in range(arrow_table.num_rows):
|
|
323
|
+
# Handle timestamp conversion
|
|
324
|
+
applied_at = applied_ats[i]
|
|
325
|
+
if hasattr(applied_at, "as_py"):
|
|
326
|
+
applied_at = applied_at.as_py()
|
|
327
|
+
|
|
328
|
+
records.append(MigrationRecord(
|
|
329
|
+
version=versions[i],
|
|
330
|
+
description=descriptions[i],
|
|
331
|
+
applied_at=applied_at,
|
|
332
|
+
embedding_model=embedding_models[i],
|
|
333
|
+
embedding_dimensions=embedding_dims[i],
|
|
334
|
+
))
|
|
335
|
+
return records
|
|
336
|
+
except Exception as e:
|
|
337
|
+
logger.warning(f"Could not get applied migrations: {e}")
|
|
338
|
+
return []
|
|
339
|
+
|
|
340
|
+
def get_pending_migrations(self) -> list[Migration]:
|
|
341
|
+
"""Get list of migrations that haven't been applied yet.
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
List of Migration instances that need to be applied.
|
|
345
|
+
"""
|
|
346
|
+
current = self.get_current_version()
|
|
347
|
+
pending = []
|
|
348
|
+
|
|
349
|
+
for version in sorted(self._migrations.keys()):
|
|
350
|
+
if self._compare_versions(version, current) > 0:
|
|
351
|
+
pending.append(self._migrations[version])
|
|
352
|
+
|
|
353
|
+
return pending
|
|
354
|
+
|
|
355
|
+
def run_pending(self, dry_run: bool = True) -> MigrationResult:
|
|
356
|
+
"""Run all pending migrations.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
dry_run: If True, preview migrations without applying.
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
MigrationResult with applied migrations and any errors.
|
|
363
|
+
"""
|
|
364
|
+
pending = self.get_pending_migrations()
|
|
365
|
+
result = MigrationResult(
|
|
366
|
+
dry_run=dry_run,
|
|
367
|
+
current_version=self.get_current_version(),
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
if not pending:
|
|
371
|
+
logger.info("No pending migrations")
|
|
372
|
+
return result
|
|
373
|
+
|
|
374
|
+
for migration in pending:
|
|
375
|
+
logger.info(
|
|
376
|
+
f"{'Would apply' if dry_run else 'Applying'} migration "
|
|
377
|
+
f"{migration.version}: {migration.description}"
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
if not dry_run:
|
|
381
|
+
try:
|
|
382
|
+
# Create snapshot before migration
|
|
383
|
+
snapshot_tag = f"pre-migration-{migration.version}"
|
|
384
|
+
snapshot_version = self._db.create_snapshot(snapshot_tag)
|
|
385
|
+
logger.info(f"Created pre-migration snapshot at version {snapshot_version}")
|
|
386
|
+
|
|
387
|
+
# Apply migration
|
|
388
|
+
migration.up(self._db, self._embeddings)
|
|
389
|
+
|
|
390
|
+
# Record migration
|
|
391
|
+
self._record_migration(migration)
|
|
392
|
+
|
|
393
|
+
result.migrations_applied.append(migration.version)
|
|
394
|
+
result.current_version = migration.version
|
|
395
|
+
except Exception as e:
|
|
396
|
+
error_msg = f"Migration {migration.version} failed: {e}"
|
|
397
|
+
logger.error(error_msg)
|
|
398
|
+
result.errors.append(error_msg)
|
|
399
|
+
|
|
400
|
+
# Stop on first error
|
|
401
|
+
break
|
|
402
|
+
else:
|
|
403
|
+
result.migrations_applied.append(migration.version)
|
|
404
|
+
|
|
405
|
+
return result
|
|
406
|
+
|
|
407
|
+
def rollback(self, target_version: str) -> MigrationResult:
|
|
408
|
+
"""Rollback to a specific version.
|
|
409
|
+
|
|
410
|
+
Args:
|
|
411
|
+
target_version: Version to rollback to.
|
|
412
|
+
|
|
413
|
+
Returns:
|
|
414
|
+
MigrationResult with rolled back migrations and any errors.
|
|
415
|
+
|
|
416
|
+
Raises:
|
|
417
|
+
MigrationError: If rollback fails.
|
|
418
|
+
"""
|
|
419
|
+
current = self.get_current_version()
|
|
420
|
+
result = MigrationResult(
|
|
421
|
+
dry_run=False,
|
|
422
|
+
current_version=current,
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
if self._compare_versions(target_version, current) >= 0:
|
|
426
|
+
logger.info(f"Already at or before version {target_version}")
|
|
427
|
+
return result
|
|
428
|
+
|
|
429
|
+
# Find migrations to rollback (newest first)
|
|
430
|
+
to_rollback = []
|
|
431
|
+
for version in sorted(self._migrations.keys(), reverse=True):
|
|
432
|
+
if self._compare_versions(version, target_version) > 0:
|
|
433
|
+
if self._compare_versions(version, current) <= 0:
|
|
434
|
+
to_rollback.append(self._migrations[version])
|
|
435
|
+
|
|
436
|
+
for migration in to_rollback:
|
|
437
|
+
logger.info(f"Rolling back migration {migration.version}")
|
|
438
|
+
try:
|
|
439
|
+
migration.down(self._db)
|
|
440
|
+
# Remove migration record
|
|
441
|
+
self._remove_migration_record(migration.version)
|
|
442
|
+
result.migrations_applied.append(migration.version)
|
|
443
|
+
except NotImplementedError:
|
|
444
|
+
error_msg = f"Rollback not supported for {migration.version}"
|
|
445
|
+
logger.error(error_msg)
|
|
446
|
+
result.errors.append(error_msg)
|
|
447
|
+
break
|
|
448
|
+
except Exception as e:
|
|
449
|
+
error_msg = f"Rollback of {migration.version} failed: {e}"
|
|
450
|
+
logger.error(error_msg)
|
|
451
|
+
result.errors.append(error_msg)
|
|
452
|
+
break
|
|
453
|
+
|
|
454
|
+
result.current_version = self.get_current_version()
|
|
455
|
+
return result
|
|
456
|
+
|
|
457
|
+
def _record_migration(self, migration: Migration) -> None:
|
|
458
|
+
"""Record that a migration was applied.
|
|
459
|
+
|
|
460
|
+
Args:
|
|
461
|
+
migration: Migration that was applied.
|
|
462
|
+
"""
|
|
463
|
+
self._ensure_schema_table()
|
|
464
|
+
|
|
465
|
+
try:
|
|
466
|
+
lance_db = self._db._db
|
|
467
|
+
assert lance_db is not None, "Database connection not initialized"
|
|
468
|
+
table = lance_db.open_table(SCHEMA_VERSIONS_TABLE)
|
|
469
|
+
|
|
470
|
+
# Get embedding info if available
|
|
471
|
+
embedding_model = None
|
|
472
|
+
embedding_dim = None
|
|
473
|
+
if self._embeddings:
|
|
474
|
+
embedding_model = getattr(self._embeddings, "model_name", None)
|
|
475
|
+
embedding_dim = getattr(self._embeddings, "dimensions", None)
|
|
476
|
+
|
|
477
|
+
record = pa.table({
|
|
478
|
+
"version": [migration.version],
|
|
479
|
+
"description": [migration.description],
|
|
480
|
+
"applied_at": [utc_now()],
|
|
481
|
+
"embedding_model": [embedding_model],
|
|
482
|
+
"embedding_dimensions": [embedding_dim],
|
|
483
|
+
})
|
|
484
|
+
table.add(record)
|
|
485
|
+
except Exception as e:
|
|
486
|
+
raise MigrationError(f"Failed to record migration: {e}") from e
|
|
487
|
+
|
|
488
|
+
def _remove_migration_record(self, version: str) -> None:
|
|
489
|
+
"""Remove a migration record (for rollback).
|
|
490
|
+
|
|
491
|
+
Args:
|
|
492
|
+
version: Version to remove.
|
|
493
|
+
"""
|
|
494
|
+
self._ensure_schema_table()
|
|
495
|
+
|
|
496
|
+
try:
|
|
497
|
+
lance_db = self._db._db
|
|
498
|
+
assert lance_db is not None, "Database connection not initialized"
|
|
499
|
+
table = lance_db.open_table(SCHEMA_VERSIONS_TABLE)
|
|
500
|
+
table.delete(f'version = "{version}"')
|
|
501
|
+
except Exception as e:
|
|
502
|
+
logger.warning(f"Failed to remove migration record: {e}")
|
|
503
|
+
|
|
504
|
+
@staticmethod
|
|
505
|
+
def _compare_versions(v1: str, v2: str) -> int:
|
|
506
|
+
"""Compare two semantic version strings.
|
|
507
|
+
|
|
508
|
+
Args:
|
|
509
|
+
v1: First version.
|
|
510
|
+
v2: Second version.
|
|
511
|
+
|
|
512
|
+
Returns:
|
|
513
|
+
-1 if v1 < v2, 0 if equal, 1 if v1 > v2.
|
|
514
|
+
"""
|
|
515
|
+
def parse(v: str) -> tuple[int, ...]:
|
|
516
|
+
return tuple(int(x) for x in v.split("."))
|
|
517
|
+
|
|
518
|
+
p1, p2 = parse(v1), parse(v2)
|
|
519
|
+
if p1 < p2:
|
|
520
|
+
return -1
|
|
521
|
+
if p1 > p2:
|
|
522
|
+
return 1
|
|
523
|
+
return 0
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
# =============================================================================
|
|
527
|
+
# Built-in Migrations
|
|
528
|
+
# =============================================================================
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
class InitialSchemaMigration(Migration):
|
|
532
|
+
"""Initial schema setup migration.
|
|
533
|
+
|
|
534
|
+
This migration represents the initial schema state. It doesn't
|
|
535
|
+
actually change anything but serves as a baseline version marker.
|
|
536
|
+
"""
|
|
537
|
+
|
|
538
|
+
@property
|
|
539
|
+
def version(self) -> str:
|
|
540
|
+
return "1.0.0"
|
|
541
|
+
|
|
542
|
+
@property
|
|
543
|
+
def description(self) -> str:
|
|
544
|
+
return "Initial schema version"
|
|
545
|
+
|
|
546
|
+
def up(
|
|
547
|
+
self,
|
|
548
|
+
db: Database,
|
|
549
|
+
embeddings: EmbeddingServiceProtocol | None = None,
|
|
550
|
+
) -> None:
|
|
551
|
+
"""No-op for initial schema - just marks version."""
|
|
552
|
+
logger.info("Initial schema version marker applied")
|
|
553
|
+
|
|
554
|
+
def down(self, db: Database) -> None:
|
|
555
|
+
"""Cannot rollback initial schema."""
|
|
556
|
+
raise NotImplementedError("Cannot rollback initial schema")
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
# =============================================================================
|
|
560
|
+
# Helper Functions
|
|
561
|
+
# =============================================================================
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def check_migration_status(db: Database) -> dict[str, Any]:
|
|
565
|
+
"""Check the migration status of a database.
|
|
566
|
+
|
|
567
|
+
Args:
|
|
568
|
+
db: Database to check.
|
|
569
|
+
|
|
570
|
+
Returns:
|
|
571
|
+
Dictionary with migration status information.
|
|
572
|
+
"""
|
|
573
|
+
manager = MigrationManager(db)
|
|
574
|
+
manager.register_builtin_migrations()
|
|
575
|
+
|
|
576
|
+
current = manager.get_current_version()
|
|
577
|
+
pending = manager.get_pending_migrations()
|
|
578
|
+
|
|
579
|
+
return {
|
|
580
|
+
"current_version": current,
|
|
581
|
+
"target_version": CURRENT_SCHEMA_VERSION,
|
|
582
|
+
"pending_count": len(pending),
|
|
583
|
+
"pending_migrations": [
|
|
584
|
+
{"version": m.version, "description": m.description}
|
|
585
|
+
for m in pending
|
|
586
|
+
],
|
|
587
|
+
"needs_migration": len(pending) > 0,
|
|
588
|
+
}
|