Skip to main content

polyglot_sql/
ast_json.rs

1//! Compatibility helpers for Expression JSON accepted at public API boundaries.
2//!
3//! The canonical AST representation is the serde JSON form of [`Expression`].
4//! Some wrappers historically exposed JavaScript values that stringify unit
5//! variants such as `Expression::Null` as `{}`. These helpers repair that
6//! narrow compatibility shape while preserving errors for unknown AST forms.
7
8use crate::expressions::Expression;
9use serde_json::{json, Value};
10
11/// Deserialize a single [`Expression`] from JSON, accepting known compatibility
12/// shapes used by SDK boundaries.
13pub fn expression_from_str(input: &str) -> Result<Expression, String> {
14    let value: Value = serde_json::from_str(input).map_err(|error| error.to_string())?;
15    expression_from_value(value)
16}
17
18/// Deserialize a list of [`Expression`] values from JSON, accepting known
19/// compatibility shapes used by SDK boundaries.
20pub fn expressions_from_str(input: &str) -> Result<Vec<Expression>, String> {
21    let value: Value = serde_json::from_str(input).map_err(|error| error.to_string())?;
22    expressions_from_value(value)
23}
24
25/// Deserialize a single [`Expression`] from a JSON value.
26pub fn expression_from_value(value: Value) -> Result<Expression, String> {
27    match serde_json::from_value::<Expression>(value.clone()) {
28        Ok(expression) => Ok(expression),
29        Err(original_error) => {
30            let mut repaired = value;
31            normalize_expression_value(&mut repaired);
32            serde_json::from_value::<Expression>(repaired)
33                .map_err(|error| format!("{error}; original error: {original_error}"))
34        }
35    }
36}
37
38/// Deserialize a list of [`Expression`] values from a JSON value.
39pub fn expressions_from_value(value: Value) -> Result<Vec<Expression>, String> {
40    match serde_json::from_value::<Vec<Expression>>(value.clone()) {
41        Ok(expressions) => Ok(expressions),
42        Err(original_error) => {
43            let mut repaired = value;
44            if let Value::Array(items) = &mut repaired {
45                for item in items {
46                    normalize_expression_value(item);
47                }
48            }
49            serde_json::from_value::<Vec<Expression>>(repaired)
50                .map_err(|error| format!("{error}; original error: {original_error}"))
51        }
52    }
53}
54
55/// Serialize an [`Expression`] through serde JSON first so wrapper layers expose
56/// canonical JSON-compatible values instead of runtime-specific JS shapes.
57pub fn expression_to_value(expression: &Expression) -> Result<Value, String> {
58    serde_json::to_value(expression).map_err(|error| error.to_string())
59}
60
61fn normalize_expression_value(value: &mut Value) {
62    match value {
63        Value::Object(map) if map.is_empty() => {
64            *value = json!({ "null": null });
65        }
66        Value::Object(map) if map.len() == 1 => {
67            let (kind, payload) = map.iter_mut().next().expect("single entry exists");
68            if kind == "null" && matches!(payload, Value::Object(inner) if inner.is_empty()) {
69                *payload = Value::Null;
70                return;
71            }
72
73            if kind == "is_null" {
74                normalize_is_null_payload(payload);
75            }
76
77            match payload {
78                Value::Object(payload_map) => normalize_struct_fields(payload_map),
79                Value::Array(items) => {
80                    for item in items {
81                        normalize_expression_value(item);
82                    }
83                }
84                _ => {}
85            }
86        }
87        Value::Object(map) => normalize_struct_fields(map),
88        Value::Array(items) => {
89            for item in items {
90                normalize_expression_value(item);
91            }
92        }
93        _ => {}
94    }
95}
96
97fn normalize_is_null_payload(payload: &mut Value) {
98    let Value::Array(items) = payload else {
99        return;
100    };
101    if items.len() != 1 {
102        return;
103    }
104
105    let mut this = items.remove(0);
106    normalize_expression_value(&mut this);
107    *payload = json!({
108        "this": this,
109        "not": false,
110        "postfix_form": false
111    });
112}
113
114fn normalize_struct_fields(map: &mut serde_json::Map<String, Value>) {
115    for (key, value) in map.iter_mut() {
116        if is_expression_array_field(key) {
117            if let Value::Array(items) = value {
118                for item in items {
119                    normalize_expression_value(item);
120                }
121                continue;
122            }
123        }
124
125        if is_expression_field(key) {
126            normalize_expression_value(value);
127        } else {
128            normalize_container_value(value);
129        }
130    }
131}
132
133fn normalize_container_value(value: &mut Value) {
134    match value {
135        Value::Object(map) => normalize_struct_fields(map),
136        Value::Array(items) => {
137            for item in items {
138                normalize_container_value(item);
139            }
140        }
141        _ => {}
142    }
143}
144
145fn is_expression_field(key: &str) -> bool {
146    matches!(
147        key,
148        "this"
149            | "left"
150            | "right"
151            | "low"
152            | "high"
153            | "expression"
154            | "condition"
155            | "query"
156            | "unnest"
157            | "on"
158            | "prewhere"
159            | "where"
160            | "where_clause"
161            | "having"
162            | "qualify"
163            | "source"
164            | "body"
165            | "default"
166            | "true_value"
167            | "false_value"
168            | "else_result"
169            | "target"
170            | "format"
171            | "limit"
172            | "offset"
173    )
174}
175
176fn is_expression_array_field(key: &str) -> bool {
177    matches!(
178        key,
179        "expressions"
180            | "args"
181            | "columns"
182            | "values"
183            | "partition_by"
184            | "order_by"
185            | "group_by"
186            | "distinct_on"
187            | "limit_by"
188            | "settings"
189            | "for_xml"
190            | "for_json"
191            | "exclude"
192            | "hints"
193            | "on_columns"
194    )
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200    use crate::expressions::Expression;
201
202    fn column_json(name: &str) -> Value {
203        json!({
204            "column": {
205                "name": {
206                    "name": name,
207                    "quoted": false,
208                    "trailing_comments": [],
209                    "span": null
210                },
211                "table": null,
212                "join_mark": false,
213                "trailing_comments": [],
214                "span": null,
215                "inferred_type": null
216            }
217        })
218    }
219
220    #[test]
221    fn expression_from_value_accepts_legacy_empty_object_as_null() {
222        assert!(matches!(
223            expression_from_value(json!({})).expect("empty object should become NULL"),
224            Expression::Null(_)
225        ));
226    }
227
228    #[test]
229    fn expression_from_value_accepts_empty_null_payload() {
230        assert!(matches!(
231            expression_from_value(json!({ "null": {} })).expect("empty null payload should work"),
232            Expression::Null(_)
233        ));
234    }
235
236    #[test]
237    fn expression_from_value_repairs_nested_null_expression_fields() {
238        let expression = expression_from_value(json!({
239            "between": {
240                "this": column_json("created_at"),
241                "low": { "literal": { "literal_type": "string", "value": "2024-01-01" } },
242                "high": {},
243                "not": false,
244                "symmetric": null
245            }
246        }))
247        .expect("between high bound should be repaired");
248
249        match expression {
250            Expression::Between(between) => assert!(matches!(between.high, Expression::Null(_))),
251            other => panic!("expected between expression, got {other:?}"),
252        }
253    }
254
255    #[test]
256    fn expression_from_value_accepts_is_null_array_shorthand() {
257        let expression = expression_from_value(json!({
258            "is_null": [column_json("deleted_at")]
259        }))
260        .expect("is_null shorthand should be repaired");
261
262        match expression {
263            Expression::IsNull(is_null) => {
264                assert!(!is_null.not);
265                assert!(!is_null.postfix_form);
266                assert!(matches!(is_null.this, Expression::Column(_)));
267            }
268            other => panic!("expected is_null expression, got {other:?}"),
269        }
270    }
271
272    #[test]
273    fn expression_from_value_keeps_unknown_ast_as_error() {
274        let error = expression_from_value(json!({ "not_a_real_expression": {} }))
275            .expect_err("unknown expression should stay invalid");
276        assert!(error.contains("not_a_real_expression"));
277    }
278}