|
0838495…
|
lmata
|
1 |
""" |
|
0838495…
|
lmata
|
2 |
Schema versioning and migrations for the navegador graph. |
|
0838495…
|
lmata
|
3 |
|
|
0838495…
|
lmata
|
4 |
The schema version is stored as a property on a singleton :Meta node. |
|
0838495…
|
lmata
|
5 |
On store open, the current version is checked and any pending migrations |
|
0838495…
|
lmata
|
6 |
are applied sequentially. |
|
0838495…
|
lmata
|
7 |
|
|
0838495…
|
lmata
|
8 |
Migration functions take a GraphStore and upgrade from version N to N+1. |
|
0838495…
|
lmata
|
9 |
""" |
|
0838495…
|
lmata
|
10 |
|
|
0838495…
|
lmata
|
11 |
import logging |
|
0838495…
|
lmata
|
12 |
|
|
0838495…
|
lmata
|
13 |
from navegador.graph.store import GraphStore |
|
0838495…
|
lmata
|
14 |
|
|
0838495…
|
lmata
|
15 |
logger = logging.getLogger(__name__) |
|
0838495…
|
lmata
|
16 |
|
|
0838495…
|
lmata
|
17 |
CURRENT_SCHEMA_VERSION = 2 |
|
0838495…
|
lmata
|
18 |
|
|
0838495…
|
lmata
|
19 |
# ── Migration registry ─────────────────────────────────────────────────────── |
|
0838495…
|
lmata
|
20 |
|
|
0838495…
|
lmata
|
21 |
_migrations: dict[int, callable] = {} |
|
0838495…
|
lmata
|
22 |
|
|
0838495…
|
lmata
|
23 |
|
|
0838495…
|
lmata
|
24 |
def migration(from_version: int): |
|
0838495…
|
lmata
|
25 |
"""Decorator to register a migration from `from_version` to `from_version + 1`.""" |
|
0838495…
|
lmata
|
26 |
|
|
0838495…
|
lmata
|
27 |
def decorator(fn): |
|
0838495…
|
lmata
|
28 |
_migrations[from_version] = fn |
|
0838495…
|
lmata
|
29 |
return fn |
|
0838495…
|
lmata
|
30 |
|
|
0838495…
|
lmata
|
31 |
return decorator |
|
0838495…
|
lmata
|
32 |
|
|
0838495…
|
lmata
|
33 |
|
|
0838495…
|
lmata
|
34 |
# ── Core API ───────────────────────────────────────────────────────────────── |
|
0838495…
|
lmata
|
35 |
|
|
0838495…
|
lmata
|
36 |
|
|
0838495…
|
lmata
|
37 |
def get_schema_version(store: GraphStore) -> int: |
|
0838495…
|
lmata
|
38 |
"""Read the current schema version from the graph (0 if unset).""" |
|
0838495…
|
lmata
|
39 |
result = store.query("MATCH (m:Meta {name: 'schema'}) RETURN m.version") |
|
0838495…
|
lmata
|
40 |
rows = result.result_set or [] |
|
0838495…
|
lmata
|
41 |
if not rows or rows[0][0] is None: |
|
0838495…
|
lmata
|
42 |
return 0 |
|
0838495…
|
lmata
|
43 |
return int(rows[0][0]) |
|
0838495…
|
lmata
|
44 |
|
|
0838495…
|
lmata
|
45 |
|
|
0838495…
|
lmata
|
46 |
def set_schema_version(store: GraphStore, version: int) -> None: |
|
0838495…
|
lmata
|
47 |
"""Write the schema version to the graph.""" |
|
0838495…
|
lmata
|
48 |
store.query( |
|
0838495…
|
lmata
|
49 |
"MERGE (m:Meta {name: 'schema'}) SET m.version = $version", |
|
0838495…
|
lmata
|
50 |
{"version": version}, |
|
0838495…
|
lmata
|
51 |
) |
|
0838495…
|
lmata
|
52 |
|
|
0838495…
|
lmata
|
53 |
|
|
0838495…
|
lmata
|
54 |
def migrate(store: GraphStore) -> list[int]: |
|
0838495…
|
lmata
|
55 |
""" |
|
0838495…
|
lmata
|
56 |
Apply all pending migrations to bring the graph up to CURRENT_SCHEMA_VERSION. |
|
0838495…
|
lmata
|
57 |
|
|
0838495…
|
lmata
|
58 |
Returns: |
|
0838495…
|
lmata
|
59 |
List of versions that were applied. |
|
0838495…
|
lmata
|
60 |
""" |
|
0838495…
|
lmata
|
61 |
current = get_schema_version(store) |
|
0838495…
|
lmata
|
62 |
applied: list[int] = [] |
|
0838495…
|
lmata
|
63 |
|
|
0838495…
|
lmata
|
64 |
while current < CURRENT_SCHEMA_VERSION: |
|
0838495…
|
lmata
|
65 |
fn = _migrations.get(current) |
|
0838495…
|
lmata
|
66 |
if fn is None: |
|
e5e1a3b…
|
lmata
|
67 |
raise RuntimeError(f"No migration registered for version {current} -> {current + 1}") |
|
0838495…
|
lmata
|
68 |
logger.info("Applying migration %d -> %d", current, current + 1) |
|
0838495…
|
lmata
|
69 |
fn(store) |
|
0838495…
|
lmata
|
70 |
current += 1 |
|
0838495…
|
lmata
|
71 |
set_schema_version(store, current) |
|
0838495…
|
lmata
|
72 |
applied.append(current) |
|
0838495…
|
lmata
|
73 |
|
|
0838495…
|
lmata
|
74 |
return applied |
|
0838495…
|
lmata
|
75 |
|
|
0838495…
|
lmata
|
76 |
|
|
0838495…
|
lmata
|
77 |
def needs_migration(store: GraphStore) -> bool: |
|
0838495…
|
lmata
|
78 |
"""Check if the graph needs migration.""" |
|
0838495…
|
lmata
|
79 |
return get_schema_version(store) < CURRENT_SCHEMA_VERSION |
|
0838495…
|
lmata
|
80 |
|
|
0838495…
|
lmata
|
81 |
|
|
0838495…
|
lmata
|
82 |
# ── Migrations ─────────────────────────────────────────────────────────────── |
|
0838495…
|
lmata
|
83 |
|
|
0838495…
|
lmata
|
84 |
|
|
0838495…
|
lmata
|
85 |
@migration(0) |
|
0838495…
|
lmata
|
86 |
def _migrate_0_to_1(store: GraphStore) -> None: |
|
0838495…
|
lmata
|
87 |
"""Initial schema — set version on existing graphs.""" |
|
0838495…
|
lmata
|
88 |
# No structural changes needed — this just stamps the version. |
|
0838495…
|
lmata
|
89 |
logger.info("Stamping initial schema version") |
|
0838495…
|
lmata
|
90 |
|
|
0838495…
|
lmata
|
91 |
|
|
0838495…
|
lmata
|
92 |
@migration(1) |
|
0838495…
|
lmata
|
93 |
def _migrate_1_to_2(store: GraphStore) -> None: |
|
0838495…
|
lmata
|
94 |
"""Add content_hash property to File nodes for incremental ingestion.""" |
|
0838495…
|
lmata
|
95 |
# Set content_hash to empty string on existing File nodes that lack it. |
|
e5e1a3b…
|
lmata
|
96 |
store.query("MATCH (f:File) WHERE f.content_hash IS NULL SET f.content_hash = ''") |
|
0838495…
|
lmata
|
97 |
logger.info("Added content_hash to File nodes") |