"""Соединение с SQLite и схема хаба. Схема создаётся при первом подключении; версия — в setting.schema_version.""" import json import sqlite3 from contextlib import contextmanager from datetime import datetime, timezone from pathlib import Path SCHEMA_VERSION = 1 SCHEMA = """ CREATE TABLE IF NOT EXISTS setting ( key TEXT PRIMARY KEY, value TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS issue ( key TEXT PRIMARY KEY, id TEXT, issue_type TEXT, summary TEXT, description TEXT, status TEXT, priority TEXT, assignee TEXT, reporter TEXT, duedate TEXT, planned_start TEXT, planned_end TEXT, epic_key TEXT, labels TEXT NOT NULL DEFAULT '[]', sprint TEXT, created TEXT, jira_updated TEXT, fetched_at TEXT NOT NULL, neighbor INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE IF NOT EXISTS issue_link ( source_key TEXT NOT NULL, target_key TEXT NOT NULL, link_type TEXT NOT NULL, PRIMARY KEY (source_key, target_key, link_type) ); CREATE INDEX IF NOT EXISTS ix_issue_link_target ON issue_link(target_key); CREATE TABLE IF NOT EXISTS issue_note ( key TEXT PRIMARY KEY, fact_status TEXT, work_kind TEXT, done_text TEXT, rest_text TEXT, owner_note TEXT, updated_at TEXT, updated_by TEXT ); CREATE TABLE IF NOT EXISTS batch ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, author TEXT NOT NULL, created_at TEXT NOT NULL, cancelled_at TEXT, estimate_seconds INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE IF NOT EXISTS command ( id INTEGER PRIMARY KEY AUTOINCREMENT, batch_id INTEGER NOT NULL REFERENCES batch(id), seq INTEGER NOT NULL, type TEXT NOT NULL, params TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'queued', claimed_at TEXT, finished_at TEXT, result TEXT, error TEXT, jira_calls INTEGER NOT NULL DEFAULT 0, rate_limited INTEGER NOT NULL DEFAULT 0, pause_seconds INTEGER, depends_on INTEGER REFERENCES command(id) ); CREATE INDEX IF NOT EXISTS ix_command_status ON command(status, batch_id, seq); CREATE TABLE IF NOT EXISTS sync_run ( id INTEGER PRIMARY KEY AUTOINCREMENT, started_at TEXT NOT NULL, finished_at TEXT, kind TEXT NOT NULL, query TEXT, issues_count INTEGER NOT NULL DEFAULT 0, links_count INTEGER NOT NULL DEFAULT 0, jira_calls INTEGER NOT NULL DEFAULT 0, rate_limited INTEGER NOT NULL DEFAULT 0, command_id INTEGER ); CREATE TABLE IF NOT EXISTS agent_status ( id INTEGER PRIMARY KEY CHECK (id = 1), last_seen_at TEXT, agent_version TEXT, settings_version INTEGER, current_command_id INTEGER, last_jira_call_at TEXT ); """ def now() -> str: """Текущее время UTC в ISO-8601 с секундами — единый формат дат в базе.""" return datetime.now(timezone.utc).replace(microsecond=0).isoformat() def connect(path) -> sqlite3.Connection: """Открыть базу (создать при отсутствии), применить схему. Соединение в автокоммите; для атомарных операций — transaction().""" is_memory = str(path) == ":memory:" if not is_memory: Path(path).parent.mkdir(parents=True, exist_ok=True) conn = sqlite3.connect(str(path), timeout=10, isolation_level=None, check_same_thread=False) conn.row_factory = sqlite3.Row if not is_memory: conn.execute("PRAGMA journal_mode = WAL") conn.execute("PRAGMA foreign_keys = ON") migrate(conn) return conn def migrate(conn: sqlite3.Connection) -> None: conn.executescript(SCHEMA) row = conn.execute("SELECT value FROM setting WHERE key = 'schema_version'").fetchone() if row is None: conn.execute("INSERT INTO setting(key, value) VALUES ('schema_version', ?)", (json.dumps(SCHEMA_VERSION),)) conn.execute("INSERT OR IGNORE INTO agent_status(id) VALUES (1)") @contextmanager def transaction(conn: sqlite3.Connection): """BEGIN IMMEDIATE … COMMIT; при исключении — ROLLBACK. Нужна там, где читаем и пишем вместе (claim, отчёт).""" conn.execute("BEGIN IMMEDIATE") try: yield conn except BaseException: conn.execute("ROLLBACK") raise else: conn.execute("COMMIT")