Skip to main content

sqlcx_core/
ir.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4// ── Enums ────────────────────────────────────────────────────────────────────
5
6#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
7#[serde(rename_all = "lowercase")]
8pub enum SqlTypeCategory {
9    String,
10    Number,
11    Boolean,
12    Date,
13    Json,
14    Uuid,
15    #[serde(rename = "binary")]
16    Binary,
17    Enum,
18    Unknown,
19}
20
21#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
22#[serde(rename_all = "lowercase")]
23pub enum QueryCommand {
24    One,
25    Many,
26    Exec,
27    #[serde(rename = "execresult")]
28    ExecResult,
29}
30
31#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
32#[serde(tag = "kind", rename_all = "lowercase")]
33pub enum JsonShape {
34    String,
35    Number,
36    Boolean,
37    Object {
38        fields: HashMap<std::string::String, JsonShape>,
39    },
40    Array {
41        element: Box<JsonShape>,
42    },
43    Nullable {
44        inner: Box<JsonShape>,
45    },
46}
47
48// ── Structs ───────────────────────────────────────────────────────────────────
49
50#[derive(Serialize, Deserialize, Clone, Debug)]
51#[serde(rename_all = "camelCase")]
52pub struct SqlType {
53    pub raw: String,
54    pub normalized: String,
55    pub category: SqlTypeCategory,
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub element_type: Option<Box<SqlType>>,
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub enum_name: Option<String>,
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub enum_values: Option<Vec<String>>,
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub json_shape: Option<JsonShape>,
64}
65
66#[derive(Serialize, Deserialize, Clone, Debug)]
67#[serde(rename_all = "camelCase")]
68pub struct ColumnDef {
69    pub name: String,
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub alias: Option<String>,
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub source_table: Option<String>,
74    #[serde(rename = "type")]
75    pub sql_type: SqlType,
76    pub nullable: bool,
77    pub has_default: bool,
78}
79
80#[derive(Serialize, Deserialize, Clone, Debug)]
81#[serde(rename_all = "camelCase")]
82pub struct TableDef {
83    pub name: String,
84    pub columns: Vec<ColumnDef>,
85    pub primary_key: Vec<String>,
86    pub unique_constraints: Vec<Vec<String>>,
87}
88
89#[derive(Serialize, Deserialize, Clone, Debug)]
90#[serde(rename_all = "camelCase")]
91pub struct ParamDef {
92    pub index: u32,
93    pub name: String,
94    #[serde(rename = "type")]
95    pub sql_type: SqlType,
96}
97
98#[derive(Serialize, Deserialize, Clone, Debug)]
99#[serde(rename_all = "camelCase")]
100pub struct QueryDef {
101    pub name: String,
102    pub command: QueryCommand,
103    pub sql: String,
104    pub params: Vec<ParamDef>,
105    pub returns: Vec<ColumnDef>,
106    pub source_file: String,
107}
108
109#[derive(Serialize, Deserialize, Clone, Debug)]
110#[serde(rename_all = "camelCase")]
111pub struct EnumDef {
112    pub name: String,
113    pub values: Vec<String>,
114}
115
116#[derive(Serialize, Deserialize, Clone, Debug)]
117#[serde(rename_all = "camelCase")]
118pub struct SqlcxIR {
119    pub tables: Vec<TableDef>,
120    pub queries: Vec<QueryDef>,
121    pub enums: Vec<EnumDef>,
122}
123
124pub type Overrides = HashMap<String, String>;
125
126// ── Tests ─────────────────────────────────────────────────────────────────────
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use serde_json::Value;
132
133    fn make_sql_type(raw: &str, category: SqlTypeCategory) -> SqlType {
134        SqlType {
135            raw: raw.to_string(),
136            normalized: raw.to_lowercase(),
137            category,
138            element_type: None,
139            enum_name: None,
140            enum_values: None,
141            json_shape: None,
142        }
143    }
144
145    fn make_column(name: &str, has_default: bool) -> ColumnDef {
146        ColumnDef {
147            name: name.to_string(),
148            alias: None,
149            source_table: None,
150            sql_type: make_sql_type("text", SqlTypeCategory::String),
151            nullable: false,
152            has_default,
153        }
154    }
155
156    #[test]
157    fn ir_round_trip_json() {
158        let ir = SqlcxIR {
159            tables: vec![TableDef {
160                name: "users".to_string(),
161                columns: vec![make_column("id", false), make_column("email", false)],
162                primary_key: vec!["id".to_string()],
163                unique_constraints: vec![vec!["email".to_string()]],
164            }],
165            queries: vec![],
166            enums: vec![],
167        };
168
169        let json = serde_json::to_string(&ir).expect("serialize");
170        let restored: SqlcxIR = serde_json::from_str(&json).expect("deserialize");
171
172        assert_eq!(restored.tables.len(), 1);
173        assert_eq!(restored.tables[0].name, "users");
174        assert_eq!(restored.tables[0].columns.len(), 2);
175        assert_eq!(restored.tables[0].columns[0].name, "id");
176        assert_eq!(restored.tables[0].primary_key, vec!["id"]);
177        assert_eq!(
178            restored.tables[0].unique_constraints,
179            vec![vec!["email".to_string()]]
180        );
181    }
182
183    #[test]
184    fn sql_type_category_serializes_lowercase() {
185        let s = serde_json::to_string(&SqlTypeCategory::String).unwrap();
186        assert_eq!(s, r#""string""#);
187
188        let b = serde_json::to_string(&SqlTypeCategory::Binary).unwrap();
189        assert_eq!(b, r#""binary""#);
190
191        let n = serde_json::to_string(&SqlTypeCategory::Number).unwrap();
192        assert_eq!(n, r#""number""#);
193    }
194
195    #[test]
196    fn json_shape_serializes_with_kind_tag() {
197        let mut fields = HashMap::new();
198        fields.insert("foo".to_string(), JsonShape::String);
199
200        let shape = JsonShape::Object { fields };
201        let v: Value = serde_json::to_value(&shape).unwrap();
202
203        assert_eq!(v["kind"], "object");
204        assert!(v["fields"].is_object());
205        assert_eq!(v["fields"]["foo"]["kind"], "string");
206    }
207
208    #[test]
209    fn query_command_serializes_lowercase() {
210        let exec_result = serde_json::to_string(&QueryCommand::ExecResult).unwrap();
211        assert_eq!(exec_result, r#""execresult""#);
212
213        let one = serde_json::to_string(&QueryCommand::One).unwrap();
214        assert_eq!(one, r#""one""#);
215
216        let many = serde_json::to_string(&QueryCommand::Many).unwrap();
217        assert_eq!(many, r#""many""#);
218    }
219
220    #[test]
221    fn camel_case_json_keys() {
222        let table = TableDef {
223            name: "items".to_string(),
224            columns: vec![make_column("price", true)],
225            primary_key: vec!["id".to_string()],
226            unique_constraints: vec![],
227        };
228
229        let v: Value = serde_json::to_value(&table).unwrap();
230
231        // primaryKey not primary_key
232        assert!(v.get("primaryKey").is_some(), "expected 'primaryKey' key");
233        assert!(
234            v.get("primary_key").is_none(),
235            "unexpected 'primary_key' key"
236        );
237
238        // uniqueConstraints not unique_constraints
239        assert!(
240            v.get("uniqueConstraints").is_some(),
241            "expected 'uniqueConstraints' key"
242        );
243
244        // hasDefault not has_default
245        let col = &v["columns"][0];
246        assert!(col.get("hasDefault").is_some(), "expected 'hasDefault' key");
247        assert!(
248            col.get("has_default").is_none(),
249            "unexpected 'has_default' key"
250        );
251    }
252}