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