Skip to main content

sqlite_graphrag/commands/
debug_schema.rs

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