alma-memory 0.5.1__py3-none-any.whl → 0.7.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.
- alma/__init__.py +296 -226
- alma/compression/__init__.py +33 -0
- alma/compression/pipeline.py +980 -0
- alma/confidence/__init__.py +47 -47
- alma/confidence/engine.py +540 -540
- alma/confidence/types.py +351 -351
- alma/config/loader.py +157 -157
- alma/consolidation/__init__.py +23 -23
- alma/consolidation/engine.py +678 -678
- alma/consolidation/prompts.py +84 -84
- alma/core.py +1189 -430
- alma/domains/__init__.py +30 -30
- alma/domains/factory.py +359 -359
- alma/domains/schemas.py +448 -448
- alma/domains/types.py +272 -272
- alma/events/__init__.py +75 -75
- alma/events/emitter.py +285 -284
- alma/events/storage_mixin.py +246 -246
- alma/events/types.py +126 -126
- alma/events/webhook.py +425 -425
- alma/exceptions.py +49 -49
- alma/extraction/__init__.py +31 -31
- alma/extraction/auto_learner.py +265 -265
- alma/extraction/extractor.py +420 -420
- alma/graph/__init__.py +106 -106
- alma/graph/backends/__init__.py +32 -32
- alma/graph/backends/kuzu.py +624 -624
- alma/graph/backends/memgraph.py +432 -432
- alma/graph/backends/memory.py +236 -236
- alma/graph/backends/neo4j.py +417 -417
- alma/graph/base.py +159 -159
- alma/graph/extraction.py +198 -198
- alma/graph/store.py +860 -860
- alma/harness/__init__.py +35 -35
- alma/harness/base.py +386 -386
- alma/harness/domains.py +705 -705
- alma/initializer/__init__.py +37 -37
- alma/initializer/initializer.py +418 -418
- alma/initializer/types.py +250 -250
- alma/integration/__init__.py +62 -62
- alma/integration/claude_agents.py +444 -444
- alma/integration/helena.py +423 -423
- alma/integration/victor.py +471 -471
- alma/learning/__init__.py +101 -86
- alma/learning/decay.py +878 -0
- alma/learning/forgetting.py +1446 -1446
- alma/learning/heuristic_extractor.py +390 -390
- alma/learning/protocols.py +374 -374
- alma/learning/validation.py +346 -346
- alma/mcp/__init__.py +123 -45
- alma/mcp/__main__.py +156 -156
- alma/mcp/resources.py +122 -122
- alma/mcp/server.py +955 -591
- alma/mcp/tools.py +3254 -509
- alma/observability/__init__.py +91 -84
- alma/observability/config.py +302 -302
- alma/observability/guidelines.py +170 -0
- alma/observability/logging.py +424 -424
- alma/observability/metrics.py +583 -583
- alma/observability/tracing.py +440 -440
- alma/progress/__init__.py +21 -21
- alma/progress/tracker.py +607 -607
- alma/progress/types.py +250 -250
- alma/retrieval/__init__.py +134 -53
- alma/retrieval/budget.py +525 -0
- alma/retrieval/cache.py +1304 -1061
- alma/retrieval/embeddings.py +202 -202
- alma/retrieval/engine.py +850 -427
- alma/retrieval/modes.py +365 -0
- alma/retrieval/progressive.py +560 -0
- alma/retrieval/scoring.py +344 -344
- alma/retrieval/trust_scoring.py +637 -0
- alma/retrieval/verification.py +797 -0
- alma/session/__init__.py +19 -19
- alma/session/manager.py +442 -399
- alma/session/types.py +288 -288
- alma/storage/__init__.py +101 -90
- alma/storage/archive.py +233 -0
- alma/storage/azure_cosmos.py +1259 -1259
- alma/storage/base.py +1083 -583
- alma/storage/chroma.py +1443 -1443
- alma/storage/constants.py +103 -103
- alma/storage/file_based.py +614 -614
- alma/storage/migrations/__init__.py +21 -21
- alma/storage/migrations/base.py +321 -321
- alma/storage/migrations/runner.py +323 -323
- alma/storage/migrations/version_stores.py +337 -337
- alma/storage/migrations/versions/__init__.py +11 -11
- alma/storage/migrations/versions/v1_0_0.py +373 -373
- alma/storage/migrations/versions/v1_1_0_workflow_context.py +551 -0
- alma/storage/pinecone.py +1080 -1080
- alma/storage/postgresql.py +1948 -1559
- alma/storage/qdrant.py +1306 -1306
- alma/storage/sqlite_local.py +3041 -1457
- alma/testing/__init__.py +46 -46
- alma/testing/factories.py +301 -301
- alma/testing/mocks.py +389 -389
- alma/types.py +292 -264
- alma/utils/__init__.py +19 -0
- alma/utils/tokenizer.py +521 -0
- alma/workflow/__init__.py +83 -0
- alma/workflow/artifacts.py +170 -0
- alma/workflow/checkpoint.py +311 -0
- alma/workflow/context.py +228 -0
- alma/workflow/outcomes.py +189 -0
- alma/workflow/reducers.py +393 -0
- {alma_memory-0.5.1.dist-info → alma_memory-0.7.0.dist-info}/METADATA +210 -72
- alma_memory-0.7.0.dist-info/RECORD +112 -0
- alma_memory-0.5.1.dist-info/RECORD +0 -93
- {alma_memory-0.5.1.dist-info → alma_memory-0.7.0.dist-info}/WHEEL +0 -0
- {alma_memory-0.5.1.dist-info → alma_memory-0.7.0.dist-info}/top_level.txt +0 -0
|
@@ -1,337 +1,337 @@
|
|
|
1
|
-
"""
|
|
2
|
-
ALMA Migration Framework - Version Stores.
|
|
3
|
-
|
|
4
|
-
Implementations of version tracking for different storage backends.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import json
|
|
8
|
-
import logging
|
|
9
|
-
import sqlite3
|
|
10
|
-
from contextlib import contextmanager
|
|
11
|
-
from datetime import datetime
|
|
12
|
-
from pathlib import Path
|
|
13
|
-
from typing import Any, List, Optional
|
|
14
|
-
|
|
15
|
-
from alma.storage.migrations.base import SchemaVersion
|
|
16
|
-
|
|
17
|
-
logger = logging.getLogger(__name__)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class SQLiteVersionStore:
|
|
21
|
-
"""
|
|
22
|
-
Version store using SQLite for tracking schema versions.
|
|
23
|
-
|
|
24
|
-
Creates a `_schema_versions` table to track applied migrations.
|
|
25
|
-
"""
|
|
26
|
-
|
|
27
|
-
def __init__(self, db_path: Path):
|
|
28
|
-
"""
|
|
29
|
-
Initialize SQLite version store.
|
|
30
|
-
|
|
31
|
-
Args:
|
|
32
|
-
db_path: Path to SQLite database file
|
|
33
|
-
"""
|
|
34
|
-
self.db_path = Path(db_path)
|
|
35
|
-
self._init_version_table()
|
|
36
|
-
|
|
37
|
-
@contextmanager
|
|
38
|
-
def _get_connection(self):
|
|
39
|
-
"""Get database connection with context manager."""
|
|
40
|
-
conn = sqlite3.connect(self.db_path)
|
|
41
|
-
conn.row_factory = sqlite3.Row
|
|
42
|
-
try:
|
|
43
|
-
yield conn
|
|
44
|
-
conn.commit()
|
|
45
|
-
except Exception:
|
|
46
|
-
conn.rollback()
|
|
47
|
-
raise
|
|
48
|
-
finally:
|
|
49
|
-
conn.close()
|
|
50
|
-
|
|
51
|
-
def _init_version_table(self) -> None:
|
|
52
|
-
"""Create schema versions table if it doesn't exist."""
|
|
53
|
-
with self._get_connection() as conn:
|
|
54
|
-
cursor = conn.cursor()
|
|
55
|
-
cursor.execute("""
|
|
56
|
-
CREATE TABLE IF NOT EXISTS _schema_versions (
|
|
57
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
58
|
-
version TEXT NOT NULL UNIQUE,
|
|
59
|
-
applied_at TEXT NOT NULL,
|
|
60
|
-
description TEXT,
|
|
61
|
-
checksum TEXT
|
|
62
|
-
)
|
|
63
|
-
""")
|
|
64
|
-
cursor.execute(
|
|
65
|
-
"CREATE INDEX IF NOT EXISTS idx_schema_version "
|
|
66
|
-
"ON _schema_versions(version)"
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
def get_current_version(self) -> Optional[str]:
|
|
70
|
-
"""Get the current (latest) schema version."""
|
|
71
|
-
with self._get_connection() as conn:
|
|
72
|
-
cursor = conn.cursor()
|
|
73
|
-
cursor.execute("""
|
|
74
|
-
SELECT version FROM _schema_versions
|
|
75
|
-
ORDER BY id DESC LIMIT 1
|
|
76
|
-
""")
|
|
77
|
-
row = cursor.fetchone()
|
|
78
|
-
return row["version"] if row else None
|
|
79
|
-
|
|
80
|
-
def get_version_history(self) -> List[SchemaVersion]:
|
|
81
|
-
"""Get all applied versions in chronological order."""
|
|
82
|
-
with self._get_connection() as conn:
|
|
83
|
-
cursor = conn.cursor()
|
|
84
|
-
cursor.execute("""
|
|
85
|
-
SELECT version, applied_at, description, checksum
|
|
86
|
-
FROM _schema_versions
|
|
87
|
-
ORDER BY id ASC
|
|
88
|
-
""")
|
|
89
|
-
rows = cursor.fetchall()
|
|
90
|
-
|
|
91
|
-
return [
|
|
92
|
-
SchemaVersion(
|
|
93
|
-
version=row["version"],
|
|
94
|
-
applied_at=datetime.fromisoformat(row["applied_at"]),
|
|
95
|
-
description=row["description"] or "",
|
|
96
|
-
checksum=row["checksum"],
|
|
97
|
-
)
|
|
98
|
-
for row in rows
|
|
99
|
-
]
|
|
100
|
-
|
|
101
|
-
def record_version(self, version: SchemaVersion) -> None:
|
|
102
|
-
"""Record a new schema version."""
|
|
103
|
-
with self._get_connection() as conn:
|
|
104
|
-
cursor = conn.cursor()
|
|
105
|
-
cursor.execute(
|
|
106
|
-
"""
|
|
107
|
-
INSERT INTO _schema_versions (version, applied_at, description, checksum)
|
|
108
|
-
VALUES (?, ?, ?, ?)
|
|
109
|
-
""",
|
|
110
|
-
(
|
|
111
|
-
version.version,
|
|
112
|
-
version.applied_at.isoformat(),
|
|
113
|
-
version.description,
|
|
114
|
-
version.checksum,
|
|
115
|
-
),
|
|
116
|
-
)
|
|
117
|
-
logger.debug(f"Recorded schema version {version.version}")
|
|
118
|
-
|
|
119
|
-
def remove_version(self, version: str) -> None:
|
|
120
|
-
"""Remove a version record (for rollback)."""
|
|
121
|
-
with self._get_connection() as conn:
|
|
122
|
-
cursor = conn.cursor()
|
|
123
|
-
cursor.execute(
|
|
124
|
-
"DELETE FROM _schema_versions WHERE version = ?",
|
|
125
|
-
(version,),
|
|
126
|
-
)
|
|
127
|
-
logger.debug(f"Removed schema version {version}")
|
|
128
|
-
|
|
129
|
-
def has_version_table(self) -> bool:
|
|
130
|
-
"""Check if the version table exists."""
|
|
131
|
-
with self._get_connection() as conn:
|
|
132
|
-
cursor = conn.cursor()
|
|
133
|
-
cursor.execute("""
|
|
134
|
-
SELECT name FROM sqlite_master
|
|
135
|
-
WHERE type='table' AND name='_schema_versions'
|
|
136
|
-
""")
|
|
137
|
-
return cursor.fetchone() is not None
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
class PostgreSQLVersionStore:
|
|
141
|
-
"""
|
|
142
|
-
Version store using PostgreSQL for tracking schema versions.
|
|
143
|
-
|
|
144
|
-
Creates an `_schema_versions` table in the configured schema.
|
|
145
|
-
"""
|
|
146
|
-
|
|
147
|
-
def __init__(self, pool: Any, schema: str = "public"):
|
|
148
|
-
"""
|
|
149
|
-
Initialize PostgreSQL version store.
|
|
150
|
-
|
|
151
|
-
Args:
|
|
152
|
-
pool: psycopg connection pool
|
|
153
|
-
schema: Database schema name
|
|
154
|
-
"""
|
|
155
|
-
self._pool = pool
|
|
156
|
-
self.schema = schema
|
|
157
|
-
self._init_version_table()
|
|
158
|
-
|
|
159
|
-
@contextmanager
|
|
160
|
-
def _get_connection(self):
|
|
161
|
-
"""Get database connection from pool."""
|
|
162
|
-
with self._pool.connection() as conn:
|
|
163
|
-
yield conn
|
|
164
|
-
|
|
165
|
-
def _init_version_table(self) -> None:
|
|
166
|
-
"""Create schema versions table if it doesn't exist."""
|
|
167
|
-
with self._get_connection() as conn:
|
|
168
|
-
conn.execute(f"""
|
|
169
|
-
CREATE TABLE IF NOT EXISTS {self.schema}._schema_versions (
|
|
170
|
-
id SERIAL PRIMARY KEY,
|
|
171
|
-
version TEXT NOT NULL UNIQUE,
|
|
172
|
-
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
173
|
-
description TEXT,
|
|
174
|
-
checksum TEXT
|
|
175
|
-
)
|
|
176
|
-
""")
|
|
177
|
-
conn.execute(f"""
|
|
178
|
-
CREATE INDEX IF NOT EXISTS idx_schema_version
|
|
179
|
-
ON {self.schema}._schema_versions(version)
|
|
180
|
-
""")
|
|
181
|
-
conn.commit()
|
|
182
|
-
|
|
183
|
-
def get_current_version(self) -> Optional[str]:
|
|
184
|
-
"""Get the current (latest) schema version."""
|
|
185
|
-
with self._get_connection() as conn:
|
|
186
|
-
cursor = conn.execute(f"""
|
|
187
|
-
SELECT version FROM {self.schema}._schema_versions
|
|
188
|
-
ORDER BY id DESC LIMIT 1
|
|
189
|
-
""")
|
|
190
|
-
row = cursor.fetchone()
|
|
191
|
-
return row["version"] if row else None
|
|
192
|
-
|
|
193
|
-
def get_version_history(self) -> List[SchemaVersion]:
|
|
194
|
-
"""Get all applied versions in chronological order."""
|
|
195
|
-
with self._get_connection() as conn:
|
|
196
|
-
cursor = conn.execute(f"""
|
|
197
|
-
SELECT version, applied_at, description, checksum
|
|
198
|
-
FROM {self.schema}._schema_versions
|
|
199
|
-
ORDER BY id ASC
|
|
200
|
-
""")
|
|
201
|
-
rows = cursor.fetchall()
|
|
202
|
-
|
|
203
|
-
return [
|
|
204
|
-
SchemaVersion(
|
|
205
|
-
version=row["version"],
|
|
206
|
-
applied_at=row["applied_at"]
|
|
207
|
-
if isinstance(row["applied_at"], datetime)
|
|
208
|
-
else datetime.fromisoformat(str(row["applied_at"])),
|
|
209
|
-
description=row["description"] or "",
|
|
210
|
-
checksum=row["checksum"],
|
|
211
|
-
)
|
|
212
|
-
for row in rows
|
|
213
|
-
]
|
|
214
|
-
|
|
215
|
-
def record_version(self, version: SchemaVersion) -> None:
|
|
216
|
-
"""Record a new schema version."""
|
|
217
|
-
with self._get_connection() as conn:
|
|
218
|
-
conn.execute(
|
|
219
|
-
f"""
|
|
220
|
-
INSERT INTO {self.schema}._schema_versions
|
|
221
|
-
(version, applied_at, description, checksum)
|
|
222
|
-
VALUES (%s, %s, %s, %s)
|
|
223
|
-
""",
|
|
224
|
-
(
|
|
225
|
-
version.version,
|
|
226
|
-
version.applied_at,
|
|
227
|
-
version.description,
|
|
228
|
-
version.checksum,
|
|
229
|
-
),
|
|
230
|
-
)
|
|
231
|
-
conn.commit()
|
|
232
|
-
logger.debug(f"Recorded schema version {version.version}")
|
|
233
|
-
|
|
234
|
-
def remove_version(self, version: str) -> None:
|
|
235
|
-
"""Remove a version record (for rollback)."""
|
|
236
|
-
with self._get_connection() as conn:
|
|
237
|
-
conn.execute(
|
|
238
|
-
f"DELETE FROM {self.schema}._schema_versions WHERE version = %s",
|
|
239
|
-
(version,),
|
|
240
|
-
)
|
|
241
|
-
conn.commit()
|
|
242
|
-
logger.debug(f"Removed schema version {version}")
|
|
243
|
-
|
|
244
|
-
def has_version_table(self) -> bool:
|
|
245
|
-
"""Check if the version table exists."""
|
|
246
|
-
with self._get_connection() as conn:
|
|
247
|
-
cursor = conn.execute(
|
|
248
|
-
"""
|
|
249
|
-
SELECT EXISTS (
|
|
250
|
-
SELECT FROM information_schema.tables
|
|
251
|
-
WHERE table_schema = %s
|
|
252
|
-
AND table_name = '_schema_versions'
|
|
253
|
-
)
|
|
254
|
-
""",
|
|
255
|
-
(self.schema,),
|
|
256
|
-
)
|
|
257
|
-
row = cursor.fetchone()
|
|
258
|
-
return row["exists"] if row else False
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
class FileBasedVersionStore:
|
|
262
|
-
"""
|
|
263
|
-
Version store using a JSON file for tracking schema versions.
|
|
264
|
-
|
|
265
|
-
Useful for file-based storage backends.
|
|
266
|
-
"""
|
|
267
|
-
|
|
268
|
-
def __init__(self, storage_dir: Path):
|
|
269
|
-
"""
|
|
270
|
-
Initialize file-based version store.
|
|
271
|
-
|
|
272
|
-
Args:
|
|
273
|
-
storage_dir: Directory to store version file
|
|
274
|
-
"""
|
|
275
|
-
self.storage_dir = Path(storage_dir)
|
|
276
|
-
self.version_file = self.storage_dir / "_schema_versions.json"
|
|
277
|
-
self._ensure_file_exists()
|
|
278
|
-
|
|
279
|
-
def _ensure_file_exists(self) -> None:
|
|
280
|
-
"""Create version file if it doesn't exist."""
|
|
281
|
-
self.storage_dir.mkdir(parents=True, exist_ok=True)
|
|
282
|
-
if not self.version_file.exists():
|
|
283
|
-
self._write_versions([])
|
|
284
|
-
|
|
285
|
-
def _read_versions(self) -> List[dict]:
|
|
286
|
-
"""Read versions from file."""
|
|
287
|
-
try:
|
|
288
|
-
with open(self.version_file, "r") as f:
|
|
289
|
-
return json.load(f)
|
|
290
|
-
except (json.JSONDecodeError, FileNotFoundError):
|
|
291
|
-
return []
|
|
292
|
-
|
|
293
|
-
def _write_versions(self, versions: List[dict]) -> None:
|
|
294
|
-
"""Write versions to file."""
|
|
295
|
-
with open(self.version_file, "w") as f:
|
|
296
|
-
json.dump(versions, f, indent=2, default=str)
|
|
297
|
-
|
|
298
|
-
def get_current_version(self) -> Optional[str]:
|
|
299
|
-
"""Get the current (latest) schema version."""
|
|
300
|
-
versions = self._read_versions()
|
|
301
|
-
if not versions:
|
|
302
|
-
return None
|
|
303
|
-
return versions[-1]["version"]
|
|
304
|
-
|
|
305
|
-
def get_version_history(self) -> List[SchemaVersion]:
|
|
306
|
-
"""Get all applied versions in chronological order."""
|
|
307
|
-
versions = self._read_versions()
|
|
308
|
-
return [
|
|
309
|
-
SchemaVersion(
|
|
310
|
-
version=v["version"],
|
|
311
|
-
applied_at=datetime.fromisoformat(v["applied_at"]),
|
|
312
|
-
description=v.get("description", ""),
|
|
313
|
-
checksum=v.get("checksum"),
|
|
314
|
-
)
|
|
315
|
-
for v in versions
|
|
316
|
-
]
|
|
317
|
-
|
|
318
|
-
def record_version(self, version: SchemaVersion) -> None:
|
|
319
|
-
"""Record a new schema version."""
|
|
320
|
-
versions = self._read_versions()
|
|
321
|
-
versions.append(
|
|
322
|
-
{
|
|
323
|
-
"version": version.version,
|
|
324
|
-
"applied_at": version.applied_at.isoformat(),
|
|
325
|
-
"description": version.description,
|
|
326
|
-
"checksum": version.checksum,
|
|
327
|
-
}
|
|
328
|
-
)
|
|
329
|
-
self._write_versions(versions)
|
|
330
|
-
logger.debug(f"Recorded schema version {version.version}")
|
|
331
|
-
|
|
332
|
-
def remove_version(self, version: str) -> None:
|
|
333
|
-
"""Remove a version record (for rollback)."""
|
|
334
|
-
versions = self._read_versions()
|
|
335
|
-
versions = [v for v in versions if v["version"] != version]
|
|
336
|
-
self._write_versions(versions)
|
|
337
|
-
logger.debug(f"Removed schema version {version}")
|
|
1
|
+
"""
|
|
2
|
+
ALMA Migration Framework - Version Stores.
|
|
3
|
+
|
|
4
|
+
Implementations of version tracking for different storage backends.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import sqlite3
|
|
10
|
+
from contextlib import contextmanager
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, List, Optional
|
|
14
|
+
|
|
15
|
+
from alma.storage.migrations.base import SchemaVersion
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SQLiteVersionStore:
|
|
21
|
+
"""
|
|
22
|
+
Version store using SQLite for tracking schema versions.
|
|
23
|
+
|
|
24
|
+
Creates a `_schema_versions` table to track applied migrations.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, db_path: Path):
|
|
28
|
+
"""
|
|
29
|
+
Initialize SQLite version store.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
db_path: Path to SQLite database file
|
|
33
|
+
"""
|
|
34
|
+
self.db_path = Path(db_path)
|
|
35
|
+
self._init_version_table()
|
|
36
|
+
|
|
37
|
+
@contextmanager
|
|
38
|
+
def _get_connection(self):
|
|
39
|
+
"""Get database connection with context manager."""
|
|
40
|
+
conn = sqlite3.connect(self.db_path)
|
|
41
|
+
conn.row_factory = sqlite3.Row
|
|
42
|
+
try:
|
|
43
|
+
yield conn
|
|
44
|
+
conn.commit()
|
|
45
|
+
except Exception:
|
|
46
|
+
conn.rollback()
|
|
47
|
+
raise
|
|
48
|
+
finally:
|
|
49
|
+
conn.close()
|
|
50
|
+
|
|
51
|
+
def _init_version_table(self) -> None:
|
|
52
|
+
"""Create schema versions table if it doesn't exist."""
|
|
53
|
+
with self._get_connection() as conn:
|
|
54
|
+
cursor = conn.cursor()
|
|
55
|
+
cursor.execute("""
|
|
56
|
+
CREATE TABLE IF NOT EXISTS _schema_versions (
|
|
57
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
58
|
+
version TEXT NOT NULL UNIQUE,
|
|
59
|
+
applied_at TEXT NOT NULL,
|
|
60
|
+
description TEXT,
|
|
61
|
+
checksum TEXT
|
|
62
|
+
)
|
|
63
|
+
""")
|
|
64
|
+
cursor.execute(
|
|
65
|
+
"CREATE INDEX IF NOT EXISTS idx_schema_version "
|
|
66
|
+
"ON _schema_versions(version)"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def get_current_version(self) -> Optional[str]:
|
|
70
|
+
"""Get the current (latest) schema version."""
|
|
71
|
+
with self._get_connection() as conn:
|
|
72
|
+
cursor = conn.cursor()
|
|
73
|
+
cursor.execute("""
|
|
74
|
+
SELECT version FROM _schema_versions
|
|
75
|
+
ORDER BY id DESC LIMIT 1
|
|
76
|
+
""")
|
|
77
|
+
row = cursor.fetchone()
|
|
78
|
+
return row["version"] if row else None
|
|
79
|
+
|
|
80
|
+
def get_version_history(self) -> List[SchemaVersion]:
|
|
81
|
+
"""Get all applied versions in chronological order."""
|
|
82
|
+
with self._get_connection() as conn:
|
|
83
|
+
cursor = conn.cursor()
|
|
84
|
+
cursor.execute("""
|
|
85
|
+
SELECT version, applied_at, description, checksum
|
|
86
|
+
FROM _schema_versions
|
|
87
|
+
ORDER BY id ASC
|
|
88
|
+
""")
|
|
89
|
+
rows = cursor.fetchall()
|
|
90
|
+
|
|
91
|
+
return [
|
|
92
|
+
SchemaVersion(
|
|
93
|
+
version=row["version"],
|
|
94
|
+
applied_at=datetime.fromisoformat(row["applied_at"]),
|
|
95
|
+
description=row["description"] or "",
|
|
96
|
+
checksum=row["checksum"],
|
|
97
|
+
)
|
|
98
|
+
for row in rows
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
def record_version(self, version: SchemaVersion) -> None:
|
|
102
|
+
"""Record a new schema version."""
|
|
103
|
+
with self._get_connection() as conn:
|
|
104
|
+
cursor = conn.cursor()
|
|
105
|
+
cursor.execute(
|
|
106
|
+
"""
|
|
107
|
+
INSERT INTO _schema_versions (version, applied_at, description, checksum)
|
|
108
|
+
VALUES (?, ?, ?, ?)
|
|
109
|
+
""",
|
|
110
|
+
(
|
|
111
|
+
version.version,
|
|
112
|
+
version.applied_at.isoformat(),
|
|
113
|
+
version.description,
|
|
114
|
+
version.checksum,
|
|
115
|
+
),
|
|
116
|
+
)
|
|
117
|
+
logger.debug(f"Recorded schema version {version.version}")
|
|
118
|
+
|
|
119
|
+
def remove_version(self, version: str) -> None:
|
|
120
|
+
"""Remove a version record (for rollback)."""
|
|
121
|
+
with self._get_connection() as conn:
|
|
122
|
+
cursor = conn.cursor()
|
|
123
|
+
cursor.execute(
|
|
124
|
+
"DELETE FROM _schema_versions WHERE version = ?",
|
|
125
|
+
(version,),
|
|
126
|
+
)
|
|
127
|
+
logger.debug(f"Removed schema version {version}")
|
|
128
|
+
|
|
129
|
+
def has_version_table(self) -> bool:
|
|
130
|
+
"""Check if the version table exists."""
|
|
131
|
+
with self._get_connection() as conn:
|
|
132
|
+
cursor = conn.cursor()
|
|
133
|
+
cursor.execute("""
|
|
134
|
+
SELECT name FROM sqlite_master
|
|
135
|
+
WHERE type='table' AND name='_schema_versions'
|
|
136
|
+
""")
|
|
137
|
+
return cursor.fetchone() is not None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class PostgreSQLVersionStore:
|
|
141
|
+
"""
|
|
142
|
+
Version store using PostgreSQL for tracking schema versions.
|
|
143
|
+
|
|
144
|
+
Creates an `_schema_versions` table in the configured schema.
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
def __init__(self, pool: Any, schema: str = "public"):
|
|
148
|
+
"""
|
|
149
|
+
Initialize PostgreSQL version store.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
pool: psycopg connection pool
|
|
153
|
+
schema: Database schema name
|
|
154
|
+
"""
|
|
155
|
+
self._pool = pool
|
|
156
|
+
self.schema = schema
|
|
157
|
+
self._init_version_table()
|
|
158
|
+
|
|
159
|
+
@contextmanager
|
|
160
|
+
def _get_connection(self):
|
|
161
|
+
"""Get database connection from pool."""
|
|
162
|
+
with self._pool.connection() as conn:
|
|
163
|
+
yield conn
|
|
164
|
+
|
|
165
|
+
def _init_version_table(self) -> None:
|
|
166
|
+
"""Create schema versions table if it doesn't exist."""
|
|
167
|
+
with self._get_connection() as conn:
|
|
168
|
+
conn.execute(f"""
|
|
169
|
+
CREATE TABLE IF NOT EXISTS {self.schema}._schema_versions (
|
|
170
|
+
id SERIAL PRIMARY KEY,
|
|
171
|
+
version TEXT NOT NULL UNIQUE,
|
|
172
|
+
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
173
|
+
description TEXT,
|
|
174
|
+
checksum TEXT
|
|
175
|
+
)
|
|
176
|
+
""")
|
|
177
|
+
conn.execute(f"""
|
|
178
|
+
CREATE INDEX IF NOT EXISTS idx_schema_version
|
|
179
|
+
ON {self.schema}._schema_versions(version)
|
|
180
|
+
""")
|
|
181
|
+
conn.commit()
|
|
182
|
+
|
|
183
|
+
def get_current_version(self) -> Optional[str]:
|
|
184
|
+
"""Get the current (latest) schema version."""
|
|
185
|
+
with self._get_connection() as conn:
|
|
186
|
+
cursor = conn.execute(f"""
|
|
187
|
+
SELECT version FROM {self.schema}._schema_versions
|
|
188
|
+
ORDER BY id DESC LIMIT 1
|
|
189
|
+
""")
|
|
190
|
+
row = cursor.fetchone()
|
|
191
|
+
return row["version"] if row else None
|
|
192
|
+
|
|
193
|
+
def get_version_history(self) -> List[SchemaVersion]:
|
|
194
|
+
"""Get all applied versions in chronological order."""
|
|
195
|
+
with self._get_connection() as conn:
|
|
196
|
+
cursor = conn.execute(f"""
|
|
197
|
+
SELECT version, applied_at, description, checksum
|
|
198
|
+
FROM {self.schema}._schema_versions
|
|
199
|
+
ORDER BY id ASC
|
|
200
|
+
""")
|
|
201
|
+
rows = cursor.fetchall()
|
|
202
|
+
|
|
203
|
+
return [
|
|
204
|
+
SchemaVersion(
|
|
205
|
+
version=row["version"],
|
|
206
|
+
applied_at=row["applied_at"]
|
|
207
|
+
if isinstance(row["applied_at"], datetime)
|
|
208
|
+
else datetime.fromisoformat(str(row["applied_at"])),
|
|
209
|
+
description=row["description"] or "",
|
|
210
|
+
checksum=row["checksum"],
|
|
211
|
+
)
|
|
212
|
+
for row in rows
|
|
213
|
+
]
|
|
214
|
+
|
|
215
|
+
def record_version(self, version: SchemaVersion) -> None:
|
|
216
|
+
"""Record a new schema version."""
|
|
217
|
+
with self._get_connection() as conn:
|
|
218
|
+
conn.execute(
|
|
219
|
+
f"""
|
|
220
|
+
INSERT INTO {self.schema}._schema_versions
|
|
221
|
+
(version, applied_at, description, checksum)
|
|
222
|
+
VALUES (%s, %s, %s, %s)
|
|
223
|
+
""",
|
|
224
|
+
(
|
|
225
|
+
version.version,
|
|
226
|
+
version.applied_at,
|
|
227
|
+
version.description,
|
|
228
|
+
version.checksum,
|
|
229
|
+
),
|
|
230
|
+
)
|
|
231
|
+
conn.commit()
|
|
232
|
+
logger.debug(f"Recorded schema version {version.version}")
|
|
233
|
+
|
|
234
|
+
def remove_version(self, version: str) -> None:
|
|
235
|
+
"""Remove a version record (for rollback)."""
|
|
236
|
+
with self._get_connection() as conn:
|
|
237
|
+
conn.execute(
|
|
238
|
+
f"DELETE FROM {self.schema}._schema_versions WHERE version = %s",
|
|
239
|
+
(version,),
|
|
240
|
+
)
|
|
241
|
+
conn.commit()
|
|
242
|
+
logger.debug(f"Removed schema version {version}")
|
|
243
|
+
|
|
244
|
+
def has_version_table(self) -> bool:
|
|
245
|
+
"""Check if the version table exists."""
|
|
246
|
+
with self._get_connection() as conn:
|
|
247
|
+
cursor = conn.execute(
|
|
248
|
+
"""
|
|
249
|
+
SELECT EXISTS (
|
|
250
|
+
SELECT FROM information_schema.tables
|
|
251
|
+
WHERE table_schema = %s
|
|
252
|
+
AND table_name = '_schema_versions'
|
|
253
|
+
)
|
|
254
|
+
""",
|
|
255
|
+
(self.schema,),
|
|
256
|
+
)
|
|
257
|
+
row = cursor.fetchone()
|
|
258
|
+
return row["exists"] if row else False
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class FileBasedVersionStore:
|
|
262
|
+
"""
|
|
263
|
+
Version store using a JSON file for tracking schema versions.
|
|
264
|
+
|
|
265
|
+
Useful for file-based storage backends.
|
|
266
|
+
"""
|
|
267
|
+
|
|
268
|
+
def __init__(self, storage_dir: Path):
|
|
269
|
+
"""
|
|
270
|
+
Initialize file-based version store.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
storage_dir: Directory to store version file
|
|
274
|
+
"""
|
|
275
|
+
self.storage_dir = Path(storage_dir)
|
|
276
|
+
self.version_file = self.storage_dir / "_schema_versions.json"
|
|
277
|
+
self._ensure_file_exists()
|
|
278
|
+
|
|
279
|
+
def _ensure_file_exists(self) -> None:
|
|
280
|
+
"""Create version file if it doesn't exist."""
|
|
281
|
+
self.storage_dir.mkdir(parents=True, exist_ok=True)
|
|
282
|
+
if not self.version_file.exists():
|
|
283
|
+
self._write_versions([])
|
|
284
|
+
|
|
285
|
+
def _read_versions(self) -> List[dict]:
|
|
286
|
+
"""Read versions from file."""
|
|
287
|
+
try:
|
|
288
|
+
with open(self.version_file, "r") as f:
|
|
289
|
+
return json.load(f)
|
|
290
|
+
except (json.JSONDecodeError, FileNotFoundError):
|
|
291
|
+
return []
|
|
292
|
+
|
|
293
|
+
def _write_versions(self, versions: List[dict]) -> None:
|
|
294
|
+
"""Write versions to file."""
|
|
295
|
+
with open(self.version_file, "w") as f:
|
|
296
|
+
json.dump(versions, f, indent=2, default=str)
|
|
297
|
+
|
|
298
|
+
def get_current_version(self) -> Optional[str]:
|
|
299
|
+
"""Get the current (latest) schema version."""
|
|
300
|
+
versions = self._read_versions()
|
|
301
|
+
if not versions:
|
|
302
|
+
return None
|
|
303
|
+
return versions[-1]["version"]
|
|
304
|
+
|
|
305
|
+
def get_version_history(self) -> List[SchemaVersion]:
|
|
306
|
+
"""Get all applied versions in chronological order."""
|
|
307
|
+
versions = self._read_versions()
|
|
308
|
+
return [
|
|
309
|
+
SchemaVersion(
|
|
310
|
+
version=v["version"],
|
|
311
|
+
applied_at=datetime.fromisoformat(v["applied_at"]),
|
|
312
|
+
description=v.get("description", ""),
|
|
313
|
+
checksum=v.get("checksum"),
|
|
314
|
+
)
|
|
315
|
+
for v in versions
|
|
316
|
+
]
|
|
317
|
+
|
|
318
|
+
def record_version(self, version: SchemaVersion) -> None:
|
|
319
|
+
"""Record a new schema version."""
|
|
320
|
+
versions = self._read_versions()
|
|
321
|
+
versions.append(
|
|
322
|
+
{
|
|
323
|
+
"version": version.version,
|
|
324
|
+
"applied_at": version.applied_at.isoformat(),
|
|
325
|
+
"description": version.description,
|
|
326
|
+
"checksum": version.checksum,
|
|
327
|
+
}
|
|
328
|
+
)
|
|
329
|
+
self._write_versions(versions)
|
|
330
|
+
logger.debug(f"Recorded schema version {version.version}")
|
|
331
|
+
|
|
332
|
+
def remove_version(self, version: str) -> None:
|
|
333
|
+
"""Remove a version record (for rollback)."""
|
|
334
|
+
versions = self._read_versions()
|
|
335
|
+
versions = [v for v in versions if v["version"] != version]
|
|
336
|
+
self._write_versions(versions)
|
|
337
|
+
logger.debug(f"Removed schema version {version}")
|