pub const SCHEMA: &str = r#"
-- Issues table: primary work items
CREATE TABLE IF NOT EXISTS issues (
id TEXT PRIMARY KEY,
content_hash TEXT,
title TEXT NOT NULL CHECK(length(title) <= 500),
description TEXT,
design TEXT,
acceptance_criteria TEXT,
notes TEXT,
status TEXT NOT NULL DEFAULT 'open',
priority INTEGER NOT NULL DEFAULT 2 CHECK(priority >= 0 AND priority <= 4),
issue_type TEXT NOT NULL DEFAULT 'task',
assignee TEXT,
owner TEXT,
estimated_minutes INTEGER,
created_at TEXT NOT NULL,
created_by TEXT NOT NULL,
updated_at TEXT NOT NULL,
closed_at TEXT,
close_reason TEXT,
due_at TEXT,
defer_until TEXT,
external_ref TEXT,
source_system TEXT,
deleted_at TEXT,
deleted_by TEXT,
delete_reason TEXT,
compaction_level INTEGER,
compacted_at TEXT,
compacted_at_commit TEXT,
original_size INTEGER,
agent_state TEXT,
mol_type TEXT,
hook_bead TEXT,
role_bead TEXT,
rig TEXT,
last_activity TEXT,
pinned INTEGER NOT NULL DEFAULT 0,
is_template INTEGER NOT NULL DEFAULT 0,
ephemeral INTEGER NOT NULL DEFAULT 0
);
-- Dependencies table: relationships between issues
CREATE TABLE IF NOT EXISTS dependencies (
issue_id TEXT NOT NULL,
depends_on_id TEXT NOT NULL,
type TEXT NOT NULL DEFAULT 'blocks',
created_at TEXT NOT NULL,
created_by TEXT,
metadata TEXT,
thread_id TEXT,
PRIMARY KEY (issue_id, depends_on_id),
FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE,
FOREIGN KEY (depends_on_id) REFERENCES issues(id) ON DELETE CASCADE
);
-- Labels table: tag associations
CREATE TABLE IF NOT EXISTS labels (
issue_id TEXT NOT NULL,
label TEXT NOT NULL,
PRIMARY KEY (issue_id, label),
FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE
);
-- Comments table: discussion threads
CREATE TABLE IF NOT EXISTS comments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
issue_id TEXT NOT NULL,
author TEXT NOT NULL,
text TEXT NOT NULL,
created_at TEXT NOT NULL,
FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE
);
-- Events table: audit trail
CREATE TABLE IF NOT EXISTS events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
issue_id TEXT NOT NULL,
event_type TEXT NOT NULL,
actor TEXT NOT NULL,
old_value TEXT,
new_value TEXT,
comment TEXT,
created_at TEXT NOT NULL,
FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE
);
-- Config table: system settings
CREATE TABLE IF NOT EXISTS config (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
-- Dirty issues table: incremental export tracking
CREATE TABLE IF NOT EXISTS dirty_issues (
issue_id TEXT PRIMARY KEY,
marked_at TEXT NOT NULL,
FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE
);
-- Child counters table: hierarchical ID generation
CREATE TABLE IF NOT EXISTS child_counters (
parent_id TEXT PRIMARY KEY,
counter INTEGER NOT NULL DEFAULT 0
);
-- Indexes for common queries
CREATE INDEX IF NOT EXISTS idx_issues_status ON issues(status);
CREATE INDEX IF NOT EXISTS idx_issues_priority ON issues(priority);
CREATE INDEX IF NOT EXISTS idx_issues_assignee ON issues(assignee);
CREATE INDEX IF NOT EXISTS idx_issues_created_at ON issues(created_at);
CREATE INDEX IF NOT EXISTS idx_issues_updated_at ON issues(updated_at);
CREATE INDEX IF NOT EXISTS idx_issues_type ON issues(issue_type);
CREATE INDEX IF NOT EXISTS idx_issues_external_ref ON issues(external_ref);
CREATE INDEX IF NOT EXISTS idx_issues_content_hash ON issues(content_hash);
CREATE INDEX IF NOT EXISTS idx_dependencies_issue_id ON dependencies(issue_id);
CREATE INDEX IF NOT EXISTS idx_dependencies_depends_on ON dependencies(depends_on_id);
CREATE INDEX IF NOT EXISTS idx_dependencies_type ON dependencies(type);
CREATE INDEX IF NOT EXISTS idx_labels_label ON labels(label);
CREATE INDEX IF NOT EXISTS idx_comments_issue_id ON comments(issue_id);
CREATE INDEX IF NOT EXISTS idx_events_issue_id ON events(issue_id);
CREATE INDEX IF NOT EXISTS idx_events_created_at ON events(created_at);
CREATE INDEX IF NOT EXISTS idx_dirty_marked_at ON dirty_issues(marked_at);
"#;
pub const SCHEMA_VERSION: i32 = 1;
pub fn get_schema_version(conn: &rusqlite::Connection) -> rusqlite::Result<i32> {
match conn.query_row(
"SELECT value FROM config WHERE key = 'schema_version'",
[],
|row| {
let value: String = row.get(0)?;
Ok(value.parse().unwrap_or(0))
},
) {
Ok(version) => Ok(version),
Err(_) => Ok(0),
}
}
pub fn set_schema_version(conn: &rusqlite::Connection, version: i32) -> rusqlite::Result<()> {
conn.execute(
"INSERT OR REPLACE INTO config (key, value) VALUES ('schema_version', ?)",
[version.to_string()],
)?;
Ok(())
}