1use crate::{Dialect, Sql};
4use nautilus_core::{BinaryOp, Delete, Expr, Insert, Result, Select, Update, Value};
5
6#[derive(Debug, Clone, Copy)]
8pub struct SqliteDialect;
9
10impl Dialect for SqliteDialect {
13 fn render_select_owned(&self, mut select: Select) -> Result<Sql> {
14 let mut ctx = RenderContext::with_estimate(crate::estimate_select_render(&select));
15 render_select_body_core_mut!(&mut ctx, &mut select, '"', render_expr_owned, false, false);
16 Ok(Sql {
17 text: ctx.sql,
18 params: ctx.params,
19 })
20 }
21
22 fn render_insert_owned(&self, mut insert: Insert) -> Result<Sql> {
23 let mut ctx = RenderContext::with_estimate(crate::estimate_insert_render(&insert));
24 render_insert_body_mut!(&mut ctx, &mut insert, '"', true, false);
25 Ok(Sql {
26 text: ctx.sql,
27 params: ctx.params,
28 })
29 }
30
31 fn render_update_owned(&self, mut update: Update) -> Result<Sql> {
32 let mut ctx = RenderContext::with_estimate(crate::estimate_update_render(&update));
33 render_update_body_mut!(&mut ctx, &mut update, '"', render_expr_owned, true, false);
34 Ok(Sql {
35 text: ctx.sql,
36 params: ctx.params,
37 })
38 }
39
40 fn render_delete_owned(&self, mut delete: Delete) -> Result<Sql> {
41 let mut ctx = RenderContext::with_estimate(crate::estimate_delete_render(&delete));
42 render_delete_body_mut!(&mut ctx, &mut delete, '"', render_expr_owned, true);
43 Ok(Sql {
44 text: ctx.sql,
45 params: ctx.params,
46 })
47 }
48}
49
50struct RenderContext {
51 sql: String,
52 params: Vec<Value>,
53}
54
55impl RenderContext {
56 fn with_estimate(estimate: crate::RenderEstimate) -> Self {
57 Self {
58 sql: String::with_capacity(estimate.sql_capacity),
59 params: Vec::with_capacity(estimate.params_capacity),
60 }
61 }
62
63 fn push_param(&mut self, value: Value) {
64 self.params.push(value);
65 self.sql.push('?');
66 }
67
68 fn take_param(&mut self, value: &mut Value) {
69 self.push_param(std::mem::replace(value, Value::Null));
70 }
71}
72
73fn render_select_body_owned(ctx: &mut RenderContext, select: &mut crate::Select) {
74 render_select_body_core_mut!(ctx, select, '"', render_expr_owned, false, false);
75}
76
77fn render_expr_owned(ctx: &mut RenderContext, expr: &mut Expr) {
78 render_expr_common_mut!(ctx, expr, '"', render_expr_owned, render_select_body_owned, {
79 Expr::CompositeField {
80 table,
81 column,
82 json_key,
83 ..
84 } => {
85 ctx.sql.push_str("json_extract(");
86 crate::push_qualified_identifier(&mut ctx.sql, table, column, '"');
87 ctx.sql.push_str(", ");
88 crate::push_json_object_path_literal(&mut ctx.sql, json_key);
89 ctx.sql.push(')');
90 }
91 Expr::Param(value) => {
92 if matches!(value, Value::Null) {
93 ctx.sql.push_str("NULL");
94 } else {
95 ctx.take_param(value);
96 }
97 }
98 Expr::Binary { left, op, right } => {
99 if matches!(*op, BinaryOp::In | BinaryOp::NotIn) {
100 ctx.sql.push('(');
101 render_expr_owned(ctx, left.as_mut());
102 ctx.sql.push(' ');
103 ctx.sql
104 .push_str(if matches!(*op, BinaryOp::In) { "IN" } else { "NOT IN" });
105 ctx.sql.push_str(" (");
106 if let Expr::List(exprs) = right.as_mut() {
107 for (i, e) in exprs.iter_mut().enumerate() {
108 if i > 0 {
109 ctx.sql.push_str(", ");
110 }
111 render_expr_owned(ctx, e);
112 }
113 } else {
114 render_expr_owned(ctx, right.as_mut());
115 }
116 ctx.sql.push(')');
117 ctx.sql.push(')');
118 } else if matches!(
119 *op,
120 BinaryOp::ArrayContains | BinaryOp::ArrayContainedBy | BinaryOp::ArrayOverlaps
121 ) {
122 match *op {
123 BinaryOp::ArrayContains => {
124 ctx.sql.push_str("NOT EXISTS (SELECT 1 FROM json_each(");
125 render_expr_owned(ctx, right.as_mut());
126 ctx.sql.push_str(") AS _rhs WHERE NOT EXISTS (SELECT 1 FROM json_each(");
127 render_expr_owned(ctx, left.as_mut());
128 ctx.sql.push_str(") AS _col WHERE _col.value IS _rhs.value))");
129 }
130 BinaryOp::ArrayContainedBy => {
131 ctx.sql.push_str("NOT EXISTS (SELECT 1 FROM json_each(");
132 render_expr_owned(ctx, left.as_mut());
133 ctx.sql.push_str(") AS _col WHERE NOT EXISTS (SELECT 1 FROM json_each(");
134 render_expr_owned(ctx, right.as_mut());
135 ctx.sql.push_str(") AS _rhs WHERE _col.value IS _rhs.value))");
136 }
137 BinaryOp::ArrayOverlaps => {
138 ctx.sql.push_str("EXISTS (SELECT 1 FROM json_each(");
139 render_expr_owned(ctx, left.as_mut());
140 ctx.sql.push_str(") AS _col WHERE EXISTS (SELECT 1 FROM json_each(");
141 render_expr_owned(ctx, right.as_mut());
142 ctx.sql.push_str(") AS _rhs WHERE _col.value IS _rhs.value))");
143 }
144 _ => unreachable!(),
145 }
146 } else {
147 ctx.sql.push('(');
148 render_expr_owned(ctx, left.as_mut());
149 ctx.sql.push(' ');
150 ctx.sql.push_str(crate::binary_op_sql(op));
151 ctx.sql.push(' ');
152 render_expr_owned(ctx, right.as_mut());
153 ctx.sql.push(')');
154 }
155 }
156 Expr::FunctionCall { name, args } => {
157 let sqlite_name = match name.as_str() {
158 "json_agg" => "json_group_array",
159 "json_build_object" => "json_object",
160 _ => name,
161 };
162 ctx.sql.push_str(sqlite_name);
163 ctx.sql.push('(');
164 for (i, arg) in args.iter_mut().enumerate() {
165 if i > 0 {
166 ctx.sql.push_str(", ");
167 }
168 render_expr_owned(ctx, arg);
169 }
170 ctx.sql.push(')');
171 }
172 Expr::Filter { expr, predicate } => {
173 render_expr_owned(ctx, expr.as_mut());
174 ctx.sql.push_str(" FILTER (WHERE ");
175 render_expr_owned(ctx, predicate.as_mut());
176 ctx.sql.push(')');
177 }
178 });
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 fn quote_identifier(name: &str) -> String {
186 let mut sql = String::new();
187 crate::push_quoted_identifier(&mut sql, name, '"');
188 sql
189 }
190
191 #[test]
192 fn test_quote_identifier() {
193 assert_eq!(quote_identifier("users"), "\"users\"");
194 assert_eq!(quote_identifier("email"), "\"email\"");
195 assert_eq!(quote_identifier("foo\"bar"), "\"foo\"\"bar\"");
196 assert_eq!(quote_identifier("a\"b\"c"), "\"a\"\"b\"\"c\"");
197 }
198
199 #[test]
200 fn test_array_contains_operator() {
201 let dialect = SqliteDialect;
202 let expr = Expr::Binary {
203 left: Box::new(Expr::column("posts__tags")),
204 op: BinaryOp::ArrayContains,
205 right: Box::new(Expr::param(Value::Array(vec![Value::String(
206 "rust".to_string(),
207 )]))),
208 };
209 let select = Select::from_table("posts").filter(expr).build().unwrap();
210 let sql = dialect.render_select(&select).unwrap();
211
212 assert_eq!(
213 sql.text,
214 "SELECT * FROM \"posts\" WHERE NOT EXISTS (SELECT 1 FROM json_each(?) AS _rhs WHERE NOT EXISTS (SELECT 1 FROM json_each(\"posts\".\"tags\") AS _col WHERE _col.value IS _rhs.value))"
215 );
216 assert_eq!(sql.params.len(), 1);
217 match &sql.params[0] {
218 Value::Array(arr) => {
219 assert_eq!(arr.len(), 1);
220 assert_eq!(arr[0], Value::String("rust".to_string()));
221 }
222 _ => panic!("Expected Array value"),
223 }
224 }
225
226 #[test]
227 fn test_array_contained_by_operator() {
228 let dialect = SqliteDialect;
229 let expr = Expr::Binary {
230 left: Box::new(Expr::column("posts__tags")),
231 op: BinaryOp::ArrayContainedBy,
232 right: Box::new(Expr::param(Value::Array(vec![
233 Value::String("rust".to_string()),
234 Value::String("go".to_string()),
235 ]))),
236 };
237 let select = Select::from_table("posts").filter(expr).build().unwrap();
238 let sql = dialect.render_select(&select).unwrap();
239
240 assert_eq!(
241 sql.text,
242 "SELECT * FROM \"posts\" WHERE NOT EXISTS (SELECT 1 FROM json_each(\"posts\".\"tags\") AS _col WHERE NOT EXISTS (SELECT 1 FROM json_each(?) AS _rhs WHERE _col.value IS _rhs.value))"
243 );
244 assert_eq!(sql.params.len(), 1);
245 match &sql.params[0] {
246 Value::Array(arr) => {
247 assert_eq!(arr.len(), 2);
248 assert_eq!(arr[0], Value::String("rust".to_string()));
249 assert_eq!(arr[1], Value::String("go".to_string()));
250 }
251 _ => panic!("Expected Array value"),
252 }
253 }
254
255 #[test]
256 fn test_array_overlaps_operator() {
257 let dialect = SqliteDialect;
258 let expr = Expr::Binary {
259 left: Box::new(Expr::column("posts__tags")),
260 op: BinaryOp::ArrayOverlaps,
261 right: Box::new(Expr::param(Value::Array(vec![
262 Value::String("rust".to_string()),
263 Value::String("python".to_string()),
264 ]))),
265 };
266 let select = Select::from_table("posts").filter(expr).build().unwrap();
267 let sql = dialect.render_select(&select).unwrap();
268
269 assert_eq!(
270 sql.text,
271 "SELECT * FROM \"posts\" WHERE EXISTS (SELECT 1 FROM json_each(\"posts\".\"tags\") AS _col WHERE EXISTS (SELECT 1 FROM json_each(?) AS _rhs WHERE _col.value IS _rhs.value))"
272 );
273 assert_eq!(sql.params.len(), 1);
274 match &sql.params[0] {
275 Value::Array(arr) => {
276 assert_eq!(arr.len(), 2);
277 assert_eq!(arr[0], Value::String("rust".to_string()));
278 assert_eq!(arr[1], Value::String("python".to_string()));
279 }
280 _ => panic!("Expected Array value"),
281 }
282 }
283
284 #[test]
285 fn composite_field_ordering_uses_json_extract() {
286 let dialect = SqliteDialect;
287 let select = Select::from_table("shipments")
288 .order_by_expr(
289 Expr::composite_field(
290 "shipments",
291 "delivery_snapshot",
292 "eta_minutes",
293 "etaMinutes",
294 nautilus_core::JsonPathCast::Signed,
295 ),
296 nautilus_core::OrderDir::Asc,
297 )
298 .build()
299 .unwrap();
300 let sql = dialect.render_select(&select).unwrap();
301
302 assert_eq!(
303 sql.text,
304 "SELECT * FROM \"shipments\" ORDER BY json_extract(\"shipments\".\"delivery_snapshot\", '$.\"etaMinutes\"') ASC"
305 );
306 }
307}