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::i18n::errors_msg;
5use crate::output;
6use crate::paths::AppPaths;
7use crate::storage::connection::open_ro;
8use serde::Serialize;
9use std::time::Instant;
10
11#[derive(clap::Args)]
12pub struct DebugSchemaArgs {
13    #[arg(long, hide = true, help = "No-op; JSON is always emitted on stdout")]
14    pub json: bool,
15    #[arg(long, env = "SQLITE_GRAPHRAG_DB_PATH")]
16    pub db: Option<String>,
17}
18
19#[derive(Serialize)]
20struct SchemaObject {
21    name: String,
22    #[serde(rename = "type")]
23    object_type: String,
24}
25
26#[derive(Serialize)]
27struct MigrationRecord {
28    version: i64,
29    name: String,
30    applied_on: String,
31}
32
33#[derive(Serialize)]
34struct DebugSchemaResponse {
35    /// Internal SQLite counter incremented on each DDL (PRAGMA schema_version).
36    /// Distinct from `user_version`: this one is managed automatically by SQLite.
37    schema_version: i64,
38    /// Canonical SCHEMA_USER_VERSION value set explicitly by migrations
39    /// (PRAGMA user_version). Distinct from `schema_version` (SQLite DDL counter)
40    /// and from `health.schema_version` (MAX version in refinery_schema_history).
41    user_version: i64,
42    objects: Vec<SchemaObject>,
43    migrations: Vec<MigrationRecord>,
44    elapsed_ms: u64,
45}
46
47pub fn run(args: DebugSchemaArgs) -> Result<(), AppError> {
48    let inicio = Instant::now();
49    let paths = AppPaths::resolve(args.db.as_deref())?;
50
51    if !paths.db.exists() {
52        return Err(AppError::NotFound(errors_msg::database_not_found(
53            &paths.db.display().to_string(),
54        )));
55    }
56
57    let conn = open_ro(&paths.db)?;
58
59    let schema_version: i64 = conn
60        .query_row("PRAGMA schema_version", [], |r| r.get(0))
61        .unwrap_or(0);
62
63    // PRAGMA user_version é setado explicitamente após migrações (valor canônico SCHEMA_USER_VERSION).
64    let user_version: i64 = conn
65        .query_row("PRAGMA user_version", [], |r| r.get(0))
66        .unwrap_or(0);
67
68    let mut stmt = conn.prepare(
69        "SELECT name, type FROM sqlite_master \
70         WHERE type IN ('table','view','trigger','index') \
71         ORDER BY type, name",
72    )?;
73    let objects: Vec<SchemaObject> = stmt
74        .query_map([], |r| {
75            Ok(SchemaObject {
76                name: r.get(0)?,
77                object_type: r.get(1)?,
78            })
79        })?
80        .collect::<Result<Vec<_>, _>>()?;
81
82    let existe_hist: i64 = conn
83        .query_row(
84            "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='refinery_schema_history'",
85            [],
86            |r| r.get(0),
87        )
88        .unwrap_or(0);
89
90    let migrations: Vec<MigrationRecord> = if existe_hist > 0 {
91        let mut stmt_mig = conn.prepare(
92            "SELECT version, name, applied_on \
93             FROM refinery_schema_history \
94             ORDER BY version",
95        )?;
96        let rows: Vec<MigrationRecord> = stmt_mig
97            .query_map([], |r| {
98                Ok(MigrationRecord {
99                    version: r.get(0)?,
100                    name: r.get(1)?,
101                    applied_on: r.get(2)?,
102                })
103            })?
104            .collect::<Result<Vec<_>, _>>()?;
105        rows
106    } else {
107        Vec::new()
108    };
109
110    let elapsed_ms = inicio.elapsed().as_millis() as u64;
111
112    output::emit_json(&DebugSchemaResponse {
113        schema_version,
114        user_version,
115        objects,
116        migrations,
117        elapsed_ms,
118    })?;
119
120    Ok(())
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126    use serde_json::Value;
127
128    #[test]
129    fn debug_schema_response_serializa_campos_obrigatorios() {
130        let resp = DebugSchemaResponse {
131            schema_version: 42,
132            user_version: 49,
133            objects: vec![SchemaObject {
134                name: "memories".to_string(),
135                object_type: "table".to_string(),
136            }],
137            migrations: vec![MigrationRecord {
138                version: 1,
139                name: "V001__init".to_string(),
140                applied_on: "2026-01-01T00:00:00Z".to_string(),
141            }],
142            elapsed_ms: 7,
143        };
144        let json: Value = serde_json::to_value(&resp).unwrap();
145        assert_eq!(json["schema_version"], 42);
146        assert_eq!(json["user_version"], 49);
147        assert!(json["objects"].is_array());
148        assert_eq!(json["objects"][0]["name"], "memories");
149        assert_eq!(json["objects"][0]["type"], "table");
150        assert!(json["migrations"].is_array());
151        assert_eq!(json["migrations"][0]["version"], 1);
152        assert_eq!(json["elapsed_ms"], 7);
153    }
154
155    #[test]
156    fn schema_object_renomeia_campo_type() {
157        let obj = SchemaObject {
158            name: "entities".to_string(),
159            object_type: "table".to_string(),
160        };
161        let json: Value = serde_json::to_value(&obj).unwrap();
162        assert!(json.get("object_type").is_none());
163        assert_eq!(json["type"], "table");
164    }
165
166    #[test]
167    fn migration_record_serializa_todos_campos() {
168        let rec = MigrationRecord {
169            version: 3,
170            name: "V003__indexes".to_string(),
171            applied_on: "2026-04-19T12:00:00Z".to_string(),
172        };
173        let json: Value = serde_json::to_value(&rec).unwrap();
174        assert_eq!(json["version"], 3);
175        assert_eq!(json["name"], "V003__indexes");
176        assert_eq!(json["applied_on"], "2026-04-19T12:00:00Z");
177    }
178}