Release 0.3.0: movements — регистратор ↔ регистры движений

Таблица register_records, мягкая миграция схемы 2→3 и команда query_1c.py movements в обе стороны.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
7 changed files with 463 additions and 10 deletions
+116 -6
View File
@@ -8,7 +8,7 @@ from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Iterable
SCHEMA_VERSION = 2
SCHEMA_VERSION = 3
DDL = """
PRAGMA journal_mode=WAL;
@@ -82,6 +82,13 @@ CREATE TABLE IF NOT EXISTS refs (
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,
@@ -123,12 +130,84 @@ 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")
@@ -161,11 +240,18 @@ def init_db(conn: sqlite3.Connection) -> None:
"INSERT OR REPLACE INTO meta(key, value) VALUES('created_at', ?)",
(utcnow(),),
)
elif int(row["value"]) != SCHEMA_VERSION:
raise RuntimeError(
f"Несовместимая схема индекса (есть {row['value']}, нужна {SCHEMA_VERSION}). "
"Запустите: python tools/index/index_1c.py reindex --full"
)
else:
ver = int(row["value"])
if ver > SCHEMA_VERSION:
raise RuntimeError(
f"Индекс новее утилиты (схема {ver}, утилита {SCHEMA_VERSION}). "
"Обновите tools/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()
@@ -231,6 +317,11 @@ def delete_file_cascade(conn: sqlite3.Connection, file_id: int) -> None:
"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(
@@ -259,6 +350,8 @@ def delete_config(conn: sqlite3.Connection, config: str) -> None:
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,))
@@ -318,6 +411,11 @@ def upsert_object(
"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=?,
@@ -411,6 +509,18 @@ def upsert_object(
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