Skip to main content

reifydb_sql/
emit.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2// Copyright (c) 2025 ReifyDB
3
4use std::collections::HashSet;
5
6use crate::{Error, ast::*};
7
8pub fn emit(stmt: &Statement) -> Result<String, Error> {
9	match stmt {
10		Statement::Select(sel) => emit_select_top(sel),
11		Statement::Insert(ins) => emit_insert(ins),
12		Statement::Update(upd) => emit_update(upd),
13		Statement::Delete(del) => emit_delete(del),
14		Statement::CreateTable(ct) => emit_create_table(ct),
15	}
16}
17
18// ── SELECT → RQL pipeline ───────────────────────────────────────────────
19
20fn emit_select_top(sel: &SelectStatement) -> Result<String, Error> {
21	let mut cte_names = HashSet::new();
22	let mut parts = Vec::new();
23
24	// Emit each CTE as a LET $name = <pipeline>
25	for cte in &sel.ctes {
26		let name_lower = cte.name.to_ascii_lowercase();
27		let pipeline = emit_select_inner(&cte.query, &cte_names)?;
28		parts.push(format!("LET ${name_lower} = {pipeline}"));
29		cte_names.insert(name_lower);
30	}
31
32	// Emit the main SELECT
33	parts.push(emit_select_inner(sel, &cte_names)?);
34
35	Ok(parts.join("; "))
36}
37
38fn emit_select(sel: &SelectStatement) -> Result<String, Error> {
39	emit_select_inner(sel, &HashSet::new())
40}
41
42fn emit_select_inner(sel: &SelectStatement, cte_names: &HashSet<String>) -> Result<String, Error> {
43	// If there is no FROM clause, this is a computed-only SELECT
44	if sel.from.is_none() {
45		return emit_select_no_from(sel);
46	}
47
48	let mut parts = Vec::new();
49
50	// FROM
51	parts.push(emit_from_clause(sel.from.as_ref().unwrap(), cte_names)?);
52
53	// JOINs
54	for join in &sel.joins {
55		parts.push(emit_join(join, cte_names)?);
56	}
57
58	// FILTER (WHERE)
59	if let Some(ref where_clause) = sel.where_clause {
60		parts.push(format!("FILTER {{{}}}", emit_expr(where_clause)?));
61	}
62
63	// AGGREGATE (GROUP BY with aggregate functions)
64	let has_aggregates = has_aggregate_functions(&sel.columns);
65	if has_aggregates || !sel.group_by.is_empty() {
66		let agg_exprs = collect_aggregate_columns(&sel.columns)?;
67		let by_exprs = emit_expr_comma_list(&sel.group_by)?;
68
69		if !agg_exprs.is_empty() {
70			let mut agg_str = format!("AGGREGATE {{{agg_exprs}}}");
71			if !sel.group_by.is_empty() {
72				agg_str.push_str(&format!(" BY {{{by_exprs}}}"));
73			}
74			parts.push(agg_str);
75		}
76
77		// HAVING
78		if let Some(ref having) = sel.having {
79			parts.push(format!("FILTER {{{}}}", emit_expr(having)?));
80		}
81
82		// MAP only non-aggregate columns that aren't already in GROUP BY
83		let map_exprs = collect_non_aggregate_map_columns(sel)?;
84		if !map_exprs.is_empty() {
85			parts.push(format!("MAP {{{map_exprs}}}"));
86		}
87	} else {
88		// DISTINCT
89		if sel.distinct {
90			if is_all_columns(&sel.columns) {
91				parts.push("DISTINCT {}".into());
92			} else {
93				let cols = emit_select_columns_plain(&sel.columns)?;
94				parts.push(format!("DISTINCT {{{cols}}}"));
95			}
96		}
97		// MAP (column projection) — only if not SELECT *
98		else if !is_all_columns(&sel.columns) {
99			let cols = emit_select_columns(&sel.columns)?;
100			parts.push(format!("MAP {{{cols}}}"));
101		}
102	}
103
104	// SORT (ORDER BY)
105	if !sel.order_by.is_empty() {
106		let sort = emit_order_by(&sel.order_by)?;
107		parts.push(format!("SORT {{{sort}}}"));
108	}
109
110	// TAKE (LIMIT)
111	if let Some(limit) = sel.limit {
112		parts.push(format!("TAKE {limit}"));
113	}
114
115	// OFFSET
116	if let Some(offset) = sel.offset {
117		parts.push(format!("OFFSET {offset}"));
118	}
119
120	Ok(parts.join(" "))
121}
122
123fn emit_select_no_from(sel: &SelectStatement) -> Result<String, Error> {
124	let cols = emit_select_columns(&sel.columns)?;
125	Ok(format!("MAP {{{cols}}}"))
126}
127
128fn emit_from_clause(from: &FromClause, cte_names: &HashSet<String>) -> Result<String, Error> {
129	match from {
130		FromClause::Table {
131			name,
132			schema,
133		} => {
134			if schema.is_none() && cte_names.contains(&name.to_ascii_lowercase()) {
135				return Ok(format!("FROM ${}", name.to_ascii_lowercase()));
136			}
137			if let Some(schema) = schema {
138				Ok(format!("FROM {schema}.{name}"))
139			} else {
140				Ok(format!("FROM {name}"))
141			}
142		}
143		FromClause::Subquery(sel) => {
144			let inner = emit_select(sel)?;
145			Ok(format!("FROM {{{inner}}}"))
146		}
147	}
148}
149
150fn emit_join(join: &JoinClause, cte_names: &HashSet<String>) -> Result<String, Error> {
151	let join_kw = match join.join_type {
152		JoinType::Inner => "JOIN",
153		JoinType::Left => "LEFT JOIN",
154	};
155
156	let table_name = match &join.table {
157		FromClause::Table {
158			name,
159			schema,
160		} => {
161			if schema.is_none() && cte_names.contains(&name.to_ascii_lowercase()) {
162				format!("${}", name.to_ascii_lowercase())
163			} else if let Some(schema) = schema {
164				format!("{schema}.{name}")
165			} else {
166				name.clone()
167			}
168		}
169		FromClause::Subquery(sel) => emit_select(sel)?,
170	};
171
172	let alias = join
173		.table_alias
174		.as_deref()
175		.or(match &join.table {
176			FromClause::Table {
177				name,
178				..
179			} => Some(name.as_str()),
180			_ => None,
181		})
182		.unwrap_or("_");
183
184	// Extract USING columns from the ON condition
185	let using = emit_join_using(&join.on, alias)?;
186
187	Ok(format!("{join_kw} {{FROM {table_name}}} AS {alias} USING ({using})"))
188}
189
190/// Convert a JOIN ON condition like `t1.a = t2.b` to USING `(a, t2.b)` format.
191fn emit_join_using(on_expr: &Expr, right_alias: &str) -> Result<String, Error> {
192	match on_expr {
193		Expr::BinaryOp {
194			left,
195			op: BinaryOp::Eq,
196			right,
197		} => {
198			let (left_col, right_col) = extract_join_columns(left, right, right_alias)?;
199			Ok(format!("{left_col}, {right_col}"))
200		}
201		Expr::BinaryOp {
202			left,
203			op: BinaryOp::And,
204			right,
205		} => {
206			let l = emit_join_using(left, right_alias)?;
207			let r = emit_join_using(right, right_alias)?;
208			Ok(format!("{l}, {r}"))
209		}
210		_ => {
211			// Fallback: emit as a filter-like expression
212			emit_expr(on_expr)
213		}
214	}
215}
216
217fn extract_join_columns(left: &Expr, right: &Expr, right_alias: &str) -> Result<(String, String), Error> {
218	let left_col = match left {
219		Expr::QualifiedIdentifier(_table, col) => col.clone(),
220		Expr::Identifier(col) => col.clone(),
221		_ => emit_expr(left)?,
222	};
223	let right_col = match right {
224		Expr::QualifiedIdentifier(table, col) => format!("{table}.{col}"),
225		Expr::Identifier(col) => format!("{right_alias}.{col}"),
226		_ => emit_expr(right)?,
227	};
228	Ok((left_col, right_col))
229}
230
231// ── INSERT → RQL ────────────────────────────────────────────────────────
232
233fn emit_insert(ins: &InsertStatement) -> Result<String, Error> {
234	let table = if let Some(ref schema) = ins.schema {
235		format!("{schema}.{}", ins.table)
236	} else {
237		ins.table.clone()
238	};
239
240	let mut rows = Vec::new();
241	for row_values in &ins.values {
242		if ins.columns.is_empty() {
243			// No column names — emit positional tuple
244			let vals: Result<Vec<_>, _> = row_values.iter().map(emit_expr).collect();
245			rows.push(format!("({})", vals?.join(", ")));
246		} else {
247			// Named columns — emit record
248			let mut fields = Vec::new();
249			for (i, val) in row_values.iter().enumerate() {
250				let col_name = if i < ins.columns.len() {
251					&ins.columns[i]
252				} else {
253					return Err(Error("more values than columns in INSERT".into()));
254				};
255				fields.push(format!("{}: {}", col_name, emit_expr(val)?));
256			}
257			rows.push(format!("{{{}}}", fields.join(", ")));
258		}
259	}
260
261	Ok(format!("INSERT {} [{}]", table, rows.join(", ")))
262}
263
264// ── UPDATE → RQL ────────────────────────────────────────────────────────
265
266fn emit_update(upd: &UpdateStatement) -> Result<String, Error> {
267	let table = if let Some(ref schema) = upd.schema {
268		format!("{schema}.{}", upd.table)
269	} else {
270		upd.table.clone()
271	};
272
273	let mut assignments = Vec::new();
274	for (col, val) in &upd.assignments {
275		assignments.push(format!("{}: {}", col, emit_expr(val)?));
276	}
277
278	let mut result = format!("UPDATE {} {{{}}}", table, assignments.join(", "));
279
280	if let Some(ref where_clause) = upd.where_clause {
281		result.push_str(&format!(" FILTER {{{}}}", emit_expr(where_clause)?));
282	}
283
284	Ok(result)
285}
286
287// ── DELETE → RQL ────────────────────────────────────────────────────────
288
289fn emit_delete(del: &DeleteStatement) -> Result<String, Error> {
290	let table = if let Some(ref schema) = del.schema {
291		format!("{schema}.{}", del.table)
292	} else {
293		del.table.clone()
294	};
295
296	let mut result = format!("DELETE {table}");
297
298	if let Some(ref where_clause) = del.where_clause {
299		result.push_str(&format!(" FILTER {{{}}}", emit_expr(where_clause)?));
300	}
301
302	Ok(result)
303}
304
305// ── CREATE TABLE → RQL ──────────────────────────────────────────────────
306
307fn emit_create_table(ct: &CreateTableStatement) -> Result<String, Error> {
308	let table = if let Some(ref schema) = ct.schema {
309		format!("{schema}.{}", ct.table)
310	} else {
311		ct.table.clone()
312	};
313
314	let mut cols = Vec::new();
315	for col in &ct.columns {
316		let ty = emit_rql_type(&col.data_type);
317		if col.nullable {
318			cols.push(format!("{}: Option({})", col.name, ty));
319		} else {
320			cols.push(format!("{}: {}", col.name, ty));
321		}
322	}
323
324	let mut result = format!("CREATE TABLE {} {{{}}}", table, cols.join(", "));
325
326	if !ct.primary_key.is_empty() {
327		result.push_str(&format!(" WITH {{primary_key: {{{}}}}}", ct.primary_key.join(", ")));
328	}
329
330	Ok(result)
331}
332
333fn emit_rql_type(ty: &SqlType) -> &'static str {
334	match ty {
335		SqlType::Int | SqlType::Int4 | SqlType::Integer => "int4",
336		SqlType::Int2 | SqlType::Smallint => "int2",
337		SqlType::Int8 | SqlType::Bigint => "int8",
338		SqlType::Float4 | SqlType::Real => "float4",
339		SqlType::Float8 | SqlType::Double => "float8",
340		SqlType::Boolean | SqlType::Bool => "bool",
341		SqlType::Varchar(_) | SqlType::Char(_) | SqlType::Text | SqlType::Utf8 => "utf8",
342		SqlType::Blob => "blob",
343	}
344}
345
346// ── Expression emitter ──────────────────────────────────────────────────
347
348fn emit_expr(expr: &Expr) -> Result<String, Error> {
349	match expr {
350		Expr::Identifier(name) => Ok(name.clone()),
351		Expr::QualifiedIdentifier(table, col) => Ok(format!("{table}.{col}")),
352		Expr::IntegerLiteral(n) => Ok(n.to_string()),
353		Expr::FloatLiteral(f) => Ok(format_float(*f)),
354		Expr::StringLiteral(s) => Ok(format!("'{s}'")),
355		Expr::BoolLiteral(b) => Ok(if *b {
356			"true"
357		} else {
358			"false"
359		}
360		.into()),
361		Expr::Null => Ok("none".into()),
362		Expr::BinaryOp {
363			left,
364			op,
365			right,
366		} => {
367			let l = emit_expr(left)?;
368			let r = emit_expr(right)?;
369			let op_str = match op {
370				BinaryOp::Eq => "==",
371				BinaryOp::NotEq => "!=",
372				BinaryOp::Lt => "<",
373				BinaryOp::Gt => ">",
374				BinaryOp::LtEq => "<=",
375				BinaryOp::GtEq => ">=",
376				BinaryOp::And => "and",
377				BinaryOp::Or => "or",
378				BinaryOp::Add => "+",
379				BinaryOp::Sub => "-",
380				BinaryOp::Mul => "*",
381				BinaryOp::Div => "/",
382				BinaryOp::Mod => "%",
383			};
384			Ok(format!("{l} {op_str} {r}"))
385		}
386		Expr::UnaryOp {
387			op,
388			expr,
389		} => {
390			let e = emit_expr(expr)?;
391			match op {
392				UnaryOp::Not => Ok(format!("not {e}")),
393				UnaryOp::Neg => Ok(format!("-{e}")),
394			}
395		}
396		Expr::FunctionCall {
397			name,
398			args,
399		} => {
400			let func_name = sql_to_rql_function(name)?;
401			let arg_strs: Result<Vec<_>, _> = args.iter().map(emit_expr).collect();
402			let args_str = arg_strs?.join(", ");
403			Ok(format!("{func_name}({args_str})"))
404		}
405		Expr::Between {
406			expr,
407			low,
408			high,
409			negated,
410		} => {
411			let e = emit_expr(expr)?;
412			let l = emit_expr(low)?;
413			let h = emit_expr(high)?;
414			if *negated {
415				Ok(format!("not ({e} between {l} and {h})"))
416			} else {
417				Ok(format!("{e} between {l} and {h}"))
418			}
419		}
420		Expr::InList {
421			expr,
422			list,
423			negated,
424		} => {
425			let e = emit_expr(expr)?;
426			let items: Result<Vec<_>, _> = list.iter().map(emit_expr).collect();
427			let items_str = items?.join(", ");
428			if *negated {
429				Ok(format!("not ({e} in ({items_str}))"))
430			} else {
431				Ok(format!("{e} in ({items_str})"))
432			}
433		}
434		Expr::IsNull {
435			expr,
436			negated,
437		} => {
438			let e = emit_expr(expr)?;
439			if *negated {
440				Ok(format!("{e} != none"))
441			} else {
442				Ok(format!("{e} == none"))
443			}
444		}
445		Expr::Cast {
446			expr,
447			data_type,
448		} => {
449			let e = emit_expr(expr)?;
450			let ty = emit_rql_type(data_type);
451			Ok(format!("cast({e}, {ty})"))
452		}
453		Expr::Nested(inner) => {
454			let e = emit_expr(inner)?;
455			Ok(format!("({e})"))
456		}
457	}
458}
459
460fn format_float(f: f64) -> String {
461	let s = f.to_string();
462	if s.contains('.') {
463		s
464	} else {
465		format!("{s}.0")
466	}
467}
468
469// ── Helpers ─────────────────────────────────────────────────────────────
470
471fn is_all_columns(cols: &[SelectColumn]) -> bool {
472	cols.len() == 1 && matches!(cols[0], SelectColumn::AllColumns)
473}
474
475fn has_aggregate_functions(cols: &[SelectColumn]) -> bool {
476	cols.iter().any(|c| match c {
477		SelectColumn::Expr {
478			expr,
479			..
480		} => expr_has_aggregate(expr),
481		_ => false,
482	})
483}
484
485fn expr_has_aggregate(expr: &Expr) -> bool {
486	match expr {
487		Expr::FunctionCall {
488			name,
489			..
490		} => {
491			matches!(
492				sql_to_rql_function(name),
493				Ok("math::count" | "math::sum" | "math::avg" | "math::min" | "math::max")
494			)
495		}
496		Expr::BinaryOp {
497			left,
498			right,
499			..
500		} => expr_has_aggregate(left) || expr_has_aggregate(right),
501		Expr::UnaryOp {
502			expr,
503			..
504		} => expr_has_aggregate(expr),
505		Expr::Nested(inner) => expr_has_aggregate(inner),
506		_ => false,
507	}
508}
509
510fn collect_aggregate_columns(cols: &[SelectColumn]) -> Result<String, Error> {
511	let mut agg_exprs = Vec::new();
512	for col in cols {
513		if let SelectColumn::Expr {
514			expr,
515			alias,
516		} = col
517		{
518			if expr_has_aggregate(expr) {
519				let e = emit_expr(expr)?;
520				if let Some(alias) = alias {
521					agg_exprs.push(format!("{alias}: {e}"));
522				} else {
523					agg_exprs.push(e);
524				}
525			}
526		}
527	}
528	Ok(agg_exprs.join(", "))
529}
530
531fn collect_non_aggregate_map_columns(sel: &SelectStatement) -> Result<String, Error> {
532	let mut map_exprs = Vec::new();
533	for col in &sel.columns {
534		if let SelectColumn::Expr {
535			expr,
536			alias,
537		} = col
538		{
539			if !expr_has_aggregate(expr) {
540				// Check if this column is already in GROUP BY
541				let is_in_group_by = sel.group_by.iter().any(|gb| expr_eq(gb, expr));
542				if !is_in_group_by {
543					let e = emit_expr(expr)?;
544					if let Some(alias) = alias {
545						map_exprs.push(format!("{alias}: {e}"));
546					} else {
547						map_exprs.push(e);
548					}
549				}
550			}
551		}
552	}
553	Ok(map_exprs.join(", "))
554}
555
556fn emit_select_columns(cols: &[SelectColumn]) -> Result<String, Error> {
557	let mut parts = Vec::new();
558	for col in cols {
559		match col {
560			SelectColumn::AllColumns => parts.push("*".into()),
561			SelectColumn::Expr {
562				expr,
563				alias,
564			} => {
565				let e = emit_expr(expr)?;
566				if let Some(alias) = alias {
567					parts.push(format!("{alias}: {e}"));
568				} else {
569					parts.push(e);
570				}
571			}
572		}
573	}
574	Ok(parts.join(", "))
575}
576
577fn emit_select_columns_plain(cols: &[SelectColumn]) -> Result<String, Error> {
578	let mut parts = Vec::new();
579	for col in cols {
580		match col {
581			SelectColumn::AllColumns => parts.push("*".into()),
582			SelectColumn::Expr {
583				expr,
584				..
585			} => {
586				parts.push(emit_expr(expr)?);
587			}
588		}
589	}
590	Ok(parts.join(", "))
591}
592
593fn sql_to_rql_function(name: &str) -> Result<&'static str, Error> {
594	match name.to_uppercase().as_str() {
595		// Aggregates
596		"COUNT" => Ok("math::count"),
597		"SUM" => Ok("math::sum"),
598		"AVG" => Ok("math::avg"),
599		"MIN" => Ok("math::min"),
600		"MAX" => Ok("math::max"),
601		// Math scalar
602		"ABS" => Ok("math::abs"),
603		"ACOS" => Ok("math::acos"),
604		"ASIN" => Ok("math::asin"),
605		"ATAN" => Ok("math::atan"),
606		"ATAN2" => Ok("math::atan2"),
607		"CEIL" | "CEILING" => Ok("math::ceil"),
608		"COS" => Ok("math::cos"),
609		"EXP" => Ok("math::exp"),
610		"FLOOR" => Ok("math::floor"),
611		"GCD" => Ok("math::gcd"),
612		"LCM" => Ok("math::lcm"),
613		"LOG" => Ok("math::log"),
614		"LOG10" => Ok("math::log10"),
615		"LOG2" => Ok("math::log2"),
616		"MOD" => Ok("math::mod"),
617		"PI" => Ok("math::pi"),
618		"POWER" | "POW" => Ok("math::power"),
619		"ROUND" => Ok("math::round"),
620		"SIGN" => Ok("math::sign"),
621		"SIN" => Ok("math::sin"),
622		"SQRT" => Ok("math::sqrt"),
623		"TAN" => Ok("math::tan"),
624		"TRUNCATE" | "TRUNC" => Ok("math::truncate"),
625		// Text
626		"ASCII" => Ok("text::ascii"),
627		"CHAR" | "CHR" => Ok("text::char"),
628		"CONCAT" => Ok("text::concat"),
629		"LENGTH" | "LEN" | "CHAR_LENGTH" | "CHARACTER_LENGTH" => Ok("text::length"),
630		"LOWER" | "LCASE" => Ok("text::lower"),
631		"LPAD" => Ok("text::pad_left"),
632		"REPEAT" => Ok("text::repeat"),
633		"REPLACE" => Ok("text::replace"),
634		"REVERSE" => Ok("text::reverse"),
635		"RPAD" => Ok("text::pad_right"),
636		"SUBSTRING" | "SUBSTR" => Ok("text::substring"),
637		"TRIM" => Ok("text::trim"),
638		"LTRIM" => Ok("text::trim_start"),
639		"RTRIM" => Ok("text::trim_end"),
640		"UPPER" | "UCASE" => Ok("text::upper"),
641		_ => Err(Error(format!("no SQL-to-RQL mapping for function: {name}"))),
642	}
643}
644
645fn emit_order_by(items: &[OrderByItem]) -> Result<String, Error> {
646	let mut parts = Vec::new();
647	for item in items {
648		let e = emit_expr(&item.expr)?;
649		match item.direction {
650			OrderDirection::Asc => parts.push(format!("{e}:asc")),
651			OrderDirection::Desc => parts.push(e),
652		};
653	}
654	Ok(parts.join(", "))
655}
656
657fn emit_expr_comma_list(exprs: &[Expr]) -> Result<String, Error> {
658	let parts: Result<Vec<_>, _> = exprs.iter().map(emit_expr).collect();
659	Ok(parts?.join(", "))
660}
661
662/// Simple structural equality check for expressions (for GROUP BY dedup).
663fn expr_eq(a: &Expr, b: &Expr) -> bool {
664	match (a, b) {
665		(Expr::Identifier(a), Expr::Identifier(b)) => a == b,
666		(Expr::QualifiedIdentifier(at, ac), Expr::QualifiedIdentifier(bt, bc)) => at == bt && ac == bc,
667		_ => false,
668	}
669}
670
671#[cfg(test)]
672mod tests {
673	use super::*;
674	use crate::{parser::Parser, token::tokenize};
675
676	fn transpile(sql: &str) -> String {
677		let tokens = tokenize(sql).unwrap();
678		let stmt = Parser::new(tokens).parse().unwrap();
679		emit(&stmt).unwrap()
680	}
681
682	#[test]
683	fn test_select_star() {
684		assert_eq!(transpile("SELECT * FROM users"), "FROM users");
685	}
686
687	#[test]
688	fn test_select_columns() {
689		assert_eq!(transpile("SELECT id, name FROM users"), "FROM users MAP {id, name}");
690	}
691
692	#[test]
693	fn test_select_alias() {
694		assert_eq!(transpile("SELECT id, name AS username FROM users"), "FROM users MAP {id, username: name}");
695	}
696
697	#[test]
698	fn test_where() {
699		assert_eq!(transpile("SELECT * FROM users WHERE age > 18"), "FROM users FILTER {age > 18}");
700	}
701
702	#[test]
703	fn test_order_by() {
704		assert_eq!(transpile("SELECT * FROM users ORDER BY name ASC"), "FROM users SORT {name:asc}");
705	}
706
707	#[test]
708	fn test_limit() {
709		assert_eq!(transpile("SELECT * FROM users LIMIT 10"), "FROM users TAKE 10");
710	}
711
712	#[test]
713	fn test_insert() {
714		assert_eq!(
715			transpile("INSERT INTO users (id, name) VALUES (1, 'Alice')"),
716			"INSERT users [{id: 1, name: 'Alice'}]"
717		);
718	}
719
720	#[test]
721	fn test_update() {
722		assert_eq!(
723			transpile("UPDATE users SET name = 'Bob' WHERE id = 1"),
724			"UPDATE users {name: 'Bob'} FILTER {id == 1}"
725		);
726	}
727
728	#[test]
729	fn test_delete() {
730		assert_eq!(transpile("DELETE FROM users WHERE id = 1"), "DELETE users FILTER {id == 1}");
731	}
732
733	#[test]
734	fn test_create_table() {
735		assert_eq!(
736			transpile("CREATE TABLE users (id INT, name TEXT, active BOOLEAN)"),
737			"CREATE TABLE users {id: Option(int4), name: Option(utf8), active: Option(bool)}"
738		);
739	}
740
741	#[test]
742	fn test_create_table_not_null() {
743		assert_eq!(
744			transpile("CREATE TABLE t (id INT NOT NULL, name TEXT)"),
745			"CREATE TABLE t {id: int4, name: Option(utf8)}"
746		);
747	}
748
749	#[test]
750	fn test_null_to_none() {
751		assert_eq!(transpile("SELECT NULL"), "MAP {none}");
752	}
753
754	#[test]
755	fn test_not_equal() {
756		assert_eq!(transpile("SELECT * FROM t WHERE a <> b"), "FROM t FILTER {a != b}");
757	}
758
759	#[test]
760	fn test_and_or() {
761		assert_eq!(transpile("SELECT * FROM t WHERE a = 1 AND b = 2"), "FROM t FILTER {a == 1 and b == 2}");
762	}
763
764	#[test]
765	fn test_distinct() {
766		assert_eq!(transpile("SELECT DISTINCT name FROM users"), "FROM users DISTINCT {name}");
767	}
768
769	#[test]
770	fn test_group_by_aggregate() {
771		assert_eq!(
772			transpile("SELECT COUNT(id) FROM users GROUP BY dept"),
773			"FROM users AGGREGATE {math::count(id)} BY {dept}"
774		);
775	}
776
777	#[test]
778	fn test_cast() {
779		assert_eq!(transpile("SELECT CAST(x AS INT)"), "MAP {cast(x, int4)}");
780	}
781
782	#[test]
783	fn test_between() {
784		assert_eq!(transpile("SELECT * FROM t WHERE x BETWEEN 1 AND 10"), "FROM t FILTER {x between 1 and 10}");
785	}
786
787	#[test]
788	fn test_in_list() {
789		assert_eq!(transpile("SELECT * FROM t WHERE x IN (1, 2, 3)"), "FROM t FILTER {x in (1, 2, 3)}");
790	}
791
792	#[test]
793	fn test_is_null() {
794		assert_eq!(transpile("SELECT * FROM t WHERE x IS NULL"), "FROM t FILTER {x == none}");
795	}
796
797	#[test]
798	fn test_is_not_null() {
799		assert_eq!(transpile("SELECT * FROM t WHERE x IS NOT NULL"), "FROM t FILTER {x != none}");
800	}
801
802	#[test]
803	fn test_join() {
804		assert_eq!(
805			transpile("SELECT * FROM t1 INNER JOIN t2 ON t1.a = t2.b"),
806			"FROM t1 JOIN {FROM t2} AS t2 USING (a, t2.b)"
807		);
808	}
809
810	#[test]
811	fn test_left_join() {
812		assert_eq!(
813			transpile("SELECT * FROM t1 LEFT JOIN t2 ON t1.a = t2.b"),
814			"FROM t1 LEFT JOIN {FROM t2} AS t2 USING (a, t2.b)"
815		);
816	}
817
818	#[test]
819	fn test_select_no_from() {
820		assert_eq!(transpile("SELECT 1 + 2"), "MAP {1 + 2}");
821	}
822
823	#[test]
824	fn test_multiple_insert_rows() {
825		assert_eq!(
826			transpile("INSERT INTO t (a, b) VALUES (1, 2), (3, 4)"),
827			"INSERT t [{a: 1, b: 2}, {a: 3, b: 4}]"
828		);
829	}
830
831	#[test]
832	fn test_schema_qualified_table() {
833		assert_eq!(transpile("SELECT * FROM test.users"), "FROM test.users");
834	}
835
836	#[test]
837	fn test_simple_cte() {
838		assert_eq!(
839			transpile("WITH t AS (SELECT * FROM users WHERE active = true) SELECT * FROM t"),
840			"LET $t = FROM users FILTER {active == true}; FROM $t"
841		);
842	}
843
844	#[test]
845	fn test_insert_without_columns() {
846		assert_eq!(transpile("INSERT INTO t1 VALUES (1, 'true')"), "INSERT t1 [(1, 'true')]");
847	}
848
849	#[test]
850	fn test_create_table_primary_key() {
851		assert_eq!(
852			transpile("CREATE TABLE t (v1 INT NOT NULL, v2 INT NOT NULL, PRIMARY KEY(v1))"),
853			"CREATE TABLE t {v1: int4, v2: int4} WITH {primary_key: {v1}}"
854		);
855		assert_eq!(
856			transpile("CREATE TABLE t (a INT NOT NULL, b INT NOT NULL, PRIMARY KEY(a, b))"),
857			"CREATE TABLE t {a: int4, b: int4} WITH {primary_key: {a, b}}"
858		);
859	}
860
861	#[test]
862	fn test_multiple_ctes() {
863		assert_eq!(
864			transpile("WITH a AS (SELECT * FROM users), b AS (SELECT id FROM a) SELECT * FROM b"),
865			"LET $a = FROM users; LET $b = FROM $a MAP {id}; FROM $b"
866		);
867	}
868}