1use crate::error::Result;
2use rusqlite::Connection;
3
4pub const SCHEMA_VERSION: &str = "4";
5
6pub fn bootstrap(conn: &Connection) -> Result<()> {
7 conn.execute_batch(
8 "
9 PRAGMA journal_mode=WAL;
10 PRAGMA foreign_keys=ON;
11 PRAGMA synchronous=NORMAL;
12
13 CREATE TABLE IF NOT EXISTS _adb_meta (
14 key TEXT PRIMARY KEY,
15 value TEXT NOT NULL
16 );
17
18 CREATE TABLE IF NOT EXISTS _adb_collections (
19 id TEXT PRIMARY KEY,
20 name TEXT UNIQUE NOT NULL,
21 dim INTEGER NOT NULL,
22 metric TEXT NOT NULL DEFAULT 'cosine',
23 count INTEGER NOT NULL DEFAULT 0,
24 created_at INTEGER NOT NULL
25 );
26
27 CREATE TABLE IF NOT EXISTS _adb_vectors (
28 id TEXT NOT NULL,
29 collection_id TEXT NOT NULL,
30 vector BLOB NOT NULL,
31 metadata TEXT,
32 created_at INTEGER NOT NULL,
33 updated_at INTEGER NOT NULL DEFAULT 0,
34 PRIMARY KEY (id, collection_id),
35 FOREIGN KEY (collection_id)
36 REFERENCES _adb_collections(id) ON DELETE CASCADE
37 );
38
39 CREATE TABLE IF NOT EXISTS _adb_hnsw_index (
40 collection_id TEXT PRIMARY KEY,
41 index_blob BLOB NOT NULL,
42 built_at INTEGER NOT NULL,
43 is_dirty INTEGER NOT NULL DEFAULT 0
44 );
45
46 CREATE TABLE IF NOT EXISTS _adb_nodes (
47 id TEXT PRIMARY KEY,
48 kind TEXT NOT NULL,
49 data TEXT,
50 created_at INTEGER NOT NULL,
51 updated_at INTEGER NOT NULL
52 );
53
54 CREATE TABLE IF NOT EXISTS _adb_edges (
55 src TEXT NOT NULL,
56 dst TEXT NOT NULL,
57 relation TEXT NOT NULL,
58 weight REAL NOT NULL DEFAULT 1.0,
59 created_at INTEGER NOT NULL,
60 PRIMARY KEY (src, dst, relation),
61 FOREIGN KEY (src) REFERENCES _adb_nodes(id) ON DELETE CASCADE,
62 FOREIGN KEY (dst) REFERENCES _adb_nodes(id) ON DELETE CASCADE
63 );
64
65 CREATE INDEX IF NOT EXISTS idx_edges_src ON _adb_edges(src);
66 CREATE INDEX IF NOT EXISTS idx_edges_dst ON _adb_edges(dst);
67 CREATE INDEX IF NOT EXISTS idx_vectors_col ON _adb_vectors(collection_id);
68 CREATE INDEX IF NOT EXISTS idx_nodes_kind ON _adb_nodes(kind);
69
70 -- Conversations / message threading
71 CREATE TABLE IF NOT EXISTS _adb_conversations (
72 id TEXT PRIMARY KEY,
73 title TEXT,
74 metadata TEXT,
75 created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
76 updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))
77 );
78
79 CREATE TABLE IF NOT EXISTS _adb_messages (
80 id TEXT PRIMARY KEY,
81 conversation_id TEXT NOT NULL REFERENCES _adb_conversations(id) ON DELETE CASCADE,
82 role TEXT NOT NULL,
83 content TEXT NOT NULL,
84 metadata TEXT,
85 created_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))
86 );
87 CREATE INDEX IF NOT EXISTS idx_messages_conv ON _adb_messages(conversation_id, created_at);
88
89 -- Workflow persistence
90 CREATE TABLE IF NOT EXISTS _adb_workflows (
91 id TEXT PRIMARY KEY,
92 name TEXT NOT NULL,
93 status TEXT NOT NULL DEFAULT 'pending',
94 input TEXT,
95 output TEXT,
96 error TEXT,
97 metadata TEXT,
98 created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
99 updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))
100 );
101
102 CREATE TABLE IF NOT EXISTS _adb_workflow_steps (
103 id TEXT PRIMARY KEY,
104 workflow_id TEXT NOT NULL REFERENCES _adb_workflows(id) ON DELETE CASCADE,
105 step_index INTEGER NOT NULL,
106 name TEXT NOT NULL,
107 status TEXT NOT NULL DEFAULT 'pending',
108 input TEXT,
109 output TEXT,
110 error TEXT,
111 started_at INTEGER,
112 completed_at INTEGER
113 );
114 CREATE INDEX IF NOT EXISTS idx_wf_steps ON _adb_workflow_steps(workflow_id, step_index);
115
116 -- Reasoning traces
117 CREATE TABLE IF NOT EXISTS _adb_traces (
118 id TEXT PRIMARY KEY,
119 session_id TEXT,
120 parent_id TEXT REFERENCES _adb_traces(id),
121 trace_type TEXT NOT NULL,
122 content TEXT NOT NULL,
123 metadata TEXT,
124 created_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))
125 );
126 CREATE INDEX IF NOT EXISTS idx_traces_session ON _adb_traces(session_id, created_at);
127 CREATE INDEX IF NOT EXISTS idx_traces_parent ON _adb_traces(parent_id);
128
129 -- Message full-text search (schema v4)
130 CREATE VIRTUAL TABLE IF NOT EXISTS _adb_messages_fts
131 USING fts5(
132 message_id UNINDEXED,
133 conversation_id UNINDEXED,
134 content,
135 tokenize='porter ascii'
136 );
137 ",
138 )?;
139
140 conn.execute(
141 "INSERT OR IGNORE INTO _adb_meta (key, value) VALUES ('schema_version', ?1)",
142 rusqlite::params![SCHEMA_VERSION],
143 )?;
144 conn.execute(
145 "INSERT OR IGNORE INTO _adb_meta (key, value) VALUES ('created_at', ?1)",
146 rusqlite::params![now_ms()],
147 )?;
148 Ok(())
149}
150
151pub fn check_version(conn: &Connection) -> Result<()> {
152 let version: Option<String> = conn
153 .query_row(
154 "SELECT value FROM _adb_meta WHERE key = 'schema_version'",
155 [],
156 |row| row.get(0),
157 )
158 .ok();
159 match version.as_deref() {
160 Some(v) if v == SCHEMA_VERSION => Ok(()),
161 Some(_) => Err(crate::error::AgentDbError::SchemaMigration),
164 None => Err(crate::error::AgentDbError::SchemaMigration),
166 }
167}
168
169pub fn migrate(conn: &Connection) -> Result<()> {
179 bootstrap(conn)?;
181
182 let _ = conn.execute_batch(
184 "ALTER TABLE _adb_workflows ADD COLUMN error TEXT;"
185 );
186
187 let _ = conn.execute_batch(
189 "ALTER TABLE _adb_vectors ADD COLUMN updated_at INTEGER NOT NULL DEFAULT 0;"
190 );
191
192 conn.execute(
197 "INSERT INTO _adb_meta (key, value) VALUES ('schema_version', ?1)
198 ON CONFLICT(key) DO UPDATE SET value = excluded.value",
199 rusqlite::params![SCHEMA_VERSION],
200 )?;
201 Ok(())
202}
203
204pub fn now_ms() -> i64 {
205 std::time::SystemTime::now()
206 .duration_since(std::time::UNIX_EPOCH)
207 .unwrap_or_default()
208 .as_millis() as i64
209}