Verzovat schéma databáze
Dokud to běželo jen doma, šlo dice.db v nejhorším smazat. Teď jsou venku binárky, které si zakládají databázi v %LOCALAPPDATA% a v Library, a ta data patří cizím lidem — první sloupec, který přibude, na nich prostě nebude. Verze se drží v PRAGMA user_version. Existující databáze mají nulu a plné schéma zároveň, což vychází: SCHEMA je samé IF NOT EXISTS, takže se na nich jen orazítkuje a nic se neztratí. Databázi z novější verze aplikace radši odmítneme otevřít. Srozumitelná hláška je lepší než tiše rozbitá data, ke kterým neexistuje záloha.
This commit is contained in:
1 parent
3af8fe22ef
commit
4ffd7d3c9d
3 files changed
+113
-1
No files matched your search
@@ -95,6 +95,12 @@ sad vedle sebe.
|
|||||||
Zálohovat i stěhovat jde prostým zkopírováním souboru; `DICE_DB` si ho
|
Zálohovat i stěhovat jde prostým zkopírováním souboru; `DICE_DB` si ho
|
||||||
najde kdekoli.
|
najde kdekoli.
|
||||||
|
|
||||||
|
Schéma má verzi uloženou v `PRAGMA user_version` a `storage.migrate()` si
|
||||||
|
databázi při každém připojení dorovná. Starší soubory z verzí, kdy se verze
|
||||||
|
schématu ještě nepsala, se poznají podle nuly a orazítkují se. Databázi
|
||||||
|
založenou novější verzí počitadla appka odmítne otevřít, místo aby ji tiše
|
||||||
|
rozbila.
|
||||||
|
|
||||||
Počítá se s domácí sítí — appka nemá přihlašování a kdokoli na stejné WiFi
|
Počítá se s domácí sítí — appka nemá přihlašování a kdokoli na stejné WiFi
|
||||||
může zapisovat.
|
může zapisovat.
|
||||||
|
|
||||||
|
|||||||
+40
-1
@@ -86,6 +86,45 @@ CREATE TABLE IF NOT EXISTS turns (
|
|||||||
CREATE INDEX IF NOT EXISTS turns_game ON turns(game_id, id);
|
CREATE INDEX IF NOT EXISTS turns_game ON turns(game_id, id);
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Každý krok posouvá schéma o jedno dál; krok na indexu 0 vede z verze 1 na 2.
|
||||||
|
# Přidávat jen na konec a nikdy nesahat na ten, který už někomu proběhl —
|
||||||
|
# databáze leží lidem na discích a zpátky se nedají stáhnout.
|
||||||
|
MIGRATIONS: list[str] = []
|
||||||
|
|
||||||
|
SCHEMA_VERSION = 1 + len(MIGRATIONS)
|
||||||
|
|
||||||
|
|
||||||
|
def migrate(conn: sqlite3.Connection) -> int:
|
||||||
|
"""Dorovná databázi na aktuální schéma a vrátí její verzi.
|
||||||
|
|
||||||
|
Verze se drží v `PRAGMA user_version`, což je celé číslo, které si SQLite
|
||||||
|
veze v hlavičce souboru a jinak si ho nevšímá.
|
||||||
|
|
||||||
|
Nula znamená dvojí: buď je soubor prázdný, nebo pochází z doby, kdy se
|
||||||
|
verze ještě nezapisovala. Obojí se vyřeší stejně — `SCHEMA` je samé
|
||||||
|
`IF NOT EXISTS`, takže na hotové databázi neudělá nic a jen se orazítkuje.
|
||||||
|
"""
|
||||||
|
version = conn.execute("PRAGMA user_version").fetchone()[0]
|
||||||
|
|
||||||
|
if version > SCHEMA_VERSION:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Databáze je ve verzi {version}, tahle aplikace umí "
|
||||||
|
f"{SCHEMA_VERSION}. Nejspíš ji založila novější verze počitadla; "
|
||||||
|
f"aktualizuj, ať o data nepřijdeš.")
|
||||||
|
|
||||||
|
if version == 0:
|
||||||
|
conn.executescript(SCHEMA)
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
for step in MIGRATIONS[version - 1:]:
|
||||||
|
conn.executescript(step)
|
||||||
|
version += 1
|
||||||
|
|
||||||
|
# PRAGMA neumí zástupné otazníky. Číslo sem teče z kódu, ne od uživatele.
|
||||||
|
conn.execute(f"PRAGMA user_version = {version}")
|
||||||
|
conn.commit()
|
||||||
|
return version
|
||||||
|
|
||||||
|
|
||||||
def connect(path: Path | str | None = None) -> sqlite3.Connection:
|
def connect(path: Path | str | None = None) -> sqlite3.Connection:
|
||||||
target = Path(path or DB_PATH)
|
target = Path(path or DB_PATH)
|
||||||
@@ -93,7 +132,7 @@ def connect(path: Path | str | None = None) -> sqlite3.Connection:
|
|||||||
conn = sqlite3.connect(target)
|
conn = sqlite3.connect(target)
|
||||||
conn.row_factory = sqlite3.Row
|
conn.row_factory = sqlite3.Row
|
||||||
conn.execute("PRAGMA foreign_keys = ON")
|
conn.execute("PRAGMA foreign_keys = ON")
|
||||||
conn.executescript(SCHEMA)
|
migrate(conn)
|
||||||
return conn
|
return conn
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import sqlite3
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -182,3 +183,69 @@ def test_connect_zalozi_chybejici_adresar(tmp_path):
|
|||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
assert (tmp_path / "novy" / "adresar" / "dice.db").exists()
|
assert (tmp_path / "novy" / "adresar" / "dice.db").exists()
|
||||||
|
|
||||||
|
|
||||||
|
# --- verze schématu ---------------------------------------------------------
|
||||||
|
|
||||||
|
def verze(conn) -> int:
|
||||||
|
return conn.execute("PRAGMA user_version").fetchone()[0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_nova_databaze_je_orazitkovana(tmp_path):
|
||||||
|
conn = storage.connect(tmp_path / "nova.db")
|
||||||
|
try:
|
||||||
|
assert verze(conn) == storage.SCHEMA_VERSION
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_databaze_z_doby_pred_verzovanim_prezije(tmp_path):
|
||||||
|
"""Přesně to, co lidem leží na disku z verzí 1.0 a 1.1.
|
||||||
|
|
||||||
|
Tabulky má, razítko ne. Nesmí se ani přijít o data, ani spadnout.
|
||||||
|
"""
|
||||||
|
path = tmp_path / "stara.db"
|
||||||
|
stara = sqlite3.connect(path)
|
||||||
|
stara.executescript(storage.SCHEMA) # bez PRAGMA user_version
|
||||||
|
stara.execute("INSERT INTO players (name, key, created_at) "
|
||||||
|
"VALUES ('Adam', 'adam', '2026-01-01T00:00:00')")
|
||||||
|
stara.commit()
|
||||||
|
stara.close()
|
||||||
|
|
||||||
|
conn = storage.connect(path)
|
||||||
|
try:
|
||||||
|
assert verze(conn) == storage.SCHEMA_VERSION
|
||||||
|
assert [p["name"] for p in storage.known_players(conn)] == ["Adam"]
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_migrace_dojede_jen_ty_chybejici(tmp_path, monkeypatch):
|
||||||
|
"""Podruhé už krok proběhnout nesmí — jinak by ALTER TABLE spadl."""
|
||||||
|
monkeypatch.setattr(storage, "MIGRATIONS",
|
||||||
|
["ALTER TABLE players ADD COLUMN barva TEXT;"])
|
||||||
|
monkeypatch.setattr(storage, "SCHEMA_VERSION", 2)
|
||||||
|
|
||||||
|
path = tmp_path / "migrovana.db"
|
||||||
|
conn = storage.connect(path)
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
conn = storage.connect(path) # druhé spuštění téže verze
|
||||||
|
try:
|
||||||
|
assert verze(conn) == 2
|
||||||
|
sloupce = [r[1] for r in conn.execute("PRAGMA table_info(players)")]
|
||||||
|
assert "barva" in sloupce
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_databaze_z_novejsi_verze_se_odmitne(tmp_path):
|
||||||
|
"""Radši srozumitelná hláška než tiše rozbitá data."""
|
||||||
|
path = tmp_path / "budouci.db"
|
||||||
|
conn = storage.connect(path)
|
||||||
|
conn.execute(f"PRAGMA user_version = {storage.SCHEMA_VERSION + 1}")
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError, match="novější verze"):
|
||||||
|
storage.connect(path)
|
||||||
Reference in new issue
Block a user