feat: sqlite migration runner with meta version table
This commit is contained in:
@@ -0,0 +1,17 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
import sqlite3
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def open_db(path: Path):
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
conn = sqlite3.connect(path)
|
||||||
|
conn.execute("PRAGMA journal_mode=WAL")
|
||||||
|
conn.execute("PRAGMA foreign_keys=ON")
|
||||||
|
try:
|
||||||
|
yield conn
|
||||||
|
conn.commit()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from chat.db.connection import open_db
|
||||||
|
|
||||||
|
MIGRATIONS_DIR = Path(__file__).parent / "migrations"
|
||||||
|
|
||||||
|
|
||||||
|
def apply_migrations(db_path: Path) -> None:
|
||||||
|
with open_db(db_path) as conn:
|
||||||
|
conn.execute(
|
||||||
|
"CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT)"
|
||||||
|
)
|
||||||
|
cur = conn.execute("SELECT value FROM meta WHERE key = 'schema_version'")
|
||||||
|
row = cur.fetchone()
|
||||||
|
current = int(row[0]) if row else 0
|
||||||
|
for path in sorted(MIGRATIONS_DIR.glob("*.sql")):
|
||||||
|
version = int(path.stem.split("_", 1)[0])
|
||||||
|
if version <= current:
|
||||||
|
continue
|
||||||
|
sql = path.read_text()
|
||||||
|
conn.executescript(sql)
|
||||||
|
conn.execute(
|
||||||
|
"INSERT OR REPLACE INTO meta (key, value) VALUES ('schema_version', ?)",
|
||||||
|
(str(version),),
|
||||||
|
)
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- meta table is created by the migrate runner; this migration is a marker.
|
||||||
|
SELECT 1;
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
from chat.db.connection import open_db
|
||||||
|
from chat.db.migrate import apply_migrations
|
||||||
|
|
||||||
|
|
||||||
|
def test_apply_migrations_creates_meta_table(tmp_path):
|
||||||
|
db = tmp_path / "test.db"
|
||||||
|
apply_migrations(db)
|
||||||
|
with open_db(db) as conn:
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT value FROM meta WHERE key = 'schema_version'"
|
||||||
|
).fetchone()
|
||||||
|
assert row is not None
|
||||||
|
assert int(row[0]) >= 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_apply_migrations_idempotent(tmp_path):
|
||||||
|
db = tmp_path / "test.db"
|
||||||
|
apply_migrations(db)
|
||||||
|
apply_migrations(db) # second call must be a no-op
|
||||||
|
with open_db(db) as conn:
|
||||||
|
count = conn.execute("SELECT COUNT(*) FROM meta").fetchone()[0]
|
||||||
|
assert count == 1
|
||||||
Reference in New Issue
Block a user