Skip to main content

reifydb_sql/
emit.rs

1// SPDX-License-Identifier: Apache-2.0
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		Statement::CreateIndex(ci) => emit_create_index(ci),
16		Statement::DropTable(dt) => emit_drop_table(dt),
17	}
18}
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		let base = emit_select_no_from(sel)?;
46		// Handle set operations even without FROM
47		if let Some((op, right)) = &sel.set_op {
48			let right_rql = emit_select_inner(right, cte_names)?;
49			let op_str = match op {
50				SetOp::Union => "UNION",
51				SetOp::UnionAll => "UNION ALL",
52				SetOp::Intersect => "INTERSECT",
53				SetOp::Except => "EXCEPT",
54			};
55			return Ok(format!("{op_str} {{{base}}} {{{right_rql}}}"));
56		}
57		return Ok(base);
58	}
59
60	let mut parts = Vec::new();
61
62	// FROM
63	let from = sel.from.as_ref().unwrap();
64	parts.push(emit_from_clause(from, cte_names)?);
65
66	// JOINs
67	for join in &sel.joins {
68		parts.push(emit_join(join, cte_names)?);
69	}
70
71	// FILTER (WHERE)
72	if let Some(ref where_clause) = sel.where_clause {
73		parts.push(format!("FILTER {{{}}}", emit_expr(where_clause)?));
74	}
75
76	// AGGREGATE (GROUP BY with aggregate functions)
77	let has_aggregates = has_aggregate_functions(&sel.columns);
78	if has_aggregates || !sel.group_by.is_empty() {
79		let agg_exprs = collect_aggregate_columns(&sel.columns)?;
80		let by_exprs = emit_expr_comma_list(&sel.group_by)?;
81
82		if !agg_exprs.is_empty() {
83			let mut agg_str = format!("AGGREGATE {{{agg_exprs}}}");
84			if !sel.group_by.is_empty() {
85				agg_str.push_str(&format!(" BY {{{by_exprs}}}"));
86			}
87			parts.push(agg_str);
88		}
89
90		// HAVING
91		if let Some(ref having) = sel.having {
92			parts.push(format!("FILTER {{{}}}", emit_expr(having)?));
93		}
94
95		// MAP only non-aggregate columns that aren't already in GROUP BY
96		let map_exprs = collect_non_aggregate_map_columns(sel)?;
97		if !map_exprs.is_empty() {
98			parts.push(format!("MAP {{{map_exprs}}}"));
99		}
100	} else {
101		// DISTINCT
102		if sel.distinct {
103			if is_all_columns(&sel.columns) {
104				parts.push("DISTINCT {}".into());
105			} else {
106				let cols = emit_select_columns_plain(&sel.columns)?;
107				parts.push(format!("DISTINCT {{{cols}}}"));
108			}
109		}
110		// MAP (column projection) — only if not SELECT *
111		else if !is_all_columns(&sel.columns) {
112			let cols = emit_select_columns(&sel.columns)?;
113			parts.push(format!("MAP {{{cols}}}"));
114		}
115	}
116
117	// SORT (ORDER BY)
118	if !sel.order_by.is_empty() {
119		let sort = emit_order_by(&sel.order_by)?;
120		parts.push(format!("SORT {{{sort}}}"));
121	}
122
123	// TAKE (LIMIT)
124	if let Some(limit) = sel.limit {
125		parts.push(format!("TAKE {limit}"));
126	}
127
128	// OFFSET
129	if let Some(offset) = sel.offset {
130		parts.push(format!("OFFSET {offset}"));
131	}
132
133	let base = parts.join(" ");
134
135	// Handle set operations: UNION / INTERSECT / EXCEPT
136	if let Some((op, right)) = &sel.set_op {
137		let right_rql = emit_select_inner(right, cte_names)?;
138		let op_str = match op {
139			SetOp::Union => "UNION",
140			SetOp::UnionAll => "UNION ALL",
141			SetOp::Intersect => "INTERSECT",
142			SetOp::Except => "EXCEPT",
143		};
144		return Ok(format!("{op_str} {{{base}}} {{{right_rql}}}"));
145	}
146
147	Ok(base)
148}
149
150fn emit_select_no_from(sel: &SelectStatement) -> Result<String, Error> {
151	let cols = emit_select_columns(&sel.columns)?;
152	Ok(format!("MAP {{{cols}}}"))
153}
154
155fn emit_from_clause(from: &FromClause, cte_names: &HashSet<String>) -> Result<String, Error> {
156	match from {
157		FromClause::Table {
158			name,
159			shape,
160			alias,
161		} => {
162			if shape.is_none() && cte_names.contains(&name.to_ascii_lowercase()) {
163				let base = format!("FROM ${}", name.to_ascii_lowercase());
164				if let Some(alias) = alias {
165					Ok(format!("{base} AS {alias}"))
166				} else {
167					Ok(base)
168				}
169			} else if let Some(shape) = shape {
170				let base = format!("FROM {shape}::{name}");
171				if let Some(alias) = alias {
172					Ok(format!("{base} AS {alias}"))
173				} else {
174					Ok(base)
175				}
176			} else {
177				let base = format!("FROM {name}");
178				if let Some(alias) = alias {
179					Ok(format!("{base} AS {alias}"))
180				} else {
181					Ok(base)
182				}
183			}
184		}
185		FromClause::Subquery(sel) => {
186			let inner = emit_select(sel)?;
187			Ok(format!("FROM {{{inner}}}"))
188		}
189	}
190}
191
192fn emit_join(join: &JoinClause, cte_names: &HashSet<String>) -> Result<String, Error> {
193	let join_kw = match join.join_type {
194		JoinType::Inner => "JOIN",
195		JoinType::Left => "LEFT JOIN",
196		JoinType::Cross => "CROSS JOIN",
197	};
198
199	let table_name = match &join.table {
200		FromClause::Table {
201			name,
202			shape,
203			..
204		} => {
205			if shape.is_none() && cte_names.contains(&name.to_ascii_lowercase()) {
206				format!("${}", name.to_ascii_lowercase())
207			} else if let Some(shape) = shape {
208				format!("{shape}::{name}")
209			} else {
210				name.clone()
211			}
212		}
213		FromClause::Subquery(sel) => emit_select(sel)?,
214	};
215
216	let alias = join
217		.table_alias
218		.as_deref()
219		.or(match &join.table {
220			FromClause::Table {
221				name,
222				..
223			} => Some(name.as_str()),
224			_ => None,
225		})
226		.unwrap_or("_");
227
228	// For CROSS JOIN, no USING clause
229	if matches!(join.join_type, JoinType::Cross) {
230		return Ok(format!("{join_kw} {{FROM {table_name}}} AS {alias}"));
231	}
232
233	// Extract USING columns from the ON condition
234	let using = emit_join_using(&join.on, alias)?;
235
236	Ok(format!("{join_kw} {{FROM {table_name}}} AS {alias} USING ({using})"))
237}
238
239/// Convert a JOIN ON condition like `t1.a = t2.b` to USING `(a, t2.b)` format.
240fn emit_join_using(on_expr: &Expr, right_alias: &str) -> Result<String, Error> {
241	match on_expr {
242		Expr::BinaryOp {
243			left,
244			op: BinaryOp::Eq,
245			right,
246		} => {
247			let (left_col, right_col) = extract_join_columns(left, right, right_alias)?;
248			Ok(format!("{left_col}, {right_col}"))
249		}
250		Expr::BinaryOp {
251			left,
252			op: BinaryOp::And,
253			right,
254		} => {
255			let l = emit_join_using(left, right_alias)?;
256			let r = emit_join_using(right, right_alias)?;
257			Ok(format!("{l}, {r}"))
258		}
259		_ => {
260			// Fallback: emit as a filter-like expression
261			emit_expr(on_expr)
262		}
263	}
264}
265
266fn extract_join_columns(left: &Expr, right: &Expr, right_alias: &str) -> Result<(String, String), Error> {
267	let left_col = match left {
268		Expr::QualifiedIdentifier(_table, col) => col.clone(),
269		Expr::Identifier(col) => col.clone(),
270		_ => emit_expr(left)?,
271	};
272	let right_col = match right {
273		Expr::QualifiedIdentifier(table, col) => format!("{table}.{col}"),
274		Expr::Identifier(col) => format!("{right_alias}.{col}"),
275		_ => emit_expr(right)?,
276	};
277	Ok((left_col, right_col))
278}
279
280fn emit_insert(ins: &InsertStatement) -> Result<String, Error> {
281	let table = if let Some(ref shape) = ins.shape {
282		format!("{shape}::{}", ins.table)
283	} else {
284		ins.table.clone()
285	};
286
287	match &ins.source {
288		InsertSource::Values(values) => {
289			let mut rows = Vec::new();
290			for row_values in values {
291				if ins.columns.is_empty() {
292					// No column names — emit positional tuple
293					let vals: Result<Vec<_>, _> = row_values.iter().map(emit_expr).collect();
294					rows.push(format!("({})", vals?.join(", ")));
295				} else {
296					// Named columns — emit record
297					let mut fields = Vec::new();
298					for (i, val) in row_values.iter().enumerate() {
299						let col_name = if i < ins.columns.len() {
300							&ins.columns[i]
301						} else {
302							return Err(Error("more values than columns in INSERT".into()));
303						};
304						fields.push(format!("{}: {}", col_name, emit_expr(val)?));
305					}
306					rows.push(format!("{{{}}}", fields.join(", ")));
307				}
308			}
309			Ok(format!("INSERT {} [{}]", table, rows.join(", ")))
310		}
311		InsertSource::Select(sel) => {
312			let select_rql = emit_select(sel)?;
313			if ins.columns.is_empty() {
314				Ok(format!("INSERT {table} {{{select_rql}}}"))
315			} else {
316				let cols = ins.columns.join(", ");
317				Ok(format!("INSERT {table} ({cols}) {{{select_rql}}}"))
318			}
319		}
320	}
321}
322
323fn emit_update(upd: &UpdateStatement) -> Result<String, Error> {
324	let table = if let Some(ref shape) = upd.shape {
325		format!("{shape}::{}", upd.table)
326	} else {
327		upd.table.clone()
328	};
329
330	let mut assignments = Vec::new();
331	for (col, val) in &upd.assignments {
332		assignments.push(format!("{}: {}", col, emit_expr(val)?));
333	}
334
335	let mut result = format!("UPDATE {} {{{}}}", table, assignments.join(", "));
336
337	if let Some(ref where_clause) = upd.where_clause {
338		result.push_str(&format!(" FILTER {{{}}}", emit_expr(where_clause)?));
339	}
340
341	Ok(result)
342}
343
344fn emit_delete(del: &DeleteStatement) -> Result<String, Error> {
345	let table = if let Some(ref shape) = del.shape {
346		format!("{shape}::{}", del.table)
347	} else {
348		del.table.clone()
349	};
350
351	let mut result = format!("DELETE {table}");
352
353	if let Some(ref where_clause) = del.where_clause {
354		result.push_str(&format!(" FILTER {{{}}}", emit_expr(where_clause)?));
355	}
356
357	Ok(result)
358}
359
360fn emit_create_table(ct: &CreateTableStatement) -> Result<String, Error> {
361	let table = if let Some(ref shape) = ct.shape {
362		format!("{shape}::{}", ct.table)
363	} else {
364		ct.table.clone()
365	};
366
367	let mut cols = Vec::new();
368	for col in &ct.columns {
369		let ty = emit_rql_type(&col.data_type);
370		if col.nullable {
371			cols.push(format!("{}: Option({})", col.name, ty));
372		} else {
373			cols.push(format!("{}: {}", col.name, ty));
374		}
375	}
376
377	let if_ne = if ct.if_not_exists {
378		" IF NOT EXISTS"
379	} else {
380		""
381	};
382	let mut result = format!("CREATE TABLE{if_ne} {} {{{}}}", table, cols.join(", "));
383
384	if !ct.primary_key.is_empty() {
385		result.push_str(&format!("; CREATE PRIMARY KEY ON {} {{{}}}", table, ct.primary_key.join(", ")));
386	}
387
388	Ok(result)
389}
390
391fn emit_create_index(ci: &CreateIndexStatement) -> Result<String, Error> {
392	let unique = if ci.unique {
393		"UNIQUE "
394	} else {
395		""
396	};
397	let table = if let Some(ref shape) = ci.shape {
398		format!("{shape}::{}", ci.table)
399	} else {
400		ci.table.clone()
401	};
402
403	let mut col_parts = Vec::new();
404	for col in &ci.columns {
405		match &col.direction {
406			Some(OrderDirection::Desc) => col_parts.push(format!("{}:desc", col.name)),
407			Some(OrderDirection::Asc) => col_parts.push(format!("{}:asc", col.name)),
408			None => col_parts.push(col.name.clone()),
409		}
410	}
411
412	Ok(format!("CREATE {unique}INDEX {} ON {table} {{{}}}", ci.index_name, col_parts.join(", ")))
413}
414
415fn emit_drop_table(dt: &DropTableStatement) -> Result<String, Error> {
416	let table = if let Some(ref shape) = dt.shape {
417		format!("{shape}::{}", dt.table)
418	} else {
419		dt.table.clone()
420	};
421
422	let if_exists = if dt.if_exists {
423		" IF EXISTS"
424	} else {
425		""
426	};
427	Ok(format!("DROP TABLE{if_exists} {table}"))
428}
429
430fn emit_rql_type(ty: &SqlType) -> &'static str {
431	match ty {
432		SqlType::Int | SqlType::Int4 | SqlType::Integer => "int4",
433		SqlType::Int2 | SqlType::Smallint => "int2",
434		SqlType::Int8 | SqlType::Bigint => "int8",
435		SqlType::Float4 | SqlType::Real => "float4",
436		SqlType::Float8 | SqlType::Double | SqlType::FloatType | SqlType::Numeric => "float8",
437		SqlType::Boolean | SqlType::Bool => "bool",
438		SqlType::Varchar(_) | SqlType::Char(_) | SqlType::Text | SqlType::Utf8 => "utf8",
439		SqlType::Blob => "blob",
440	}
441}
442
443fn emit_expr(expr: &Expr) -> Result<String, Error> {
444	match expr {
445		Expr::Identifier(name) => Ok(name.clone()),
446		Expr::QualifiedIdentifier(table, col) => Ok(format!("{table}.{col}")),
447		Expr::IntegerLiteral(n) => Ok(n.to_string()),
448		Expr::FloatLiteral(f) => Ok(format_float(*f)),
449		Expr::StringLiteral(s) => Ok(format!("'{s}'")),
450		Expr::BoolLiteral(b) => Ok(if *b {
451			"true"
452		} else {
453			"false"
454		}
455		.into()),
456		Expr::Null => Ok("none".into()),
457		Expr::BinaryOp {
458			left,
459			op,
460			right,
461		} => {
462			let l = emit_expr(left)?;
463			let r = emit_expr(right)?;
464			match op {
465				BinaryOp::Concat => Ok(format!("text::concat({l}, {r})")),
466				_ => {
467					let op_str = match op {
468						BinaryOp::Eq => "==",
469						BinaryOp::NotEq => "!=",
470						BinaryOp::Lt => "<",
471						BinaryOp::Gt => ">",
472						BinaryOp::LtEq => "<=",
473						BinaryOp::GtEq => ">=",
474						BinaryOp::And => "and",
475						BinaryOp::Or => "or",
476						BinaryOp::Add => "+",
477						BinaryOp::Sub => "-",
478						BinaryOp::Mul => "*",
479						BinaryOp::Div => "/",
480						BinaryOp::Mod => "%",
481						BinaryOp::Concat => unreachable!(),
482					};
483					Ok(format!("{l} {op_str} {r}"))
484				}
485			}
486		}
487		Expr::UnaryOp {
488			op,
489			expr,
490		} => {
491			let e = emit_expr(expr)?;
492			match op {
493				UnaryOp::Not => Ok(format!("not {e}")),
494				UnaryOp::Neg => Ok(format!("-{e}")),
495			}
496		}
497		Expr::FunctionCall {
498			name,
499			args,
500		} => {
501			let func_name = sql_to_rql_function(name)?;
502			let arg_strs: Result<Vec<_>, _> = args.iter().map(emit_expr).collect();
503			let args_str = arg_strs?.join(", ");
504			Ok(format!("{func_name}({args_str})"))
505		}
506		Expr::Between {
507			expr,
508			low,
509			high,
510			negated,
511		} => {
512			let e = emit_expr(expr)?;
513			let l = emit_expr(low)?;
514			let h = emit_expr(high)?;
515			if *negated {
516				Ok(format!("not ({e} between {l} and {h})"))
517			} else {
518				Ok(format!("{e} between {l} and {h}"))
519			}
520		}
521		Expr::InList {
522			expr,
523			list,
524			negated,
525		} => {
526			let e = emit_expr(expr)?;
527			let items: Result<Vec<_>, _> = list.iter().map(emit_expr).collect();
528			let items_str = items?.join(", ");
529			if *negated {
530				Ok(format!("not ({e} in ({items_str}))"))
531			} else {
532				Ok(format!("{e} in ({items_str})"))
533			}
534		}
535		Expr::InSelect {
536			expr,
537			subquery,
538			negated,
539		} => {
540			let e = emit_expr(expr)?;
541			let sub = emit_select(subquery)?;
542			if *negated {
543				Ok(format!("not ({e} in ({{{sub}}}))"))
544			} else {
545				Ok(format!("{e} in ({{{sub}}})"))
546			}
547		}
548		Expr::IsNull {
549			expr,
550			negated,
551		} => {
552			let e = emit_expr(expr)?;
553			if *negated {
554				Ok(format!("{e} != none"))
555			} else {
556				Ok(format!("{e} == none"))
557			}
558		}
559		Expr::Cast {
560			expr,
561			data_type,
562		} => {
563			let e = emit_expr(expr)?;
564			let ty = emit_rql_type(data_type);
565			Ok(format!("cast({e}, {ty})"))
566		}
567		Expr::Nested(inner) => {
568			let e = emit_expr(inner)?;
569			Ok(format!("({e})"))
570		}
571		Expr::Case {
572			operand,
573			when_clauses,
574			else_clause,
575		} => emit_case(operand, when_clauses, else_clause),
576		Expr::Exists(sel) => {
577			let inner = emit_select(sel)?;
578			Ok(format!("exists({{{inner}}})"))
579		}
580		Expr::Subquery(sel) => {
581			let inner = emit_select(sel)?;
582			Ok(format!("{{{inner}}}"))
583		}
584		Expr::Like {
585			expr,
586			pattern,
587			negated,
588		} => {
589			let e = emit_expr(expr)?;
590			let p = emit_expr(pattern)?;
591			if *negated {
592				Ok(format!("not ({e} like {p})"))
593			} else {
594				Ok(format!("{e} like {p}"))
595			}
596		}
597	}
598}
599
600fn emit_case(
601	operand: &Option<Box<Expr>>,
602	when_clauses: &[(Expr, Expr)],
603	else_clause: &Option<Box<Expr>>,
604) -> Result<String, Error> {
605	let mut parts = Vec::new();
606
607	for (i, (condition, result)) in when_clauses.iter().enumerate() {
608		let cond_str = if let Some(op) = operand {
609			let op_str = emit_expr(op)?;
610			let val_str = emit_expr(condition)?;
611			format!("{op_str} == {val_str}")
612		} else {
613			emit_expr(condition)?
614		};
615		let result_str = emit_expr(result)?;
616
617		if i == 0 {
618			parts.push(format!("if {cond_str} {{ {result_str} }}"));
619		} else {
620			parts.push(format!("else if {cond_str} {{ {result_str} }}"));
621		}
622	}
623
624	if let Some(else_expr) = else_clause {
625		let else_str = emit_expr(else_expr)?;
626		parts.push(format!("else {{ {else_str} }}"));
627	}
628
629	Ok(parts.join(" "))
630}
631
632fn format_float(f: f64) -> String {
633	let s = f.to_string();
634	if s.contains('.') {
635		s
636	} else {
637		format!("{s}.0")
638	}
639}
640
641fn is_all_columns(cols: &[SelectColumn]) -> bool {
642	cols.len() == 1 && matches!(cols[0], SelectColumn::AllColumns)
643}
644
645fn has_aggregate_functions(cols: &[SelectColumn]) -> bool {
646	cols.iter().any(|c| match c {
647		SelectColumn::Expr {
648			expr,
649			..
650		} => expr_has_aggregate(expr),
651		_ => false,
652	})
653}
654
655fn expr_has_aggregate(expr: &Expr) -> bool {
656	match expr {
657		Expr::FunctionCall {
658			name,
659			..
660		} => {
661			let upper = name.to_uppercase();
662			// Check for aggregate function names (with or without _DISTINCT suffix)
663			let base = upper.strip_suffix("_DISTINCT").unwrap_or(&upper);
664			matches!(
665				sql_to_rql_function(base),
666				Ok("math::count" | "math::sum" | "math::avg" | "math::min" | "math::max")
667			)
668		}
669		Expr::BinaryOp {
670			left,
671			right,
672			..
673		} => expr_has_aggregate(left) || expr_has_aggregate(right),
674		Expr::UnaryOp {
675			expr,
676			..
677		} => expr_has_aggregate(expr),
678		Expr::Nested(inner) => expr_has_aggregate(inner),
679		Expr::Case {
680			when_clauses,
681			else_clause,
682			..
683		} => {
684			when_clauses.iter().any(|(c, r)| expr_has_aggregate(c) || expr_has_aggregate(r))
685				|| else_clause.as_ref().is_some_and(|e| expr_has_aggregate(e))
686		}
687		_ => false,
688	}
689}
690
691fn collect_aggregate_columns(cols: &[SelectColumn]) -> Result<String, Error> {
692	let mut agg_exprs = Vec::new();
693	for col in cols {
694		if let SelectColumn::Expr {
695			expr,
696			alias,
697		} = col && expr_has_aggregate(expr)
698		{
699			let e = emit_expr(expr)?;
700			if let Some(alias) = alias {
701				agg_exprs.push(format!("{alias}: {e}"));
702			} else {
703				agg_exprs.push(e);
704			}
705		}
706	}
707	Ok(agg_exprs.join(", "))
708}
709
710fn collect_non_aggregate_map_columns(sel: &SelectStatement) -> Result<String, Error> {
711	let mut map_exprs = Vec::new();
712	for col in &sel.columns {
713		if let SelectColumn::Expr {
714			expr,
715			alias,
716		} = col && !expr_has_aggregate(expr)
717		{
718			// Check if this column is already in GROUP BY
719			let is_in_group_by = sel.group_by.iter().any(|gb| expr_eq(gb, expr));
720			if !is_in_group_by {
721				let e = emit_expr(expr)?;
722				if let Some(alias) = alias {
723					map_exprs.push(format!("{alias}: {e}"));
724				} else {
725					map_exprs.push(e);
726				}
727			}
728		}
729	}
730	Ok(map_exprs.join(", "))
731}
732
733fn emit_select_columns(cols: &[SelectColumn]) -> Result<String, Error> {
734	let mut parts = Vec::new();
735	for col in cols {
736		match col {
737			SelectColumn::AllColumns => parts.push("*".into()),
738			SelectColumn::Expr {
739				expr,
740				alias,
741			} => {
742				let e = emit_expr(expr)?;
743				if let Some(alias) = alias {
744					parts.push(format!("{alias}: {e}"));
745				} else {
746					parts.push(e);
747				}
748			}
749		}
750	}
751	Ok(parts.join(", "))
752}
753
754fn emit_select_columns_plain(cols: &[SelectColumn]) -> Result<String, Error> {
755	let mut parts = Vec::new();
756	for col in cols {
757		match col {
758			SelectColumn::AllColumns => parts.push("*".into()),
759			SelectColumn::Expr {
760				expr,
761				..
762			} => {
763				parts.push(emit_expr(expr)?);
764			}
765		}
766	}
767	Ok(parts.join(", "))
768}
769
770fn sql_to_rql_function(name: &str) -> Result<&'static str, Error> {
771	match name.to_uppercase().as_str() {
772		// Aggregates
773		"COUNT" | "COUNT_DISTINCT" => Ok("math::count"),
774		"SUM" | "SUM_DISTINCT" => Ok("math::sum"),
775		"AVG" | "AVG_DISTINCT" => Ok("math::avg"),
776		"MIN" | "MIN_DISTINCT" => Ok("math::min"),
777		"MAX" | "MAX_DISTINCT" => Ok("math::max"),
778		"TOTAL" => Ok("math::sum"),
779		// Math scalar
780		"ABS" => Ok("math::abs"),
781		"ACOS" => Ok("math::acos"),
782		"ASIN" => Ok("math::asin"),
783		"ATAN" => Ok("math::atan"),
784		"ATAN2" => Ok("math::atan2"),
785		"CEIL" | "CEILING" => Ok("math::ceil"),
786		"COS" => Ok("math::cos"),
787		"EXP" => Ok("math::exp"),
788		"FLOOR" => Ok("math::floor"),
789		"GCD" => Ok("math::gcd"),
790		"LCM" => Ok("math::lcm"),
791		"LOG" => Ok("math::log"),
792		"LOG10" => Ok("math::log10"),
793		"LOG2" => Ok("math::log2"),
794		"MOD" => Ok("math::mod"),
795		"PI" => Ok("math::pi"),
796		"POWER" | "POW" => Ok("math::power"),
797		"ROUND" => Ok("math::round"),
798		"SIGN" => Ok("math::sign"),
799		"SIN" => Ok("math::sin"),
800		"SQRT" => Ok("math::sqrt"),
801		"TAN" => Ok("math::tan"),
802		"TRUNCATE" | "TRUNC" => Ok("math::truncate"),
803		"RANDOM" => Ok("math::random"),
804		// Text
805		"ASCII" => Ok("text::ascii"),
806		"CHAR" | "CHR" => Ok("text::char"),
807		"CONCAT" => Ok("text::concat"),
808		"LENGTH" | "LEN" | "CHAR_LENGTH" | "CHARACTER_LENGTH" => Ok("text::length"),
809		"LOWER" | "LCASE" => Ok("text::lower"),
810		"LPAD" => Ok("text::pad_left"),
811		"REPEAT" => Ok("text::repeat"),
812		"REPLACE" => Ok("text::replace"),
813		"REVERSE" => Ok("text::reverse"),
814		"RPAD" => Ok("text::pad_right"),
815		"SUBSTRING" | "SUBSTR" => Ok("text::substring"),
816		"TRIM" => Ok("text::trim"),
817		"LTRIM" => Ok("text::trim_start"),
818		"RTRIM" => Ok("text::trim_end"),
819		"UPPER" | "UCASE" => Ok("text::upper"),
820		"TYPEOF" => Ok("type::of"),
821		"UNICODE" => Ok("text::unicode"),
822		"INSTR" => Ok("text::instr"),
823		"HEX" => Ok("text::hex"),
824		"QUOTE" => Ok("text::quote"),
825		"ZEROBLOB" => Ok("blob::zeroblob"),
826		"GROUP_CONCAT" => Ok("text::group_concat"),
827		// COALESCE and NULLIF handled as regular function calls
828		"COALESCE" => Ok("coalesce"),
829		"NULLIF" => Ok("nullif"),
830		"IIF" => Ok("iif"),
831		"IFNULL" => Ok("ifnull"),
832		"PRINTF" => Ok("text::printf"),
833		_ => Err(Error(format!("no SQL-to-RQL mapping for function: {name}"))),
834	}
835}
836
837fn emit_order_by(items: &[OrderByItem]) -> Result<String, Error> {
838	let mut parts = Vec::new();
839	for item in items {
840		let e = emit_expr(&item.expr)?;
841		match item.direction {
842			OrderDirection::Asc => parts.push(format!("{e}:asc")),
843			OrderDirection::Desc => parts.push(format!("{e}:desc")),
844		};
845	}
846	Ok(parts.join(", "))
847}
848
849fn emit_expr_comma_list(exprs: &[Expr]) -> Result<String, Error> {
850	let parts: Result<Vec<_>, _> = exprs.iter().map(emit_expr).collect();
851	Ok(parts?.join(", "))
852}
853
854/// Simple structural equality check for expressions (for GROUP BY dedup).
855fn expr_eq(a: &Expr, b: &Expr) -> bool {
856	match (a, b) {
857		(Expr::Identifier(a), Expr::Identifier(b)) => a == b,
858		(Expr::QualifiedIdentifier(at, ac), Expr::QualifiedIdentifier(bt, bc)) => at == bt && ac == bc,
859		_ => false,
860	}
861}
862
863#[cfg(test)]
864mod tests {
865	use super::*;
866	use crate::{parser::Parser, token::tokenize};
867
868	fn transpile(sql: &str) -> String {
869		let tokens = tokenize(sql).unwrap();
870		let stmt = Parser::new(tokens).parse().unwrap();
871		emit(&stmt).unwrap()
872	}
873
874	#[test]
875	fn test_select_star() {
876		assert_eq!(transpile("SELECT * FROM users"), "FROM users");
877	}
878
879	#[test]
880	fn test_select_columns() {
881		assert_eq!(transpile("SELECT id, name FROM users"), "FROM users MAP {id, name}");
882	}
883
884	#[test]
885	fn test_select_alias() {
886		assert_eq!(transpile("SELECT id, name AS username FROM users"), "FROM users MAP {id, username: name}");
887	}
888
889	#[test]
890	fn test_where() {
891		assert_eq!(transpile("SELECT * FROM users WHERE age > 18"), "FROM users FILTER {age > 18}");
892	}
893
894	#[test]
895	fn test_order_by() {
896		assert_eq!(transpile("SELECT * FROM users ORDER BY name ASC"), "FROM users SORT {name:asc}");
897	}
898
899	#[test]
900	fn test_limit() {
901		assert_eq!(transpile("SELECT * FROM users LIMIT 10"), "FROM users TAKE 10");
902	}
903
904	#[test]
905	fn test_insert() {
906		assert_eq!(
907			transpile("INSERT INTO users (id, name) VALUES (1, 'Alice')"),
908			"INSERT users [{id: 1, name: 'Alice'}]"
909		);
910	}
911
912	#[test]
913	fn test_update() {
914		assert_eq!(
915			transpile("UPDATE users SET name = 'Bob' WHERE id = 1"),
916			"UPDATE users {name: 'Bob'} FILTER {id == 1}"
917		);
918	}
919
920	#[test]
921	fn test_delete() {
922		assert_eq!(transpile("DELETE FROM users WHERE id = 1"), "DELETE users FILTER {id == 1}");
923	}
924
925	#[test]
926	fn test_create_table() {
927		assert_eq!(
928			transpile("CREATE TABLE users (id INT, name TEXT, active BOOLEAN)"),
929			"CREATE TABLE users {id: Option(int4), name: Option(utf8), active: Option(bool)}"
930		);
931	}
932
933	#[test]
934	fn test_create_table_not_null() {
935		assert_eq!(
936			transpile("CREATE TABLE t (id INT NOT NULL, name TEXT)"),
937			"CREATE TABLE t {id: int4, name: Option(utf8)}"
938		);
939	}
940
941	#[test]
942	fn test_null_to_none() {
943		assert_eq!(transpile("SELECT NULL"), "MAP {none}");
944	}
945
946	#[test]
947	fn test_not_equal() {
948		assert_eq!(transpile("SELECT * FROM t WHERE a <> b"), "FROM t FILTER {a != b}");
949	}
950
951	#[test]
952	fn test_and_or() {
953		assert_eq!(transpile("SELECT * FROM t WHERE a = 1 AND b = 2"), "FROM t FILTER {a == 1 and b == 2}");
954	}
955
956	#[test]
957	fn test_distinct() {
958		assert_eq!(transpile("SELECT DISTINCT name FROM users"), "FROM users DISTINCT {name}");
959	}
960
961	#[test]
962	fn test_group_by_aggregate() {
963		assert_eq!(
964			transpile("SELECT COUNT(id) FROM users GROUP BY dept"),
965			"FROM users AGGREGATE {math::count(id)} BY {dept}"
966		);
967	}
968
969	#[test]
970	fn test_cast() {
971		assert_eq!(transpile("SELECT CAST(x AS INT)"), "MAP {cast(x, int4)}");
972	}
973
974	#[test]
975	fn test_between() {
976		assert_eq!(transpile("SELECT * FROM t WHERE x BETWEEN 1 AND 10"), "FROM t FILTER {x between 1 and 10}");
977	}
978
979	#[test]
980	fn test_in_list() {
981		assert_eq!(transpile("SELECT * FROM t WHERE x IN (1, 2, 3)"), "FROM t FILTER {x in (1, 2, 3)}");
982	}
983
984	#[test]
985	fn test_is_null() {
986		assert_eq!(transpile("SELECT * FROM t WHERE x IS NULL"), "FROM t FILTER {x == none}");
987	}
988
989	#[test]
990	fn test_is_not_null() {
991		assert_eq!(transpile("SELECT * FROM t WHERE x IS NOT NULL"), "FROM t FILTER {x != none}");
992	}
993
994	#[test]
995	fn test_join() {
996		assert_eq!(
997			transpile("SELECT * FROM t1 INNER JOIN t2 ON t1.a = t2.b"),
998			"FROM t1 JOIN {FROM t2} AS t2 USING (a, t2.b)"
999		);
1000	}
1001
1002	#[test]
1003	fn test_left_join() {
1004		assert_eq!(
1005			transpile("SELECT * FROM t1 LEFT JOIN t2 ON t1.a = t2.b"),
1006			"FROM t1 LEFT JOIN {FROM t2} AS t2 USING (a, t2.b)"
1007		);
1008	}
1009
1010	#[test]
1011	fn test_select_no_from() {
1012		assert_eq!(transpile("SELECT 1 + 2"), "MAP {1 + 2}");
1013	}
1014
1015	#[test]
1016	fn test_multiple_insert_rows() {
1017		assert_eq!(
1018			transpile("INSERT INTO t (a, b) VALUES (1, 2), (3, 4)"),
1019			"INSERT t [{a: 1, b: 2}, {a: 3, b: 4}]"
1020		);
1021	}
1022
1023	#[test]
1024	fn test_shape_qualified_table() {
1025		assert_eq!(transpile("SELECT * FROM test.users"), "FROM test::users");
1026	}
1027
1028	#[test]
1029	fn test_simple_cte() {
1030		assert_eq!(
1031			transpile("WITH t AS (SELECT * FROM users WHERE active = true) SELECT * FROM t"),
1032			"LET $t = FROM users FILTER {active == true}; FROM $t"
1033		);
1034	}
1035
1036	#[test]
1037	fn test_insert_without_columns() {
1038		assert_eq!(transpile("INSERT INTO t1 VALUES (1, 'true')"), "INSERT t1 [(1, 'true')]");
1039	}
1040
1041	#[test]
1042	fn test_create_table_primary_key() {
1043		assert_eq!(
1044			transpile("CREATE TABLE t (v1 INT NOT NULL, v2 INT NOT NULL, PRIMARY KEY(v1))"),
1045			"CREATE TABLE t {v1: int4, v2: int4}; CREATE PRIMARY KEY ON t {v1}"
1046		);
1047		assert_eq!(
1048			transpile("CREATE TABLE t (a INT NOT NULL, b INT NOT NULL, PRIMARY KEY(a, b))"),
1049			"CREATE TABLE t {a: int4, b: int4}; CREATE PRIMARY KEY ON t {a, b}"
1050		);
1051	}
1052
1053	#[test]
1054	fn test_multiple_ctes() {
1055		assert_eq!(
1056			transpile("WITH a AS (SELECT * FROM users), b AS (SELECT id FROM a) SELECT * FROM b"),
1057			"LET $a = FROM users; LET $b = FROM $a MAP {id}; FROM $b"
1058		);
1059	}
1060
1061	// CASE expressions
1062	#[test]
1063	fn test_case_when_single() {
1064		assert_eq!(
1065			transpile("SELECT CASE WHEN x > 0 THEN 'pos' END FROM t"),
1066			"FROM t MAP {if x > 0 { 'pos' }}"
1067		);
1068	}
1069
1070	#[test]
1071	fn test_case_when_multiple() {
1072		assert_eq!(
1073			transpile("SELECT CASE WHEN x > 0 THEN 'pos' WHEN x < 0 THEN 'neg' END FROM t"),
1074			"FROM t MAP {if x > 0 { 'pos' } else if x < 0 { 'neg' }}"
1075		);
1076	}
1077
1078	#[test]
1079	fn test_case_when_else() {
1080		assert_eq!(
1081			transpile("SELECT CASE WHEN x > 0 THEN 'pos' ELSE 'non-pos' END FROM t"),
1082			"FROM t MAP {if x > 0 { 'pos' } else { 'non-pos' }}"
1083		);
1084	}
1085
1086	#[test]
1087	fn test_case_simple() {
1088		assert_eq!(
1089			transpile("SELECT CASE x WHEN 1 THEN 'one' WHEN 2 THEN 'two' ELSE 'other' END FROM t"),
1090			"FROM t MAP {if x == 1 { 'one' } else if x == 2 { 'two' } else { 'other' }}"
1091		);
1092	}
1093
1094	#[test]
1095	fn test_case_in_where() {
1096		assert_eq!(
1097			transpile("SELECT * FROM t WHERE CASE WHEN a > 10 THEN 1 ELSE 0 END = 1"),
1098			"FROM t FILTER {if a > 10 { 1 } else { 0 } == 1}"
1099		);
1100	}
1101
1102	#[test]
1103	fn test_case_nested() {
1104		assert_eq!(
1105			transpile(
1106				"SELECT CASE WHEN a > 0 THEN CASE WHEN b > 0 THEN 'pp' ELSE 'pn' END ELSE 'neg' END FROM t"
1107			),
1108			"FROM t MAP {if a > 0 { if b > 0 { 'pp' } else { 'pn' } } else { 'neg' }}"
1109		);
1110	}
1111
1112	#[test]
1113	fn test_case_in_select_projection() {
1114		assert_eq!(
1115			transpile("SELECT id, CASE WHEN active = true THEN 'yes' ELSE 'no' END AS status FROM users"),
1116			"FROM users MAP {id, status: if active == true { 'yes' } else { 'no' }}"
1117		);
1118	}
1119
1120	#[test]
1121	fn test_case_with_aggregate() {
1122		assert_eq!(
1123			transpile("SELECT SUM(CASE WHEN x > 0 THEN 1 ELSE 0 END) FROM t"),
1124			"FROM t AGGREGATE {math::sum(if x > 0 { 1 } else { 0 })}"
1125		);
1126	}
1127
1128	// EXISTS / Subqueries
1129	#[test]
1130	fn test_exists_in_where() {
1131		assert_eq!(
1132			transpile("SELECT * FROM t1 WHERE EXISTS (SELECT 1 FROM t2 WHERE t2.a = t1.a)"),
1133			"FROM t1 FILTER {exists({FROM t2 FILTER {t2.a == t1.a} MAP {1}})}"
1134		);
1135	}
1136
1137	#[test]
1138	fn test_not_exists_in_where() {
1139		assert_eq!(
1140			transpile("SELECT * FROM t1 WHERE NOT EXISTS (SELECT 1 FROM t2 WHERE t2.a = t1.a)"),
1141			"FROM t1 FILTER {not exists({FROM t2 FILTER {t2.a == t1.a} MAP {1}})}"
1142		);
1143	}
1144
1145	#[test]
1146	fn test_scalar_subquery_in_select() {
1147		assert_eq!(
1148			transpile("SELECT (SELECT COUNT(*) FROM t2) FROM t1"),
1149			"FROM t1 MAP {{FROM t2 AGGREGATE {math::count(*)}}}"
1150		);
1151	}
1152
1153	#[test]
1154	fn test_scalar_subquery_in_where() {
1155		assert_eq!(
1156			transpile("SELECT * FROM t1 WHERE a > (SELECT MIN(b) FROM t2)"),
1157			"FROM t1 FILTER {a > {FROM t2 AGGREGATE {math::min(b)}}}"
1158		);
1159	}
1160
1161	#[test]
1162	fn test_in_subquery() {
1163		assert_eq!(
1164			transpile("SELECT * FROM t1 WHERE a IN (SELECT b FROM t2)"),
1165			"FROM t1 FILTER {a in ({FROM t2 MAP {b}})}"
1166		);
1167	}
1168
1169	#[test]
1170	fn test_not_in_subquery() {
1171		assert_eq!(
1172			transpile("SELECT * FROM t1 WHERE a NOT IN (SELECT b FROM t2)"),
1173			"FROM t1 FILTER {not (a in ({FROM t2 MAP {b}}))}"
1174		);
1175	}
1176
1177	// CREATE INDEX
1178	#[test]
1179	fn test_create_index_basic() {
1180		assert_eq!(transpile("CREATE INDEX idx1 ON t1 (a)"), "CREATE INDEX idx1 ON t1 {a}");
1181	}
1182
1183	#[test]
1184	fn test_create_unique_index() {
1185		assert_eq!(transpile("CREATE UNIQUE INDEX idx1 ON t1 (a)"), "CREATE UNIQUE INDEX idx1 ON t1 {a}");
1186	}
1187
1188	#[test]
1189	fn test_create_composite_index() {
1190		assert_eq!(transpile("CREATE INDEX idx1 ON t1 (a, b, c)"), "CREATE INDEX idx1 ON t1 {a, b, c}");
1191	}
1192
1193	#[test]
1194	fn test_create_index_with_direction() {
1195		assert_eq!(
1196			transpile("CREATE INDEX idx1 ON t1 (a DESC, b ASC)"),
1197			"CREATE INDEX idx1 ON t1 {a:desc, b:asc}"
1198		);
1199	}
1200
1201	// DROP TABLE
1202	#[test]
1203	fn test_drop_table() {
1204		assert_eq!(transpile("DROP TABLE t1"), "DROP TABLE t1");
1205	}
1206
1207	#[test]
1208	fn test_drop_table_if_exists() {
1209		assert_eq!(transpile("DROP TABLE IF EXISTS t1"), "DROP TABLE IF EXISTS t1");
1210	}
1211
1212	// INSERT...SELECT
1213	#[test]
1214	fn test_insert_select() {
1215		assert_eq!(transpile("INSERT INTO t1 SELECT * FROM t2"), "INSERT t1 {FROM t2}");
1216	}
1217
1218	#[test]
1219	fn test_insert_select_with_columns() {
1220		assert_eq!(
1221			transpile("INSERT INTO t1 (a, b) SELECT x, y FROM t2"),
1222			"INSERT t1 (a, b) {FROM t2 MAP {x, y}}"
1223		);
1224	}
1225
1226	// LIKE
1227	#[test]
1228	fn test_like_basic() {
1229		assert_eq!(transpile("SELECT * FROM t WHERE name LIKE '%foo%'"), "FROM t FILTER {name like '%foo%'}");
1230	}
1231
1232	#[test]
1233	fn test_not_like() {
1234		assert_eq!(
1235			transpile("SELECT * FROM t WHERE name NOT LIKE '%foo%'"),
1236			"FROM t FILTER {not (name like '%foo%')}"
1237		);
1238	}
1239
1240	#[test]
1241	fn test_like_with_special() {
1242		assert_eq!(transpile("SELECT * FROM t WHERE name LIKE 'a_b%'"), "FROM t FILTER {name like 'a_b%'}");
1243	}
1244
1245	// Column-level PRIMARY KEY
1246	#[test]
1247	fn test_column_primary_key() {
1248		assert_eq!(
1249			transpile("CREATE TABLE t (id INT PRIMARY KEY, name TEXT)"),
1250			"CREATE TABLE t {id: int4, name: Option(utf8)}; CREATE PRIMARY KEY ON t {id}"
1251		);
1252	}
1253
1254	#[test]
1255	fn test_mixed_primary_key() {
1256		assert_eq!(
1257			transpile("CREATE TABLE t (id INT PRIMARY KEY, val INT NOT NULL, PRIMARY KEY(id))"),
1258			"CREATE TABLE t {id: int4, val: int4}; CREATE PRIMARY KEY ON t {id}"
1259		);
1260	}
1261
1262	// Table aliases
1263	#[test]
1264	fn test_from_table_as_alias() {
1265		assert_eq!(transpile("SELECT a.id FROM users AS a"), "FROM users AS a MAP {a.id}");
1266	}
1267
1268	#[test]
1269	fn test_from_table_bare_alias() {
1270		assert_eq!(transpile("SELECT a.id FROM users a"), "FROM users AS a MAP {a.id}");
1271	}
1272
1273	#[test]
1274	fn test_self_join_with_aliases() {
1275		assert_eq!(
1276			transpile("SELECT a.id, b.id FROM t1 AS a INNER JOIN t1 AS b ON a.x = b.y"),
1277			"FROM t1 AS a JOIN {FROM t1} AS b USING (x, b.y) MAP {a.id, b.id}"
1278		);
1279	}
1280
1281	// Multi-table FROM (cross join)
1282	#[test]
1283	fn test_two_table_from() {
1284		assert_eq!(transpile("SELECT * FROM t1, t2"), "FROM t1 CROSS JOIN {FROM t2} AS t2");
1285	}
1286
1287	#[test]
1288	fn test_three_table_from() {
1289		assert_eq!(
1290			transpile("SELECT * FROM t1, t2, t3"),
1291			"FROM t1 CROSS JOIN {FROM t2} AS t2 CROSS JOIN {FROM t3} AS t3"
1292		);
1293	}
1294
1295	// UNION / INTERSECT / EXCEPT
1296	#[test]
1297	fn test_union_all() {
1298		assert_eq!(
1299			transpile("SELECT a FROM t1 UNION ALL SELECT a FROM t2"),
1300			"UNION ALL {FROM t1 MAP {a}} {FROM t2 MAP {a}}"
1301		);
1302	}
1303
1304	#[test]
1305	fn test_union() {
1306		assert_eq!(
1307			transpile("SELECT a FROM t1 UNION SELECT a FROM t2"),
1308			"UNION {FROM t1 MAP {a}} {FROM t2 MAP {a}}"
1309		);
1310	}
1311
1312	#[test]
1313	fn test_intersect() {
1314		assert_eq!(
1315			transpile("SELECT a FROM t1 INTERSECT SELECT a FROM t2"),
1316			"INTERSECT {FROM t1 MAP {a}} {FROM t2 MAP {a}}"
1317		);
1318	}
1319
1320	#[test]
1321	fn test_except() {
1322		assert_eq!(
1323			transpile("SELECT a FROM t1 EXCEPT SELECT a FROM t2"),
1324			"EXCEPT {FROM t1 MAP {a}} {FROM t2 MAP {a}}"
1325		);
1326	}
1327
1328	// String concatenation
1329	#[test]
1330	fn test_concat_operator() {
1331		assert_eq!(transpile("SELECT a || b FROM t"), "FROM t MAP {text::concat(a, b)}");
1332	}
1333
1334	#[test]
1335	fn test_concat_chain() {
1336		assert_eq!(transpile("SELECT a || b || c FROM t"), "FROM t MAP {text::concat(text::concat(a, b), c)}");
1337	}
1338
1339	// COALESCE / NULLIF
1340	#[test]
1341	fn test_coalesce() {
1342		assert_eq!(transpile("SELECT COALESCE(a, b, c) FROM t"), "FROM t MAP {coalesce(a, b, c)}");
1343	}
1344
1345	#[test]
1346	fn test_nullif() {
1347		assert_eq!(transpile("SELECT NULLIF(a, 0) FROM t"), "FROM t MAP {nullif(a, 0)}");
1348	}
1349
1350	// FLOAT type
1351	#[test]
1352	fn test_float_type() {
1353		assert_eq!(transpile("CREATE TABLE t (x FLOAT NOT NULL)"), "CREATE TABLE t {x: float8}");
1354	}
1355
1356	// NUMERIC type
1357	#[test]
1358	fn test_numeric_type() {
1359		assert_eq!(transpile("CREATE TABLE t (x NUMERIC NOT NULL)"), "CREATE TABLE t {x: float8}");
1360	}
1361
1362	// ORDER BY ordinal
1363	#[test]
1364	fn test_order_by_ordinal() {
1365		assert_eq!(transpile("SELECT a, b FROM t ORDER BY 1"), "FROM t MAP {a, b} SORT {1:asc}");
1366	}
1367
1368	#[test]
1369	fn test_order_by_ordinal_desc() {
1370		assert_eq!(
1371			transpile("SELECT a, b FROM t ORDER BY 1, 2 DESC"),
1372			"FROM t MAP {a, b} SORT {1:asc, 2:desc}"
1373		);
1374	}
1375
1376	// IF NOT EXISTS
1377	#[test]
1378	fn test_create_table_if_not_exists() {
1379		assert_eq!(
1380			transpile("CREATE TABLE IF NOT EXISTS t (id INT NOT NULL)"),
1381			"CREATE TABLE IF NOT EXISTS t {id: int4}"
1382		);
1383	}
1384
1385	// Unary plus
1386	#[test]
1387	fn test_unary_plus() {
1388		assert_eq!(transpile("SELECT +1"), "MAP {1}");
1389	}
1390}