404659a545
Конфиг утилиты переведён на YAML; default_baseconfs для reindex без -b; автопоиск config.local.yml/.yaml/.json; обобщённая документация. Co-authored-by: Cursor <cursoragent@cursor.com>
631 lines
19 KiB
Python
631 lines
19 KiB
Python
"""Единый SQLite-индекс проекта (все конфигурации 1С)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sqlite3
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from typing import Any, Iterable
|
|
|
|
SCHEMA_VERSION = 3
|
|
|
|
DDL = """
|
|
PRAGMA journal_mode=WAL;
|
|
PRAGMA synchronous=NORMAL;
|
|
|
|
CREATE TABLE IF NOT EXISTS meta (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS configs (
|
|
name TEXT PRIMARY KEY,
|
|
src TEXT NOT NULL,
|
|
objects INTEGER NOT NULL DEFAULT 0,
|
|
fields INTEGER NOT NULL DEFAULT 0,
|
|
refs INTEGER NOT NULL DEFAULT 0,
|
|
modules INTEGER NOT NULL DEFAULT 0,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS files (
|
|
id INTEGER PRIMARY KEY,
|
|
config TEXT NOT NULL,
|
|
rel_path TEXT NOT NULL,
|
|
kind TEXT NOT NULL, -- meta_xml | module_bsl
|
|
mtime_ns INTEGER NOT NULL,
|
|
size INTEGER NOT NULL,
|
|
sha1 TEXT,
|
|
object_type TEXT,
|
|
UNIQUE(config, rel_path)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS objects (
|
|
id INTEGER PRIMARY KEY,
|
|
config TEXT NOT NULL,
|
|
object_type TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
full_name TEXT NOT NULL,
|
|
synonym TEXT,
|
|
comment TEXT,
|
|
tooltip TEXT,
|
|
explanation TEXT,
|
|
uuid TEXT,
|
|
path TEXT,
|
|
file_id INTEGER,
|
|
json TEXT NOT NULL,
|
|
UNIQUE(config, full_name),
|
|
FOREIGN KEY(file_id) REFERENCES files(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS fields (
|
|
id INTEGER PRIMARY KEY,
|
|
object_id INTEGER NOT NULL,
|
|
config TEXT NOT NULL,
|
|
kind TEXT,
|
|
name TEXT,
|
|
synonym TEXT,
|
|
comment TEXT,
|
|
types TEXT,
|
|
tabular_section TEXT,
|
|
refs_json TEXT,
|
|
FOREIGN KEY(object_id) REFERENCES objects(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS refs (
|
|
id INTEGER PRIMARY KEY,
|
|
config TEXT NOT NULL,
|
|
from_full_name TEXT NOT NULL,
|
|
from_field TEXT NOT NULL,
|
|
to_kind TEXT NOT NULL,
|
|
to_name TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS register_records (
|
|
id INTEGER PRIMARY KEY,
|
|
config TEXT NOT NULL,
|
|
document_full_name TEXT NOT NULL,
|
|
register_full_name TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS modules (
|
|
id INTEGER PRIMARY KEY,
|
|
config TEXT NOT NULL,
|
|
rel_path TEXT NOT NULL,
|
|
owner TEXT,
|
|
module_kind TEXT,
|
|
symbols TEXT,
|
|
line_count INTEGER,
|
|
size INTEGER,
|
|
file_id INTEGER,
|
|
UNIQUE(config, rel_path),
|
|
FOREIGN KEY(file_id) REFERENCES files(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE VIRTUAL TABLE IF NOT EXISTS objects_fts USING fts5(
|
|
full_name,
|
|
synonym,
|
|
comment,
|
|
fields_text,
|
|
object_id UNINDEXED,
|
|
config UNINDEXED,
|
|
tokenize='unicode61'
|
|
);
|
|
|
|
CREATE VIRTUAL TABLE IF NOT EXISTS modules_fts USING fts5(
|
|
rel_path,
|
|
owner,
|
|
module_kind,
|
|
symbols,
|
|
body,
|
|
module_id UNINDEXED,
|
|
config UNINDEXED,
|
|
tokenize='unicode61'
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_objects_config_type ON objects(config, object_type);
|
|
CREATE INDEX IF NOT EXISTS idx_objects_name ON objects(name);
|
|
CREATE INDEX IF NOT EXISTS idx_fields_object ON fields(object_id);
|
|
CREATE INDEX IF NOT EXISTS idx_refs_to ON refs(to_kind, to_name);
|
|
CREATE INDEX IF NOT EXISTS idx_refs_from ON refs(from_full_name);
|
|
CREATE INDEX IF NOT EXISTS idx_refs_config ON refs(config);
|
|
CREATE INDEX IF NOT EXISTS idx_regrec_register ON register_records(register_full_name);
|
|
CREATE INDEX IF NOT EXISTS idx_regrec_document ON register_records(document_full_name);
|
|
CREATE INDEX IF NOT EXISTS idx_regrec_config ON register_records(config);
|
|
CREATE INDEX IF NOT EXISTS idx_modules_config ON modules(config);
|
|
CREATE INDEX IF NOT EXISTS idx_modules_owner ON modules(owner);
|
|
CREATE INDEX IF NOT EXISTS idx_files_config ON files(config);
|
|
"""
|
|
|
|
|
|
def _table_exists(conn: sqlite3.Connection, name: str) -> bool:
|
|
row = conn.execute(
|
|
"SELECT 1 FROM sqlite_master WHERE type='table' AND name=?",
|
|
(name,),
|
|
).fetchone()
|
|
return row is not None
|
|
|
|
|
|
def backfill_register_records(conn: sqlite3.Connection) -> int:
|
|
"""Заполнить register_records из JSON объектов (документы → RegisterRecords)."""
|
|
conn.execute("DELETE FROM register_records")
|
|
n = 0
|
|
rows = conn.execute(
|
|
"SELECT config, full_name, json FROM objects WHERE object_type='Document'"
|
|
).fetchall()
|
|
batch: list[tuple[str, str, str]] = []
|
|
for r in rows:
|
|
try:
|
|
data = json.loads(r["json"])
|
|
except json.JSONDecodeError:
|
|
continue
|
|
for reg in data.get("register_records") or []:
|
|
if not reg or not isinstance(reg, str):
|
|
continue
|
|
batch.append((r["config"], r["full_name"], reg))
|
|
n += 1
|
|
if len(batch) >= 500:
|
|
conn.executemany(
|
|
"""
|
|
INSERT INTO register_records(config, document_full_name, register_full_name)
|
|
VALUES(?,?,?)
|
|
""",
|
|
batch,
|
|
)
|
|
batch.clear()
|
|
if batch:
|
|
conn.executemany(
|
|
"""
|
|
INSERT INTO register_records(config, document_full_name, register_full_name)
|
|
VALUES(?,?,?)
|
|
""",
|
|
batch,
|
|
)
|
|
return n
|
|
|
|
|
|
def migrate_schema(conn: sqlite3.Connection, from_version: int) -> None:
|
|
"""Мягкая миграция без wipe БД."""
|
|
if from_version < 3:
|
|
conn.executescript(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS register_records (
|
|
id INTEGER PRIMARY KEY,
|
|
config TEXT NOT NULL,
|
|
document_full_name TEXT NOT NULL,
|
|
register_full_name TEXT NOT NULL
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_regrec_register ON register_records(register_full_name);
|
|
CREATE INDEX IF NOT EXISTS idx_regrec_document ON register_records(document_full_name);
|
|
CREATE INDEX IF NOT EXISTS idx_regrec_config ON register_records(config);
|
|
"""
|
|
)
|
|
backfill_register_records(conn)
|
|
conn.execute(
|
|
"INSERT OR REPLACE INTO meta(key, value) VALUES('schema_version', ?)",
|
|
(str(SCHEMA_VERSION),),
|
|
)
|
|
|
|
|
|
def utcnow() -> str:
|
|
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
|
|
|
|
def global_db_path(cache_root: Path) -> Path:
|
|
return cache_root / "index.sqlite"
|
|
|
|
|
|
def connect(db_path: Path, *, readonly: bool = False) -> sqlite3.Connection:
|
|
db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
if readonly and db_path.is_file():
|
|
uri = f"file:{db_path.resolve()}?mode=ro"
|
|
conn = sqlite3.connect(uri, uri=True)
|
|
else:
|
|
conn = sqlite3.connect(db_path)
|
|
conn.row_factory = sqlite3.Row
|
|
conn.execute("PRAGMA foreign_keys=ON")
|
|
return conn
|
|
|
|
|
|
def init_db(conn: sqlite3.Connection) -> None:
|
|
conn.executescript(DDL)
|
|
row = conn.execute("SELECT value FROM meta WHERE key='schema_version'").fetchone()
|
|
if row is None:
|
|
conn.execute(
|
|
"INSERT INTO meta(key, value) VALUES('schema_version', ?)",
|
|
(str(SCHEMA_VERSION),),
|
|
)
|
|
conn.execute(
|
|
"INSERT OR REPLACE INTO meta(key, value) VALUES('created_at', ?)",
|
|
(utcnow(),),
|
|
)
|
|
else:
|
|
ver = int(row["value"])
|
|
if ver > SCHEMA_VERSION:
|
|
raise RuntimeError(
|
|
f"Индекс новее утилиты (схема {ver}, утилита {SCHEMA_VERSION}). "
|
|
"Обновите утилиту index до более новой версии."
|
|
)
|
|
if ver < SCHEMA_VERSION:
|
|
migrate_schema(conn, ver)
|
|
elif not _table_exists(conn, "register_records"):
|
|
# schema_version=3, но таблица потеряна — восстановить
|
|
migrate_schema(conn, 2)
|
|
conn.commit()
|
|
|
|
|
|
def reset_db(db_path: Path) -> sqlite3.Connection:
|
|
if db_path.exists():
|
|
db_path.unlink()
|
|
for suffix in ("-wal", "-shm"):
|
|
p = Path(str(db_path) + suffix)
|
|
if p.exists():
|
|
p.unlink()
|
|
conn = connect(db_path)
|
|
init_db(conn)
|
|
return conn
|
|
|
|
|
|
def ensure_db(db_path: Path) -> sqlite3.Connection:
|
|
conn = connect(db_path)
|
|
try:
|
|
init_db(conn)
|
|
except RuntimeError:
|
|
conn.close()
|
|
raise
|
|
return conn
|
|
|
|
|
|
def get_file_index(conn: sqlite3.Connection, config: str | None = None) -> dict[str, dict[str, Any]]:
|
|
"""rel_path → {mtime_ns, size, id, kind} для инкремента."""
|
|
if config:
|
|
rows = conn.execute(
|
|
"SELECT id, rel_path, kind, mtime_ns, size FROM files WHERE config=?",
|
|
(config,),
|
|
).fetchall()
|
|
else:
|
|
rows = conn.execute(
|
|
"SELECT id, config, rel_path, kind, mtime_ns, size FROM files"
|
|
).fetchall()
|
|
out: dict[str, dict[str, Any]] = {}
|
|
for r in rows:
|
|
key = r["rel_path"] if config else f"{r['config']}:{r['rel_path']}"
|
|
out[key] = {
|
|
"id": r["id"],
|
|
"kind": r["kind"],
|
|
"mtime_ns": r["mtime_ns"],
|
|
"size": r["size"],
|
|
}
|
|
return out
|
|
|
|
|
|
def delete_file_cascade(conn: sqlite3.Connection, file_id: int) -> None:
|
|
"""Удалить файл и связанные объекты/модули/FTS."""
|
|
obj = conn.execute(
|
|
"SELECT id FROM objects WHERE file_id=?", (file_id,)
|
|
).fetchone()
|
|
if obj:
|
|
oid = obj["id"]
|
|
conn.execute("DELETE FROM objects_fts WHERE object_id=?", (oid,))
|
|
conn.execute("DELETE FROM fields WHERE object_id=?", (oid,))
|
|
row = conn.execute(
|
|
"SELECT config, full_name FROM objects WHERE id=?", (oid,)
|
|
).fetchone()
|
|
if row:
|
|
conn.execute(
|
|
"DELETE FROM refs WHERE config=? AND from_full_name=?",
|
|
(row["config"], row["full_name"]),
|
|
)
|
|
if _table_exists(conn, "register_records"):
|
|
conn.execute(
|
|
"DELETE FROM register_records WHERE config=? AND document_full_name=?",
|
|
(row["config"], row["full_name"]),
|
|
)
|
|
conn.execute("DELETE FROM objects WHERE id=?", (oid,))
|
|
|
|
mod = conn.execute(
|
|
"SELECT id FROM modules WHERE file_id=?", (file_id,)
|
|
).fetchone()
|
|
if mod:
|
|
mid = mod["id"]
|
|
conn.execute("DELETE FROM modules_fts WHERE module_id=?", (mid,))
|
|
conn.execute("DELETE FROM modules WHERE id=?", (mid,))
|
|
|
|
conn.execute("DELETE FROM files WHERE id=?", (file_id,))
|
|
|
|
|
|
def delete_config(conn: sqlite3.Connection, config: str) -> None:
|
|
oids = [
|
|
r["id"]
|
|
for r in conn.execute("SELECT id FROM objects WHERE config=?", (config,))
|
|
]
|
|
for oid in oids:
|
|
conn.execute("DELETE FROM objects_fts WHERE object_id=?", (oid,))
|
|
mids = [
|
|
r["id"]
|
|
for r in conn.execute("SELECT id FROM modules WHERE config=?", (config,))
|
|
]
|
|
for mid in mids:
|
|
conn.execute("DELETE FROM modules_fts WHERE module_id=?", (mid,))
|
|
conn.execute("DELETE FROM fields WHERE config=?", (config,))
|
|
conn.execute("DELETE FROM refs WHERE config=?", (config,))
|
|
if _table_exists(conn, "register_records"):
|
|
conn.execute("DELETE FROM register_records WHERE config=?", (config,))
|
|
conn.execute("DELETE FROM objects WHERE config=?", (config,))
|
|
conn.execute("DELETE FROM modules WHERE config=?", (config,))
|
|
conn.execute("DELETE FROM files WHERE config=?", (config,))
|
|
conn.execute("DELETE FROM configs WHERE name=?", (config,))
|
|
|
|
|
|
def upsert_file(
|
|
conn: sqlite3.Connection,
|
|
*,
|
|
config: str,
|
|
rel_path: str,
|
|
kind: str,
|
|
mtime_ns: int,
|
|
size: int,
|
|
object_type: str | None = None,
|
|
sha1: str | None = None,
|
|
) -> int:
|
|
conn.execute(
|
|
"""
|
|
INSERT INTO files(config, rel_path, kind, mtime_ns, size, sha1, object_type)
|
|
VALUES(?,?,?,?,?,?,?)
|
|
ON CONFLICT(config, rel_path) DO UPDATE SET
|
|
kind=excluded.kind,
|
|
mtime_ns=excluded.mtime_ns,
|
|
size=excluded.size,
|
|
sha1=excluded.sha1,
|
|
object_type=excluded.object_type
|
|
""",
|
|
(config, rel_path, kind, mtime_ns, size, sha1, object_type),
|
|
)
|
|
row = conn.execute(
|
|
"SELECT id FROM files WHERE config=? AND rel_path=?",
|
|
(config, rel_path),
|
|
).fetchone()
|
|
assert row is not None
|
|
return int(row["id"])
|
|
|
|
|
|
def upsert_object(
|
|
conn: sqlite3.Connection,
|
|
obj: dict[str, Any],
|
|
*,
|
|
file_id: int,
|
|
fields_text: str,
|
|
) -> int:
|
|
config = obj["config"]
|
|
full_name = obj["full_name"]
|
|
old = conn.execute(
|
|
"SELECT id FROM objects WHERE config=? AND full_name=?",
|
|
(config, full_name),
|
|
).fetchone()
|
|
if old:
|
|
oid = int(old["id"])
|
|
conn.execute("DELETE FROM objects_fts WHERE object_id=?", (oid,))
|
|
conn.execute("DELETE FROM fields WHERE object_id=?", (oid,))
|
|
conn.execute(
|
|
"DELETE FROM refs WHERE config=? AND from_full_name=?",
|
|
(config, full_name),
|
|
)
|
|
if _table_exists(conn, "register_records"):
|
|
conn.execute(
|
|
"DELETE FROM register_records WHERE config=? AND document_full_name=?",
|
|
(config, full_name),
|
|
)
|
|
conn.execute(
|
|
"""
|
|
UPDATE objects SET object_type=?, name=?, synonym=?, comment=?, tooltip=?,
|
|
explanation=?, uuid=?, path=?, file_id=?, json=?
|
|
WHERE id=?
|
|
""",
|
|
(
|
|
obj["object_type"],
|
|
obj["name"],
|
|
obj.get("synonym"),
|
|
obj.get("comment"),
|
|
obj.get("tooltip"),
|
|
obj.get("explanation"),
|
|
obj.get("uuid"),
|
|
obj.get("path"),
|
|
file_id,
|
|
json.dumps(obj, ensure_ascii=False),
|
|
oid,
|
|
),
|
|
)
|
|
else:
|
|
cur = conn.execute(
|
|
"""
|
|
INSERT INTO objects(config, object_type, name, full_name, synonym, comment,
|
|
tooltip, explanation, uuid, path, file_id, json)
|
|
VALUES(?,?,?,?,?,?,?,?,?,?,?,?)
|
|
""",
|
|
(
|
|
config,
|
|
obj["object_type"],
|
|
obj["name"],
|
|
full_name,
|
|
obj.get("synonym"),
|
|
obj.get("comment"),
|
|
obj.get("tooltip"),
|
|
obj.get("explanation"),
|
|
obj.get("uuid"),
|
|
obj.get("path"),
|
|
file_id,
|
|
json.dumps(obj, ensure_ascii=False),
|
|
),
|
|
)
|
|
oid = int(cur.lastrowid)
|
|
|
|
conn.execute(
|
|
"""
|
|
INSERT INTO objects_fts(full_name, synonym, comment, fields_text, object_id, config)
|
|
VALUES(?,?,?,?,?,?)
|
|
""",
|
|
(
|
|
full_name,
|
|
obj.get("synonym") or "",
|
|
obj.get("comment") or "",
|
|
fields_text,
|
|
oid,
|
|
config,
|
|
),
|
|
)
|
|
|
|
for f in obj.get("fields") or []:
|
|
conn.execute(
|
|
"""
|
|
INSERT INTO fields(object_id, config, kind, name, synonym, comment, types,
|
|
tabular_section, refs_json)
|
|
VALUES(?,?,?,?,?,?,?,?,?)
|
|
""",
|
|
(
|
|
oid,
|
|
config,
|
|
f.get("kind"),
|
|
f.get("name"),
|
|
f.get("synonym"),
|
|
f.get("comment"),
|
|
", ".join(f.get("types") or []),
|
|
f.get("tabular_section") or "",
|
|
json.dumps(f.get("refs") or [], ensure_ascii=False),
|
|
),
|
|
)
|
|
for r in f.get("refs") or []:
|
|
loc = f"{f['tabular_section']}." if f.get("tabular_section") else ""
|
|
conn.execute(
|
|
"""
|
|
INSERT INTO refs(config, from_full_name, from_field, to_kind, to_name)
|
|
VALUES(?,?,?,?,?)
|
|
""",
|
|
(
|
|
config,
|
|
full_name,
|
|
f"{loc}{f.get('name')}",
|
|
r.get("kind"),
|
|
r.get("name"),
|
|
),
|
|
)
|
|
|
|
if _table_exists(conn, "register_records"):
|
|
for reg in obj.get("register_records") or []:
|
|
if not reg or not isinstance(reg, str):
|
|
continue
|
|
conn.execute(
|
|
"""
|
|
INSERT INTO register_records(config, document_full_name, register_full_name)
|
|
VALUES(?,?,?)
|
|
""",
|
|
(config, full_name, reg),
|
|
)
|
|
return oid
|
|
|
|
|
|
def upsert_module(
|
|
conn: sqlite3.Connection,
|
|
*,
|
|
config: str,
|
|
rel_path: str,
|
|
owner: str,
|
|
module_kind: str,
|
|
symbols: list[str],
|
|
body: str,
|
|
line_count: int,
|
|
size: int,
|
|
file_id: int,
|
|
) -> int:
|
|
sym_text = "\n".join(symbols)
|
|
old = conn.execute(
|
|
"SELECT id FROM modules WHERE config=? AND rel_path=?",
|
|
(config, rel_path),
|
|
).fetchone()
|
|
if old:
|
|
mid = int(old["id"])
|
|
conn.execute("DELETE FROM modules_fts WHERE module_id=?", (mid,))
|
|
conn.execute(
|
|
"""
|
|
UPDATE modules SET owner=?, module_kind=?, symbols=?, line_count=?, size=?, file_id=?
|
|
WHERE id=?
|
|
""",
|
|
(owner, module_kind, sym_text, line_count, size, file_id, mid),
|
|
)
|
|
else:
|
|
cur = conn.execute(
|
|
"""
|
|
INSERT INTO modules(config, rel_path, owner, module_kind, symbols, line_count, size, file_id)
|
|
VALUES(?,?,?,?,?,?,?,?)
|
|
""",
|
|
(config, rel_path, owner, module_kind, sym_text, line_count, size, file_id),
|
|
)
|
|
mid = int(cur.lastrowid)
|
|
|
|
conn.execute(
|
|
"""
|
|
INSERT INTO modules_fts(rel_path, owner, module_kind, symbols, body, module_id, config)
|
|
VALUES(?,?,?,?,?,?,?)
|
|
""",
|
|
(rel_path, owner, module_kind, sym_text, body, mid, config),
|
|
)
|
|
return mid
|
|
|
|
|
|
def refresh_config_stats(conn: sqlite3.Connection, config: str, src: str) -> dict[str, Any]:
|
|
objects = conn.execute(
|
|
"SELECT COUNT(*) AS n FROM objects WHERE config=?", (config,)
|
|
).fetchone()["n"]
|
|
fields = conn.execute(
|
|
"SELECT COUNT(*) AS n FROM fields WHERE config=?", (config,)
|
|
).fetchone()["n"]
|
|
refs = conn.execute(
|
|
"SELECT COUNT(*) AS n FROM refs WHERE config=?", (config,)
|
|
).fetchone()["n"]
|
|
modules = conn.execute(
|
|
"SELECT COUNT(*) AS n FROM modules WHERE config=?", (config,)
|
|
).fetchone()["n"]
|
|
stats = {
|
|
"name": config,
|
|
"src": src,
|
|
"objects": objects,
|
|
"fields": fields,
|
|
"refs": refs,
|
|
"modules": modules,
|
|
"updated_at": utcnow(),
|
|
}
|
|
conn.execute(
|
|
"""
|
|
INSERT INTO configs(name, src, objects, fields, refs, modules, updated_at)
|
|
VALUES(?,?,?,?,?,?,?)
|
|
ON CONFLICT(name) DO UPDATE SET
|
|
src=excluded.src,
|
|
objects=excluded.objects,
|
|
fields=excluded.fields,
|
|
refs=excluded.refs,
|
|
modules=excluded.modules,
|
|
updated_at=excluded.updated_at
|
|
""",
|
|
(
|
|
stats["name"],
|
|
stats["src"],
|
|
stats["objects"],
|
|
stats["fields"],
|
|
stats["refs"],
|
|
stats["modules"],
|
|
stats["updated_at"],
|
|
),
|
|
)
|
|
return stats
|
|
|
|
|
|
def list_configs(conn: sqlite3.Connection) -> list[dict[str, Any]]:
|
|
return [dict(r) for r in conn.execute("SELECT * FROM configs ORDER BY name")]
|
|
|
|
|
|
def set_meta(conn: sqlite3.Connection, key: str, value: str) -> None:
|
|
conn.execute(
|
|
"INSERT OR REPLACE INTO meta(key, value) VALUES(?, ?)",
|
|
(key, value),
|
|
)
|