Skip to main content

nodedb_sql/parser/array_stmt/
parse.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Recursive-descent parser for the four `ARRAY` statements.
4//!
5//! `try_parse_array_statement` returns `Ok(None)` for any SQL that does
6//! not begin with one of the array prefixes; this lets the caller fall
7//! through to the standard sqlparser path. When the prefix matches, the
8//! parser commits — any further error is surfaced as `SqlError::Parse`.
9
10#[path = "parse_impl.rs"]
11mod parse_impl;
12
13use super::ast::ArrayStatement;
14use super::lexer::tokenize;
15use crate::error::Result;
16use parse_impl::Parser;
17
18/// Top-level entry. Returns `Ok(None)` if the SQL doesn't start with an
19/// array statement keyword sequence.
20pub fn try_parse_array_statement(sql: &str) -> Result<Option<ArrayStatement>> {
21    let trimmed = sql.trim_start();
22    let upper: String = trimmed
23        .chars()
24        .take(40)
25        .collect::<String>()
26        .to_ascii_uppercase();
27
28    if upper.starts_with("CREATE ARRAY ") || upper == "CREATE ARRAY" {
29        let toks = tokenize(trimmed)?;
30        let mut p = Parser::new(&toks);
31        p.expect_kw("CREATE")?;
32        p.expect_kw("ARRAY")?;
33        return Ok(Some(ArrayStatement::Create(p.parse_create()?)));
34    }
35    if upper.starts_with("DROP ARRAY ") || upper == "DROP ARRAY" {
36        let toks = tokenize(trimmed)?;
37        let mut p = Parser::new(&toks);
38        p.expect_kw("DROP")?;
39        p.expect_kw("ARRAY")?;
40        return Ok(Some(ArrayStatement::Drop(p.parse_drop()?)));
41    }
42    if upper.starts_with("INSERT INTO ARRAY ") {
43        let toks = tokenize(trimmed)?;
44        let mut p = Parser::new(&toks);
45        p.expect_kw("INSERT")?;
46        p.expect_kw("INTO")?;
47        p.expect_kw("ARRAY")?;
48        return Ok(Some(ArrayStatement::Insert(p.parse_insert()?)));
49    }
50    if upper.starts_with("DELETE FROM ARRAY ") {
51        let toks = tokenize(trimmed)?;
52        let mut p = Parser::new(&toks);
53        p.expect_kw("DELETE")?;
54        p.expect_kw("FROM")?;
55        p.expect_kw("ARRAY")?;
56        return Ok(Some(ArrayStatement::Delete(p.parse_delete()?)));
57    }
58    if upper.starts_with("ALTER ARRAY ") || upper == "ALTER ARRAY" {
59        let toks = tokenize(trimmed)?;
60        let mut p = Parser::new(&toks);
61        p.expect_kw("ALTER")?;
62        p.expect_kw("ARRAY")?;
63        return Ok(Some(ArrayStatement::Alter(p.parse_alter()?)));
64    }
65    Ok(None)
66}
67
68#[cfg(test)]
69mod tests {
70    use super::super::ast::ArrayStatement;
71    use super::*;
72    use crate::types_array::ArrayCellOrderAst;
73
74    #[test]
75    fn passthrough_non_array_sql() {
76        assert!(
77            try_parse_array_statement("SELECT * FROM t")
78                .unwrap()
79                .is_none()
80        );
81        assert!(
82            try_parse_array_statement("CREATE TABLE t (x INT)")
83                .unwrap()
84                .is_none()
85        );
86        assert!(
87            try_parse_array_statement("INSERT INTO foo VALUES (1)")
88                .unwrap()
89                .is_none()
90        );
91    }
92
93    #[test]
94    fn parse_create_array_full() {
95        let sql = "CREATE ARRAY genome \
96                   DIMS (chrom INT64 [1..23], pos INT64 [0..300000000]) \
97                   ATTRS (variant STRING, qual FLOAT64) \
98                   TILE_EXTENTS (1, 1000000) \
99                   CELL_ORDER HILBERT";
100        let stmt = try_parse_array_statement(sql).unwrap().unwrap();
101        match stmt {
102            ArrayStatement::Create(c) => {
103                assert_eq!(c.name, "genome");
104                assert_eq!(c.dims.len(), 2);
105                assert_eq!(c.attrs.len(), 2);
106                assert_eq!(c.tile_extents, vec![1, 1_000_000]);
107                assert_eq!(c.cell_order, ArrayCellOrderAst::Hilbert);
108            }
109            _ => panic!("wrong variant"),
110        }
111    }
112
113    #[test]
114    fn parse_drop_array_if_exists() {
115        let stmt = try_parse_array_statement("DROP ARRAY IF EXISTS g")
116            .unwrap()
117            .unwrap();
118        match stmt {
119            ArrayStatement::Drop(d) => {
120                assert!(d.if_exists);
121                assert_eq!(d.name, "g");
122            }
123            _ => panic!("wrong variant"),
124        }
125    }
126
127    #[test]
128    fn parse_insert_multi_row() {
129        let sql = "INSERT INTO ARRAY g \
130                   COORDS (1, 100) VALUES ('SNP', 99.5), \
131                   COORDS (1, 200) VALUES ('INS', 88.0)";
132        let stmt = try_parse_array_statement(sql).unwrap().unwrap();
133        match stmt {
134            ArrayStatement::Insert(i) => {
135                assert_eq!(i.name, "g");
136                assert_eq!(i.rows.len(), 2);
137                assert_eq!(i.rows[0].coords.len(), 2);
138                assert_eq!(i.rows[0].attrs.len(), 2);
139            }
140            _ => panic!("wrong variant"),
141        }
142    }
143
144    #[test]
145    fn parse_delete_coords_in() {
146        let sql = "DELETE FROM ARRAY g WHERE COORDS IN ((1, 100), (1, 200))";
147        let stmt = try_parse_array_statement(sql).unwrap().unwrap();
148        match stmt {
149            ArrayStatement::Delete(d) => {
150                assert_eq!(d.name, "g");
151                assert_eq!(d.coords.len(), 2);
152            }
153            _ => panic!("wrong variant"),
154        }
155    }
156
157    #[test]
158    fn create_rejects_unknown_dim_type() {
159        let sql = "CREATE ARRAY g DIMS (x BOGUS [0..10]) ATTRS (v INT64) TILE_EXTENTS (1)";
160        assert!(try_parse_array_statement(sql).is_err());
161    }
162
163    #[test]
164    fn parse_create_array_with_audit_retain() {
165        let sql = "CREATE ARRAY g \
166                   DIMS (x INT64 [0..100]) \
167                   ATTRS (v INT64) \
168                   TILE_EXTENTS (10) \
169                   WITH (audit_retain_ms = 86400000)";
170        let stmt = try_parse_array_statement(sql).unwrap().unwrap();
171        match stmt {
172            ArrayStatement::Create(c) => {
173                assert_eq!(c.audit_retain_ms, Some(86_400_000));
174                assert_eq!(c.minimum_audit_retain_ms, None);
175                assert_eq!(c.prefix_bits, 8);
176            }
177            _ => panic!("wrong variant"),
178        }
179    }
180
181    #[test]
182    fn parse_create_array_with_all_retention_keys() {
183        let sql = "CREATE ARRAY genomes \
184                   DIMS (variant_id INT64 [0..1000000000], sample_id INT64 [0..100000]) \
185                   ATTRS (gt INT64, dp INT64) \
186                   TILE_EXTENTS (1024, 256) \
187                   WITH (prefix_bits = 8, audit_retain_ms = 86400000, minimum_audit_retain_ms = 3600000)";
188        let stmt = try_parse_array_statement(sql).unwrap().unwrap();
189        match stmt {
190            ArrayStatement::Create(c) => {
191                assert_eq!(c.prefix_bits, 8);
192                assert_eq!(c.audit_retain_ms, Some(86_400_000));
193                assert_eq!(c.minimum_audit_retain_ms, Some(3_600_000));
194            }
195            _ => panic!("wrong variant"),
196        }
197    }
198
199    #[test]
200    fn parse_create_array_unknown_with_key_rejected() {
201        let sql = "CREATE ARRAY g \
202                   DIMS (x INT64 [0..10]) \
203                   ATTRS (v INT64) \
204                   TILE_EXTENTS (1) \
205                   WITH (bogus_key = 42)";
206        assert!(try_parse_array_statement(sql).is_err());
207    }
208
209    #[test]
210    fn parse_create_array_negative_retain_rejected() {
211        let sql = "CREATE ARRAY g \
212                   DIMS (x INT64 [0..10]) \
213                   ATTRS (v INT64) \
214                   TILE_EXTENTS (1) \
215                   WITH (audit_retain_ms = -1)";
216        assert!(try_parse_array_statement(sql).is_err());
217    }
218
219    #[test]
220    fn parse_alter_array_single_key() {
221        let sql = "ALTER ARRAY my_array SET (audit_retain_ms = 86400000)";
222        let stmt = try_parse_array_statement(sql).unwrap().unwrap();
223        match stmt {
224            ArrayStatement::Alter(a) => {
225                assert_eq!(a.name, "my_array");
226                assert_eq!(a.set.len(), 1);
227                assert_eq!(a.set[0].0, "audit_retain_ms");
228                assert_eq!(a.set[0].1, Some(86_400_000));
229            }
230            _ => panic!("expected Alter variant"),
231        }
232    }
233
234    #[test]
235    fn parse_alter_array_null_value() {
236        let sql = "ALTER ARRAY my_array SET (audit_retain_ms = NULL)";
237        let stmt = try_parse_array_statement(sql).unwrap().unwrap();
238        match stmt {
239            ArrayStatement::Alter(a) => {
240                assert_eq!(a.set[0].0, "audit_retain_ms");
241                assert_eq!(a.set[0].1, None);
242            }
243            _ => panic!("expected Alter variant"),
244        }
245    }
246
247    #[test]
248    fn parse_alter_array_multi_key() {
249        let sql = "ALTER ARRAY arr SET (audit_retain_ms = 5000, minimum_audit_retain_ms = 1000)";
250        let stmt = try_parse_array_statement(sql).unwrap().unwrap();
251        match stmt {
252            ArrayStatement::Alter(a) => {
253                assert_eq!(a.set.len(), 2);
254                assert!(
255                    a.set
256                        .iter()
257                        .any(|(k, v)| k == "audit_retain_ms" && *v == Some(5000))
258                );
259                assert!(
260                    a.set
261                        .iter()
262                        .any(|(k, v)| k == "minimum_audit_retain_ms" && *v == Some(1000))
263                );
264            }
265            _ => panic!("expected Alter variant"),
266        }
267    }
268
269    #[test]
270    fn parse_alter_array_unknown_key_rejected() {
271        let sql = "ALTER ARRAY arr SET (bogus_key = 42)";
272        assert!(try_parse_array_statement(sql).is_err());
273    }
274
275    #[test]
276    fn parse_alter_array_minimum_null_rejected() {
277        let sql = "ALTER ARRAY arr SET (minimum_audit_retain_ms = NULL)";
278        assert!(try_parse_array_statement(sql).is_err());
279    }
280
281    #[test]
282    fn parse_alter_not_matched_for_other_sql() {
283        assert!(
284            try_parse_array_statement("ALTER TABLE foo ADD COLUMN x INT")
285                .unwrap()
286                .is_none()
287        );
288    }
289}