sqlite_graphrag/commands/
debug_schema.rs1use crate::errors::AppError;
4use crate::output;
5use crate::paths::AppPaths;
6use crate::storage::connection::open_ro;
7use serde::Serialize;
8use std::time::Instant;
9
10#[derive(clap::Args)]
11#[command(after_long_help = "EXAMPLES:\n \
12 # Dump the SQLite schema (tables, indices, triggers) as JSON\n \
13 sqlite-graphrag __debug_schema\n\n \
14 # Dump schema of a database at a custom path\n \
15 sqlite-graphrag __debug_schema --db /path/to/graphrag.sqlite\n\n \
16 # Persist the default path in the XDG config instead of passing --db\n \
17 sqlite-graphrag config set db.path /data/graphrag.sqlite")]
18pub struct DebugSchemaArgs {
20 #[arg(long, hide = true, help = "No-op; JSON is always emitted on stdout")]
22 pub json: bool,
23 #[arg(long)]
25 pub db: Option<String>,
26}
27
28#[derive(Serialize)]
29struct SchemaObject {
30 name: String,
31 #[serde(rename = "type")]
32 object_type: String,
33}
34
35#[derive(Serialize)]
36struct MigrationRecord {
37 version: i64,
38 name: String,
39 applied_on: Option<String>,
40}
41
42#[derive(Serialize)]
43struct DebugSchemaResponse {
44 schema_version: i64,
47 user_version: i64,
51 objects: Vec<SchemaObject>,
52 migrations: Vec<MigrationRecord>,
53 elapsed_ms: u64,
54}
55
56pub fn run(args: DebugSchemaArgs) -> Result<(), AppError> {
58 let inicio = Instant::now();
59 let paths = AppPaths::resolve(args.db.as_deref())?;
60
61 crate::storage::connection::ensure_db_ready(&paths)?;
62
63 let conn = open_ro(&paths.db)?;
64
65 let schema_version: i64 = conn
66 .query_row("PRAGMA schema_version", [], |r| r.get(0))
67 .unwrap_or(0);
68
69 let user_version: i64 = conn
71 .query_row("PRAGMA user_version", [], |r| r.get(0))
72 .unwrap_or(0);
73
74 let mut stmt = conn.prepare_cached(
75 "SELECT name, type FROM sqlite_master \
76 WHERE type IN ('table','view','trigger','index') \
77 ORDER BY type, name",
78 )?;
79 let objects: Vec<SchemaObject> = stmt
80 .query_map([], |r| {
81 Ok(SchemaObject {
82 name: r.get(0)?,
83 object_type: r.get(1)?,
84 })
85 })?
86 .collect::<Result<Vec<_>, _>>()?;
87
88 let existe_hist: i64 = conn
89 .query_row(
90 "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='refinery_schema_history'",
91 [],
92 |r| r.get(0),
93 )
94 .unwrap_or(0);
95
96 let migrations: Vec<MigrationRecord> = if existe_hist > 0 {
97 let mut stmt_mig = conn.prepare_cached(
98 "SELECT version, name, applied_on \
99 FROM refinery_schema_history \
100 ORDER BY version",
101 )?;
102 let rows: Vec<MigrationRecord> = stmt_mig
103 .query_map([], |r| {
104 Ok(MigrationRecord {
105 version: r.get(0)?,
106 name: r.get(1)?,
107 applied_on: r.get(2)?,
108 })
109 })?
110 .collect::<Result<Vec<_>, _>>()?;
111 rows
112 } else {
113 Vec::new()
114 };
115
116 let elapsed_ms = inicio.elapsed().as_millis() as u64;
117
118 output::emit_json(&DebugSchemaResponse {
119 schema_version,
120 user_version,
121 objects,
122 migrations,
123 elapsed_ms,
124 })?;
125
126 Ok(())
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 use serde_json::Value;
133
134 #[test]
135 fn debug_schema_response_serializes_required_fields() {
136 let resp = DebugSchemaResponse {
137 schema_version: 42,
138 user_version: 49,
139 objects: vec![SchemaObject {
140 name: "memories".to_string(),
141 object_type: "table".to_string(),
142 }],
143 migrations: vec![MigrationRecord {
144 version: 1,
145 name: "V001__init".to_string(),
146 applied_on: Some("2026-01-01T00:00:00Z".to_string()),
147 }],
148 elapsed_ms: 7,
149 };
150 let json: Value = serde_json::to_value(&resp).unwrap();
151 assert_eq!(json["schema_version"], 42);
152 assert_eq!(json["user_version"], 49);
153 assert!(json["objects"].is_array());
154 assert_eq!(json["objects"][0]["name"], "memories");
155 assert_eq!(json["objects"][0]["type"], "table");
156 assert!(json["migrations"].is_array());
157 assert_eq!(json["migrations"][0]["version"], 1);
158 assert_eq!(json["elapsed_ms"], 7);
159 }
160
161 #[test]
162 fn schema_object_renomeia_campo_type() {
163 let obj = SchemaObject {
164 name: "entities".to_string(),
165 object_type: "table".to_string(),
166 };
167 let json: Value = serde_json::to_value(&obj).unwrap();
168 assert!(json.get("object_type").is_none());
169 assert_eq!(json["type"], "table");
170 }
171
172 #[test]
173 fn migration_record_serializes_all_fields() {
174 let rec = MigrationRecord {
175 version: 3,
176 name: "V003__indexes".to_string(),
177 applied_on: Some("2026-04-19T12:00:00Z".to_string()),
178 };
179 let json: Value = serde_json::to_value(&rec).unwrap();
180 assert_eq!(json["version"], 3);
181 assert_eq!(json["name"], "V003__indexes");
182 assert_eq!(json["applied_on"], "2026-04-19T12:00:00Z");
183 }
184}