Skip to main content

sqlite_graphrag/commands/
debug_schema.rs

1//! Handler for the `debug-schema` CLI subcommand.
2
3use 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")]
18/// Debug schema args.
19pub struct DebugSchemaArgs {
20    /// Emit machine-readable JSON on stdout.
21    #[arg(long, hide = true, help = "No-op; JSON is always emitted on stdout")]
22    pub json: bool,
23    /// Path to the SQLite database file.
24    #[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    /// Internal SQLite counter incremented on each DDL (PRAGMA schema_version).
45    /// Distinct from `user_version`: this one is managed automatically by SQLite.
46    schema_version: i64,
47    /// Canonical SCHEMA_USER_VERSION value set explicitly by migrations
48    /// (PRAGMA user_version). Distinct from `schema_version` (SQLite DDL counter)
49    /// and from `health.schema_version` (MAX version in refinery_schema_history).
50    user_version: i64,
51    objects: Vec<SchemaObject>,
52    migrations: Vec<MigrationRecord>,
53    elapsed_ms: u64,
54}
55
56/// Run.
57pub 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    // PRAGMA user_version is set explicitly after migrations (canonical value SCHEMA_USER_VERSION).
70    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}