1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4#[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#[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#[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 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 assert!(
240 v.get("uniqueConstraints").is_some(),
241 "expected 'uniqueConstraints' key"
242 );
243
244 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}