1use crate::ir::{
21 IrArraySource, IrCteDef, IrDelete, IrExpr, IrFor, IrForIterator, IrFreeExpr, IrFtsSearch, IrFunctionSelect,
22 IrGlobalCte, IrGroup, IrGroupOutput, IrInsert, IrLiteral, IrLockClause, IrLockStrength, IrLockWait,
23 IrMultiLinkJoin, IrMultiLinkMutation, IrMultiLinkPointer, IrMultiLinkValueSource, IrMultiLinkValues, IrNulls,
24 IrOutput, IrPathJoin, IrPathResult, IrPathSelect, IrPolyFanout, IrPolyImplementor, IrRewrite, IrRowSource,
25 IrScalarPointer, IrScalarSetPointer, IrSelect, IrShapePointer, IrSingleLinkCorrelation, IrSingleLinkPointer,
26 IrSort, IrSortDir, IrSource, IrStmt, IrUpdate, IrVectorSearch, SearchEnqueueInfo, VectorEnqueueInfo,
27};
28use crate::parse::ast::{BinOpKind, UnaryOpKind};
29use crate::query::{Cardinality, InferencePlan, ShapeDescriptor, ShapeNode};
30use std::collections::HashMap;
31
32pub struct SqlOutput {
33 pub sql: String,
34 pub shape: ShapeDescriptor,
35 pub inference_plan: Option<InferencePlan>,
36}
37
38fn update_set_fragments(assignments: &[(String, IrExpr)], rewrites: &[IrRewrite], indent: &str) -> Vec<String> {
49 let rewrite_cols: std::collections::HashSet<&str> = rewrites.iter().map(|r| r.column.as_str()).collect();
50 assignments
51 .iter()
52 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
53 .map(|(col, expr)| format!("{indent}{} = {}", qi(col), emit_expr(expr)))
54 .chain(
55 rewrites
56 .iter()
57 .map(|rw| format!("{indent}{} = {}", qi(&rw.column), emit_expr(&rw.expr))),
58 )
59 .collect()
60}
61
62fn emit_for_global_cte(stmt: &IrStmt) -> String {
64 match stmt {
65 IrStmt::Select(sel) => match sel.rows.as_slice() {
66 [IrRowSource::Bound { source, .. }] => {
67 let alias = &source.alias;
68 let mut sql = format!(
69 "SELECT {}.\"id\" AS \"value\"\nFROM {} AS {}",
70 qi(alias),
71 source_ref(source),
72 qi(alias)
73 );
74 append_filter(&mut sql, &sel.filter);
75 sql
76 }
77 rows => match rows.first() {
78 Some(IrRowSource::Free(IrFreeExpr::Scalar(e))) => format!("SELECT {} AS \"value\"", emit_expr(e)),
79 _ => "SELECT NULL AS \"value\"".to_string(),
80 },
81 },
82 IrStmt::PathSelect(sel) => {
83 let from_sql = emit_path_joins(&sel.root, &sel.joins);
84 let scalar_expr = match &sel.result {
85 IrPathResult::Scalar(e, _) => emit_expr(e),
86 IrPathResult::Object { alias, .. } => format!("{}.\"id\"", qi(alias)),
87 };
88 let mut sql = format!("SELECT {} AS \"value\"\nFROM {}", scalar_expr, from_sql);
89 append_filter(&mut sql, &sel.filter);
90 append_order_by(&mut sql, &sel.order_by);
91 append_offset_limit(&mut sql, &sel.offset, &sel.limit);
92 sql
93 }
94 _ => "SELECT NULL AS \"value\"".to_string(),
95 }
96}
97
98fn emit_global_cte_parts(global_ctes: &[IrGlobalCte]) -> Vec<String> {
99 global_ctes
100 .iter()
101 .map(|g| match g {
102 IrGlobalCte::Session(s) => format!(
103 "\"{}\" AS (SELECT ${}::{} AS \"value\")",
104 s.cte_name,
105 s.param_index + 1,
106 s.pg_type
107 ),
108 IrGlobalCte::Computed(c) => {
109 let body = emit_for_global_cte(&c.stmt);
110 format!("\"{}\" AS (\n{}\n)", c.cte_name, body)
111 }
112 })
113 .collect()
114}
115
116pub fn emit(ir: &IrOutput) -> SqlOutput {
117 let out = with_subtype_fanouts(&ir.subtype_fanouts, || {
118 with_correlated_ctes(&ir.ctes, || emit_output(ir))
119 });
120 #[cfg(debug_assertions)]
121 if let Some(problem) = forward_cte_reference(&out.sql) {
122 panic!("{problem}\n{}", out.sql);
123 }
124 out
125}
126
127#[cfg(debug_assertions)]
133fn forward_cte_reference(sql: &str) -> Option<String> {
134 let definitions: Vec<(String, usize)> = sql
135 .match_indices("\" AS (")
136 .filter_map(|(end, _)| {
137 let head = &sql[..end];
138 let start = head.rfind('"')?;
139 Some((head[start + 1..].to_string(), start))
140 })
141 .collect();
142 for (name, defined_at) in &definitions {
143 let quoted = qi(name);
146 for prefix in ["FROM ", "JOIN ", "CROSS JOIN "] {
147 let needle = format!("{prefix}{quoted}");
148 if let Some(used_at) = sql.find(&needle)
149 && used_at < *defined_at
150 {
151 return Some(format!(
152 "CTE {quoted} is read at byte {used_at} but not defined until {defined_at}"
153 ));
154 }
155 }
156 }
157 None
158}
159
160fn emit_output(ir: &IrOutput) -> SqlOutput {
161 let mut out = match &ir.stmt {
162 IrStmt::Update(upd) => emit_update_stmt(upd, &ir.ctes),
163 IrStmt::For(f) => emit_for_stmt(f, &ir.ctes),
164 stmt => {
165 let mut o = match stmt {
166 IrStmt::Select(sel) => emit_select_stmt(sel, &ir.ctes),
167 IrStmt::PathSelect(sel) => emit_path_select(sel),
168 IrStmt::Insert(ins) => emit_insert_stmt(ins),
169 IrStmt::Delete(del) => emit_delete_stmt(del),
170 IrStmt::Group(grp) => emit_group(grp),
171 IrStmt::FunctionSelect(sel) => emit_function_select(sel),
172 IrStmt::VectorSearch(vs) => emit_vector_search(vs),
173 IrStmt::FtsSearch(fs) => emit_fts_search(fs),
174 IrStmt::ScalarUnion(branches) => SqlOutput {
175 sql: emit_scalar_union(branches),
176 shape: ShapeDescriptor {
177 root: ShapeNode::Scalar {
178 name: String::new(),
179 position: 0,
180 },
181 },
182 inference_plan: None,
183 },
184 IrStmt::Update(_) | IrStmt::For(_) => unreachable!(),
185 };
186 if !ir.ctes.is_empty() {
187 o.sql = merge_into_existing_with(&o.sql, &emit_user_cte_parts(&ir.ctes))
191 .unwrap_or_else(|| format!("{}{}", emit_cte_prefix(&ir.ctes), o.sql));
192 }
193 o
194 }
195 };
196
197 if !ir.global_ctes.is_empty() {
198 let global_parts = emit_global_cte_parts(&ir.global_ctes);
199 out.sql = merge_into_existing_with(&out.sql, &global_parts)
200 .unwrap_or_else(|| format!("WITH {}\n{}", global_parts.join(",\n "), out.sql));
201 }
202
203 out
204}
205
206fn unwrap_unnest_for_any(right: &IrExpr, rendered: &str) -> String {
239 if let Some(array) = unnest_argument(right) {
240 return array;
241 }
242 if let IrExpr::TypeCast(cast) = right
245 && let Some(array) = unnest_argument(&cast.expr)
246 {
247 return format!("({})::{}[]", array, cast.pg_type);
248 }
249 rendered.to_string()
250}
251
252fn unnest_argument(expr: &IrExpr) -> Option<String> {
254 let IrExpr::FunctionCall(call) = expr else {
255 return None;
256 };
257 (call.schema.is_none() && call.name == "unnest" && call.sql_template.is_none() && call.args.len() == 1)
258 .then(|| emit_expr(&call.args[0]))
259}
260
261fn merge_into_existing_with(sql: &str, parts: &[String]) -> Option<String> {
262 if parts.is_empty() || !(sql.starts_with("WITH\n") || sql.starts_with("WITH ")) {
263 return None;
264 }
265 Some(format!("WITH {},\n {}", parts.join(",\n "), &sql[5..]))
266}
267
268fn qi(s: &str) -> String {
271 format!("\"{}\"", s.replace('"', "\"\""))
272}
273
274fn pg_schema(module: &str) -> String {
275 if module == "default" {
276 "\"public\"".into()
277 } else {
278 qi(module)
279 }
280}
281
282pub fn pg_schema_str(module: &str) -> String {
283 pg_schema(module)
284}
285
286fn qn(module: &str, name: &str) -> String {
287 format!("{}.{}", pg_schema(module), qi(name))
288}
289
290fn sql_str(s: &str) -> String {
291 format!("'{}'", s.replace('\'', "''"))
292}
293
294fn type_disc(type_name: &str) -> String {
296 format!("{}::text", sql_str(type_name))
297}
298
299fn module_of(type_name: &str) -> &str {
301 type_name.split("::").next().unwrap_or("public")
302}
303
304fn source_ref(src: &IrSource) -> String {
305 if let Some(cte_name) = src.table.strip_prefix("@cte:") {
307 return match correlated_cte_iterator(cte_name) {
311 Some(iterator) => format!(
312 "(SELECT * FROM {} WHERE {}.{} = {}.\"v\")",
313 qi(cte_name),
314 qi(cte_name),
315 qi(OUTER_KEY),
316 qi(&iterator),
317 ),
318 None => qi(cte_name),
319 };
320 }
321 if let Some(row) = src.table.strip_prefix("@row:") {
323 return format!("(SELECT ({row}).*)");
324 }
325 match source_fanout(src) {
328 Some(fanout) => format!("(\n{}\n)", emit_poly_union(&fanout.implementors, &fanout.columns)),
329 None => junction_ref(module_of(&src.type_name), &src.table),
330 }
331}
332
333fn target_ref(src: &IrSource) -> String {
336 match src.table.strip_prefix("@cte:") {
337 Some(cte_name) => qi(cte_name),
338 None => qn(module_of(&src.type_name), &src.table),
339 }
340}
341
342thread_local! {
343 static SUBTYPE_FANOUTS: std::cell::RefCell<HashMap<(String, String), IrPolyFanout>> =
345 std::cell::RefCell::new(HashMap::new());
346
347 static CORRELATED_CTES: std::cell::RefCell<HashMap<String, String>> =
350 std::cell::RefCell::new(HashMap::new());
351}
352
353fn correlated_cte_iterator(cte_name: &str) -> Option<String> {
356 CORRELATED_CTES.with(|cell| cell.borrow().get(cte_name).cloned())
357}
358
359fn with_correlated_ctes<T>(ctes: &[IrCteDef], emit: impl FnOnce() -> T) -> T {
362 fn collect(ctes: &[IrCteDef], into: &mut HashMap<String, String>) {
363 for c in ctes {
364 if let Some(iterator) = &c.correlated_to {
365 into.insert(c.name.clone(), iterator.clone());
366 }
367 match &c.stmt {
368 IrStmt::For(f) => collect(&f.body_ctes, into),
369 IrStmt::Insert(ins) => collect(&ins.nested_ctes, into),
370 IrStmt::Update(upd) => collect(&upd.nested_ctes, into),
371 _ => {}
372 }
373 }
374 }
375 let mut collected = HashMap::new();
376 collect(ctes, &mut collected);
377 let previous = CORRELATED_CTES.with(|cell| cell.replace(collected));
378 let result = emit();
379 CORRELATED_CTES.with(|cell| cell.replace(previous));
380 result
381}
382
383fn with_subtype_fanouts<T>(fanouts: &HashMap<(String, String), IrPolyFanout>, emit: impl FnOnce() -> T) -> T {
386 let previous = SUBTYPE_FANOUTS.with(|cell| cell.replace(fanouts.clone()));
387 let result = emit();
388 SUBTYPE_FANOUTS.with(|cell| cell.replace(previous));
389 result
390}
391
392fn source_fanout(src: &IrSource) -> Option<IrPolyFanout> {
395 if let Some(fanout) = &src.poly {
396 return Some(fanout.clone());
397 }
398 if src.table.starts_with('@') {
399 return None;
400 }
401 let key = (module_of(&src.type_name).to_string(), src.table.clone());
402 SUBTYPE_FANOUTS.with(|cell| cell.borrow().get(&key).cloned())
403}
404
405pub const DML_CTE: &str = "_dml";
415
416fn junction_ref(module: &str, junction_table: &str) -> String {
417 if let Some(cte_name) = junction_table.strip_prefix("@cte:") {
418 return qi(cte_name);
419 }
420 let Some((tables, columns)) = crate::ir::parse_inherited_junction(junction_table) else {
421 return qn(module, junction_table);
422 };
423 let columns = columns.iter().map(|c| qi(c)).collect::<Vec<_>>().join(", ");
424 let branches = tables
425 .iter()
426 .map(|(module, table)| format!("SELECT {columns} FROM {}", qn(module, table)))
427 .collect::<Vec<_>>()
428 .join(" UNION ALL ");
429 format!("({branches})")
430}
431
432fn result_type_disc(path: &IrPathSelect, alias: &str, type_name: &str) -> String {
437 match terminal_source(path, alias) {
438 Some(source) => source_type_disc(source),
439 None => type_disc(type_name),
440 }
441}
442
443fn terminal_source<'a>(path: &'a IrPathSelect, alias: &str) -> Option<&'a IrSource> {
444 if path.root.alias == alias {
445 return Some(&path.root);
446 }
447 path.joins
448 .iter()
449 .map(path_join_target)
450 .find(|target| target.alias == alias)
451}
452
453fn path_join_target(join: &IrPathJoin) -> &IrSource {
454 match join {
455 IrPathJoin::Single { target, .. }
456 | IrPathJoin::Multi { target, .. }
457 | IrPathJoin::BacklinkSingle { target, .. }
458 | IrPathJoin::BacklinkMulti { target, .. }
459 | IrPathJoin::Function { target, .. }
460 | IrPathJoin::Lateral { target, .. } => target,
461 }
462}
463
464fn source_type_disc(src: &IrSource) -> String {
467 match source_fanout(src) {
468 Some(_) => format!("{}.\"__type__\"", qi(&src.alias)),
469 None => type_disc(&src.type_name),
470 }
471}
472
473fn emit_poly_union(implementors: &[IrPolyImplementor], columns: &[String]) -> String {
476 let col_list = columns.iter().map(|c| qi(c)).collect::<Vec<_>>().join(", ");
477 implementors
478 .iter()
479 .map(|imp| {
480 format!(
481 " SELECT {}::text AS \"__type__\", {} FROM {}",
482 sql_str(&imp.type_name),
483 col_list,
484 qn(&imp.module, &imp.table),
485 )
486 })
487 .collect::<Vec<_>>()
488 .join("\n UNION ALL\n")
489}
490
491fn emit_select_stmt(sel: &IrSelect, ctes: &[IrCteDef]) -> SqlOutput {
494 match sel.rows.as_slice() {
495 [IrRowSource::Bound { source, shape }] => emit_bound_select(sel, source, shape),
496 rows if rows.len() > 1 && rows.iter().all(|r| matches!(r, IrRowSource::Bound { .. })) => {
497 emit_bound_union_select(sel, rows)
498 }
499 rows => emit_free_rows(sel, rows, ctes),
500 }
501}
502
503fn bound_union_from_clause(rows: &[IrRowSource], common_columns: &[String]) -> String {
510 let columns = common_columns.iter().map(|c| qi(c)).collect::<Vec<_>>().join(", ");
511 rows.iter()
512 .map(|row| match row {
513 IrRowSource::Bound { source, .. } if !common_columns.is_empty() => {
514 let own_type = match source_fanout(source) {
515 Some(_) => "\"__type__\"".to_string(),
516 None => format!("{} AS \"__type__\"", type_disc(&source.type_name)),
517 };
518 format!(" SELECT {own_type}, {columns} FROM {}", source_ref(source))
519 }
520 IrRowSource::Bound { source, .. } => format!(" SELECT * FROM {}", source_ref(source)),
521 IrRowSource::Free(_) => unreachable!("caller checked every row is bound"),
522 })
523 .collect::<Vec<_>>()
524 .join("\n UNION ALL\n")
525}
526
527fn emit_bound_union_select(sel: &IrSelect, rows: &[IrRowSource]) -> SqlOutput {
528 let IrRowSource::Bound { source, shape } = &rows[0] else {
529 unreachable!("caller checked every row is bound")
530 };
531 let alias = &source.alias;
532 let (pointer_exprs, shape_pointers) = build_shape(shape, alias);
533 let row_type = if sel.poly_columns.is_empty() {
534 type_disc(&source.type_name)
535 } else {
536 format!("{}.\"__type__\"", qi(alias))
537 };
538 let mut parts = vec![row_type];
539 parts.extend(pointer_exprs);
540
541 let mut sql = format!(
542 "SELECT {}(\n {}\n) AS result\nFROM (\n{}\n) AS {}",
543 if sel.distinct { "DISTINCT " } else { "" },
544 parts.join(",\n "),
545 bound_union_from_clause(rows, &sel.poly_columns),
546 qi(alias),
547 );
548 append_filter(&mut sql, &sel.filter);
549 append_order_by(&mut sql, &sel.order_by);
550 append_offset_limit(&mut sql, &sel.offset, &sel.limit);
551
552 SqlOutput {
553 sql,
554 shape: ShapeDescriptor {
555 root: ShapeNode::Object {
556 name: String::new(),
557 type_name: Some(source.type_name.clone()),
558 position: 0,
559 cardinality: Cardinality::Many,
560 pointers: prepend_type(shape_pointers),
561 has_implicit_id: shape_has_implicit_id(shape),
562 },
563 },
564 inference_plan: None,
565 }
566}
567
568fn emit_bound_select(sel: &IrSelect, source: &IrSource, shape: &[IrShapePointer]) -> SqlOutput {
574 let alias = &source.alias;
575 let (pointer_exprs, shape_pointers) = build_shape(shape, alias);
576
577 let type_expr = if sel.polymorphic {
578 format!("{}.\"__type__\"", qi(alias))
579 } else {
580 type_disc(&source.type_name)
581 };
582 let mut parts = vec![type_expr];
583 parts.extend(pointer_exprs);
584 let tuple = parts.join(",\n ");
585
586 let distinct = if sel.distinct { "DISTINCT " } else { "" };
587
588 let from_clause = if let Some(dml) = &sel.dml_source {
590 let mut cte_parts = match dml.as_ref() {
591 IrStmt::Update(upd) if update_has_any_multilink(upd) => emit_update_multilink_ctes(upd, DML_CTE),
592 IrStmt::Insert(ins) if insert_has_any_multilink(ins) => emit_insert_multilink_ctes(ins, DML_CTE),
593 IrStmt::Insert(ins) if !ins.nested_ctes.is_empty() => {
596 let mut parts = emit_user_cte_parts(&ins.nested_ctes);
597 parts.push(format!("\"_dml\" AS (\n{}\n)", emit_dml_as_cte_source(dml)));
598 parts
599 }
600 IrStmt::Update(upd) if !upd.nested_ctes.is_empty() => {
601 let mut parts = emit_user_cte_parts(&upd.nested_ctes);
602 parts.push(format!("\"_dml\" AS (\n{}\n)", emit_dml_as_cte_source(dml)));
603 parts
604 }
605 IrStmt::Update(upd) if !upd.poly_implementors.is_empty() => emit_poly_update_dml_ctes(upd, "_dml"),
606 IrStmt::Delete(del) if !del.poly_implementors.is_empty() => emit_poly_delete_dml_ctes(del, "_dml"),
607 _ => vec![format!("\"_dml\" AS (\n{}\n)", emit_dml_as_cte_source(dml))],
608 };
609 let (enqueue_v, enqueue_s) = match dml.as_ref() {
610 IrStmt::Insert(ins) => (ins.enqueue_vector.as_slice(), ins.enqueue_search.as_slice()),
611 IrStmt::Update(upd) => (upd.enqueue_vector.as_slice(), upd.enqueue_search.as_slice()),
612 _ => (&[][..], &[][..]),
613 };
614 cte_parts.extend(enqueue_ctes(enqueue_v, "_dml"));
615 cte_parts.extend(enqueue_search_ctes(enqueue_s, "_dml", enqueue_v.len()));
616 format!(
617 "WITH\n{}\nSELECT {}(\n {}\n) AS result\nFROM \"_dml\" AS {}",
618 cte_parts.join(",\n"),
619 distinct,
620 tuple,
621 qi(alias)
622 )
623 } else if sel.polymorphic && !source.table.starts_with("@cte:") {
624 let union_sql = emit_poly_union(&sel.poly_implementors, &sel.poly_columns);
626 format!(
627 "SELECT {}(\n {}\n) AS result\nFROM (\n{}\n) AS {}",
628 distinct,
629 tuple,
630 union_sql,
631 qi(alias)
632 )
633 } else {
634 format!(
637 "SELECT {}(\n {}\n) AS result\nFROM {} AS {}",
638 distinct,
639 tuple,
640 source_ref(source),
641 qi(alias)
642 )
643 };
644
645 let mut sql = from_clause;
646 append_filter(&mut sql, &sel.filter);
647 append_order_by(&mut sql, &sel.order_by);
648 append_offset_limit(&mut sql, &sel.offset, &sel.limit);
649 append_lock_clause(&mut sql, &sel.lock);
650
651 let root_pointers = prepend_type(shape_pointers);
652 SqlOutput {
653 sql,
654 shape: ShapeDescriptor {
655 root: ShapeNode::Object {
656 name: String::new(),
657 type_name: Some(source.type_name.clone()),
658 position: 0,
659 cardinality: Cardinality::Many,
660 pointers: root_pointers,
661 has_implicit_id: shape_has_implicit_id(shape),
662 },
663 },
664 inference_plan: None,
665 }
666}
667
668fn nested_cte_from(ctes: &[IrCteDef], reading: &str, lead: &str) -> String {
675 let joined: Vec<String> = ctes
676 .iter()
677 .map(|c| qi(&c.name))
678 .filter(|name| reading.contains(&format!("{name}.")))
679 .collect();
680 if joined.is_empty() {
681 return String::new();
682 }
683 format!("{lead}FROM {}", joined.join(", "))
684}
685
686fn emit_scalar_union(branches: &[IrStmt]) -> String {
689 let operands: Vec<String> = branches
690 .iter()
691 .enumerate()
692 .map(|(i, branch)| {
693 format!(
694 "SELECT \"v\" FROM (\n{}\n) AS \"_u{i}\"",
695 emit_dml_as_cte_source(branch)
696 )
697 })
698 .collect();
699 format!(
700 "SELECT ROW(v) AS result, v FROM (\n{}\n) AS _scalar",
701 operands.join("\nUNION ALL\n")
702 )
703}
704
705fn emit_dml_as_cte_source(stmt: &IrStmt) -> String {
706 match stmt {
707 IrStmt::ScalarUnion(branches) => format!(" {}", emit_scalar_union(branches)),
708 IrStmt::Insert(ins) => {
709 let rewrite_cols: std::collections::HashSet<&str> =
710 ins.rewrites.iter().map(|r| r.column.as_str()).collect();
711 let cols: Vec<String> = ins
712 .assignments
713 .iter()
714 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
715 .map(|(c, _)| format!(" {}", qi(c)))
716 .chain(ins.rewrites.iter().map(|r| format!(" {}", qi(&r.column))))
717 .collect();
718 let vals: Vec<String> = ins
719 .assignments
720 .iter()
721 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
722 .map(|(_, e)| format!(" {}", emit_expr(e)))
723 .chain(ins.rewrites.iter().map(|r| format!(" {}", emit_expr(&r.expr))))
724 .collect();
725 let mut sql = if !ins.nested_ctes.is_empty() || ins.guard.is_some() {
726 format!(" {}", emit_insert_row_sql(ins))
733 } else if cols.is_empty() {
734 format!(" INSERT INTO {} DEFAULT VALUES", target_ref(&ins.target))
736 } else {
737 format!(
738 " INSERT INTO {} (\n{}\n ) VALUES (\n{}\n )",
739 target_ref(&ins.target),
740 cols.join(",\n"),
741 vals.join(",\n"),
742 )
743 };
744 if let Some(conflict) = &ins.unless_conflict {
745 emit_conflict(&mut sql, conflict);
746 }
747 sql.push_str("\n RETURNING *");
748 sql
749 }
750 IrStmt::Update(upd) => {
751 let alias = &upd.target.alias;
760 let sets = update_set_fragments(&upd.assignments, &upd.rewrites, " ");
761 let mut sql = format!(
762 " UPDATE {} AS {}\n SET\n{}",
763 target_ref(&upd.target),
764 qi(alias),
765 sets.join(",\n"),
766 );
767 sql.push_str(&nested_cte_from(&upd.nested_ctes, &sets.join(","), "\n "));
770 append_filter(&mut sql, &upd.filter);
771 sql.push_str(&format!("\n RETURNING {}.*", qi(alias)));
774 sql
775 }
776 IrStmt::Delete(del) => {
777 let alias = &del.target.alias;
778 let mut sql = format!(" DELETE FROM {} AS {}", target_ref(&del.target), qi(alias),);
779 append_filter(&mut sql, &del.filter);
780 sql.push_str("\n RETURNING *");
781 sql
782 }
783 IrStmt::Select(inner) => match inner.rows.as_slice() {
784 [IrRowSource::Bound { source, .. }]
789 if let Some(read) = inner.dml_source.as_deref()
790 && !matches!(read, IrStmt::Insert(_) | IrStmt::Update(_) | IrStmt::Delete(_)) =>
791 {
792 let mut sql = format!(
794 " SELECT {}* FROM (\n{}\n ) AS {}",
795 if inner.distinct { "DISTINCT " } else { "" },
796 emit_dml_as_cte_source(read),
797 qi(&source.alias),
798 );
799 append_filter(&mut sql, &inner.filter);
800 append_order_by(&mut sql, &inner.order_by);
801 append_offset_limit(&mut sql, &inner.offset, &inner.limit);
802 sql
803 }
804 [IrRowSource::Bound { source, .. }] => {
805 let from = if inner.polymorphic && !source.table.starts_with("@cte:") {
808 format!(
809 "(\n{}\n ) AS {}",
810 emit_poly_union(&inner.poly_implementors, &inner.poly_columns),
811 qi(&source.alias),
812 )
813 } else {
814 format!("{} AS {}", source_ref(source), qi(&source.alias))
815 };
816 let mut sql = format!(
817 " SELECT {}* FROM {}",
818 if inner.distinct { "DISTINCT " } else { "" },
819 from
820 );
821 append_filter(&mut sql, &inner.filter);
822 append_order_by(&mut sql, &inner.order_by);
823 append_offset_limit(&mut sql, &inner.offset, &inner.limit);
824 sql
825 }
826 rows if rows.len() > 1 && rows.iter().all(|r| matches!(r, IrRowSource::Bound { .. })) => {
829 let IrRowSource::Bound { source, .. } = &rows[0] else {
830 unreachable!("checked by the guard")
831 };
832 let mut sql = format!(
833 " SELECT {}* FROM (\n{}\n ) AS {}",
834 if inner.distinct { "DISTINCT " } else { "" },
835 bound_union_from_clause(rows, &inner.poly_columns),
836 qi(&source.alias),
837 );
838 append_filter(&mut sql, &inner.filter);
839 append_order_by(&mut sql, &inner.order_by);
840 append_offset_limit(&mut sql, &inner.offset, &inner.limit);
841 sql
842 }
843 _ => emit_select_stmt(inner, &[]).sql,
849 },
850 IrStmt::FunctionSelect(sel) => {
851 let args_sql = sel.fn_args.iter().map(emit_expr).collect::<Vec<_>>().join(", ");
854 let fn_call = format!("{}.{}({})", pg_schema(&sel.fn_module), qi(&sel.fn_name), args_sql);
855 let mut sql = format!(" SELECT * FROM {} AS {}", fn_call, qi(&sel.alias));
856 append_filter(&mut sql, &sel.filter);
857 append_order_by(&mut sql, &sel.order_by);
858 append_offset_limit(&mut sql, &sel.offset, &sel.limit);
859 sql
860 }
861 IrStmt::For(f) => {
862 if matches!(&f.iterator, IrForIterator::Values { exprs, .. } if exprs.is_empty()) {
867 return " SELECT NULL AS \"id\" WHERE FALSE".to_string();
868 }
869 let (values_from, _) = emit_for_iterator(&f.iterator, &format!("_for_{}", f.var_name));
870 let body =
871 prefix_body_ctes(emit_dml_as_cte_source(&f.body), &uncorrelated_body_ctes(f)).replace('\n', "\n ");
872 format!(
873 " SELECT \"_body\".*\n FROM {}\n CROSS JOIN LATERAL (\n {}\n ) AS \"_body\"",
874 values_from.replace('\n', "\n "),
875 body,
876 )
877 }
878 IrStmt::Group(grp) if matches!(grp.output, IrGroupOutput::Elements) => emit_group_elements_source(grp),
879 IrStmt::Group(_) | IrStmt::VectorSearch(_) | IrStmt::FtsSearch(_) => {
880 unreachable!("cannot appear as a CTE source")
881 }
882 IrStmt::PathSelect(ps) => {
883 let distinct = if ps.distinct { "DISTINCT " } else { "" };
889 let projection = match &ps.result {
890 IrPathResult::Scalar(e, _) => format!("{}{} AS v", distinct, emit_expr(e)),
894 IrPathResult::Object { alias, .. } => format!("{}{}.*", distinct, qi(alias)),
895 };
896 let mut sql = format!(
897 "SELECT {}\n FROM {}",
898 projection,
899 emit_path_joins(&ps.root, &ps.joins)
900 );
901 append_filter(&mut sql, &ps.filter);
902 append_order_by(&mut sql, &ps.order_by);
903 append_offset_limit(&mut sql, &ps.offset, &ps.limit);
904 match &ps.result {
905 IrPathResult::Scalar(e, _) => {
906 let row_value = if enum_type_of_expr(e).is_some() { "v::text" } else { "v" };
907 format!(" SELECT ROW({row_value}) AS result, v FROM ({sql}) AS _scalar")
908 }
909 IrPathResult::Object { .. } => format!(" {sql}"),
910 }
911 }
912 }
913}
914
915fn emit_poly_update_dml_ctes(upd: &IrUpdate, name: &str) -> Vec<String> {
950 let alias = &upd.target.alias;
951 let sets = update_set_fragments(&upd.assignments, &upd.rewrites, "");
952 let col_list = upd.poly_columns.iter().map(|c| qi(c)).collect::<Vec<_>>().join(", ");
953
954 let mut cte_parts = vec![];
955 let mut union_parts = vec![];
956 for (i, imp) in upd.poly_implementors.iter().enumerate() {
957 let cte_name = format!("{}__u{}", name, i);
958 let mut upd_sql = format!(
959 "UPDATE {} AS {}\nSET {}",
960 qn(&imp.module, &imp.table),
961 qi(alias),
962 sets.join(", "),
963 );
964 append_filter(&mut upd_sql, &upd.filter);
965 upd_sql.push_str(&format!("\nRETURNING {}", col_list));
966 cte_parts.push(format!("\"{}\" AS (\n{}\n)", cte_name, upd_sql));
967
968 union_parts.push(format!(
969 "SELECT {}::text AS \"__type__\", {} FROM \"{}\"",
970 sql_str(&imp.type_name),
971 col_list,
972 cte_name,
973 ));
974 }
975 cte_parts.push(format!("\"{}\" AS (\n{}\n)", name, union_parts.join("\nUNION ALL\n")));
976 cte_parts
977}
978
979fn concrete_poly_update(upd: &IrUpdate, imp: &IrPolyImplementor) -> IrUpdate {
984 let interface_prefix = format!("{}.", upd.target.table);
985 let own_prefix = format!("{}.", imp.table);
986 let own_junction = |table: &mut String, module: &mut String| {
987 if let Some(link) = table.strip_prefix(&interface_prefix) {
988 *table = format!("{own_prefix}{link}");
989 module.clone_from(&imp.module);
990 }
991 };
992 let mut concrete = upd.clone();
993 concrete.poly_implementors = vec![];
994 concrete.target.table = imp.table.clone();
995 concrete.target.type_name = imp.type_name.clone();
996 concrete.target.poly = None;
997 concrete
998 .multi_link_clears
999 .iter_mut()
1000 .for_each(|c| own_junction(&mut c.junction_table, &mut c.module));
1001 concrete
1002 .multi_link_replaces
1003 .iter_mut()
1004 .chain(concrete.multi_link_appends.iter_mut())
1005 .chain(concrete.multi_link_removals.iter_mut())
1006 .for_each(|m| own_junction(&mut m.junction_table, &mut m.module));
1007 concrete
1008}
1009
1010fn emit_poly_update_multilink_ctes(upd: &IrUpdate, name: &str) -> Vec<String> {
1014 let col_list = upd.poly_columns.iter().map(|c| qi(c)).collect::<Vec<_>>().join(", ");
1015 let mut cte_parts = emit_user_cte_parts(&upd.nested_ctes);
1016 let mut union_parts = vec![];
1017 for (i, imp) in upd.poly_implementors.iter().enumerate() {
1018 let cte_name = format!("{}__u{}", name, i);
1019 cte_parts.extend(emit_update_multilink_cte_parts(
1020 &concrete_poly_update(upd, imp),
1021 &cte_name,
1022 false,
1023 ));
1024 union_parts.push(format!(
1025 "SELECT {}::text AS \"__type__\", {} FROM \"{}\"",
1026 sql_str(&imp.type_name),
1027 col_list,
1028 cte_name,
1029 ));
1030 }
1031 cte_parts.push(format!("\"{}\" AS (\n{}\n)", name, union_parts.join("\nUNION ALL\n")));
1032 cte_parts
1033}
1034
1035fn emit_poly_delete_dml_ctes(del: &IrDelete, name: &str) -> Vec<String> {
1036 let alias = &del.target.alias;
1037 let col_list = del.poly_columns.iter().map(|c| qi(c)).collect::<Vec<_>>().join(", ");
1038
1039 let mut cte_parts = vec![];
1040 let mut union_parts = vec![];
1041 for (i, imp) in del.poly_implementors.iter().enumerate() {
1042 let cte_name = format!("{}__d{}", name, i);
1043 let mut del_sql = format!("DELETE FROM {} AS {}", qn(&imp.module, &imp.table), qi(alias),);
1044 append_filter(&mut del_sql, &del.filter);
1045 del_sql.push_str(&format!("\nRETURNING {}", col_list));
1046 cte_parts.push(format!("\"{}\" AS (\n{}\n)", cte_name, del_sql));
1047
1048 union_parts.push(format!(
1049 "SELECT {}::text AS \"__type__\", {} FROM \"{}\"",
1050 sql_str(&imp.type_name),
1051 col_list,
1052 cte_name,
1053 ));
1054 }
1055 cte_parts.push(format!("\"{}\" AS (\n{}\n)", name, union_parts.join("\nUNION ALL\n")));
1056 cte_parts
1057}
1058
1059fn update_has_any_multilink(upd: &IrUpdate) -> bool {
1062 !upd.multi_link_clears.is_empty()
1063 || !upd.multi_link_replaces.is_empty()
1064 || !upd.multi_link_appends.is_empty()
1065 || !upd.multi_link_removals.is_empty()
1066}
1067
1068fn ml_clear_exclusion(rep: Option<&IrMultiLinkMutation>) -> String {
1086 match rep {
1087 Some(rep) => {
1088 let vals_ref = emit_multilink_values_subquery(&rep.values, &[]);
1089 format!(
1090 " AND {} NOT IN (SELECT \"_v\".\"id\" FROM {} AS \"_v\")",
1091 qi(&rep.target_col),
1092 vals_ref
1093 )
1094 }
1095 None => String::new(),
1096 }
1097}
1098
1099fn emit_update_multilink_ctes(upd: &IrUpdate, name: &str) -> Vec<String> {
1113 emit_update_multilink_cte_parts(upd, name, true)
1114}
1115
1116fn emit_update_multilink_cte_parts(upd: &IrUpdate, name: &str, emit_nested: bool) -> Vec<String> {
1121 let alias = &upd.target.alias;
1122 let has_scalar_changes = !upd.assignments.is_empty() || !upd.rewrites.is_empty();
1123 let ids_name = format!("{}__ids", name);
1124 let mut parts: Vec<String> = if emit_nested {
1126 emit_user_cte_parts(&upd.nested_ctes)
1127 } else {
1128 vec![]
1129 };
1130
1131 if has_scalar_changes {
1132 let sets = update_set_fragments(&upd.assignments, &upd.rewrites, "");
1133 let mut upd_sql = format!(
1134 "UPDATE {} AS {}\nSET {}",
1135 target_ref(&upd.target),
1136 qi(alias),
1137 sets.join(", "),
1138 );
1139 upd_sql.push_str(&nested_cte_from(&upd.nested_ctes, &sets.join(","), "\n"));
1142 append_filter(&mut upd_sql, &upd.filter);
1143 upd_sql.push_str(&format!("\nRETURNING {}.*", qi(alias)));
1146 parts.push(format!("\"{}\" AS (\n{}\n)", ids_name, upd_sql));
1147 } else {
1148 let mut sel = format!(
1149 "SELECT {}.* FROM {} AS {}",
1150 qi(alias),
1151 target_ref(&upd.target),
1152 qi(alias),
1153 );
1154 append_filter(&mut sel, &upd.filter);
1155 parts.push(format!("\"{}\" AS (\n{}\n)", ids_name, sel));
1156 }
1157
1158 for (i, clr) in upd.multi_link_clears.iter().enumerate() {
1159 let exclude = ml_clear_exclusion(upd.multi_link_replaces.get(i));
1160 let del = format!(
1161 "DELETE FROM {} WHERE {} IN (SELECT id FROM \"{}\"){}",
1162 qn(&clr.module, &clr.junction_table),
1163 qi(&clr.source_col),
1164 ids_name,
1165 exclude,
1166 );
1167 parts.push(format!("\"{}__clr_{}\" AS (\n{}\n)", name, i, del));
1168 }
1169 for (i, app) in upd.multi_link_appends.iter().enumerate() {
1170 parts.push(emit_ml_append_cte(
1171 app,
1172 &ids_name,
1173 &upd.target.alias,
1174 &format!("{}__ml_add_{}", name, i),
1175 ));
1176 }
1177 for (i, rem) in upd.multi_link_removals.iter().enumerate() {
1178 parts.push(emit_ml_remove_cte(
1179 rem,
1180 &ids_name,
1181 &upd.target.alias,
1182 &format!("{}__ml_rm_{}", name, i),
1183 ));
1184 }
1185 for (i, rep) in upd.multi_link_replaces.iter().enumerate() {
1186 parts.push(emit_ml_append_cte(
1187 rep,
1188 &ids_name,
1189 &upd.target.alias,
1190 &format!("{}__ml_rep_{}", name, i),
1191 ));
1192 }
1193
1194 parts.push(format!("\"{}\" AS (\n SELECT * FROM \"{}\"\n)", name, ids_name));
1195 parts
1196}
1197
1198fn insert_has_any_multilink(ins: &IrInsert) -> bool {
1199 !ins.multi_link_appends.is_empty()
1200}
1201
1202fn emit_insert_multilink_ctes(ins: &IrInsert, name: &str) -> Vec<String> {
1208 let ids_name = format!("{}__ids", name);
1209 let mut parts: Vec<String> = emit_user_cte_parts(&ins.nested_ctes);
1210
1211 let mut insert_sql = emit_insert_row_sql(ins);
1212 if let Some(conflict) = &ins.unless_conflict {
1213 emit_conflict(&mut insert_sql, conflict);
1214 }
1215 insert_sql.push_str("\nRETURNING *");
1216 parts.push(format!("\"{}\" AS (\n{}\n)", ids_name, insert_sql));
1217
1218 for (i, app) in ins.multi_link_appends.iter().enumerate() {
1219 parts.push(emit_ml_append_cte(
1220 app,
1221 &ids_name,
1222 &ins.target.alias,
1223 &format!("{}__ml_add_{}", name, i),
1224 ));
1225 }
1226
1227 parts.push(format!("\"{}\" AS (\n SELECT * FROM \"{}\"\n)", name, ids_name));
1228 parts
1229}
1230
1231fn emit_for_dml_ctes_within(f: &IrFor, name: &str, outer_alias: Option<&str>) -> Vec<String> {
1242 let iter_alias = format!("_for_{}", f.var_name);
1243 let (_, plain_iter_cte) = emit_for_iterator(&f.iterator, &iter_alias);
1244 let correlated = outer_alias;
1245 let iter_cte = match correlated {
1246 Some(outer) => emit_nested_for_iterator(&f.iterator, &iter_alias, outer),
1247 None => plain_iter_cte,
1248 };
1249 let mut parts: Vec<String> = vec![iter_cte];
1252 parts.extend(emit_user_cte_parts(&f.body_ctes));
1253 let ids_name = format!("{}__ids", name);
1254
1255 if correlated.is_some()
1258 && let IrStmt::Insert(ins) = f.body.as_ref()
1259 && ins.multi_link_appends.is_empty()
1260 {
1261 let rows_name = format!("{}__rows", name);
1262 let outer_source = format!("{}.{}", qi(&iter_alias), qi(OUTER_KEY));
1263 parts.push(emit_iteration_rows_cte(ins, &rows_name, &iter_alias, &outer_source));
1264 parts.push(emit_insert_from_rows_cte(ins, &ids_name, &rows_name));
1265 parts.push(format!("\"{}\" AS (\n SELECT * FROM \"{}\"\n)", name, rows_name));
1266 return parts;
1267 }
1268
1269 match f.body.as_ref() {
1270 IrStmt::Insert(ins) => {
1271 let rewrite_cols: std::collections::HashSet<&str> =
1272 ins.rewrites.iter().map(|r| r.column.as_str()).collect();
1273 let cols: Vec<String> = ins
1274 .assignments
1275 .iter()
1276 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
1277 .map(|(c, _)| qi(c))
1278 .chain(ins.rewrites.iter().map(|r| qi(&r.column)))
1279 .collect();
1280 let values: Vec<String> = ins
1281 .assignments
1282 .iter()
1283 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
1284 .map(|(_, e)| emit_expr(e))
1285 .chain(ins.rewrites.iter().map(|r| emit_expr(&r.expr)))
1286 .collect();
1287 let rows_name = format!("{}__rows", name);
1290 let correlated_targets =
1291 correlated_append_indices(&ins.multi_link_appends, &[&f.body_ctes, &ins.nested_ctes]);
1292 if correlated_targets.is_empty() {
1293 let mut sql = format!(
1294 "INSERT INTO {} ({})\nSELECT {} FROM {}",
1295 target_ref(&ins.target),
1296 cols.join(", "),
1297 values.join(", "),
1298 qi(&iter_alias),
1299 );
1300 if let Some(conflict) = &ins.unless_conflict {
1301 emit_conflict(&mut sql, conflict);
1302 }
1303 sql.push_str("\nRETURNING *");
1304 parts.push(format!("\"{}\" AS (\n{}\n)", ids_name, sql));
1305 } else {
1306 let outer_source = match correlated {
1307 Some(_) => format!("{}.{}", qi(&iter_alias), qi(OUTER_KEY)),
1308 None => format!("{}.{}", qi(&iter_alias), qi("v")),
1309 };
1310 parts.push(emit_iteration_rows_cte(ins, &rows_name, &iter_alias, &outer_source));
1311 parts.push(emit_insert_from_rows_cte(ins, &ids_name, &rows_name));
1312 }
1313 for (i, append) in ins.multi_link_appends.iter().enumerate() {
1314 let cte_name = format!("{}__ml_add_{}", name, i);
1315 if correlated_targets.contains(&i) {
1316 let IrMultiLinkValueSource::CteRef(target) = &append.values.source else {
1317 unreachable!("only a CTE-backed value can carry an iteration key")
1318 };
1319 parts.push(emit_correlated_ml_append_cte(append, &rows_name, target, &cte_name));
1320 } else {
1321 parts.push(emit_ml_append_cte(append, &ids_name, &ins.target.alias, &cte_name));
1322 }
1323 }
1324 }
1325 IrStmt::Update(upd) => {
1326 let alias = &upd.target.alias;
1327 if upd.assignments.is_empty() && upd.rewrites.is_empty() && !upd.multi_link_appends.is_empty() {
1328 const ITER_COL: &str = "_iter";
1329 let generated: Vec<Option<&IrInsert>> = upd
1333 .multi_link_appends
1334 .iter()
1335 .map(|a| per_iteration_insert(a, &upd.nested_ctes))
1336 .collect();
1337 let new_cols: String = generated
1338 .iter()
1339 .enumerate()
1340 .filter_map(|(i, ins)| {
1341 ins.map(|ins| {
1342 format!(
1343 ", {} AS {}",
1344 ins.id_default_sql.as_deref().unwrap_or("uuidv7()"),
1345 qi(&format!("_new_{i}"))
1346 )
1347 })
1348 })
1349 .collect();
1350 let mut ids_sql = format!(
1351 "\"{}\" AS (\nSELECT {}.*, {}.\"v\" AS {}{} FROM {} AS {}, {}",
1352 ids_name,
1353 qi(alias),
1354 qi(&iter_alias),
1355 qi(ITER_COL),
1356 new_cols,
1357 target_ref(&upd.target),
1358 qi(alias),
1359 qi(&iter_alias),
1360 );
1361 append_filter(&mut ids_sql, &upd.filter);
1362 ids_sql.push_str("\n)");
1363 parts.push(ids_sql);
1364 for (i, append) in upd.multi_link_appends.iter().enumerate() {
1365 match generated[i] {
1366 Some(ins) => {
1367 parts.push(emit_for_nested_insert_cte(
1368 ins,
1369 &ids_name,
1370 &iter_alias,
1371 &format!("{}__ml_ins_{}", name, i),
1372 &format!("_new_{i}"),
1373 ));
1374 parts.push(emit_for_ml_append_cte(
1375 append,
1376 &ids_name,
1377 &format!("{}__ml_add_{}", name, i),
1378 &format!("_new_{i}"),
1379 ));
1380 }
1381 None => parts.push(emit_for_ml_append_cte(
1382 append,
1383 &ids_name,
1384 &format!("{}__ml_add_{}", name, i),
1385 ITER_COL,
1386 )),
1387 }
1388 }
1389 } else {
1390 let sets = update_set_fragments(&upd.assignments, &upd.rewrites, "");
1391 let mut sql = format!(
1392 "UPDATE {} AS {}\nSET {}\nFROM {}",
1393 target_ref(&upd.target),
1394 qi(alias),
1395 sets.join(", "),
1396 qi(&iter_alias),
1397 );
1398 append_filter(&mut sql, &upd.filter);
1399 sql.push_str(&format!("\nRETURNING {}.*", qi(alias)));
1400 parts.push(format!("\"{}\" AS (\n{}\n)", ids_name, sql));
1401 }
1402 }
1403 IrStmt::For(inner) => {
1407 let inner_alias = format!("_for_{}", inner.var_name);
1408 parts.push(emit_nested_for_iterator(&inner.iterator, &inner_alias, &iter_alias));
1409 parts.extend(emit_user_cte_parts(&inner.body_ctes));
1410 let IrStmt::Insert(ins) = inner.body.as_ref() else {
1411 unreachable!("a nested for-loop's own body is an insert: {:?}", inner.body)
1412 };
1413 let rewrite_cols: std::collections::HashSet<&str> =
1414 ins.rewrites.iter().map(|r| r.column.as_str()).collect();
1415 let cols: Vec<String> = ins
1416 .assignments
1417 .iter()
1418 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
1419 .map(|(c, _)| qi(c))
1420 .chain(ins.rewrites.iter().map(|r| qi(&r.column)))
1421 .collect();
1422 let values: Vec<String> = ins
1423 .assignments
1424 .iter()
1425 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
1426 .map(|(_, e)| emit_expr(e))
1427 .chain(ins.rewrites.iter().map(|r| emit_expr(&r.expr)))
1428 .collect();
1429 let mut sql = format!(
1430 "INSERT INTO {} ({})\nSELECT {} FROM {}",
1431 target_ref(&ins.target),
1432 cols.join(", "),
1433 values.join(", "),
1434 nested_for_from(&inner_alias, &iter_alias),
1435 );
1436 if let Some(conflict) = &ins.unless_conflict {
1437 emit_conflict(&mut sql, conflict);
1438 }
1439 sql.push_str("\nRETURNING *");
1440 parts.push(format!("\"{}\" AS (\n{}\n)", ids_name, sql));
1441 for (i, append) in ins.multi_link_appends.iter().enumerate() {
1442 parts.push(emit_ml_append_cte(
1443 append,
1444 &ids_name,
1445 &ins.target.alias,
1446 &format!("{}__ml_add_{}", name, i),
1447 ));
1448 }
1449 }
1450 other => unreachable!("for-loop body is not a mutation: {other:?}"),
1452 }
1453
1454 parts.push(format!("\"{}\" AS (\n SELECT * FROM \"{}\"\n)", name, ids_name));
1455 parts
1456}
1457
1458fn emit_user_cte_parts(ctes: &[IrCteDef]) -> Vec<String> {
1467 let mut parts: Vec<String> = vec![];
1468 for c in ctes {
1469 let nested: &[IrCteDef] = match &c.stmt {
1475 IrStmt::Insert(ins) if insert_has_any_multilink(ins) => &[],
1476 IrStmt::Update(upd) if update_has_any_multilink(upd) => &[],
1477 IrStmt::Insert(ins) => &ins.nested_ctes,
1478 IrStmt::Update(upd) => &upd.nested_ctes,
1479 _ => &[],
1480 };
1481 if !nested.is_empty() {
1482 parts.extend(emit_user_cte_parts(nested));
1483 }
1484 if let IrStmt::Update(upd) = &c.stmt {
1485 if !upd.poly_implementors.is_empty() {
1486 parts.extend(if update_has_any_multilink(upd) {
1487 emit_poly_update_multilink_ctes(upd, &c.name)
1488 } else {
1489 emit_poly_update_dml_ctes(upd, &c.name)
1490 });
1491 continue;
1492 }
1493 if update_has_any_multilink(upd) {
1494 parts.extend(emit_update_multilink_ctes(upd, &c.name));
1495 continue;
1496 }
1497 }
1498 if let IrStmt::Insert(ins) = &c.stmt
1499 && insert_has_any_multilink(ins)
1500 {
1501 parts.extend(emit_insert_multilink_ctes(ins, &c.name));
1502 continue;
1503 }
1504 if let IrStmt::Delete(del) = &c.stmt
1505 && !del.poly_implementors.is_empty()
1506 {
1507 parts.extend(emit_poly_delete_dml_ctes(del, &c.name));
1508 continue;
1509 }
1510 if let IrStmt::For(f) = &c.stmt
1511 && matches!(f.body.as_ref(), IrStmt::Insert(_) | IrStmt::Update(_) | IrStmt::For(_))
1512 {
1513 parts.extend(emit_for_dml_ctes_within(f, &c.name, c.correlated_to.as_deref()));
1514 continue;
1515 }
1516 let body = emit_dml_as_cte_source(&c.stmt);
1517 parts.push(match &c.correlated_to {
1518 Some(iterator) => format!(
1521 "\"{}\" AS (\nSELECT {}.\"v\" AS {}, \"_row\".*\nFROM {}\nCROSS JOIN LATERAL (\n{}\n) AS \"_row\"\n)",
1522 c.name,
1523 qi(iterator),
1524 qi(OUTER_KEY),
1525 qi(iterator),
1526 body,
1527 ),
1528 None => format!("\"{}\" AS (\n{}\n)", c.name, body),
1529 });
1530 }
1531 parts
1532}
1533
1534fn emit_cte_prefix(ctes: &[IrCteDef]) -> String {
1536 format!("WITH\n{}\n", emit_user_cte_parts(ctes).join(",\n"))
1537}
1538
1539pub(crate) fn collect_link_prop_names(vals: &IrMultiLinkValues, names: &mut Vec<String>) {
1544 for (name, _) in &vals.link_props {
1545 if !names.contains(name) {
1546 names.push(name.clone());
1547 }
1548 }
1549 match &vals.source {
1550 IrMultiLinkValueSource::Union(a, b) => {
1551 collect_link_prop_names(a, names);
1552 collect_link_prop_names(b, names);
1553 }
1554 IrMultiLinkValueSource::Asserted { inner, .. } => collect_link_prop_names(inner, names),
1555 _ => {}
1556 }
1557}
1558
1559fn emit_link_prop_cols(vals: &IrMultiLinkValues, prop_names: &[String]) -> String {
1564 prop_names
1565 .iter()
1566 .map(|name| match vals.link_props.iter().find(|(n, _)| n == name) {
1567 Some((_, expr)) => format!(", {} AS {}", emit_expr(expr), qi(name)),
1568 None => format!(", NULL AS {}", qi(name)),
1569 })
1570 .collect()
1571}
1572
1573fn emit_multilink_values_subquery(vals: &IrMultiLinkValues, prop_names: &[String]) -> String {
1579 emit_multilink_values_inner(vals, prop_names, true)
1580}
1581
1582fn emit_multilink_values_inner(vals: &IrMultiLinkValues, prop_names: &[String], bare_cte_ok: bool) -> String {
1587 if let IrMultiLinkValueSource::Union(a, b) = &vals.source {
1588 return format!(
1589 "({}\nUNION ALL\n{})",
1590 emit_multilink_values_inner(a, prop_names, false),
1591 emit_multilink_values_inner(b, prop_names, false),
1592 );
1593 }
1594
1595 if let IrMultiLinkValueSource::Asserted {
1600 fn_name,
1601 inner,
1602 message,
1603 } = &vals.source
1604 {
1605 let inner_sql = emit_multilink_values_inner(inner, &[], false);
1606 let prop_cols = emit_link_prop_cols(vals, prop_names);
1607 return format!(
1608 "(SELECT unnest(\"_pylon\".{}(ARRAY(SELECT \"_v\".\"id\" FROM {} AS \"_v\"){})) AS \"id\"{})",
1609 qi(fn_name),
1610 inner_sql,
1611 assert_message_arg(message),
1612 prop_cols,
1613 );
1614 }
1615
1616 let prop_cols = emit_link_prop_cols(vals, prop_names);
1617
1618 match &vals.source {
1619 IrMultiLinkValueSource::CteRef(name) => {
1620 if prop_cols.is_empty() && bare_cte_ok {
1621 format!("\"{}\"", name)
1623 } else {
1624 format!("(SELECT \"_s\".\"id\"{} FROM \"{}\" AS \"_s\")", prop_cols, name)
1625 }
1626 }
1627 IrMultiLinkValueSource::Select(s) => {
1628 let [IrRowSource::Bound { source, .. }] = s.rows.as_slice() else {
1633 unreachable!("IrMultiLinkValueSource::Select is always schema-bound")
1634 };
1635 let alias = &source.alias;
1636 let mut sql = format!(
1637 "(SELECT {}.\"id\"{} FROM {} AS {}",
1638 qi(alias),
1639 prop_cols,
1640 source_ref(source),
1641 qi(alias)
1642 );
1643 append_filter(&mut sql, &s.filter);
1644 sql.push(')');
1645 sql
1646 }
1647 IrMultiLinkValueSource::PathSelect(ps) => {
1648 let root_alias = &ps.root.alias;
1649 let target_alias = match &ps.result {
1652 IrPathResult::Object { alias, .. } => alias,
1653 IrPathResult::Scalar(..) => root_alias,
1654 };
1655 let mut sql = format!(
1656 "(SELECT {}.\"id\"{} FROM {} AS {}",
1657 qi(target_alias),
1658 prop_cols,
1659 source_ref(&ps.root),
1660 qi(root_alias)
1661 );
1662 for join in &ps.joins {
1663 sql.push_str(&emit_path_join_sql(join));
1664 }
1665 append_filter(&mut sql, &ps.filter);
1666 sql.push(')');
1667 sql
1668 }
1669 IrMultiLinkValueSource::Function(fs) => {
1670 let args = fs.fn_args.iter().map(emit_expr).collect::<Vec<_>>().join(", ");
1671 let mut sql = format!(
1672 "(SELECT {}.\"id\"{} FROM {}.{}({}) AS {}",
1673 qi(&fs.alias),
1674 prop_cols,
1675 pg_schema(&fs.fn_module),
1676 qi(&fs.fn_name),
1677 args,
1678 qi(&fs.alias),
1679 );
1680 append_filter(&mut sql, &fs.filter);
1681 sql.push(')');
1682 sql
1683 }
1684 IrMultiLinkValueSource::Union(..) | IrMultiLinkValueSource::Asserted { .. } => {
1685 unreachable!("handled above")
1686 }
1687 }
1688}
1689
1690fn emit_path_join_sql(join: &IrPathJoin) -> String {
1692 match join {
1693 IrPathJoin::Lateral { inner, target } => {
1694 let projected = match &inner.result {
1695 IrPathResult::Object { alias, .. } => alias.clone(),
1696 IrPathResult::Scalar(..) => unreachable!("a lateral step is only built for an object-valued computed"),
1697 };
1698 let mut body = format!(
1699 "SELECT {}.* FROM {}",
1700 qi(&projected),
1701 emit_path_joins(&inner.root, &inner.joins)
1702 );
1703 append_filter(&mut body, &inner.filter);
1704 append_order_by(&mut body, &inner.order_by);
1705 append_offset_limit(&mut body, &inner.offset, &inner.limit);
1706 format!(" JOIN LATERAL (\n{}\n) AS {} ON TRUE", body, qi(&target.alias))
1707 }
1708 IrPathJoin::Function {
1709 fn_module,
1710 fn_name,
1711 args,
1712 target,
1713 } => {
1714 let args_sql = args.iter().map(emit_expr).collect::<Vec<_>>().join(", ");
1715 format!(
1716 " CROSS JOIN LATERAL {}.{}({}) AS {}",
1717 pg_schema(fn_module),
1718 qi(fn_name),
1719 args_sql,
1720 qi(&target.alias),
1721 )
1722 }
1723 IrPathJoin::Single {
1724 source_alias,
1725 fk_col,
1726 target,
1727 } => {
1728 format!(
1729 " JOIN {} AS {} ON {}.\"id\" = {}.{}",
1730 source_ref(target),
1731 qi(&target.alias),
1732 qi(&target.alias),
1733 qi(source_alias),
1734 qi(fk_col)
1735 )
1736 }
1737 IrPathJoin::Multi {
1738 source_alias,
1739 junction_alias,
1740 join: ml_join,
1741 target,
1742 } => {
1743 let (jt_ref, src_col, tgt_col) = match ml_join {
1744 IrMultiLinkJoin::Standard { junction_table, module } => (
1745 junction_ref(module, junction_table),
1746 "source".to_string(),
1747 "target".to_string(),
1748 ),
1749 IrMultiLinkJoin::Through {
1750 junction_table,
1751 module,
1752 source_col,
1753 target_col,
1754 } => (
1755 junction_ref(module, junction_table),
1756 source_col.clone(),
1757 target_col.clone(),
1758 ),
1759 IrMultiLinkJoin::BacklinkFk { .. } | IrMultiLinkJoin::BacklinkJunction { .. } => {
1764 unreachable!("a forward multi-link path step never uses a backlink join variant")
1765 }
1766 };
1767 format!(
1768 " JOIN {} AS {} ON {}.{} = {}.\"id\" JOIN {} AS {} ON {}.{} = {}.\"id\"",
1769 jt_ref,
1770 qi(junction_alias),
1771 qi(junction_alias),
1772 qi(&src_col),
1773 qi(source_alias),
1774 source_ref(target),
1775 qi(&target.alias),
1776 qi(junction_alias),
1777 qi(&tgt_col),
1778 qi(&target.alias),
1779 )
1780 }
1781 IrPathJoin::BacklinkSingle {
1782 source_alias,
1783 fk_col,
1784 target,
1785 } => {
1786 format!(
1787 " JOIN {} AS {} ON {}.{} = {}.\"id\"",
1788 source_ref(target),
1789 qi(&target.alias),
1790 qi(&target.alias),
1791 qi(fk_col),
1792 qi(source_alias),
1793 )
1794 }
1795 IrPathJoin::BacklinkMulti {
1796 source_alias,
1797 junction_alias,
1798 junction_table,
1799 module,
1800 owner_col,
1801 current_col,
1802 target,
1803 } => {
1804 format!(
1805 " JOIN {} AS {} ON {}.{} = {}.\"id\" JOIN {} AS {} ON {}.\"id\" = {}.{}",
1806 junction_ref(module, junction_table),
1807 qi(junction_alias),
1808 qi(junction_alias),
1809 qi(current_col),
1810 qi(source_alias),
1811 source_ref(target),
1812 qi(&target.alias),
1813 qi(&target.alias),
1814 qi(junction_alias),
1815 qi(owner_col),
1816 )
1817 }
1818 }
1819}
1820
1821pub(crate) fn append_value_is_the_loop_variable(values: &IrMultiLinkValues, var: &str) -> bool {
1833 let IrMultiLinkValueSource::Select(sel) = &values.source else {
1834 return false;
1835 };
1836 let Some(IrExpr::BinOp(cmp)) = sel.filter.as_ref() else {
1837 return false;
1838 };
1839 matches!(
1840 (&cmp.left, &cmp.op, &cmp.right),
1841 (
1842 IrExpr::ColumnRef { column, .. },
1843 crate::parse::ast::BinOpKind::Eq,
1844 IrExpr::ForVar { name, .. },
1845 ) if column == "id" && name == var
1846 ) && values.link_props.is_empty()
1847}
1848
1849pub(crate) fn per_iteration_insert<'c>(mutation: &IrMultiLinkMutation, nested: &'c [IrCteDef]) -> Option<&'c IrInsert> {
1855 let IrMultiLinkValueSource::CteRef(name) = &mutation.values.source else {
1856 return None;
1857 };
1858 if !mutation.values.link_props.is_empty() {
1859 return None;
1860 }
1861 nested.iter().find(|c| &c.name == name).and_then(|c| match &c.stmt {
1862 IrStmt::Insert(ins) if ins.id_default_sql.is_some() && ins.multi_link_appends.is_empty() => Some(ins),
1863 _ => None,
1864 })
1865}
1866
1867fn emit_for_nested_insert_cte(
1872 ins: &IrInsert,
1873 ids_name: &str,
1874 iter_alias: &str,
1875 cte_name: &str,
1876 id_col: &str,
1877) -> String {
1878 let mut cols = vec![qi("id")];
1879 let mut values = vec![format!("\"{}\".{}", ids_name, qi(id_col))];
1880 for (col, expr) in &ins.assignments {
1881 cols.push(qi(col));
1882 values.push(emit_expr(expr));
1883 }
1884 format!(
1885 "\"{}\" AS (\nINSERT INTO {} ({})\nSELECT {} FROM \"{}\", {}\nWHERE \"{}\".{} = {}.\"v\"\nRETURNING {}\n)",
1886 cte_name,
1887 target_ref(&ins.target),
1888 cols.join(", "),
1889 values.join(", "),
1890 ids_name,
1891 qi(iter_alias),
1892 ids_name,
1893 qi("_iter"),
1894 qi(iter_alias),
1895 qi("id"),
1896 )
1897}
1898
1899fn emit_for_ml_append_cte(mutation: &IrMultiLinkMutation, ids_name: &str, cte_name: &str, iter_col: &str) -> String {
1900 format!(
1901 "\"{}\" AS (\nINSERT INTO {} ({}, {})\nSELECT \"{}\".\"id\", \"{}\".{} FROM \"{}\"\nON CONFLICT DO NOTHING\nRETURNING {}, {}\n)",
1902 cte_name,
1903 qn(&mutation.module, &mutation.junction_table),
1904 qi(&mutation.source_col),
1905 qi(&mutation.target_col),
1906 ids_name,
1907 ids_name,
1908 qi(iter_col),
1909 ids_name,
1910 qi(&mutation.source_col),
1911 qi(&mutation.target_col),
1912 )
1913}
1914
1915fn emit_ml_append_cte(mutation: &IrMultiLinkMutation, ids_name: &str, ids_alias: &str, cte_name: &str) -> String {
1918 let mut prop_names = vec![];
1919 collect_link_prop_names(&mutation.values, &mut prop_names);
1920 let vals_ref = emit_multilink_values_subquery(&mutation.values, &prop_names);
1921
1922 let extra_cols: String = prop_names.iter().map(|n| format!(", {}", qi(n))).collect();
1923 let extra_select: String = prop_names.iter().map(|n| format!(", \"_v\".{}", qi(n))).collect();
1924
1925 let conflict_clause = if mutation.single {
1929 let mut sets = vec![format!(
1939 "{} = EXCLUDED.{}",
1940 qi(&mutation.target_col),
1941 qi(&mutation.target_col)
1942 )];
1943 sets.extend(prop_names.iter().map(|n| format!("{} = EXCLUDED.{}", qi(n), qi(n))));
1944 format!(
1945 "ON CONFLICT ({}) DO UPDATE SET {}",
1946 qi(&mutation.source_col),
1947 sets.join(", ")
1948 )
1949 } else if prop_names.is_empty() {
1950 "ON CONFLICT DO NOTHING".to_string()
1951 } else {
1952 let sets: Vec<String> = prop_names
1953 .iter()
1954 .map(|n| format!("{} = EXCLUDED.{}", qi(n), qi(n)))
1955 .collect();
1956 format!(
1957 "ON CONFLICT ({}, {}) DO UPDATE SET {}",
1958 qi(&mutation.source_col),
1959 qi(&mutation.target_col),
1960 sets.join(", "),
1961 )
1962 };
1963
1964 let lateral = if vals_ref.starts_with('(') { "LATERAL " } else { "" };
1967 let ins = format!(
1968 "INSERT INTO {} ({}, {}{})\nSELECT {}.\"id\", \"_v\".\"id\"{} FROM \"{}\" AS {} CROSS JOIN {}{} AS \"_v\"\n{}\nRETURNING {}, {}",
1969 qn(&mutation.module, &mutation.junction_table),
1970 qi(&mutation.source_col),
1971 qi(&mutation.target_col),
1972 extra_cols,
1973 qi(ids_alias),
1974 extra_select,
1975 ids_name,
1976 qi(ids_alias),
1977 lateral,
1978 vals_ref,
1979 conflict_clause,
1980 qi(&mutation.source_col),
1981 qi(&mutation.target_col),
1982 );
1983 format!("\"{}\" AS (\n{}\n)", cte_name, ins)
1984}
1985
1986fn emit_ml_remove_cte(mutation: &IrMultiLinkMutation, ids_name: &str, ids_alias: &str, cte_name: &str) -> String {
1989 let vals_ref = emit_multilink_values_subquery(&mutation.values, &[]);
1992 let del = format!(
1996 "DELETE FROM {} AS \"_jn\"\nUSING \"{}\" AS {}\nWHERE \"_jn\".{} = {}.\"id\"\n AND \"_jn\".{} IN (SELECT \"id\" FROM {})\nRETURNING \"_jn\".{}, \"_jn\".{}",
1997 qn(&mutation.module, &mutation.junction_table),
1998 ids_name,
1999 qi(ids_alias),
2000 qi(&mutation.source_col),
2001 qi(ids_alias),
2002 qi(&mutation.target_col),
2003 vals_ref,
2004 qi(&mutation.source_col),
2005 qi(&mutation.target_col),
2006 );
2007 format!("\"{}\" AS (\n{}\n)", cte_name, del)
2008}
2009
2010use crate::ir::IrConflict;
2013
2014fn emit_conflict(sql: &mut String, conflict: &IrConflict) {
2015 let on_sql = conflict.on.as_ref().map(|e| match e {
2019 IrExpr::Tuple(elements) => format!("({})", elements.iter().map(emit_expr).collect::<Vec<_>>().join(", ")),
2020 other => format!("({})", emit_expr(other)),
2021 });
2022 match (&on_sql, &conflict.do_update) {
2023 (None, None) => sql.push_str(" ON CONFLICT DO NOTHING"),
2024 (Some(on), None) => sql.push_str(&format!(" ON CONFLICT {} DO NOTHING", on)),
2025 (None, Some(updates)) => {
2026 sql.push_str(&format!(" ON CONFLICT DO UPDATE SET {}", do_update_sets(updates)));
2027 }
2028 (Some(on), Some(updates)) => {
2029 sql.push_str(&format!(
2030 " ON CONFLICT {} DO UPDATE SET {}",
2031 on,
2032 do_update_sets(updates),
2033 ));
2034 }
2035 }
2036 if conflict.do_update.is_some()
2037 && let Some(predicate) = &conflict.do_update_where
2038 {
2039 sql.push_str(&format!(" WHERE {}", emit_expr(predicate)));
2040 }
2041}
2042
2043fn do_update_sets(updates: &[(String, IrExpr)]) -> String {
2044 updates
2045 .iter()
2046 .map(|(col, expr)| format!("{} = {}", qi(col), emit_expr(expr)))
2047 .collect::<Vec<_>>()
2048 .join(", ")
2049}
2050
2051fn is_integer_expr(expr: &IrExpr) -> bool {
2056 match expr {
2057 IrExpr::ColumnRef { pg_type, .. } => matches!(
2058 pg_type.as_str(),
2059 "int2" | "int4" | "int8" | "integer" | "bigint" | "smallint"
2060 ),
2061 IrExpr::Literal(crate::ir::IrLiteral::Int(_)) => true,
2062 IrExpr::BinOp(op) => is_integer_expr(&op.left) && is_integer_expr(&op.right),
2063 _ => false,
2064 }
2065}
2066
2067fn is_raw_scalar(expr: &IrExpr) -> bool {
2068 matches!(expr, IrExpr::Array(_))
2069 || matches!(expr, IrExpr::TypeCast(c) if c.pg_type == "jsonb")
2070 || matches!(expr, IrExpr::NamedTuple { .. })
2071 || matches!(expr, IrExpr::Tuple(_))
2072 || matches!(expr, IrExpr::JsonbField { .. })
2073 || matches!(expr, IrExpr::JsonbIndex { .. })
2074}
2075
2076fn emit_free_rows(sel: &IrSelect, rows: &[IrRowSource], ctes: &[IrCteDef]) -> SqlOutput {
2082 use crate::query::ShapeNode;
2083
2084 let items: Vec<&IrFreeExpr> = rows
2085 .iter()
2086 .map(|r| match r {
2087 IrRowSource::Free(item) => item,
2088 IrRowSource::Bound { .. } => unreachable!("mixed Bound/Free rows rejected at compile time"),
2089 })
2090 .collect();
2091
2092 if items.is_empty() {
2093 return SqlOutput {
2094 sql: "SELECT NULL AS result WHERE FALSE".to_string(),
2095 shape: ShapeDescriptor {
2096 root: ShapeNode::Scalar {
2097 name: String::new(),
2098 position: 0,
2099 },
2100 },
2101 inference_plan: None,
2102 };
2103 }
2104
2105 if items.len() == 1
2107 && let IrFreeExpr::AssertSet {
2108 fn_name,
2109 inner,
2110 message,
2111 } = items[0]
2112 {
2113 let array_sql = emit_array_source(inner) + &assert_message_arg(message);
2114 let mut sql = format!(
2115 "SELECT ROW(v) AS result FROM unnest(\"_pylon\".{}({})) AS _assert(v)",
2116 fn_name, array_sql,
2117 );
2118 if sel.distinct {
2119 sql = format!("SELECT DISTINCT * FROM ({}) AS \"_distinct\"", sql);
2120 }
2121 append_order_by(&mut sql, &sel.order_by);
2122 append_offset_limit(&mut sql, &sel.offset, &sel.limit);
2123 return SqlOutput {
2124 sql,
2125 shape: ShapeDescriptor {
2126 root: ShapeNode::Scalar {
2127 name: String::new(),
2128 position: 0,
2129 },
2130 },
2131 inference_plan: None,
2132 };
2133 }
2134
2135 let shape_root = free_item_shape(items.first().unwrap(), ctes);
2136
2137 let branches: Vec<String> = items
2138 .iter()
2139 .map(|item| match item {
2140 IrFreeExpr::Scalar(expr) => {
2141 if matches!(expr, IrExpr::ObjectSubquery(_)) {
2146 return format!("SELECT v AS result, v FROM (SELECT {} AS v) AS _obj", emit_expr(expr));
2147 }
2148 if is_raw_scalar(expr) {
2151 format!("SELECT v AS result, v FROM (SELECT {} AS v) AS _raw", emit_expr(expr))
2154 } else {
2155 let e = emit_expr(expr);
2162 let row_value = if enum_type_of_expr(expr).is_some() {
2163 "v::text"
2164 } else {
2165 "v"
2166 };
2167 let present = if items.len() > 1 { " WHERE v IS NOT NULL" } else { "" };
2171 format!("SELECT ROW({row_value}) AS result, v FROM (SELECT {e} AS v) AS _scalar{present}")
2172 }
2173 }
2174 IrFreeExpr::FreeObject(fields) => {
2175 let inner_cols: Vec<String> = fields
2181 .iter()
2182 .enumerate()
2183 .map(|(i, (_, e))| format!("{} AS \"_f{}\"", emit_expr(e), i))
2184 .collect();
2185 let row_items: Vec<String> = fields
2186 .iter()
2187 .enumerate()
2188 .map(|(i, (_, e))| {
2189 if enum_type_of_expr(e).is_some() {
2190 format!("\"_f{}\"::text", i)
2191 } else {
2192 format!("\"_f{}\"", i)
2193 }
2194 })
2195 .collect();
2196 let named_cols: Vec<String> = fields
2197 .iter()
2198 .enumerate()
2199 .map(|(i, (name, _))| format!("\"_f{}\" AS {}", i, qi(name)))
2200 .collect();
2201 format!(
2202 "SELECT ROW({}) AS result, {} FROM (SELECT {}) AS _obj",
2203 row_items.join(", "),
2204 named_cols.join(", "),
2205 inner_cols.join(", "),
2206 )
2207 }
2208 IrFreeExpr::Tuple(exprs) => {
2209 if exprs.len() == 1 {
2210 format!("SELECT ROW({}) AS result", emit_free_field_expr(&exprs[0]))
2211 } else {
2212 let parts: Vec<String> = exprs.iter().map(emit_free_field_expr).collect();
2213 format!("SELECT ({}) AS result", parts.join(", "))
2214 }
2215 }
2216 IrFreeExpr::NamedTupleRow(fields) => {
2217 let parts: Vec<String> = fields.iter().map(|(_, e)| emit_free_field_expr(e)).collect();
2218 if parts.len() == 1 {
2219 format!("SELECT ROW({}) AS result", parts[0])
2220 } else {
2221 format!("SELECT ({}) AS result", parts.join(", "))
2222 }
2223 }
2224 IrFreeExpr::AssertSet { .. } => unreachable!("AssertSet is handled by early return above"),
2225 IrFreeExpr::CtePassthrough(name) => format!("SELECT \"result\" FROM {}", qi(name)),
2226 })
2227 .collect();
2228
2229 let union_sql = branches.join("\nUNION ALL\n");
2230
2231 let mut sql = if sel.distinct {
2232 format!("SELECT DISTINCT * FROM (\n{}\n) AS \"_distinct\"", union_sql)
2234 } else {
2235 union_sql
2236 };
2237 if let Some(filter) = &sel.filter {
2241 sql = format!(
2242 "SELECT * FROM (\n{}\n) AS \"_filtered\"\nWHERE {}",
2243 sql,
2244 emit_expr(filter)
2245 );
2246 }
2247 append_order_by(&mut sql, &sel.order_by);
2248 append_offset_limit(&mut sql, &sel.offset, &sel.limit);
2249
2250 SqlOutput {
2251 sql,
2252 shape: ShapeDescriptor { root: shape_root },
2253 inference_plan: None,
2254 }
2255}
2256
2257fn enum_type_of_expr(expr: &IrExpr) -> Option<QualifiedPgType> {
2267 match expr {
2268 IrExpr::ColumnRef { pg_type, .. } => QualifiedPgType::of(pg_type),
2269 IrExpr::EnumLiteral { pg_type, .. } => Some(QualifiedPgType {
2270 name: pg_quoted_to_pylon(pg_type),
2271 is_array: false,
2272 }),
2273 _ => None,
2274 }
2275}
2276
2277fn enum_type_of_shape_expr(expr: &IrExpr) -> Option<QualifiedPgType> {
2286 if let Some(qualified) = enum_type_of_expr(expr) {
2287 return Some(qualified);
2288 }
2289 let IrExpr::Subquery(select) = expr else {
2290 return None;
2291 };
2292 let [IrRowSource::Bound { shape, .. }] = select.rows.as_slice() else {
2293 return None;
2294 };
2295 let [IrShapePointer::Scalar(scalar)] = shape.as_slice() else {
2296 return None;
2297 };
2298 QualifiedPgType::of(&scalar.pg_type)
2299}
2300
2301fn emit_free_field_expr(expr: &IrExpr) -> String {
2307 match enum_type_of_expr(expr) {
2308 Some(qualified) => format!("{}{}", emit_expr(expr), qualified.text_cast()),
2309 None => emit_expr(expr),
2310 }
2311}
2312
2313fn free_field_shape_node(name: &str, position: usize, expr: &IrExpr) -> crate::query::ShapeNode {
2316 use crate::query::{Cardinality, ShapeNode};
2317 if let IrExpr::ObjectSubquery(sel) = expr {
2318 let [IrRowSource::Bound { source, shape }] = sel.rows.as_slice() else {
2319 unreachable!("an object subquery is always schema-bound")
2320 };
2321 let (_, nodes) = build_shape(shape, &source.alias);
2322 return ShapeNode::Object {
2323 name: name.to_string(),
2324 type_name: Some(source.type_name.clone()),
2325 position,
2326 cardinality: Cardinality::Optional,
2327 pointers: prepend_type(nodes),
2328 has_implicit_id: shape_has_implicit_id(shape),
2329 };
2330 }
2331 if matches!(expr, IrExpr::ObjectPathSubquery(_) | IrExpr::ObjectPathUnion { .. }) {
2333 return expr_shape_node(name, position, expr);
2334 }
2335 if let IrExpr::ArrayFromSelect(source) = expr
2338 && matches!(
2339 source.as_ref(),
2340 IrArraySource::ObjectSelect(_)
2341 | IrArraySource::ObjectFunction(_)
2342 | IrArraySource::PathSelect(_)
2343 | IrArraySource::Group(_)
2344 )
2345 {
2346 return expr_shape_node(name, position, expr);
2347 }
2348 match enum_type_of_shape_expr(expr) {
2349 Some(qualified) => qualified.shape_node(name.to_string(), position),
2350 None => ShapeNode::Scalar {
2351 name: name.to_string(),
2352 position,
2353 },
2354 }
2355}
2356
2357fn expr_shape_node(name: &str, position: usize, expr: &IrExpr) -> crate::query::ShapeNode {
2364 use crate::query::{Cardinality, ShapeNode};
2365 match expr {
2366 IrExpr::ObjectPathUnion { branches, multi, .. } => {
2367 let first = branches.first().expect("a union has at least one branch");
2368 let IrPathResult::Object {
2369 alias,
2370 type_name,
2371 shape,
2372 } = &first.result
2373 else {
2374 unreachable!("an object path union's branches always land on objects")
2375 };
2376 let (_, pointer_nodes) = build_shape(shape, alias);
2377 let object = ShapeNode::Object {
2378 name: if *multi { String::new() } else { name.to_string() },
2379 type_name: Some(type_name.clone()),
2380 position: if *multi { 0 } else { position },
2381 cardinality: if *multi {
2382 Cardinality::Many
2383 } else {
2384 Cardinality::Optional
2385 },
2386 pointers: prepend_type(pointer_nodes),
2387 has_implicit_id: shape_has_implicit_id(shape),
2388 };
2389 if *multi {
2390 ShapeNode::Array {
2391 name: name.to_string(),
2392 position,
2393 element: Box::new(object),
2394 }
2395 } else {
2396 object
2397 }
2398 }
2399 IrExpr::ObjectPathSubquery(ps) => {
2400 let IrPathResult::Object {
2401 alias,
2402 type_name,
2403 shape,
2404 } = &ps.result
2405 else {
2406 unreachable!("an object path subquery always lands on an object")
2407 };
2408 let (_, pointer_nodes) = build_shape(shape, alias);
2409 ShapeNode::Object {
2410 name: name.to_string(),
2411 type_name: Some(type_name.clone()),
2412 position,
2413 cardinality: Cardinality::Optional,
2414 pointers: prepend_type(pointer_nodes),
2415 has_implicit_id: shape_has_implicit_id(shape),
2416 }
2417 }
2418 IrExpr::TypeCast(c) if c.tuple_shape.is_some() => {
2419 let shape = c.tuple_shape.as_ref().unwrap();
2420 ShapeNode::NamedTuple {
2421 name: name.to_string(),
2422 position,
2423 type_name: shape.type_name.clone(),
2424 members: Some(shape.members.clone()),
2425 is_free_object: false,
2426 }
2427 }
2428 IrExpr::TypeCast(c) if c.pg_type == "jsonb" => ShapeNode::Scalar {
2434 name: name.to_string(),
2435 position,
2436 },
2437 IrExpr::NamedTuple { is_free_object, .. } => ShapeNode::NamedTuple {
2438 name: name.to_string(),
2439 position,
2440 type_name: None,
2441 members: None,
2442 is_free_object: *is_free_object,
2443 },
2444 IrExpr::ArrayFromSelect(source) => match source.as_ref() {
2447 IrArraySource::ObjectFunction(fs) => {
2448 let (_, nodes) = build_shape(&fs.shape, &fs.alias);
2449 ShapeNode::Array {
2450 name: name.to_string(),
2451 position,
2452 element: Box::new(ShapeNode::Object {
2453 name: String::new(),
2454 type_name: Some(fs.type_name.clone()),
2455 position: 0,
2456 cardinality: Cardinality::Many,
2457 pointers: prepend_type(nodes),
2458 has_implicit_id: shape_has_implicit_id(&fs.shape),
2459 }),
2460 }
2461 }
2462 IrArraySource::Group(grp) => ShapeNode::Array {
2463 name: name.to_string(),
2464 position,
2465 element: Box::new(emit_group(grp).shape.root),
2466 },
2467 IrArraySource::ObjectSelect(sel) => {
2468 let [IrRowSource::Bound { source, shape }] = sel.rows.as_slice() else {
2469 unreachable!("IrArraySource::ObjectSelect is always schema-bound")
2470 };
2471 let (_, nodes) = build_shape(shape, &source.alias);
2472 ShapeNode::Array {
2473 name: name.to_string(),
2474 position,
2475 element: Box::new(ShapeNode::Object {
2476 name: String::new(),
2477 type_name: Some(source.type_name.clone()),
2478 position: 0,
2479 cardinality: Cardinality::Many,
2480 pointers: prepend_type(nodes),
2481 has_implicit_id: shape_has_implicit_id(shape),
2482 }),
2483 }
2484 }
2485 IrArraySource::PathSelect(ps) => match &ps.result {
2486 IrPathResult::Object {
2487 alias,
2488 type_name,
2489 shape,
2490 } => {
2491 let (_, pointer_nodes) = build_shape(shape, alias);
2492 ShapeNode::Array {
2493 name: name.to_string(),
2494 position,
2495 element: Box::new(ShapeNode::Object {
2496 name: String::new(),
2497 type_name: Some(type_name.clone()),
2498 position: 0,
2499 cardinality: Cardinality::Many,
2500 pointers: prepend_type(pointer_nodes),
2501 has_implicit_id: shape_has_implicit_id(shape),
2502 }),
2503 }
2504 }
2505 IrPathResult::Scalar(..) => ShapeNode::Scalar {
2506 name: name.to_string(),
2507 position,
2508 },
2509 },
2510 _ => ShapeNode::Scalar {
2511 name: name.to_string(),
2512 position,
2513 },
2514 },
2515 e if is_raw_scalar(e) => ShapeNode::RawScalar,
2516 e => free_field_shape_node(name, position, e),
2517 }
2518}
2519
2520fn free_item_shape(item: &IrFreeExpr, ctes: &[IrCteDef]) -> crate::query::ShapeNode {
2521 use crate::query::{Cardinality, ShapeNode};
2522 match item {
2523 IrFreeExpr::Scalar(e) => match e {
2524 IrExpr::TypeCast(c) if c.pg_type == "jsonb" && c.tuple_shape.is_none() => ShapeNode::JsonScalar,
2528 _ => expr_shape_node("", 0, e),
2529 },
2530 IrFreeExpr::FreeObject(fields) => ShapeNode::Object {
2531 name: String::new(),
2532 type_name: None,
2533 position: 0,
2534 cardinality: Cardinality::Many,
2535 pointers: fields
2536 .iter()
2537 .enumerate()
2538 .map(|(i, (name, e))| free_field_shape_node(name, i, e))
2539 .collect(),
2540 has_implicit_id: false,
2541 },
2542 IrFreeExpr::Tuple(exprs) => ShapeNode::Tuple {
2543 position: 0,
2544 elements: exprs
2545 .iter()
2546 .enumerate()
2547 .map(|(i, e)| free_field_shape_node("", i, e))
2548 .collect(),
2549 names: None,
2550 },
2551 IrFreeExpr::NamedTupleRow(fields) => ShapeNode::Tuple {
2552 position: 0,
2553 elements: fields
2554 .iter()
2555 .enumerate()
2556 .map(|(i, (name, e))| free_field_shape_node(name, i, e))
2557 .collect(),
2558 names: Some(fields.iter().map(|(name, _)| name.clone()).collect()),
2559 },
2560 IrFreeExpr::AssertSet { .. } => ShapeNode::Scalar {
2561 name: String::new(),
2562 position: 0,
2563 },
2564 IrFreeExpr::CtePassthrough(name) => ctes
2570 .iter()
2571 .find(|c| &c.name == name)
2572 .and_then(|c| match &c.stmt {
2573 IrStmt::Select(sel) => match sel.rows.first() {
2574 Some(IrRowSource::Free(inner)) => Some(free_item_shape(inner, ctes)),
2575 _ => None,
2576 },
2577 _ => None,
2578 })
2579 .unwrap_or(ShapeNode::Scalar {
2580 name: String::new(),
2581 position: 0,
2582 }),
2583 }
2584}
2585
2586fn emit_path_joins(root: &IrSource, joins: &[IrPathJoin]) -> String {
2589 let mut parts = vec![format!("{} AS {}", source_ref(root), qi(&root.alias))];
2590 for join in joins {
2591 match join {
2592 IrPathJoin::Single {
2593 source_alias,
2594 fk_col,
2595 target,
2596 } => {
2597 parts.push(format!(
2598 "JOIN {} AS {} ON {}.{} = {}.\"id\"",
2599 source_ref(target),
2600 qi(&target.alias),
2601 qi(source_alias),
2602 qi(fk_col),
2603 qi(&target.alias),
2604 ));
2605 }
2606 IrPathJoin::Multi {
2607 source_alias,
2608 junction_alias,
2609 join,
2610 target,
2611 } => {
2612 match join {
2613 IrMultiLinkJoin::Standard { junction_table, module } => {
2614 parts.push(format!(
2615 "JOIN {} AS {} ON {}.\"source\" = {}.\"id\"",
2616 junction_ref(module, junction_table),
2617 qi(junction_alias),
2618 qi(junction_alias),
2619 qi(source_alias),
2620 ));
2621 parts.push(format!(
2622 "JOIN {} AS {} ON {}.\"id\" = {}.\"target\"",
2623 source_ref(target),
2624 qi(&target.alias),
2625 qi(&target.alias),
2626 qi(junction_alias),
2627 ));
2628 }
2629 IrMultiLinkJoin::Through {
2630 junction_table,
2631 module,
2632 source_col,
2633 target_col,
2634 } => {
2635 parts.push(format!(
2636 "JOIN {} AS {} ON {}.{} = {}.\"id\"",
2637 junction_ref(module, junction_table),
2638 qi(junction_alias),
2639 qi(junction_alias),
2640 qi(source_col),
2641 qi(source_alias),
2642 ));
2643 parts.push(format!(
2644 "JOIN {} AS {} ON {}.\"id\" = {}.{}",
2645 source_ref(target),
2646 qi(&target.alias),
2647 qi(&target.alias),
2648 qi(junction_alias),
2649 qi(target_col),
2650 ));
2651 }
2652 IrMultiLinkJoin::BacklinkFk { .. } | IrMultiLinkJoin::BacklinkJunction { .. } => {
2654 unreachable!("a forward multi-link path step never uses a backlink join variant")
2655 }
2656 }
2657 }
2658 IrPathJoin::BacklinkSingle {
2659 source_alias,
2660 fk_col,
2661 target,
2662 } => {
2663 parts.push(format!(
2664 "JOIN {} AS {} ON {}.{} = {}.\"id\"",
2665 source_ref(target),
2666 qi(&target.alias),
2667 qi(&target.alias),
2668 qi(fk_col),
2669 qi(source_alias),
2670 ));
2671 }
2672 IrPathJoin::Function {
2676 fn_module,
2677 fn_name,
2678 args,
2679 target,
2680 } => {
2681 let args_sql = args.iter().map(emit_expr).collect::<Vec<_>>().join(", ");
2682 parts.push(format!(
2683 "CROSS JOIN LATERAL {}.{}({}) AS {}",
2684 pg_schema(fn_module),
2685 qi(fn_name),
2686 args_sql,
2687 qi(&target.alias),
2688 ));
2689 }
2690 IrPathJoin::Lateral { inner, target } => {
2691 let projected = match &inner.result {
2692 IrPathResult::Object { alias, .. } => alias.clone(),
2693 IrPathResult::Scalar(..) => {
2694 unreachable!("a lateral step is only built for an object-valued computed")
2695 }
2696 };
2697 let mut body = format!(
2698 "SELECT {}.* FROM {}",
2699 qi(&projected),
2700 emit_path_joins(&inner.root, &inner.joins)
2701 );
2702 append_filter(&mut body, &inner.filter);
2703 append_order_by(&mut body, &inner.order_by);
2704 append_offset_limit(&mut body, &inner.offset, &inner.limit);
2705 parts.push(format!("JOIN LATERAL (\n{}\n) AS {} ON TRUE", body, qi(&target.alias)));
2706 }
2707 IrPathJoin::BacklinkMulti {
2708 source_alias,
2709 junction_alias,
2710 junction_table,
2711 module,
2712 owner_col,
2713 current_col,
2714 target,
2715 } => {
2716 parts.push(format!(
2717 "JOIN {} AS {} ON {}.{} = {}.\"id\"",
2718 junction_ref(module, junction_table),
2719 qi(junction_alias),
2720 qi(junction_alias),
2721 qi(current_col),
2722 qi(source_alias),
2723 ));
2724 parts.push(format!(
2725 "JOIN {} AS {} ON {}.\"id\" = {}.{}",
2726 source_ref(target),
2727 qi(&target.alias),
2728 qi(&target.alias),
2729 qi(junction_alias),
2730 qi(owner_col),
2731 ));
2732 }
2733 }
2734 }
2735 parts.join("\n")
2736}
2737
2738fn emit_array_source(src: &IrArraySource) -> String {
2740 match src {
2741 IrArraySource::Select(s) => {
2742 let [IrRowSource::Bound { source, shape }] = s.rows.as_slice() else {
2745 unreachable!("IrArraySource::Select is always schema-bound")
2746 };
2747 let scalar = match shape.first() {
2748 Some(IrShapePointer::Scalar(sf)) => format!("{}.{}", qi(&source.alias), qi(&sf.column)),
2749 _ => format!("{}.\"id\"", qi(&source.alias)),
2750 };
2751 let mut sql = format!("SELECT {} FROM {} AS {}", scalar, source_ref(source), qi(&source.alias));
2752 append_filter(&mut sql, &s.filter);
2753 format!("ARRAY({})", sql)
2754 }
2755 IrArraySource::ObjectFunction(fs) => {
2756 let (exprs, _) = build_shape(&fs.shape, &fs.alias);
2757 let mut parts = vec![sql_str(&fs.type_name) + "::text"];
2758 parts.extend(exprs);
2759 let args_sql = fs.fn_args.iter().map(emit_expr).collect::<Vec<_>>().join(", ");
2760 let mut sql = format!(
2761 "SELECT (\n {}\n) FROM {}.{}({}) AS {}",
2762 parts.join(",\n "),
2763 pg_schema(&fs.fn_module),
2764 qi(&fs.fn_name),
2765 args_sql,
2766 qi(&fs.alias),
2767 );
2768 append_filter(&mut sql, &fs.filter);
2769 append_order_by(&mut sql, &fs.order_by);
2770 append_offset_limit(&mut sql, &fs.offset, &fs.limit);
2771 format!("ARRAY({})", sql)
2772 }
2773 IrArraySource::ObjectSelect(s) => {
2774 let [IrRowSource::Bound { source, shape }] = s.rows.as_slice() else {
2775 unreachable!("IrArraySource::ObjectSelect is always schema-bound")
2776 };
2777 let (exprs, _) = build_shape(shape, &source.alias);
2778 let mut parts = vec![source_type_disc(source)];
2779 parts.extend(exprs);
2780 let mut sql = format!(
2781 "SELECT (\n {}\n) FROM {} AS {}",
2782 parts.join(",\n "),
2783 source_ref(source),
2784 qi(&source.alias)
2785 );
2786 append_filter(&mut sql, &s.filter);
2787 append_order_by(&mut sql, &s.order_by);
2788 append_offset_limit(&mut sql, &s.offset, &s.limit);
2789 format!("ARRAY({})", sql)
2790 }
2791 IrArraySource::Group(grp) => format!("ARRAY({})", emit_group(grp).sql),
2792 IrArraySource::StmtColumn { stmt, column } => format!(
2793 "ARRAY(SELECT {} FROM (\n{}\n) AS \"_rows\")",
2794 qi(column),
2795 emit_dml_as_cte_source(stmt),
2796 ),
2797 IrArraySource::PathSelect(ps) => {
2798 let scalar = match &ps.result {
2799 IrPathResult::Scalar(e, _) => emit_expr(e),
2800 IrPathResult::Object {
2804 alias,
2805 type_name,
2806 shape,
2807 } => {
2808 let (pointer_exprs, _) = build_shape(shape, alias);
2809 let mut parts = vec![result_type_disc(ps, alias, type_name)];
2810 parts.extend(pointer_exprs);
2811 format!("(\n {}\n)", parts.join(",\n "))
2812 }
2813 };
2814 let from_sql = emit_path_joins(&ps.root, &ps.joins);
2815 let mut sql = format!(
2816 "SELECT {}{} FROM {}",
2817 if ps.distinct { "DISTINCT " } else { "" },
2818 scalar,
2819 from_sql
2820 );
2821 append_filter(&mut sql, &ps.filter);
2822 append_order_by(&mut sql, &ps.order_by);
2823 append_offset_limit(&mut sql, &ps.offset, &ps.limit);
2824 format!("ARRAY({})", sql)
2825 }
2826 IrArraySource::RawExpr {
2827 source,
2828 poly_implementors,
2829 poly_columns,
2830 expr,
2831 } => {
2832 let from_sql = if !poly_implementors.is_empty() {
2833 format!(
2834 "(\n{}\n) AS {}",
2835 emit_poly_union(poly_implementors, poly_columns),
2836 qi(&source.alias)
2837 )
2838 } else {
2839 format!("{} AS {}", source_ref(source), qi(&source.alias))
2840 };
2841 format!("ARRAY(SELECT {} FROM {})", emit_expr(expr), from_sql)
2842 }
2843 }
2844}
2845
2846fn emit_key_expr(expr: &IrExpr) -> String {
2849 if let IrExpr::ColumnRef { alias, column, pg_type } = expr
2850 && let Some(qualified) = QualifiedPgType::of(pg_type)
2851 {
2852 let col_ref = if alias.is_empty() {
2853 qi(column)
2854 } else {
2855 format!("{}.{}", qi(alias), qi(column))
2856 };
2857 return format!("{}{}", col_ref, qualified.text_cast());
2858 }
2859 emit_expr(expr)
2860}
2861
2862fn group_rows(grp: &IrGroup) -> (String, Option<String>) {
2866 let alias = &grp.source.alias;
2867 if grp.limit.is_none() && grp.offset.is_none() {
2868 return (
2869 format!("{} AS {}", source_ref(&grp.source), qi(alias)),
2870 grp.filter.as_ref().map(emit_expr),
2871 );
2872 }
2873 let ranked = format!(
2874 "(SELECT {}.*, row_number() OVER (PARTITION BY {}{}) AS \"__rk\"\n FROM {} AS {}{}) AS {}",
2875 qi(alias),
2876 group_by_sql(grp),
2877 group_element_order(grp),
2878 source_ref(&grp.source),
2879 qi(alias),
2880 grp.filter
2881 .as_ref()
2882 .map(|f| format!("\n WHERE {}", emit_expr(f)))
2883 .unwrap_or_default(),
2884 qi(alias),
2885 );
2886 let lower = grp.offset.as_ref().map(emit_expr).unwrap_or_else(|| "0".to_string());
2887 let mut conds = vec![format!("\"__rk\" > {}", lower)];
2888 if let Some(l) = &grp.limit {
2889 conds.push(format!("\"__rk\" <= {} + {}", lower, emit_expr(l)));
2890 }
2891 (ranked, Some(conds.join(" AND ")))
2892}
2893
2894fn group_by_sql(grp: &IrGroup) -> String {
2895 grp.keys
2896 .iter()
2897 .map(|(_, key_expr)| emit_expr(key_expr))
2898 .collect::<Vec<_>>()
2899 .join(", ")
2900}
2901
2902fn group_element_order(grp: &IrGroup) -> String {
2903 if grp.order_by.is_empty() {
2904 return String::new();
2905 }
2906 let s: Vec<_> = grp.order_by.iter().map(emit_sort_clause).collect();
2907 format!(" ORDER BY {}", s.join(", "))
2908}
2909
2910fn emit_group_elements_source(grp: &IrGroup) -> String {
2913 let (from_sql, cond) = group_rows(grp);
2914 let mut sql = format!("SELECT {}.*\n FROM {}", qi(&grp.source.alias), from_sql);
2915 if let Some(cond) = cond {
2916 sql.push_str(&format!("\n WHERE {}", cond));
2917 }
2918 sql
2919}
2920
2921fn emit_group(grp: &IrGroup) -> SqlOutput {
2922 match &grp.output {
2923 IrGroupOutput::Groups => emit_group_rows(grp),
2924 IrGroupOutput::Projection(projection) => emit_group_projection(
2925 grp,
2926 &projection.pointers,
2927 &projection.order_by,
2928 &projection.offset,
2929 &projection.limit,
2930 ),
2931 IrGroupOutput::Elements => emit_group_elements(grp),
2932 }
2933}
2934
2935fn emit_group_projection(
2936 grp: &IrGroup,
2937 pointers: &[IrShapePointer],
2938 order_by: &[IrSort],
2939 offset: &Option<IrExpr>,
2940 limit: &Option<IrExpr>,
2941) -> SqlOutput {
2942 let (exprs, nodes) = build_shape(pointers, &grp.source.alias);
2943 let mut parts = vec!["NULL::text".to_string()];
2944 parts.extend(exprs);
2945 let (from_sql, cond) = group_rows(grp);
2946 let mut sql = format!(
2947 "SELECT (\n {}\n) AS \"result\"\nFROM {}",
2948 parts.join(",\n "),
2949 from_sql
2950 );
2951 if let Some(cond) = cond {
2952 sql.push_str(&format!("\nWHERE {}", cond));
2953 }
2954 sql.push_str(&format!("\nGROUP BY {}", group_by_sql(grp)));
2955 if !order_by.is_empty() {
2956 let sorts: Vec<_> = order_by.iter().map(emit_sort_clause).collect();
2957 sql.push_str(&format!("\nORDER BY {}", sorts.join(", ")));
2958 }
2959 append_offset_limit(&mut sql, offset, limit);
2960 SqlOutput {
2961 sql,
2962 shape: ShapeDescriptor {
2963 root: ShapeNode::Object {
2964 name: String::new(),
2965 type_name: None,
2966 position: 0,
2967 cardinality: Cardinality::Many,
2968 pointers: nodes,
2969 has_implicit_id: false,
2970 },
2971 },
2972 inference_plan: None,
2973 }
2974}
2975
2976fn emit_group_elements(grp: &IrGroup) -> SqlOutput {
2977 let (exprs, nodes) = build_shape(&grp.shape, &grp.source.alias);
2978 let mut parts = vec![source_type_disc(&grp.source)];
2979 parts.extend(exprs);
2980 let (from_sql, cond) = group_rows(grp);
2981 let mut sql = format!(
2982 "SELECT (\n {}\n) AS \"result\"\nFROM {}",
2983 parts.join(",\n "),
2984 from_sql
2985 );
2986 if let Some(cond) = cond {
2987 sql.push_str(&format!("\nWHERE {}", cond));
2988 }
2989 SqlOutput {
2990 sql,
2991 shape: ShapeDescriptor {
2992 root: ShapeNode::Object {
2993 name: String::new(),
2994 type_name: Some(grp.source.type_name.clone()),
2995 position: 0,
2996 cardinality: Cardinality::Many,
2997 pointers: prepend_type(nodes),
2998 has_implicit_id: shape_has_implicit_id(&grp.shape),
2999 },
3000 },
3001 inference_plan: None,
3002 }
3003}
3004
3005fn emit_group_rows(grp: &IrGroup) -> SqlOutput {
3006 let alias = &grp.source.alias;
3007 let (shape_exprs, shape_nodes) = build_shape(&grp.shape, alias);
3008
3009 let mut elem_row_parts = vec![source_type_disc(&grp.source)];
3011 elem_row_parts.extend(shape_exprs);
3012 let elem_row = elem_row_parts.join(",\n ");
3013
3014 let n_keys = grp.keys.len();
3016 let grouping_pos = n_keys + 1;
3017 let elements_pos = n_keys + 2;
3018
3019 let mut key_exprs_sql: Vec<String> = vec![];
3021 let mut key_nodes: Vec<ShapeNode> = vec![];
3022 for (i, (key_name, key_expr)) in grp.keys.iter().enumerate() {
3023 let pos = i + 1;
3024 if let IrExpr::ColumnRef { pg_type, .. } = key_expr
3025 && let Some(qualified) = QualifiedPgType::of(pg_type)
3026 {
3027 key_exprs_sql.push(emit_key_expr(key_expr));
3028 key_nodes.push(qualified.shape_node(key_name.clone(), pos));
3029 continue;
3030 }
3031 key_exprs_sql.push(emit_expr(key_expr));
3032 key_nodes.push(ShapeNode::Scalar {
3033 name: key_name.clone(),
3034 position: pos,
3035 });
3036 }
3037
3038 let mut outer_parts = vec!["NULL::text".to_string()];
3040 outer_parts.extend(key_exprs_sql.clone());
3041 let key_names_sql = grp
3042 .keys
3043 .iter()
3044 .map(|(name, _)| format!("'{}'", name))
3045 .collect::<Vec<_>>()
3046 .join(", ");
3047 outer_parts.push(format!("ARRAY[{}]::text[]", key_names_sql));
3048 outer_parts.push(format!(
3049 "array_agg(ROW(\n {}\n )::record{})",
3050 elem_row,
3051 group_element_order(grp)
3052 ));
3053
3054 let outer_tuple = outer_parts.join(",\n ");
3055 let (from_sql, cond) = group_rows(grp);
3056 let mut sql = format!("SELECT (\n {}\n) AS \"result\"\nFROM {}", outer_tuple, from_sql,);
3057 if let Some(cond) = cond {
3058 sql.push_str(&format!("\nWHERE {}", cond));
3059 }
3060 sql.push_str(&format!("\nGROUP BY {}", group_by_sql(grp)));
3061
3062 let element_node = ShapeNode::Object {
3064 name: String::new(),
3065 type_name: Some(grp.source.type_name.clone()),
3066 position: 0,
3067 cardinality: Cardinality::Many,
3068 pointers: prepend_type(shape_nodes),
3069 has_implicit_id: shape_has_implicit_id(&grp.shape),
3070 };
3071
3072 let root = ShapeNode::Group {
3073 key_nodes,
3074 grouping_position: grouping_pos,
3075 elements_position: elements_pos,
3076 element: Box::new(element_node),
3077 };
3078
3079 SqlOutput {
3080 sql,
3081 shape: ShapeDescriptor { root },
3082 inference_plan: None,
3083 }
3084}
3085
3086fn emit_poly_union_type_only(implementors: &[IrPolyImplementor]) -> String {
3087 implementors
3088 .iter()
3089 .map(|imp| {
3090 format!(
3091 " SELECT {}::text AS \"__type__\" FROM {}",
3092 sql_str(&imp.type_name),
3093 qn(&imp.module, &imp.table),
3094 )
3095 })
3096 .collect::<Vec<_>>()
3097 .join("\n UNION ALL\n")
3098}
3099
3100fn emit_path_select(sel: &IrPathSelect) -> SqlOutput {
3101 let distinct = if sel.distinct { "DISTINCT " } else { "" };
3102 let from_sql = if !sel.poly_implementors.is_empty() {
3103 format!(
3104 "(\n{}\n) AS {}",
3105 emit_poly_union_type_only(&sel.poly_implementors),
3106 qi(&sel.root.alias),
3107 )
3108 } else {
3109 emit_path_joins(&sel.root, &sel.joins)
3110 };
3111
3112 let (result_expr, shape_root) = match &sel.result {
3113 IrPathResult::Scalar(ir_expr, tuple_shape) => {
3114 let is_nt = matches!(ir_expr, IrExpr::NamedTuple { .. })
3116 || matches!(ir_expr, IrExpr::Tuple(_))
3117 || matches!(ir_expr, IrExpr::JsonbField { .. })
3118 || matches!(ir_expr, IrExpr::JsonbIndex { .. })
3119 || matches!(ir_expr, IrExpr::ColumnRef { pg_type, .. } if pg_type.starts_with("__nt__:"))
3120 || tuple_shape.is_some();
3121 if is_nt {
3122 let expr_sql = format!("{} AS result", emit_expr(ir_expr));
3123 let shape = if matches!(
3124 ir_expr,
3125 IrExpr::JsonbField { .. } | IrExpr::JsonbIndex { .. } | IrExpr::Tuple(_)
3126 ) {
3127 ShapeNode::RawScalar
3128 } else if let Some(shape) = tuple_shape {
3129 ShapeNode::NamedTuple {
3135 name: String::new(),
3136 position: 0,
3137 type_name: shape.type_name.clone(),
3138 members: Some(shape.members.clone()),
3139 is_free_object: false,
3140 }
3141 } else {
3142 let type_name = match ir_expr {
3143 IrExpr::ColumnRef { pg_type, .. } => pg_type.strip_prefix("__nt__:").map(|s| s.to_string()),
3144 _ => None,
3145 };
3146 ShapeNode::NamedTuple {
3147 name: String::new(),
3148 position: 0,
3149 type_name,
3150 members: None,
3151 is_free_object: false,
3152 }
3153 };
3154 (expr_sql, shape)
3155 } else {
3156 if let IrExpr::ColumnRef { pg_type, .. } = ir_expr {
3159 if let Some(qualified) = QualifiedPgType::of(pg_type) {
3160 let expr = format!("ROW({}{}) AS result", emit_expr(ir_expr), qualified.text_cast());
3161 let shape = qualified.shape_node(String::new(), 0);
3162 (expr, shape)
3163 } else {
3164 let expr = format!("ROW({}) AS result", emit_expr(ir_expr));
3165 (
3166 expr,
3167 ShapeNode::Scalar {
3168 name: String::new(),
3169 position: 0,
3170 },
3171 )
3172 }
3173 } else {
3174 let expr = format!("ROW({}) AS result", emit_expr(ir_expr));
3175 (
3176 expr,
3177 ShapeNode::Scalar {
3178 name: String::new(),
3179 position: 0,
3180 },
3181 )
3182 }
3183 }
3184 }
3185 IrPathResult::Object {
3186 alias,
3187 type_name,
3188 shape,
3189 } => {
3190 let (pointer_exprs, pointer_nodes) = build_shape(shape, alias);
3191 let mut parts = vec![result_type_disc(sel, alias, type_name)];
3192 parts.extend(pointer_exprs);
3193 let expr = format!("(\n {}\n) AS result", parts.join(",\n "));
3194 let shape_root = ShapeNode::Object {
3195 name: String::new(),
3196 type_name: Some(type_name.clone()),
3197 position: 0,
3198 cardinality: Cardinality::Many,
3199 pointers: prepend_type(pointer_nodes),
3200 has_implicit_id: shape_has_implicit_id(shape),
3201 };
3202 (expr, shape_root)
3203 }
3204 };
3205
3206 let mut sql = format!("SELECT {}{}\nFROM {}", distinct, result_expr, from_sql);
3207 append_filter(&mut sql, &sel.filter);
3208 append_order_by(&mut sql, &sel.order_by);
3209 append_offset_limit(&mut sql, &sel.offset, &sel.limit);
3210
3211 SqlOutput {
3212 sql,
3213 shape: ShapeDescriptor { root: shape_root },
3214 inference_plan: None,
3215 }
3216}
3217
3218fn yields_jsonb(expr: &IrExpr) -> bool {
3225 match expr {
3226 IrExpr::JsonbField { .. } | IrExpr::JsonbIndex { .. } | IrExpr::NamedTuple { .. } => true,
3227 IrExpr::TypeCast(c) => c.pg_type == "jsonb",
3228 IrExpr::ColumnRef { pg_type, .. } | IrExpr::FnParam { pg_type, .. } => pg_type == "jsonb",
3229 IrExpr::CteRef { pg_type, .. } => pg_type.as_deref() == Some("jsonb"),
3230 IrExpr::FunctionCall(f) if f.schema.is_none() => {
3231 let mut overloads = crate::stdlib::registry().iter().filter(|d| d.name == f.name).peekable();
3232 overloads.peek().is_some() && overloads.all(|d| matches!(d.return_type, crate::stdlib::PylonType::Json))
3233 }
3234 _ => false,
3235 }
3236}
3237
3238fn emit_for_iterator(it: &IrForIterator, iter_alias: &str) -> (String, String) {
3239 match it {
3240 IrForIterator::Values { exprs, pg_type } => {
3241 let rows: Vec<String> = exprs
3242 .iter()
3243 .map(|e| format!("({}::{})", emit_expr(e), pg_type))
3244 .collect();
3245 (
3246 format!("(VALUES {}) AS {}(\"v\")", rows.join(", "), qi(iter_alias)),
3247 format!("{}(\"v\") AS (VALUES {})", qi(iter_alias), rows.join(", ")),
3248 )
3249 }
3250 IrForIterator::Query { stmt, scalar } => {
3251 let inner = emit_dml_as_cte_source(stmt);
3252 let body = if *scalar {
3256 inner
3257 } else {
3258 format!(" SELECT \"id\" AS v FROM (\n{}\n ) AS _src", inner)
3259 };
3260 (
3261 format!("(\n{}\n) AS {}", body, qi(iter_alias)),
3262 format!("{} AS (\n{}\n)", qi(iter_alias), body),
3263 )
3264 }
3265 IrForIterator::SetReturning { expr, pg_type } => {
3266 let body = format!(" SELECT {}::{} AS v", emit_expr(expr), pg_type);
3267 (
3268 format!("(\n{}\n) AS {}", body, qi(iter_alias)),
3269 format!("{} AS (\n{}\n)", qi(iter_alias), body),
3270 )
3271 }
3272 }
3273}
3274
3275fn emit_nested_for_iterator(it: &IrForIterator, iter_alias: &str, outer_alias: &str) -> String {
3280 const OUTER: &str = "_outer";
3281 match it {
3282 IrForIterator::Values { exprs, pg_type } => {
3283 let rows: Vec<String> = exprs
3284 .iter()
3285 .map(|e| format!("({}::{})", emit_expr(e), pg_type))
3286 .collect();
3287 format!(
3288 "{} AS (\nSELECT {}.\"v\" AS {}, \"_vals\".\"v\" AS v\nFROM {}, (VALUES {}) AS \"_vals\"(\"v\")\n)",
3289 qi(iter_alias),
3290 qi(outer_alias),
3291 qi(OUTER),
3292 qi(outer_alias),
3293 rows.join(", "),
3294 )
3295 }
3296 IrForIterator::Query { stmt, scalar } => {
3297 let inner = emit_dml_as_cte_source(stmt);
3298 let projected = if *scalar { "\"_src\".\"v\"" } else { "\"_src\".\"id\"" };
3299 format!(
3300 "{} AS (\nSELECT {}.\"v\" AS {}, {} AS v\nFROM {}\nCROSS JOIN LATERAL (\n{}\n) AS \"_src\"\n)",
3301 qi(iter_alias),
3302 qi(outer_alias),
3303 qi(OUTER),
3304 projected,
3305 qi(outer_alias),
3306 inner,
3307 )
3308 }
3309 IrForIterator::SetReturning { expr, pg_type } => format!(
3310 "{} AS (\nSELECT {}.\"v\" AS {}, \"_vals\".\"v\" AS v\nFROM {}, LATERAL (SELECT {}::{} AS v) AS \"_vals\"\n)",
3311 qi(iter_alias),
3312 qi(outer_alias),
3313 qi(OUTER),
3314 qi(outer_alias),
3315 emit_expr(expr),
3316 pg_type,
3317 ),
3318 }
3319}
3320
3321fn nested_for_from(inner_alias: &str, outer_alias: &str) -> String {
3324 format!(
3325 "{} JOIN {} ON {}.\"v\" = {}.\"_outer\"",
3326 qi(inner_alias),
3327 qi(outer_alias),
3328 qi(outer_alias),
3329 qi(inner_alias),
3330 )
3331}
3332
3333fn uncorrelated_body_ctes(f: &IrFor) -> Vec<IrCteDef> {
3347 f.body_ctes
3348 .iter()
3349 .map(|cte| IrCteDef {
3350 correlated_to: None,
3351 ..cte.clone()
3352 })
3353 .collect()
3354}
3355
3356fn prefix_body_ctes(sql: String, body_ctes: &[IrCteDef]) -> String {
3357 if body_ctes.is_empty() {
3358 return sql;
3359 }
3360 merge_into_existing_with(&sql, &emit_user_cte_parts(body_ctes))
3361 .unwrap_or_else(|| format!("{}{}", emit_cte_prefix(body_ctes), sql))
3362}
3363
3364fn emit_for_stmt(f: &IrFor, user_ctes: &[IrCteDef]) -> SqlOutput {
3365 let iter_alias = format!("_for_{}", f.var_name);
3366
3367 if matches!(&f.iterator, IrForIterator::Values { exprs, .. } if exprs.is_empty()) {
3368 let empty = SqlOutput {
3369 sql: "SELECT NULL AS result WHERE FALSE".to_string(),
3370 shape: ShapeDescriptor {
3371 root: ShapeNode::Scalar {
3372 name: String::new(),
3373 position: 0,
3374 },
3375 },
3376 inference_plan: None,
3377 };
3378 return empty;
3379 }
3380
3381 let (values_from, iter_cte) = emit_for_iterator(&f.iterator, &iter_alias);
3382
3383 match f.body.as_ref() {
3384 IrStmt::Insert(ins) => emit_for_insert(ins, &iter_alias, &iter_cte, user_ctes, &f.body_ctes),
3385 IrStmt::Update(upd) => emit_for_update(upd, &iter_alias, &iter_cte, user_ctes, &f.body_ctes),
3386 body => {
3387 let body_out = match body {
3388 IrStmt::Select(sel) => emit_select_stmt(sel, user_ctes),
3389 IrStmt::PathSelect(sel) => emit_path_select(sel),
3390 other => unreachable!("for-loop body should have been rejected at compile time: {other:?}"),
3393 };
3394 let indent_body = prefix_body_ctes(body_out.sql, &uncorrelated_body_ctes(f)).replace('\n', "\n ");
3395 let cte_prefix = if !user_ctes.is_empty() {
3396 emit_cte_prefix(user_ctes)
3397 } else {
3398 String::new()
3399 };
3400 let sql = format!(
3401 "{}SELECT \"_body\".result\nFROM {}\nCROSS JOIN LATERAL (\n {}\n) AS \"_body\"",
3402 cte_prefix, values_from, indent_body,
3403 );
3404 SqlOutput {
3405 sql,
3406 shape: body_out.shape,
3407 inference_plan: None,
3408 }
3409 }
3410 }
3411}
3412
3413fn emit_for_update(
3418 upd: &IrUpdate,
3419 iter_alias: &str,
3420 iter_cte: &str,
3421 user_ctes: &[IrCteDef],
3422 body_ctes: &[IrCteDef],
3423) -> SqlOutput {
3424 let mut cte_parts: Vec<String> = emit_user_cte_parts(user_ctes);
3425 cte_parts.extend(emit_user_cte_parts(body_ctes));
3426 cte_parts.push(iter_cte.to_string());
3427
3428 let alias = &upd.target.alias;
3429 if upd.assignments.is_empty() && upd.rewrites.is_empty() && !upd.multi_link_appends.is_empty() {
3433 const ITER_COL: &str = "_iter";
3434 let ids_name = "_ids";
3435 let generated: Vec<Option<&IrInsert>> = upd
3439 .multi_link_appends
3440 .iter()
3441 .map(|a| per_iteration_insert(a, &upd.nested_ctes))
3442 .collect();
3443 let new_cols: String = generated
3444 .iter()
3445 .enumerate()
3446 .filter_map(|(i, ins)| {
3447 ins.map(|ins| {
3448 format!(
3449 ", {} AS {}",
3450 ins.id_default_sql.as_deref().unwrap_or("uuidv7()"),
3451 qi(&format!("_new_{i}"))
3452 )
3453 })
3454 })
3455 .collect();
3456 let mut ids_sql = format!(
3457 "\"{}\" AS (\nSELECT {}.*, {}.\"v\" AS {}{} FROM {} AS {}, {}",
3458 ids_name,
3459 qi(alias),
3460 qi(iter_alias),
3461 qi(ITER_COL),
3462 new_cols,
3463 target_ref(&upd.target),
3464 qi(alias),
3465 qi(iter_alias),
3466 );
3467 append_filter(&mut ids_sql, &upd.filter);
3468 ids_sql.push_str("\n)");
3469 cte_parts.push(ids_sql);
3470 for (i, append) in upd.multi_link_appends.iter().enumerate() {
3471 match generated[i] {
3472 Some(ins) => {
3473 cte_parts.push(emit_for_nested_insert_cte(
3474 ins,
3475 ids_name,
3476 iter_alias,
3477 &format!("_ml_ins_{i}"),
3478 &format!("_new_{i}"),
3479 ));
3480 cte_parts.push(emit_for_ml_append_cte(
3481 append,
3482 ids_name,
3483 &format!("_ml_add_{i}"),
3484 &format!("_new_{i}"),
3485 ));
3486 }
3487 None => cte_parts.push(emit_for_ml_append_cte(
3488 append,
3489 ids_name,
3490 &format!("_ml_add_{i}"),
3491 ITER_COL,
3492 )),
3493 }
3494 }
3495 let (pointer_exprs, shape_nodes) = build_shape(&upd.returning, ids_name);
3496 let mut parts = vec![type_disc(&upd.target.type_name)];
3497 parts.extend(pointer_exprs);
3498 let sql = format!(
3499 "WITH {}\nSELECT (\n {}\n) AS result\nFROM \"{}\"",
3500 cte_parts.join(",\n"),
3501 parts.join(",\n "),
3502 ids_name,
3503 );
3504 return SqlOutput {
3505 sql,
3506 shape: crate::query::ShapeDescriptor {
3507 root: ShapeNode::Object {
3508 name: String::new(),
3509 type_name: Some(upd.target.type_name.clone()),
3510 position: 0,
3511 cardinality: Cardinality::Many,
3512 pointers: prepend_type(shape_nodes),
3513 has_implicit_id: shape_has_implicit_id(&upd.returning),
3514 },
3515 },
3516 inference_plan: None,
3517 };
3518 }
3519 let sets: Vec<String> = upd
3520 .assignments
3521 .iter()
3522 .map(|(col, expr)| format!("{} = {}", qi(col), emit_expr(expr)))
3523 .chain(
3524 upd.rewrites
3525 .iter()
3526 .map(|rw| format!("{} = {}", qi(&rw.column), emit_expr(&rw.expr))),
3527 )
3528 .collect();
3529 let mut sql = format!(
3530 "WITH {}\nUPDATE {} AS {}\nSET {}\nFROM {}",
3531 cte_parts.join(",\n"),
3532 target_ref(&upd.target),
3533 qi(alias),
3534 sets.join(", "),
3535 qi(iter_alias),
3536 );
3537 append_filter(&mut sql, &upd.filter);
3538 let mut parts = vec![type_disc(&upd.target.type_name)];
3539 let (pointer_exprs, shape_nodes) = build_shape(&upd.returning, alias);
3540 parts.extend(pointer_exprs);
3541 sql.push_str(&format!("\nRETURNING (\n {}\n) AS result", parts.join(",\n ")));
3542 SqlOutput {
3543 sql,
3544 shape: crate::query::ShapeDescriptor {
3545 root: ShapeNode::Object {
3546 name: String::new(),
3547 type_name: Some(upd.target.type_name.clone()),
3548 position: 0,
3549 cardinality: Cardinality::Many,
3550 pointers: prepend_type(shape_nodes),
3551 has_implicit_id: shape_has_implicit_id(&upd.returning),
3552 },
3553 },
3554 inference_plan: None,
3555 }
3556}
3557
3558const OUTER_KEY: &str = "_outer";
3563
3564fn emit_iteration_rows_cte(ins: &IrInsert, cte_name: &str, iter_alias: &str, outer_source: &str) -> String {
3569 let rewrite_cols: std::collections::HashSet<&str> = ins.rewrites.iter().map(|r| r.column.as_str()).collect();
3570 let mut selected = vec![
3571 format!(
3572 "{} AS {}",
3573 ins.id_default_sql.as_deref().unwrap_or("uuidv7()"),
3574 qi("id")
3575 ),
3576 format!("{} AS {}", outer_source, qi(OUTER_KEY)),
3577 ];
3578 selected.extend(
3579 ins.assignments
3580 .iter()
3581 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
3582 .map(|(c, e)| format!("{} AS {}", emit_expr(e), qi(c)))
3583 .chain(
3584 ins.rewrites
3585 .iter()
3586 .map(|r| format!("{} AS {}", emit_expr(&r.expr), qi(&r.column))),
3587 ),
3588 );
3589 format!(
3590 "\"{}\" AS (\nSELECT {}\nFROM {}\n)",
3591 cte_name,
3592 selected.join(", "),
3593 qi(iter_alias),
3594 )
3595}
3596
3597fn emit_insert_from_rows_cte(ins: &IrInsert, cte_name: &str, rows_name: &str) -> String {
3599 let rewrite_cols: std::collections::HashSet<&str> = ins.rewrites.iter().map(|r| r.column.as_str()).collect();
3600 let cols: Vec<String> = std::iter::once(qi("id"))
3601 .chain(
3602 ins.assignments
3603 .iter()
3604 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
3605 .map(|(c, _)| qi(c)),
3606 )
3607 .chain(ins.rewrites.iter().map(|r| qi(&r.column)))
3608 .collect();
3609 let mut sql = format!(
3610 "INSERT INTO {} ({})\nSELECT {} FROM \"{}\"",
3611 target_ref(&ins.target),
3612 cols.join(", "),
3613 cols.join(", "),
3614 rows_name,
3615 );
3616 if let Some(conflict) = &ins.unless_conflict {
3617 emit_conflict(&mut sql, conflict);
3618 }
3619 sql.push_str("\nRETURNING *");
3620 format!("\"{}\" AS (\n{}\n)", cte_name, sql)
3621}
3622
3623fn emit_correlated_ml_append_cte(
3627 mutation: &IrMultiLinkMutation,
3628 rows_name: &str,
3629 targets_name: &str,
3630 cte_name: &str,
3631) -> String {
3632 format!(
3633 "\"{}\" AS (\nINSERT INTO {} ({}, {})\nSELECT \"_src\".{}, \"_tgt\".{} FROM \"{}\" AS \"_src\" JOIN \"{}\" AS \"_tgt\" ON \"_tgt\".{} = \"_src\".{}\nON CONFLICT DO NOTHING\nRETURNING {}, {}\n)",
3634 cte_name,
3635 qn(&mutation.module, &mutation.junction_table),
3636 qi(&mutation.source_col),
3637 qi(&mutation.target_col),
3638 qi("id"),
3639 qi("id"),
3640 rows_name,
3641 targets_name,
3642 qi(OUTER_KEY),
3643 qi(OUTER_KEY),
3644 qi(&mutation.source_col),
3645 qi(&mutation.target_col),
3646 )
3647}
3648
3649fn correlated_append_indices(appends: &[IrMultiLinkMutation], defs: &[&[IrCteDef]]) -> Vec<usize> {
3652 appends
3653 .iter()
3654 .enumerate()
3655 .filter(|(_, append)| match &append.values.source {
3656 IrMultiLinkValueSource::CteRef(target) => defs
3657 .iter()
3658 .flat_map(|group| group.iter())
3659 .any(|c| &c.name == target && c.correlated_to.is_some()),
3660 _ => false,
3661 })
3662 .map(|(i, _)| i)
3663 .collect()
3664}
3665
3666fn emit_for_insert(
3667 ins: &IrInsert,
3668 iter_alias: &str,
3669 iter_cte: &str,
3670 user_ctes: &[IrCteDef],
3671 body_ctes: &[IrCteDef],
3672) -> SqlOutput {
3673 let rewrite_cols: std::collections::HashSet<&str> = ins.rewrites.iter().map(|r| r.column.as_str()).collect();
3674
3675 let cols: Vec<String> = ins
3676 .assignments
3677 .iter()
3678 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
3679 .map(|(c, _)| qi(c))
3680 .chain(ins.rewrites.iter().map(|r| qi(&r.column)))
3681 .collect();
3682 let sel_exprs: Vec<String> = ins
3683 .assignments
3684 .iter()
3685 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
3686 .map(|(_, e)| emit_expr(e))
3687 .chain(ins.rewrites.iter().map(|r| emit_expr(&r.expr)))
3688 .collect();
3689
3690 let mut cte_parts: Vec<String> = emit_user_cte_parts(user_ctes);
3691 cte_parts.push(iter_cte.to_string());
3695 cte_parts.extend(emit_user_cte_parts(body_ctes));
3696 cte_parts.extend(emit_user_cte_parts(&ins.nested_ctes));
3697 let correlated_targets = correlated_append_indices(&ins.multi_link_appends, &[body_ctes, &ins.nested_ctes]);
3700
3701 let mut insert_sql = format!(
3702 "INSERT INTO {} ({})\nSELECT {} FROM {}",
3703 target_ref(&ins.target),
3704 cols.join(", "),
3705 sel_exprs.join(", "),
3706 qi(iter_alias),
3707 );
3708 if let Some(conflict) = &ins.unless_conflict {
3709 emit_conflict(&mut insert_sql, conflict);
3710 }
3711 let (shape, returning_sql) = emit_returning_shape(&ins.target, &ins.returning, false);
3712
3713 if ins.multi_link_appends.is_empty() {
3714 let mut sql = format!("WITH {}\n{}", cte_parts.join(",\n"), insert_sql);
3715 if let Some(r) = returning_sql {
3716 sql.push_str(&r);
3717 }
3718 return SqlOutput {
3719 sql,
3720 shape,
3721 inference_plan: None,
3722 };
3723 }
3724
3725 const IDS: &str = "_for_dml__ids";
3728 const ROWS: &str = "_for_dml__rows";
3729 if correlated_targets.is_empty() {
3730 insert_sql.push_str("\nRETURNING *");
3731 cte_parts.push(format!("\"{}\" AS (\n{}\n)", IDS, insert_sql));
3732 } else {
3733 let outer_source = format!("{}.{}", qi(iter_alias), qi("v"));
3734 cte_parts.push(emit_iteration_rows_cte(ins, ROWS, iter_alias, &outer_source));
3735 cte_parts.push(emit_insert_from_rows_cte(ins, IDS, ROWS));
3736 }
3737 for (i, append) in ins.multi_link_appends.iter().enumerate() {
3738 let cte_name = format!("_for_dml__ml_add_{}", i);
3739 if correlated_targets.contains(&i) {
3740 let IrMultiLinkValueSource::CteRef(target) = &append.values.source else {
3741 unreachable!("only a CTE-backed value can carry an iteration key")
3742 };
3743 cte_parts.push(emit_correlated_ml_append_cte(append, ROWS, target, &cte_name));
3744 } else {
3745 cte_parts.push(emit_ml_append_cte(append, IDS, &ins.target.alias, &cte_name));
3746 }
3747 }
3748
3749 let projection = match &returning_sql {
3750 Some(r) => r.trim_start_matches('\n').replacen("RETURNING ", "SELECT ", 1),
3753 None => "SELECT 1".to_string(),
3754 };
3755 SqlOutput {
3756 sql: format!("WITH {}\n{}\nFROM \"{}\"", cte_parts.join(",\n"), projection, IDS),
3757 shape,
3758 inference_plan: None,
3759 }
3760}
3761
3762fn enqueue_cte_sql(eq: &VectorEnqueueInfo, source_cte: &str, cte_name: &str) -> String {
3768 let index_name_sql = match &eq.index_name {
3769 None => "NULL".to_string(),
3770 Some(name) => sql_str(name),
3771 };
3772 format!(
3773 concat!(
3774 "\"{}\" AS (\n",
3775 " INSERT INTO _pylon.\"IndexOutbox\"\n",
3776 " (object_id, type_name, index_kind, index_name)\n",
3777 " SELECT \"id\", {}, 'Vector'::_pylon.\"IndexKind\", {}\n",
3778 " FROM \"{}\"\n",
3779 " ON CONFLICT (object_id, index_kind, index_name)\n",
3780 " DO UPDATE SET status = 'Pending', enqueued_at = now()\n",
3781 ")",
3782 ),
3783 cte_name,
3784 sql_str(&eq.type_name),
3785 index_name_sql,
3786 source_cte,
3787 )
3788}
3789
3790fn enqueue_ctes(enqueue: &[VectorEnqueueInfo], source_cte: &str) -> Vec<String> {
3792 enqueue
3793 .iter()
3794 .enumerate()
3795 .map(|(i, eq)| enqueue_cte_sql(eq, source_cte, &format!("_eq{}", i)))
3796 .collect()
3797}
3798
3799fn search_backend_index_kind(backend: &crate::schema::SearchBackend) -> &'static str {
3803 match backend {
3804 crate::schema::SearchBackend::OpenSearch => "OpenSearch",
3805 crate::schema::SearchBackend::Meilisearch => "Meilisearch",
3806 crate::schema::SearchBackend::Postgres => {
3807 unreachable!("Postgres-backed search indexes are never collected into SearchEnqueueInfo")
3808 }
3809 }
3810}
3811
3812fn enqueue_search_cte_sql(eq: &SearchEnqueueInfo, source_cte: &str, cte_name: &str) -> String {
3814 let index_name_sql = match &eq.index_name {
3815 None => "NULL".to_string(),
3816 Some(name) => sql_str(name),
3817 };
3818 format!(
3819 concat!(
3820 "\"{}\" AS (\n",
3821 " INSERT INTO _pylon.\"IndexOutbox\"\n",
3822 " (object_id, type_name, index_kind, index_name, operation)\n",
3823 " SELECT \"id\", {}, '{}'::_pylon.\"IndexKind\", {}, {}\n",
3824 " FROM \"{}\"\n",
3825 " ON CONFLICT (object_id, index_kind, index_name)\n",
3826 " DO UPDATE SET status = 'Pending', operation = EXCLUDED.operation, enqueued_at = now()\n",
3827 ")",
3828 ),
3829 cte_name,
3830 sql_str(&eq.type_name),
3831 search_backend_index_kind(&eq.backend),
3832 index_name_sql,
3833 sql_str(eq.operation),
3834 source_cte,
3835 )
3836}
3837
3838fn enqueue_search_ctes(enqueue: &[SearchEnqueueInfo], source_cte: &str, offset: usize) -> Vec<String> {
3840 enqueue
3841 .iter()
3842 .enumerate()
3843 .map(|(i, eq)| enqueue_search_cte_sql(eq, source_cte, &format!("_es{}", offset + i)))
3844 .collect()
3845}
3846
3847fn shape_select_from_cte(
3850 target: &IrSource,
3851 returning: &[IrShapePointer],
3852 cte_name: &str,
3853) -> (ShapeDescriptor, Option<String>) {
3854 if returning.is_empty() {
3855 return (
3856 ShapeDescriptor {
3857 root: ShapeNode::Scalar {
3858 name: String::new(),
3859 position: 0,
3860 },
3861 },
3862 None,
3863 );
3864 }
3865 let (pointer_exprs, shape_pointers) = build_shape(returning, "");
3866 let mut parts = vec![type_disc(&target.type_name)];
3867 parts.extend(pointer_exprs);
3868 let tuple = parts.join(",\n ");
3869 let sql = format!("SELECT (\n {}\n) AS result\nFROM {}", tuple, qi(cte_name));
3870 let root_pointers = prepend_type(shape_pointers);
3871 let shape = ShapeDescriptor {
3872 root: ShapeNode::Object {
3873 name: String::new(),
3874 type_name: Some(target.type_name.clone()),
3875 position: 0,
3876 cardinality: Cardinality::Required,
3877 pointers: root_pointers,
3878 has_implicit_id: shape_has_implicit_id(returning),
3879 },
3880 };
3881 (shape, Some(sql))
3882}
3883
3884fn emit_insert_row_sql(ins: &IrInsert) -> String {
3893 let rewrite_cols: std::collections::HashSet<&str> = ins.rewrites.iter().map(|r| r.column.as_str()).collect();
3894 let cols: Vec<String> = ins
3895 .assignments
3896 .iter()
3897 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
3898 .map(|(c, _)| qi(c))
3899 .chain(ins.rewrites.iter().map(|r| qi(&r.column)))
3900 .collect();
3901 let vals: Vec<String> = ins
3902 .assignments
3903 .iter()
3904 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
3905 .map(|(_, e)| emit_expr(e))
3906 .chain(ins.rewrites.iter().map(|r| emit_expr(&r.expr)))
3907 .collect();
3908 if let Some(guard) = &ins.guard {
3911 let from_ctes = nested_cte_from(&ins.nested_ctes, &vals.join(","), " ");
3912 let projection = if cols.is_empty() {
3913 String::new()
3914 } else {
3915 format!(" {}", vals.join(", "))
3916 };
3917 let column_list = if cols.is_empty() {
3918 String::new()
3919 } else {
3920 format!(" ({})", cols.join(", "))
3921 };
3922 return format!(
3923 "INSERT INTO {}{} SELECT{}{} WHERE {}",
3924 target_ref(&ins.target),
3925 column_list,
3926 projection,
3927 from_ctes,
3928 emit_expr(guard),
3929 );
3930 }
3931 if cols.is_empty() && ins.nested_ctes.is_empty() {
3932 format!("INSERT INTO {} DEFAULT VALUES", target_ref(&ins.target))
3935 } else if ins.nested_ctes.is_empty() {
3936 format!(
3937 "INSERT INTO {} ({}) VALUES ({})",
3938 target_ref(&ins.target),
3939 cols.join(", "),
3940 vals.join(", ")
3941 )
3942 } else {
3943 format!(
3944 "INSERT INTO {} ({}) SELECT {}{}",
3945 target_ref(&ins.target),
3946 cols.join(", "),
3947 vals.join(", "),
3948 nested_cte_from(&ins.nested_ctes, &vals.join(","), " "),
3949 )
3950 }
3951}
3952
3953fn emit_insert_stmt(ins: &IrInsert) -> SqlOutput {
3954 if ins.enqueue_vector.is_empty()
3955 && ins.enqueue_search.is_empty()
3956 && !insert_has_any_multilink(ins)
3957 && ins.nested_ctes.is_empty()
3958 {
3959 let mut sql = emit_insert_row_sql(ins);
3960 if let Some(conflict) = &ins.unless_conflict {
3961 emit_conflict(&mut sql, conflict);
3962 }
3963 let (shape, returning_sql) = emit_returning_shape(&ins.target, &ins.returning, false);
3964 if let Some(r) = returning_sql {
3965 sql.push_str(&r);
3966 }
3967 return SqlOutput {
3968 sql,
3969 shape,
3970 inference_plan: None,
3971 };
3972 }
3973
3974 let mut cte_parts = if insert_has_any_multilink(ins) {
3980 emit_insert_multilink_ctes(ins, "_w")
3981 } else {
3982 let mut cte_parts = emit_user_cte_parts(&ins.nested_ctes);
3983 let mut insert_sql = emit_insert_row_sql(ins);
3984 if let Some(conflict) = &ins.unless_conflict {
3985 emit_conflict(&mut insert_sql, conflict);
3986 }
3987 insert_sql.push_str("\nRETURNING \"id\"");
3988 cte_parts.push(format!("\"_w\" AS (\n{}\n)", insert_sql));
3989 cte_parts
3990 };
3991 cte_parts.extend(enqueue_ctes(&ins.enqueue_vector, "_w"));
3992 cte_parts.extend(enqueue_search_ctes(&ins.enqueue_search, "_w", ins.enqueue_vector.len()));
3993
3994 let (shape, select_sql) = shape_select_from_cte(&ins.target, &ins.returning, "_w");
3995 let sql = format!(
3996 "WITH\n{}\n{}",
3997 cte_parts.join(",\n"),
3998 select_sql.unwrap_or_else(|| "SELECT * FROM \"_w\"".to_string()),
3999 );
4000 SqlOutput {
4001 sql,
4002 shape,
4003 inference_plan: None,
4004 }
4005}
4006
4007fn emit_poly_update_stmt(upd: &IrUpdate, user_ctes: &[IrCteDef]) -> SqlOutput {
4010 let alias = &upd.target.alias;
4011 let sets = update_set_fragments(&upd.assignments, &upd.rewrites, "");
4012
4013 let mut cte_parts: Vec<String> = emit_user_cte_parts(user_ctes);
4019 cte_parts.extend(emit_user_cte_parts(&upd.nested_ctes));
4020 let mut union_parts = vec![];
4021 let from_ctes = nested_cte_from(&upd.nested_ctes, &sets.join(","), "\n");
4022
4023 let has_any_multilink = !upd.multi_link_clears.is_empty()
4024 || !upd.multi_link_replaces.is_empty()
4025 || !upd.multi_link_appends.is_empty()
4026 || !upd.multi_link_removals.is_empty();
4027 for (i, imp) in upd.poly_implementors.iter().enumerate() {
4028 let cte_name = format!("_u{}", i);
4029 if has_any_multilink {
4033 cte_parts.extend(emit_update_multilink_cte_parts(
4034 &concrete_poly_update(upd, imp),
4035 &cte_name,
4036 false,
4037 ));
4038 } else {
4039 let mut upd_sql = format!(
4040 "UPDATE {} AS {}\nSET {}{}",
4041 qn(&imp.module, &imp.table),
4042 qi(alias),
4043 sets.join(", "),
4044 from_ctes,
4045 );
4046 append_filter(&mut upd_sql, &upd.filter);
4047 upd_sql.push_str(&format!("\nRETURNING {}.\"id\"", qi(alias)));
4048 cte_parts.push(format!("\"{}\" AS (\n{}\n)", cte_name, upd_sql));
4049 }
4050
4051 let r_alias = format!("_r{}", i);
4052 union_parts.push(format!(
4053 "SELECT ROW({}::text, {}.\"id\") AS result FROM \"{}\" AS {}",
4054 sql_str(&imp.type_name),
4055 qi(&r_alias),
4056 cte_name,
4057 qi(&r_alias),
4058 ));
4059 }
4060
4061 let sql = format!("WITH\n{}\n{}", cte_parts.join(",\n"), union_parts.join("\nUNION ALL\n"),);
4062
4063 let (shape, _) = emit_returning_shape(&upd.target, &upd.returning, true);
4064 SqlOutput {
4065 sql,
4066 shape,
4067 inference_plan: None,
4068 }
4069}
4070
4071fn emit_update_stmt(upd: &IrUpdate, user_ctes: &[IrCteDef]) -> SqlOutput {
4072 if !upd.poly_implementors.is_empty() {
4073 return emit_poly_update_stmt(upd, user_ctes);
4074 }
4075 let alias = &upd.target.alias;
4076 let (shape, returning_sql) = emit_returning_shape(&upd.target, &upd.returning, true);
4077
4078 let has_any_multilink = !upd.multi_link_clears.is_empty()
4079 || !upd.multi_link_replaces.is_empty()
4080 || !upd.multi_link_appends.is_empty()
4081 || !upd.multi_link_removals.is_empty();
4082
4083 if !has_any_multilink && upd.enqueue_vector.is_empty() && upd.enqueue_search.is_empty() {
4084 let sets = update_set_fragments(&upd.assignments, &upd.rewrites, "");
4091 let mut sql = format!(
4092 "UPDATE {} AS {}\nSET {}",
4093 target_ref(&upd.target),
4094 qi(alias),
4095 sets.join(", "),
4096 );
4097 sql.push_str(&nested_cte_from(&upd.nested_ctes, &sets.join(","), "\n"));
4098 append_filter(&mut sql, &upd.filter);
4099 if let Some(r) = returning_sql {
4100 sql.push_str(&r);
4101 }
4102 let combined_ctes: Vec<IrCteDef> = user_ctes
4105 .iter()
4106 .cloned()
4107 .chain(upd.nested_ctes.iter().cloned())
4108 .collect();
4109 if !combined_ctes.is_empty() {
4110 sql = format!("{}{}", emit_cte_prefix(&combined_ctes), sql);
4111 }
4112 return SqlOutput {
4113 sql,
4114 shape,
4115 inference_plan: None,
4116 };
4117 }
4118
4119 if !has_any_multilink && (!upd.enqueue_vector.is_empty() || !upd.enqueue_search.is_empty()) {
4120 let sets = update_set_fragments(&upd.assignments, &upd.rewrites, "");
4122 let mut upd_sql = format!(
4123 " UPDATE {} AS {}\n SET {}",
4124 target_ref(&upd.target),
4125 qi(alias),
4126 sets.join(", "),
4127 );
4128 upd_sql.push_str(&nested_cte_from(&upd.nested_ctes, &sets.join(","), "\n "));
4129 append_filter(&mut upd_sql, &upd.filter);
4130 upd_sql.push_str("\n RETURNING \"id\"");
4131
4132 let mut cte_parts: Vec<String> = emit_user_cte_parts(user_ctes);
4133 cte_parts.extend(emit_user_cte_parts(&upd.nested_ctes));
4134 cte_parts.push(format!("\"_w\" AS (\n{}\n)", upd_sql));
4135 cte_parts.extend(enqueue_ctes(&upd.enqueue_vector, "_w"));
4136 cte_parts.extend(enqueue_search_ctes(&upd.enqueue_search, "_w", upd.enqueue_vector.len()));
4137
4138 let (shape2, select_sql) = shape_select_from_cte(&upd.target, &upd.returning, "_w");
4139 let sql = format!(
4140 "WITH\n{}\n{}",
4141 cte_parts.join(",\n"),
4142 select_sql.unwrap_or_else(|| "SELECT * FROM \"_w\"".to_string()),
4143 );
4144 return SqlOutput {
4145 sql,
4146 shape: shape2,
4147 inference_plan: None,
4148 };
4149 }
4150
4151 let result_expr = if !upd.returning.is_empty() {
4153 let (pointer_exprs, _) = build_shape(&upd.returning, alias);
4154 let mut parts = vec![type_disc(&upd.target.type_name)];
4155 parts.extend(pointer_exprs);
4156 parts.join(",\n ")
4157 } else {
4158 format!("{}.id", qi(alias))
4159 };
4160
4161 let has_scalar_changes = !upd.assignments.is_empty() || !upd.rewrites.is_empty();
4162
4163 let mut cte_parts: Vec<String> = emit_user_cte_parts(user_ctes);
4167 cte_parts.extend(emit_user_cte_parts(&upd.nested_ctes));
4168
4169 if has_scalar_changes {
4171 let sets = update_set_fragments(&upd.assignments, &upd.rewrites, "");
4172 let mut upd_sql = format!(
4173 "UPDATE {} AS {}\nSET {}",
4174 target_ref(&upd.target),
4175 qi(alias),
4176 sets.join(", "),
4177 );
4178 upd_sql.push_str(&nested_cte_from(&upd.nested_ctes, &sets.join(","), "\n"));
4179 append_filter(&mut upd_sql, &upd.filter);
4180 upd_sql.push_str("\nRETURNING *");
4181 cte_parts.push(format!("\"_ids\" AS (\n{}\n)", upd_sql));
4182 } else {
4183 let mut sel = format!(
4184 "SELECT {}.* FROM {} AS {}",
4185 qi(alias),
4186 target_ref(&upd.target),
4187 qi(alias),
4188 );
4189 append_filter(&mut sel, &upd.filter);
4190 cte_parts.push(format!("\"_ids\" AS (\n{}\n)", sel));
4191 }
4192
4193 for (i, clr) in upd.multi_link_clears.iter().enumerate() {
4195 let exclude = ml_clear_exclusion(upd.multi_link_replaces.get(i));
4196 let del = format!(
4197 "DELETE FROM {} WHERE {} IN (SELECT id FROM \"_ids\"){}",
4198 qn(&clr.module, &clr.junction_table),
4199 qi(&clr.source_col),
4200 exclude,
4201 );
4202 cte_parts.push(format!("\"_clr_{}\" AS (\n{}\n)", i, del));
4203 }
4204
4205 for (i, app) in upd.multi_link_appends.iter().enumerate() {
4207 cte_parts.push(emit_ml_append_cte(
4208 app,
4209 "_ids",
4210 &upd.target.alias,
4211 &format!("_ml_add_{}", i),
4212 ));
4213 }
4214
4215 for (i, rem) in upd.multi_link_removals.iter().enumerate() {
4217 cte_parts.push(emit_ml_remove_cte(
4218 rem,
4219 "_ids",
4220 &upd.target.alias,
4221 &format!("_ml_rm_{}", i),
4222 ));
4223 }
4224
4225 for (i, rep) in upd.multi_link_replaces.iter().enumerate() {
4227 cte_parts.push(emit_ml_append_cte(
4228 rep,
4229 "_ids",
4230 &upd.target.alias,
4231 &format!("_ml_rep_{}", i),
4232 ));
4233 }
4234
4235 cte_parts.extend(enqueue_ctes(&upd.enqueue_vector, "_ids"));
4237 cte_parts.extend(enqueue_search_ctes(
4238 &upd.enqueue_search,
4239 "_ids",
4240 upd.enqueue_vector.len(),
4241 ));
4242
4243 let sql = format!(
4244 "WITH\n{}\nSELECT (\n {}\n) AS result\nFROM \"_ids\" AS {}",
4245 cte_parts.join(",\n"),
4246 result_expr,
4247 qi(alias),
4248 );
4249 SqlOutput {
4250 sql,
4251 shape,
4252 inference_plan: None,
4253 }
4254}
4255
4256fn emit_delete_stmt(del: &IrDelete) -> SqlOutput {
4259 if !del.poly_implementors.is_empty() {
4260 return emit_poly_delete_stmt(del);
4261 }
4262 let alias = &del.target.alias;
4263
4264 if del.enqueue_search.is_empty() {
4265 let mut sql = format!("DELETE FROM {} AS {}", target_ref(&del.target), qi(alias),);
4266 append_filter(&mut sql, &del.filter);
4267 let (shape, returning_sql) = emit_returning_shape(&del.target, &del.returning, true);
4268 if let Some(r) = returning_sql {
4269 sql.push_str(&r);
4270 }
4271 return SqlOutput {
4272 sql,
4273 shape,
4274 inference_plan: None,
4275 };
4276 }
4277
4278 let mut del_sql = format!(" DELETE FROM {} AS {}", target_ref(&del.target), qi(alias),);
4280 append_filter(&mut del_sql, &del.filter);
4281 del_sql.push_str("\n RETURNING \"id\"");
4282
4283 let mut cte_parts = vec![format!("\"_del\" AS (\n{}\n)", del_sql)];
4284 cte_parts.extend(enqueue_search_ctes(&del.enqueue_search, "_del", 0));
4285
4286 let (shape, select_sql) = shape_select_from_cte(&del.target, &del.returning, "_del");
4287 let sql = format!(
4288 "WITH\n{}\n{}",
4289 cte_parts.join(",\n"),
4290 select_sql.unwrap_or_else(|| "SELECT * FROM \"_del\"".to_string()),
4291 );
4292 SqlOutput {
4293 sql,
4294 shape,
4295 inference_plan: None,
4296 }
4297}
4298
4299fn emit_poly_delete_stmt(del: &IrDelete) -> SqlOutput {
4300 let alias = &del.target.alias;
4301 let mut cte_parts = vec![];
4302 let mut union_parts = vec![];
4303
4304 for (i, imp) in del.poly_implementors.iter().enumerate() {
4305 let cte_name = format!("_d{}", i);
4306 let mut del_sql = format!("DELETE FROM {} AS {}", qn(&imp.module, &imp.table), qi(alias),);
4307 append_filter(&mut del_sql, &del.filter);
4308 del_sql.push_str(&format!("\nRETURNING {}.\"id\"", qi(alias)));
4309 cte_parts.push(format!("\"{}\" AS (\n{}\n)", cte_name, del_sql));
4310
4311 let r_alias = format!("_r{}", i);
4312 union_parts.push(format!(
4313 "SELECT ROW({}::text, {}.\"id\") AS result FROM \"{}\" AS {}",
4314 sql_str(&imp.type_name),
4315 qi(&r_alias),
4316 cte_name,
4317 qi(&r_alias),
4318 ));
4319 }
4320
4321 let sql = format!("WITH\n{}\n{}", cte_parts.join(",\n"), union_parts.join("\nUNION ALL\n"),);
4322
4323 let (shape, _) = emit_returning_shape(&del.target, &del.returning, true);
4324 SqlOutput {
4325 sql,
4326 shape,
4327 inference_plan: None,
4328 }
4329}
4330
4331fn emit_returning_shape(
4336 target: &IrSource,
4337 returning: &[IrShapePointer],
4338 with_alias: bool,
4339) -> (ShapeDescriptor, Option<String>) {
4340 if returning.is_empty() {
4341 return (
4342 ShapeDescriptor {
4343 root: ShapeNode::Scalar {
4344 name: String::new(),
4345 position: 0,
4346 },
4347 },
4348 None,
4349 );
4350 }
4351
4352 let alias = if with_alias { target.alias.as_str() } else { "" };
4353 let (pointer_exprs, shape_pointers) = build_shape(returning, alias);
4354
4355 let mut parts = vec![type_disc(&target.type_name)];
4356 parts.extend(pointer_exprs);
4357 let tuple = parts.join(",\n ");
4358 let sql = format!("\nRETURNING (\n {}\n) AS result", tuple);
4359
4360 let root_pointers = prepend_type(shape_pointers);
4361 let shape = ShapeDescriptor {
4362 root: ShapeNode::Object {
4363 name: String::new(),
4364 type_name: Some(target.type_name.clone()),
4365 position: 0,
4366 cardinality: Cardinality::Required,
4367 pointers: root_pointers,
4368 has_implicit_id: shape_has_implicit_id(returning),
4369 },
4370 };
4371 (shape, Some(sql))
4372}
4373
4374fn emit_scalar_set(f: &IrScalarSetPointer, pos: usize) -> (String, ShapeNode) {
4377 let from_sql = if !f.poly_implementors.is_empty() {
4378 format!(
4379 "(\n{}\n) AS {}",
4380 emit_poly_union(&f.poly_implementors, &f.poly_columns),
4381 qi(&f.source.alias)
4382 )
4383 } else {
4384 format!("{} AS {}", source_ref(&f.source), qi(&f.source.alias))
4385 };
4386 let sql = format!(
4387 "(SELECT COALESCE(array_agg(ROW({})::record), ARRAY[]::record[]) FROM {})",
4388 emit_expr(&f.bool_expr),
4389 from_sql,
4390 );
4391 let node = ShapeNode::Array {
4392 name: f.alias.clone(),
4393 position: pos,
4394 element: Box::new(ShapeNode::Scalar {
4395 name: String::new(),
4396 position: 0,
4397 }),
4398 };
4399 (sql, node)
4400}
4401
4402fn shape_has_implicit_id(pointers: &[IrShapePointer]) -> bool {
4407 matches!(pointers.first(), Some(IrShapePointer::Scalar(p)) if p.implicit_id)
4408}
4409
4410fn build_shape(pointers: &[IrShapePointer], table_alias: &str) -> (Vec<String>, Vec<ShapeNode>) {
4411 let mut exprs = Vec::new();
4412 let mut nodes = Vec::new();
4413
4414 for (i, pointer) in pointers.iter().enumerate() {
4415 let (sql, node) = emit_shape_pointer(pointer, table_alias, i + 1);
4416 exprs.push(sql);
4417 nodes.push(node);
4418 }
4419
4420 (exprs, nodes)
4421}
4422
4423fn fill_template(template: &str, args: &[String]) -> String {
4429 let mut out = String::with_capacity(template.len());
4430 let mut chars = template.char_indices().peekable();
4431 while let Some((start, c)) = chars.next() {
4432 if c != '$' {
4433 out.push(c);
4434 continue;
4435 }
4436 let mut end = start + 1;
4437 while let Some((i, d)) = chars.peek().copied()
4438 && d.is_ascii_digit()
4439 {
4440 end = i + d.len_utf8();
4441 chars.next();
4442 }
4443 match template[start + 1..end].parse::<usize>() {
4444 Ok(n) if (1..=args.len()).contains(&n) => out.push_str(&args[n - 1]),
4445 _ => out.push_str(&template[start..end]),
4446 }
4447 }
4448 out
4449}
4450
4451fn assert_message_arg(message: &Option<IrExpr>) -> String {
4453 message
4454 .as_ref()
4455 .map(|m| format!(", ({})::text", emit_expr(m)))
4456 .unwrap_or_default()
4457}
4458
4459fn emit_shape_pointer(pointer: &IrShapePointer, table_alias: &str, pos: usize) -> (String, ShapeNode) {
4460 match pointer {
4461 IrShapePointer::Scalar(f) => emit_scalar(f, table_alias, pos),
4462 IrShapePointer::SingleLink(f) => emit_single_link(f, table_alias, pos),
4463 IrShapePointer::MultiLink(f) => emit_multi_link(f, table_alias, pos),
4464 IrShapePointer::Computed(f) => (emit_expr(&f.expr), expr_shape_node(&f.alias, pos, &f.expr)),
4465 IrShapePointer::ScalarSet(f) => emit_scalar_set(f, pos),
4466 IrShapePointer::Asserted(a) => {
4467 let (sql, node) = emit_shape_pointer(&a.inner, table_alias, pos);
4468 let checked_set = match &a.check {
4479 Some(check) if emits_one_object(check) => {
4480 format!("ARRAY{}", emit_shape_pointer(check, table_alias, pos).0)
4481 }
4482 Some(check) => emit_shape_pointer(check, table_alias, pos).0,
4483 None if emits_one_object(&a.inner) => format!("ARRAY{sql}"),
4484 None => "\"_a\".\"v\"".to_string(),
4485 };
4486 let checked = format!(
4487 "(SELECT \"_a\".\"v\" FROM (SELECT {sql} AS \"v\") AS \"_a\"\n WHERE cardinality(\"_pylon\".{}({checked_set}::text[]{})) >= 0)",
4488 qi(&a.fn_name),
4489 assert_message_arg(&a.message),
4490 );
4491 (checked, node)
4492 }
4493 }
4494}
4495
4496fn emits_one_object(pointer: &IrShapePointer) -> bool {
4499 match pointer {
4500 IrShapePointer::SingleLink(_) => true,
4501 IrShapePointer::Computed(c) => matches!(
4502 c.expr,
4503 IrExpr::ObjectPathSubquery(_) | IrExpr::ObjectPathUnion { multi: false, .. }
4504 ),
4505 IrShapePointer::Asserted(a) => emits_one_object(&a.inner),
4506 _ => false,
4507 }
4508}
4509
4510fn pg_quoted_to_pylon(pg_type: &str) -> String {
4513 let inner = pg_type.trim_start_matches('"');
4514 if let Some(idx) = inner.find(r#""."#) {
4515 let module = &inner[..idx];
4516 let type_name = inner[idx + 3..].trim_end_matches('"');
4517 format!("{}::{}", module, type_name)
4518 } else {
4519 pg_type.to_string()
4520 }
4521}
4522
4523struct QualifiedPgType {
4534 name: String,
4536 is_array: bool,
4537}
4538
4539impl QualifiedPgType {
4540 fn of(pg_type: &str) -> Option<Self> {
4541 let (element, is_array) = match pg_type.strip_suffix("[]") {
4542 Some(element) => (element, true),
4543 None => (pg_type, false),
4544 };
4545 element.starts_with('"').then(|| Self {
4546 name: pg_quoted_to_pylon(element),
4547 is_array,
4548 })
4549 }
4550
4551 fn text_cast(&self) -> &'static str {
4553 if self.is_array { "::text[]" } else { "::text" }
4554 }
4555
4556 fn shape_node(&self, name: String, position: usize) -> ShapeNode {
4558 let element = ShapeNode::Enum {
4559 name: name.clone(),
4560 position,
4561 enum_type: self.name.clone(),
4562 };
4563 if !self.is_array {
4564 return element;
4565 }
4566 ShapeNode::Array {
4567 name,
4568 element: Box::new(ShapeNode::Enum {
4571 name: String::new(),
4572 position: 0,
4573 enum_type: self.name.clone(),
4574 }),
4575 position,
4576 }
4577 }
4578}
4579
4580fn emit_scalar(f: &IrScalarPointer, table_alias: &str, pos: usize) -> (String, ShapeNode) {
4581 if let Some(nt_name) = f.pg_type.strip_prefix("__nt__:") {
4582 let sql = if table_alias.is_empty() {
4583 format!("{}::jsonb", qi(&f.column))
4584 } else {
4585 format!("{}.{}::jsonb", qi(table_alias), qi(&f.column))
4586 };
4587 return (
4588 sql,
4589 ShapeNode::NamedTuple {
4590 name: f.alias.clone(),
4591 position: pos,
4592 type_name: Some(nt_name.to_string()),
4593 members: f.tuple_shape.as_ref().map(|s| s.members.clone()),
4594 is_free_object: false,
4595 },
4596 );
4597 }
4598 if let Some(qualified) = QualifiedPgType::of(&f.pg_type) {
4601 let cast = qualified.text_cast();
4602 let sql = if table_alias.is_empty() {
4603 format!("{}{}", qi(&f.column), cast)
4604 } else {
4605 format!("{}.{}{}", qi(table_alias), qi(&f.column), cast)
4606 };
4607 return (sql, qualified.shape_node(f.alias.clone(), pos));
4608 }
4609 if let Some(shape) = &f.tuple_shape {
4613 let sql = if table_alias.is_empty() {
4614 format!("{}::jsonb", qi(&f.column))
4615 } else {
4616 format!("{}.{}::jsonb", qi(table_alias), qi(&f.column))
4617 };
4618 return (
4619 sql,
4620 ShapeNode::NamedTuple {
4621 name: f.alias.clone(),
4622 position: pos,
4623 type_name: shape.type_name.clone(),
4624 members: Some(shape.members.clone()),
4625 is_free_object: false,
4626 },
4627 );
4628 }
4629 let sql = if table_alias.is_empty() {
4630 format!("{}::{}", qi(&f.column), f.pg_type)
4631 } else {
4632 format!("{}.{}::{}", qi(table_alias), qi(&f.column), f.pg_type)
4633 };
4634 (
4635 sql,
4636 ShapeNode::Scalar {
4637 name: f.alias.clone(),
4638 position: pos,
4639 },
4640 )
4641}
4642
4643fn emit_single_link(f: &IrSingleLinkPointer, parent_alias: &str, pos: usize) -> (String, ShapeNode) {
4644 let sub = &f.subquery;
4645 let [IrRowSource::Bound { source, shape }] = sub.rows.as_slice() else {
4646 unreachable!("single-link subquery is always schema-bound")
4647 };
4648 let sub_alias = &source.alias;
4649
4650 let (sub_exprs, mut sub_nodes) = build_shape(shape, sub_alias);
4651 let mut parts = vec![source_type_disc(source)];
4652 parts.extend(sub_exprs);
4653
4654 for lp in &f.link_properties {
4659 parts.push(format!("\"jt\".{}", qi(&lp.name)));
4660 let pos = sub_nodes.len() + 1;
4661 sub_nodes.push(ShapeNode::Scalar {
4662 name: format!("@{}", lp.name),
4663 position: pos,
4664 });
4665 }
4666
4667 let tuple = parts.join(",\n ");
4668
4669 let (from_sql, mut where_parts) = match &f.correlation {
4670 IrSingleLinkCorrelation::Fk { fk_column, target_pk } => {
4671 let from = format!("FROM {} AS {}", source_ref(source), qi(sub_alias));
4672 let cond = format!(
4673 "{}.{} = {}.{}",
4674 qi(parent_alias),
4675 qi(fk_column),
4676 qi(sub_alias),
4677 qi(target_pk),
4678 );
4679 (from, vec![cond])
4680 }
4681 IrSingleLinkCorrelation::Junction { join, target_pk: _ } => match join {
4685 IrMultiLinkJoin::Standard { junction_table, module } => {
4686 let from = format!(
4687 "FROM {} AS \"jt\"\n INNER JOIN {} AS {}\n ON {}.id = \"jt\".target",
4688 junction_ref(module, junction_table),
4689 source_ref(source),
4690 qi(sub_alias),
4691 qi(sub_alias),
4692 );
4693 let cond = format!("\"jt\".source = {}.id", qi(parent_alias));
4694 (from, vec![cond])
4695 }
4696 IrMultiLinkJoin::Through {
4697 junction_table,
4698 module,
4699 source_col,
4700 target_col,
4701 } => {
4702 let from = format!(
4703 "FROM {} AS \"jt\"\n INNER JOIN {} AS {}\n ON {}.id = \"jt\".{}",
4704 junction_ref(module, junction_table),
4705 source_ref(source),
4706 qi(sub_alias),
4707 qi(sub_alias),
4708 qi(target_col),
4709 );
4710 let cond = format!("\"jt\".{} = {}.id", qi(source_col), qi(parent_alias));
4711 (from, vec![cond])
4712 }
4713 IrMultiLinkJoin::BacklinkFk { .. } | IrMultiLinkJoin::BacklinkJunction { .. } => {
4714 unreachable!("a junction-backed single link's own forward join is always Standard or Through")
4715 }
4716 },
4717 };
4718 if let Some(filter) = &sub.filter {
4719 where_parts.push(emit_expr(filter));
4720 }
4721
4722 let mut sql = format!(
4723 "(SELECT (\n {}\n )\n {}\n WHERE {}",
4724 tuple,
4725 from_sql,
4726 where_parts.join(" AND "),
4727 );
4728 if !sub.order_by.is_empty() {
4729 let s: Vec<_> = sub.order_by.iter().map(emit_sort_clause).collect();
4730 sql.push_str(&format!("\n ORDER BY {}", s.join(", ")));
4731 }
4732 sql.push(')');
4733
4734 let node = ShapeNode::Object {
4735 name: f.alias.clone(),
4736 type_name: Some(source.type_name.clone()),
4737 position: pos,
4738 cardinality: Cardinality::Optional,
4739 pointers: prepend_type(sub_nodes),
4740 has_implicit_id: shape_has_implicit_id(shape),
4741 };
4742 (sql, node)
4743}
4744
4745fn emit_multi_link(f: &IrMultiLinkPointer, parent_alias: &str, pos: usize) -> (String, ShapeNode) {
4746 let sub = &f.subquery;
4747 let [IrRowSource::Bound { source, shape }] = sub.rows.as_slice() else {
4748 unreachable!("multi-link subquery is always schema-bound")
4749 };
4750 let sub_alias = &source.alias;
4751
4752 let (sub_exprs, mut sub_nodes) = build_shape(shape, sub_alias);
4753 let mut row_parts = vec![source_type_disc(source)];
4754 row_parts.extend(sub_exprs);
4755
4756 for lp in &f.link_properties {
4760 row_parts.push(format!("\"jt\".{}", qi(&lp.name)));
4761 let pos = sub_nodes.len() + 1;
4764 sub_nodes.push(ShapeNode::Scalar {
4765 name: format!("@{}", lp.name),
4766 position: pos,
4767 });
4768 }
4769
4770 let row = row_parts.join(",\n ");
4771
4772 let order_sql = if !sub.order_by.is_empty() {
4774 let s: Vec<_> = sub.order_by.iter().map(emit_sort_clause).collect();
4775 format!(" ORDER BY {}", s.join(", "))
4776 } else {
4777 String::new()
4778 };
4779
4780 let (from_sql, source_cond) = match &f.join {
4781 IrMultiLinkJoin::Standard { junction_table, module } => {
4782 let from = format!(
4783 "FROM {} AS \"jt\"\n INNER JOIN {} AS {}\n ON {}.id = \"jt\".target",
4784 junction_ref(module, junction_table),
4785 source_ref(source),
4786 qi(sub_alias),
4787 qi(sub_alias),
4788 );
4789 let cond = format!("\"jt\".source = {}.id", qi(parent_alias));
4790 (from, cond)
4791 }
4792 IrMultiLinkJoin::Through {
4793 junction_table,
4794 module,
4795 source_col,
4796 target_col,
4797 } => {
4798 let from = format!(
4799 "FROM {} AS \"jt\"\n INNER JOIN {} AS {}\n ON {}.id = \"jt\".{}",
4800 junction_ref(module, junction_table),
4801 source_ref(source),
4802 qi(sub_alias),
4803 qi(sub_alias),
4804 qi(target_col),
4805 );
4806 let cond = format!("\"jt\".{} = {}.id", qi(source_col), qi(parent_alias));
4807 (from, cond)
4808 }
4809 IrMultiLinkJoin::BacklinkFk { fk_col } => {
4812 let from = format!("FROM {} AS {}", source_ref(source), qi(sub_alias));
4813 let cond = format!("{}.{} = {}.id", qi(sub_alias), qi(fk_col), qi(parent_alias));
4814 (from, cond)
4815 }
4816 IrMultiLinkJoin::BacklinkJunction {
4821 junction_table,
4822 module,
4823 owner_col,
4824 current_col,
4825 } => {
4826 let from = format!(
4827 "FROM {} AS \"jt\"\n INNER JOIN {} AS {}\n ON {}.id = \"jt\".{}",
4828 junction_ref(module, junction_table),
4829 source_ref(source),
4830 qi(sub_alias),
4831 qi(sub_alias),
4832 qi(owner_col),
4833 );
4834 let cond = format!("\"jt\".{} = {}.id", qi(current_col), qi(parent_alias));
4835 (from, cond)
4836 }
4837 };
4838
4839 let mut where_parts = vec![source_cond];
4840 if let Some(filter) = &sub.filter {
4841 where_parts.push(emit_expr(filter));
4842 }
4843
4844 if f.single {
4845 let mut sql = format!(
4846 "(SELECT (\n {}\n )\n {}\n WHERE {}",
4847 row,
4848 from_sql,
4849 where_parts.join(" AND "),
4850 );
4851 append_order_by(&mut sql, &sub.order_by);
4852 append_offset_limit(&mut sql, &sub.offset, &sub.limit);
4853 sql.push(')');
4854 let node = ShapeNode::Object {
4855 name: f.alias.clone(),
4856 type_name: Some(source.type_name.clone()),
4857 position: pos,
4858 cardinality: Cardinality::Optional,
4859 pointers: prepend_type(sub_nodes),
4860 has_implicit_id: shape_has_implicit_id(shape),
4861 };
4862 return (sql, node);
4863 }
4864
4865 if f.single {
4866 let mut sql = format!(
4867 "(SELECT (\n {}\n )\n {}\n WHERE {}",
4868 row,
4869 from_sql,
4870 where_parts.join(" AND "),
4871 );
4872 append_order_by(&mut sql, &sub.order_by);
4873 append_offset_limit(&mut sql, &sub.offset, &sub.limit);
4874 sql.push(')');
4875 let node = ShapeNode::Object {
4876 name: f.alias.clone(),
4877 type_name: Some(source.type_name.clone()),
4878 position: pos,
4879 cardinality: Cardinality::Optional,
4880 pointers: prepend_type(sub_nodes),
4881 has_implicit_id: shape_has_implicit_id(shape),
4882 };
4883 return (sql, node);
4884 }
4885
4886 let sql = if sub.limit.is_some() || sub.offset.is_some() {
4887 let mut cols: Vec<String> = row_parts
4892 .iter()
4893 .enumerate()
4894 .map(|(i, part)| format!("{} AS \"c{}\"", part, i))
4895 .collect();
4896 let agg_row: Vec<String> = (0..row_parts.len()).map(|i| format!("\"__lim\".\"c{}\"", i)).collect();
4897 let mut agg_order: Vec<String> = vec![];
4898 for (i, s) in sub.order_by.iter().enumerate() {
4899 cols.push(format!("{} AS \"s{}\"", emit_expr(&s.expr), i));
4900 agg_order.push(emit_sort_clause(&IrSort {
4901 expr: IrExpr::ColumnRef {
4902 alias: "__lim".to_string(),
4903 column: format!("s{}", i),
4904 pg_type: String::new(),
4905 },
4906 direction: s.direction.clone(),
4907 nulls: s.nulls.clone(),
4908 }));
4909 }
4910 let mut inner = format!(
4911 "SELECT {}\n {}\n WHERE {}",
4912 cols.join(",\n "),
4913 from_sql,
4914 where_parts.join(" AND "),
4915 );
4916 append_order_by(&mut inner, &sub.order_by);
4917 append_offset_limit(&mut inner, &sub.offset, &sub.limit);
4918 let agg_order_sql = if agg_order.is_empty() {
4919 String::new()
4920 } else {
4921 format!(" ORDER BY {}", agg_order.join(", "))
4922 };
4923 format!(
4924 "(SELECT COALESCE(\n array_agg(ROW(\n {}\n )::record{}),\n ARRAY[]::record[]\n )\n FROM ({}) AS \"__lim\")",
4925 agg_row.join(",\n "),
4926 agg_order_sql,
4927 inner,
4928 )
4929 } else {
4930 format!(
4931 "(SELECT COALESCE(\n array_agg(ROW(\n {}\n )::record{}),\n ARRAY[]::record[]\n )\n {}\n WHERE {})",
4932 row,
4933 order_sql,
4934 from_sql,
4935 where_parts.join(" AND "),
4936 )
4937 };
4938
4939 let node = ShapeNode::Array {
4940 name: f.alias.clone(),
4941 position: pos,
4942 element: Box::new(ShapeNode::Object {
4943 name: String::new(),
4944 type_name: Some(source.type_name.clone()),
4945 position: 0,
4946 cardinality: Cardinality::Required,
4947 pointers: prepend_type(sub_nodes),
4948 has_implicit_id: shape_has_implicit_id(shape),
4949 }),
4950 };
4951 (sql, node)
4952}
4953
4954fn prepend_type(nodes: Vec<ShapeNode>) -> Vec<ShapeNode> {
4957 let mut out = vec![ShapeNode::Scalar {
4958 name: "__type__".into(),
4959 position: 0,
4960 }];
4961 out.extend(nodes);
4962 out
4963}
4964
4965fn append_filter(sql: &mut String, filter: &Option<IrExpr>) {
4968 if let Some(f) = filter {
4969 sql.push_str(&format!("\nWHERE {}", emit_expr(f)));
4970 }
4971}
4972
4973fn append_order_by(sql: &mut String, order_by: &[IrSort]) {
4974 if !order_by.is_empty() {
4975 let s: Vec<_> = order_by.iter().map(emit_sort_clause).collect();
4976 sql.push_str(&format!("\nORDER BY {}", s.join(", ")));
4977 }
4978}
4979
4980fn append_offset_limit(sql: &mut String, offset: &Option<IrExpr>, limit: &Option<IrExpr>) {
4981 if let Some(o) = offset {
4982 sql.push_str(&format!("\nOFFSET {}", emit_expr(o)));
4983 }
4984 if let Some(l) = limit {
4985 sql.push_str(&format!("\nLIMIT {}", emit_expr(l)));
4986 }
4987}
4988
4989fn append_lock_clause(sql: &mut String, lock: &Option<IrLockClause>) {
4996 let Some(lock) = lock else { return };
4997 let strength = match lock.strength {
4998 IrLockStrength::Update => "UPDATE",
4999 IrLockStrength::NoKeyUpdate => "NO KEY UPDATE",
5000 IrLockStrength::Share => "SHARE",
5001 IrLockStrength::KeyShare => "KEY SHARE",
5002 };
5003 sql.push_str(&format!("\nFOR {}", strength));
5004 match lock.wait {
5005 IrLockWait::Block => {}
5006 IrLockWait::NoWait => sql.push_str(" NOWAIT"),
5007 IrLockWait::SkipLocked => sql.push_str(" SKIP LOCKED"),
5008 }
5009}
5010
5011fn emit_sort_clause(s: &IrSort) -> String {
5012 let dir = match s.direction {
5013 IrSortDir::Asc => "ASC",
5014 IrSortDir::Desc => "DESC",
5015 };
5016 let nulls = match s.nulls {
5017 IrNulls::First => "NULLS FIRST",
5018 IrNulls::Last => "NULLS LAST",
5019 };
5020 format!("{} {} {}", emit_expr(&s.expr), dir, nulls)
5021}
5022
5023fn set_operand(expr: &IrExpr) -> String {
5028 match expr {
5029 IrExpr::ArrayFromSelect(_) => format!("SELECT unnest({})", emit_expr(expr)),
5030 IrExpr::CteRef { name, scalar, .. } => {
5031 format!("SELECT {} FROM {}", if *scalar { "\"v\"" } else { "\"id\"" }, qi(name))
5032 }
5033 _ => format!("SELECT {}", emit_expr(expr)),
5034 }
5035}
5036
5037pub fn emit_expr_with_fanouts(expr: &IrExpr, fanouts: &HashMap<(String, String), IrPolyFanout>) -> String {
5040 with_subtype_fanouts(fanouts, || emit_expr(expr))
5041}
5042
5043pub fn emit_expr(expr: &IrExpr) -> String {
5044 match expr {
5045 IrExpr::ColumnRef { alias, column, .. } => {
5046 if alias.is_empty() {
5047 qi(column)
5048 } else if alias == "NEW" || alias == "OLD" {
5049 format!("{alias}.{}", qi(column))
5060 } else {
5061 format!("{}.{}", qi(alias), qi(column))
5062 }
5063 }
5064 IrExpr::Param { index } => format!("${}", index + 1),
5065 IrExpr::Literal(lit) => emit_literal(lit),
5066 IrExpr::BinOp(op) => {
5067 let l = emit_expr(&op.left);
5068 let r = emit_expr(&op.right);
5069 match op.op {
5070 BinOpKind::Add => format!("({} + {})", l, r),
5071 BinOpKind::Sub => format!("({} - {})", l, r),
5072 BinOpKind::Mul => format!("({} * {})", l, r),
5073 BinOpKind::Div => format!("({} / {})", l, r),
5074 BinOpKind::FloorDiv => {
5075 if is_integer_expr(&op.left) && is_integer_expr(&op.right) {
5076 format!("({} / {})", l, r)
5077 } else {
5078 format!("floor(({}) / ({}))", l, r)
5079 }
5080 }
5081 BinOpKind::Mod => format!("({} % {})", l, r),
5082 BinOpKind::Pow => format!("power({}, {})", l, r),
5083 BinOpKind::Eq => format!("({} = {})", l, r),
5084 BinOpKind::Ne => format!("({} <> {})", l, r),
5085 BinOpKind::Lt => format!("({} < {})", l, r),
5086 BinOpKind::Le => format!("({} <= {})", l, r),
5087 BinOpKind::Gt => format!("({} > {})", l, r),
5088 BinOpKind::Ge => format!("({} >= {})", l, r),
5089 BinOpKind::And => format!("({} AND {})", l, r),
5090 BinOpKind::Or => format!("({} OR {})", l, r),
5091 BinOpKind::Like => format!("({} LIKE {})", l, r),
5092 BinOpKind::Ilike => format!("({} ILIKE {})", l, r),
5093 BinOpKind::NotLike => format!("({} NOT LIKE {})", l, r),
5094 BinOpKind::NotIlike => format!("({} NOT ILIKE {})", l, r),
5095 BinOpKind::In => format!("({} = ANY({}))", l, unwrap_unnest_for_any(&op.right, &r)),
5101 BinOpKind::NotIn => format!("({} <> ALL({}))", l, unwrap_unnest_for_any(&op.right, &r)),
5102 BinOpKind::Coalesce => format!("COALESCE({}, {})", l, r),
5103 BinOpKind::CoalesceEq => format!("({} IS NOT DISTINCT FROM {})", l, r),
5104 BinOpKind::CoalesceNe => format!("({} IS DISTINCT FROM {})", l, r),
5105 BinOpKind::Concat => format!("({} || {})", l, r),
5106 }
5107 }
5108 IrExpr::UnaryOp(op) => {
5109 let inner = emit_expr(&op.operand);
5110 match op.op {
5111 UnaryOpKind::Not => format!("(NOT {})", inner),
5112 UnaryOpKind::Minus => format!("(-{})", inner),
5113 UnaryOpKind::Exists => format!("EXISTS({})", inner),
5114 UnaryOpKind::Distinct => inner,
5119 }
5120 }
5121 IrExpr::FunctionCall(f) => {
5122 let args: Vec<_> = f.args.iter().map(emit_expr).collect();
5123 if let Some(tmpl) = &f.sql_template {
5124 return fill_template(tmpl, &args);
5125 }
5126 let name = match &f.schema {
5127 Some(s) => format!("{}.{}", pg_schema(s), qi(&f.name)),
5128 None => f.name.clone(),
5129 };
5130 format!("{}({})", name, args.join(", "))
5131 }
5132 IrExpr::TypeCast(c) => {
5133 if c.pg_type == "jsonb" {
5136 match &c.expr {
5137 IrExpr::Param { .. } => format!("({})::jsonb", emit_expr(&c.expr)),
5148 IrExpr::Literal(IrLiteral::Str(_)) => {
5149 format!("to_jsonb({}::text)", emit_expr(&c.expr))
5150 }
5151 _ => format!("to_jsonb({})", emit_expr(&c.expr)),
5152 }
5153 } else if yields_jsonb(&c.expr) {
5154 format!("(({}) #>> '{{}}')::{}", emit_expr(&c.expr), c.pg_type)
5158 } else {
5159 format!("({})::{}", emit_expr(&c.expr), c.pg_type)
5160 }
5161 }
5162 IrExpr::IfElse(ie) => format!(
5163 "CASE WHEN {} THEN {} ELSE {} END",
5164 emit_expr(&ie.condition),
5165 emit_expr(&ie.if_),
5166 emit_expr(&ie.else_),
5167 ),
5168 IrExpr::Array(elems) => {
5169 if elems.is_empty() {
5170 "ARRAY[]::text[]".to_string()
5171 } else {
5172 let parts: Vec<String> = elems.iter().map(emit_expr).collect();
5173 format!("ARRAY[{}]", parts.join(", "))
5174 }
5175 }
5176 IrExpr::Null => "NULL".to_string(),
5177 IrExpr::AggOverSet {
5178 fn_name,
5179 schema: _,
5180 elems,
5181 } => {
5182 let union_all = elems
5183 .iter()
5184 .map(|e| format!("SELECT {}", emit_expr(e)))
5185 .collect::<Vec<_>>()
5186 .join(" UNION ALL ");
5187 format!("(SELECT {}(v) FROM ({}) AS _set(v))", fn_name, union_all)
5188 }
5189 IrExpr::AggOverCte { fn_name, cte, column } => format!(
5190 "(SELECT {}({}) FROM {})",
5191 fn_name,
5192 column.as_deref().map(qi).unwrap_or_else(|| "*".to_string()),
5193 qi(cte),
5194 ),
5195 IrExpr::ExistsOverCte { cte, column } => match column {
5196 Some(column) => format!("EXISTS(SELECT 1 FROM {} WHERE {} IS NOT NULL)", qi(cte), qi(column)),
5197 None => format!("EXISTS(SELECT 1 FROM {})", qi(cte)),
5198 },
5199 IrExpr::AggOverQuery { fn_name, inner } => {
5200 let inner_sql = emit_select_stmt(inner, &[]).sql;
5201 format!("(SELECT {}(*) FROM ({}) _agg)", fn_name, inner_sql)
5202 }
5203 IrExpr::ArrayFromSelect(src) => emit_array_source(src),
5204 IrExpr::ScalarSubquery(sel) => {
5205 format!(
5206 "(SELECT \"v\" FROM (\n{}\n) AS \"_scalar_sub\")",
5207 emit_select_stmt(sel, &[]).sql
5208 )
5209 }
5210
5211 IrExpr::SetOp { op, left, right, mode } => {
5212 let set = format!("({}) {} ({})", set_operand(left), op.sql(), set_operand(right));
5213 match mode {
5214 crate::ir::SetOpMode::Exists => format!("EXISTS({set})"),
5215 crate::ir::SetOpMode::Array => format!("ARRAY({set})"),
5216 crate::ir::SetOpMode::Aggregate(f) => {
5217 format!("(SELECT {f}(\"_s\".\"v\") FROM ({set}) AS \"_s\"(\"v\"))")
5218 }
5219 }
5220 }
5221
5222 IrExpr::CteRef { name, scalar, .. } => {
5223 let col = if *scalar { "v" } else { "id" };
5226 format!("(SELECT \"{}\" FROM \"{}\")", col, name)
5227 }
5228
5229 IrExpr::CteFieldRef { name, field, .. } => {
5230 format!("(SELECT {} FROM {})", qi(field), qi(name))
5231 }
5232
5233 IrExpr::ForVar { name, .. } => format!("\"_for_{}\".\"v\"", name),
5234
5235 IrExpr::EnumLiteral { pg_type, variant } => {
5236 format!("'{}'::{}", variant.replace('\'', "''"), pg_type)
5237 }
5238
5239 IrExpr::GlobalParam { index, pg_type } => {
5240 format!("(${}::{})", index + 1, pg_type)
5241 }
5242
5243 IrExpr::GlobalRef { cte_name } => {
5244 format!("(SELECT \"value\" FROM \"{}\")", cte_name)
5245 }
5246
5247 IrExpr::NamedTuple { fields, .. } => {
5248 let pairs: Vec<String> = fields
5249 .iter()
5250 .flat_map(|(k, v)| [format!("'{}'", k.replace('\'', "''")), emit_expr(v)])
5251 .collect();
5252 format!("jsonb_build_object({})", pairs.join(", "))
5253 }
5254
5255 IrExpr::Tuple(elems) => {
5256 let items: Vec<String> = elems.iter().map(emit_expr).collect();
5257 format!("jsonb_build_array({})", items.join(", "))
5258 }
5259
5260 IrExpr::Subscript { expr, index, is_array } => {
5261 let e = emit_expr(expr);
5262 let i = emit_expr(index);
5263 if *is_array {
5264 format!("_pylon.array_subscript({}, ({})::bigint)", e, i)
5265 } else {
5266 format!("_pylon.str_subscript({}, ({})::bigint)", e, i)
5267 }
5268 }
5269
5270 IrExpr::Slice {
5271 expr,
5272 lower,
5273 upper,
5274 is_array,
5275 } => {
5276 let e = emit_expr(expr);
5277 if *is_array {
5278 let lo = lower
5279 .as_deref()
5280 .map(|x| format!("({}) + 1", emit_expr(x)))
5281 .unwrap_or_else(|| "1".to_string());
5282 let hi = upper.as_deref().map(emit_expr).unwrap_or_default();
5283 if hi.is_empty() {
5284 format!("({})[{}:]", e, lo)
5285 } else {
5286 format!("({})[{}:{}]", e, lo, hi)
5287 }
5288 } else {
5289 let start = lower
5291 .as_deref()
5292 .map(|x| format!("({}) + 1", emit_expr(x)))
5293 .unwrap_or_else(|| "1".to_string());
5294 match upper.as_deref() {
5295 Some(hi_expr) => {
5296 let lo_val = lower.as_deref().map(emit_expr).unwrap_or_else(|| "0".to_string());
5297 format!(
5299 "substr({}, {}, GREATEST(0, ({}) - ({})))",
5300 e,
5301 start,
5302 emit_expr(hi_expr),
5303 lo_val
5304 )
5305 }
5306 None => format!("substr({}, {})", e, start),
5307 }
5308 }
5309 }
5310
5311 IrExpr::JsonbField { expr, field } => {
5312 format!("({}->{})", emit_expr(expr), sql_str(field))
5313 }
5314
5315 IrExpr::JsonbIndex { expr, index } => {
5316 format!("({}->{})", emit_expr(expr), index)
5317 }
5318
5319 IrExpr::FnParam { name, .. } => qi(name),
5320
5321 IrExpr::FnSubquery(fs) => {
5322 let scalar = match fs.shape.as_slice() {
5323 [IrShapePointer::Computed(c)] => emit_expr(&c.expr),
5324 _ => unreachable!("a function scalar subquery always projects exactly one computed pointer"),
5325 };
5326 let args_sql = fs.fn_args.iter().map(emit_expr).collect::<Vec<_>>().join(", ");
5327 let mut sql = format!(
5328 "(SELECT {}{}\nFROM {}.{}({}) AS {}",
5329 if fs.distinct { "DISTINCT " } else { "" },
5330 scalar,
5331 pg_schema(&fs.fn_module),
5332 qi(&fs.fn_name),
5333 args_sql,
5334 qi(&fs.alias),
5335 );
5336 append_filter(&mut sql, &fs.filter);
5337 append_order_by(&mut sql, &fs.order_by);
5338 append_offset_limit(&mut sql, &fs.offset, &fs.limit);
5339 sql.push(')');
5340 sql
5341 }
5342
5343 IrExpr::PathSubquery(ps) => {
5344 let scalar = match &ps.result {
5345 IrPathResult::Scalar(e, _) => emit_expr(e),
5346 IrPathResult::Object { alias, .. } => format!("{}.\"id\"", qi(alias)),
5347 };
5348 let from_sql = emit_path_joins(&ps.root, &ps.joins);
5349 let mut sql = format!(
5350 "(SELECT {}{}\nFROM {}",
5351 if ps.distinct { "DISTINCT " } else { "" },
5352 scalar,
5353 from_sql
5354 );
5355 append_filter(&mut sql, &ps.filter);
5356 append_order_by(&mut sql, &ps.order_by);
5357 append_offset_limit(&mut sql, &ps.offset, &ps.limit);
5358 sql.push(')');
5359 sql
5360 }
5361
5362 IrExpr::ObjectPathUnion { branches, limit, multi } => {
5363 let arms: Vec<String> = branches
5364 .iter()
5365 .map(|ps| {
5366 let IrPathResult::Object {
5367 alias,
5368 type_name,
5369 shape,
5370 } = &ps.result
5371 else {
5372 unreachable!("an object path union's branches always land on objects")
5373 };
5374 let (pointer_exprs, _) = build_shape(shape, alias);
5375 let mut parts = vec![result_type_disc(ps, alias, type_name)];
5376 parts.extend(pointer_exprs);
5377 let mut sql = format!(
5378 "SELECT (\n {}\n) AS \"r\"\nFROM {}",
5379 parts.join(",\n "),
5380 emit_path_joins(&ps.root, &ps.joins),
5381 );
5382 append_filter(&mut sql, &ps.filter);
5383 sql
5384 })
5385 .collect();
5386 let mut sql = format!(
5387 "{}(SELECT \"r\" FROM (\n{}\n) AS \"_u\"",
5388 if *multi { "ARRAY" } else { "" },
5389 arms.join("\nUNION ALL\n")
5390 );
5391 if let Some(limit) = limit {
5392 sql.push_str(&format!("\nLIMIT {}", emit_expr(limit)));
5393 }
5394 sql.push(')');
5395 sql
5396 }
5397
5398 IrExpr::ObjectPathSubquery(ps) => {
5399 let IrPathResult::Object {
5400 alias,
5401 type_name,
5402 shape,
5403 } = &ps.result
5404 else {
5405 unreachable!("an object path subquery always lands on an object")
5406 };
5407 let (pointer_exprs, _) = build_shape(shape, alias);
5408 let mut parts = vec![result_type_disc(ps, alias, type_name)];
5409 parts.extend(pointer_exprs);
5410 let mut sql = format!(
5411 "(SELECT (\n {}\n)\nFROM {}",
5412 parts.join(",\n "),
5413 emit_path_joins(&ps.root, &ps.joins),
5414 );
5415 append_filter(&mut sql, &ps.filter);
5416 append_order_by(&mut sql, &ps.order_by);
5417 append_offset_limit(&mut sql, &ps.offset, &ps.limit);
5418 sql.push(')');
5419 sql
5420 }
5421
5422 IrExpr::ObjectSubquery(sel) => {
5423 let [IrRowSource::Bound { source, shape }] = sel.rows.as_slice() else {
5424 unreachable!("an object subquery is always schema-bound")
5425 };
5426 let alias = &source.alias;
5427 let (sub_exprs, _) = build_shape(shape, alias);
5428 let mut row_parts = vec![source_type_disc(source)];
5429 row_parts.extend(sub_exprs);
5430 let mut sql = format!(
5431 "(SELECT (\n {}\n )\n FROM {} AS {}",
5432 row_parts.join(",\n "),
5433 source_ref(source),
5434 qi(alias),
5435 );
5436 append_filter(&mut sql, &sel.filter);
5437 append_order_by(&mut sql, &sel.order_by);
5438 append_offset_limit(&mut sql, &sel.offset, &sel.limit);
5439 sql.push(')');
5440 sql
5441 }
5442
5443 IrExpr::Subquery(sel) => {
5444 let [IrRowSource::Bound { source, shape }] = sel.rows.as_slice() else {
5445 unreachable!("scalar/exists subquery is always schema-bound")
5446 };
5447 let alias = &source.alias;
5448 let mut sql = if shape.is_empty() {
5449 format!("(SELECT 1\nFROM {} AS {}", source_ref(source), qi(alias))
5451 } else if let Some(c) = shape.iter().find_map(|f| {
5452 if let IrShapePointer::Computed(c) = f {
5453 Some(c)
5454 } else {
5455 None
5456 }
5457 }) {
5458 format!(
5463 "(SELECT {}\nFROM {} AS {}",
5464 emit_expr(&c.expr),
5465 source_ref(source),
5466 qi(alias)
5467 )
5468 } else {
5469 let pk_col = shape
5471 .iter()
5472 .find_map(|f| {
5473 if let IrShapePointer::Scalar(s) = f {
5474 Some(s.column.as_str())
5475 } else {
5476 None
5477 }
5478 })
5479 .unwrap_or("id");
5480 format!(
5481 "(SELECT {}.{}\nFROM {} AS {}",
5482 qi(alias),
5483 qi(pk_col),
5484 source_ref(source),
5485 qi(alias),
5486 )
5487 };
5488 append_filter(&mut sql, &sel.filter);
5489 append_order_by(&mut sql, &sel.order_by);
5490 append_offset_limit(&mut sql, &sel.offset, &sel.limit);
5491 sql.push(')');
5492 sql
5493 }
5494 IrExpr::RawSql(s) => format!("({s})"),
5495 }
5496}
5497
5498fn emit_vector_search(vs: &IrVectorSearch) -> SqlOutput {
5501 let alias = &vs.source.alias;
5502 let dist_sql = format!(
5503 "{}.{} {} {}",
5504 qi(alias),
5505 qi(&vs.vector_col),
5506 vs.distance_op,
5507 emit_expr(&vs.query_expr),
5508 );
5509
5510 let (obj_tuple, object_shape_nodes) = if vs.object_shape.is_empty() {
5515 let type_expr = type_disc(&vs.source.type_name);
5517 let id_expr = format!("{}.\"id\"", qi(alias));
5518 let tuple = format!("{},\n {}", type_expr, id_expr);
5519 let id_node = ShapeNode::Scalar {
5520 name: "id".to_string(),
5521 position: 1,
5522 };
5523 (tuple, vec![id_node])
5524 } else {
5525 let (pointer_exprs, shape_pointers) = build_shape(&vs.object_shape, alias);
5526 let mut parts = vec![type_disc(&vs.source.type_name)];
5527 parts.extend(pointer_exprs);
5528 (parts.join(",\n "), prepend_type(shape_pointers))
5529 };
5530
5531 let outer = format!(
5533 "NULL::text,\n ROW(\n {}\n )::record,\n {}",
5534 obj_tuple, dist_sql,
5535 );
5536 let mut sql = format!(
5537 "SELECT (\n {}\n) AS result\nFROM {} AS {}",
5538 outer,
5539 source_ref(&vs.source),
5540 qi(alias),
5541 );
5542 append_filter(&mut sql, &vs.filter);
5543
5544 if let Some(dir) = &vs.order_by_distance {
5546 let dir_sql = match dir {
5547 IrSortDir::Asc => "ASC",
5548 IrSortDir::Desc => "DESC",
5549 };
5550 sql.push_str(&format!("\nORDER BY {} {}", dist_sql, dir_sql));
5551 }
5552 append_offset_limit(&mut sql, &vs.offset, &vs.limit);
5553
5554 let object_node = ShapeNode::Object {
5555 name: "object".to_string(),
5556 type_name: Some(vs.source.type_name.clone()),
5557 position: 1,
5558 cardinality: Cardinality::Many,
5559 pointers: object_shape_nodes,
5560 has_implicit_id: shape_has_implicit_id(&vs.object_shape),
5561 };
5562 let shape = ShapeDescriptor {
5563 root: ShapeNode::VectorSearch {
5564 object_position: 1,
5565 distance_position: 2,
5566 object_node: Box::new(object_node),
5567 },
5568 };
5569 let inference_plan = vs.inference_model.as_ref().map(|model_name| InferencePlan::Embedding {
5570 model_name: model_name.clone(),
5571 type_name: vs.inference_type_name.clone().unwrap_or_default(),
5572 index_name: vs.inference_index_name.clone().unwrap_or(None),
5573 query_param_name: vs.inference_query_param_name.clone().unwrap_or_default(),
5574 query_literal: vs.inference_query_literal.clone(),
5575 });
5576 SqlOutput {
5577 sql,
5578 shape,
5579 inference_plan,
5580 }
5581}
5582
5583fn emit_fts_search(fs: &IrFtsSearch) -> SqlOutput {
5586 use crate::schema::SearchBackend;
5587 if fs.backend != SearchBackend::Postgres {
5588 return emit_fts_search_deferred(fs);
5589 }
5590
5591 let alias = &fs.source.alias;
5592 let search_col = format!("{}.{}", qi(alias), qi(&fs.search_col));
5593 let query_sql = emit_expr(&fs.query_expr);
5594 let tsquery = format!("{}('english', {})", fs.tsquery_fn, query_sql);
5595 let rank_sql = format!("ts_rank({}, {})", search_col, tsquery);
5596
5597 let (obj_tuple, object_shape_nodes) = if fs.object_shape.is_empty() {
5598 let type_expr = type_disc(&fs.source.type_name);
5599 let id_expr = format!("{}.\"id\"", qi(alias));
5600 let tuple = format!("{},\n {}", type_expr, id_expr);
5601 let id_node = ShapeNode::Scalar {
5602 name: "id".to_string(),
5603 position: 1,
5604 };
5605 (tuple, vec![id_node])
5606 } else {
5607 let (pointer_exprs, shape_pointers) = build_shape(&fs.object_shape, alias);
5608 let mut parts = vec![type_disc(&fs.source.type_name)];
5609 parts.extend(pointer_exprs);
5610 (parts.join(",\n "), prepend_type(shape_pointers))
5611 };
5612
5613 let outer = format!(
5614 "NULL::text,\n ROW(\n {}\n )::record,\n {}",
5615 obj_tuple, rank_sql,
5616 );
5617 let mut sql = format!(
5618 "SELECT (\n {}\n) AS result\nFROM {} AS {}\nWHERE {} @@ {}",
5619 outer,
5620 source_ref(&fs.source),
5621 qi(alias),
5622 search_col,
5623 tsquery,
5624 );
5625 if let Some(f) = &fs.filter {
5626 sql.push_str(&format!(" AND ({})", emit_expr(f)));
5627 }
5628 if let Some(dir) = &fs.order_by_rank {
5629 let dir_sql = match dir {
5630 IrSortDir::Asc => "ASC",
5631 IrSortDir::Desc => "DESC",
5632 };
5633 sql.push_str(&format!("\nORDER BY {} {}", rank_sql, dir_sql));
5634 }
5635 append_offset_limit(&mut sql, &fs.offset, &fs.limit);
5636
5637 let object_node = ShapeNode::Object {
5638 name: "object".to_string(),
5639 type_name: Some(fs.source.type_name.clone()),
5640 position: 1,
5641 cardinality: Cardinality::Many,
5642 pointers: object_shape_nodes,
5643 has_implicit_id: shape_has_implicit_id(&fs.object_shape),
5644 };
5645 let shape = ShapeDescriptor {
5646 root: ShapeNode::FtsSearch {
5647 object_position: 1,
5648 rank_position: 2,
5649 object_node: Box::new(object_node),
5650 },
5651 };
5652 SqlOutput {
5653 sql,
5654 shape,
5655 inference_plan: None,
5656 }
5657}
5658
5659fn emit_fts_search_deferred(fs: &IrFtsSearch) -> SqlOutput {
5660 let alias = &fs.source.alias;
5661 let ids_idx = fs
5662 .deferred_ids_param
5663 .expect("deferred_ids_param must be set for deferred backend");
5664 let scores_idx = fs
5665 .deferred_scores_param
5666 .expect("deferred_scores_param must be set for deferred backend");
5667 let ids_param = format!("${}", ids_idx + 1);
5668 let scores_param = format!("${}", scores_idx + 1);
5669
5670 let (obj_tuple, object_shape_nodes) = if fs.object_shape.is_empty() {
5671 let type_expr = type_disc(&fs.source.type_name);
5672 let id_expr = format!("{}.\"id\"", qi(alias));
5673 let tuple = format!("{},\n {}", type_expr, id_expr);
5674 let id_node = ShapeNode::Scalar {
5675 name: "id".to_string(),
5676 position: 1,
5677 };
5678 (tuple, vec![id_node])
5679 } else {
5680 let (pointer_exprs, shape_pointers) = build_shape(&fs.object_shape, alias);
5681 let mut parts = vec![type_disc(&fs.source.type_name)];
5682 parts.extend(pointer_exprs);
5683 (parts.join(",\n "), prepend_type(shape_pointers))
5684 };
5685
5686 let outer = format!(
5687 "NULL::text,\n ROW(\n {}\n )::record,\n \"_os\".\"score\"",
5688 obj_tuple,
5689 );
5690 let mut sql = format!(
5691 concat!(
5692 "SELECT (\n {}\n) AS result\n",
5693 "FROM {} AS {}\n",
5694 "JOIN UNNEST({}::uuid[], {}::float8[]) AS \"_os\"(\"id\", \"score\")\n",
5695 " ON \"_os\".\"id\" = {}.\"id\"",
5696 ),
5697 outer,
5698 source_ref(&fs.source),
5699 qi(alias),
5700 ids_param,
5701 scores_param,
5702 qi(alias),
5703 );
5704 if let Some(f) = &fs.filter {
5705 sql.push_str(&format!("\nWHERE ({})", emit_expr(f)));
5706 }
5707 if let Some(dir) = &fs.order_by_rank {
5708 let dir_sql = match dir {
5709 IrSortDir::Asc => "ASC",
5710 IrSortDir::Desc => "DESC",
5711 };
5712 sql.push_str(&format!("\nORDER BY \"_os\".\"score\" {}", dir_sql));
5713 }
5714 let size = fs.limit.as_ref().and_then(|lim| {
5716 if let IrExpr::Literal(IrLiteral::Int(n)) = lim {
5717 Some(*n as usize)
5718 } else {
5719 None
5720 }
5721 });
5722
5723 let object_node = ShapeNode::Object {
5724 name: "object".to_string(),
5725 type_name: Some(fs.source.type_name.clone()),
5726 position: 1,
5727 cardinality: Cardinality::Many,
5728 pointers: object_shape_nodes,
5729 has_implicit_id: shape_has_implicit_id(&fs.object_shape),
5730 };
5731 let shape = ShapeDescriptor {
5732 root: ShapeNode::FtsSearch {
5733 object_position: 1,
5734 rank_position: 2,
5735 object_node: Box::new(object_node),
5736 },
5737 };
5738 let backend_str = match fs.backend {
5739 crate::schema::SearchBackend::Meilisearch => "meilisearch",
5740 _ => "opensearch",
5741 };
5742 let inference_plan = Some(InferencePlan::Search {
5743 backend: backend_str.to_string(),
5744 index_name: fs.deferred_index_name.clone().unwrap_or_default(),
5745 query_param_name: fs.deferred_query_param_name.clone().unwrap_or_default(),
5746 query_literal: fs.deferred_query_literal.clone(),
5747 size,
5748 });
5749 SqlOutput {
5750 sql,
5751 shape,
5752 inference_plan,
5753 }
5754}
5755
5756fn emit_function_select(sel: &IrFunctionSelect) -> SqlOutput {
5759 let alias = &sel.alias;
5760 let (pointer_exprs, shape_pointers) = build_shape(&sel.shape, alias);
5761
5762 let type_expr = if sel.polymorphic {
5763 format!("{}.\"__type__\"", qi(alias))
5764 } else {
5765 type_disc(&sel.type_name)
5766 };
5767 let mut parts = vec![type_expr];
5768 parts.extend(pointer_exprs);
5769 let tuple = parts.join(",\n ");
5770 let distinct = if sel.distinct { "DISTINCT " } else { "" };
5771
5772 let args_sql = sel.fn_args.iter().map(emit_expr).collect::<Vec<_>>().join(", ");
5773 let fn_call = format!("{}.{}({})", pg_schema(&sel.fn_module), qi(&sel.fn_name), args_sql);
5774
5775 let from_clause = if sel.polymorphic {
5776 format!("{} AS {}", fn_call, qi(alias))
5779 } else {
5780 format!("{} AS {}", fn_call, qi(alias))
5781 };
5782
5783 let mut sql = format!("SELECT {}(\n {}\n) AS result\nFROM {}", distinct, tuple, from_clause,);
5784 append_filter(&mut sql, &sel.filter);
5785 append_order_by(&mut sql, &sel.order_by);
5786 append_offset_limit(&mut sql, &sel.offset, &sel.limit);
5787
5788 let root_pointers = prepend_type(shape_pointers);
5789 SqlOutput {
5790 sql,
5791 shape: ShapeDescriptor {
5792 root: ShapeNode::Object {
5793 name: String::new(),
5794 type_name: Some(sel.type_name.clone()),
5795 position: 0,
5796 cardinality: Cardinality::Many,
5797 pointers: root_pointers,
5798 has_implicit_id: shape_has_implicit_id(&sel.shape),
5799 },
5800 },
5801 inference_plan: None,
5802 }
5803}
5804
5805pub fn emit_fn_body(ir: &crate::ir::IrOutput) -> String {
5811 with_subtype_fanouts(&ir.subtype_fanouts, || emit_output_fn_body(ir))
5812}
5813
5814fn emit_output_fn_body(ir: &crate::ir::IrOutput) -> String {
5815 let body = match &ir.stmt {
5816 IrStmt::Select(sel) if matches!(sel.rows.as_slice(), [IrRowSource::Free(IrFreeExpr::Scalar(_))]) => {
5817 let IrRowSource::Free(IrFreeExpr::Scalar(e)) = &sel.rows[0] else {
5818 unreachable!()
5819 };
5820 format!("SELECT {}", emit_expr(e))
5821 }
5822 other => emit_dml_as_cte_source(other),
5823 };
5824 if ir.ctes.is_empty() {
5825 body
5826 } else {
5827 let cte_prefix = emit_cte_prefix(&ir.ctes);
5828 format!("{}{}", cte_prefix, body)
5829 }
5830}
5831
5832fn emit_literal(lit: &IrLiteral) -> String {
5833 match lit {
5834 IrLiteral::Str(s) => sql_str(s),
5835 IrLiteral::Int(i) => i.to_string(),
5836 IrLiteral::Float(f) => {
5837 let s = f.to_string();
5846 let s = if s.contains('.') || s.contains('e') {
5847 s
5848 } else {
5849 format!("{}.0", s)
5850 };
5851 format!("({}::float8)", s)
5852 }
5853 IrLiteral::Bool(b) => {
5854 if *b {
5855 "TRUE".into()
5856 } else {
5857 "FALSE".into()
5858 }
5859 }
5860 }
5861}
5862
5863#[cfg(test)]
5866mod tests {
5867 use super::*;
5868 use crate::ir;
5869 use crate::parse;
5870 use crate::schema::{
5871 FunctionDescriptor, FunctionParamDescriptor, GlobalDescriptor, LinkDescriptor, MultiLinkDescriptor,
5872 NamedTupleDescriptor, PropertyDescriptor, SchemaDescriptor, TypeDescriptor,
5873 };
5874
5875 #[test]
5876 fn a_template_argument_keeps_the_parameters_it_carries() {
5877 let filled = fill_template("f($1, $2, $10)", &["($2)::int".to_string(), "b".to_string()]);
5880 assert_eq!(filled, "f(($2)::int, b, $10)");
5881 }
5882
5883 fn make_schema() -> SchemaDescriptor {
5884 SchemaDescriptor {
5885 types: vec![
5886 TypeDescriptor {
5887 name: "Person".into(),
5888 module: "default".into(),
5889 table: "Person".into(),
5890 abstract_: false,
5891 materialized: false,
5892 description: None,
5893 parents: vec![],
5894 interfaces: vec![],
5895 bases: vec![],
5896 properties: vec![
5897 PropertyDescriptor {
5898 name: "id".into(),
5899 pg_type: "uuid".into(),
5900 nullable: false,
5901 default_sql: Some("uuidv7()".into()),
5902 default_pyql: None,
5903 description: None,
5904 check_constraints: vec![],
5905 is_exclusive: true,
5906 is_pk: true,
5907 is_readonly: true,
5908 rewrites: vec![],
5909 tuple_members: None,
5910 column_type: None,
5911 },
5912 PropertyDescriptor {
5913 name: "name".into(),
5914 pg_type: "text".into(),
5915 nullable: false,
5916 default_sql: None,
5917 default_pyql: None,
5918 description: None,
5919 check_constraints: vec![],
5920 is_exclusive: false,
5921 is_pk: false,
5922 is_readonly: false,
5923 rewrites: vec![],
5924 tuple_members: None,
5925 column_type: None,
5926 },
5927 PropertyDescriptor {
5928 name: "age".into(),
5929 pg_type: "int8".into(),
5930 nullable: true,
5931 default_sql: None,
5932 default_pyql: None,
5933 description: None,
5934 check_constraints: vec![],
5935 is_exclusive: false,
5936 is_pk: false,
5937 is_readonly: false,
5938 rewrites: vec![],
5939 tuple_members: None,
5940 column_type: None,
5941 },
5942 ],
5943 links: vec![LinkDescriptor {
5944 name: "company".into(),
5945 target: "default::Company".into(),
5946 nullable: true,
5947 through: None,
5948 description: None,
5949 default_pyql: None,
5950 is_exclusive: false,
5951 is_readonly: false,
5952 rewrites: vec![],
5953 on_delete: vec![],
5954 }],
5955 multilinks: vec![MultiLinkDescriptor {
5956 name: "posts".into(),
5957 target: "default::Post".into(),
5958 through: None,
5959 nullable: false,
5960 description: None,
5961 default_pyql: None,
5962 on_delete: vec![],
5963 is_exclusive: false,
5964 }],
5965 computed: vec![],
5966 constraints: vec![],
5967 indexes: vec![],
5968 partition: None,
5969 vector_indexes: vec![],
5970 search_indexes: vec![],
5971 triggers: vec![],
5972 junction: false,
5973 signals: vec![],
5974 },
5975 TypeDescriptor {
5976 name: "Company".into(),
5977 module: "default".into(),
5978 table: "Company".into(),
5979 abstract_: false,
5980 materialized: false,
5981 description: None,
5982 parents: vec![],
5983 interfaces: vec![],
5984 bases: vec![],
5985 properties: vec![PropertyDescriptor {
5986 name: "name".into(),
5987 pg_type: "text".into(),
5988 nullable: false,
5989 default_sql: None,
5990 default_pyql: None,
5991 description: None,
5992 check_constraints: vec![],
5993 is_exclusive: false,
5994 is_pk: false,
5995 is_readonly: false,
5996 rewrites: vec![],
5997 tuple_members: None,
5998 column_type: None,
5999 }],
6000 links: vec![],
6001 multilinks: vec![],
6002 computed: vec![],
6003 constraints: vec![],
6004 indexes: vec![],
6005 partition: None,
6006 vector_indexes: vec![],
6007 search_indexes: vec![],
6008 triggers: vec![],
6009 junction: false,
6010 signals: vec![],
6011 },
6012 TypeDescriptor {
6013 name: "Post".into(),
6014 module: "default".into(),
6015 table: "Post".into(),
6016 abstract_: false,
6017 materialized: false,
6018 description: None,
6019 parents: vec![],
6020 interfaces: vec![],
6021 bases: vec![],
6022 properties: vec![PropertyDescriptor {
6023 name: "title".into(),
6024 pg_type: "text".into(),
6025 nullable: false,
6026 default_sql: None,
6027 default_pyql: None,
6028 description: None,
6029 check_constraints: vec![],
6030 is_exclusive: false,
6031 is_pk: false,
6032 is_readonly: false,
6033 rewrites: vec![],
6034 tuple_members: None,
6035 column_type: None,
6036 }],
6037 links: vec![],
6038 multilinks: vec![],
6039 computed: vec![],
6040 constraints: vec![],
6041 indexes: vec![],
6042 partition: None,
6043 vector_indexes: vec![],
6044 search_indexes: vec![],
6045 triggers: vec![],
6046 junction: false,
6047 signals: vec![],
6048 },
6049 ],
6050 scalars: vec![],
6051 enums: vec![],
6052 named_tuples: vec![],
6053 globals: vec![],
6054 functions: vec![],
6055 aliases: vec![],
6056 channels: vec![],
6057 ..Default::default()
6058 }
6059 }
6060
6061 fn compile_and_emit(query: &str) -> SqlOutput {
6062 let schema = make_schema();
6063 compile_and_emit_with(query, &schema)
6064 }
6065
6066 fn compile_and_emit_with(query: &str, schema: &SchemaDescriptor) -> SqlOutput {
6067 let ast = parse::parse(query).expect("parse failed");
6068 let ir = ir::compile(&ast, schema).expect("IR compile failed");
6069 emit(&ir)
6070 }
6071
6072 #[test]
6073 fn a_nested_read_inside_a_with_opens_no_second_with() {
6074 let out = compile_and_emit_with(
6075 "WITH a := (SELECT Person FILTER .age > 1), b := (SELECT Person LIMIT 1) SELECT (SELECT (a UNION b))",
6076 &make_schema(),
6077 );
6078 assert_eq!(out.sql.matches("WITH").count(), 1, "{}", out.sql);
6079 }
6080
6081 #[test]
6082 fn a_comparison_against_a_multi_row_with_binding_tests_membership() {
6083 let out = compile_and_emit_with(
6087 "WITH companies := (SELECT Company FILTER .name = 'x') SELECT Person FILTER .company = companies",
6088 &make_schema(),
6089 );
6090 assert!(
6091 out.sql.contains("= ANY((SELECT \"id\" FROM \"companies\"))"),
6092 "{}",
6093 out.sql
6094 );
6095
6096 let out = compile_and_emit_with(
6097 "WITH companies := (SELECT Company) SELECT Person FILTER any(.company = companies)",
6098 &make_schema(),
6099 );
6100 assert!(
6101 out.sql.contains("= ANY((SELECT \"id\" FROM \"companies\"))"),
6102 "{}",
6103 out.sql
6104 );
6105
6106 let out = compile_and_emit_with(
6107 "WITH companies := (SELECT Company) SELECT Person FILTER .company != companies",
6108 &make_schema(),
6109 );
6110 assert!(out.sql.contains("NOT (\"t1\".\"company_id\" = ANY("), "{}", out.sql);
6111 }
6112
6113 #[test]
6114 fn a_comparison_against_a_single_row_with_binding_stays_a_scalar_read() {
6115 let out = compile_and_emit_with(
6116 "WITH company := (SELECT Company LIMIT 1) SELECT Person FILTER .company = company",
6117 &make_schema(),
6118 );
6119 assert!(out.sql.contains("= (SELECT \"id\" FROM \"company\")"), "{}", out.sql);
6120 }
6121
6122 #[test]
6123 fn an_aggregate_over_a_backlink_counts_inside_a_subquery() {
6124 let out = compile_and_emit_with("SELECT Post { n := count(.<posts) }", &make_schema());
6125 assert!(
6126 !out.sql.contains("count((SELECT"),
6127 "an aggregate over a scalar subquery:\n{}",
6128 out.sql
6129 );
6130 assert!(out.sql.contains("count("), "{}", out.sql);
6131 }
6132
6133 #[test]
6134 fn an_aggregate_over_a_relative_subselect_counts_inside_a_subquery() {
6135 let out = compile_and_emit_with(
6136 "SELECT Post { n := count((SELECT .<posts FILTER .age > 1)) }",
6137 &make_schema(),
6138 );
6139 assert!(
6140 !out.sql.contains("count((SELECT"),
6141 "an aggregate over a scalar subquery:\n{}",
6142 out.sql
6143 );
6144 assert!(
6145 out.sql.contains(r#"(SELECT count("_s"."v") FROM unnest(ARRAY("#),
6146 "{}",
6147 out.sql
6148 );
6149 }
6150
6151 #[test]
6152 fn any_over_a_link_type_check_tests_each_element() {
6153 let out = compile_and_emit_with(
6154 "SELECT Company { name } FILTER any(.<company.posts IS Post)",
6155 &make_schema(),
6156 );
6157 assert!(
6158 !out.sql.contains("bool_or(ARRAY"),
6159 "an aggregate over outer rows:\n{}",
6160 out.sql
6161 );
6162 assert!(
6163 out.sql
6164 .contains("FROM unnest(ARRAY(SELECT ('default::Post' = 'default::Post')"),
6165 "{}",
6166 out.sql
6167 );
6168 }
6169
6170 #[test]
6171 fn a_link_type_check_reads_the_linked_object() {
6172 let out = compile_and_emit_with("SELECT Person { name } FILTER .company IS Company", &make_schema());
6173 assert!(!out.sql.contains("WHERE FALSE"), "{}", out.sql);
6174 assert!(out.sql.contains("'default::Company'"), "{}", out.sql);
6175 }
6176
6177 fn schema_with_a_subtype() -> SchemaDescriptor {
6179 let mut schema = make_schema();
6180 let mut admin = schema.types[0].clone();
6181 admin.name = "Admin".into();
6182 admin.table = "Admin".into();
6183 admin.bases = vec!["default::Person".into()];
6184 schema.types.push(admin);
6185 schema
6186 }
6187
6188 #[test]
6189 fn a_type_with_subtypes_reads_their_rows_too() {
6190 let out = compile_and_emit_with("SELECT Person { name }", &schema_with_a_subtype());
6191 assert!(
6192 out.sql.contains("'default::Admin'::text AS \"__type__\"") && out.sql.contains("FROM \"public\".\"Admin\""),
6193 "expected the subtype's table in the read:\n{}",
6194 out.sql
6195 );
6196 }
6197
6198 #[test]
6199 fn a_path_from_a_type_with_subtypes_reads_their_rows_too() {
6200 let out = compile_and_emit_with("SELECT Person.name", &schema_with_a_subtype());
6201 assert!(out.sql.contains("FROM \"public\".\"Admin\""), "{}", out.sql);
6202 }
6203
6204 #[test]
6205 fn a_multilink_of_a_type_with_subtypes_reads_their_junctions_too() {
6206 let out = compile_and_emit_with("SELECT Person { posts: { title } }", &schema_with_a_subtype());
6207 assert!(
6208 out.sql.contains(
6209 "(SELECT \"source\", \"target\" FROM \"public\".\"Person.posts\" UNION ALL \
6210 SELECT \"source\", \"target\" FROM \"public\".\"Admin.posts\")"
6211 ),
6212 "expected the junctions unioned:\n{}",
6213 out.sql
6214 );
6215 }
6216
6217 #[test]
6218 fn is_a_supertype_matches_its_subtypes() {
6219 let out = compile_and_emit_with(
6220 "SELECT Person { name } FILTER Person IS Admin",
6221 &schema_with_a_subtype(),
6222 );
6223 assert!(out.sql.contains("\"__type__\" = 'default::Admin'"), "{}", out.sql);
6224 }
6225
6226 #[test]
6227 fn a_write_to_a_type_with_subtypes_reaches_their_tables() {
6228 let out = compile_and_emit_with("UPDATE Person SET { age := 1 }", &schema_with_a_subtype());
6229 assert!(
6230 out.sql.contains("UPDATE \"public\".\"Person\"") && out.sql.contains("UPDATE \"public\".\"Admin\""),
6231 "expected both tables updated:\n{}",
6232 out.sql
6233 );
6234 assert!(
6235 !out.sql.contains("UPDATE (\n"),
6236 "a write never targets the union:\n{}",
6237 out.sql
6238 );
6239 }
6240
6241 #[test]
6242 fn test_comparing_a_multilink_itself_to_an_object() {
6243 let out = compile_and_emit_with(
6247 "WITH p := (SELECT Post LIMIT 1) SELECT Person { id } FILTER any(.posts = p)",
6248 &make_schema(),
6249 );
6250 assert!(
6251 out.sql.contains("EXISTS((SELECT 1\nFROM \"public\".\"Person.posts\""),
6252 "expected a junction EXISTS:\n{}",
6253 out.sql
6254 );
6255 assert!(
6256 out.sql.contains("\"target\" = (SELECT \"id\" FROM \"p\")"),
6257 "expected the junction target compared to the bound object's id:\n{}",
6258 out.sql
6259 );
6260 }
6261
6262 #[test]
6263 fn test_comparing_a_multilink_itself_warns_without_any() {
6264 let ast = parse::parse("WITH p := (SELECT Post LIMIT 1) SELECT Person { id } FILTER .posts = p").unwrap();
6265 let ir = ir::compile(&ast, &make_schema()).unwrap();
6266 assert!(
6267 ir.warnings.iter().any(|w| w.contains("multi-link '.posts'")),
6268 "expected the set-valued FILTER warning, got: {:?}",
6269 ir.warnings
6270 );
6271 }
6272
6273 #[test]
6274 fn test_detached_names_the_type_of_a_nested_select() {
6275 let out = compile_and_emit_with(
6279 "INSERT Person { name := $n, company := (SELECT detached Company FILTER .name = $c LIMIT 1) }",
6280 &make_schema(),
6281 );
6282 assert!(
6283 out.sql.contains("INSERT INTO \"public\".\"Person\""),
6284 "expected the insert to compile:\n{}",
6285 out.sql
6286 );
6287 assert!(
6288 out.sql.contains("FROM \"public\".\"Company\""),
6289 "expected the detached select to read the Company table:\n{}",
6290 out.sql
6291 );
6292 }
6293
6294 #[test]
6295 fn test_select_over_an_update_on_a_with_binding() {
6296 let out = compile_and_emit_with(
6301 "WITH p := (SELECT Person FILTER .id = $a LIMIT 1) SELECT (UPDATE p SET { name := $n }) { id }",
6302 &make_schema(),
6303 );
6304 assert!(
6305 out.sql.contains("UPDATE \"public\".\"Person\""),
6306 "expected the update to resolve to the bound type's table:\n{}",
6307 out.sql
6308 );
6309 }
6310
6311 #[test]
6312 fn test_a_mutation_as_a_free_object_field() {
6313 let out = compile_and_emit_with(
6317 "SELECT { renamed := (UPDATE Person FILTER .id = $a SET { name := $n }), n := 1 }",
6318 &make_schema(),
6319 );
6320 assert!(
6321 out.sql.contains("UPDATE \"public\".\"Person\""),
6322 "expected the mutation to become a data-modifying CTE:\n{}",
6323 out.sql
6324 );
6325 assert!(
6326 out.sql
6327 .contains("(SELECT \"t1\".\"id\"\nFROM \"_nested_dml_0\" AS \"t1\") AS \"_f0\""),
6328 "expected the field to carry the rows the mutation touched:\n{}",
6329 out.sql
6330 );
6331 }
6332
6333 #[test]
6334 fn test_a_sub_selects_filter_stays_on_its_own_subject_when_a_field_chain_projects_off_it() {
6335 let out = compile_and_emit_with("SELECT (SELECT Person FILTER .name = $n).company.name", &make_schema());
6339 assert!(
6340 out.sql.contains("WHERE (\"t0\".\"name\" = $1)"),
6341 "the inner filter belongs to the Person alias:\n{}",
6342 out.sql
6343 );
6344 }
6345
6346 #[test]
6347 fn test_reading_back_a_nested_insert_through_its_own_statement() {
6348 let out = compile_and_emit_with(
6349 "SELECT (INSERT Person { name := $n, posts := (INSERT Post { title := $t }) }) { id, posts: { title } }",
6350 &make_schema(),
6351 );
6352 assert!(out.sql.contains("__ml_add_0"), "got:\n{}", out.sql);
6353 }
6354
6355 #[test]
6356 fn test_reading_back_a_nested_insert_within_its_own_statement() {
6357 let out = compile_and_emit_with(
6361 "SELECT (INSERT Person { name := $n, posts := (INSERT Post { title := $t }) }) \
6362 { id, posts: { title } }",
6363 &make_schema(),
6364 );
6365 assert!(
6366 out.sql.contains("FROM \"_dml__ml_add_0\" AS \"jt\""),
6367 "the junction rows must come from the CTE that wrote them:\n{}",
6368 out.sql
6369 );
6370 assert!(
6371 out.sql.contains("\"_nested_dml_0\""),
6372 "and the targets from the nested insert's own CTE:\n{}",
6373 out.sql
6374 );
6375 }
6376
6377 #[test]
6378 fn test_a_nested_single_link_insert_reads_back_from_its_cte() {
6379 let out = compile_and_emit_with(
6380 "SELECT (INSERT Person { name := $n, company := (INSERT Company { name := $c }) }) \
6381 { id, company: { name } }",
6382 &make_schema(),
6383 );
6384 assert!(
6385 out.sql.contains("FROM \"_nested_dml_0\""),
6386 "the linked row must be read from the CTE that inserted it:\n{}",
6387 out.sql
6388 );
6389 }
6390
6391 #[test]
6392 fn test_a_free_object_field_holding_an_object_keeps_it_an_object() {
6393 let out = compile_and_emit_with(
6397 "WITH c := (SELECT Company LIMIT 1) SELECT { co := c { name }, n := 1 }",
6398 &make_schema(),
6399 );
6400 assert!(
6401 out.sql.contains("'default::Company'::text"),
6402 "the field must carry the object's own row:\n{}",
6403 out.sql
6404 );
6405 }
6406
6407 #[test]
6408 fn test_projecting_an_object_off_a_free_object() {
6409 let out = compile_and_emit_with(
6412 "WITH c := (SELECT Company LIMIT 1) SELECT { co := c { name }, n := 1 }.co",
6413 &make_schema(),
6414 );
6415 assert!(
6416 !out.sql.contains("jsonb_build_object"),
6417 "projecting a pointer must not go through jsonb:\n{}",
6418 out.sql
6419 );
6420 assert!(
6421 out.sql.contains("SELECT v AS result, v FROM"),
6422 "the object row is already the result row:\n{}",
6423 out.sql
6424 );
6425 }
6426
6427 #[test]
6428 fn test_a_shape_on_a_single_valued_object_path() {
6429 let out = compile_and_emit_with("SELECT Person { c := .company { name } }", &make_schema());
6434 assert!(
6435 out.sql.contains("'default::Company'::text"),
6436 "the pointer must carry the object's own row:\n{}",
6437 out.sql
6438 );
6439 assert!(
6440 !out.sql.contains("ARRAY(SELECT"),
6441 "a single-valued walk is one object, not an array of one:\n{}",
6442 out.sql
6443 );
6444 }
6445
6446 #[test]
6447 fn test_a_shapeless_single_link_still_reads_as_its_value() {
6448 let out = compile_and_emit_with("SELECT Person { c := .company }", &make_schema());
6451 assert!(
6452 !out.sql.contains("'default::Company'::text"),
6453 "a bare link reference must not grow an object row:\n{}",
6454 out.sql
6455 );
6456 }
6457
6458 #[test]
6459 fn test_a_tuple_element_holding_a_shaped_object() {
6460 let out = compile_and_emit_with(
6464 "WITH c := (SELECT Company LIMIT 1) SELECT (c { name }, 1)",
6465 &make_schema(),
6466 );
6467 assert!(
6468 out.sql.contains("'default::Company'::text"),
6469 "the element must carry the object's own row:\n{}",
6470 out.sql
6471 );
6472 }
6473
6474 #[test]
6475 fn test_an_object_returning_function_as_a_link_value() {
6476 let mut schema = make_schema();
6481 let company = schema
6483 .types
6484 .iter_mut()
6485 .find(|t| t.name == "Company")
6486 .expect("make_schema declares Company");
6487 company.properties.push(PropertyDescriptor {
6488 name: "id".into(),
6489 pg_type: "uuid".into(),
6490 nullable: false,
6491 default_sql: None,
6492 default_pyql: None,
6493 description: None,
6494 check_constraints: vec![],
6495 is_exclusive: true,
6496 is_pk: true,
6497 is_readonly: true,
6498 rewrites: vec![],
6499 tuple_members: None,
6500 column_type: None,
6501 });
6502 schema.functions.push(crate::schema::FunctionDescriptor {
6503 name: "current_company".into(),
6504 module: "default".into(),
6505 params: vec![],
6506 return_pg_type: "default::Company".into(),
6507 return_is_object: true,
6508 return_is_set: false,
6509 return_is_polymorphic: false,
6510 volatility: "stable".into(),
6511 body: "select Company limit 1".into(),
6512 });
6513 let out = compile_and_emit_with("INSERT Person { name := $n, company := current_company() }", &schema);
6514 assert!(
6515 out.sql.contains("FROM \"public\".\"current_company\"()"),
6516 "the function must supply the foreign key:\n{}",
6517 out.sql
6518 );
6519 }
6520
6521 fn make_schema_with_an_object_returning_default() -> SchemaDescriptor {
6525 let mut schema = make_schema();
6526 let company = schema
6527 .types
6528 .iter_mut()
6529 .find(|t| t.name == "Company")
6530 .expect("make_schema declares Company");
6531 company.properties.push(PropertyDescriptor {
6532 name: "id".into(),
6533 pg_type: "uuid".into(),
6534 nullable: false,
6535 default_sql: None,
6536 default_pyql: None,
6537 description: None,
6538 check_constraints: vec![],
6539 is_exclusive: true,
6540 is_pk: true,
6541 is_readonly: true,
6542 rewrites: vec![],
6543 tuple_members: None,
6544 column_type: None,
6545 });
6546 schema.functions.push(crate::schema::FunctionDescriptor {
6547 name: "current_company".into(),
6548 module: "default".into(),
6549 params: vec![],
6550 return_pg_type: "default::Company".into(),
6551 return_is_object: true,
6552 return_is_set: false,
6553 return_is_polymorphic: false,
6554 volatility: "stable".into(),
6555 body: "select Company limit 1".into(),
6556 });
6557 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
6558 let company_link = person.links.iter_mut().find(|l| l.name == "company").unwrap();
6559 company_link.default_pyql = Some("current_company()".into());
6560 schema
6561 }
6562
6563 #[test]
6564 fn an_object_returning_link_default_is_applied_by_the_insert() {
6565 let schema = make_schema_with_an_object_returning_default();
6569 let out = compile_and_emit_with("INSERT Person { name := $n }", &schema);
6570 assert!(
6571 out.sql.contains("\"company_id\"") && out.sql.contains("current_company"),
6572 "the default must supply the foreign key:\n{}",
6573 out.sql
6574 );
6575 }
6576
6577 #[test]
6578 fn an_explicit_value_wins_over_an_inlined_default() {
6579 let schema = make_schema_with_an_object_returning_default();
6580 let out = compile_and_emit_with("INSERT Person { name := $n, company := {} }", &schema);
6581 assert!(
6582 !out.sql.contains("current_company"),
6583 "the shape named the pointer, so the default does not apply:\n{}",
6584 out.sql
6585 );
6586 }
6587
6588 #[test]
6589 fn an_object_returning_link_default_gets_no_column_default() {
6590 let schema = make_schema_with_an_object_returning_default();
6593 let ddl = crate::export::export_schema(&schema).unwrap();
6594 assert!(
6595 !ddl.contains("\"company_id\" uuid DEFAULT"),
6596 "the column must carry no default:\n{ddl}"
6597 );
6598 crate::validate::validate_schema_types(&schema).expect("an inlined default is not an error");
6599 }
6600
6601 #[test]
6602 fn a_column_expressible_default_stays_in_the_ddl() {
6603 let mut schema = make_schema();
6605 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
6606 let age = person.properties.iter_mut().find(|p| p.name == "age").unwrap();
6607 age.default_pyql = Some("21".into());
6608 assert!(
6609 crate::ir::inlined_pointer_defaults(schema.types.iter().find(|t| t.name == "Person").unwrap(), &schema)
6610 .is_empty()
6611 );
6612 let ddl = crate::export::export_schema(&schema).unwrap();
6613 assert!(ddl.contains("DEFAULT 21"), "{ddl}");
6614 let out = compile_and_emit_with("INSERT Person { name := $n }", &schema);
6615 assert!(
6616 !out.sql.contains("21"),
6617 "the column DEFAULT still applies it:\n{}",
6618 out.sql
6619 );
6620 }
6621
6622 fn make_schema_with_an_inherited_self_naming_computed() -> SchemaDescriptor {
6626 let mut schema = make_schema();
6627 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
6628 person.computed.push(crate::schema::ComputedDescriptor {
6629 name: "own_name".into(),
6630 expression: "default::Person.name".into(),
6631 return_type: Some("text".into()),
6632 link_target: None,
6633 link_multi: false,
6634 });
6635 let mut employee = person.clone();
6636 employee.name = "Employee".into();
6637 employee.table = "Employee".into();
6638 employee.bases = vec!["default::Person".into()];
6639 schema.types.push(employee);
6640 schema
6641 }
6642
6643 #[test]
6644 fn an_inherited_computed_naming_its_declaring_type_still_means_the_row() {
6645 let schema = make_schema_with_an_inherited_self_naming_computed();
6650 let out = compile_and_emit_with("SELECT Employee { own_name }", &schema);
6651 assert!(
6652 !out.sql.contains("\"Person\""),
6653 "the computed must read the Employee row, not Person's table:\n{}",
6654 out.sql
6655 );
6656 crate::validate::validate_schema_types(&schema).expect("an inherited computed is single-valued");
6657 }
6658
6659 #[test]
6660 fn an_inherited_type_check_computed_tests_the_row_it_is_read_on() {
6661 let mut schema = make_schema();
6667 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
6668 person.computed.push(crate::schema::ComputedDescriptor {
6669 name: "employed".into(),
6670 expression: "(default::Person is default::Employee)".into(),
6671 return_type: Some("boolean".into()),
6672 link_target: None,
6673 link_multi: false,
6674 });
6675 let mut employee = person.clone();
6676 employee.name = "Employee".into();
6677 employee.table = "Employee".into();
6678 employee.bases = vec!["default::Person".into()];
6679 schema.types.push(employee);
6680
6681 crate::validate::validate_schema_types(&schema).expect("the check is single-valued on both types");
6682 let out = compile_and_emit_with("SELECT Employee { employed }", &schema);
6683 assert!(
6684 !out.sql.contains("\"Person\""),
6685 "the check must read the Employee row's own type:\n{}",
6686 out.sql
6687 );
6688 }
6689
6690 #[test]
6691 fn a_shape_writing_the_computed_out_itself_does_not_get_the_anchor() {
6692 let schema = make_schema_with_an_inherited_self_naming_computed();
6700 let ast = parse::parse("SELECT Employee { own_name := default::Person.name }").expect("parse failed");
6701 let Err(error) = ir::compile(&ast, &schema) else {
6702 panic!("an absolute path has nothing to anchor to here")
6703 };
6704 assert!(
6705 format!("{error:?}").contains("absolute paths are not valid"),
6706 "{error:?}"
6707 );
6708 }
6709
6710 #[test]
6711 fn a_default_that_compiles_nowhere_is_still_an_error() {
6712 let mut schema = make_schema();
6715 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
6716 let age = person.properties.iter_mut().find(|p| p.name == "age").unwrap();
6717 age.default_pyql = Some("sequence_next(INTROSPECT default::AgeSeq)".into());
6718 let errs = crate::validate::validate_schema_types(&schema).unwrap_err();
6719 let (_, msg, _) = errs[0].class_name_message_position();
6720 assert!(msg.contains("default"), "{msg}");
6721 }
6722
6723 #[test]
6724 fn test_comparing_a_link_with_an_object_returning_function() {
6725 let mut schema = make_schema();
6729 let company = schema
6731 .types
6732 .iter_mut()
6733 .find(|t| t.name == "Company")
6734 .expect("make_schema declares Company");
6735 company.properties.push(PropertyDescriptor {
6736 name: "id".into(),
6737 pg_type: "uuid".into(),
6738 nullable: false,
6739 default_sql: None,
6740 default_pyql: None,
6741 description: None,
6742 check_constraints: vec![],
6743 is_exclusive: true,
6744 is_pk: true,
6745 is_readonly: true,
6746 rewrites: vec![],
6747 tuple_members: None,
6748 column_type: None,
6749 });
6750 schema.functions.push(crate::schema::FunctionDescriptor {
6751 name: "current_company".into(),
6752 module: "default".into(),
6753 params: vec![],
6754 return_pg_type: "default::Company".into(),
6755 return_is_object: true,
6756 return_is_set: false,
6757 return_is_polymorphic: false,
6758 volatility: "stable".into(),
6759 body: "select Company limit 1".into(),
6760 });
6761 let out = compile_and_emit_with("SELECT Person FILTER .company = current_company()", &schema);
6762 assert!(
6763 out.sql.contains("\"company_id\"") && out.sql.contains("current_company"),
6764 "the link's key against the function's row:\n{}",
6765 out.sql
6766 );
6767 }
6768
6769 #[test]
6770 fn test_choosing_between_two_object_sets() {
6771 let out = compile_and_emit_with(
6775 "WITH a := (SELECT Person FILTER .name = $n), b := (SELECT Person) \
6776 SELECT (a IF EXISTS a ELSE b) { name }",
6777 &make_schema(),
6778 );
6779 assert!(
6780 out.sql.contains("UNION ALL"),
6781 "expected one branch per side:\n{}",
6782 out.sql
6783 );
6784 }
6785
6786 #[test]
6787 fn test_a_union_operand_written_inline() {
6788 let out = compile_and_emit_with(
6792 "SELECT ((SELECT Person FILTER .name = $n) UNION (SELECT Person)) { name }",
6793 &make_schema(),
6794 );
6795 assert!(
6796 out.sql.starts_with("WITH"),
6797 "expected the operands hoisted:\n{}",
6798 out.sql
6799 );
6800 assert!(out.sql.contains("UNION ALL"), "{}", out.sql);
6801 }
6802
6803 #[test]
6804 fn test_a_for_loop_variable_over_objects() {
6805 let out = compile_and_emit_with("FOR p IN (SELECT Person) UNION (SELECT p.name)", &make_schema());
6809 assert!(
6810 out.sql.contains("FROM \"public\".\"Person\"") && out.sql.contains("\"_for_p\".\"v\""),
6811 "the body must read the row the variable names:\n{}",
6812 out.sql
6813 );
6814 }
6815
6816 #[test]
6817 fn test_a_shape_on_a_for_loop_variable() {
6818 let out = compile_and_emit_with("FOR p IN (SELECT Person) UNION (SELECT p { name })", &make_schema());
6819 assert!(
6820 out.sql.contains("'default::Person'::text"),
6821 "a shape on the variable yields its object:\n{}",
6822 out.sql
6823 );
6824 }
6825
6826 #[test]
6827 fn test_a_for_loop_variable_over_values_is_still_a_scalar() {
6828 let out = compile_and_emit_with("FOR n IN {1, 2} UNION (SELECT n)", &make_schema());
6829 assert!(out.sql.contains("VALUES"), "{}", out.sql);
6830 }
6831
6832 #[test]
6833 fn test_exists_over_a_sub_select_on_a_relative_path() {
6834 let out = compile_and_emit_with(
6839 "SELECT Person { id } FILTER EXISTS ((SELECT .posts FILTER .title = $t))",
6840 &make_schema(),
6841 );
6842 assert!(out.sql.contains("EXISTS("), "{}", out.sql);
6843 assert!(
6844 out.sql.contains("\"Person.posts\""),
6845 "the junction join must survive into the EXISTS:\n{}",
6846 out.sql
6847 );
6848 assert!(
6849 out.sql.contains("\"t0\".\"id\""),
6850 "and it must correlate to the enclosing row:\n{}",
6851 out.sql
6852 );
6853 }
6854
6855 #[test]
6856 fn test_counting_the_rows_a_mutation_touched() {
6857 let out = compile_and_emit_with("SELECT count((DELETE Person FILTER .name = $n))", &make_schema());
6861 assert!(out.sql.starts_with("WITH"), "{}", out.sql);
6862 assert!(out.sql.contains("DELETE FROM \"public\".\"Person\""), "{}", out.sql);
6863 assert!(out.sql.contains("count(*)"), "{}", out.sql);
6864 }
6865
6866 #[test]
6867 fn test_a_conflict_target_naming_several_columns() {
6868 let out = compile_and_emit_with(
6872 "INSERT Person { name := $n } UNLESS CONFLICT ON (.name, .id) ELSE (UPDATE Person SET { name := $n })",
6873 &make_schema(),
6874 );
6875 assert!(
6876 out.sql.contains("ON CONFLICT (\"name\", \"id\")"),
6877 "expected a column list:\n{}",
6878 out.sql
6879 );
6880 }
6881
6882 #[test]
6883 fn test_a_for_loop_over_a_path() {
6884 let out = compile_and_emit_with(
6888 "WITH p := (SELECT Person LIMIT 1) FOR post IN p.posts UNION (SELECT post.title)",
6889 &make_schema(),
6890 );
6891 assert!(out.sql.contains("CROSS JOIN LATERAL"), "{}", out.sql);
6892 }
6893
6894 #[test]
6895 fn test_a_relative_sub_select_keeps_its_enclosing_object() {
6896 let out = compile_and_emit_with(
6901 "SELECT Person { ps := assert_exists((SELECT .posts { title })) }",
6902 &make_schema(),
6903 );
6904 assert!(out.sql.contains("assert_exists"), "{}", out.sql);
6905 assert!(
6906 out.sql.contains("\"Person.posts\""),
6907 "the walk must reach the junction:\n{}",
6908 out.sql
6909 );
6910 }
6911
6912 #[test]
6913 fn test_a_relative_sub_select_as_a_multi_link_value() {
6914 let out = compile_and_emit_with(
6915 "UPDATE Person FILTER .name = $n SET { posts := (SELECT .posts FILTER .title = $t) }",
6916 &make_schema(),
6917 );
6918 assert!(
6919 out.sql.contains("\"Person.posts\""),
6920 "the value must read the row's own posts:\n{}",
6921 out.sql
6922 );
6923 }
6924
6925 #[test]
6926 fn test_a_shape_written_after_a_sub_select() {
6927 let out = compile_and_emit_with(
6931 "SELECT Person { ps := (SELECT Post FILTER .title = $t) { title } }",
6932 &make_schema(),
6933 );
6934 assert!(
6935 out.sql.contains("'default::Post'::text"),
6936 "the pointer must carry the object rows:\n{}",
6937 out.sql
6938 );
6939 }
6940
6941 #[test]
6942 fn test_a_shape_after_a_sub_select_with_a_with_block() {
6943 let out = compile_and_emit_with(
6944 "SELECT Person { ps := (WITH t := $t SELECT Post FILTER .title = t) { title } }",
6945 &make_schema(),
6946 );
6947 assert!(out.sql.contains("'default::Post'::text"), "{}", out.sql);
6948 }
6949
6950 #[test]
6951 fn test_a_computed_declared_as_an_object_returning_call() {
6952 let mut schema = make_schema();
6957 schema.functions.push(crate::schema::FunctionDescriptor {
6958 name: "recent".into(),
6959 module: "default".into(),
6960 params: vec![],
6961 return_pg_type: "default::Post".into(),
6962 return_is_object: true,
6963 return_is_set: true,
6964 return_is_polymorphic: false,
6965 volatility: "stable".into(),
6966 body: "select Post".into(),
6967 });
6968 schema.types[0].computed.push(crate::schema::ComputedDescriptor {
6969 name: "recent".into(),
6970 expression: "default::recent()".into(),
6971 return_type: None,
6972 link_target: None,
6973 link_multi: false,
6974 });
6975 let out = compile_and_emit_with("SELECT Person { recent }", &schema);
6976 assert!(
6977 out.sql.contains("ARRAY(SELECT") && out.sql.contains("'default::Post'::text"),
6978 "the pointer must carry the function's rows:\n{}",
6979 out.sql
6980 );
6981 }
6982
6983 #[test]
6984 fn test_one_branch_of_an_object_if_else_is_empty() {
6985 let out = compile_and_emit_with(
6989 "WITH p := (SELECT Person LIMIT 1) SELECT p { name } IF EXISTS p ELSE {}",
6990 &make_schema(),
6991 );
6992 assert!(!out.sql.contains("UNION ALL"), "nothing to union with:\n{}", out.sql);
6993 assert!(out.sql.contains("'default::Person'::text"), "{}", out.sql);
6994 }
6995
6996 #[test]
6997 fn test_a_conditional_insert_is_guarded_by_its_condition() {
6998 let out = compile_and_emit_with("SELECT (INSERT Person { name := $n }) IF FALSE ELSE {}", &make_schema());
7003 assert!(
7004 out.sql.contains("SELECT") && out.sql.contains("WHERE"),
7005 "a guarded insert selects its row rather than listing values:\n{}",
7006 out.sql
7007 );
7008 assert!(!out.sql.contains("VALUES"), "VALUES always yields a row:\n{}", out.sql);
7009 }
7010
7011 #[test]
7012 fn test_a_conditional_update_carries_its_condition() {
7013 let out = compile_and_emit(
7018 "SELECT (UPDATE Person FILTER .name = $n SET { name := $m }) IF EXISTS (SELECT Company) ELSE {}",
7019 );
7020 let update = out.sql.find("UPDATE").expect("an update is emitted");
7021 let where_clause = out.sql[update..].find("WHERE").expect("the update is filtered");
7022 assert!(
7023 out.sql[update + where_clause..].contains("EXISTS"),
7024 "the condition narrows the rows the update touches:\n{}",
7025 out.sql
7026 );
7027 }
7028
7029 #[test]
7030 fn test_a_conditional_delete_carries_its_condition() {
7031 let out = compile_and_emit("SELECT (DELETE Person FILTER .name = $n) IF FALSE ELSE {}");
7035 let delete = out
7036 .sql
7037 .split("DELETE FROM")
7038 .nth(1)
7039 .expect("the delete should still be emitted");
7040 let where_clause = delete.split("WHERE").nth(1).expect("the delete should be filtered");
7041 assert!(
7042 where_clause.contains("FALSE") || where_clause.contains("false"),
7043 "the condition must narrow the delete itself:\n{}",
7044 out.sql
7045 );
7046 }
7047
7048 #[test]
7049 fn test_a_pointer_a_binding_declared_in_its_own_shape() {
7050 let out = compile_and_emit_with(
7054 "WITH p := (SELECT Person { co := .company { name } } LIMIT 1) SELECT p { name, co: { name } }",
7055 &make_schema(),
7056 );
7057 assert!(
7058 out.sql.contains("'default::Company'::text"),
7059 "the declared pointer must be inlined:\n{}",
7060 out.sql
7061 );
7062 }
7063
7064 #[test]
7065 fn test_a_binding_declared_pointer_read_from_a_tuple_element() {
7066 let out = compile_and_emit_with(
7067 "WITH p := (SELECT Person { co := .company { name } } LIMIT 1) SELECT (p { co: { name } }, 1)",
7068 &make_schema(),
7069 );
7070 assert!(out.sql.contains("'default::Company'::text"), "{}", out.sql);
7071 }
7072
7073 #[test]
7074 fn test_a_for_loop_variable_shaped_inside_a_tuple() {
7075 let out = compile_and_emit_with(
7079 "WITH p := (SELECT Person LIMIT 1) FOR q IN p.posts UNION (SELECT (a := q { title }, b := 1))",
7080 &make_schema(),
7081 );
7082 assert!(
7083 out.sql.contains("\"_for_q\".\"v\""),
7084 "the element must be narrowed to the loop's row:\n{}",
7085 out.sql
7086 );
7087 }
7088
7089 #[test]
7090 fn test_choosing_between_two_walks() {
7091 let out = compile_and_emit_with(
7094 "WITH p := (SELECT Person LIMIT 1), q := (SELECT p.posts LIMIT 1) IF TRUE ELSE (SELECT p.posts) \
7095 SELECT q { title }",
7096 &make_schema(),
7097 );
7098 assert!(out.sql.contains("UNION ALL"), "one branch per side:\n{}", out.sql);
7099 }
7100
7101 #[test]
7102 fn test_a_bare_coalesce_of_correlated_walks_reaching_many_objects() {
7103 let out = compile_and_emit_with(
7106 "SELECT Post { owners := .<posts[is Person] ?? .<posts[is Person] }",
7107 &make_schema(),
7108 );
7109 assert!(
7110 !out.sql.contains("COALESCE("),
7111 "not a value-by-value choice:\n{}",
7112 out.sql
7113 );
7114 assert!(
7115 out.sql.contains("ARRAY(SELECT \"r\" FROM ("),
7116 "the arms are aggregated:\n{}",
7117 out.sql
7118 );
7119 }
7120
7121 #[test]
7122 fn test_a_union_of_correlated_walks_reaching_many() {
7123 let out = compile_and_emit_with(
7128 "SELECT Post { owners := (.<posts[is Person] UNION .<posts[is Person]) { name } }",
7129 &make_schema(),
7130 );
7131 assert!(
7132 out.sql.contains("ARRAY(SELECT \"r\" FROM ("),
7133 "the arms are aggregated:\n{}",
7134 out.sql
7135 );
7136 }
7137
7138 #[test]
7139 fn test_a_coalesce_of_correlated_walks_reaching_many() {
7140 let out = compile_and_emit_with(
7143 "SELECT Post { owners := (.<posts[is Person] ?? .<posts[is Person]) { name } }",
7144 &make_schema(),
7145 );
7146 assert!(
7147 out.sql.contains("ARRAY(SELECT \"r\" FROM ("),
7148 "the arms are aggregated:\n{}",
7149 out.sql
7150 );
7151 }
7152
7153 #[test]
7154 fn test_a_union_of_correlated_walks() {
7155 let out = compile_and_emit_with(
7159 "SELECT Post { owners := (SELECT (.<posts[is Person] UNION .<posts[is Person]) { name } LIMIT 1) }",
7160 &make_schema(),
7161 );
7162 assert!(out.sql.contains("UNION ALL"), "one arm per operand:\n{}", out.sql);
7163 assert!(
7164 out.sql.contains("AS \"_u\""),
7165 "the arms are read as one set:\n{}",
7166 out.sql
7167 );
7168 }
7169
7170 #[test]
7171 fn test_a_for_loop_whose_body_updates() {
7172 let out = compile_and_emit_with(
7176 "FOR p IN (SELECT Person) UNION (UPDATE Person FILTER .id = p.id SET { name := $n })",
7177 &make_schema(),
7178 );
7179 assert!(out.sql.contains("UPDATE \"public\".\"Person\""), "{}", out.sql);
7180 assert!(
7181 out.sql.contains("FROM \"_for_p\""),
7182 "driven from the iteration:\n{}",
7183 out.sql
7184 );
7185 assert!(
7186 !out.sql.contains("LATERAL"),
7187 "DML cannot sit in a LATERAL:\n{}",
7188 out.sql
7189 );
7190 }
7191
7192 #[test]
7193 fn test_a_for_loop_body_appends_the_row_it_is_iterating() {
7194 let out = compile_and_emit(
7195 "WITH t := (SELECT Post) FOR q IN t UNION (UPDATE Person FILTER .id = $i SET { posts += q })",
7196 );
7197 assert!(
7200 out.sql.contains("\"_iter\"") && out.sql.contains("FROM \"_ids\""),
7201 "junction rows driven from the iteration:\n{}",
7202 out.sql
7203 );
7204 assert!(
7205 !out.sql.contains("LATERAL"),
7206 "DML cannot sit in a LATERAL:\n{}",
7207 out.sql
7208 );
7209 }
7210
7211 #[test]
7212 fn test_select_on_a_for_loop_variable_reads_only_its_row() {
7213 let out = compile_and_emit("FOR p IN (SELECT Person) UNION (SELECT (SELECT p) { name })");
7214 assert!(
7215 out.sql.contains("= \"_for_p\".\"v\""),
7216 "narrowed to the row the variable holds:\n{}",
7217 out.sql
7218 );
7219 }
7220
7221 #[test]
7222 fn test_a_walk_off_a_for_loop_variable_reads_the_binding() {
7223 let out = compile_and_emit("WITH ps := (SELECT Post) FOR q IN ps UNION (SELECT Person FILTER .name = q.title)");
7226 assert!(
7227 out.sql.contains("FROM \"ps\""),
7228 "the walk reads the binding:\n{}",
7229 out.sql
7230 );
7231 }
7232
7233 #[test]
7234 fn test_a_mutating_for_loop_binding_becomes_sibling_ctes() {
7235 let out = compile_and_emit(
7236 "WITH made := (FOR n IN {'a', 'b'} UNION (INSERT Post { title := n })) SELECT count(made)",
7237 );
7238 assert!(
7239 out.sql.contains("\"made__ids\" AS (\nINSERT INTO"),
7240 "the insert is its own top-level CTE:\n{}",
7241 out.sql
7242 );
7243 assert!(
7244 !out.sql.contains("LATERAL"),
7245 "DML cannot sit in a LATERAL:\n{}",
7246 out.sql
7247 );
7248 }
7249
7250 #[test]
7251 fn test_count_of_a_binding_counts_its_rows() {
7252 let out = compile_and_emit("WITH people := (SELECT Person) SELECT count(people)");
7255 assert!(out.sql.contains("(SELECT count(*) FROM \"people\")"), "{}", out.sql);
7256 }
7257
7258 #[test]
7259 fn test_dividing_integers_yields_a_float() {
7260 for (query, expected) in [
7261 ("SELECT 100 / 133", "((100)::float8 / 133)"),
7262 ("SELECT Person { a := .age / 2 }", "::float8 / 2)"),
7263 ("SELECT Person.age / 2", "::float8 / 2)"),
7264 (
7265 "WITH total := max({133}), current := 38 - 1 SELECT 100 / total * current",
7266 "(((100)::float8 / (SELECT \"v\" FROM \"total\")) * (SELECT \"v\" FROM \"current\"))",
7267 ),
7268 (
7269 "WITH p := (SELECT Person LIMIT 1), index := p.age - 1, total := max(Person.age) SELECT 100 / total * index",
7270 "((100)::float8 / (SELECT \"v\" FROM \"total\"))",
7271 ),
7272 ] {
7273 let out = compile_and_emit(query);
7274 assert!(out.sql.contains(expected), "{query}:\n{}", out.sql);
7275 }
7276 }
7277
7278 #[test]
7279 fn test_dividing_anything_but_two_integers_is_left_to_postgres() {
7280 for query in [
7281 "SELECT 100 // 133",
7282 "SELECT 100 / 1.5",
7283 "SELECT <decimal>100 / <decimal>3",
7284 "SELECT 100 / <decimal>3",
7285 ] {
7286 let out = compile_and_emit(query);
7287 assert!(!out.sql.contains("::float8 /"), "{query}:\n{}", out.sql);
7288 }
7289 }
7290
7291 #[test]
7292 fn test_a_free_object_under_a_condition_stays_an_object() {
7293 for query in [
7294 "SELECT { a := 1 } IF true ELSE {}",
7295 "SELECT {} IF true ELSE { a := 1 }",
7296 "SELECT { a := 1 } IF true ELSE {} LIMIT 1",
7297 ] {
7298 let out = compile_and_emit(query);
7299 assert!(
7300 matches!(&out.shape.root, ShapeNode::Object { type_name: None, pointers, .. } if pointers.len() == 1),
7301 "{query}: {:?}\n{}",
7302 out.shape.root,
7303 out.sql
7304 );
7305 }
7306 }
7307
7308 #[test]
7309 fn test_exists_of_a_binding_asks_for_any_row() {
7310 let out = compile_and_emit("WITH people := (SELECT Person) SELECT exists people");
7311 assert!(out.sql.contains("EXISTS(SELECT 1 FROM \"people\")"), "{}", out.sql);
7312
7313 let out = compile_and_emit("WITH names := {'a', 'b'} SELECT exists names");
7314 assert!(
7315 out.sql
7316 .contains("EXISTS(SELECT 1 FROM \"names\" WHERE \"v\" IS NOT NULL)"),
7317 "{}",
7318 out.sql
7319 );
7320 }
7321
7322 #[test]
7323 fn test_a_conditional_insert_beside_a_read_branch_unions_both() {
7324 let out = compile_and_emit(
7325 "WITH existing := (SELECT Person FILTER .name = $n LIMIT 1) \
7326 SELECT (existing IF EXISTS existing ELSE (INSERT Person { name := $n })) { name }",
7327 );
7328 assert!(out.sql.contains("UNION ALL"), "{}", out.sql);
7329 assert!(
7330 out.sql.contains("INSERT INTO") && out.sql.contains("WHERE (NOT EXISTS(SELECT 1 FROM \"existing\"))"),
7331 "the insert carries the condition itself:\n{}",
7332 out.sql
7333 );
7334 }
7335
7336 fn make_schema_with_a_plain_abstract() -> SchemaDescriptor {
7341 let mut schema = make_schema();
7342 let mut archivable = schema
7343 .types
7344 .iter()
7345 .find(|t| t.name == "Person")
7346 .expect("Person is in the test schema")
7347 .clone();
7348 archivable.name = "Archivable".into();
7349 archivable.table = "Archivable".into();
7350 archivable.abstract_ = true;
7351 archivable.materialized = false;
7352 archivable.links = vec![];
7353 archivable.multilinks = vec![];
7354 schema.types.push(archivable);
7355 let person = schema
7356 .types
7357 .iter_mut()
7358 .find(|t| t.name == "Person")
7359 .expect("just found");
7360 person.parents.push("default::Archivable".into());
7361 schema
7362 }
7363
7364 #[test]
7365 fn test_an_intersection_with_a_plain_abstract_fans_out() {
7366 let schema = make_schema_with_a_plain_abstract();
7367 let out = compile_and_emit_with("SELECT Post { who := [is default::Archivable].name }", &schema);
7368 assert!(
7369 !out.sql.contains("\"Archivable\""),
7370 "a plain abstract backs no relation:\n{}",
7371 out.sql
7372 );
7373 assert!(
7374 out.sql.contains("\"public\".\"Person\""),
7375 "read from the type that carries the column:\n{}",
7376 out.sql
7377 );
7378 }
7379
7380 fn make_schema_with_a_linking_plain_abstract() -> SchemaDescriptor {
7384 let mut schema = make_schema_with_a_plain_abstract();
7385 let person_links = schema
7386 .types
7387 .iter()
7388 .find(|t| t.name == "Person")
7389 .expect("Person is in the test schema")
7390 .links
7391 .clone();
7392 let archivable = schema
7393 .types
7394 .iter_mut()
7395 .find(|t| t.name == "Archivable")
7396 .expect("just added");
7397 archivable.links = person_links;
7398 schema
7399 }
7400
7401 #[test]
7402 fn test_a_plain_abstract_in_a_subquery_fans_out() {
7403 let schema = make_schema_with_a_plain_abstract();
7404 let out = compile_and_emit_with(
7405 "SELECT Person FILTER .id IN (SELECT default::Archivable FILTER .age > 1).id",
7406 &schema,
7407 );
7408 assert!(
7409 !out.sql.contains("\"Archivable\""),
7410 "a plain abstract backs no relation:\n{}",
7411 out.sql
7412 );
7413 }
7414
7415 #[test]
7416 fn test_a_backlink_narrowed_to_a_plain_abstract_fans_out() {
7417 let schema = make_schema_with_a_linking_plain_abstract();
7418 let out = compile_and_emit_with(
7419 "SELECT Company FILTER .<company[is default::Archivable].name = 'a'",
7420 &schema,
7421 );
7422 assert!(
7423 !out.sql.contains("\"Archivable\""),
7424 "a plain abstract backs no relation:\n{}",
7425 out.sql
7426 );
7427 }
7428
7429 fn make_interface_schema_with_a_plain_abstract() -> SchemaDescriptor {
7433 let mut schema = make_interface_schema();
7434 let mut archivable = schema
7435 .types
7436 .iter()
7437 .find(|t| t.name == "Individual")
7438 .expect("Individual is in the interface schema")
7439 .clone();
7440 archivable.name = "Archivable".into();
7441 archivable.table = "Archivable".into();
7442 archivable.abstract_ = true;
7443 archivable.materialized = false;
7444 archivable.interfaces = vec![];
7445 archivable.links = vec![];
7446 archivable.multilinks = vec![];
7447 archivable.computed = vec![];
7448 schema.types.push(archivable);
7449 for t in schema.types.iter_mut() {
7450 match t.name.as_str() {
7451 "Account" => t.materialized = true,
7453 "Individual" => t.parents.push("default::Archivable".into()),
7454 _ => {}
7455 }
7456 }
7457 schema
7458 }
7459
7460 #[test]
7461 fn test_a_splat_over_a_plain_abstract_fans_out() {
7462 let schema = make_interface_schema_with_a_plain_abstract();
7463 let out = compile_and_emit_with("SELECT Account { id, [is default::Archivable].* }", &schema);
7464 assert!(
7465 !out.sql.contains("\"Archivable\""),
7466 "a plain abstract backs no relation:\n{}",
7467 out.sql
7468 );
7469 assert!(
7470 out.sql.contains("first_name"),
7471 "the splat still reaches the mixin's own pointers:\n{}",
7472 out.sql
7473 );
7474 }
7475
7476 #[test]
7477 fn test_an_aggregate_over_a_relative_walk_runs_inside_the_subquery() {
7478 let out = compile_and_emit("SELECT Person { latest := max(.posts.title) }");
7481 assert!(out.sql.contains("(SELECT max("), "{}", out.sql);
7482 assert!(!out.sql.contains("max(ARRAY("), "{}", out.sql);
7483 }
7484
7485 fn make_schema_with_a_computed_backlink() -> SchemaDescriptor {
7488 use crate::schema::ComputedDescriptor;
7489 let mut schema = make_schema();
7490 let company = schema
7491 .types
7492 .iter_mut()
7493 .find(|t| t.name == "Company")
7494 .expect("Company is in the test schema");
7495 company.computed = vec![ComputedDescriptor {
7496 name: "staff".into(),
7497 expression: ".<company[is default::Person]".into(),
7498 return_type: None,
7499 link_target: None,
7500 link_multi: false,
7501 }];
7502 schema
7503 }
7504
7505 #[test]
7506 fn test_a_computed_backlink_reads_the_objects_it_lands_on() {
7507 let schema = make_schema_with_a_computed_backlink();
7508 let out = compile_and_emit_with("SELECT Company { staff: { name } }", &schema);
7509 assert!(
7510 out.sql.contains("\"public\".\"Person\""),
7511 "the walk reaches Person:\n{}",
7512 out.sql
7513 );
7514 }
7515
7516 #[test]
7517 fn test_a_computed_backlinks_modifiers_belong_to_what_it_lands_on() {
7518 let schema = make_schema_with_a_computed_backlink();
7521 let out = compile_and_emit_with("SELECT Company.staff { name } FILTER .name = 'ada'", &schema);
7522 assert!(out.sql.contains("'ada'"), "{}", out.sql);
7523 assert!(
7524 out.sql.contains("\"public\".\"Person\""),
7525 "filtered on the Person the walk lands on:\n{}",
7526 out.sql
7527 );
7528 }
7529
7530 #[test]
7531 fn test_an_unreserved_keyword_names_a_free_shape_field() {
7532 let out = compile_and_emit("SELECT { last := 2, first := 1, order := 3 }");
7535 assert!(out.sql.contains("\"last\""), "{}", out.sql);
7536 assert!(out.sql.contains("\"first\""), "{}", out.sql);
7537 assert!(out.sql.contains("\"order\""), "{}", out.sql);
7538 }
7539
7540 #[test]
7541 fn test_a_shaped_sub_select_over_a_type_is_an_object() {
7542 let out = compile_and_emit("SELECT Person { latest := (SELECT Post { title } LIMIT 1) }");
7545 assert!(out.sql.contains("\"title\""), "{}", out.sql);
7546 }
7547
7548 #[test]
7549 fn test_a_walk_inside_a_path_selects_shape_finds_its_row() {
7550 let out = compile_and_emit("SELECT Person.posts { mine := (SELECT Post FILTER .title = .title LIMIT 1) }");
7553 assert!(out.sql.contains("\"public\".\"Post\""), "{}", out.sql);
7554 }
7555
7556 #[test]
7557 fn test_a_walk_off_a_binding_in_a_free_select() {
7558 let out = compile_and_emit("WITH p := (SELECT Person) SELECT { n := p.company.name }");
7561 assert!(out.sql.contains("\"p\""), "{}", out.sql);
7562 assert!(out.sql.contains("\"public\".\"Company\""), "{}", out.sql);
7563 }
7564
7565 #[test]
7566 fn test_a_free_selects_filter_can_root_at_a_binding() {
7567 let out = compile_and_emit("WITH p := (SELECT Person) SELECT { n := 1 } FILTER p.company.name = 'Acme'");
7568 assert!(out.sql.contains("'Acme'"), "{}", out.sql);
7569 }
7570
7571 #[test]
7572 fn test_a_trailing_shape_on_a_field_access_select() {
7573 let out = compile_and_emit("SELECT (SELECT Person).company { name }");
7574 assert!(out.sql.contains("\"name\""), "{}", out.sql);
7575 assert!(out.sql.contains("\"public\".\"Company\""), "{}", out.sql);
7576 }
7577
7578 #[test]
7579 fn test_a_union_of_walks_names_each_branch() {
7580 let out = compile_and_emit(
7583 "WITH p := (SELECT Person FILTER .name = $a), q := (SELECT Person FILTER .name = $b), \
7584 t := (SELECT p.posts UNION q.posts) SELECT t { title }",
7585 );
7586 assert!(out.sql.contains("UNION ALL"), "{}", out.sql);
7587 assert!(out.sql.contains("\"title\""), "{}", out.sql);
7588 }
7589
7590 #[test]
7591 fn test_sibling_for_loops_each_get_their_own_iterator() {
7592 let out = compile_and_emit(
7595 "WITH a := (SELECT Post), b := (SELECT Post), \
7596 x := (FOR p IN a UNION (INSERT Person { name := p.title, age := 1 })), \
7597 y := (FOR p IN b UNION (INSERT Person { name := p.title, age := 2 })) \
7598 SELECT x",
7599 );
7600 assert!(out.sql.contains("\"_for_p\" AS"), "{}", out.sql);
7601 assert!(out.sql.contains("\"_for_p__1\" AS"), "{}", out.sql);
7602 }
7603
7604 #[test]
7605 fn test_a_nested_for_carries_the_outer_loops_key() {
7606 let out = compile_and_emit(
7609 "WITH made := (FOR p IN (SELECT Person) UNION ( \
7610 FOR q IN p.posts UNION (INSERT Company { name := q.title }) \
7611 )) SELECT count(made)",
7612 );
7613 assert!(
7614 out.sql.contains("\"_outer\""),
7615 "the inner iterator carries the bond:\n{}",
7616 out.sql
7617 );
7618 assert!(
7619 out.sql.contains("JOIN \"_for_p\" ON"),
7620 "the insert reads both loop variables:\n{}",
7621 out.sql
7622 );
7623 assert!(
7624 !out.sql.contains("LATERAL (\nINSERT"),
7625 "DML cannot sit in a LATERAL:\n{}",
7626 out.sql
7627 );
7628 }
7629
7630 #[test]
7631 fn test_a_link_value_can_be_a_walk_off_a_binding() {
7632 let mut schema = make_schema();
7635 let company = schema
7636 .types
7637 .iter_mut()
7638 .find(|t| t.name == "Company")
7639 .expect("make_schema declares Company");
7640 company.properties.push(PropertyDescriptor {
7641 name: "id".into(),
7642 pg_type: "uuid".into(),
7643 nullable: false,
7644 default_sql: None,
7645 default_pyql: None,
7646 description: None,
7647 check_constraints: vec![],
7648 is_exclusive: true,
7649 is_pk: true,
7650 is_readonly: true,
7651 rewrites: vec![],
7652 tuple_members: None,
7653 column_type: None,
7654 });
7655 let out = compile_and_emit_with(
7656 "WITH others := (SELECT Person) \
7657 SELECT (INSERT Person { name := $n, age := 1, company := (SELECT others.company LIMIT 1) })",
7658 &schema,
7659 );
7660 assert!(out.sql.contains("\"company_id\""), "{}", out.sql);
7661 }
7662
7663 #[test]
7664 fn test_a_shape_over_a_coalesce_of_walks() {
7665 let schema = make_schema_with_computed_links();
7668 let out = compile_and_emit_with("SELECT Person { x := (.published ?? .plain) { title } }", &schema);
7669 assert!(out.sql.contains("UNION ALL"), "{}", out.sql);
7670 assert!(
7671 out.sql.contains("NOT EXISTS"),
7672 "the later branch only stands in when the first is empty:\n{}",
7673 out.sql
7674 );
7675 }
7676
7677 #[test]
7678 fn test_an_aggregate_over_a_walk_onto_objects_counts_its_rows() {
7679 let out = compile_and_emit("WITH c := (SELECT Company) SELECT c { n := count(.<company[is default::Person]) }");
7683 assert!(out.sql.contains("count("), "{}", out.sql);
7684 assert!(out.sql.contains("\"public\".\"Person\""), "{}", out.sql);
7685 }
7686
7687 #[test]
7688 fn an_aggregate_over_a_computed_set_counts_inside_a_subquery() {
7689 let schema = make_schema_with_computed_links();
7695 let out = compile_and_emit_with("SELECT Person { n := count(.plain) }", &schema);
7696 assert!(
7697 !out.sql.contains("count((SELECT"),
7698 "the aggregate belongs inside the walk's subquery, not around it:\n{}",
7699 out.sql
7700 );
7701 assert!(
7702 out.sql.contains("(SELECT count(") && out.sql.contains("\"public\".\"Post\""),
7703 "it counts the rows the computed's own path lands on:\n{}",
7704 out.sql
7705 );
7706 }
7707
7708 #[test]
7711 fn an_aggregate_over_a_stored_multi_link_still_counts_its_junction_rows() {
7712 let schema = make_schema_with_computed_links();
7713 let out = compile_and_emit_with("SELECT Person { n := count(.posts) }", &schema);
7714 assert!(out.sql.contains("\"public\".\"Person.posts\""), "{}", out.sql);
7715 assert!(
7716 !out.sql.contains("JOIN \"public\".\"Post\""),
7717 "counting junction rows needs no join onto the targets:\n{}",
7718 out.sql
7719 );
7720 }
7721
7722 #[test]
7723 fn test_a_select_can_name_its_own_result() {
7724 let out = compile_and_emit("SELECT oldest := max(Person.age)");
7727 assert!(out.sql.contains("max("), "{}", out.sql);
7728 let filtered = compile_and_emit("SELECT n := Person.age FILTER n > 18");
7729 assert!(
7730 filtered.sql.contains("18"),
7731 "the alias resolves to the result:\n{}",
7732 filtered.sql
7733 );
7734 }
7735
7736 #[test]
7737 fn test_exists_sees_a_computed_pointer() {
7738 let schema = make_schema_with_computed_links();
7742 let out = compile_and_emit_with("SELECT Person FILTER EXISTS .published", &schema);
7743 assert!(out.sql.contains("EXISTS"), "{}", out.sql);
7744 assert!(
7745 out.sql.contains("\"public\".\"Post\""),
7746 "the computed's own path:\n{}",
7747 out.sql
7748 );
7749 }
7750
7751 #[test]
7752 fn test_walking_through_a_computed_field_access() {
7753 use crate::schema::ComputedDescriptor;
7754 let mut schema = make_schema_with_computed_links();
7757 let person = schema.types.iter_mut().find(|t| t.name == "Person").expect("Person");
7758 person.computed.push(ComputedDescriptor {
7759 name: "latest_title".into(),
7760 expression: "(select .posts order by .title desc limit 1).title".into(),
7761 return_type: None,
7762 link_target: None,
7763 link_multi: false,
7764 });
7765 let out = compile_and_emit_with("SELECT Person { x := .latest_title }", &schema);
7766 assert!(out.sql.contains("\"title\""), "{}", out.sql);
7767 assert!(
7768 out.sql.contains("LIMIT"),
7769 "the computed's own limit survives:\n{}",
7770 out.sql
7771 );
7772 }
7773
7774 #[test]
7775 fn test_a_computed_field_access_onto_an_object_takes_a_shape() {
7776 use crate::schema::ComputedDescriptor;
7777 let mut schema = make_schema_with_computed_links();
7780 let post = schema.types.iter_mut().find(|t| t.name == "Post").expect("Post");
7781 post.links.push(LinkDescriptor {
7782 name: "author".into(),
7783 target: "default::Person".into(),
7784 nullable: true,
7785 through: None,
7786 description: None,
7787 default_pyql: None,
7788 is_exclusive: false,
7789 is_readonly: false,
7790 rewrites: vec![],
7791 on_delete: vec![],
7792 });
7793 let person = schema.types.iter_mut().find(|t| t.name == "Person").expect("Person");
7794 person.computed.retain(|c| c.name != "looper");
7795 person.computed.push(ComputedDescriptor {
7796 name: "latest_author".into(),
7797 expression: "((select .posts order by .title desc limit 1)).author".into(),
7798 return_type: None,
7799 link_target: None,
7800 link_multi: false,
7801 });
7802 let out = compile_and_emit_with("SELECT Person { latest_author: { name } }", &schema);
7803 assert!(out.sql.contains("\"name\""), "{}", out.sql);
7804 assert!(
7805 out.sql.contains("\"title\" DESC"),
7806 "the order stays on the posts:\n{}",
7807 out.sql
7808 );
7809 assert!(
7810 out.sql.contains("LIMIT"),
7811 "the computed's own limit survives:\n{}",
7812 out.sql
7813 );
7814 let splat = compile_and_emit_with("SELECT Person { * }", &schema);
7815 assert!(
7816 !splat.sql.contains("latest_author"),
7817 "`*` leaves links out:\n{}",
7818 splat.sql
7819 );
7820 }
7821
7822 #[test]
7823 fn test_a_narrowed_pointer_can_carry_a_shape() {
7824 let out = compile_and_emit("SELECT Company { x := 1, [is default::Person].posts: { title } }");
7828 assert!(out.sql.contains("\"title\""), "{}", out.sql);
7829 assert!(out.sql.contains("\"public\".\"Post\""), "{}", out.sql);
7830 }
7831
7832 #[test]
7833 fn test_select_type_name_as_a_path_step() {
7834 let out = compile_and_emit("SELECT Person.__type__");
7835 assert!(out.sql.contains("ROW('default::Person')"), "{}", out.sql);
7836 assert!(out.sql.contains("\"Person\" AS \"t0\""), "{}", out.sql);
7837 }
7838
7839 #[test]
7840 fn test_aggregate_over_a_with_bound_object_set() {
7841 let out = compile_and_emit("WITH people := (SELECT Person) SELECT max(people.age)");
7842 assert!(out.sql.contains("\"people\" AS ("));
7843 assert!(out.sql.contains("max("));
7844 assert!(!out.sql.contains("FROM \"default\".\"Person\" AS \"t1\""));
7846 }
7847
7848 #[test]
7849 fn test_select_union_of_two_object_bindings() {
7850 let out = compile_and_emit("WITH a := (SELECT Person LIMIT 1), b := (SELECT Person) SELECT (a UNION b)");
7851 assert!(out.sql.contains("SELECT * FROM \"a\""));
7852 assert!(out.sql.contains("UNION ALL"));
7853 assert!(out.sql.contains("SELECT * FROM \"b\""));
7854 }
7855
7856 #[test]
7857 fn test_select_union_of_different_object_types_is_rejected() {
7858 let schema = make_schema();
7859 let ast = parse::parse("WITH a := (SELECT Person), b := (SELECT Company) SELECT (a UNION b)").unwrap();
7860 let Err(err) = ir::compile(&ast, &schema) else {
7861 panic!("union of unrelated types must be rejected")
7862 };
7863 assert!(format!("{err}").contains("UNION"));
7864 }
7865
7866 #[test]
7867 fn test_for_union_as_a_function_body() {
7868 let schema = make_schema();
7869 let descriptor = FunctionDescriptor {
7870 name: "recent".into(),
7871 module: "default".into(),
7872 params: vec![],
7873 return_pg_type: "default::Person".into(),
7874 return_is_object: true,
7875 return_is_set: true,
7876 return_is_polymorphic: false,
7877 volatility: "stable".into(),
7878 body: "for n in {1, 2} union (select Person filter .age = n)".into(),
7879 };
7880 let ir = ir::compile_fn_body(&descriptor, &schema).expect("function body must compile");
7881 let body = emit_fn_body(&ir);
7882 assert!(body.contains("VALUES"), "{body}");
7883 assert!(body.contains("CROSS JOIN LATERAL"), "{body}");
7884 assert!(body.contains("\"_body\".*"), "{body}");
7885 }
7886
7887 #[test]
7888 fn test_with_binding_in_a_computed_reads_the_enclosing_object() {
7889 let out = compile_and_emit("SELECT Person { n := (WITH own := .name SELECT own) }");
7890 assert!(out.sql.contains("\"name\""), "{}", out.sql);
7891 }
7892
7893 #[test]
7894 fn test_subject_path_update_names_its_own_alias() {
7895 let mut schema = make_schema();
7896 let company = schema.types.iter_mut().find(|t| t.name == "Company").unwrap();
7897 company.properties.insert(
7898 0,
7899 PropertyDescriptor {
7900 name: "id".into(),
7901 pg_type: "uuid".into(),
7902 nullable: false,
7903 default_sql: Some("uuidv7()".into()),
7904 default_pyql: None,
7905 description: None,
7906 check_constraints: vec![],
7907 is_exclusive: true,
7908 is_pk: true,
7909 is_readonly: true,
7910 rewrites: vec![],
7911 tuple_members: None,
7912 column_type: None,
7913 },
7914 );
7915 let out = compile_and_emit_with("SELECT (UPDATE Person.company SET { name := 'x' }) { name }", &schema);
7919 assert!(out.sql.contains("\"t1\".\"id\" = ANY("), "{}", out.sql);
7920 }
7921
7922 #[test]
7923 fn test_an_update_past_a_from_returns_only_its_own_columns() {
7924 let out = compile_and_emit(
7928 "SELECT { (UPDATE Person FILTER .name = 'a' SET { company := (INSERT Company { name := 'c' }) }) }",
7929 );
7930 assert!(out.sql.contains("FROM \"_nested_dml_0\""), "{}", out.sql);
7931 assert!(
7932 out.sql.contains("RETURNING \"t0\".*"),
7933 "the update names its own target:\n{}",
7934 out.sql
7935 );
7936 }
7937
7938 #[test]
7939 fn test_nested_insert_in_a_mutation_written_as_a_free_set() {
7940 let out = compile_and_emit(
7941 "SELECT { (UPDATE Person FILTER .name = 'a' SET { company := (INSERT Company { name := 'c' }) }) }",
7942 );
7943 assert!(
7944 out.sql.contains("INSERT INTO \"public\".\"Company\""),
7945 "the hoisted insert is defined, not just referenced:\n{}",
7946 out.sql
7947 );
7948 }
7949
7950 #[test]
7951 fn test_nested_insert_as_a_link_value_in_an_update() {
7952 let out = compile_and_emit(
7956 "SELECT (UPDATE Person FILTER .name = 'a' SET { company := (INSERT Company { name := 'c' }) }) { name }",
7957 );
7958 assert!(out.sql.contains("INSERT INTO \"public\".\"Company\""), "{}", out.sql);
7959 assert!(out.sql.contains("_nested_dml_0"), "{}", out.sql);
7960 assert!(
7961 out.sql.contains("FROM \"_nested_dml_0\""),
7962 "the update reads the hoisted CTE:\n{}",
7963 out.sql
7964 );
7965 }
7966
7967 #[test]
7968 fn test_nested_insert_as_a_link_value() {
7969 let out = compile_and_emit(
7970 "SELECT (INSERT Person { name := 'a', company := (INSERT Company { name := 'c' }) }) { name }",
7971 );
7972 assert!(out.sql.contains("_nested_dml_0"), "{}", out.sql);
7973 assert!(out.sql.contains("INSERT INTO \"public\".\"Company\""), "{}", out.sql);
7974 }
7975
7976 #[test]
7977 fn test_nested_insert_in_a_one_element_set_is_that_insert() {
7978 let out = compile_and_emit(
7979 "SELECT (INSERT Person { name := 'a', company := { (INSERT Company { name := 'c' }) } }) { name }",
7980 );
7981 assert!(out.sql.contains("INSERT INTO \"public\".\"Company\""), "{}", out.sql);
7982 }
7983
7984 #[test]
7985 fn test_insert_with_no_assignments_uses_default_values() {
7986 let out = compile_and_emit("SELECT (INSERT Person { name := 'a', company := (INSERT Company {}) }) { name }");
7987 assert!(out.sql.contains("DEFAULT VALUES"), "{}", out.sql);
7988 }
7989
7990 #[test]
7991 fn test_backlink_narrowed_to_an_interface_its_implementor_declares() {
7992 use crate::schema::{LinkDescriptor, TypeDescriptor};
7995 let mut schema = make_interface_schema();
7996 let individual = schema.types.iter().find(|t| t.name == "Individual").unwrap().clone();
7997 schema.types.push(TypeDescriptor {
7998 name: "Note".into(),
7999 module: "default".into(),
8000 table: "Note".into(),
8001 abstract_: false,
8002 materialized: true,
8003 description: None,
8004 parents: vec![],
8005 interfaces: vec![],
8006 bases: vec![],
8007 properties: individual.properties[..1].to_vec(),
8008 links: vec![],
8009 multilinks: vec![],
8010 computed: vec![],
8011 constraints: vec![],
8012 indexes: vec![],
8013 partition: None,
8014 vector_indexes: vec![],
8015 search_indexes: vec![],
8016 triggers: vec![],
8017 junction: false,
8018 signals: vec![],
8019 });
8020 schema
8021 .types
8022 .iter_mut()
8023 .find(|t| t.name == "Individual")
8024 .unwrap()
8025 .links
8026 .push(LinkDescriptor {
8027 name: "note".into(),
8028 target: "default::Note".into(),
8029 nullable: true,
8030 description: None,
8031 default_pyql: None,
8032 is_exclusive: false,
8033 is_readonly: false,
8034 rewrites: vec![],
8035 on_delete: vec![],
8036 through: None,
8037 });
8038 let out = compile_and_emit_with("SELECT Note FILTER EXISTS .<note[is Account]", &schema);
8039 assert!(out.sql.contains("\"Individual\""), "{}", out.sql);
8040 }
8041
8042 #[test]
8043 fn test_subject_reads_the_row_a_constraint_checks() {
8044 let schema = make_schema();
8045 let sql = ir::compile_constraint_expr("__subject__.age > 18", "default::Person", &schema)
8046 .expect("__subject__ names the row under check");
8047 assert!(sql.contains("\"age\""), "{sql}");
8048 }
8049
8050 #[test]
8051 fn test_free_select_filter_gates_the_result_and_warns() {
8052 let schema = make_schema();
8053 let ast = parse::parse("SELECT count(Person) FILTER (Person.age > 18)").unwrap();
8054 let ir = ir::compile(&ast, &schema).unwrap();
8055 let out = emit(&ir);
8056 assert!(
8057 out.sql.contains("EXISTS("),
8058 "the condition gates the count:\n{}",
8059 out.sql
8060 );
8061 assert!(
8062 ir.warnings.iter().any(|w| w.contains("FILTER clause")),
8063 "a set-valued filter warns: {:?}",
8064 ir.warnings
8065 );
8066 }
8067
8068 #[test]
8069 fn test_free_sub_select_with_modifiers_reads_inline() {
8070 let out = compile_and_emit(
8073 "SELECT Person { n := (WITH mine := .name SELECT count(Post) FILTER (Post.title = mine)) }",
8074 );
8075 assert!(out.sql.contains("_scalar_sub"), "{}", out.sql);
8076 assert!(!out.sql.contains("WITH"), "nothing is hoisted:\n{}", out.sql);
8077 }
8078
8079 #[test]
8080 fn test_correlated_with_binding_is_not_hoisted() {
8081 let out = compile_and_emit("SELECT Person { n := (WITH own := .name SELECT own) }");
8082 assert!(
8083 !out.sql.contains("\"own\" AS ("),
8084 "the binding is inlined:\n{}",
8085 out.sql
8086 );
8087 assert!(out.sql.contains("\"name\""), "{}", out.sql);
8088 }
8089
8090 #[test]
8091 fn test_array_literal_binding_is_readable_and_typed() {
8092 let out = compile_and_emit("WITH order := ['a', 'b'] SELECT std::find(order, 'b')");
8093 assert!(out.sql.contains("array_position"), "its type is known:\n{}", out.sql);
8094 assert!(
8095 out.sql.contains("AS v"),
8096 "the binding exposes a value column:\n{}",
8097 out.sql
8098 );
8099 }
8100
8101 #[test]
8102 fn test_any_over_a_single_boolean_is_that_boolean() {
8103 let out = compile_and_emit("SELECT Person FILTER std::any(.age > 18)");
8104 assert!(!out.sql.contains("bool_or"), "no aggregate in WHERE:\n{}", out.sql);
8105 assert!(out.sql.contains("> 18"), "{}", out.sql);
8106 }
8107
8108 #[test]
8109 fn test_comparing_a_value_to_a_set_is_membership() {
8110 let schema = make_schema_with_computed_links();
8111 let out = compile_and_emit_with("SELECT Person FILTER .name = Person.posts.title", &schema);
8112 assert!(out.sql.contains("= ANY("), "{}", out.sql);
8113 }
8114
8115 #[test]
8116 fn test_for_over_a_with_binding_iterates_every_row() {
8117 let out = compile_and_emit(
8118 "WITH names := (SELECT Person.name) FOR n IN names UNION (SELECT Person FILTER .name = n)",
8119 );
8120 assert!(out.sql.contains("FROM \"names\""), "{}", out.sql);
8121 assert!(!out.sql.contains("VALUES"), "the whole set is iterated:\n{}", out.sql);
8122 }
8123
8124 #[test]
8125 fn test_for_body_bindings_stay_inside_the_body() {
8126 let out = compile_and_emit(
8129 "WITH names := (SELECT Person.name) \
8130 FOR n IN names UNION (WITH matching := (SELECT Person FILTER .name = n) SELECT matching)",
8131 );
8132 let lateral = out.sql.find("CROSS JOIN LATERAL").expect("a lateral body");
8133 let binding = out.sql.find("\"matching\" AS (").expect("the body's binding");
8134 assert!(binding > lateral, "the binding sits inside the body:\n{}", out.sql);
8135 }
8136
8137 #[test]
8138 fn test_array_valued_binding_picks_the_array_overload() {
8139 let out = compile_and_emit("WITH l := <array<std::str>>$0 SELECT std::find(l, 'a')");
8142 assert!(out.sql.contains("array_position"), "{}", out.sql);
8143 assert!(!out.sql.contains("strpos"), "{}", out.sql);
8144 }
8145
8146 #[test]
8147 fn test_aggregate_over_a_path_in_a_filter_is_a_subquery() {
8148 let out = compile_and_emit("SELECT Person FILTER .age = std::max(Person.age)");
8149 assert!(
8150 out.sql.contains("(SELECT max("),
8151 "an aggregate in WHERE needs its own query:\n{}",
8152 out.sql
8153 );
8154 }
8155
8156 #[test]
8157 fn test_distinct_over_a_single_value_is_that_value() {
8158 let out = compile_and_emit("SELECT Person { n := (DISTINCT .name) }");
8159 assert!(out.sql.contains("\"name\""), "{}", out.sql);
8160 assert!(!out.sql.contains("DISTINCT \""), "{}", out.sql);
8161 }
8162
8163 #[test]
8164 fn test_narrowing_an_interface_joins_the_implementor_table() {
8165 let mut schema = make_interface_schema();
8168 let account = schema.types.iter_mut().find(|t| t.name == "Account").unwrap();
8169 account.materialized = true;
8170 let out = compile_and_emit_with("SELECT Account[is Individual].first_name", &schema);
8171 assert!(
8172 out.sql.contains("JOIN \"public\".\"Individual\""),
8173 "the implementor's own table is joined:\n{}",
8174 out.sql
8175 );
8176 assert!(out.sql.contains("\"first_name\""), "{}", out.sql);
8177 }
8178
8179 #[test]
8180 fn test_sub_select_shape_declares_names_its_filter_and_projection_read() {
8181 let out = compile_and_emit("SELECT Person { n := (SELECT .posts { t := .title } FILTER .t = 'x' LIMIT 1).t }");
8182 assert!(out.sql.contains("'x'"), "{}", out.sql);
8183 assert!(out.sql.contains("\"title\""), "{}", out.sql);
8184 }
8185
8186 #[test]
8187 fn test_assert_over_an_object_set_returns_rows() {
8188 let out = compile_and_emit("SELECT std::assert_distinct((SELECT Person))");
8189 assert!(
8190 out.sql.contains("\"assert_distinct\"(ARRAY(SELECT \"id\""),
8191 "{}",
8192 out.sql
8193 );
8194 assert!(out.sql.contains("= ANY("), "{}", out.sql);
8195 assert!(out.sql.contains("\"Person\" AS \"t1\""), "{}", out.sql);
8196 assert!(!out.sql.contains("unnest("), "{}", out.sql);
8197 }
8198
8199 #[test]
8200 fn test_assert_over_a_for_union_reads_its_rows() {
8201 let out =
8202 compile_and_emit("SELECT std::assert_distinct((FOR n IN {1, 2} UNION (SELECT Person FILTER .age = n)))");
8203 assert!(out.sql.contains("CROSS JOIN LATERAL"), "{}", out.sql);
8204 assert!(out.sql.contains("= ANY("), "{}", out.sql);
8205 }
8206
8207 #[test]
8208 fn test_declared_computed_with_binding_reads_its_own_object() {
8209 use crate::schema::ComputedDescriptor;
8212 let schema = make_schema();
8213 let cd = ComputedDescriptor {
8214 name: "shouted".into(),
8215 expression: "(with own := .name select std::str_upper(own))".into(),
8216 return_type: None,
8217 link_target: None,
8218 link_multi: false,
8219 };
8220 let ir = ir::compile_computed_in_type(&cd, "default::Person", &schema)
8221 .expect("a computed may read the object it is declared on");
8222 assert!(ir.is_some());
8223 }
8224
8225 #[test]
8226 fn test_sub_select_modifiers_scope_to_its_own_subject() {
8227 let mut schema = make_schema();
8230 let post = schema
8231 .types
8232 .iter_mut()
8233 .find(|t| t.name == "Post")
8234 .expect("Post is in the test schema");
8235 post.links.push(LinkDescriptor {
8236 name: "author".into(),
8237 target: "default::Person".into(),
8238 nullable: true,
8239 description: None,
8240 default_pyql: None,
8241 is_exclusive: false,
8242 is_readonly: false,
8243 rewrites: vec![],
8244 on_delete: vec![],
8245 through: None,
8246 });
8247 let out = compile_and_emit_with(
8248 "SELECT Person { a := (SELECT .posts FILTER .title = 'x' LIMIT 1).author.name }",
8249 &schema,
8250 );
8251 assert!(out.sql.contains("'x'"), "{}", out.sql);
8252 }
8253
8254 #[test]
8255 fn test_free_select_set_literal() {
8256 let schema = make_schema();
8257 let ast = parse::parse("SELECT {1, 2, 3}").unwrap();
8258 let ir = ir::compile(&ast, &schema).unwrap();
8259 let out = emit(&ir);
8260 assert_eq!(out.sql.matches("UNION ALL").count(), 2);
8262 assert!(out.sql.contains("1 AS v"));
8263 assert!(out.sql.contains("2 AS v"));
8264 assert!(out.sql.contains("3 AS v"));
8265 assert!(out.sql.contains("ROW(v) AS result"));
8266 assert!(matches!(out.shape.root, crate::query::ShapeNode::Scalar { .. }));
8267 }
8268
8269 #[test]
8270 fn test_free_select_free_object() {
8271 let schema = make_schema();
8272 let ast = parse::parse("SELECT { foo := 'bar', n := 42 }").unwrap();
8273 let ir = ir::compile(&ast, &schema).unwrap();
8274 let out = emit(&ir);
8275 assert!(out.sql.contains("'bar'"));
8276 assert!(out.sql.contains("42"));
8277 assert!(out.sql.contains("AS result"));
8278 let crate::query::ShapeNode::Object {
8280 pointers, type_name, ..
8281 } = &out.shape.root
8282 else {
8283 panic!("expected Object shape")
8284 };
8285 assert!(type_name.is_none());
8286 assert_eq!(pointers.len(), 2);
8287 assert!(matches!(&pointers[0], crate::query::ShapeNode::Scalar { name, position: 0 } if name == "foo"));
8288 assert!(matches!(&pointers[1], crate::query::ShapeNode::Scalar { name, position: 1 } if name == "n"));
8289 }
8290
8291 #[test]
8292 fn test_free_select_object_with_enum_field_casts_to_text_and_tags_shape() {
8293 let mut schema = make_schema();
8301 schema.enums.push(crate::schema::EnumDescriptor {
8302 name: "Gender".into(),
8303 module: "default".into(),
8304 members: vec!["Male".into(), "Female".into()],
8305 });
8306 let out = compile_and_emit_with("select { gender := default::Gender.Male }", &schema);
8307 assert!(
8314 out.sql.contains("'Male'::\"public\".\"Gender\""),
8315 "expected the enum literal, got:\n{}",
8316 out.sql
8317 );
8318 assert!(
8319 out.sql.contains("ROW(\"_f0\"::text) AS result"),
8320 "expected the ROW composite to cast the enum field to text, got:\n{}",
8321 out.sql
8322 );
8323 let crate::query::ShapeNode::Object { pointers, .. } = &out.shape.root else {
8324 panic!("expected Object shape")
8325 };
8326 assert_eq!(pointers.len(), 1);
8327 assert!(
8328 matches!(
8329 &pointers[0],
8330 crate::query::ShapeNode::Enum { name, position: 0, enum_type }
8331 if name == "gender" && enum_type == "public::Gender"
8332 ),
8333 "expected Enum-tagged shape, got: {:?}",
8334 pointers[0]
8335 );
8336 }
8337
8338 #[test]
8339 fn test_free_select_bare_enum_literal_casts_to_text_inside_row() {
8340 let mut schema = make_schema();
8344 schema.enums.push(crate::schema::EnumDescriptor {
8345 name: "Gender".into(),
8346 module: "default".into(),
8347 members: vec!["Male".into(), "Female".into()],
8348 });
8349 let out = compile_and_emit_with("select default::Gender.Male", &schema);
8350 assert!(
8351 out.sql.contains("ROW(v::text) AS result"),
8352 "expected ROW(v::text), got:\n{}",
8353 out.sql
8354 );
8355 assert!(
8356 matches!(
8357 &out.shape.root,
8358 crate::query::ShapeNode::Enum { enum_type, .. } if enum_type == "public::Gender"
8359 ),
8360 "expected Enum-tagged shape, got: {:?}",
8361 out.shape.root
8362 );
8363 }
8364
8365 #[test]
8368 fn test_for_update_defaults_to_blocking() {
8369 let out = compile_and_emit("SELECT Person FOR UPDATE");
8370 assert!(out.sql.trim_end().ends_with("FOR UPDATE"), "got:\n{}", out.sql);
8371 }
8372
8373 #[test]
8374 fn test_for_update_skip_locked() {
8375 let out = compile_and_emit("SELECT Person FOR UPDATE SKIP LOCKED");
8376 assert!(
8377 out.sql.trim_end().ends_with("FOR UPDATE SKIP LOCKED"),
8378 "got:\n{}",
8379 out.sql
8380 );
8381 }
8382
8383 #[test]
8384 fn test_for_update_nowait() {
8385 let out = compile_and_emit("SELECT Person FOR UPDATE NOWAIT");
8386 assert!(out.sql.trim_end().ends_with("FOR UPDATE NOWAIT"), "got:\n{}", out.sql);
8387 }
8388
8389 #[test]
8390 fn test_for_no_key_update_skip_locked() {
8391 let out = compile_and_emit("SELECT Person FOR NO KEY UPDATE SKIP LOCKED");
8392 assert!(
8393 out.sql.trim_end().ends_with("FOR NO KEY UPDATE SKIP LOCKED"),
8394 "got:\n{}",
8395 out.sql
8396 );
8397 }
8398
8399 #[test]
8400 fn test_for_share() {
8401 let out = compile_and_emit("SELECT Person FOR SHARE");
8402 assert!(out.sql.trim_end().ends_with("FOR SHARE"), "got:\n{}", out.sql);
8403 }
8404
8405 #[test]
8406 fn test_for_key_share_nowait() {
8407 let out = compile_and_emit("SELECT Person FOR KEY SHARE NOWAIT");
8408 assert!(
8409 out.sql.trim_end().ends_with("FOR KEY SHARE NOWAIT"),
8410 "got:\n{}",
8411 out.sql
8412 );
8413 }
8414
8415 #[test]
8416 fn test_for_update_comes_after_order_by_limit_offset_in_emitted_sql() {
8417 let out = compile_and_emit("SELECT Person { name } ORDER BY .name OFFSET 1 LIMIT 5 FOR UPDATE SKIP LOCKED");
8420 let order_pos = out.sql.find("ORDER BY").unwrap();
8421 let offset_pos = out.sql.find("OFFSET").unwrap();
8422 let limit_pos = out.sql.find("LIMIT").unwrap();
8423 let for_pos = out.sql.find("FOR UPDATE").unwrap();
8424 assert!(
8425 order_pos < offset_pos && offset_pos < limit_pos && limit_pos < for_pos,
8426 "got:\n{}",
8427 out.sql
8428 );
8429 }
8430
8431 #[test]
8432 fn test_for_update_combined_with_distinct_is_rejected() {
8433 let ast = parse::parse("SELECT DISTINCT Person FOR UPDATE").expect("parse failed");
8434 let err = ir::compile(&ast, &make_schema())
8435 .err()
8436 .expect("expected a compile error");
8437 assert!(err.to_string().contains("DISTINCT"), "unexpected: {err}");
8438 }
8439
8440 #[test]
8441 fn test_for_update_combined_with_select_over_insert_is_rejected() {
8442 let ast = parse::parse("SELECT (INSERT Person { name := 'Alice' }) { name } FOR UPDATE").expect("parse failed");
8443 let err = ir::compile(&ast, &make_schema())
8444 .err()
8445 .expect("expected a compile error");
8446 assert!(err.to_string().contains("INSERT"), "unexpected: {err}");
8447 }
8448
8449 #[test]
8450 fn test_for_update_on_an_interface_type_is_rejected() {
8451 fn id_prop() -> PropertyDescriptor {
8452 PropertyDescriptor {
8453 name: "id".into(),
8454 pg_type: "uuid".into(),
8455 nullable: false,
8456 default_sql: Some("uuidv7()".into()),
8457 default_pyql: None,
8458 description: None,
8459 check_constraints: vec![],
8460 is_exclusive: true,
8461 is_pk: true,
8462 is_readonly: true,
8463 rewrites: vec![],
8464 tuple_members: None,
8465 column_type: None,
8466 }
8467 }
8468 let schema = SchemaDescriptor {
8469 types: vec![
8470 TypeDescriptor {
8471 name: "Account".into(),
8472 module: "default".into(),
8473 table: "Account".into(),
8474 abstract_: true,
8475 materialized: true,
8476 description: None,
8477 parents: vec![],
8478 interfaces: vec![],
8479 bases: vec![],
8480 properties: vec![id_prop()],
8481 links: vec![],
8482 multilinks: vec![],
8483 computed: vec![],
8484 constraints: vec![],
8485 indexes: vec![],
8486 partition: None,
8487 vector_indexes: vec![],
8488 search_indexes: vec![],
8489 triggers: vec![],
8490 junction: false,
8491 signals: vec![],
8492 },
8493 TypeDescriptor {
8494 name: "Individual".into(),
8495 module: "default".into(),
8496 table: "Individual".into(),
8497 abstract_: false,
8498 materialized: true,
8499 description: None,
8500 parents: vec![],
8501 interfaces: vec!["default::Account".into()],
8502 bases: vec![],
8503 properties: vec![id_prop()],
8504 links: vec![],
8505 multilinks: vec![],
8506 computed: vec![],
8507 constraints: vec![],
8508 indexes: vec![],
8509 partition: None,
8510 vector_indexes: vec![],
8511 search_indexes: vec![],
8512 triggers: vec![],
8513 junction: false,
8514 signals: vec![],
8515 },
8516 ],
8517 scalars: vec![],
8518 enums: vec![],
8519 named_tuples: vec![],
8520 globals: vec![],
8521 functions: vec![],
8522 aliases: vec![],
8523 channels: vec![],
8524 ..Default::default()
8525 };
8526 let ast = parse::parse("SELECT Account FOR UPDATE").expect("parse failed");
8527 let err = ir::compile(&ast, &schema).err().expect("expected a compile error");
8528 assert!(err.to_string().contains("interface"), "unexpected: {err}");
8529 }
8530
8531 #[test]
8532 fn test_schema_select_distinct_emits_distinct_keyword() {
8533 let out = compile_and_emit("SELECT DISTINCT Person { name }");
8538 assert!(out.sql.contains("DISTINCT"), "expected DISTINCT in SQL:\n{}", out.sql);
8539 }
8540
8541 #[test]
8542 fn test_schema_select_detached_compiles_as_ordinary_select() {
8543 let out = compile_and_emit("SELECT DETACHED Person { name }");
8547 assert!(
8548 out.sql.contains("\"name\""),
8549 "expected name column in SQL:\n{}",
8550 out.sql
8551 );
8552 }
8553
8554 #[test]
8555 fn test_detached_subquery_correlates_against_the_outer_row() {
8556 let out = compile_and_emit(
8563 "SELECT Person { name } FILTER NOT EXISTS (\
8564 SELECT DETACHED Person FILTER .name = Person.name AND .age > Person.age)",
8565 );
8566 assert!(
8567 out.sql.contains("\"t1\".\"name\" = \"t0\".\"name\""),
8568 "the inner row should be compared against the outer one:\n{}",
8569 out.sql
8570 );
8571 assert!(
8572 !out.sql.contains("\"t1\".\"name\" = \"t1\".\"name\""),
8573 "self-comparison means the correlation was lost:\n{}",
8574 out.sql
8575 );
8576 }
8577
8578 #[test]
8579 fn test_an_assert_on_a_pointer_checks_its_rows() {
8580 let out = compile_and_emit("SELECT Person { name, p := assert_exists(.posts { title }) }");
8584 assert!(
8585 out.sql
8586 .contains(r#"cardinality("_pylon"."assert_exists"("_a"."v"::text[]))"#),
8587 "the assert should read the aggregated rows:\n{}",
8588 out.sql
8589 );
8590 assert_eq!(
8591 out.sql.matches("array_agg(ROW(").count(),
8592 1,
8593 "the pointer's subquery should be evaluated once, not once per use:\n{}",
8594 out.sql
8595 );
8596 }
8597
8598 #[test]
8599 fn test_an_assert_on_a_scalar_stays_an_ordinary_call() {
8600 let out = compile_and_emit("SELECT Person { n := assert_exists(.name) }");
8603 assert!(
8604 !out.sql.contains(r#""_a"."v""#),
8605 "a scalar assert should not be wrapped as a pointer check:\n{}",
8606 out.sql
8607 );
8608 }
8609
8610 #[test]
8611 fn test_a_backlink_tail_walks_past_the_first_step() {
8612 let out = compile_and_emit("SELECT Post FILTER any(.<posts[is Person].posts.title = $t)");
8617 assert!(
8618 out.sql.contains(r#""public"."Person.posts""#),
8619 "the tail should walk the multi-link's junction:\n{}",
8620 out.sql
8621 );
8622 }
8623
8624 #[test]
8625 fn test_an_intersection_narrows_the_rest_of_a_multilink_walk() {
8626 let out = compile_and_emit("SELECT Person FILTER any(.posts[is Post].title = $t)");
8630 assert!(
8631 out.sql.contains(r#""public"."Post""#),
8632 "the walk should continue from the narrowed type:\n{}",
8633 out.sql
8634 );
8635 }
8636
8637 #[test]
8638 fn test_a_union_of_bindings_emits_selects_not_bare_names() {
8639 let out = compile_and_emit_with(
8643 "WITH a := (INSERT Post { title := $t1 }), b := (INSERT Post { title := $t2 }) \
8644 SELECT (INSERT Person { name := $n, posts := (a UNION b) })",
8645 &make_schema(),
8646 );
8647 assert!(
8648 out.sql.contains(r#"(SELECT "_s"."id" FROM "a" AS "_s")"#),
8649 "each union operand should be a select:\n{}",
8650 out.sql
8651 );
8652 }
8653
8654 #[test]
8655 fn test_a_bare_narrowing_reads_as_a_type_check_and_as_an_object() {
8656 for query in [
8660 "SELECT Person FILTER EXISTS [is Person]",
8661 "SELECT Person { p := [is Person] { name } }",
8662 ] {
8663 let ast = parse::parse(query).unwrap_or_else(|e| panic!("{query}: {e}"));
8664 assert!(ir::compile(&ast, &make_schema()).is_ok(), "{query} should compile");
8665 }
8666 }
8667
8668 #[test]
8669 fn test_a_multilink_takes_a_set_wrapped_in_a_select() {
8670 let out = compile_and_emit_with(
8675 "WITH a := (INSERT Post { title := $t1 }), b := (INSERT Post { title := $t2 }) \
8676 SELECT (INSERT Person { name := $n, posts := (SELECT { a, b }) })",
8677 &make_schema(),
8678 );
8679 assert!(
8680 out.sql.contains(r#""public"."Person.posts""#),
8681 "both targets should reach the junction:\n{}",
8682 out.sql
8683 );
8684 }
8685
8686 #[test]
8687 fn test_an_aggregate_over_a_subselected_path() {
8688 let out = compile_and_emit("SELECT array_agg((SELECT Person.name))");
8694 assert!(
8695 out.sql.contains("array_agg") && out.sql.contains(r#""public"."Person""#),
8696 "the aggregate should wrap the walk's column:\n{}",
8697 out.sql
8698 );
8699 }
8700
8701 #[test]
8702 fn test_a_distinct_aggregate_argument_reaches_the_aggregate() {
8703 for query in [
8709 "SELECT array_agg(DISTINCT Person.name)",
8710 "SELECT array_agg((SELECT DISTINCT Person.name))",
8711 ] {
8712 let out = compile_and_emit(query);
8713 assert!(
8714 out.sql.contains("array_agg(DISTINCT "),
8715 "{query} should deduplicate inside the aggregate:\n{}",
8716 out.sql
8717 );
8718 }
8719 }
8720
8721 #[test]
8722 fn test_is_not_negates_the_type_check() {
8723 let positive = compile_and_emit("SELECT Person FILTER Person IS Person");
8727 let negative = compile_and_emit("SELECT Person FILTER Person IS NOT Person");
8728 assert!(
8729 negative.sql.contains("NOT") && !positive.sql.contains("NOT"),
8730 "the negation should reach the SQL:\npositive:\n{}\nnegative:\n{}",
8731 positive.sql,
8732 negative.sql
8733 );
8734 }
8735
8736 #[test]
8737 fn test_a_cast_takes_a_prefix_operator_as_its_operand() {
8738 for query in ["SELECT <bool>EXISTS (SELECT Person LIMIT 1)", "SELECT <int64>-1"] {
8742 let ast = parse::parse(query).unwrap_or_else(|e| panic!("{query} should parse: {e}"));
8743 assert!(ir::compile(&ast, &make_schema()).is_ok(), "{query} should compile");
8744 }
8745 }
8746
8747 #[test]
8748 fn test_a_bare_multilink_reads_as_a_set() {
8749 let out =
8754 compile_and_emit("WITH p := (SELECT DETACHED Post LIMIT 1) SELECT Person { name } FILTER p IN .posts");
8755 assert!(
8756 out.sql.contains(r#""public"."Person.posts""#),
8757 "the membership test should read the link's junction:\n{}",
8758 out.sql
8759 );
8760 }
8761
8762 #[test]
8763 fn test_a_select_reads_its_own_shape_computed_in_its_clauses() {
8764 let out = compile_and_emit("SELECT Person { n := .name } ORDER BY .n ASC");
8768 assert!(
8769 out.sql.contains("ORDER BY"),
8770 "the order by should compile against the declared pointer:\n{}",
8771 out.sql
8772 );
8773 }
8774
8775 #[test]
8776 fn test_a_declared_pointer_can_be_walked_through() {
8777 let out = compile_and_emit_with(
8781 "WITH p := (SELECT Person { boss := .company } LIMIT 1) SELECT p { name, boss_name := .boss.name }",
8782 &make_schema(),
8783 );
8784 assert!(
8785 out.sql.contains(r#""public"."Company""#),
8786 "the walk should reach the declared pointer's own target table:\n{}",
8787 out.sql
8788 );
8789 }
8790
8791 #[test]
8792 fn test_a_replaced_subject_shape_stays_in_scope() {
8793 let out = compile_and_emit("SELECT Person { c := (SELECT .company { n := .name }) { n } }");
8798 assert!(
8799 out.sql.contains(r#"'default::Company'::text"#) && out.sql.contains(r#""t2"."name""#),
8800 "`n` should resolve to the company's own column:\n{}",
8801 out.sql
8802 );
8803 }
8804
8805 #[test]
8806 fn test_a_sibling_shape_pointer_is_not_in_scope() {
8807 let schema = make_schema();
8810 let ast = parse::parse("SELECT Person { n := .name, copy := .n }").unwrap();
8811 assert!(ir::compile(&ast, &schema).is_err());
8812 }
8813
8814 #[test]
8815 fn test_shape_over_a_with_binding_reads_the_binding() {
8816 let out = compile_and_emit("WITH c := (SELECT Company LIMIT 1) SELECT Person { name, employer := c { name } }");
8821 assert!(
8822 out.sql.contains("\"c\""),
8823 "the pointer should read the binding's CTE:\n{}",
8824 out.sql
8825 );
8826 }
8827
8828 #[test]
8829 fn test_shape_over_a_bare_type_name_is_still_rejected() {
8830 let schema = make_schema();
8833 let ast = parse::parse("SELECT Person { name, other := Company { name } }").unwrap();
8834 assert!(ir::compile(&ast, &schema).is_err());
8835 }
8836
8837 #[test]
8838 fn test_nested_shape_reads_a_prefix_bound_by_the_outer_select() {
8839 let out = compile_and_emit("SELECT Person { name, posts: { title, who := Person.name } }");
8844 assert!(
8845 out.sql.contains(r#""t0"."name""#),
8846 "the nested computed should read the outer Person alias:\n{}",
8847 out.sql
8848 );
8849 assert!(
8850 !out.sql.contains(r#"FROM "public"."Person" "t2""#),
8851 "the prefix should not open a second Person source:\n{}",
8852 out.sql
8853 );
8854 }
8855
8856 #[test]
8857 fn test_prefix_naming_a_type_out_of_scope_is_rejected() {
8858 let schema = make_schema();
8861 let ast = parse::parse("SELECT Person { name, who := Company.name }").unwrap();
8862 assert!(ir::compile(&ast, &schema).is_err());
8863 }
8864
8865 #[test]
8866 fn test_free_select_tuple() {
8867 let schema = make_schema();
8868 let ast = parse::parse("SELECT (1, 2)").unwrap();
8869 let ir = ir::compile(&ast, &schema).unwrap();
8870 let out = emit(&ir);
8871 assert!(out.sql.contains("1"));
8872 assert!(out.sql.contains("2"));
8873 assert!(out.sql.contains("AS result"));
8874 assert!(matches!(out.shape.root, crate::query::ShapeNode::Tuple { .. }));
8875 }
8876
8877 #[test]
8878 fn test_free_select_scalar_literal() {
8879 let schema = make_schema();
8880 let ast = parse::parse("SELECT 'hello'").unwrap();
8881 let ir = ir::compile(&ast, &schema).unwrap();
8882 let out = emit(&ir);
8883 assert!(out.sql.contains("SELECT 'hello' AS v"));
8884 assert!(out.sql.contains("ROW(v) AS result"));
8885 assert!(matches!(out.shape.root, crate::query::ShapeNode::Scalar { .. }));
8886 }
8887
8888 #[test]
8889 fn test_float_literal_casts_to_float8() {
8890 let out = compile_and_emit("SELECT 1.0");
8897 assert!(
8898 out.sql.contains("(1.0::float8)"),
8899 "expected explicit float8 cast, got:\n{}",
8900 out.sql
8901 );
8902 }
8903
8904 #[test]
8905 fn test_free_select_array_literal() {
8906 let schema = make_schema();
8907 let ast = parse::parse("SELECT [1, 2, 3]").unwrap();
8908 let ir = ir::compile(&ast, &schema).unwrap();
8909 let out = emit(&ir);
8910 assert!(out.sql.contains("SELECT ARRAY[1, 2, 3] AS v"), "{}", out.sql);
8911 assert!(out.sql.contains("SELECT v AS result, v FROM"), "{}", out.sql);
8912 assert!(matches!(out.shape.root, crate::query::ShapeNode::RawScalar));
8913 }
8914
8915 #[test]
8916 fn test_select_scalars() {
8917 let out = compile_and_emit("SELECT Person { name, age }");
8918 assert!(out.sql.contains("'default::Person'::text"));
8919 assert!(out.sql.contains("\"name\"::text"));
8920 assert!(out.sql.contains("\"age\"::int8"));
8921 assert!(out.sql.contains("FROM \"public\".\"Person\""));
8922 assert!(out.sql.contains(") AS result"));
8923 }
8924
8925 #[test]
8926 fn test_select_filter_param() {
8927 let out = compile_and_emit("SELECT Person { name } FILTER .name = $name");
8928 assert!(out.sql.contains("WHERE"));
8929 assert!(out.sql.contains("$1"));
8930 }
8931
8932 #[test]
8933 fn test_filter_in_set_literal_compiles_to_any_array() {
8934 let out = compile_and_emit("SELECT Person { name } FILTER .name IN {'Carol'}");
8935 assert!(out.sql.contains("= ANY(ARRAY['Carol'])"), "got:\n{}", out.sql);
8936 }
8937
8938 #[test]
8939 fn test_filter_in_multi_element_set_literal_compiles_to_any_array() {
8940 let out = compile_and_emit("SELECT Person { name } FILTER .name IN {'Carol', 'Bob'}");
8941 assert!(out.sql.contains("= ANY(ARRAY['Carol', 'Bob'])"), "got:\n{}", out.sql);
8942 }
8943
8944 #[test]
8951 fn test_filter_in_array_unpack_passes_the_array_straight_to_any() {
8952 let out = compile_and_emit("SELECT Person { name } FILTER .name IN std::array_unpack(<array<str>>$names)");
8953 assert!(
8954 !out.sql.contains("ANY(unnest("),
8955 "unnest inside ANY is invalid in WHERE:\n{}",
8956 out.sql
8957 );
8958 assert!(out.sql.contains("= ANY("), "got:\n{}", out.sql);
8959 }
8960
8961 #[test]
8962 fn test_filter_not_in_array_unpack_passes_the_array_straight_to_all() {
8963 let out = compile_and_emit("SELECT Person { name } FILTER .name NOT IN std::array_unpack(<array<str>>$names)");
8964 assert!(
8965 !out.sql.contains("ALL(unnest("),
8966 "unnest inside ALL is invalid in WHERE:\n{}",
8967 out.sql
8968 );
8969 assert!(out.sql.contains("<> ALL("), "got:\n{}", out.sql);
8970 }
8971
8972 #[test]
8977 fn test_all_over_unpacked_array_aggregates_the_array_not_the_multilink() {
8978 let out = compile_and_emit(
8979 "SELECT Person { name } FILTER all(std::array_unpack(<array<str>>$titles) IN .posts.title)",
8980 );
8981 assert!(out.sql.contains("bool_and("), "got:\n{}", out.sql);
8982 assert!(out.sql.contains("_unnested"), "got:\n{}", out.sql);
8983 }
8984
8985 #[test]
8989 fn test_any_over_a_multilink_against_an_unpacked_array_still_folds_into_any() {
8990 let out = compile_and_emit(
8991 "SELECT Person { name } FILTER any(.posts.title IN std::array_unpack(<array<str>>$titles))",
8992 );
8993 assert!(!out.sql.contains("unnest("), "got:\n{}", out.sql);
8994 assert!(out.sql.contains("= ANY("), "got:\n{}", out.sql);
8995 }
8996
8997 #[test]
8998 fn test_any_over_unpacked_array_aggregates_the_array_not_the_multilink() {
8999 let out = compile_and_emit(
9000 "SELECT Person { name } FILTER any(std::array_unpack(<array<str>>$titles) IN .posts.title)",
9001 );
9002 assert!(out.sql.contains("bool_or("), "got:\n{}", out.sql);
9003 assert!(out.sql.contains("_unnested"), "got:\n{}", out.sql);
9004 }
9005
9006 fn schema_with_array_property() -> SchemaDescriptor {
9008 let mut schema = make_schema();
9009 schema.types[0].properties.push(crate::schema::PropertyDescriptor {
9010 name: "perms".into(),
9011 pg_type: "text[]".into(),
9012 nullable: true,
9013 default_sql: None,
9014 default_pyql: None,
9015 description: None,
9016 check_constraints: vec![],
9017 is_exclusive: false,
9018 is_pk: false,
9019 is_readonly: false,
9020 rewrites: vec![],
9021 tuple_members: None,
9022 column_type: None,
9023 });
9024 schema
9025 }
9026
9027 #[test]
9033 fn test_aggregate_over_an_unpacked_walk_unnests_in_a_row_source() {
9034 let out = compile_and_emit_with(
9035 "WITH teams := (SELECT Person { perms } FILTER .age > 1) SELECT std::array_agg(std::array_unpack(teams.perms))",
9036 &schema_with_array_property(),
9037 );
9038 assert!(!out.sql.contains("array_agg(unnest("), "got:\n{}", out.sql);
9039 assert!(
9040 out.sql.contains(
9041 r#"coalesce(array_agg("_s"."v"), '{}') FROM unnest(ARRAY(SELECT unnest("t1"."perms") FROM "teams" AS "t1"))"#
9042 ),
9043 "got:\n{}",
9044 out.sql
9045 );
9046 }
9047
9048 #[test]
9049 fn test_aggregate_over_an_unpacked_type_rooted_path_unnests_in_a_row_source() {
9050 let out = compile_and_emit_with(
9051 "SELECT std::count(std::array_unpack(Person.perms))",
9052 &schema_with_array_property(),
9053 );
9054 assert!(!out.sql.contains("count(unnest("), "got:\n{}", out.sql);
9055 assert!(
9056 out.sql
9057 .contains(r#"FROM unnest(ARRAY(SELECT unnest("t0"."perms") FROM "public"."Person" AS "t0"))"#),
9058 "got:\n{}",
9059 out.sql
9060 );
9061 }
9062
9063 #[test]
9066 fn test_aggregate_over_an_unpacked_parameter_unnests_the_array_itself() {
9067 let out = compile_and_emit("SELECT std::array_agg(std::array_unpack(<array<str>>$names))");
9068 assert!(!out.sql.contains("array_agg(unnest("), "got:\n{}", out.sql);
9069 assert!(
9070 out.sql.contains(r#"FROM unnest(($1)::text[]) AS "_s"("v")"#),
9071 "got:\n{}",
9072 out.sql
9073 );
9074 }
9075
9076 #[test]
9077 fn test_aggregate_over_an_unpacked_property_unnests_that_row_s_array() {
9078 let out = compile_and_emit_with(
9079 "SELECT Person { n := std::array_agg(std::array_unpack(.perms)) }",
9080 &schema_with_array_property(),
9081 );
9082 assert!(!out.sql.contains("array_agg(unnest("), "got:\n{}", out.sql);
9083 assert!(
9084 out.sql.contains(r#"FROM unnest("t0"."perms") AS "_s"("v")"#),
9085 "got:\n{}",
9086 out.sql
9087 );
9088 }
9089
9090 #[test]
9093 fn test_array_agg_over_nothing_is_an_empty_array_not_null() {
9094 for query in [
9095 "SELECT std::array_agg(Person.name)",
9096 "SELECT std::array_agg((SELECT Person).name)",
9097 ] {
9098 let out = compile_and_emit(query);
9099 assert!(
9100 out.sql.contains("coalesce(") && out.sql.contains("'{}'"),
9101 "{query} got:\n{}",
9102 out.sql
9103 );
9104 }
9105 }
9106
9107 #[test]
9111 fn test_an_array_agg_binding_stays_an_array_through_its_coalesce() {
9112 let out = compile_and_emit(
9113 "WITH ids := std::array_agg(Person.id) SELECT Person { name } FILTER std::contains(ids, .id)",
9114 );
9115 assert!(
9116 out.sql.contains("@> ARRAY["),
9117 "expected the array overload, got:\n{}",
9118 out.sql
9119 );
9120 assert!(
9121 !out.sql.contains("strpos("),
9122 "the string overload must not win, got:\n{}",
9123 out.sql
9124 );
9125 }
9126
9127 #[test]
9130 fn test_aggregate_over_a_plain_walk_keeps_its_row_source() {
9131 let out = compile_and_emit("SELECT std::array_agg(Person.name)");
9132 assert!(
9133 out.sql.contains(r#"array_agg("t0"."name")"#) && out.sql.contains(r#"FROM "public"."Person""#),
9134 "got:\n{}",
9135 out.sql
9136 );
9137 }
9138
9139 #[test]
9142 fn test_the_cte_order_check_catches_a_forward_reference() {
9143 let bad = "WITH \"b\" AS (\n SELECT * FROM \"a\"\n),\n\"a\" AS (\n SELECT 1\n)\nSELECT 1";
9144 assert!(
9145 super::forward_cte_reference(bad).is_some(),
9146 "expected a forward reference"
9147 );
9148 let good = "WITH \"a\" AS (\n SELECT 1\n),\n\"b\" AS (\n SELECT * FROM \"a\"\n)\nSELECT 1";
9149 assert_eq!(super::forward_cte_reference(good), None, "ordered CTEs must pass");
9150 }
9151
9152 #[test]
9156 fn test_a_cast_unpacked_array_still_folds_into_any() {
9157 let out = compile_and_emit("SELECT Person { name } FILTER .name IN <str>std::array_unpack(<array<str>>$names)");
9158 assert!(
9159 !out.sql.contains("unnest("),
9160 "the unpack must fold into ANY, got:\n{}",
9161 out.sql
9162 );
9163 assert!(out.sql.contains("= ANY("), "got:\n{}", out.sql);
9164 }
9165
9166 #[test]
9169 fn test_array_unpack_outside_in_still_unnests() {
9170 let out = compile_and_emit("SELECT std::array_unpack(<array<str>>$names)");
9171 assert!(out.sql.contains("unnest("), "got:\n{}", out.sql);
9172 }
9173
9174 #[test]
9175 fn test_filter_not_in_set_literal_compiles_to_all_array() {
9176 let out = compile_and_emit("SELECT Person { name } FILTER .name NOT IN {'Carol'}");
9177 assert!(out.sql.contains("<> ALL(ARRAY['Carol'])"), "got:\n{}", out.sql);
9178 }
9179
9180 #[test]
9181 fn test_deep_splat_fetches_single_link_target_properties_not_just_id() {
9182 let out = compile_and_emit("SELECT Person { ** }");
9187 assert!(
9188 out.sql.contains("\"name\""),
9189 "expected Company.name pulled in via .company's ** expansion, got:\n{}",
9190 out.sql
9191 );
9192 }
9193
9194 #[test]
9195 fn test_deep_splat_fetches_multilink_target_properties_not_just_id() {
9196 let out = compile_and_emit("SELECT Person { ** }");
9197 assert!(
9198 out.sql.contains("\"title\""),
9199 "expected Post.title pulled in via .posts' ** expansion, got:\n{}",
9200 out.sql
9201 );
9202 }
9203
9204 #[test]
9205 fn test_schema_type_cast_select() {
9206 let out = compile_and_emit("SELECT <default::Person><uuid>'019ef1bb-0d42-7a9f-8f6b-b38d028a49ba'");
9207 assert!(out.sql.contains("FROM \"public\".\"Person\""));
9208 assert!(out.sql.contains("WHERE"));
9209 assert!(out.sql.contains("'019ef1bb-0d42-7a9f-8f6b-b38d028a49ba'"));
9210 }
9211
9212 #[test]
9213 fn test_select_single_link() {
9214 let out = compile_and_emit("SELECT Person { name, company { name } }");
9215 assert!(out.sql.contains("'default::Company'::text"));
9216 assert!(out.sql.contains("FROM \"public\".\"Company\""));
9217 assert!(out.sql.contains("\"company_id\" = "));
9219 }
9220
9221 #[test]
9222 fn test_select_multi_link() {
9223 let out = compile_and_emit("SELECT Person { name, posts { title } }");
9224 assert!(out.sql.contains("array_agg(ROW("));
9225 assert!(out.sql.contains("ARRAY[]::record[]"));
9226 assert!(out.sql.contains("'default::Post'::text"));
9227 assert!(out.sql.contains("\"Person.posts\""));
9228 }
9229
9230 fn compile_err(query: &str) -> String {
9231 let schema = make_schema();
9232 let ast = parse::parse(query).expect("parse failed");
9233 match ir::compile(&ast, &schema) {
9234 Ok(_) => panic!("expected a compile error"),
9235 Err(e) => format!("{e}"),
9236 }
9237 }
9238
9239 #[test]
9240 fn test_multi_link_limit_cuts_rows_before_aggregating() {
9241 let out = compile_and_emit("SELECT Person { posts: { title } filter .title = 'x' limit 1 }");
9242 assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
9245 assert!(out.sql.contains("AS \"__lim\""), "{}", out.sql);
9246 let agg = out.sql.find("array_agg").unwrap();
9247 let limit = out.sql.find("LIMIT 1").unwrap();
9248 assert!(agg < limit, "{}", out.sql);
9249 assert!(out.sql.contains("\"t1\".\"title\"::text AS \"c1\""), "{}", out.sql);
9250 }
9251
9252 #[test]
9253 fn test_multi_link_order_by_with_limit_orders_the_aggregate_too() {
9254 let out = compile_and_emit("SELECT Person { posts: { title } order by .title desc offset 1 limit 2 }");
9255 assert!(out.sql.contains("\"t1\".\"title\" AS \"s0\""), "{}", out.sql);
9258 assert!(
9259 out.sql.contains("ORDER BY \"__lim\".\"s0\" DESC NULLS LAST"),
9260 "{}",
9261 out.sql
9262 );
9263 assert!(out.sql.contains("OFFSET 1"), "{}", out.sql);
9264 assert!(out.sql.contains("LIMIT 2"), "{}", out.sql);
9265 }
9266
9267 #[test]
9268 fn test_multi_link_without_limit_keeps_the_flat_aggregate() {
9269 let out = compile_and_emit("SELECT Person { posts: { title } filter .title = 'x' }");
9270 assert!(!out.sql.contains("__lim"), "{}", out.sql);
9271 assert!(out.sql.contains("array_agg(ROW("), "{}", out.sql);
9272 }
9273
9274 #[test]
9275 fn test_sub_select_as_computed_pointer_is_a_link_pointer() {
9276 let out = compile_and_emit("SELECT Person { recent := (select .posts filter .title = 'x' limit 1) { title } }");
9277 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
9278 panic!()
9279 };
9280 let ShapeNode::Object {
9283 name,
9284 pointers: elem_pointers,
9285 ..
9286 } = &pointers[2]
9287 else {
9288 panic!("{:?}", pointers[2])
9289 };
9290 assert_eq!(name, "recent");
9291 assert!(matches!(&elem_pointers[1], ShapeNode::Scalar { name, .. } if name == "title"));
9292 assert!(out.sql.contains("\"jt\".source = \"t0\".id"), "{}", out.sql);
9293 assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
9294 }
9295
9296 #[test]
9297 fn test_sub_select_shape_inside_the_parens_is_equivalent() {
9298 let outer = compile_and_emit("SELECT Person { recent := (select .posts limit 1) { title } }");
9299 let inner = compile_and_emit("SELECT Person { recent := (select .posts { title } limit 1) }");
9300 assert_eq!(outer.sql, inner.sql);
9301 }
9302
9303 #[test]
9304 fn test_sub_select_field_access_is_a_correlated_scalar_subquery() {
9305 let out = compile_and_emit("SELECT Person { t := (select .posts filter .title = 'x' limit 1).title }");
9306 assert!(out.sql.contains("(\"t1\".\"id\" = \"t0\".\"id\")"), "{}", out.sql);
9309 assert!(out.sql.contains("\"t2\".\"title\""), "{}", out.sql);
9310 assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
9311 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
9312 panic!()
9313 };
9314 assert!(matches!(&pointers[2], ShapeNode::Scalar { name, .. } if name == "t"));
9315 }
9316
9317 #[test]
9318 fn test_sub_select_field_access_over_a_single_link() {
9319 let out = compile_and_emit("SELECT Person { c := (select .company).name }");
9320 assert!(out.sql.contains("\"t1\".\"company_id\" = \"t2\".\"id\""), "{}", out.sql);
9321 assert!(out.sql.contains("(\"t1\".\"id\" = \"t0\".\"id\")"), "{}", out.sql);
9322 }
9323
9324 #[test]
9325 fn test_sub_select_field_access_in_a_filter() {
9326 let out = compile_and_emit("SELECT Person { name } filter (select .posts limit 1).title = 'x'");
9327 assert!(out.sql.contains("WHERE ((SELECT \"t2\".\"title\""), "{}", out.sql);
9328 assert!(out.sql.contains("LIMIT 1) = 'x')"), "{}", out.sql);
9329 }
9330
9331 #[test]
9332 fn test_sub_select_over_an_absolute_path_is_not_correlated() {
9333 let out = compile_and_emit("SELECT Person { n := (select Company.name limit 1) }");
9334 assert!(out.sql.contains("FROM \"public\".\"Company\""), "{}", out.sql);
9335 assert!(!out.sql.contains("= \"t0\".\"id\""), "{}", out.sql);
9336 }
9337
9338 #[test]
9339 fn test_backlink_sub_select_carries_its_modifiers() {
9340 let out = compile_and_emit("SELECT Post { authors := (select .<posts[is Person] { name } limit 2) }");
9341 assert!(out.sql.contains("LIMIT 2"), "{}", out.sql);
9342 assert!(out.sql.contains("AS \"__lim\""), "{}", out.sql);
9343 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
9344 panic!()
9345 };
9346 assert!(matches!(&pointers[1], ShapeNode::Array { name, .. } if name == "authors"));
9347 }
9348
9349 #[test]
9350 fn test_with_bound_scalar_is_typed_for_overload_resolution() {
9351 let out = compile_and_emit("WITH xs := (select Person.name) SELECT contains(xs, 'a')");
9354 assert!(
9355 out.sql.contains("strpos((SELECT \"v\" FROM \"xs\"), 'a')"),
9356 "{}",
9357 out.sql
9358 );
9359 }
9360
9361 fn make_schema_with_computed_links() -> SchemaDescriptor {
9364 use crate::schema::ComputedDescriptor;
9365 let mut schema = make_schema();
9366 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
9367 person.computed = vec![
9368 ComputedDescriptor {
9369 name: "published".into(),
9370 expression: "(select .posts filter .title != '')".into(),
9371 return_type: None,
9372 link_target: None,
9373 link_multi: false,
9374 },
9375 ComputedDescriptor {
9376 name: "plain".into(),
9377 expression: ".posts".into(),
9378 return_type: None,
9379 link_target: None,
9380 link_multi: false,
9381 },
9382 ComputedDescriptor {
9383 name: "featured".into(),
9384 expression: "(select .published filter .title != 'draft')".into(),
9385 return_type: None,
9386 link_target: None,
9387 link_multi: false,
9388 },
9389 ComputedDescriptor {
9390 name: "looper".into(),
9391 expression: "(select .looper)".into(),
9392 return_type: None,
9393 link_target: None,
9394 link_multi: false,
9395 },
9396 ComputedDescriptor {
9397 name: "capped".into(),
9398 expression: "(select .posts limit 1)".into(),
9399 return_type: None,
9400 link_target: None,
9401 link_multi: false,
9402 },
9403 ];
9404 schema
9405 }
9406
9407 fn make_schema_with_object_fn_computed() -> SchemaDescriptor {
9410 use crate::schema::{ComputedDescriptor, FunctionDescriptor, FunctionParamDescriptor};
9411 let mut schema = make_schema();
9412 schema.functions = vec![FunctionDescriptor {
9413 name: "latest".into(),
9414 module: "default".into(),
9415 params: vec![FunctionParamDescriptor {
9416 name: "owner".into(),
9417 pg_type: "uuid".into(),
9418 }],
9419 return_pg_type: "default::Post".into(),
9420 return_is_object: true,
9421 return_is_set: true,
9422 return_is_polymorphic: false,
9423 volatility: "stable".into(),
9424 body: String::new(),
9425 }];
9426 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
9427 person.computed = vec![
9428 ComputedDescriptor {
9429 name: "recent".into(),
9430 expression: "latest(.id)".into(),
9431 return_type: None,
9432 link_target: None,
9433 link_multi: false,
9434 },
9435 ComputedDescriptor {
9436 name: "vetted".into(),
9437 expression: "(select latest(.id) filter .title != '')".into(),
9438 return_type: None,
9439 link_target: None,
9440 link_multi: false,
9441 },
9442 ];
9443 schema
9444 }
9445
9446 #[test]
9447 fn test_path_traverses_through_a_function_backed_computed() {
9448 let schema = make_schema_with_object_fn_computed();
9452 let out = compile_and_emit_with("SELECT Person { t := .recent.title }", &schema);
9453 assert!(
9454 out.sql
9455 .contains("CROSS JOIN LATERAL \"public\".\"latest\"(\"t1\".\"id\") AS \"t2\""),
9456 "{}",
9457 out.sql
9458 );
9459 assert!(out.sql.contains("ARRAY(SELECT \"t2\".\"title\""), "{}", out.sql);
9461 }
9462
9463 #[test]
9464 fn test_sub_select_over_a_function_backed_computed() {
9465 let schema = make_schema_with_object_fn_computed();
9468 let out = compile_and_emit_with(
9469 "SELECT Person { t := (select .recent filter .title = 'x' limit 1).title }",
9470 &schema,
9471 );
9472 assert!(
9473 out.sql.contains("CROSS JOIN LATERAL \"public\".\"latest\""),
9474 "{}",
9475 out.sql
9476 );
9477 assert!(out.sql.contains("\"t2\".\"title\" = 'x'"), "{}", out.sql);
9478 assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
9479 assert!(!out.sql.contains("ARRAY("), "{}", out.sql);
9480 }
9481
9482 #[test]
9483 fn test_function_backed_computed_carries_its_own_filter() {
9484 let schema = make_schema_with_object_fn_computed();
9485 let out = compile_and_emit_with(
9486 "SELECT Person { t := (select .vetted filter .title = 'x' limit 1).title }",
9487 &schema,
9488 );
9489 assert!(
9490 out.sql
9491 .contains("(\"t2\".\"title\" = 'x') AND (\"t2\".\"title\" <> '')"),
9492 "{}",
9493 out.sql
9494 );
9495 }
9496
9497 #[test]
9498 fn test_function_backed_computed_checks_its_argument_count() {
9499 use crate::schema::ComputedDescriptor;
9500 let mut schema = make_schema_with_object_fn_computed();
9501 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
9502 person.computed = vec![ComputedDescriptor {
9503 name: "recent".into(),
9504 expression: "latest()".into(),
9505 return_type: None,
9506 link_target: None,
9507 link_multi: false,
9508 }];
9509 let ast = parse::parse("SELECT Person { t := .recent.title }").unwrap();
9510 let err = match ir::compile(&ast, &schema) {
9511 Ok(_) => panic!("expected a compile error"),
9512 Err(e) => format!("{e}"),
9513 };
9514 assert!(err.contains("expects 1 argument(s), got 0"), "{err}");
9515 }
9516
9517 #[test]
9518 fn test_path_traverses_through_a_computed_pointer() {
9519 let schema = make_schema_with_computed_links();
9522 let out = compile_and_emit_with("SELECT Person { t := .published.title }", &schema);
9523 assert!(out.sql.contains("\"public\".\"Person.posts\""), "{}", out.sql);
9524 assert!(
9525 out.sql
9526 .contains("(\"t1\".\"id\" = \"t0\".\"id\") AND (\"t2\".\"title\" <> '')"),
9527 "{}",
9528 out.sql
9529 );
9530 }
9531
9532 #[test]
9533 fn test_sub_select_over_a_computed_pointer_projects_a_property() {
9534 let schema = make_schema_with_computed_links();
9537 let out = compile_and_emit_with(
9538 "SELECT Person { t := (select .published filter .title = 'a' limit 1).title }",
9539 &schema,
9540 );
9541 assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
9542 assert!(out.sql.contains("\"t2\".\"title\" = 'a'"), "{}", out.sql);
9544 assert!(out.sql.contains("\"t2\".\"title\" <> ''"), "{}", out.sql);
9545 }
9546
9547 #[test]
9548 fn test_chained_computed_pointers_each_contribute_their_filter() {
9549 let schema = make_schema_with_computed_links();
9550 let out = compile_and_emit_with("SELECT Person { t := .featured.title }", &schema);
9551 assert_eq!(out.sql.matches("\"public\".\"Person.posts\"").count(), 1, "{}", out.sql);
9552 assert!(out.sql.contains("\"t2\".\"title\" <> 'draft'"), "{}", out.sql);
9553 assert!(out.sql.contains("\"t2\".\"title\" <> ''"), "{}", out.sql);
9554 }
9555
9556 #[test]
9557 fn test_computed_pointer_naming_a_bare_link_is_traversable() {
9558 let schema = make_schema_with_computed_links();
9559 let out = compile_and_emit_with("SELECT Person { t := .plain.title }", &schema);
9560 assert!(out.sql.contains("ARRAY(SELECT \"t2\".\"title\""), "{}", out.sql);
9561 }
9562
9563 #[test]
9564 fn test_a_cycle_of_computed_pointers_is_reported_not_hung_on() {
9565 let schema = make_schema_with_computed_links();
9566 let ast = parse::parse("SELECT Person { t := .looper.title }").unwrap();
9567 let err = match ir::compile(&ast, &schema) {
9568 Ok(_) => panic!("expected a compile error"),
9569 Err(e) => format!("{e}"),
9570 };
9571 assert!(err.contains("expands into itself"), "{err}");
9572 }
9573
9574 #[test]
9575 fn test_a_cycle_of_computed_pointers_read_as_a_value_is_reported_not_hung_on() {
9576 let schema = make_schema_with_computed_links();
9579 for query in ["SELECT Person { looper }", "SELECT Person { * }"] {
9580 let ast = parse::parse(query).unwrap();
9581 let err = match ir::compile(&ast, &schema) {
9582 Ok(_) => panic!("expected a compile error for {query}"),
9583 Err(e) => format!("{e}"),
9584 };
9585 assert!(err.contains("expands into itself"), "{query}: {err}");
9586 }
9587 }
9588
9589 #[test]
9590 fn test_traversing_through_a_limited_computed_uses_a_lateral() {
9591 let schema = make_schema_with_computed_links();
9595 let out = compile_and_emit_with("SELECT Person { t := .capped.title }", &schema);
9596 assert!(out.sql.contains("JOIN LATERAL ("), "{}", out.sql);
9597 assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
9598 assert!(!out.sql.contains("ARRAY(SELECT"), "{}", out.sql);
9599 }
9600
9601 #[test]
9602 fn test_backlink_from_a_type_whose_interface_the_link_targets() {
9603 use crate::schema::{LinkDescriptor, TypeDescriptor};
9604 let mut schema = make_interface_schema();
9605 let individual = schema.types.iter().find(|t| t.name == "Individual").unwrap().clone();
9606 schema.types.push(TypeDescriptor {
9607 name: "Note".into(),
9608 module: "default".into(),
9609 table: "Note".into(),
9610 abstract_: false,
9611 materialized: true,
9612 description: None,
9613 parents: vec![],
9614 interfaces: vec![],
9615 bases: vec![],
9616 properties: individual.properties[..1].to_vec(),
9617 links: vec![LinkDescriptor {
9619 name: "owner".into(),
9620 target: "default::Account".into(),
9621 nullable: true,
9622 through: None,
9623 description: None,
9624 default_pyql: None,
9625 is_exclusive: false,
9626 is_readonly: false,
9627 rewrites: vec![],
9628 on_delete: vec![],
9629 }],
9630 multilinks: vec![],
9631 computed: vec![],
9632 constraints: vec![],
9633 indexes: vec![],
9634 partition: None,
9635 vector_indexes: vec![],
9636 search_indexes: vec![],
9637 triggers: vec![],
9638 junction: false,
9639 signals: vec![],
9640 });
9641 let out = compile_and_emit_with("SELECT Individual { notes := .<owner[is Note] }", &schema);
9644 assert!(out.sql.contains("\"public\".\"Note\""), "{}", out.sql);
9645 assert!(out.sql.contains("\"owner_id\" = \"t0\".id"), "{}", out.sql);
9646 }
9647
9648 #[test]
9649 fn test_with_block_in_expression_position_hoists_its_bindings() {
9650 let out = compile_and_emit("SELECT Person { t := (with c := (select Company.name limit 1) select c) }");
9653 assert!(out.sql.starts_with("WITH\n\"c\" AS ("), "{}", out.sql);
9654 assert!(out.sql.contains("(SELECT \"v\" FROM \"c\")"), "{}", out.sql);
9655 }
9656
9657 #[test]
9658 fn test_hoisted_binding_is_usable_in_the_expression_around_it() {
9659 let out =
9660 compile_and_emit("SELECT Person { t := (with c := (select Company.name limit 1) select .name ++ c) }");
9661 assert!(
9662 out.sql.contains("(\"t0\".\"name\" || (SELECT \"v\" FROM \"c\"))"),
9663 "{}",
9664 out.sql
9665 );
9666 }
9667
9668 #[test]
9669 fn test_hoisted_binding_joins_the_statements_own_with_clause() {
9670 let out = compile_and_emit(
9671 "WITH top := (select Person.name limit 1) \
9672 SELECT Person { t := (with c := (select Company.name limit 1) select c) }",
9673 );
9674 let top = out.sql.find("\"top\" AS (").expect("top-level binding");
9675 let inner = out.sql.find("\"c\" AS (").expect("hoisted binding");
9676 assert!(top < inner, "the statement's own bindings come first:\n{}", out.sql);
9677 }
9678
9679 #[test]
9680 fn test_a_binding_named_after_a_keyword_is_usable() {
9681 let out = compile_and_emit("WITH order := (select Person.name) SELECT Person { name } FILTER .name = order");
9686 assert!(out.sql.contains("(SELECT \"v\" FROM \"order\")"), "{}", out.sql);
9687 }
9688
9689 #[test]
9690 fn test_computed_declared_on_an_interface_is_visible_from_an_implementor() {
9691 use crate::schema::ComputedDescriptor;
9692 let mut schema = make_interface_schema();
9693 let account = schema.types.iter_mut().find(|t| t.name == "Account").unwrap();
9694 account.computed = vec![ComputedDescriptor {
9695 name: "tier".into(),
9696 expression: "'gold'".into(),
9697 return_type: None,
9698 link_target: None,
9699 link_multi: false,
9700 }];
9701 let individual = schema.types.iter_mut().find(|t| t.name == "Individual").unwrap();
9702 individual.computed = vec![];
9703 for q in ["SELECT Individual { tier }", "SELECT Individual { t := .tier }"] {
9706 let out = compile_and_emit_with(q, &schema);
9707 assert!(out.sql.contains("'gold'"), "{q}: {}", out.sql);
9708 }
9709 }
9710
9711 #[test]
9712 fn test_computed_may_carry_trailing_modifiers_without_a_select() {
9713 use crate::schema::ComputedDescriptor;
9714 let mut schema = make_schema();
9715 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
9716 person.computed = vec![ComputedDescriptor {
9717 name: "recent".into(),
9718 expression: ".posts order by .title desc limit 5".into(),
9719 return_type: None,
9720 link_target: None,
9721 link_multi: false,
9722 }];
9723 let out = compile_and_emit_with("SELECT Person { recent { title } }", &schema);
9726 assert!(out.sql.contains("LIMIT 5"), "{}", out.sql);
9727 assert!(out.sql.contains("ORDER BY \"t1\".\"title\" DESC"), "{}", out.sql);
9728 }
9729
9730 #[test]
9731 fn test_computed_may_lead_with_a_bare_select() {
9732 use crate::schema::ComputedDescriptor;
9733 let mut schema = make_schema();
9734 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
9735 person.computed = vec![ComputedDescriptor {
9736 name: "recent".into(),
9737 expression: "select .posts order by .title desc limit 5".into(),
9738 return_type: None,
9739 link_target: None,
9740 link_multi: false,
9741 }];
9742 let out = compile_and_emit_with("SELECT Person { recent { title } }", &schema);
9743 assert!(out.sql.contains("LIMIT 5"), "{}", out.sql);
9744 }
9745
9746 #[test]
9747 fn test_sub_select_with_a_shape_may_project_a_property_off_it() {
9748 let out = compile_and_emit("SELECT Person { t := (select .posts { title } limit 1).title }");
9749 assert!(out.sql.contains("\"t2\".\"title\""), "{}", out.sql);
9750 assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
9751 }
9752
9753 #[test]
9754 fn test_sub_statement_error_names_what_it_got() {
9755 let err = compile_err("SELECT Person { t := (insert Company { name := 'a' }) }");
9756 assert!(err.contains("an insert cannot stand in for a value"), "{err}");
9757 }
9758
9759 #[test]
9760 fn test_with_bound_scalar_path_select_emits_a_real_select() {
9761 let out = compile_and_emit("WITH xs := (select Person.name) SELECT Person { name }");
9764 assert!(
9765 out.sql
9766 .contains("SELECT ROW(v) AS result, v FROM (SELECT \"t0\".\"name\" AS v"),
9767 "{}",
9768 out.sql
9769 );
9770 }
9771
9772 #[test]
9773 fn test_with_bound_scalar_path_select_is_typed_as_its_result() {
9774 let out = compile_and_emit("WITH xs := (select Person.name) SELECT Person { name } FILTER .name IN xs");
9777 assert!(out.sql.contains("ANY((SELECT \"v\" FROM \"xs\"))"), "{}", out.sql);
9778 }
9779
9780 #[test]
9781 fn test_distinct_survives_a_with_binding() {
9782 let out = compile_and_emit("WITH xs := (select distinct Person.name) SELECT Person { name }");
9783 assert!(out.sql.contains("SELECT DISTINCT \"t0\".\"name\" AS v"), "{}", out.sql);
9784 }
9785
9786 #[test]
9787 fn test_for_over_a_derived_set_iterates_every_row() {
9788 let out = compile_and_emit("FOR x IN (select Person.name) UNION (SELECT Person { name } FILTER .name = x)");
9791 assert!(!out.sql.contains("VALUES"), "{}", out.sql);
9792 assert!(out.sql.contains(") AS \"_for_x\""), "{}", out.sql);
9793 assert!(out.sql.contains("CROSS JOIN LATERAL"), "{}", out.sql);
9794 assert!(out.sql.contains("\"t1\".\"name\" = \"_for_x\".\"v\""), "{}", out.sql);
9795 }
9796
9797 #[test]
9798 fn test_for_insert_over_a_derived_set_uses_it_as_the_cte() {
9799 let out = compile_and_emit("FOR x IN (select Person.name) UNION (INSERT Company { name := x })");
9800 assert!(out.sql.starts_with("WITH \"_for_x\" AS ("), "{}", out.sql);
9801 assert!(
9802 out.sql.contains("SELECT \"_for_x\".\"v\" FROM \"_for_x\""),
9803 "{}",
9804 out.sql
9805 );
9806 }
9807
9808 #[test]
9809 fn test_for_over_objects_binds_their_id() {
9810 let out = compile_and_emit("FOR p IN (select Person) UNION (INSERT Company { name := <str>p })");
9811 assert!(out.sql.contains("SELECT \"id\" AS v FROM ("), "{}", out.sql);
9812 }
9813
9814 #[test]
9815 fn test_for_over_a_non_select_statement_is_rejected() {
9816 let ast = parse::parse("FOR x IN (INSERT Company { name := 'a' }) UNION (SELECT Person { name })").unwrap();
9817 let schema = make_schema();
9818 let err = match ir::compile(&ast, &schema) {
9819 Ok(_) => panic!("expected a compile error"),
9820 Err(e) => format!("{e}"),
9821 };
9822 assert!(err.contains("only a select can be iterated"), "{err}");
9823 }
9824
9825 #[test]
9826 fn test_multi_link_path_outside_a_comparison_is_an_array() {
9827 let out = compile_and_emit("SELECT Person { t := .posts.title }");
9830 assert!(out.sql.contains("ARRAY(SELECT \"t2\".\"title\""), "{}", out.sql);
9831 assert!(out.sql.contains("(\"t1\".\"id\" = \"t0\".\"id\")"), "{}", out.sql);
9832 }
9833
9834 #[test]
9835 fn test_multi_link_path_inside_a_comparison_is_still_exists() {
9836 let out = compile_and_emit("SELECT Person { name } filter .posts.title = 'x'");
9837 assert!(out.sql.contains("WHERE EXISTS("), "{}", out.sql);
9838 assert!(!out.sql.contains("ARRAY("), "{}", out.sql);
9839 }
9840
9841 #[test]
9842 fn test_path_traversal_deeper_than_two_steps() {
9843 let schema = make_schema_with_through();
9844 let out = compile_and_emit_with("SELECT Person { t := .friends.friends.name }", &schema);
9845 assert_eq!(out.sql.matches("\"public\".\"PersonFriend\"").count(), 2, "{}", out.sql);
9847 assert!(out.sql.contains("ARRAY(SELECT \"t4\".\"name\""), "{}", out.sql);
9848 }
9849
9850 #[test]
9851 fn test_coalesce_over_backlinked_values_is_a_set_choice() {
9852 let out = compile_and_emit("SELECT Post ORDER BY max(.<posts[is Person].age ?? .<posts[is Person].age)");
9855 assert!(!out.sql.contains("EXISTS"), "{}", out.sql);
9856 assert!(!out.sql.contains("COALESCE("), "{}", out.sql);
9857 assert!(out.sql.contains("CASE WHEN (cardinality("), "{}", out.sql);
9858 assert!(
9859 out.sql
9860 .contains("(SELECT coalesce(max(\"_s\".\"v\"), NULL) FROM unnest("),
9861 "{}",
9862 out.sql
9863 );
9864 }
9865
9866 #[test]
9867 fn test_coalesce_over_multilink_values_is_a_set_choice() {
9868 let out = compile_and_emit("SELECT Person { t := .posts.title ?? .posts.title }");
9869 assert!(!out.sql.contains("EXISTS"), "{}", out.sql);
9870 assert!(out.sql.contains("CASE WHEN (cardinality("), "{}", out.sql);
9871 }
9872
9873 #[test]
9874 fn test_coalesce_of_a_set_with_a_single_value_falls_back_to_it() {
9875 let out = compile_and_emit("SELECT Post { a := .<posts[is Person].age ?? 0 }");
9876 assert!(out.sql.contains("ELSE array_remove(ARRAY[0], NULL) END"), "{}", out.sql);
9877 }
9878
9879 #[test]
9880 fn test_traversal_after_a_backlink() {
9881 let out = compile_and_emit("SELECT Post { t := .<posts[is Person].name }");
9882 assert!(out.sql.contains("ARRAY(SELECT \"t2\".\"name\""), "{}", out.sql);
9883 assert!(out.sql.contains("\"t3\".\"target\" = \"t1\".\"id\""), "{}", out.sql);
9884 }
9885
9886 #[test]
9887 fn test_computed_pointer_on_a_linked_type_is_reachable() {
9888 use crate::schema::ComputedDescriptor;
9889 let mut schema = make_schema();
9890 let company = schema.types.iter_mut().find(|t| t.name == "Company").unwrap();
9891 company.computed = vec![ComputedDescriptor {
9892 name: "shout".into(),
9893 expression: ".name ++ '!'".into(),
9894 return_type: Some("text".into()),
9895 link_target: None,
9896 link_multi: false,
9897 }];
9898 let out = compile_and_emit_with("SELECT Person { t := .company.shout }", &schema);
9902 assert!(out.sql.contains("(\"t2\".\"name\" || '!')"), "{}", out.sql);
9903 }
9904
9905 #[test]
9906 fn test_computed_pointer_behind_a_type_intersection() {
9907 let schema = make_interface_schema();
9908 let out = compile_and_emit_with("SELECT Account { n := [is Individual].full_name }", &schema);
9909 assert!(out.sql.contains("upper(\"t1\".\"first_name\")"), "{}", out.sql);
9910 assert!(out.sql.contains("FROM \"public\".\"Individual\""), "{}", out.sql);
9911 assert!(out.sql.contains("(\"t1\".\"id\" = \"t0\".\"id\")"), "{}", out.sql);
9912 }
9913
9914 #[test]
9915 fn test_computed_pointer_cannot_be_traversed_through() {
9916 use crate::schema::ComputedDescriptor;
9917 let mut schema = make_schema();
9918 let company = schema.types.iter_mut().find(|t| t.name == "Company").unwrap();
9919 company.computed = vec![ComputedDescriptor {
9920 name: "shout".into(),
9921 expression: ".name ++ '!'".into(),
9922 return_type: Some("text".into()),
9923 link_target: None,
9924 link_multi: false,
9925 }];
9926 let ast = parse::parse("SELECT Person { t := .company.shout.nope }").unwrap();
9927 let err = match ir::compile(&ast, &schema) {
9928 Ok(_) => panic!("expected a compile error"),
9929 Err(e) => format!("{e}"),
9930 };
9931 assert!(err.contains("is a computed pointer"), "{err}");
9932 }
9933
9934 #[test]
9935 fn test_sub_select_over_a_multi_link_path_keeps_its_modifiers() {
9936 let out = compile_and_emit("SELECT Person { t := (select .posts.title order by .title desc limit 2) }");
9937 assert!(out.sql.contains("ARRAY(SELECT"), "{}", out.sql);
9938 assert!(out.sql.contains("ORDER BY \"t2\".\"title\" DESC"), "{}", out.sql);
9939 assert!(out.sql.contains("LIMIT 2"), "{}", out.sql);
9940 }
9941
9942 #[test]
9943 fn test_limit_one_over_a_multi_link_path_stays_a_scalar() {
9944 let out = compile_and_emit("SELECT Person { t := (select .posts.title limit 1) }");
9947 assert!(!out.sql.contains("ARRAY("), "{}", out.sql);
9948 assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
9949 }
9950
9951 #[test]
9952 fn test_computed_multilink_can_carry_a_nested_shape() {
9953 let out = compile_and_emit("SELECT Person { p := .posts { title } }");
9954 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
9955 panic!()
9956 };
9957 let ShapeNode::Array { name, element, .. } = &pointers[2] else {
9958 panic!("{:?}", pointers[2])
9959 };
9960 assert_eq!(name, "p");
9961 let ShapeNode::Object {
9962 pointers: elem_pointers,
9963 ..
9964 } = element.as_ref()
9965 else {
9966 panic!()
9967 };
9968 assert!(matches!(&elem_pointers[1], ShapeNode::Scalar { name, .. } if name == "title"));
9969 }
9970
9971 #[test]
9972 fn test_schema_declared_backlink_computed_is_an_object_pointer() {
9973 use crate::schema::ComputedDescriptor;
9974 let mut schema = make_schema();
9975 let post = schema.types.iter_mut().find(|t| t.name == "Post").unwrap();
9976 post.computed = vec![ComputedDescriptor {
9977 name: "authors".into(),
9978 expression: ".<posts[is Person]".into(),
9979 return_type: None,
9980 link_target: None,
9981 link_multi: false,
9982 }];
9983 let out = compile_and_emit_with("SELECT Post { authors { name } }", &schema);
9986 assert!(out.sql.contains("array_agg(ROW("), "{}", out.sql);
9987 assert!(out.sql.contains("\"t1\".\"name\"::text"), "{}", out.sql);
9988 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
9989 panic!()
9990 };
9991 assert!(matches!(&pointers[1], ShapeNode::Array { name, .. } if name == "authors"));
9992 }
9993
9994 #[test]
9995 fn test_schema_declared_multilink_computed_defaults_to_ids_without_a_shape() {
9996 use crate::schema::ComputedDescriptor;
9997 let mut schema = make_schema();
9998 schema.types[0].computed = vec![ComputedDescriptor {
9999 name: "everything".into(),
10000 expression: ".posts".into(),
10001 return_type: None,
10002 link_target: None,
10003 link_multi: false,
10004 }];
10005 let out = compile_and_emit_with("SELECT Person { everything }", &schema);
10006 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
10007 panic!()
10008 };
10009 assert!(matches!(&pointers[2], ShapeNode::Array { name, .. } if name == "everything"));
10010 }
10011
10012 #[test]
10013 fn test_schema_declared_computed_sub_select_is_a_link_pointer() {
10014 use crate::schema::ComputedDescriptor;
10015 let mut schema = make_schema();
10016 schema.types[0].computed = vec![
10017 ComputedDescriptor {
10018 name: "recent".into(),
10019 expression: "(select .posts order by .title desc limit 1)".into(),
10020 return_type: None,
10021 link_target: None,
10022 link_multi: false,
10023 },
10024 ComputedDescriptor {
10025 name: "recent_title".into(),
10026 expression: "(select .posts order by .title desc limit 1).title".into(),
10027 return_type: Some("text".into()),
10028 link_target: None,
10029 link_multi: false,
10030 },
10031 ];
10032 let out = compile_and_emit_with("SELECT Person { recent { title }, recent_title }", &schema);
10033 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
10034 panic!()
10035 };
10036 assert!(matches!(&pointers[2], ShapeNode::Object { name, .. } if name == "recent"));
10039 assert!(matches!(&pointers[3], ShapeNode::Scalar { name, .. } if name == "recent_title"));
10040 assert!(out.sql.contains("\"jt\".source = \"t0\".id"), "{}", out.sql);
10041 assert_eq!(out.sql.matches("LIMIT 1").count(), 2, "{}", out.sql);
10042 crate::validate::validate_schema_types(&schema).expect("schema should validate");
10043 }
10044
10045 #[test]
10050 fn test_a_mutation_read_through_a_path_runs_as_a_hoisted_cte() {
10051 let out = compile_and_emit("SELECT (INSERT Person { name := 'x' }).name");
10052 assert!(
10053 out.sql.contains("\"_nested_dml_0\" AS (") && out.sql.contains("INSERT INTO"),
10054 "the mutation must run as a CTE:\n{}",
10055 out.sql
10056 );
10057 assert!(
10058 out.sql.contains("FROM \"_nested_dml_0\""),
10059 "the projection must read the mutation's own rows:\n{}",
10060 out.sql
10061 );
10062 }
10063
10064 #[test]
10065 fn test_dml_sub_statement_in_expression_position_still_rejected() {
10066 let err = compile_err("SELECT Person { x := (insert Company { name := 'c' }).name }");
10067 assert!(err.contains("an insert cannot stand in for a value"), "{err}");
10068 }
10069
10070 #[test]
10071 fn test_sub_select_with_a_shape_in_expression_position_is_rejected() {
10072 let err = compile_err("SELECT Person { name } filter (select .posts { title }) = 1");
10076 assert!(err.contains("sub-select with a shape"), "{err}");
10077 }
10078
10079 fn make_schema_with_through() -> SchemaDescriptor {
10080 let id_prop = || PropertyDescriptor {
10081 name: "id".into(),
10082 pg_type: "uuid".into(),
10083 nullable: false,
10084 default_sql: Some("gen_random_uuid()".into()),
10085 description: None,
10086 default_pyql: None,
10087 check_constraints: vec![],
10088 is_exclusive: true,
10089 is_pk: true,
10090 is_readonly: true,
10091 rewrites: vec![],
10092 tuple_members: None,
10093 column_type: None,
10094 };
10095 let name_prop = || PropertyDescriptor {
10096 name: "name".into(),
10097 pg_type: "text".into(),
10098 nullable: false,
10099 default_sql: None,
10100 description: None,
10101 check_constraints: vec![],
10102 default_pyql: None,
10103 is_exclusive: false,
10104 is_pk: false,
10105 is_readonly: false,
10106 rewrites: vec![],
10107 tuple_members: None,
10108 column_type: None,
10109 };
10110 SchemaDescriptor {
10111 types: vec![
10112 TypeDescriptor {
10113 name: "Person".into(),
10114 module: "default".into(),
10115 table: "Person".into(),
10116 abstract_: false,
10117 materialized: false,
10118 description: None,
10119 parents: vec![],
10120 interfaces: vec![],
10121 bases: vec![],
10122 properties: vec![id_prop(), name_prop()],
10123 links: vec![],
10124 multilinks: vec![MultiLinkDescriptor {
10125 name: "friends".into(),
10126 target: "default::Person".into(),
10127 through: Some("default::PersonFriend".into()),
10128 nullable: false,
10129 description: None,
10130 default_pyql: None,
10131 on_delete: vec![],
10132 is_exclusive: false,
10133 }],
10134 computed: vec![],
10135 constraints: vec![],
10136 indexes: vec![],
10137 partition: None,
10138 vector_indexes: vec![],
10139 search_indexes: vec![],
10140 triggers: vec![],
10141 junction: false,
10142 signals: vec![],
10143 },
10144 TypeDescriptor {
10145 name: "PersonFriend".into(),
10146 module: "default".into(),
10147 table: "PersonFriend".into(),
10148 abstract_: false,
10149 materialized: false,
10150 description: None,
10151 parents: vec![],
10152 interfaces: vec![],
10153 bases: vec![],
10154 properties: vec![id_prop()],
10155 links: vec![
10156 LinkDescriptor {
10157 name: "person".into(),
10158 target: "default::Person".into(),
10159 nullable: false,
10160 through: None,
10161 description: None,
10162 default_pyql: None,
10163 is_exclusive: false,
10164 is_readonly: false,
10165 rewrites: vec![],
10166 on_delete: vec![],
10167 },
10168 LinkDescriptor {
10169 name: "friend".into(),
10170 target: "default::Person".into(),
10171 nullable: false,
10172 through: None,
10173 description: None,
10174 default_pyql: None,
10175 is_exclusive: false,
10176 is_readonly: false,
10177 rewrites: vec![],
10178 on_delete: vec![],
10179 },
10180 ],
10181 multilinks: vec![],
10182 computed: vec![],
10183 constraints: vec![],
10184 indexes: vec![],
10185 partition: None,
10186 vector_indexes: vec![],
10187 search_indexes: vec![],
10188 triggers: vec![],
10189 junction: false,
10190 signals: vec![],
10191 },
10192 ],
10193 scalars: vec![],
10194 enums: vec![],
10195 named_tuples: vec![],
10196 globals: vec![],
10197 functions: vec![],
10198 aliases: vec![],
10199 channels: vec![],
10200 ..Default::default()
10201 }
10202 }
10203
10204 #[test]
10205 fn test_select_through_multi_link() {
10206 let schema = make_schema_with_through();
10207 let ast = crate::parse::parse("SELECT Person { name, friends { name } }").unwrap();
10208 let ir = crate::ir::compile(&ast, &schema).unwrap();
10209 let out = emit(&ir);
10210 assert!(out.sql.contains("\"public\".\"PersonFriend\""));
10212 assert!(out.sql.contains("\"friend\""));
10214 assert!(out.sql.contains("\"person\""));
10215 assert!(out.sql.contains("array_agg(ROW("));
10217 }
10218
10219 #[test]
10225 fn test_limited_multi_link_keeps_a_nested_multi_link_in_scope() {
10226 let schema = make_schema_with_through();
10229 let ast = crate::parse::parse("SELECT Person { friends: { name, friends { name } } limit 1 }").unwrap();
10230 let ir = crate::ir::compile(&ast, &schema).unwrap();
10231 let out = emit(&ir);
10232 assert_eq!(out.sql.matches("array_agg(ROW(").count(), 2, "{}", out.sql);
10233 let derived = out.sql.find("AS \"__lim\"").unwrap();
10234 let nested = out.sql.rfind("array_agg(ROW(").unwrap();
10235 assert!(
10236 nested < derived,
10237 "nested aggregate must sit inside the derived table:\n{}",
10238 out.sql
10239 );
10240 }
10241
10242 fn make_schema_with_junction_backed_link() -> SchemaDescriptor {
10243 let id_prop = || PropertyDescriptor {
10244 name: "id".into(),
10245 pg_type: "uuid".into(),
10246 nullable: false,
10247 default_sql: Some("gen_random_uuid()".into()),
10248 description: None,
10249 default_pyql: None,
10250 check_constraints: vec![],
10251 is_exclusive: true,
10252 is_pk: true,
10253 is_readonly: true,
10254 rewrites: vec![],
10255 tuple_members: None,
10256 column_type: None,
10257 };
10258 let name_prop = || PropertyDescriptor {
10259 name: "name".into(),
10260 pg_type: "text".into(),
10261 nullable: false,
10262 default_sql: None,
10263 description: None,
10264 check_constraints: vec![],
10265 default_pyql: None,
10266 is_exclusive: false,
10267 is_pk: false,
10268 is_readonly: false,
10269 rewrites: vec![],
10270 tuple_members: None,
10271 column_type: None,
10272 };
10273 SchemaDescriptor {
10274 types: vec![
10275 TypeDescriptor {
10276 name: "Person".into(),
10277 module: "default".into(),
10278 table: "Person".into(),
10279 abstract_: false,
10280 materialized: false,
10281 description: None,
10282 parents: vec![],
10283 interfaces: vec![],
10284 bases: vec![],
10285 properties: vec![id_prop(), name_prop()],
10286 links: vec![LinkDescriptor {
10287 name: "spouse".into(),
10288 target: "default::Org".into(),
10289 nullable: true,
10290 through: Some("default::Marriage".into()),
10291 description: None,
10292 default_pyql: None,
10293 is_exclusive: true,
10294 is_readonly: false,
10295 rewrites: vec![],
10296 on_delete: vec![],
10297 }],
10298 multilinks: vec![],
10299 computed: vec![],
10300 constraints: vec![],
10301 indexes: vec![],
10302 partition: None,
10303 vector_indexes: vec![],
10304 search_indexes: vec![],
10305 triggers: vec![],
10306 junction: false,
10307 signals: vec![],
10308 },
10309 TypeDescriptor {
10310 name: "Org".into(),
10311 module: "default".into(),
10312 table: "Org".into(),
10313 abstract_: false,
10314 materialized: false,
10315 description: None,
10316 parents: vec![],
10317 interfaces: vec![],
10318 bases: vec![],
10319 properties: vec![id_prop(), name_prop()],
10320 links: vec![],
10321 multilinks: vec![],
10322 computed: vec![],
10323 constraints: vec![],
10324 indexes: vec![],
10325 partition: None,
10326 vector_indexes: vec![],
10327 search_indexes: vec![],
10328 triggers: vec![],
10329 junction: false,
10330 signals: vec![],
10331 },
10332 TypeDescriptor {
10333 name: "Marriage".into(),
10334 module: "default".into(),
10335 table: "Person.spouse".into(),
10336 abstract_: false,
10337 materialized: false,
10338 description: None,
10339 parents: vec![],
10340 interfaces: vec![],
10341 bases: vec![],
10342 properties: vec![
10343 id_prop(),
10344 PropertyDescriptor {
10345 name: "since".into(),
10346 pg_type: "int8".into(),
10347 nullable: true,
10348 default_sql: None,
10349 description: None,
10350 check_constraints: vec![],
10351 default_pyql: None,
10352 is_exclusive: false,
10353 is_pk: false,
10354 is_readonly: false,
10355 rewrites: vec![],
10356 tuple_members: None,
10357 column_type: None,
10358 },
10359 ],
10360 links: vec![
10361 LinkDescriptor {
10362 name: "source".into(),
10363 target: "default::Person".into(),
10364 nullable: false,
10365 through: None,
10366 description: None,
10367 default_pyql: None,
10368 is_exclusive: false,
10369 is_readonly: false,
10370 rewrites: vec![],
10371 on_delete: vec![],
10372 },
10373 LinkDescriptor {
10374 name: "target".into(),
10375 target: "default::Org".into(),
10376 nullable: false,
10377 through: None,
10378 description: None,
10379 default_pyql: None,
10380 is_exclusive: false,
10381 is_readonly: false,
10382 rewrites: vec![],
10383 on_delete: vec![],
10384 },
10385 ],
10386 multilinks: vec![],
10387 computed: vec![],
10388 constraints: vec![],
10389 indexes: vec![],
10390 partition: None,
10391 vector_indexes: vec![],
10392 search_indexes: vec![],
10393 triggers: vec![],
10394 junction: true,
10395 signals: vec![],
10396 },
10397 ],
10398 scalars: vec![],
10399 enums: vec![],
10400 named_tuples: vec![],
10401 globals: vec![],
10402 functions: vec![],
10403 aliases: vec![],
10404 channels: vec![],
10405 ..Default::default()
10406 }
10407 }
10408
10409 #[test]
10410 fn test_select_shape_over_junction_backed_single_link() {
10411 let schema = make_schema_with_junction_backed_link();
10412 let ast = crate::parse::parse("SELECT Person { name, spouse { name } }").unwrap();
10413 let ir = crate::ir::compile(&ast, &schema).unwrap();
10414 let out = emit(&ir);
10415 assert!(out.sql.contains("\"public\".\"Person.spouse\""), "got:\n{}", out.sql);
10417 assert!(out.sql.contains("\"jt\".source"), "got:\n{}", out.sql);
10418 assert!(out.sql.contains("\"jt\".target"), "got:\n{}", out.sql);
10419 assert!(!out.sql.contains("array_agg"), "got:\n{}", out.sql);
10421 assert!(!out.sql.contains("spouse_id"), "got:\n{}", out.sql);
10422 }
10423
10424 #[test]
10425 fn test_select_shape_over_junction_backed_single_link_with_link_property() {
10426 let schema = make_schema_with_junction_backed_link();
10432 let ast = crate::parse::parse("SELECT Person { name, spouse { name, @since } }").unwrap();
10433 let ir = crate::ir::compile(&ast, &schema).unwrap();
10434 let out = emit(&ir);
10435 assert!(out.sql.contains("\"jt\".\"since\""), "got:\n{}", out.sql);
10436 assert!(!out.sql.contains("array_agg"), "got:\n{}", out.sql);
10437 }
10438
10439 #[test]
10440 fn test_select_path_over_junction_backed_single_link() {
10441 let schema = make_schema_with_junction_backed_link();
10442 let ast = crate::parse::parse("SELECT Person.spouse { name }").unwrap();
10443 let ir = crate::ir::compile(&ast, &schema).unwrap();
10444 let out = emit(&ir);
10445 assert!(out.sql.contains("\"public\".\"Person.spouse\""), "got:\n{}", out.sql);
10446 assert!(out.sql.contains("\"source\""), "got:\n{}", out.sql);
10447 assert!(out.sql.contains("\"target\""), "got:\n{}", out.sql);
10448 assert!(!out.sql.contains("spouse_id"), "got:\n{}", out.sql);
10449 }
10450
10451 #[test]
10452 fn test_insert_with_junction_backed_single_link() {
10453 let schema = make_schema_with_junction_backed_link();
10454 let out = compile_and_emit_with(
10455 "INSERT Person { name := $name, \
10456 spouse := (SELECT Org FILTER .id = $oid) { @since := <int64>$since } }",
10457 &schema,
10458 );
10459 assert!(out.sql.contains("\"public\".\"Person.spouse\""), "got:\n{}", out.sql);
10460 assert!(out.sql.contains("\"since\""), "got:\n{}", out.sql);
10461 assert!(!out.sql.contains("spouse_id"), "got:\n{}", out.sql);
10462 assert!(out.sql.contains("ON CONFLICT (\"source\") DO UPDATE SET \"target\" = EXCLUDED.\"target\", \"since\" = EXCLUDED.\"since\""), "got:\n{}", out.sql);
10469 }
10470
10471 #[test]
10472 fn test_update_replace_junction_backed_single_link() {
10473 let schema = make_schema_with_junction_backed_link();
10474 let out = compile_and_emit_with(
10475 "UPDATE Person FILTER .id = $id SET { \
10476 spouse := (SELECT Org FILTER .id = $oid) { @since := <int64>$since } }",
10477 &schema,
10478 );
10479 assert!(
10481 out.sql.contains("DELETE FROM \"public\".\"Person.spouse\""),
10482 "got:\n{}",
10483 out.sql
10484 );
10485 assert!(
10486 out.sql.contains("INSERT INTO \"public\".\"Person.spouse\""),
10487 "got:\n{}",
10488 out.sql
10489 );
10490 assert!(out.sql.contains("ON CONFLICT (\"source\") DO UPDATE SET \"target\" = EXCLUDED.\"target\", \"since\" = EXCLUDED.\"since\""), "got:\n{}", out.sql);
10491 assert!(out.sql.contains("NOT IN"), "got:\n{}", out.sql);
10496 }
10497
10498 #[test]
10503 fn test_unless_conflict_else_update_keeps_its_filter() {
10504 let out = compile_and_emit(
10505 "INSERT Person { name := 'a' } UNLESS CONFLICT ON .name \
10506 ELSE (UPDATE Person FILTER .age < 30 SET { name := 'b' })",
10507 );
10508 assert!(
10509 out.sql.contains("DO UPDATE SET") && out.sql.contains("WHERE"),
10510 "the ELSE UPDATE's filter must survive as a DO UPDATE predicate:\n{}",
10511 out.sql
10512 );
10513 assert!(
10514 out.sql.contains("\"Person\".\"age\""),
10515 "the predicate must read the existing row, not `excluded`:\n{}",
10516 out.sql
10517 );
10518 }
10519
10520 #[test]
10522 fn test_unless_conflict_else_update_without_a_filter_has_no_predicate() {
10523 let out = compile_and_emit(
10524 "INSERT Person { name := 'a' } UNLESS CONFLICT ON .name ELSE (UPDATE Person SET { name := 'b' })",
10525 );
10526 let after_set = out.sql.split("DO UPDATE SET").nth(1).unwrap_or_default();
10527 assert!(
10528 !after_set.contains("WHERE"),
10529 "got a predicate we never asked for:\n{}",
10530 out.sql
10531 );
10532 }
10533
10534 #[test]
10535 fn test_junction_backed_single_link_rejected_in_unless_conflict_else() {
10536 let schema = make_schema_with_junction_backed_link();
10543 let ast = parse::parse(
10544 "INSERT Person { name := $name } \
10545 UNLESS CONFLICT ON .name ELSE (UPDATE Person SET { \
10546 spouse := (SELECT Org FILTER .id = $oid) })",
10547 )
10548 .unwrap();
10549 assert!(ir::compile(&ast, &schema).is_err());
10550 }
10551
10552 #[test]
10553 fn test_update_clear_junction_backed_single_link() {
10554 let schema = make_schema_with_junction_backed_link();
10555 let out = compile_and_emit_with("UPDATE Person FILTER .id = $id SET { spouse := {} }", &schema);
10556 assert!(
10557 out.sql.contains("DELETE FROM \"public\".\"Person.spouse\""),
10558 "got:\n{}",
10559 out.sql
10560 );
10561 assert!(
10562 !out.sql.contains("INSERT INTO \"public\".\"Person.spouse\""),
10563 "clearing must not also insert:\n{}",
10564 out.sql
10565 );
10566 }
10567
10568 #[test]
10569 fn test_update_clear_junction_backed_single_link_with_cast_empty_set() {
10570 let schema = make_schema_with_junction_backed_link();
10578 let out = compile_and_emit_with("UPDATE Person FILTER .id = $id SET { spouse := <Org>{} }", &schema);
10579 assert!(
10580 out.sql.contains("DELETE FROM \"public\".\"Person.spouse\""),
10581 "got:\n{}",
10582 out.sql
10583 );
10584 assert!(
10585 !out.sql.contains("INSERT INTO \"public\".\"Person.spouse\""),
10586 "clearing must not also insert:\n{}",
10587 out.sql
10588 );
10589 }
10590
10591 fn make_schema_with_through_and_prop() -> SchemaDescriptor {
10599 let id_prop = || PropertyDescriptor {
10600 name: "id".into(),
10601 pg_type: "uuid".into(),
10602 nullable: false,
10603 default_sql: Some("gen_random_uuid()".into()),
10604 description: None,
10605 default_pyql: None,
10606 check_constraints: vec![],
10607 is_exclusive: true,
10608 is_pk: true,
10609 is_readonly: true,
10610 rewrites: vec![],
10611 tuple_members: None,
10612 column_type: None,
10613 };
10614 let name_prop = || PropertyDescriptor {
10615 name: "name".into(),
10616 pg_type: "text".into(),
10617 nullable: false,
10618 default_sql: None,
10619 description: None,
10620 check_constraints: vec![],
10621 default_pyql: None,
10622 is_exclusive: false,
10623 is_pk: false,
10624 is_readonly: false,
10625 rewrites: vec![],
10626 tuple_members: None,
10627 column_type: None,
10628 };
10629 SchemaDescriptor {
10630 types: vec![
10631 TypeDescriptor {
10632 name: "Product".into(),
10633 module: "default".into(),
10634 table: "Product".into(),
10635 abstract_: false,
10636 materialized: false,
10637 description: None,
10638 parents: vec![],
10639 interfaces: vec![],
10640 bases: vec![],
10641 properties: vec![id_prop(), name_prop()],
10642 links: vec![],
10643 multilinks: vec![MultiLinkDescriptor {
10644 name: "tags".into(),
10645 target: "default::Tag".into(),
10646 through: Some("default::ProductTag".into()),
10647 nullable: false,
10648 description: None,
10649 default_pyql: None,
10650 on_delete: vec![],
10651 is_exclusive: false,
10652 }],
10653 computed: vec![],
10654 constraints: vec![],
10655 indexes: vec![],
10656 partition: None,
10657 vector_indexes: vec![],
10658 search_indexes: vec![],
10659 triggers: vec![],
10660 junction: false,
10661 signals: vec![],
10662 },
10663 TypeDescriptor {
10664 name: "Tag".into(),
10665 module: "default".into(),
10666 table: "Tag".into(),
10667 abstract_: false,
10668 materialized: false,
10669 description: None,
10670 parents: vec![],
10671 interfaces: vec![],
10672 bases: vec![],
10673 properties: vec![id_prop(), name_prop()],
10674 links: vec![],
10675 multilinks: vec![],
10676 computed: vec![],
10677 constraints: vec![],
10678 indexes: vec![],
10679 partition: None,
10680 vector_indexes: vec![],
10681 search_indexes: vec![],
10682 triggers: vec![],
10683 junction: false,
10684 signals: vec![],
10685 },
10686 TypeDescriptor {
10687 name: "ProductTag".into(),
10688 module: "default".into(),
10689 table: "Product.tags".into(),
10690 abstract_: false,
10691 materialized: false,
10692 description: None,
10693 parents: vec![],
10694 interfaces: vec![],
10695 bases: vec![],
10696 properties: vec![
10697 id_prop(),
10698 PropertyDescriptor {
10699 name: "weight".into(),
10700 pg_type: "float8".into(),
10701 nullable: false,
10702 default_sql: None,
10703 default_pyql: None,
10704 description: None,
10705 check_constraints: vec![],
10706 is_exclusive: false,
10707 is_pk: false,
10708 is_readonly: false,
10709 rewrites: vec![],
10710 tuple_members: None,
10711 column_type: None,
10712 },
10713 ],
10714 links: vec![],
10718 multilinks: vec![],
10719 computed: vec![],
10720 constraints: vec![],
10721 indexes: vec![],
10722 partition: None,
10723 vector_indexes: vec![],
10724 search_indexes: vec![],
10725 triggers: vec![],
10726 junction: true,
10727 signals: vec![],
10728 },
10729 ],
10730 scalars: vec![],
10731 enums: vec![],
10732 named_tuples: vec![],
10733 globals: vec![],
10734 functions: vec![],
10735 aliases: vec![],
10736 channels: vec![],
10737 ..Default::default()
10738 }
10739 }
10740
10741 #[test]
10742 fn test_link_property_in_a_links_own_filter() {
10743 let schema = make_schema_with_through_and_prop();
10747 let out = compile_and_emit_with("SELECT Product { tags: { } filter @weight > 0.5 }", &schema);
10748 assert!(out.sql.contains("\"jt\".\"weight\" > (0.5::float8)"), "{}", out.sql);
10749 }
10750
10751 #[test]
10752 fn test_exists_on_a_link_property() {
10753 let schema = make_schema_with_through_and_prop();
10756 let out = compile_and_emit_with("SELECT Product { tags: { } filter exists @weight }", &schema);
10757 assert!(out.sql.contains("\"jt\".\"weight\" IS NOT NULL"), "{}", out.sql);
10758 }
10759
10760 #[test]
10761 fn test_link_property_in_a_computed_link_filter() {
10762 let schema = make_schema_with_through_and_prop();
10763 let out = compile_and_emit_with(
10764 "SELECT Product { best := (select .tags filter @weight > 0.5 limit 1) { @weight } }",
10765 &schema,
10766 );
10767 assert!(out.sql.contains("\"jt\".\"weight\" > (0.5::float8)"), "{}", out.sql);
10768 assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
10769 }
10770
10771 #[test]
10772 fn test_link_property_in_a_projected_sub_select_reads_the_junction() {
10773 let schema = make_schema_with_through_and_prop();
10776 let out = compile_and_emit_with(
10777 "SELECT Product { t := (select .tags filter @weight > 0.5 limit 1).id }",
10778 &schema,
10779 );
10780 assert!(out.sql.contains("\"t3\".\"weight\" > (0.5::float8)"), "{}", out.sql);
10781 }
10782
10783 #[test]
10784 fn test_unknown_link_property_names_the_through_type() {
10785 let schema = make_schema_with_through_and_prop();
10786 let ast = parse::parse("SELECT Product { tags: { } filter @nope > 0.5 }").unwrap();
10787 let err = match ir::compile(&ast, &schema) {
10788 Ok(_) => panic!("expected a compile error"),
10789 Err(e) => format!("{e}"),
10790 };
10791 assert!(err.contains("default::ProductTag") && err.contains("nope"), "{err}");
10792 }
10793
10794 #[test]
10795 fn test_link_property_outside_a_link_says_where_it_belongs() {
10796 let schema = make_schema_with_through_and_prop();
10797 let ast = parse::parse("SELECT Product { t := @weight }").unwrap();
10798 let err = match ir::compile(&ast, &schema) {
10799 Ok(_) => panic!("expected a compile error"),
10800 Err(e) => format!("{e}"),
10801 };
10802 assert!(err.contains("is a link property"), "{err}");
10803 }
10804
10805 #[test]
10806 fn test_multilink_append_with_link_property() {
10807 let schema = make_schema_with_through_and_prop();
10808 let out = compile_and_emit_with(
10809 "UPDATE Product FILTER .id = $id SET { tags += (SELECT Tag FILTER .id = $tid) { @weight := <float64>$w } }",
10810 &schema,
10811 );
10812 assert!(out.sql.contains("\"weight\""), "missing weight column:\n{}", out.sql);
10814 assert!(
10817 out.sql
10818 .contains("ON CONFLICT (\"source\", \"target\") DO UPDATE SET \"weight\" = EXCLUDED.\"weight\""),
10819 "missing upsert conflict clause:\n{}",
10820 out.sql
10821 );
10822 }
10823
10824 #[test]
10825 fn test_multilink_append_union_with_different_link_property_values() {
10826 let schema = make_schema_with_through_and_prop();
10830 let out = compile_and_emit_with(
10831 "UPDATE Product FILTER .id = $id SET { \
10832 tags += (SELECT Tag FILTER .id = $aid) { @weight := <float64>$w1 } \
10833 union (SELECT Tag FILTER .id = $bid) { @weight := <float64>$w2 } \
10834 }",
10835 &schema,
10836 );
10837 assert!(
10838 out.sql.contains("UNION ALL"),
10839 "expected a UNION ALL between the two shaped targets:\n{}",
10840 out.sql
10841 );
10842 assert_eq!(
10845 out.sql.matches("AS \"weight\"").count(),
10846 2,
10847 "each union branch must project its own weight:\n{}",
10848 out.sql
10849 );
10850 }
10851
10852 #[test]
10853 fn test_multilink_append_without_link_property_keeps_do_nothing() {
10854 let schema = make_schema_with_through_and_prop();
10857 let out = compile_and_emit_with(
10858 "UPDATE Product FILTER .id = $id SET { tags += (SELECT Tag FILTER .id = $tid) }",
10859 &schema,
10860 );
10861 assert!(
10862 out.sql.contains("ON CONFLICT DO NOTHING"),
10863 "expected plain DO NOTHING when no link properties are set:\n{}",
10864 out.sql
10865 );
10866 assert!(
10867 !out.sql.contains("\"weight\""),
10868 "unexpected weight column with no link property assignment:\n{}",
10869 out.sql
10870 );
10871 }
10872
10873 #[test]
10874 fn test_multilink_link_property_rejected_on_standard_junction() {
10875 let ast = crate::parse::parse(
10878 "UPDATE Person FILTER .id = $id SET { posts += (SELECT Post FILTER .title = $t) { @weight := <float64>$w } }",
10879 ).unwrap();
10880 match crate::ir::compile(&ast, &make_schema()) {
10881 Ok(_) => panic!("expected a compile error for link property on a Standard junction"),
10882 Err(e) => assert!(
10883 e.to_string().contains("Through"),
10884 "expected a Through[...]-related error, got: {e}"
10885 ),
10886 }
10887 }
10888
10889 #[test]
10890 fn test_multilink_link_property_rejected_on_remove() {
10891 let schema = make_schema_with_through_and_prop();
10892 let ast = crate::parse::parse(
10893 "UPDATE Product FILTER .id = $id SET { tags -= (SELECT Tag FILTER .id = $tid) { @weight := <float64>$w } }",
10894 )
10895 .unwrap();
10896 match crate::ir::compile(&ast, &schema) {
10897 Ok(_) => panic!("expected a compile error for link property on a remove (-=)"),
10898 Err(e) => assert!(
10899 e.to_string().contains("removing"),
10900 "expected a remove-related error, got: {e}"
10901 ),
10902 }
10903 }
10904
10905 #[test]
10906 fn test_insert_with_multilink_assignment() {
10907 let schema = make_schema_with_through_and_prop();
10912 let out = compile_and_emit_with(
10913 "INSERT Product { name := $name, tags := (SELECT Tag FILTER .id = $tid) { @weight := <float64>$w } }",
10914 &schema,
10915 );
10916 assert!(
10918 out.sql.contains("\"_w__ids\" AS (\nINSERT INTO"),
10919 "missing row-insert CTE:\n{}",
10920 out.sql
10921 );
10922 assert!(
10923 out.sql.contains("\"_w__ml_add_0\" AS ("),
10924 "missing junction-append CTE:\n{}",
10925 out.sql
10926 );
10927 assert!(out.sql.contains("\"weight\""), "missing weight column:\n{}", out.sql);
10928 assert!(
10929 out.sql.contains("\"_w\" AS (\n SELECT * FROM \"_w__ids\"\n)"),
10930 "missing _w passthrough:\n{}",
10931 out.sql
10932 );
10933 assert_eq!(
10934 out.sql.matches("WITH\n").count(),
10935 1,
10936 "must be a single flat top-level WITH block:\n{}",
10937 out.sql
10938 );
10939 }
10940
10941 #[test]
10949 fn test_user_with_binding_merges_into_a_statements_own_ctes() {
10950 let schema = make_schema_with_through_and_prop();
10951 for query in [
10952 "with t := (select Tag filter .id = $tid) insert Product { name := $name, tags := t }",
10953 "with t := (select Tag filter .id = $tid) \
10954 update Product filter .name = $name set { tags += t }",
10955 ] {
10956 let out = compile_and_emit_with(query, &schema);
10957 let flat: String = out.sql.split_whitespace().collect::<Vec<_>>().join(" ");
10958 assert!(
10959 !flat.contains(") WITH "),
10960 "a second WITH clause is invalid SQL:\n{}",
10961 out.sql
10962 );
10963 assert_eq!(
10964 out.sql.matches("WITH").count(),
10965 1,
10966 "exactly one WITH keyword expected:\n{}",
10967 out.sql
10968 );
10969 }
10970 }
10971
10972 #[test]
10973 fn test_with_bound_insert_with_multilink_assignment() {
10974 let schema = make_schema_with_through_and_prop();
10978 let out = compile_and_emit_with(
10979 "with insert0 := (insert Product { name := $name, tags := (select Tag filter .id = $tid) }) select insert0",
10980 &schema,
10981 );
10982 assert!(
10983 out.sql.contains("\"insert0__ids\" AS (\nINSERT INTO"),
10984 "missing row-insert CTE:\n{}",
10985 out.sql
10986 );
10987 assert!(
10988 out.sql.contains("\"insert0__ml_add_0\" AS ("),
10989 "missing junction-append CTE:\n{}",
10990 out.sql
10991 );
10992 assert!(
10993 out.sql
10994 .contains("\"insert0\" AS (\n SELECT * FROM \"insert0__ids\"\n)"),
10995 "missing insert0 passthrough:\n{}",
10996 out.sql
10997 );
10998 assert_eq!(
10999 out.sql.matches("WITH\n").count(),
11000 1,
11001 "must be a single flat top-level WITH block:\n{}",
11002 out.sql
11003 );
11004 }
11005
11006 #[test]
11007 fn test_with_block_cte_over_computed_global_merges_into_single_with_clause() {
11008 let mut schema = make_schema();
11017 schema.globals.push(GlobalDescriptor {
11018 name: "current_user_id".into(),
11019 module: "default".into(),
11020 scalar_type: "std::uuid".into(),
11021 required: false,
11022 default_expr: None,
11023 computed_expr: None,
11024 });
11025 schema.globals.push(GlobalDescriptor {
11026 name: "current_user".into(),
11027 module: "default".into(),
11028 scalar_type: "Person".into(),
11029 required: false,
11030 default_expr: None,
11031 computed_expr: Some("select default::Person filter .id = global current_user_id".into()),
11032 });
11033 let out = compile_and_emit_with("with\n user := (select global current_user)\nselect user;", &schema);
11034 assert_eq!(
11035 out.sql.matches("WITH").count(),
11036 1,
11037 "must be a single WITH clause, got:\n{}",
11038 out.sql
11039 );
11040 }
11041
11042 #[test]
11043 fn test_several_mutations_in_a_free_set_each_become_a_cte() {
11044 let schema = make_schema();
11045 let out = compile_and_emit_with(
11046 "select { (update Person filter .name = 'a' set { age := 1 }), \
11047 (update Company filter .name = 'b' set { name := 'c' }) }",
11048 &schema,
11049 );
11050 assert_eq!(
11051 out.sql.matches("UPDATE").count(),
11052 2,
11053 "both mutations have to run, got:\n{}",
11054 out.sql
11055 );
11056 assert!(out.sql.contains("WITH"), "each becomes a CTE, got:\n{}", out.sql);
11057 }
11058
11059 #[test]
11060 fn test_updating_a_binding_touches_only_its_own_rows() {
11061 let schema = make_schema();
11064 let out = compile_and_emit_with(
11065 "with mine := (select Person filter .name = 'a') select { (update mine set { age := 1 }) }",
11066 &schema,
11067 );
11068 assert!(
11069 out.sql.contains("UPDATE") && out.sql.contains("= ANY(ARRAY(SELECT"),
11070 "expected the update narrowed to the binding's rows, got:\n{}",
11071 out.sql
11072 );
11073 }
11074
11075 #[test]
11076 fn test_updating_a_traversal_touches_only_the_rows_it_lands_on() {
11077 let mut schema = make_schema();
11078 let company = schema
11081 .types
11082 .iter_mut()
11083 .find(|t| t.name == "Company")
11084 .expect("test schema has a Company type");
11085 company.properties.push(crate::schema::PropertyDescriptor {
11086 name: "id".into(),
11087 pg_type: "uuid".into(),
11088 nullable: false,
11089 default_sql: Some("uuidv7()".into()),
11090 default_pyql: None,
11091 description: None,
11092 check_constraints: vec![],
11093 is_exclusive: true,
11094 is_pk: true,
11095 is_readonly: true,
11096 rewrites: vec![],
11097 tuple_members: None,
11098 column_type: None,
11099 });
11100 let out = compile_and_emit_with(
11101 "with mine := (select Person filter .name = 'a') select { (update mine.company set { name := 'x' }) }",
11102 &schema,
11103 );
11104 assert!(
11105 out.sql.contains("= ANY(ARRAY(SELECT"),
11106 "expected the update narrowed to the traversal's rows, got:\n{}",
11107 out.sql
11108 );
11109 assert!(
11110 out.sql.contains("\"Company\""),
11111 "expected the table the traversal ends on, got:\n{}",
11112 out.sql
11113 );
11114 }
11115
11116 #[test]
11117 fn test_aggregate_over_a_multi_valued_path_keeps_the_set_flat() {
11118 let schema = make_schema();
11123 let out = compile_and_emit_with(
11124 "with a := (select Person limit 1) select { titles := array_agg(a.posts.title) }",
11125 &schema,
11126 );
11127 assert!(
11128 !out.sql.contains("array_agg(ARRAY("),
11129 "the aggregate must take the set, not the array standing for it, got:\n{}",
11130 out.sql
11131 );
11132 assert!(
11133 out.sql.contains("array_agg(") && out.sql.contains("\"title\""),
11134 "expected the aggregate over the traversal's own column, got:\n{}",
11135 out.sql
11136 );
11137 assert!(
11140 out.sql.contains("ROW(") && !out.sql.contains("jsonb_build_object"),
11141 "expected a composite row, got:\n{}",
11142 out.sql
11143 );
11144 }
11145
11146 #[test]
11147 fn test_with_bound_root_read_by_name_inside_a_nested_select() {
11148 let schema = make_schema();
11153 let out = compile_and_emit_with(
11154 "with
11155 owner := (select Person limit 1)
11156select owner { posts := (select owner.posts.title) };",
11157 &schema,
11158 );
11159 assert!(
11160 out.sql.contains("WITH") && out.sql.contains("\"title\""),
11161 "expected the nested select to traverse the binding, got:\n{}",
11162 out.sql
11163 );
11164 }
11165
11166 #[test]
11167 fn test_path_traversal_into_with_bound_cte_of_object_type() {
11168 let mut schema = make_schema();
11174 schema.globals.push(GlobalDescriptor {
11175 name: "current_user_id".into(),
11176 module: "default".into(),
11177 scalar_type: "std::uuid".into(),
11178 required: false,
11179 default_expr: None,
11180 computed_expr: None,
11181 });
11182 schema.globals.push(GlobalDescriptor {
11183 name: "current_user".into(),
11184 module: "default".into(),
11185 scalar_type: "Person".into(),
11186 required: false,
11187 default_expr: None,
11188 computed_expr: Some("select default::Person filter .id = global current_user_id".into()),
11189 });
11190 let out = compile_and_emit_with(
11191 "with\n user := (select global current_user)\nselect user.name;",
11192 &schema,
11193 );
11194 assert!(
11195 out.sql.contains("FROM \"user\""),
11196 "expected path traversal from the CTE, got:\n{}",
11197 out.sql
11198 );
11199 assert_eq!(
11200 out.sql.matches("WITH").count(),
11201 1,
11202 "must be a single WITH clause, got:\n{}",
11203 out.sql
11204 );
11205 }
11206
11207 #[test]
11208 fn test_with_bound_schema_object_property_access_in_filter_expression() {
11209 let schema = make_schema();
11217 let out = compile_and_emit_with(
11218 "with\n person := (select detached Person filter .id = <uuid>$id)\n\
11219 select Person filter .id not in person.id;",
11220 &schema,
11221 );
11222 assert!(
11223 out.sql.contains("<> ALL((SELECT") && out.sql.contains(".\"id\"") && out.sql.contains("FROM \"person\""),
11224 "got:\n{}",
11225 out.sql
11226 );
11227 }
11228
11229 #[test]
11230 fn test_with_bound_schema_object_scalar_property_access() {
11231 let schema = make_schema();
11235 let out = compile_and_emit_with(
11236 "with\n person := (select detached Person filter .id = <uuid>$id)\n\
11237 select Person filter .name = person.name;",
11238 &schema,
11239 );
11240 assert!(
11241 out.sql.contains("(SELECT") && out.sql.contains(".\"name\"") && out.sql.contains("FROM \"person\""),
11242 "got:\n{}",
11243 out.sql
11244 );
11245 }
11246
11247 #[test]
11248 fn test_with_bound_schema_object_link_traversal_reaches_nested_property() {
11249 let schema = make_schema();
11256 let out = compile_and_emit_with(
11257 "with\n person := (select detached Person filter .id = <uuid>$id)\n\
11258 select Company filter .name = person.company.name;",
11259 &schema,
11260 );
11261 assert!(
11262 out.sql.contains("(SELECT") && out.sql.contains("\"name\"") && out.sql.contains("\"Company\""),
11263 "got:\n{}",
11264 out.sql
11265 );
11266 }
11267
11268 #[test]
11269 fn test_with_bound_schema_object_bare_link_reduces_to_id() {
11270 let schema = make_schema();
11274 let out = compile_and_emit_with(
11275 "with\n person := (select detached Person filter .id = <uuid>$id)\n\
11276 select Person filter .id = person.company;",
11277 &schema,
11278 );
11279 assert!(
11280 out.sql.contains("(SELECT") && out.sql.contains("\"id\""),
11281 "got:\n{}",
11282 out.sql
11283 );
11284 }
11285
11286 #[test]
11287 fn test_with_bound_schema_object_unknown_property_suggests_a_close_match() {
11288 let schema = make_schema();
11289 let ast = parse::parse(
11290 "with\n person := (select detached Person filter .id = <uuid>$id)\n\
11291 select Person filter .name = person.nam;",
11292 )
11293 .unwrap();
11294 match ir::compile(&ast, &schema) {
11295 Err(err) => assert!(format!("{err}").contains("Did you mean 'name'"), "got: {err}"),
11296 Ok(_) => panic!("expected a compile error"),
11297 }
11298 }
11299
11300 #[test]
11301 fn test_with_bound_free_object_passthrough_preserves_all_fields() {
11302 let out = compile_and_emit("with\n test := { test2 := 1.0, test3 := 'str' }\nselect test;");
11308 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
11309 panic!("expected Object shape, got {:?}", out.shape.root)
11310 };
11311 assert_eq!(pointers.len(), 2);
11312 assert!(matches!(&pointers[0], ShapeNode::Scalar { name, .. } if name == "test2"));
11313 assert!(matches!(&pointers[1], ShapeNode::Scalar { name, .. } if name == "test3"));
11314 }
11315
11316 #[test]
11317 fn test_with_bound_free_object_field_access() {
11318 let out = compile_and_emit("with\n test := { test2 := 1.0, test3 := 'str' }\nselect test.test2;");
11323 assert!(out.sql.contains("\"test2\" FROM \"test\""), "got:\n{}", out.sql);
11324 }
11325
11326 #[test]
11327 fn test_with_bound_free_object_nested_field_access_chain() {
11328 let out =
11333 compile_and_emit("with\n test := { test2 := 1.0, test3 := { foo := 'bar' } }\nselect test.test3.foo;");
11334 assert!(out.sql.contains("\"test3\" FROM \"test\""), "got:\n{}", out.sql);
11335 assert!(
11336 out.sql.contains("->'foo'"),
11337 "expected jsonb field extraction, got:\n{}",
11338 out.sql
11339 );
11340 }
11341
11342 #[test]
11343 fn test_with_bound_free_object_nested_field_access_wrong_field_errors() {
11344 let schema = make_schema();
11347 let ast = parse::parse("with\n test := { test2 := 1.0, test3 := { foo := 'bar' } }\nselect test.test3.nope;")
11348 .unwrap();
11349 assert!(ir::compile(&ast, &schema).is_err());
11350 }
11351
11352 #[test]
11353 fn test_nested_free_object_literal_in_computed_shape_element() {
11354 let out = compile_and_emit("select default::Person { id, test := { foo := 'bar' } };");
11360 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
11361 panic!("expected Object shape")
11362 };
11363 let test_node = pointers
11364 .iter()
11365 .find(|p| matches!(p, ShapeNode::NamedTuple { name, .. } if name == "test"))
11366 .unwrap_or_else(|| panic!("expected a NamedTuple shape node for 'test', got {:?}", pointers));
11367 assert!(matches!(
11368 test_node,
11369 ShapeNode::NamedTuple {
11370 is_free_object: true,
11371 ..
11372 }
11373 ));
11374 }
11375
11376 #[test]
11377 fn test_bare_free_cte_reference_in_computed_shape_collapses_to_empty() {
11378 let out = compile_and_emit(
11383 "with\n test := { test2 := 1.0, test3 := 'str' }\n\
11384 select default::Person { id, test := test };",
11385 );
11386 assert!(
11387 out.sql.contains("jsonb_build_object()"),
11388 "expected an empty free object, got:\n{}",
11389 out.sql
11390 );
11391 }
11392
11393 #[test]
11394 fn test_shaped_free_cte_reference_projects_fields() {
11395 let out = compile_and_emit(
11399 "with\n test := { test2 := 1.0, test3 := 'str' }\n\
11400 select default::Person { id, test := test { test2 } };",
11401 );
11402 assert!(out.sql.contains("jsonb_build_object('test2'"), "got:\n{}", out.sql);
11403 assert!(
11404 !out.sql.contains("'test3'"),
11405 "test3 should not be projected, got:\n{}",
11406 out.sql
11407 );
11408 }
11409
11410 #[test]
11411 fn test_insert_multilink_remove_rejected() {
11412 let schema = make_schema_with_through_and_prop();
11413 let ast =
11414 crate::parse::parse("INSERT Product { name := $name, tags -= (SELECT Tag FILTER .id = $tid) }").unwrap();
11415 match crate::ir::compile(&ast, &schema) {
11416 Ok(_) => panic!("expected a compile error for `-=` on a multi-link at insert time"),
11417 Err(e) => assert!(
11418 e.to_string().contains("nothing to remove"),
11419 "expected a 'nothing to remove yet' error, got: {e}"
11420 ),
11421 }
11422 }
11423
11424 #[test]
11425 fn test_multilink_junction_info_disambiguates_self_referencing_through_type() {
11426 let schema = make_schema_with_through();
11433 let out = compile_and_emit_with(
11434 "UPDATE Person FILTER .id = $id SET { friends += (SELECT Person FILTER .id = $fid) }",
11435 &schema,
11436 );
11437 assert!(
11438 out.sql.contains("(\"person\", \"friend\")"),
11439 "expected two distinct FK columns:\n{}",
11440 out.sql
11441 );
11442 assert!(
11443 !out.sql.contains("(\"person\", \"person\")"),
11444 "source/target collapsed to the same column:\n{}",
11445 out.sql
11446 );
11447 }
11448
11449 #[test]
11450 fn test_shape_descriptor_scalars() {
11451 let out = compile_and_emit("SELECT Person { name, age }");
11452 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
11453 panic!()
11454 };
11455 assert_eq!(pointers.len(), 4); assert!(matches!(&pointers[0], ShapeNode::Scalar { name, position: 0 } if name == "__type__"));
11457 assert!(matches!(&pointers[1], ShapeNode::Scalar { name, position: 1 } if name == "id"));
11458 assert!(matches!(&pointers[2], ShapeNode::Scalar { name, position: 2 } if name == "name"));
11459 assert!(matches!(&pointers[3], ShapeNode::Scalar { name, position: 3 } if name == "age"));
11460 }
11461
11462 #[test]
11463 fn test_shape_descriptor_multi_link() {
11464 let out = compile_and_emit("SELECT Person { name, posts { title } }");
11465 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
11466 panic!()
11467 };
11468 assert_eq!(pointers.len(), 4);
11470 let ShapeNode::Array {
11471 name,
11472 position,
11473 element,
11474 } = &pointers[3]
11475 else {
11476 panic!()
11477 };
11478 assert_eq!(name, "posts");
11479 assert_eq!(*position, 3);
11480 let ShapeNode::Object {
11481 pointers: elem_pointers,
11482 ..
11483 } = element.as_ref()
11484 else {
11485 panic!()
11486 };
11487 assert_eq!(elem_pointers.len(), 2);
11490 }
11491
11492 #[test]
11493 fn test_select_order_by_limit() {
11494 let out = compile_and_emit("SELECT Person { name } ORDER BY .name ASC LIMIT 10");
11495 assert!(out.sql.contains("ORDER BY"));
11496 assert!(out.sql.contains("LIMIT 10"));
11497 }
11498
11499 #[test]
11500 fn test_insert_returning() {
11501 let out = compile_and_emit("INSERT Person { name := 'Alice', age := 30 }");
11502 assert!(out.sql.contains("INSERT INTO \"public\".\"Person\""));
11503 assert!(out.sql.contains("RETURNING"));
11504 assert!(out.sql.contains("'default::Person'::text"));
11505 assert!(out.sql.contains(") AS result"));
11506 let ShapeNode::Object {
11508 cardinality, pointers, ..
11509 } = &out.shape.root
11510 else {
11511 panic!()
11512 };
11513 assert_eq!(*cardinality, Cardinality::Required);
11514 assert!(
11516 pointers
11517 .iter()
11518 .any(|f| matches!(f, ShapeNode::Scalar { name, .. } if name == "id"))
11519 );
11520 assert!(
11521 !pointers
11522 .iter()
11523 .any(|f| matches!(f, ShapeNode::Scalar { name, .. } if name == "name"))
11524 );
11525 }
11526
11527 #[test]
11528 fn test_update_returning() {
11529 let out = compile_and_emit("UPDATE Person FILTER .name = $name SET { age := 31 }");
11530 assert!(out.sql.contains("UPDATE \"public\".\"Person\""));
11531 assert!(out.sql.contains("SET"));
11532 assert!(out.sql.contains("RETURNING"));
11534 assert!(out.sql.contains("'default::Person'::text"));
11535 assert!(!out.sql.contains("\"name\"::text"), "bare UPDATE must not return name");
11536 }
11537
11538 #[test]
11539 fn test_update_set_tuple_param_cast_uses_direct_jsonb_cast_not_to_jsonb() {
11540 let out = compile_and_emit("UPDATE Person FILTER .id = $id SET { age := <tuple<x: float64>>$val }");
11547 assert!(
11548 out.sql.contains(")::jsonb"),
11549 "expected a direct ::jsonb cast, got:\n{}",
11550 out.sql
11551 );
11552 assert!(
11553 !out.sql.contains("to_jsonb($"),
11554 "must not pass a bare param straight into to_jsonb(): got:\n{}",
11555 out.sql
11556 );
11557 }
11558
11559 #[test]
11560 fn test_empty_set_cast_to_object_type_clears_optional_link() {
11561 let out = compile_and_emit("UPDATE Person FILTER .id = $id SET { company := <default::Company>{} }");
11570 assert!(out.sql.contains("\"company_id\" = NULL"), "got:\n{}", out.sql);
11571 }
11572
11573 #[test]
11574 fn test_delete_returning() {
11575 let out = compile_and_emit("DELETE Person FILTER .id = $id");
11576 assert!(out.sql.contains("DELETE FROM \"public\".\"Person\""));
11577 assert!(out.sql.contains("RETURNING"));
11579 assert!(out.sql.contains("'default::Person'::text"));
11580 assert!(!out.sql.contains("\"name\"::text"), "bare DELETE must not return name");
11581 }
11582
11583 #[test]
11584 fn test_select_over_insert() {
11585 let out = compile_and_emit("SELECT (INSERT Person { name := $name, age := $age }) { id, name }");
11586 assert!(out.sql.contains("WITH\n\"_dml\" AS ("));
11588 assert!(out.sql.contains("INSERT INTO"));
11589 assert!(out.sql.contains("RETURNING *"));
11590 assert!(out.sql.contains("'default::Person'::text"));
11592 assert!(out.sql.contains("\"name\"::text"));
11593 }
11594
11595 #[test]
11596 fn test_select_over_update() {
11597 let out = compile_and_emit("SELECT (UPDATE Person FILTER .id = $id SET { name := $name }) { id, name }");
11598 assert!(out.sql.contains("WITH\n\"_dml\" AS ("));
11599 assert!(out.sql.contains("UPDATE"));
11600 assert!(out.sql.contains("RETURNING \"t1\".*"), "{}", out.sql);
11601 assert!(out.sql.contains("\"name\"::text"));
11602 }
11603
11604 #[test]
11605 fn test_insert_user_specified_id_denied_by_default() {
11606 let schema = make_schema();
11607 let ast = parse::parse("INSERT Person { id := <uuid>$id, name := $name, age := $age }").unwrap();
11608 match ir::compile_with_config(&ast, &schema, &ir::SessionConfig::default()) {
11609 Err(err) => assert!(err.to_string().contains("cannot assign to property 'id'"), "got: {err}"),
11610 Ok(_) => panic!("expected id assignment to be denied by default"),
11611 }
11612 }
11613
11614 #[test]
11615 fn test_insert_user_specified_id_allowed_when_configured() {
11616 let schema = make_schema();
11617 let ast = parse::parse("INSERT Person { id := <uuid>$id, name := $name, age := $age }").unwrap();
11618 let config = ir::SessionConfig {
11619 allow_user_specified_id: true,
11620 };
11621 let ir_out = ir::compile_with_config(&ast, &schema, &config)
11622 .expect("expected id assignment to be allowed with allow_user_specified_id");
11623 let out = emit(&ir_out);
11624 assert!(out.sql.contains("INSERT INTO"));
11625 }
11626
11627 #[test]
11628 fn test_update_user_specified_id_denied_even_when_configured() {
11629 let schema = make_schema();
11630 let ast = parse::parse("UPDATE Person FILTER .name = $name SET { id := <uuid>$id }").unwrap();
11631 let config = ir::SessionConfig {
11632 allow_user_specified_id: true,
11633 };
11634 match ir::compile_with_config(&ast, &schema, &config) {
11635 Err(err) => assert!(err.to_string().contains("cannot assign to property 'id'"), "got: {err}"),
11636 Ok(_) => panic!("expected UPDATE to always deny reassigning id"),
11637 }
11638 }
11639
11640 #[test]
11641 fn test_select_over_update_multilink_only() {
11642 let out = compile_and_emit(
11652 "SELECT (UPDATE Person FILTER .id = $id SET { posts += (SELECT Post FILTER .title = $title) }) { id, name }",
11653 );
11654 assert!(
11655 out.sql.contains("\"_dml__ml_add_0\""),
11656 "missing junction-append CTE:\n{}",
11657 out.sql
11658 );
11659 assert!(out.sql.contains("INSERT INTO"), "missing junction INSERT:\n{}", out.sql);
11660 assert!(
11663 out.sql.contains("\"_dml__ids\" AS (\nSELECT"),
11664 "expected SELECT-based _ids CTE:\n{}",
11665 out.sql
11666 );
11667 assert!(
11668 !out.sql.contains("SET\n\nWHERE") && !out.sql.contains("SET \nWHERE"),
11669 "empty SET clause regression:\n{}",
11670 out.sql
11671 );
11672 assert_eq!(
11675 out.sql.matches("WITH\n").count(),
11676 1,
11677 "junction CTE must not be nested in a second WITH:\n{}",
11678 out.sql
11679 );
11680 assert!(
11682 out.sql.contains("\"_dml\" AS (\n SELECT * FROM \"_dml__ids\"\n)"),
11683 "missing _dml passthrough:\n{}",
11684 out.sql
11685 );
11686 }
11687
11688 #[test]
11689 fn test_select_over_update_scalar_and_multilink() {
11690 let out = compile_and_emit(
11694 "SELECT (UPDATE Person FILTER .id = $id SET { name := $name, posts += (SELECT Post FILTER .title = $title) }) { id, name }",
11695 );
11696 assert!(
11697 out.sql.contains("\"_dml__ml_add_0\""),
11698 "missing junction-append CTE:\n{}",
11699 out.sql
11700 );
11701 assert!(
11702 out.sql.contains("\"_dml__ids\" AS (\nUPDATE"),
11703 "expected UPDATE-based _ids CTE:\n{}",
11704 out.sql
11705 );
11706 assert!(
11707 out.sql.contains("\"name\" = "),
11708 "missing scalar SET assignment:\n{}",
11709 out.sql
11710 );
11711 assert_eq!(
11712 out.sql.matches("WITH\n").count(),
11713 1,
11714 "junction CTE must not be nested in a second WITH:\n{}",
11715 out.sql
11716 );
11717 }
11718
11719 #[test]
11720 fn test_with_bound_insert_and_multilink_update_forward_ref() {
11721 let out = compile_and_emit(
11729 "with insert0 := (insert Post { title := $title }), update0 := (update Person filter .id = $id set { posts += (select insert0) }) select { insert0, update0 }",
11730 );
11731 assert!(
11732 out.sql.contains("\"insert0\" AS (\n INSERT INTO"),
11733 "missing insert0 CTE:\n{}",
11734 out.sql
11735 );
11736 assert!(
11737 out.sql.contains("\"update0__ml_add_0\""),
11738 "missing junction-append CTE for update0:\n{}",
11739 out.sql
11740 );
11741 assert!(
11742 out.sql.contains("\"update0__ids\" AS (\nSELECT"),
11743 "expected SELECT-based update0 ids CTE (no scalar changes):\n{}",
11744 out.sql
11745 );
11746 assert!(
11747 out.sql
11748 .contains("\"update0\" AS (\n SELECT * FROM \"update0__ids\"\n)"),
11749 "missing update0 passthrough:\n{}",
11750 out.sql
11751 );
11752 assert_eq!(
11755 out.sql.matches("WITH\n").count(),
11756 1,
11757 "must be a single flat top-level WITH block:\n{}",
11758 out.sql
11759 );
11760 }
11761
11762 #[test]
11763 fn test_select_over_delete() {
11764 let out = compile_and_emit("SELECT (DELETE Person FILTER .id = $id) { id, name }");
11765 assert!(out.sql.contains("WITH\n\"_dml\" AS ("));
11766 assert!(out.sql.contains("DELETE FROM"));
11767 assert!(out.sql.contains("RETURNING *"));
11768 assert!(out.sql.contains("\"name\"::text"));
11769 }
11770
11771 fn make_schema_with_rewrite() -> SchemaDescriptor {
11772 use crate::schema::RewriteEntry;
11773 let mut schema = make_schema();
11774 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
11776 person.properties.push(PropertyDescriptor {
11777 name: "slug".into(),
11778 pg_type: "text".into(),
11779 nullable: true,
11780 default_sql: None,
11781 default_pyql: None,
11782 description: None,
11783 check_constraints: vec![],
11784 is_exclusive: false,
11785 is_pk: false,
11786 is_readonly: false,
11787 rewrites: vec![
11788 RewriteEntry {
11789 on: 1,
11790 handler: "str_lower(.name)".into(),
11791 }, RewriteEntry {
11793 on: 2,
11794 handler: "str_lower(.name)".into(),
11795 }, ],
11797 tuple_members: None,
11798 column_type: None,
11799 });
11800 schema
11801 }
11802
11803 #[test]
11804 fn a_link_assigned_from_a_select_keeps_its_clauses() {
11805 let out = compile_and_emit(
11806 "INSERT Person { name := 'a', age := 1, company := (SELECT Company ORDER BY .name LIMIT 1) }",
11807 );
11808 assert!(
11809 out.sql.contains("ORDER BY") && out.sql.contains("LIMIT 1"),
11810 "{}",
11811 out.sql
11812 );
11813 }
11814
11815 #[test]
11816 fn a_rewrite_leaves_the_statement_alone() {
11817 let schema = make_schema_with_rewrite();
11819 for query in [
11820 "INSERT Person { name := $name, age := 30 }",
11821 "UPDATE Person FILTER .id = $id SET { name := $name }",
11822 ] {
11823 let out = compile_and_emit_with(query, &schema);
11824 assert!(!out.sql.contains("\"slug\""), "{}", out.sql);
11825 }
11826 }
11827
11828 #[test]
11829 fn a_rewrite_reads_the_row_being_written() {
11830 let schema = make_schema_with_rewrite();
11831 let assignments = crate::ir::compile_rewrite_assignments("default::Person", 1, &schema).unwrap();
11832 let [assignment] = assignments.as_slice() else {
11833 panic!("one rewrite on insert")
11834 };
11835 assert_eq!(assignment.column, "slug");
11836 assert!(assignment.sql.contains("NEW.\"name\""), "{}", assignment.sql);
11837 }
11838
11839 #[test]
11840 fn rewrites_become_before_triggers() {
11841 let ddl = crate::export::export_schema(&make_schema_with_rewrite()).unwrap();
11842 for event in ["BEFORE INSERT", "BEFORE UPDATE"] {
11843 assert!(
11844 ddl.contains(&format!("{event} ON \"public\".\"Person\"")),
11845 "{event}:\n{ddl}"
11846 );
11847 }
11848 assert!(ddl.contains("NEW.\"slug\" := _pylon_rewrites.\"v0\";"), "{ddl}");
11849 }
11850
11851 #[test]
11852 fn a_rewrite_walking_a_link_starts_from_the_new_row() {
11853 use crate::schema::RewriteEntry;
11855 let mut schema = make_schema_with_rewrite();
11856 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
11857 let slug = person.properties.iter_mut().find(|p| p.name == "slug").unwrap();
11858 slug.rewrites = vec![RewriteEntry {
11859 on: 1,
11860 handler: "<str>(.company is Company) ++ .company.name".into(),
11861 }];
11862 let assignments = crate::ir::compile_rewrite_assignments("default::Person", 1, &schema).unwrap();
11863 let sql = &assignments[0].sql;
11864 assert!(!sql.contains("\"public\".\"Person\""), "{sql}");
11865 assert!(
11866 sql.contains("(SELECT (NEW).*)") && sql.contains("NEW.\"company_id\""),
11867 "{sql}"
11868 );
11869 }
11870
11871 #[test]
11872 fn test_unless_conflict_do_nothing() {
11873 let out = compile_and_emit("INSERT Person { name := $name } UNLESS CONFLICT");
11874 assert!(out.sql.contains("ON CONFLICT DO NOTHING"));
11875 }
11876
11877 #[test]
11878 fn test_unless_conflict_on_do_nothing() {
11879 let out = compile_and_emit("INSERT Person { name := $name } UNLESS CONFLICT ON .name");
11880 assert!(out.sql.contains("ON CONFLICT (\"name\") DO NOTHING"));
11881 }
11882
11883 #[test]
11884 fn test_unless_conflict_do_update() {
11885 let out = compile_and_emit(
11886 "INSERT Person { name := $name, age := $age } \
11887 UNLESS CONFLICT ON .name \
11888 ELSE (UPDATE Person SET { age := $age })",
11889 );
11890 assert!(out.sql.contains("ON CONFLICT (\"name\") DO UPDATE SET"));
11891 assert!(out.sql.contains("\"age\" = $2"));
11892 assert!(!out.sql.contains("DO NOTHING"));
11894 }
11895
11896 #[test]
11897 fn test_unless_conflict_else_update_self_reference_is_qualified() {
11898 let out = compile_and_emit(
11906 "INSERT Person { name := $name, age := 0 } \
11907 UNLESS CONFLICT ON .name \
11908 ELSE (UPDATE Person SET { age := .age + 1 })",
11909 );
11910 assert!(
11911 out.sql.contains("\"Person\".\"age\""),
11912 "self-reference must be qualified with the table's own name, got:\n{}",
11913 out.sql
11914 );
11915 assert!(
11916 !out.sql.contains("SET \"age\" = (\"age\""),
11917 "must not emit an unqualified (ambiguous) self-reference, got:\n{}",
11918 out.sql
11919 );
11920 }
11921
11922 #[test]
11923 fn test_unless_conflict_do_update_no_on() {
11924 let out = compile_and_emit(
11925 "INSERT Person { name := $name } \
11926 UNLESS CONFLICT \
11927 ELSE (UPDATE Person SET { age := 0 })",
11928 );
11929 assert!(out.sql.contains("ON CONFLICT DO UPDATE SET"));
11930 assert!(out.sql.contains("\"age\" = 0"));
11931 }
11932
11933 #[test]
11934 fn test_select_over_select() {
11935 let out = compile_and_emit("SELECT (SELECT Person FILTER .age > 18) { name }");
11936 assert!(out.sql.contains("WITH\n\"_dml\" AS ("));
11938 assert!(out.sql.contains("SELECT *"));
11940 assert!(out.sql.contains("FROM \"public\".\"Person\""));
11941 assert!(out.sql.contains("WHERE"));
11943 assert!(out.sql.contains("'default::Person'::text"));
11945 assert!(out.sql.contains("\"name\"::text"));
11946 }
11947
11948 #[test]
11949 fn test_select_over_select_with_outer_filter() {
11950 let out = compile_and_emit("SELECT (SELECT Person FILTER .age > 18) { name } FILTER .name = $name");
11951 assert!(out.sql.contains("WITH\n\"_dml\" AS ("));
11952 assert!(out.sql.contains("SELECT *"));
11953 assert_eq!(out.sql.matches("WHERE").count(), 2);
11955 assert!(out.sql.contains("$1"));
11956 }
11957
11958 #[test]
11959 fn test_insert_link_subquery() {
11960 let out = compile_and_emit("INSERT Person { name := $name, company := (SELECT Company FILTER .name = $co) }");
11961 assert!(out.sql.contains("\"company_id\""));
11963 assert!(out.sql.contains("SELECT"));
11964 assert!(out.sql.contains("\"id\""));
11966 assert!(out.sql.contains("FROM \"public\".\"Company\""));
11967 assert!(out.sql.contains("$2")); }
11970
11971 #[test]
11972 fn test_update_link_subquery() {
11973 let out =
11974 compile_and_emit("UPDATE Person FILTER .id = $id SET { company := (SELECT Company FILTER .name = $co) }");
11975 assert!(out.sql.contains("\"company_id\""));
11976 assert!(out.sql.contains("SELECT"));
11977 assert!(out.sql.contains("FROM \"public\".\"Company\""));
11978 }
11979
11980 #[test]
11981 fn test_insert_link_value_from_nested_insert_hoists_a_with_cte() {
11982 let out = compile_and_emit(
11990 "INSERT Person { name := 'Alice', company := (select (insert Company { name := 'Acme' }) { id }) }",
11991 );
11992 assert!(
11993 out.sql.starts_with("WITH"),
11994 "expected a WITH-hoisted CTE, got:\n{}",
11995 out.sql
11996 );
11997 assert!(
11998 out.sql.contains("INSERT INTO \"public\".\"Company\""),
11999 "expected the nested insert to be its own CTE, got:\n{}",
12000 out.sql,
12001 );
12002 assert!(
12005 out.sql.contains("\"company_id\") SELECT") && out.sql.contains(".\"id\" FROM"),
12006 "expected the outer insert to switch from VALUES to SELECT ... FROM <cte>, got:\n{}",
12007 out.sql,
12008 );
12009 assert!(
12010 !out.sql.contains("FROM \"public\".\"Company\" AS"),
12011 "must not select from the real Company table, got:\n{}",
12012 out.sql
12013 );
12014 }
12015
12016 #[test]
12017 fn test_update_link_value_from_nested_insert_hoists_a_with_cte_and_adds_from_clause() {
12018 let out = compile_and_emit(
12019 "UPDATE Person FILTER .id = $id SET { company := (select (insert Company { name := 'Acme' }) { id }) }",
12020 );
12021 assert!(
12022 out.sql.starts_with("WITH"),
12023 "expected a WITH-hoisted CTE, got:\n{}",
12024 out.sql
12025 );
12026 assert!(
12027 out.sql.contains("INSERT INTO \"public\".\"Company\""),
12028 "expected the nested insert to be its own CTE, got:\n{}",
12029 out.sql,
12030 );
12031 assert!(
12034 out.sql.contains("\nFROM \""),
12035 "expected a FROM clause referencing the nested CTE, got:\n{}",
12036 out.sql
12037 );
12038 assert!(out.sql.contains("SET \"company_id\" = "), "got:\n{}", out.sql);
12039 }
12040
12041 #[test]
12042 fn test_update_link_value_from_nested_insert_combines_with_multilink_mutation() {
12043 let out = compile_and_emit(
12048 "UPDATE Person FILTER .id = $id SET { \
12049 company := (select (insert Company { name := 'Acme' }) { id }), \
12050 posts += (SELECT Post FILTER .title = $t) \
12051 }",
12052 );
12053 assert!(out.sql.starts_with("WITH"), "got:\n{}", out.sql);
12054 assert!(
12055 out.sql.contains("INSERT INTO \"public\".\"Company\""),
12056 "expected the nested insert to be its own CTE, got:\n{}",
12057 out.sql,
12058 );
12059 assert!(
12060 out.sql.contains("\"_ids\" AS (\nUPDATE") && out.sql.contains("\nFROM \""),
12061 "expected the _ids UPDATE to gain a FROM clause referencing the nested CTE, got:\n{}",
12062 out.sql,
12063 );
12064 assert!(
12065 out.sql.contains("\"_ml_add_0\""),
12066 "expected the junction-append CTE to still be present, got:\n{}",
12067 out.sql
12068 );
12069 }
12070
12071 #[test]
12072 fn test_update_link_value_from_nested_insert_combines_with_interface_target() {
12073 fn id_prop() -> PropertyDescriptor {
12079 PropertyDescriptor {
12080 name: "id".into(),
12081 pg_type: "uuid".into(),
12082 nullable: false,
12083 default_sql: Some("uuidv7()".into()),
12084 default_pyql: None,
12085 description: None,
12086 check_constraints: vec![],
12087 is_exclusive: true,
12088 is_pk: true,
12089 is_readonly: true,
12090 rewrites: vec![],
12091 tuple_members: None,
12092 column_type: None,
12093 }
12094 }
12095 fn text_prop(name: &str) -> PropertyDescriptor {
12096 PropertyDescriptor {
12097 name: name.into(),
12098 pg_type: "text".into(),
12099 nullable: false,
12100 default_sql: None,
12101 default_pyql: None,
12102 description: None,
12103 check_constraints: vec![],
12104 is_exclusive: false,
12105 is_pk: false,
12106 is_readonly: false,
12107 rewrites: vec![],
12108 tuple_members: None,
12109 column_type: None,
12110 }
12111 }
12112 fn company_link() -> LinkDescriptor {
12113 LinkDescriptor {
12114 name: "company".into(),
12115 target: "default::Company".into(),
12116 nullable: true,
12117 through: None,
12118 description: None,
12119 default_pyql: None,
12120 is_exclusive: false,
12121 is_readonly: false,
12122 rewrites: vec![],
12123 on_delete: vec![],
12124 }
12125 }
12126 let schema = SchemaDescriptor {
12127 types: vec![
12128 TypeDescriptor {
12129 name: "Company".into(),
12130 module: "default".into(),
12131 table: "Company".into(),
12132 abstract_: false,
12133 materialized: true,
12134 description: None,
12135 parents: vec![],
12136 interfaces: vec![],
12137 bases: vec![],
12138 properties: vec![id_prop(), text_prop("name")],
12139 links: vec![],
12140 multilinks: vec![],
12141 computed: vec![],
12142 constraints: vec![],
12143 indexes: vec![],
12144 partition: None,
12145 vector_indexes: vec![],
12146 search_indexes: vec![],
12147 triggers: vec![],
12148 junction: false,
12149 signals: vec![],
12150 },
12151 TypeDescriptor {
12152 name: "Account".into(),
12153 module: "default".into(),
12154 table: "Account".into(),
12155 abstract_: true,
12156 materialized: true,
12157 description: None,
12158 parents: vec![],
12159 interfaces: vec![],
12160 bases: vec![],
12161 properties: vec![id_prop(), text_prop("email")],
12162 links: vec![company_link()],
12163 multilinks: vec![],
12164 computed: vec![],
12165 constraints: vec![],
12166 indexes: vec![],
12167 partition: None,
12168 vector_indexes: vec![],
12169 search_indexes: vec![],
12170 triggers: vec![],
12171 junction: false,
12172 signals: vec![],
12173 },
12174 TypeDescriptor {
12175 name: "Individual".into(),
12176 module: "default".into(),
12177 table: "Individual".into(),
12178 abstract_: false,
12179 materialized: true,
12180 description: None,
12181 parents: vec![],
12182 interfaces: vec!["default::Account".into()],
12183 bases: vec![],
12184 properties: vec![id_prop(), text_prop("email"), text_prop("first_name")],
12185 links: vec![company_link()],
12186 multilinks: vec![],
12187 computed: vec![],
12188 constraints: vec![],
12189 indexes: vec![],
12190 partition: None,
12191 vector_indexes: vec![],
12192 search_indexes: vec![],
12193 triggers: vec![],
12194 junction: false,
12195 signals: vec![],
12196 },
12197 ],
12198 scalars: vec![],
12199 enums: vec![],
12200 named_tuples: vec![],
12201 globals: vec![],
12202 functions: vec![],
12203 aliases: vec![],
12204 channels: vec![],
12205 ..Default::default()
12206 };
12207 let out = compile_and_emit_with(
12208 "UPDATE Account FILTER .email = $email \
12209 SET { company := (select (insert Company { name := 'Acme' }) { id }) }",
12210 &schema,
12211 );
12212 assert!(out.sql.starts_with("WITH"), "got:\n{}", out.sql);
12213 assert!(
12214 out.sql.contains("INSERT INTO \"public\".\"Company\""),
12215 "expected the nested insert to be its own CTE, got:\n{}",
12216 out.sql,
12217 );
12218 assert!(
12219 out.sql.contains("UPDATE \"public\".\"Individual\"") && out.sql.contains("\nFROM \"_nested_dml_0\""),
12220 "expected the per-implementor UPDATE to gain a FROM clause referencing the nested CTE, got:\n{}",
12221 out.sql,
12222 );
12223 }
12224
12225 #[test]
12226 fn test_a_backlink_narrowed_to_a_supertype() {
12227 let mut schema = interface_link_schema();
12231 schema
12234 .types
12235 .iter_mut()
12236 .find(|t| t.name == "Account")
12237 .expect("the helper declares Account")
12238 .links
12239 .clear();
12240 let out = compile_and_emit_with("SELECT Company { owners := .<company[is Account] { email } }", &schema);
12241 assert!(
12242 out.sql.contains("\"public\".\"Individual\""),
12243 "the backlink must read the type that declares it:\n{}",
12244 out.sql
12245 );
12246 }
12247
12248 #[test]
12249 fn a_path_through_a_backlink_narrowed_to_a_supertype() {
12250 let mut schema = interface_link_schema();
12251 schema
12252 .types
12253 .iter_mut()
12254 .find(|t| t.name == "Account")
12255 .expect("the helper declares Account")
12256 .links
12257 .clear();
12258 let out = compile_and_emit_with(
12259 "SELECT Company { name } FILTER EXISTS (SELECT .<company[is Account])",
12260 &schema,
12261 );
12262 assert!(out.sql.contains("\"public\".\"Individual\""), "{}", out.sql);
12263 }
12264
12265 fn interface_link_schema() -> SchemaDescriptor {
12269 fn id_prop() -> PropertyDescriptor {
12275 PropertyDescriptor {
12276 name: "id".into(),
12277 pg_type: "uuid".into(),
12278 nullable: false,
12279 default_sql: None,
12280 default_pyql: None,
12281 description: None,
12282 check_constraints: vec![],
12283 is_exclusive: true,
12284 is_pk: true,
12285 is_readonly: true,
12286 rewrites: vec![],
12287 tuple_members: None,
12288 column_type: None,
12289 }
12290 }
12291 fn text_prop(name: &str) -> PropertyDescriptor {
12292 PropertyDescriptor {
12293 name: name.into(),
12294 pg_type: "text".into(),
12295 nullable: false,
12296 default_sql: None,
12297 default_pyql: None,
12298 description: None,
12299 check_constraints: vec![],
12300 is_exclusive: false,
12301 is_pk: false,
12302 is_readonly: false,
12303 rewrites: vec![],
12304 tuple_members: None,
12305 column_type: None,
12306 }
12307 }
12308 fn link(name: &str, target: &str) -> LinkDescriptor {
12309 LinkDescriptor {
12310 name: name.into(),
12311 target: target.into(),
12312 nullable: true,
12313 through: None,
12314 description: None,
12315 default_pyql: None,
12316 is_exclusive: false,
12317 is_readonly: false,
12318 rewrites: vec![],
12319 on_delete: vec![],
12320 }
12321 }
12322 fn ty(
12323 name: &str,
12324 abstract_: bool,
12325 interfaces: Vec<String>,
12326 properties: Vec<PropertyDescriptor>,
12327 links: Vec<LinkDescriptor>,
12328 ) -> TypeDescriptor {
12329 TypeDescriptor {
12330 name: name.into(),
12331 module: "default".into(),
12332 table: name.into(),
12333 abstract_,
12334 materialized: true,
12335 description: None,
12336 parents: vec![],
12337 interfaces,
12338 bases: vec![],
12339 properties,
12340 links,
12341 multilinks: vec![],
12342 computed: vec![],
12343 constraints: vec![],
12344 indexes: vec![],
12345 partition: None,
12346 vector_indexes: vec![],
12347 search_indexes: vec![],
12348 triggers: vec![],
12349 junction: false,
12350 signals: vec![],
12351 }
12352 }
12353 SchemaDescriptor {
12354 types: vec![
12355 ty("Company", false, vec![], vec![id_prop(), text_prop("name")], vec![]),
12356 ty(
12357 "Account",
12358 true,
12359 vec![],
12360 vec![id_prop(), text_prop("email")],
12361 vec![link("company", "default::Company")],
12362 ),
12363 ty(
12364 "Individual",
12365 false,
12366 vec!["default::Account".into()],
12367 vec![id_prop(), text_prop("email")],
12368 vec![link("company", "default::Company")],
12369 ),
12370 ty(
12371 "Token",
12372 false,
12373 vec![],
12374 vec![id_prop(), text_prop("value")],
12375 vec![link("account", "default::Account")],
12376 ),
12377 ],
12378 scalars: vec![],
12379 enums: vec![],
12380 named_tuples: vec![],
12381 globals: vec![],
12382 functions: vec![],
12383 aliases: vec![],
12384 channels: vec![],
12385 ..Default::default()
12386 }
12387 }
12388
12389 fn two_hop_interface_schema() -> SchemaDescriptor {
12393 let mut schema = interface_link_schema();
12394 let token = schema
12395 .types
12396 .iter()
12397 .find(|t| t.name == "Token")
12398 .expect("the helper declares Token")
12399 .clone();
12400 let mut connector = token.clone();
12401 connector.name = "Connector".into();
12402 connector.table = "Connector".into();
12403 connector.links[0].name = "provider".into();
12404 let mut installation = token;
12405 installation.name = "Installation".into();
12406 installation.table = "Installation".into();
12407 installation.links[0].name = "connector".into();
12408 installation.links[0].target = "default::Connector".into();
12409 schema.types.push(connector);
12410 schema.types.push(installation);
12411 schema
12412 }
12413
12414 #[test]
12415 fn a_narrowed_two_hop_walk_off_a_pinned_row_is_one_value() {
12416 let out = compile_and_emit_with(
12422 "SELECT { s := ((SELECT Installation FILTER .id = <uuid>$0).connector.provider[is Individual].email) ?? 'y' }",
12423 &two_hop_interface_schema(),
12424 );
12425 assert!(
12426 !out.sql.contains("ARRAY(SELECT"),
12427 "expected a value, not a set:\n{}",
12428 out.sql
12429 );
12430 assert!(
12431 out.sql.contains("COALESCE("),
12432 "the default must stay a scalar one:\n{}",
12433 out.sql
12434 );
12435 }
12436
12437 #[test]
12438 fn a_narrowed_two_hop_walk_off_an_unpinned_row_stays_a_set() {
12439 let out = compile_and_emit_with(
12440 "SELECT { s := (SELECT Installation FILTER .value = 'x').connector.provider[is Individual].email }",
12441 &two_hop_interface_schema(),
12442 );
12443 assert!(
12444 out.sql.contains("ARRAY(SELECT"),
12445 "a walk off many rows is a set:\n{}",
12446 out.sql
12447 );
12448 }
12449
12450 #[test]
12457 fn a_walk_ending_in_a_type_intersection_is_valid_in_a_filter() {
12458 let out = compile_and_emit_with(
12459 "SELECT Token { id } FILTER EXISTS .account[is Individual]",
12460 &interface_link_schema(),
12461 );
12462 assert!(
12463 out.sql.contains("\"public\".\"Individual\""),
12464 "the narrowing must reach the implementor's table:\n{}",
12465 out.sql
12466 );
12467 assert!(
12468 out.sql.contains("IS NOT NULL"),
12469 "exists over the narrowed walk is a nullness test:\n{}",
12470 out.sql
12471 );
12472 }
12473
12474 #[test]
12475 fn a_walk_ending_in_a_type_intersection_is_valid_off_a_binding() {
12476 let out = compile_and_emit_with(
12477 "WITH t := (SELECT Token LIMIT 1) SELECT { e := EXISTS t.account[is Individual] }",
12478 &interface_link_schema(),
12479 );
12480 assert!(
12481 out.sql.contains("\"public\".\"Individual\""),
12482 "the narrowing must reach the implementor's table:\n{}",
12483 out.sql
12484 );
12485 }
12486
12487 #[test]
12488 fn test_reading_a_link_through_an_interface_target() {
12489 let schema = interface_link_schema();
12490 let out = compile_and_emit_with("SELECT Token { account: { email, company: { name } } }", &schema);
12491 assert!(
12492 out.sql
12493 .contains("\"email\", \"company_id\" FROM \"public\".\"Individual\""),
12494 "the fanned-out interface must carry its own link columns:\n{}",
12495 out.sql
12496 );
12497 }
12498
12499 #[test]
12500 fn test_computed_pointer_in_shape_emits_expression() {
12501 let mut schema = make_schema();
12502 schema.types[0].computed.push(crate::schema::ComputedDescriptor {
12503 name: "upper_name".into(),
12504 expression: "str_upper(.name)".into(),
12505 return_type: Some("text".into()),
12506 link_target: None,
12507 link_multi: false,
12508 });
12509 let out = compile_and_emit_with("SELECT Person { upper_name }", &schema);
12510 assert!(
12511 out.sql.to_lowercase().contains("upper"),
12512 "expected upper() in SQL, got:\n{}",
12513 out.sql
12514 );
12515 }
12516
12517 fn make_interface_schema() -> SchemaDescriptor {
12520 fn id_prop() -> PropertyDescriptor {
12521 PropertyDescriptor {
12522 name: "id".into(),
12523 pg_type: "uuid".into(),
12524 nullable: false,
12525 default_sql: Some("uuidv7()".into()),
12526 default_pyql: None,
12527 description: None,
12528 check_constraints: vec![],
12529 is_exclusive: true,
12530 is_pk: true,
12531 is_readonly: true,
12532 rewrites: vec![],
12533 tuple_members: None,
12534 column_type: None,
12535 }
12536 }
12537 SchemaDescriptor {
12538 types: vec![
12539 TypeDescriptor {
12540 name: "Account".into(),
12541 module: "default".into(),
12542 table: "Account".into(),
12543 abstract_: true,
12544 materialized: false,
12545 description: None,
12546 parents: vec![],
12547 interfaces: vec![],
12548 bases: vec![],
12549 properties: vec![
12550 id_prop(),
12551 PropertyDescriptor {
12552 name: "email".into(),
12553 pg_type: "text".into(),
12554 nullable: false,
12555 default_sql: None,
12556 default_pyql: None,
12557 description: None,
12558 check_constraints: vec![],
12559 is_exclusive: false,
12560 is_pk: false,
12561 is_readonly: false,
12562 rewrites: vec![],
12563 tuple_members: None,
12564 column_type: None,
12565 },
12566 ],
12567 links: vec![],
12568 multilinks: vec![],
12569 computed: vec![],
12570 constraints: vec![],
12571 indexes: vec![],
12572 partition: None,
12573 vector_indexes: vec![],
12574 search_indexes: vec![],
12575 triggers: vec![],
12576 junction: false,
12577 signals: vec![],
12578 },
12579 TypeDescriptor {
12580 name: "Individual".into(),
12581 module: "default".into(),
12582 table: "Individual".into(),
12583 abstract_: false,
12584 materialized: true,
12585 description: None,
12586 parents: vec![],
12587 interfaces: vec!["default::Account".into()],
12588 bases: vec![],
12589 properties: vec![
12590 id_prop(),
12591 PropertyDescriptor {
12592 name: "first_name".into(),
12593 pg_type: "text".into(),
12594 nullable: false,
12595 default_sql: None,
12596 default_pyql: None,
12597 description: None,
12598 check_constraints: vec![],
12599 is_exclusive: false,
12600 is_pk: false,
12601 is_readonly: false,
12602 rewrites: vec![],
12603 tuple_members: None,
12604 column_type: None,
12605 },
12606 ],
12607 links: vec![],
12608 multilinks: vec![],
12609 computed: vec![crate::schema::ComputedDescriptor {
12610 name: "full_name".into(),
12611 expression: "str_upper(.first_name)".into(),
12612 return_type: Some("text".into()),
12613 link_target: None,
12614 link_multi: false,
12615 }],
12616 constraints: vec![],
12617 indexes: vec![],
12618 partition: None,
12619 vector_indexes: vec![],
12620 search_indexes: vec![],
12621 triggers: vec![],
12622 junction: false,
12623 signals: vec![],
12624 },
12625 ],
12626 scalars: vec![],
12627 enums: vec![],
12628 named_tuples: vec![],
12629 globals: vec![],
12630 functions: vec![],
12631 aliases: vec![],
12632 channels: vec![],
12633 ..Default::default()
12634 }
12635 }
12636
12637 #[test]
12638 fn test_type_intersection_splat_includes_concrete_computed_pointers() {
12639 let schema = make_interface_schema();
12644 let out = compile_and_emit_with("SELECT Account { *, [is Individual].* }", &schema);
12645 assert!(
12646 out.sql.to_lowercase().contains("upper"),
12647 "expected the concrete type's computed pointer (str_upper(...)) in the shape, got:\n{}",
12648 out.sql
12649 );
12650 assert!(
12651 out.sql.contains("\"first_name\""),
12652 "expected the concrete type's stored property too, got:\n{}",
12653 out.sql
12654 );
12655 }
12656
12657 #[test]
12658 fn test_enum_array_property_casts_to_text_array_and_shapes_as_a_list() {
12659 let mut schema = make_schema();
12666 schema.enums.push(crate::schema::EnumDescriptor {
12667 name: "Gender".into(),
12668 module: "default".into(),
12669 members: vec!["Male".into(), "Female".into()],
12670 });
12671 schema.types[0].properties.push(crate::schema::PropertyDescriptor {
12672 name: "genders".into(),
12673 pg_type: r#""default"."Gender"[]"#.into(),
12674 nullable: true,
12675 default_sql: None,
12676 default_pyql: None,
12677 description: None,
12678 check_constraints: vec![],
12679 is_exclusive: false,
12680 is_pk: false,
12681 is_readonly: false,
12682 rewrites: vec![],
12683 tuple_members: None,
12684 column_type: None,
12685 });
12686
12687 let out = compile_and_emit_with("SELECT Person { genders }", &schema);
12688 assert!(
12689 out.sql.contains(r#""genders"::text[]"#),
12690 "expected a text[] cast, got:\n{}",
12691 out.sql
12692 );
12693
12694 let crate::query::ShapeNode::Object { pointers, .. } = &out.shape.root else {
12695 panic!("expected Object shape, got {:?}", out.shape.root)
12696 };
12697 let genders = pointers
12698 .iter()
12699 .find(|node| matches!(node, crate::query::ShapeNode::Array { name, .. } if name == "genders"))
12700 .unwrap_or_else(|| panic!("expected an Array-shaped pointer, got {pointers:?}"));
12701 let crate::query::ShapeNode::Array { element, .. } = genders else {
12702 unreachable!()
12703 };
12704 assert!(
12705 matches!(
12706 element.as_ref(),
12707 crate::query::ShapeNode::Enum { enum_type, position: 0, .. }
12708 if enum_type == "default::Gender"
12709 ),
12710 "expected the elements to be enum-shaped, got {element:?}",
12711 );
12712 }
12713
12714 #[test]
12715 fn test_scalar_enum_property_still_casts_to_a_single_text() {
12716 let mut schema = make_schema();
12717 schema.enums.push(crate::schema::EnumDescriptor {
12718 name: "Gender".into(),
12719 module: "default".into(),
12720 members: vec!["Male".into(), "Female".into()],
12721 });
12722 schema.types[0].properties.push(crate::schema::PropertyDescriptor {
12723 name: "gender".into(),
12724 pg_type: r#""default"."Gender""#.into(),
12725 nullable: true,
12726 default_sql: None,
12727 default_pyql: None,
12728 description: None,
12729 check_constraints: vec![],
12730 is_exclusive: false,
12731 is_pk: false,
12732 is_readonly: false,
12733 rewrites: vec![],
12734 tuple_members: None,
12735 column_type: None,
12736 });
12737
12738 let out = compile_and_emit_with("SELECT Person { gender }", &schema);
12739 assert!(
12740 out.sql.contains(r#""gender"::text"#) && !out.sql.contains(r#""gender"::text[]"#),
12741 "expected a plain text cast, got:\n{}",
12742 out.sql
12743 );
12744 let crate::query::ShapeNode::Object { pointers, .. } = &out.shape.root else {
12745 panic!("expected Object shape")
12746 };
12747 assert!(
12748 pointers.iter().any(|node| matches!(
12749 node,
12750 crate::query::ShapeNode::Enum { name, enum_type, .. }
12751 if name == "gender" && enum_type == "default::Gender"
12752 )),
12753 "expected an Enum-shaped pointer, got {pointers:?}",
12754 );
12755 }
12756
12757 #[test]
12758 fn test_type_intersection_splat_keeps_enum_pointers_enum_shaped() {
12759 let mut schema = make_interface_schema();
12764 schema.enums.push(crate::schema::EnumDescriptor {
12765 name: "Gender".into(),
12766 module: "default".into(),
12767 members: vec!["Male".into(), "Female".into()],
12768 });
12769 let individual = schema
12772 .types
12773 .iter_mut()
12774 .find(|t| t.name == "Individual")
12775 .expect("the interface schema has an Individual type");
12776 individual.properties.push(crate::schema::PropertyDescriptor {
12777 name: "gender".into(),
12778 pg_type: r#""default"."Gender""#.into(),
12779 nullable: true,
12780 default_sql: None,
12781 default_pyql: None,
12782 description: None,
12783 check_constraints: vec![],
12784 is_exclusive: false,
12785 is_pk: false,
12786 is_readonly: false,
12787 rewrites: vec![],
12788 tuple_members: None,
12789 column_type: None,
12790 });
12791
12792 let out = compile_and_emit_with("SELECT Account { [is Individual].* }", &schema);
12793 let crate::query::ShapeNode::Object { pointers, .. } = &out.shape.root else {
12794 panic!("expected Object shape, got {:?}", out.shape.root)
12795 };
12796 assert!(
12797 pointers.iter().any(|node| matches!(
12798 node,
12799 crate::query::ShapeNode::Enum { name, enum_type, .. }
12800 if name == "gender" && enum_type == "default::Gender"
12801 )),
12802 "expected the enum property to stay enum-shaped through the intersection, got {pointers:?}",
12803 );
12804 }
12805
12806 #[test]
12807 fn test_shallow_splat_leaves_object_valued_computeds_to_the_deep_form() {
12808 let mut schema = make_schema();
12813 schema.types[0].computed.push(crate::schema::ComputedDescriptor {
12814 name: "authors".into(),
12815 expression: ".posts".into(),
12816 return_type: None,
12817 link_target: None,
12818 link_multi: false,
12819 });
12820 schema.types[0].computed.push(crate::schema::ComputedDescriptor {
12821 name: "age_next".into(),
12822 expression: ".age + 1".into(),
12823 return_type: Some("int8".into()),
12824 link_target: None,
12825 link_multi: false,
12826 });
12827
12828 let shallow = compile_and_emit_with("SELECT Person { * }", &schema);
12829 let crate::query::ShapeNode::Object { pointers, .. } = &shallow.shape.root else {
12830 panic!("expected Object shape")
12831 };
12832 let names: Vec<&str> = pointers.iter().map(shape_pointer_name).collect();
12833 assert!(
12834 names.contains(&"age_next"),
12835 "a computed property belongs in `*`: {names:?}"
12836 );
12837 assert!(!names.contains(&"authors"), "a computed link does not: {names:?}");
12838
12839 let deep = compile_and_emit_with("SELECT Person { ** }", &schema);
12840 let crate::query::ShapeNode::Object { pointers, .. } = &deep.shape.root else {
12841 panic!("expected Object shape")
12842 };
12843 let names: Vec<&str> = pointers.iter().map(shape_pointer_name).collect();
12844 assert!(
12845 names.contains(&"authors"),
12846 "`**` includes links, computed ones included: {names:?}"
12847 );
12848 }
12849
12850 fn shape_pointer_name(node: &crate::query::ShapeNode) -> &str {
12851 use crate::query::ShapeNode;
12852 match node {
12853 ShapeNode::Scalar { name, .. }
12854 | ShapeNode::Enum { name, .. }
12855 | ShapeNode::NamedTuple { name, .. }
12856 | ShapeNode::Object { name, .. }
12857 | ShapeNode::Array { name, .. } => name,
12858 _ => "",
12859 }
12860 }
12861
12862 #[test]
12863 fn test_any_over_a_multilink_comparison_does_not_warn() {
12864 let schema = make_schema();
12869 let ast = parse::parse("SELECT Person FILTER any(.posts.title = 'x')").unwrap();
12870 let ir = ir::compile(&ast, &schema).unwrap();
12871 assert!(
12872 !ir.warnings.iter().any(|w| w.contains("FILTER clause")),
12873 "any() states the intent, so nothing should warn: {:?}",
12874 ir.warnings
12875 );
12876 }
12877
12878 #[test]
12879 fn test_bare_multilink_comparison_still_warns() {
12880 let schema = make_schema();
12881 let ast = parse::parse("SELECT Person FILTER .posts.title = 'x'").unwrap();
12882 let ir = ir::compile(&ast, &schema).unwrap();
12883 assert!(
12884 ir.warnings.iter().any(|w| w.contains("FILTER clause")),
12885 "an unwrapped set-valued comparison still warns: {:?}",
12886 ir.warnings
12887 );
12888 }
12889
12890 #[test]
12891 fn test_link_to_an_interface_expands_over_its_implementors() {
12892 let mut schema = make_interface_schema();
12898 let account = schema
12899 .types
12900 .iter_mut()
12901 .find(|t| t.name == "Account")
12902 .expect("the interface schema has an Account type");
12903 account.materialized = true;
12904 let mut organization = schema
12906 .types
12907 .iter()
12908 .find(|t| t.name == "Individual")
12909 .expect("the interface schema has an Individual type")
12910 .clone();
12911 organization.name = "Organization".into();
12912 organization.table = "Organization".into();
12913 organization.computed.clear();
12914 schema.types.push(organization);
12915
12916 let owner = TypeDescriptor {
12917 name: "Note".into(),
12918 module: "default".into(),
12919 table: "Note".into(),
12920 abstract_: false,
12921 materialized: false,
12922 description: None,
12923 parents: vec![],
12924 interfaces: vec![],
12925 bases: vec![],
12926 properties: vec![],
12927 links: vec![LinkDescriptor {
12928 name: "owner".into(),
12929 target: "default::Account".into(),
12930 nullable: true,
12931 description: None,
12932 default_pyql: None,
12933 is_exclusive: false,
12934 is_readonly: false,
12935 rewrites: vec![],
12936 on_delete: vec![],
12937 through: None,
12938 }],
12939 multilinks: vec![],
12940 computed: vec![],
12941 constraints: vec![],
12942 indexes: vec![],
12943 partition: None,
12944 vector_indexes: vec![],
12945 search_indexes: vec![],
12946 triggers: vec![],
12947 junction: false,
12948 signals: vec![],
12949 };
12950 schema.types.push(owner);
12951
12952 let out = compile_and_emit_with("SELECT Note { owner: { id } }", &schema);
12953 assert!(
12954 out.sql.contains("UNION ALL"),
12955 "the link's target should expand over its implementors, got:\n{}",
12956 out.sql
12957 );
12958 assert!(
12959 out.sql.contains("'default::Individual'::text AS \"__type__\""),
12960 "each branch should carry its own discriminator, got:\n{}",
12961 out.sql
12962 );
12963 assert!(
12964 !out.sql.contains("FROM \"default\".\"Account\" AS"),
12965 "the interface's own view should no longer be read directly, got:\n{}",
12966 out.sql
12967 );
12968 }
12969
12970 fn make_two_implementor_schema() -> SchemaDescriptor {
12973 let mut schema = make_interface_schema();
12974 let mut organization = schema
12975 .types
12976 .iter()
12977 .find(|t| t.name == "Individual")
12978 .expect("the interface schema has an Individual type")
12979 .clone();
12980 organization.name = "Organization".into();
12981 organization.table = "Organization".into();
12982 organization.computed.clear();
12983 schema.types.push(organization);
12984 schema
12985 }
12986
12987 fn assert_rows_carry_their_concrete_type(query: &str) {
12990 let out = compile_and_emit_with(query, &make_two_implementor_schema());
12991 assert!(
12992 !out.sql.contains("'default::Account'::text"),
12993 "rows are labelled with the interface instead of their own type:\n{}",
12994 out.sql
12995 );
12996 assert!(
12997 out.sql.contains("'default::Individual'::text AS \"__type__\""),
12998 "the rows should come from the implementors, each tagging its own:\n{}",
12999 out.sql
13000 );
13001 }
13002
13003 #[test]
13004 fn an_aggregated_select_of_an_interface_carries_the_concrete_type() {
13005 assert_rows_carry_their_concrete_type("SELECT (xs := array_agg((SELECT Account { id })))");
13006 }
13007
13008 #[test]
13009 fn an_aggregated_binding_of_an_interface_carries_the_concrete_type() {
13010 assert_rows_carry_their_concrete_type("WITH a := (SELECT Account) SELECT (xs := array_agg((SELECT a { id })))");
13011 }
13012
13013 #[test]
13014 fn a_single_object_read_off_a_binding_carries_the_concrete_type() {
13015 assert_rows_carry_their_concrete_type("WITH a := (SELECT Account) SELECT (x := (SELECT a { id } LIMIT 1))");
13016 }
13017
13018 #[test]
13019 fn grouped_elements_of_an_interface_carry_the_concrete_type() {
13020 assert_rows_carry_their_concrete_type("group Account { id } by .email");
13021 }
13022
13023 fn make_note_schema() -> SchemaDescriptor {
13026 fn link_to(name: &str, target: &str) -> LinkDescriptor {
13027 LinkDescriptor {
13028 name: name.into(),
13029 target: target.into(),
13030 nullable: true,
13031 description: None,
13032 default_pyql: None,
13033 is_exclusive: false,
13034 is_readonly: false,
13035 rewrites: vec![],
13036 on_delete: vec![],
13037 through: None,
13038 }
13039 }
13040 let mut schema = make_two_implementor_schema();
13041 for account in schema
13042 .types
13043 .iter_mut()
13044 .filter(|t| ["Account", "Individual", "Organization"].contains(&t.name.as_str()))
13045 {
13046 account.links.push(link_to("pinned", "default::Note"));
13047 }
13048 schema.types.push(TypeDescriptor {
13049 name: "Note".into(),
13050 module: "default".into(),
13051 table: "Note".into(),
13052 abstract_: false,
13053 materialized: false,
13054 description: None,
13055 parents: vec![],
13056 interfaces: vec![],
13057 bases: vec![],
13058 properties: vec![],
13059 links: vec![link_to("owner", "default::Account")],
13060 multilinks: vec![],
13061 computed: vec![],
13062 constraints: vec![],
13063 indexes: vec![],
13064 partition: None,
13065 vector_indexes: vec![],
13066 search_indexes: vec![],
13067 triggers: vec![],
13068 junction: false,
13069 signals: vec![],
13070 });
13071 schema
13072 }
13073
13074 #[test]
13075 fn a_backlink_to_an_interface_carries_the_concrete_type() {
13076 let out = compile_and_emit_with(
13077 "SELECT Note { pinners := .<pinned[is Account] { id } }",
13078 &make_note_schema(),
13079 );
13080 assert!(
13081 !out.sql.contains("'default::Account'::text"),
13082 "the backlink's rows are labelled with the interface instead of their own type:\n{}",
13083 out.sql
13084 );
13085 }
13086
13087 #[test]
13088 fn a_link_reached_through_a_deep_splat_carries_the_concrete_type() {
13089 let out = compile_and_emit_with("SELECT Note { ** }", &make_note_schema());
13090 assert!(
13091 !out.sql.contains("'default::Account'::text"),
13092 "the splatted link's rows are labelled with the interface instead of their own type:\n{}",
13093 out.sql
13094 );
13095 }
13096
13097 #[test]
13098 fn test_path_traversal_onto_an_interface_carries_the_concrete_type() {
13099 let mut schema = make_interface_schema();
13104 let account = schema
13105 .types
13106 .iter_mut()
13107 .find(|t| t.name == "Account")
13108 .expect("the interface schema has an Account type");
13109 account.materialized = true;
13110 let mut organization = schema
13111 .types
13112 .iter()
13113 .find(|t| t.name == "Individual")
13114 .expect("the interface schema has an Individual type")
13115 .clone();
13116 organization.name = "Organization".into();
13117 organization.table = "Organization".into();
13118 organization.computed.clear();
13119 schema.types.push(organization);
13120 schema.types.push(TypeDescriptor {
13121 name: "Note".into(),
13122 module: "default".into(),
13123 table: "Note".into(),
13124 abstract_: false,
13125 materialized: false,
13126 description: None,
13127 parents: vec![],
13128 interfaces: vec![],
13129 bases: vec![],
13130 properties: vec![],
13131 links: vec![LinkDescriptor {
13132 name: "owner".into(),
13133 target: "default::Account".into(),
13134 nullable: true,
13135 description: None,
13136 default_pyql: None,
13137 is_exclusive: false,
13138 is_readonly: false,
13139 rewrites: vec![],
13140 on_delete: vec![],
13141 through: None,
13142 }],
13143 multilinks: vec![],
13144 computed: vec![],
13145 constraints: vec![],
13146 indexes: vec![],
13147 partition: None,
13148 vector_indexes: vec![],
13149 search_indexes: vec![],
13150 triggers: vec![],
13151 junction: false,
13152 signals: vec![],
13153 });
13154
13155 let out = compile_and_emit_with("SELECT Note.owner { id }", &schema);
13156 assert!(
13157 out.sql.contains("UNION ALL") && out.sql.contains("'default::Individual'::text AS \"__type__\""),
13158 "the traversal's target should fan out over implementors, got:\n{}",
13159 out.sql
13160 );
13161 assert!(
13162 !out.sql.contains("'default::Account'::text,"),
13163 "the row's type should be read off the row, not fixed to the interface, got:\n{}",
13164 out.sql
13165 );
13166 }
13167
13168 #[test]
13169 fn test_computed_multilink_chain_comes_back_as_rows() {
13170 let mut schema = make_schema();
13175 schema.types[0].computed.push(crate::schema::ComputedDescriptor {
13176 name: "coauthors".into(),
13177 expression: ".posts.author".into(),
13178 return_type: None,
13179 link_target: None,
13180 link_multi: false,
13181 });
13182 let post = schema
13183 .types
13184 .iter_mut()
13185 .find(|t| t.name == "Post")
13186 .expect("test schema has a Post type");
13187 post.links.push(LinkDescriptor {
13188 name: "author".into(),
13189 target: "default::Person".into(),
13190 nullable: true,
13191 description: None,
13192 default_pyql: None,
13193 is_exclusive: false,
13194 is_readonly: false,
13195 rewrites: vec![],
13196 on_delete: vec![],
13197 through: None,
13198 });
13199
13200 let out = compile_and_emit_with("SELECT Person { coauthors: { name } }", &schema);
13201 assert!(
13202 out.sql.contains("ARRAY(SELECT"),
13203 "the chain should aggregate into an array, got:\n{}",
13204 out.sql
13205 );
13206 assert!(
13207 out.sql.contains("\"name\""),
13208 "the requested sub-shape should survive, got:\n{}",
13209 out.sql
13210 );
13211
13212 let crate::query::ShapeNode::Object { pointers, .. } = &out.shape.root else {
13213 panic!("expected Object shape, got {:?}", out.shape.root)
13214 };
13215 let coauthors = pointers
13216 .iter()
13217 .find(|node| matches!(node, crate::query::ShapeNode::Array { name, .. } if name == "coauthors"))
13218 .unwrap_or_else(|| panic!("expected an Array-shaped pointer, got {pointers:?}"));
13219 let crate::query::ShapeNode::Array { element, .. } = coauthors else {
13220 unreachable!()
13221 };
13222 assert!(
13223 matches!(
13224 element.as_ref(),
13225 crate::query::ShapeNode::Object { type_name: Some(t), .. } if t == "default::Person"
13226 ),
13227 "the elements should hydrate as objects, got {element:?}",
13228 );
13229 }
13230
13231 #[test]
13232 fn test_multi_sort_with_then_emits_two_order_keys() {
13233 let out = compile_and_emit("SELECT Person { name } ORDER BY .name THEN .age DESC");
13234 assert!(out.sql.contains("ORDER BY"), "expected ORDER BY");
13235 assert!(out.sql.contains("\"name\""));
13237 assert!(out.sql.contains("\"age\""));
13238 assert!(out.sql.contains("DESC"));
13239 }
13240
13241 #[test]
13242 fn test_string_index_emits_str_subscript() {
13243 let out = compile_and_emit("SELECT 'hello'[1]");
13244 assert!(
13245 out.sql.contains("_pylon.str_subscript"),
13246 "expected _pylon.str_subscript() for string index, got:\n{}",
13247 out.sql
13248 );
13249 }
13250
13251 #[test]
13254 fn test_for_over_a_set_returning_call_is_not_a_values_clause() {
13255 let out = compile_and_emit("FOR n IN array_unpack(<array<int64>>$ns) UNION (SELECT n)");
13256 assert!(
13257 !out.sql.contains("VALUES (unnest"),
13258 "a set-returning iterator must not land in VALUES, got:\n{}",
13259 out.sql
13260 );
13261 assert!(
13262 out.sql.contains("SELECT unnest"),
13263 "expected the iterator in a select list, got:\n{}",
13264 out.sql
13265 );
13266 }
13267
13268 #[test]
13271 fn test_for_over_array_unpack_binds_the_element_type() {
13272 let out = compile_and_emit("FOR j IN array_unpack(<array<json>>$rows) UNION (SELECT <str>j['k'])");
13273 assert!(
13274 out.sql.contains("::jsonb AS v"),
13275 "the loop variable should carry the element type, got:\n{}",
13276 out.sql
13277 );
13278 }
13279
13280 #[test]
13283 fn test_indexing_json_emits_a_jsonb_accessor() {
13284 let out = compile_and_emit("SELECT (<json>$payload)['label']");
13285 assert!(
13286 out.sql.contains("->'label'"),
13287 "expected a jsonb field access, got:\n{}",
13288 out.sql
13289 );
13290 assert!(
13291 !out.sql.contains("str_subscript"),
13292 "json must not be subscripted as a string, got:\n{}",
13293 out.sql
13294 );
13295 }
13296
13297 #[test]
13300 fn test_casting_json_to_a_scalar_unwraps_it() {
13301 let out = compile_and_emit("SELECT <str>to_json(<str>$s)");
13302 assert!(
13303 out.sql.contains("#>> '{}'"),
13304 "expected the json value to be extracted, got:\n{}",
13305 out.sql
13306 );
13307 }
13308
13309 #[test]
13313 fn test_a_binding_that_reads_the_loop_variable_is_keyed_by_iteration() {
13314 let out = compile_and_emit(
13315 "WITH made := (FOR p IN (SELECT Person) UNION ( \
13316 WITH mine := (SELECT p.posts LIMIT 1) \
13317 INSERT Company { name := mine.title } \
13318 )) SELECT count(made)",
13319 );
13320 assert!(
13321 out.sql.contains("\"mine\" AS (\nSELECT \"_for_p\".\"v\" AS \"_outer\""),
13322 "the binding must be evaluated per iteration, got:\n{}",
13323 out.sql
13324 );
13325 assert!(
13326 out.sql.contains("\"mine\".\"_outer\" = \"_for_p\".\"v\""),
13327 "a read of it must pin to the iteration in scope, got:\n{}",
13328 out.sql
13329 );
13330 }
13331
13332 #[test]
13336 fn test_a_loop_nested_in_a_loop_pairs_its_rows_by_iteration() {
13337 let out = compile_and_emit(
13338 "FOR n IN {'a', 'b'} UNION (INSERT Person { name := n, posts := (FOR t IN {n} UNION (INSERT Post { title := t })) })",
13339 );
13340 assert!(
13341 out.sql.contains("\"_outer\""),
13342 "the nested rows must carry an iteration key, got:\n{}",
13343 out.sql
13344 );
13345 assert!(
13346 !out.sql.contains("CROSS JOIN \"_nested_dml_0\""),
13347 "they must not be cross-joined onto every row, got:\n{}",
13348 out.sql
13349 );
13350 }
13351
13352 #[test]
13355 fn test_for_bodied_insert_writes_its_multi_link() {
13356 let out = compile_and_emit("FOR n IN {'a', 'b'} UNION (INSERT Person { name := n, posts := (SELECT Post) })");
13357 assert!(
13358 out.sql.contains("INSERT INTO \"public\".\"Person.posts\""),
13359 "the junction rows must be written too, got:\n{}",
13360 out.sql
13361 );
13362 }
13363
13364 #[test]
13365 fn test_string_slice_emits_substr() {
13366 let out = compile_and_emit("SELECT 'hello'[1:3]");
13367 assert!(
13368 out.sql.contains("substr"),
13369 "expected substr() for string slice, got:\n{}",
13370 out.sql
13371 );
13372 }
13373
13374 #[test]
13375 fn test_array_index_emits_subscript() {
13376 let out = compile_and_emit("SELECT [1, 2, 3][1]");
13377 assert!(
13378 out.sql.contains("_pylon.array_subscript"),
13379 "expected _pylon.array_subscript() for array index, got:\n{}",
13380 out.sql
13381 );
13382 }
13383
13384 #[test]
13385 fn test_index_into_array_returning_function_emits_array_subscript() {
13386 let out = compile_and_emit("SELECT str_split('a::b', '::')[-1]");
13387 assert!(
13388 out.sql.contains("_pylon.array_subscript(string_to_array"),
13389 "expected _pylon.array_subscript() over str_split, got:\n{}",
13390 out.sql
13391 );
13392 }
13393
13394 #[test]
13395 fn test_array_slice_emits_subscript() {
13396 let out = compile_and_emit("SELECT [1, 2, 3][0:2]");
13397 assert!(
13398 !out.sql.contains("substr"),
13399 "should not use substr for array, got:\n{}",
13400 out.sql
13401 );
13402 assert!(out.sql.contains(")["), "expected array slice syntax, got:\n{}", out.sql);
13403 }
13404
13405 #[test]
13406 fn test_open_ended_string_slice_emits_substr_no_length() {
13407 let out = compile_and_emit("SELECT 'hello'[2:]");
13408 assert!(out.sql.contains("substr"), "expected substr(), got:\n{}", out.sql);
13410 let substr_idx = out.sql.find("substr").unwrap();
13412 let after = &out.sql[substr_idx..];
13413 let commas = after.chars().take_while(|&c| c != ')').filter(|&c| c == ',').count();
13414 assert_eq!(commas, 1, "open-ended slice should use 2-arg substr, got:\n{}", out.sql);
13415 }
13416
13417 #[test]
13418 fn test_group_by_single_key() {
13419 let out = compile_and_emit("group Person { name } by .age");
13420 assert!(out.sql.contains("GROUP BY"), "expected GROUP BY, got:\n{}", out.sql);
13422 assert!(out.sql.contains("\"age\""), "expected age column, got:\n{}", out.sql);
13424 assert!(
13426 out.sql.contains("array_agg(ROW("),
13427 "expected array_agg, got:\n{}",
13428 out.sql
13429 );
13430 assert!(
13432 out.sql.contains("ARRAY['age']"),
13433 "expected grouping array, got:\n{}",
13434 out.sql
13435 );
13436 assert!(matches!(out.shape.root, crate::query::ShapeNode::Group { .. }));
13438 if let crate::query::ShapeNode::Group {
13439 key_nodes,
13440 grouping_position,
13441 elements_position,
13442 ..
13443 } = &out.shape.root
13444 {
13445 assert_eq!(key_nodes.len(), 1);
13446 assert!(matches!(&key_nodes[0], crate::query::ShapeNode::Scalar { name, position: 1 } if name == "age"));
13447 assert_eq!(*grouping_position, 2);
13448 assert_eq!(*elements_position, 3);
13449 }
13450 }
13451
13452 #[test]
13453 fn test_group_using_alias() {
13454 let out = compile_and_emit("group Person using decade := .age // 10 by decade");
13455 assert!(out.sql.contains("GROUP BY"), "expected GROUP BY, got:\n{}", out.sql);
13456 assert!(
13457 out.sql.contains("ARRAY['decade']"),
13458 "expected grouping array, got:\n{}",
13459 out.sql
13460 );
13461 if let crate::query::ShapeNode::Group { key_nodes, .. } = &out.shape.root {
13462 assert_eq!(key_nodes.len(), 1);
13463 assert!(matches!(&key_nodes[0], crate::query::ShapeNode::Scalar { name, .. } if name == "decade"));
13464 }
13465 }
13466
13467 #[test]
13468 fn test_group_orders_elements_within_each_group() {
13469 let out = compile_and_emit("group Person { name } by .age order by .name desc");
13470 assert!(
13471 out.sql.contains(")::record ORDER BY \"t0\".\"name\" DESC NULLS LAST)"),
13472 "{}",
13473 out.sql
13474 );
13475 assert!(!out.sql.contains("row_number()"), "{}", out.sql);
13476 }
13477
13478 #[test]
13479 fn test_group_limit_trims_each_group_not_the_result() {
13480 let out = compile_and_emit("group Person { name } by .age order by .name desc limit 1");
13483 assert!(
13484 out.sql
13485 .contains("row_number() OVER (PARTITION BY \"t0\".\"age\" ORDER BY \"t0\".\"name\" DESC NULLS LAST)"),
13486 "{}",
13487 out.sql
13488 );
13489 assert!(
13490 out.sql.contains("WHERE \"__rk\" > 0 AND \"__rk\" <= 0 + 1"),
13491 "{}",
13492 out.sql
13493 );
13494 assert!(!out.sql.contains("\nLIMIT"), "{}", out.sql);
13495 }
13496
13497 #[test]
13498 fn test_group_by_two_keys_with_a_per_group_limit() {
13499 let out = compile_and_emit(
13500 "group Person { name } by .age, .name filter .age > 18 order by .name desc offset 1 limit 2",
13501 );
13502 assert!(
13503 out.sql.contains("PARTITION BY \"t0\".\"age\", \"t0\".\"name\""),
13504 "{}",
13505 out.sql
13506 );
13507 let where_pos = out.sql.find("WHERE (\"t0\".\"age\" > 18)").expect("filter");
13510 let rank_pos = out.sql.find("WHERE \"__rk\"").expect("rank filter");
13511 assert!(where_pos < rank_pos, "{}", out.sql);
13512 assert!(
13513 out.sql.contains("GROUP BY \"t0\".\"age\", \"t0\".\"name\""),
13514 "{}",
13515 out.sql
13516 );
13517 }
13518
13519 #[test]
13520 fn test_group_filter_without_a_limit_is_a_plain_where() {
13521 let out = compile_and_emit("group Person { name } by .age filter .age > 18");
13522 assert!(out.sql.contains("WHERE (\"t0\".\"age\" > 18)"), "{}", out.sql);
13523 assert!(!out.sql.contains("row_number()"), "{}", out.sql);
13524 }
13525
13526 #[test]
13527 fn test_abs_path_concat_same_type() {
13528 let out = compile_and_emit("SELECT Person.name ++ ' ' ++ Person.name");
13529 assert!(out.sql.contains("\"name\""), "expected name column, got:\n{}", out.sql);
13530 assert!(out.sql.contains("||"), "expected concat operator, got:\n{}", out.sql);
13531 assert!(out.sql.contains("FROM"), "expected FROM clause, got:\n{}", out.sql);
13532 }
13533
13534 #[test]
13535 fn test_abs_path_single_property() {
13536 let out = compile_and_emit("SELECT Person.name");
13537 assert!(out.sql.contains("\"name\""), "expected name column, got:\n{}", out.sql);
13538 assert!(out.sql.contains("FROM"), "expected FROM clause, got:\n{}", out.sql);
13539 }
13540
13541 #[test]
13542 fn test_pgvector_cast_emits_vector_type() {
13543 let out = compile_and_emit("SELECT <pgvector::vector>[1.0, 2.0, 3.0]");
13544 assert!(
13545 out.sql.contains("::vector"),
13546 "expected ::vector cast, got:\n{}",
13547 out.sql
13548 );
13549 assert!(out.sql.contains("ARRAY["), "expected ARRAY literal, got:\n{}", out.sql);
13550 }
13551
13552 #[test]
13553 fn test_pgvector_euclidean_distance_emits_l2_operator() {
13554 let out = compile_and_emit(
13555 "SELECT pgvector::euclidean_distance(<pgvector::vector>[1.0, 2.0], <pgvector::vector>[3.0, 4.0])",
13556 );
13557 assert!(out.sql.contains("<->"), "expected <-> operator, got:\n{}", out.sql);
13558 }
13559
13560 #[test]
13561 fn test_pgvector_cosine_distance_emits_cosine_operator() {
13562 let out = compile_and_emit(
13563 "SELECT pgvector::cosine_distance(<pgvector::vector>[1.0, 2.0], <pgvector::vector>[3.0, 4.0])",
13564 );
13565 assert!(out.sql.contains("<=>"), "expected <=> operator, got:\n{}", out.sql);
13566 }
13567
13568 #[test]
13569 fn test_pgvector_neg_inner_product_emits_ip_operator() {
13570 let out = compile_and_emit(
13571 "SELECT pgvector::neg_inner_product(<pgvector::vector>[1.0, 2.0], <pgvector::vector>[3.0, 4.0])",
13572 );
13573 assert!(out.sql.contains("<#>"), "expected <#> operator, got:\n{}", out.sql);
13574 }
13575
13576 #[test]
13577 fn test_pgvector_inner_product_negates_ip_operator() {
13578 let out = compile_and_emit(
13579 "SELECT pgvector::inner_product(<pgvector::vector>[1.0, 2.0], <pgvector::vector>[3.0, 4.0])",
13580 );
13581 assert!(out.sql.contains("<#>"), "expected <#> operator, got:\n{}", out.sql);
13582 assert!(out.sql.contains("0.0"), "expected negation of <#>, got:\n{}", out.sql);
13583 }
13584
13585 #[test]
13586 fn test_crypto_digest_str_and_bytes_overloads_both_use_pgcrypto_digest() {
13587 let out = compile_and_emit("SELECT crypto::digest('hello', 'sha256')");
13588 assert!(
13589 out.sql.contains("digest("),
13590 "expected pgcrypto's digest(), got:\n{}",
13591 out.sql
13592 );
13593
13594 let out = compile_and_emit("SELECT crypto::digest(std::from_hex('68656c6c6f'), 'sha256')");
13595 assert!(
13596 out.sql.contains("digest("),
13597 "expected pgcrypto's digest(), got:\n{}",
13598 out.sql
13599 );
13600 }
13601
13602 #[test]
13603 fn test_crypto_hmac_str_and_bytes_overloads_both_use_pgcrypto_hmac() {
13604 let out = compile_and_emit("SELECT crypto::hmac('hello', 'key', 'sha256')");
13605 assert!(
13606 out.sql.contains("hmac("),
13607 "expected pgcrypto's hmac(), got:\n{}",
13608 out.sql
13609 );
13610
13611 let out =
13612 compile_and_emit("SELECT crypto::hmac(std::from_hex('68656c6c6f'), std::from_hex('6b6579'), 'sha256')");
13613 assert!(
13614 out.sql.contains("hmac("),
13615 "expected pgcrypto's hmac(), got:\n{}",
13616 out.sql
13617 );
13618 }
13619
13620 #[test]
13621 fn test_crypto_gen_salt_zero_arg_defaults_to_blowfish() {
13622 let out = compile_and_emit("SELECT crypto::gen_salt()");
13623 assert!(
13624 out.sql.contains("gen_salt('bf')"),
13625 "expected default 'bf' salt type, got:\n{}",
13626 out.sql
13627 );
13628 }
13629
13630 #[test]
13631 fn test_crypto_gen_salt_one_arg_passes_type_through() {
13632 let out = compile_and_emit("SELECT crypto::gen_salt('xdes')");
13633 assert!(
13634 out.sql.contains("gen_salt("),
13635 "expected gen_salt() call, got:\n{}",
13636 out.sql
13637 );
13638 }
13639
13640 #[test]
13641 fn test_crypto_gen_salt_iter_count_casts_to_int4() {
13642 let out = compile_and_emit("SELECT crypto::gen_salt('xdes', 5)");
13643 assert!(
13644 out.sql.contains("::int4"),
13645 "expected int8 -> int4 narrowing cast, got:\n{}",
13646 out.sql
13647 );
13648 }
13649
13650 #[test]
13651 fn test_crypto_crypt_uses_pgcrypto_crypt() {
13652 let out = compile_and_emit("SELECT crypto::crypt('hunter2', crypto::gen_salt())");
13653 assert!(
13654 out.sql.contains("crypt("),
13655 "expected pgcrypto's crypt(), got:\n{}",
13656 out.sql
13657 );
13658 }
13659
13660 #[test]
13661 fn test_postgis_cast_emits_geometry_type() {
13662 let out = compile_and_emit("SELECT <postgis::geometry>'POINT(1 2)'");
13663 assert!(
13664 out.sql.contains("::geometry"),
13665 "expected ::geometry cast, got:\n{}",
13666 out.sql
13667 );
13668 }
13669
13670 #[test]
13671 fn test_postgis_x_uses_st_x_builtin() {
13672 let out = compile_and_emit("SELECT postgis::x(<postgis::geometry>'POINT(1 2)')");
13673 assert!(out.sql.contains("st_x("), "expected st_x() call, got:\n{}", out.sql);
13674 }
13675
13676 #[test]
13677 fn test_postgis_area_geometry_and_geography_overloads() {
13678 let out = compile_and_emit("SELECT postgis::area(<postgis::geometry>'POINT(1 2)')");
13679 assert!(
13680 out.sql.contains("st_area("),
13681 "expected st_area() call, got:\n{}",
13682 out.sql
13683 );
13684
13685 let out = compile_and_emit("SELECT postgis::area(<postgis::geography>'POINT(1 2)', true)");
13686 assert!(
13687 out.sql.contains("st_area("),
13688 "expected st_area() call, got:\n{}",
13689 out.sql
13690 );
13691 }
13692
13693 #[test]
13694 fn test_postgis_setsrid_casts_int64_arg_to_int4() {
13695 let out = compile_and_emit("SELECT postgis::setsrid(<postgis::geometry>'POINT(1 2)', 4326)");
13696 assert!(
13697 out.sql.contains("st_setsrid("),
13698 "expected st_setsrid() call, got:\n{}",
13699 out.sql
13700 );
13701 assert!(
13702 out.sql.contains("::int4"),
13703 "expected int8 -> int4 narrowing cast, got:\n{}",
13704 out.sql
13705 );
13706 }
13707
13708 #[test]
13709 fn test_postgis_quantizecoordinates_default_arity_variants_compile() {
13710 let out = compile_and_emit("SELECT postgis::quantizecoordinates(<postgis::geometry>'POINT(1 2)', 5)");
13715 assert!(out.sql.contains("st_quantizecoordinates("), "got:\n{}", out.sql);
13716
13717 let out = compile_and_emit("SELECT postgis::quantizecoordinates(<postgis::geometry>'POINT(1 2)', 5, 5, 5)");
13718 assert!(out.sql.contains("st_quantizecoordinates("), "got:\n{}", out.sql);
13719 }
13720
13721 #[test]
13722 fn test_postgis_op_contains_emits_infix_operator_not_function_call() {
13723 let out = compile_and_emit(
13728 "SELECT postgis::op_contains(<postgis::geometry>'POINT(1 2)', <postgis::geometry>'POINT(3 4)')",
13729 );
13730 assert!(out.sql.contains(" ~ "), "expected infix ~ operator, got:\n{}", out.sql);
13731 assert!(
13732 !out.sql.contains("op_contains("),
13733 "must not call a literal op_contains function, got:\n{}",
13734 out.sql
13735 );
13736 }
13737
13738 #[test]
13739 fn test_postgis_op_overlaps_geometry_and_geography_overloads() {
13740 let out = compile_and_emit(
13741 "SELECT postgis::op_overlaps(<postgis::geometry>'POINT(1 2)', <postgis::geometry>'POINT(3 4)')",
13742 );
13743 assert!(
13744 out.sql.contains(" && "),
13745 "expected infix && operator, got:\n{}",
13746 out.sql
13747 );
13748
13749 let out = compile_and_emit(
13750 "SELECT postgis::op_overlaps(<postgis::geography>'POINT(1 2)', <postgis::geography>'POINT(3 4)')",
13751 );
13752 assert!(
13753 out.sql.contains(" && "),
13754 "expected infix && operator, got:\n{}",
13755 out.sql
13756 );
13757 }
13758
13759 fn make_schema_with_fns() -> SchemaDescriptor {
13762 let mut s = make_schema();
13763 s.functions = vec![
13764 FunctionDescriptor {
13765 name: "mysum".into(),
13766 module: "default".into(),
13767 params: vec![
13768 FunctionParamDescriptor {
13769 name: "a".into(),
13770 pg_type: "int8".into(),
13771 },
13772 FunctionParamDescriptor {
13773 name: "b".into(),
13774 pg_type: "int8".into(),
13775 },
13776 ],
13777 return_pg_type: "int8".into(),
13778 return_is_object: false,
13779 return_is_set: false,
13780 return_is_polymorphic: false,
13781 volatility: "immutable".into(),
13782 body: "a + b".into(),
13783 },
13784 FunctionDescriptor {
13785 name: "adults".into(),
13786 module: "default".into(),
13787 params: vec![],
13788 return_pg_type: "default::Person".into(),
13789 return_is_object: true,
13790 return_is_set: true,
13791 return_is_polymorphic: false,
13792 volatility: "stable".into(),
13793 body: "select Person filter .age > 18".into(),
13794 },
13795 FunctionDescriptor {
13802 name: "mysum".into(),
13803 module: "default".into(),
13804 params: vec![
13805 FunctionParamDescriptor {
13806 name: "a".into(),
13807 pg_type: "int8".into(),
13808 },
13809 FunctionParamDescriptor {
13810 name: "b".into(),
13811 pg_type: "int8".into(),
13812 },
13813 FunctionParamDescriptor {
13814 name: "c".into(),
13815 pg_type: "int8".into(),
13816 },
13817 ],
13818 return_pg_type: "int8".into(),
13819 return_is_object: false,
13820 return_is_set: false,
13821 return_is_polymorphic: false,
13822 volatility: "immutable".into(),
13823 body: "a + b + c".into(),
13824 },
13825 ];
13826 s
13827 }
13828
13829 #[test]
13830 fn test_user_fn_scalar_call() {
13831 let schema = make_schema_with_fns();
13832 let out = compile_and_emit_with("SELECT mysum(1, 2)", &schema);
13833 assert!(out.sql.contains("\"public\".\"mysum\""), "got:\n{}", out.sql);
13834 }
13835
13836 #[test]
13837 fn test_user_fn_overload_resolved_by_argument_count() {
13838 let schema = make_schema_with_fns();
13843 let out = compile_and_emit_with("SELECT mysum(1, 2, 3)", &schema);
13844 assert!(
13845 out.sql
13846 .contains("\"public\".\"mysum\"((1)::int8, (2)::int8, (3)::int8)"),
13847 "got:\n{}",
13848 out.sql,
13849 );
13850
13851 let out = compile_and_emit_with("SELECT mysum(1, 2)", &schema);
13853 assert!(
13854 out.sql.contains("\"public\".\"mysum\"((1)::int8, (2)::int8)"),
13855 "got:\n{}",
13856 out.sql,
13857 );
13858 }
13859
13860 #[test]
13861 fn test_user_fn_object_select_no_shape() {
13862 let schema = make_schema_with_fns();
13863 let out = compile_and_emit_with("SELECT adults()", &schema);
13864 assert!(out.sql.contains("\"public\".\"adults\"()"), "got:\n{}", out.sql);
13865 assert!(out.sql.contains("FROM"), "got:\n{}", out.sql);
13866 }
13867
13868 #[test]
13869 fn test_user_fn_object_select_with_shape() {
13870 let schema = make_schema_with_fns();
13871 let out = compile_and_emit_with("SELECT adults() { name }", &schema);
13872 assert!(out.sql.contains("\"public\".\"adults\"()"), "got:\n{}", out.sql);
13873 assert!(out.sql.contains("\"name\""), "got:\n{}", out.sql);
13874 }
13875
13876 #[test]
13877 fn test_object_fn_projected_to_a_property_inside_an_expression() {
13878 let schema = make_schema_with_fns();
13879 let out = compile_and_emit_with("SELECT Person { n := adults().name }", &schema);
13880 assert!(out.sql.contains("FROM \"public\".\"adults\"() AS"), "got:\n{}", out.sql);
13881 assert!(out.sql.contains("\".\"name\"\n"), "got:\n{}", out.sql);
13882 }
13883
13884 #[test]
13885 fn test_object_fn_sub_select_carries_its_modifiers() {
13886 let schema = make_schema_with_fns();
13887 let out = compile_and_emit_with(
13888 "SELECT Person { n := (select adults() filter .age > 21 limit 1).name }",
13889 &schema,
13890 );
13891 assert!(out.sql.contains("FROM \"public\".\"adults\"() AS"), "got:\n{}", out.sql);
13892 assert!(out.sql.contains("\"age\" > 21"), "got:\n{}", out.sql);
13893 assert!(out.sql.contains("LIMIT 1"), "got:\n{}", out.sql);
13894 }
13895
13896 #[test]
13897 fn test_object_fn_without_a_property_still_explains_the_restriction() {
13898 let schema = make_schema_with_fns();
13899 let ast = parse::parse("SELECT Person { n := adults() + 1 }").unwrap();
13900 let err = match ir::compile(&ast, &schema) {
13901 Ok(_) => panic!("expected a compile error"),
13902 Err(e) => format!("{e}"),
13903 };
13904 assert!(err.contains("returns objects"), "{err}");
13905 }
13906
13907 #[test]
13908 fn test_object_fn_projected_to_an_unknown_property_is_rejected() {
13909 let schema = make_schema_with_fns();
13910 let ast = parse::parse("SELECT Person { n := adults().nope }").unwrap();
13911 let err = match ir::compile(&ast, &schema) {
13912 Ok(_) => panic!("expected a compile error"),
13913 Err(e) => format!("{e}"),
13914 };
13915 assert!(err.contains("nope"), "{err}");
13916 }
13917
13918 #[test]
13919 fn test_user_fn_in_cte_exposes_raw_columns() {
13920 let schema = make_schema_with_fns();
13923 let out = compile_and_emit_with("WITH persons := adults() SELECT persons FILTER .age > 25", &schema);
13924 assert!(
13925 out.sql.contains("SELECT * FROM \"public\".\"adults\"()"),
13926 "CTE source must be SELECT * FROM fn(), got:\n{}",
13927 out.sql,
13928 );
13929 assert!(
13930 out.sql.contains("\"age\""),
13931 "outer filter must reference raw column, got:\n{}",
13932 out.sql
13933 );
13934 }
13935
13936 fn make_schema_with_vector() -> SchemaDescriptor {
13939 use crate::schema::VectorIndexDescriptor;
13940 let mut s = make_schema();
13941 if let Some(td) = s.types.iter_mut().find(|t| t.name == "Person") {
13942 td.vector_indexes.push(VectorIndexDescriptor {
13943 index_name: None,
13944 pointers: vec!["name".into()],
13945 model: "test-embed".into(),
13946 metric: "cosine".into(),
13947 dimensions: 4,
13948 });
13949 }
13950 s
13951 }
13952
13953 #[test]
13954 fn test_vector_search_bare_type_name() {
13955 let schema = make_schema_with_vector();
13956 let out = compile_and_emit_with(
13957 "WITH search := vector::search(Person, <pgvector::vector>[1.0, 2.0, 3.0, 4.0]) \
13958 SELECT search { object { name }, distance }",
13959 &schema,
13960 );
13961 assert!(
13962 out.sql.contains("\"Person\""),
13963 "expected Person table, got:\n{}",
13964 out.sql
13965 );
13966 assert!(out.sql.contains("<=>"), "expected cosine operator, got:\n{}", out.sql);
13967 }
13968
13969 #[test]
13970 fn test_vector_search_qualified_type_name() {
13971 let schema = make_schema_with_vector();
13972 let out = compile_and_emit_with(
13973 "WITH search := vector::search(default::Person, <pgvector::vector>[1.0, 2.0, 3.0, 4.0]) \
13974 SELECT search { object { name }, distance }",
13975 &schema,
13976 );
13977 assert!(
13978 out.sql.contains("\"Person\""),
13979 "expected Person table, got:\n{}",
13980 out.sql
13981 );
13982 assert!(out.sql.contains("<=>"), "expected cosine operator, got:\n{}", out.sql);
13983 }
13984
13985 #[test]
13986 fn test_vector_search_subquery_filter_included_in_where() {
13987 let schema = make_schema_with_vector();
13988 let out = compile_and_emit_with(
13989 "WITH search := vector::search((select Person filter .name = 'Alice'), <pgvector::vector>[1.0, 2.0, 3.0, 4.0]) \
13990 SELECT search { object { name }, distance }",
13991 &schema,
13992 );
13993 assert!(
13994 out.sql.contains("\"Person\""),
13995 "expected Person table, got:\n{}",
13996 out.sql
13997 );
13998 assert!(out.sql.contains("\"name\""), "expected name filter, got:\n{}", out.sql);
13999 assert!(out.sql.contains("Alice"), "expected filter value, got:\n{}", out.sql);
14000 assert!(out.sql.contains("<=>"), "expected cosine operator, got:\n{}", out.sql);
14001 }
14002
14003 #[test]
14004 fn test_vector_search_subquery_filter_combined_with_outer_property_filter() {
14005 let schema = make_schema_with_vector();
14006 let out = compile_and_emit_with(
14007 "WITH search := vector::search((select Person filter .age > 18), <pgvector::vector>[1.0, 2.0, 3.0, 4.0]) \
14008 SELECT search { object { name }, distance }",
14009 &schema,
14010 );
14011 assert!(
14012 out.sql.contains("\"age\""),
14013 "expected age pre-filter, got:\n{}",
14014 out.sql
14015 );
14016 assert!(out.sql.contains("18"), "expected filter value 18, got:\n{}", out.sql);
14017 assert!(out.sql.contains("<=>"), "expected cosine operator, got:\n{}", out.sql);
14018 }
14019
14020 #[test]
14021 fn test_vector_search_text_overload_with_subquery_filter() {
14022 let schema = make_schema_with_vector();
14023 let out = compile_and_emit_with(
14024 "WITH search := vector::search((select Person filter .name = 'Alice'), query := $q) \
14025 SELECT search { object { name }, distance }",
14026 &schema,
14027 );
14028 assert!(
14029 out.sql.contains("\"Person\""),
14030 "expected Person table, got:\n{}",
14031 out.sql
14032 );
14033 assert!(
14034 out.sql.contains("Alice"),
14035 "expected pre-filter value, got:\n{}",
14036 out.sql
14037 );
14038 assert!(
14039 out.sql.contains("float8[]"),
14040 "expected float8[] cast for deferred vec param, got:\n{}",
14041 out.sql
14042 );
14043 assert!(out.sql.contains("<=>"), "expected cosine operator, got:\n{}", out.sql);
14044 }
14045
14046 #[test]
14047 fn test_count_type_ref_compiles_to_agg_over_query() {
14048 let out = compile_and_emit("SELECT count(Person)");
14049 assert!(out.sql.contains("count(*)"), "expected count(*), got:\n{}", out.sql);
14050 assert!(
14051 out.sql.contains("\"Person\""),
14052 "expected Person table, got:\n{}",
14053 out.sql
14054 );
14055 }
14056
14057 #[test]
14058 fn test_count_qualified_type_ref_compiles_to_agg_over_query() {
14059 let out = compile_and_emit("SELECT count(default::Person)");
14060 assert!(out.sql.contains("count(*)"), "expected count(*), got:\n{}", out.sql);
14061 assert!(
14062 out.sql.contains("\"Person\""),
14063 "expected Person table, got:\n{}",
14064 out.sql
14065 );
14066 }
14067
14068 #[test]
14069 fn test_count_subquery_compiles_to_agg_over_query() {
14070 let out = compile_and_emit("SELECT count((select Person))");
14071 assert!(out.sql.contains("count(*)"), "expected count(*), got:\n{}", out.sql);
14072 assert!(
14073 out.sql.contains("\"Person\""),
14074 "expected Person table, got:\n{}",
14075 out.sql
14076 );
14077 }
14078
14079 #[test]
14080 fn test_count_subquery_with_filter() {
14081 let out = compile_and_emit("SELECT count((select Person filter .name = 'Alice'))");
14082 assert!(out.sql.contains("count(*)"), "expected count(*), got:\n{}", out.sql);
14083 assert!(
14084 out.sql.contains("\"name\""),
14085 "expected filter on name, got:\n{}",
14086 out.sql
14087 );
14088 }
14089
14090 #[test]
14091 fn test_positional_param_compiles_to_dollar_n() {
14092 let out = compile_and_emit("SELECT Person FILTER .name = $0");
14093 assert!(out.sql.contains("$1"), "expected $1 placeholder, got:\n{}", out.sql);
14094 }
14095
14096 #[test]
14097 fn test_multiple_positional_params_compile_in_order() {
14098 let out = compile_and_emit("SELECT Person FILTER .name = $0 AND .age > $1");
14099 assert!(out.sql.contains("$1"), "expected $1, got:\n{}", out.sql);
14100 assert!(out.sql.contains("$2"), "expected $2, got:\n{}", out.sql);
14101 }
14102
14103 #[test]
14104 fn test_repeated_positional_param_reuses_slot() {
14105 let out = compile_and_emit("SELECT Person FILTER .name = $0 OR .name = $0");
14106 assert_eq!(
14107 out.sql.matches("$1").count(),
14108 2,
14109 "both uses must reference $1, got:\n{}",
14110 out.sql
14111 );
14112 }
14113
14114 #[test]
14115 fn test_cast_to_nonexistent_type_names_full_type() {
14116 let schema = make_schema();
14117 let ast = parse::parse("SELECT Person FILTER .name = <default::Ghost>$name").unwrap();
14118 match ir::compile(&ast, &schema) {
14119 Ok(_) => panic!("expected compile error for unknown type"),
14120 Err(e) => {
14121 let msg = e.to_string();
14122 assert!(
14123 msg.contains("unknown type 'default::Ghost'"),
14124 "expected full type name in error, got: {msg}",
14125 );
14126 }
14127 }
14128 }
14129
14130 #[test]
14131 fn test_cast_to_nonexistent_unqualified_type_names_type() {
14132 let schema = make_schema();
14133 let ast = parse::parse("SELECT Person FILTER .name = <Ghost>$name").unwrap();
14134 match ir::compile(&ast, &schema) {
14135 Ok(_) => panic!("expected compile error for unknown type"),
14136 Err(e) => {
14137 let msg = e.to_string();
14138 assert!(
14139 msg.contains("unknown type 'Ghost'"),
14140 "expected type name in error, got: {msg}",
14141 );
14142 }
14143 }
14144 }
14145
14146 #[test]
14147 fn test_top_level_cast_to_registered_scalar_uses_its_own_domain() {
14148 use crate::schema::ScalarDescriptor;
14154 let mut schema = make_schema();
14155 schema.scalars.push(ScalarDescriptor {
14156 name: "Email".into(),
14157 module: "default".into(),
14158 base: "Str".into(),
14159 pg_type: "text".into(),
14160 check_constraints: vec!["value ~ '^[^@]+@[^@]+\\.[^@]+$'".into()],
14161 is_sequence: false,
14162 });
14163 let out = compile_and_emit_with("SELECT <default::Email>'test@test.de'", &schema);
14164 assert!(
14165 out.sql.contains("\"public\".\"Email\""),
14166 "expected a cast to the scalar's own domain, got:\n{}",
14167 out.sql
14168 );
14169 assert!(
14170 !out.sql.to_lowercase().contains("\"person\""),
14171 "must not be misrouted to an object-type lookup, got:\n{}",
14172 out.sql
14173 );
14174 }
14175
14176 #[test]
14177 fn test_top_level_cast_to_unknown_type_names_full_type() {
14178 let schema = make_schema();
14183 let ast = parse::parse("SELECT <default::Ghost>$name").unwrap();
14184 match ir::compile(&ast, &schema) {
14185 Ok(_) => panic!("expected compile error for unknown type"),
14186 Err(e) => {
14187 let msg = e.to_string();
14188 assert!(
14189 msg.contains("unknown type 'default::Ghost'"),
14190 "expected full type name in error, got: {msg}",
14191 );
14192 }
14193 }
14194 }
14195
14196 #[test]
14197 fn test_structural_tuple_cast_unnamed_resolves_to_jsonb() {
14198 let out = compile_and_emit("SELECT <tuple<str, bool>>$p");
14199 assert!(out.sql.contains("($1)::jsonb"), "got:\n{}", out.sql);
14200 }
14201
14202 #[test]
14203 fn test_jsonb_to_uuid_cast_extracts_via_text() {
14204 let out = compile_and_emit("SELECT <uuid>(<json>$p)");
14206 assert!(out.sql.contains("#>> '{}'"), "got:\n{}", out.sql);
14207 assert!(out.sql.contains("::uuid"), "got:\n{}", out.sql);
14208 }
14209
14210 #[test]
14211 fn test_jsonb_to_datetime_cast_extracts_via_text() {
14212 let out = compile_and_emit("SELECT <datetime>(<json>$p)");
14213 assert!(out.sql.contains("#>> '{}'"), "got:\n{}", out.sql);
14214 assert!(out.sql.contains("::timestamptz"), "got:\n{}", out.sql);
14215 }
14216
14217 #[test]
14218 fn test_jsonb_to_duration_cast_extracts_via_text() {
14219 let out = compile_and_emit("SELECT <duration>(<json>$p)");
14220 assert!(out.sql.contains("#>> '{}'"), "got:\n{}", out.sql);
14221 assert!(out.sql.contains("::interval"), "got:\n{}", out.sql);
14222 }
14223
14224 #[test]
14225 fn test_jsonb_to_array_cast_unpacks_each_element() {
14226 let out = compile_and_emit("SELECT <array<int64>>(<json>$p)");
14227 assert!(out.sql.contains("jsonb_array_elements("), "got:\n{}", out.sql);
14228 assert!(out.sql.contains("#>> '{}'"), "got:\n{}", out.sql);
14229 assert!(out.sql.contains("::int8"), "got:\n{}", out.sql);
14230 }
14231
14232 #[test]
14233 fn test_non_jsonb_cast_is_unaffected_by_jsonb_extraction() {
14234 let out = compile_and_emit("SELECT <uuid>$p");
14237 assert!(!out.sql.contains("#>>"), "got:\n{}", out.sql);
14238 assert!(out.sql.contains("::uuid"), "got:\n{}", out.sql);
14239 }
14240
14241 #[test]
14242 fn test_datetime_plus_duration_type_checks() {
14243 let out = compile_and_emit("SELECT <datetime>$p + <duration>$d");
14248 assert!(out.sql.contains(" + "), "got:\n{}", out.sql);
14249 }
14250
14251 #[test]
14252 fn test_duration_plus_datetime_type_checks_either_order() {
14253 let out = compile_and_emit("SELECT <duration>$d + <datetime>$p");
14254 assert!(out.sql.contains(" + "), "got:\n{}", out.sql);
14255 }
14256
14257 #[test]
14258 fn test_local_date_plus_duration_type_checks() {
14259 let out = compile_and_emit("SELECT <cal::local_date>$p + <duration>$d");
14260 assert!(out.sql.contains(" + "), "got:\n{}", out.sql);
14261 }
14262
14263 #[test]
14264 fn test_datetime_minus_duration_type_checks() {
14265 let out = compile_and_emit("SELECT <datetime>$p - <duration>$d");
14266 assert!(out.sql.contains(" - "), "got:\n{}", out.sql);
14267 }
14268
14269 #[test]
14270 fn test_local_time_plus_duration_type_checks() {
14271 let out = compile_and_emit("SELECT <cal::local_time>$p + <duration>$d");
14272 assert!(out.sql.contains(" + "), "got:\n{}", out.sql);
14273 }
14274
14275 #[test]
14276 fn test_datetime_plus_duration_only_allowed_for_add_and_sub() {
14277 let schema = make_schema();
14280 let ast = parse::parse("SELECT <datetime>$p = <duration>$d").unwrap();
14281 match ir::compile(&ast, &schema) {
14282 Err(err) => assert!(format!("{err}").contains("cannot be applied"), "got: {err}"),
14283 Ok(_) => panic!("expected a compile error"),
14284 }
14285 }
14286
14287 #[test]
14288 fn test_int_and_float_mix_is_allowed() {
14289 let out = compile_and_emit("SELECT <int16>1 + <float32>2.0");
14293 assert!(out.sql.contains(" + "), "got:\n{}", out.sql);
14294 }
14295
14296 #[test]
14297 fn test_int_and_decimal_mix_is_allowed() {
14298 let out = compile_and_emit("SELECT <int64>1 + <decimal>2.0");
14302 assert!(out.sql.contains(" + "), "got:\n{}", out.sql);
14303 }
14304
14305 #[test]
14306 fn test_float_and_decimal_mix_still_rejected() {
14307 let schema = make_schema();
14311 let ast = parse::parse("SELECT <decimal>$p + <float64>$n").unwrap();
14312 match ir::compile(&ast, &schema) {
14313 Err(err) => assert!(format!("{err}").contains("cannot be applied"), "got: {err}"),
14314 Ok(_) => panic!("expected a compile error"),
14315 }
14316 }
14317
14318 #[test]
14319 fn test_unknown_pointer_suggests_a_close_match() {
14320 let schema = make_schema();
14324 let ast = parse::parse("SELECT Person { nam }").unwrap();
14325 match ir::compile(&ast, &schema) {
14326 Err(err) => assert_eq!(
14327 format!("{err}"),
14328 "object type 'default::Person' has no link or property 'nam'. Did you mean 'name'?"
14329 ),
14330 Ok(_) => panic!("expected a compile error"),
14331 }
14332 }
14333
14334 #[test]
14335 fn test_unrelated_unknown_pointer_gets_no_suggestion() {
14336 let schema = make_schema();
14339 let ast = parse::parse("SELECT Person { xyzxyzxyz }").unwrap();
14340 match ir::compile(&ast, &schema) {
14341 Err(err) => {
14342 let msg = format!("{err}");
14343 assert!(msg.contains("has no link or property 'xyzxyzxyz'"), "got: {msg}");
14344 assert!(!msg.contains("Did you mean"), "got: {msg}");
14345 }
14346 Ok(_) => panic!("expected a compile error"),
14347 }
14348 }
14349
14350 #[test]
14351 fn test_multilink_sourced_backlink_exists_filter_compiles() {
14352 let out = compile_and_emit("SELECT Post filter exists .<posts[is Person]");
14362 assert!(out.sql.contains("EXISTS"), "got:\n{}", out.sql);
14363 }
14364
14365 #[test]
14366 fn test_backlink_as_computed_shape_pointer_compiles() {
14367 let out = compile_and_emit("SELECT Post { title, authors := .<posts[is Person] { name } }");
14380 assert!(out.sql.contains("array_agg(ROW("), "got:\n{}", out.sql);
14381 }
14382
14383 fn schema_with_exclusive_posts() -> SchemaDescriptor {
14384 let mut schema = make_schema();
14385 for td in schema.types.iter_mut().filter(|td| td.name == "Person") {
14386 for ml in td.multilinks.iter_mut().filter(|ml| ml.name == "posts") {
14387 ml.is_exclusive = true;
14388 }
14389 }
14390 schema
14391 }
14392
14393 #[test]
14394 fn test_backlink_through_an_exclusive_multilink_is_single() {
14395 let out = compile_and_emit_with(
14396 "SELECT Post { title, author := .<posts[is Person] { name } }",
14397 &schema_with_exclusive_posts(),
14398 );
14399 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
14400 panic!("expected an object root, got {:?}", out.shape.root)
14401 };
14402 assert!(
14403 matches!(pointers.last(), Some(ShapeNode::Object { name, .. }) if name == "author"),
14404 "got {pointers:?}"
14405 );
14406 }
14407
14408 #[test]
14409 fn test_a_path_through_an_exclusive_backlink_is_single() {
14410 let out = compile_and_emit_with(
14411 "SELECT Post { title, author_name := .<posts[is Person].name }",
14412 &schema_with_exclusive_posts(),
14413 );
14414 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
14415 panic!("expected an object root, got {:?}", out.shape.root)
14416 };
14417 assert!(
14418 matches!(pointers.last(), Some(ShapeNode::Scalar { name, .. }) if name == "author_name"),
14419 "got {pointers:?}"
14420 );
14421 assert!(!out.sql.contains("ARRAY("), "got:\n{}", out.sql);
14422 }
14423
14424 #[test]
14425 fn test_structural_tuple_cast_named_resolves_to_jsonb() {
14426 let out = compile_and_emit("SELECT <tuple<x: float64, y: float64>>$p");
14427 assert!(out.sql.contains("($1)::jsonb"), "got:\n{}", out.sql);
14428 }
14429
14430 #[test]
14431 fn test_structural_tuple_cast_nested_resolves_to_jsonb() {
14432 let out = compile_and_emit("SELECT <tuple<point: tuple<x: float64, y: float64>, label: str>>$p");
14433 assert!(out.sql.contains("($1)::jsonb"), "got:\n{}", out.sql);
14434 }
14435
14436 #[test]
14437 fn test_nominal_named_tuple_cast_resolves_to_jsonb() {
14438 let mut schema = make_schema();
14439 schema.named_tuples.push(NamedTupleDescriptor {
14440 name: "Point".into(),
14441 module: "default".into(),
14442 members: vec![],
14443 });
14444 let out = compile_and_emit_with("SELECT <default::Point>$p", &schema);
14445 assert!(out.sql.contains("($1)::jsonb"), "got:\n{}", out.sql);
14446 }
14447
14448 #[test]
14449 fn test_array_literal_cast_resolves_to_native_pg_array_not_jsonb() {
14450 let out = compile_and_emit("SELECT <array<str>>['foo', 'bar']");
14454 assert!(
14455 out.sql.contains("::text[]") || out.sql.contains("ARRAY["),
14456 "got:\n{}",
14457 out.sql
14458 );
14459 assert!(
14460 !out.sql.contains("jsonb"),
14461 "arrays must not use jsonb, got:\n{}",
14462 out.sql
14463 );
14464 }
14465
14466 #[test]
14467 fn test_array_literal_cast_applies_per_element_cast() {
14468 let out = compile_and_emit("SELECT <array<int64>>['1', '2']");
14472 assert!(out.sql.contains("ARRAY[('1')::int8, ('2')::int8]"), "got:\n{}", out.sql);
14473 }
14474
14475 #[test]
14476 fn test_array_param_cast_uses_direct_suffix_cast() {
14477 let out = compile_and_emit("SELECT <array<int64>>$p");
14478 assert!(out.sql.contains("::int8[]"), "got:\n{}", out.sql);
14479 }
14480
14481 #[test]
14482 fn test_array_of_named_tuple_element_casts_to_jsonb_array() {
14483 let out = compile_and_emit("SELECT <array<tuple<x: float64, y: float64>>>$p");
14487 assert!(out.sql.contains("::jsonb[]"), "got:\n{}", out.sql);
14488 }
14489
14490 #[test]
14491 fn test_contains_on_array_literal_cast_uses_array_overload_not_strpos() {
14492 let out = compile_and_emit("SELECT contains(<array<str>>[1, 2], '2')");
14498 assert!(out.sql.contains("@> ARRAY["), "got:\n{}", out.sql);
14499 assert!(
14500 !out.sql.contains("strpos"),
14501 "must not fall back to the str/str overload, got:\n{}",
14502 out.sql
14503 );
14504 }
14505
14506 #[test]
14507 fn test_nested_array_type_rejected_at_parse_time() {
14508 match parse::parse("SELECT <array<array<str>>>$p") {
14509 Ok(_) => panic!("expected parse error for nested array type"),
14510 Err(e) => assert!(e.to_string().contains("nested arrays are not supported"), "got: {}", e),
14511 }
14512 }
14513
14514 #[test]
14515 fn test_array_cast_in_computed_shape_field_schema_bound_context() {
14516 let out = compile_and_emit("SELECT Person { name, tags := <array<str>>['a', 'b'] }");
14519 assert!(out.sql.contains("ARRAY[('a')::text, ('b')::text]"), "got:\n{}", out.sql);
14520 }
14521
14522 #[test]
14523 fn test_nominal_named_tuple_cast_shape_carries_real_members() {
14524 use crate::schema::{TupleMemberDescriptor, TupleMemberKind};
14525 let mut schema = make_schema();
14526 schema.named_tuples.push(NamedTupleDescriptor {
14527 name: "Point".into(),
14528 module: "default".into(),
14529 members: vec![
14530 TupleMemberDescriptor {
14531 name: Some("x".into()),
14532 kind: TupleMemberKind::Scalar {
14533 pg_type: "float8".into(),
14534 },
14535 },
14536 TupleMemberDescriptor {
14537 name: Some("y".into()),
14538 kind: TupleMemberKind::Scalar {
14539 pg_type: "float8".into(),
14540 },
14541 },
14542 ],
14543 });
14544 let out = compile_and_emit_with("SELECT <default::Point>$p", &schema);
14545 match &out.shape.root {
14546 crate::query::ShapeNode::NamedTuple { type_name, members, .. } => {
14547 assert_eq!(type_name.as_deref(), Some("default::Point"));
14548 let members = members.as_ref().expect("expected resolved members");
14549 assert_eq!(members.len(), 2);
14550 assert_eq!(members[0].key.as_deref(), Some("x"));
14551 assert_eq!(members[1].key.as_deref(), Some("y"));
14552 }
14553 other => panic!("expected ShapeNode::NamedTuple, got {other:?}"),
14554 }
14555 }
14556
14557 #[test]
14558 fn test_structural_tuple_property_read_shape_carries_real_members() {
14559 use crate::schema::{TupleMemberDescriptor, TupleMemberKind};
14560 let schema = SchemaDescriptor {
14561 types: vec![TypeDescriptor {
14562 name: "Person".into(),
14563 module: "default".into(),
14564 table: "Person".into(),
14565 abstract_: false,
14566 materialized: false,
14567 description: None,
14568 parents: vec![],
14569 interfaces: vec![],
14570 bases: vec![],
14571 properties: vec![PropertyDescriptor {
14572 name: "address".into(),
14573 pg_type: "jsonb".into(),
14574 nullable: true,
14575 default_sql: None,
14576 default_pyql: None,
14577 description: None,
14578 check_constraints: vec![],
14579 is_exclusive: false,
14580 is_pk: false,
14581 is_readonly: false,
14582 rewrites: vec![],
14583 tuple_members: Some(vec![
14584 TupleMemberDescriptor {
14585 name: Some("street".into()),
14586 kind: TupleMemberKind::Scalar { pg_type: "text".into() },
14587 },
14588 TupleMemberDescriptor {
14589 name: Some("zip".into()),
14590 kind: TupleMemberKind::Scalar { pg_type: "text".into() },
14591 },
14592 ]),
14593 column_type: None,
14594 }],
14595 links: vec![],
14596 multilinks: vec![],
14597 computed: vec![],
14598 constraints: vec![],
14599 indexes: vec![],
14600 partition: None,
14601 vector_indexes: vec![],
14602 search_indexes: vec![],
14603 triggers: vec![],
14604 junction: false,
14605 signals: vec![],
14606 }],
14607 scalars: vec![],
14608 enums: vec![],
14609 named_tuples: vec![],
14610 globals: vec![],
14611 functions: vec![],
14612 aliases: vec![],
14613 channels: vec![],
14614 ..Default::default()
14615 };
14616 let out = compile_and_emit_with("SELECT Person { address }", &schema);
14617 assert!(out.sql.contains("::jsonb"), "got:\n{}", out.sql);
14618 match &out.shape.root {
14619 crate::query::ShapeNode::Object { pointers, .. } => {
14620 let address = pointers
14621 .iter()
14622 .find(|p| matches!(p, crate::query::ShapeNode::NamedTuple { name, .. } if name == "address"))
14623 .expect("expected address pointer in shape");
14624 match address {
14625 crate::query::ShapeNode::NamedTuple { type_name, members, .. } => {
14626 assert_eq!(*type_name, None);
14627 let members = members.as_ref().expect("expected resolved members");
14628 assert_eq!(members.len(), 2);
14629 assert_eq!(members[0].key.as_deref(), Some("street"));
14630 assert_eq!(members[1].key.as_deref(), Some("zip"));
14631 }
14632 other => panic!("expected NamedTuple, got {other:?}"),
14633 }
14634 }
14635 other => panic!("expected ShapeNode::Object, got {other:?}"),
14636 }
14637 }
14638
14639 #[test]
14640 fn test_bare_path_select_structural_tuple_property_shape_carries_real_members() {
14641 use crate::schema::{TupleMemberDescriptor, TupleMemberKind};
14651 let schema = SchemaDescriptor {
14652 types: vec![TypeDescriptor {
14653 name: "Person".into(),
14654 module: "default".into(),
14655 table: "Person".into(),
14656 abstract_: false,
14657 materialized: false,
14658 description: None,
14659 parents: vec![],
14660 interfaces: vec![],
14661 bases: vec![],
14662 properties: vec![PropertyDescriptor {
14663 name: "address".into(),
14664 pg_type: "jsonb".into(),
14665 nullable: true,
14666 default_sql: None,
14667 default_pyql: None,
14668 description: None,
14669 check_constraints: vec![],
14670 is_exclusive: false,
14671 is_pk: false,
14672 is_readonly: false,
14673 rewrites: vec![],
14674 tuple_members: Some(vec![
14675 TupleMemberDescriptor {
14676 name: Some("street".into()),
14677 kind: TupleMemberKind::Scalar { pg_type: "text".into() },
14678 },
14679 TupleMemberDescriptor {
14680 name: Some("zip".into()),
14681 kind: TupleMemberKind::Scalar { pg_type: "text".into() },
14682 },
14683 ]),
14684 column_type: None,
14685 }],
14686 links: vec![],
14687 multilinks: vec![],
14688 computed: vec![],
14689 constraints: vec![],
14690 indexes: vec![],
14691 partition: None,
14692 vector_indexes: vec![],
14693 search_indexes: vec![],
14694 triggers: vec![],
14695 junction: false,
14696 signals: vec![],
14697 }],
14698 scalars: vec![],
14699 enums: vec![],
14700 named_tuples: vec![],
14701 globals: vec![],
14702 functions: vec![],
14703 aliases: vec![],
14704 channels: vec![],
14705 ..Default::default()
14706 };
14707 let out = compile_and_emit_with("SELECT Person.address", &schema);
14708 match &out.shape.root {
14709 crate::query::ShapeNode::NamedTuple { type_name, members, .. } => {
14710 assert_eq!(*type_name, None);
14711 let members = members.as_ref().expect("expected resolved members");
14712 assert_eq!(members.len(), 2);
14713 assert_eq!(members[0].key.as_deref(), Some("street"));
14714 assert_eq!(members[1].key.as_deref(), Some("zip"));
14715 }
14716 other => panic!("expected ShapeNode::NamedTuple, got {other:?}"),
14717 }
14718 }
14719
14720 #[test]
14721 fn test_path_traversal_into_structural_tuple_property() {
14722 use crate::schema::{TupleMemberDescriptor, TupleMemberKind};
14723 let schema = SchemaDescriptor {
14724 types: vec![TypeDescriptor {
14725 name: "Person".into(),
14726 module: "default".into(),
14727 table: "Person".into(),
14728 abstract_: false,
14729 materialized: false,
14730 description: None,
14731 parents: vec![],
14732 interfaces: vec![],
14733 bases: vec![],
14734 properties: vec![PropertyDescriptor {
14735 name: "address".into(),
14736 pg_type: "jsonb".into(),
14737 nullable: true,
14738 default_sql: None,
14739 default_pyql: None,
14740 description: None,
14741 check_constraints: vec![],
14742 is_exclusive: false,
14743 is_pk: false,
14744 is_readonly: false,
14745 rewrites: vec![],
14746 tuple_members: Some(vec![
14747 TupleMemberDescriptor {
14748 name: Some("street".into()),
14749 kind: TupleMemberKind::Scalar { pg_type: "text".into() },
14750 },
14751 TupleMemberDescriptor {
14752 name: Some("zip".into()),
14753 kind: TupleMemberKind::Scalar { pg_type: "text".into() },
14754 },
14755 ]),
14756 column_type: None,
14757 }],
14758 links: vec![],
14759 multilinks: vec![],
14760 computed: vec![],
14761 constraints: vec![],
14762 indexes: vec![],
14763 partition: None,
14764 vector_indexes: vec![],
14765 search_indexes: vec![],
14766 triggers: vec![],
14767 junction: false,
14768 signals: vec![],
14769 }],
14770 scalars: vec![],
14771 enums: vec![],
14772 named_tuples: vec![],
14773 globals: vec![],
14774 functions: vec![],
14775 aliases: vec![],
14776 channels: vec![],
14777 ..Default::default()
14778 };
14779 let out = compile_and_emit_with("SELECT default::Person.address.street", &schema);
14785 assert!(out.sql.contains("\"address\"->'street'"), "got:\n{}", out.sql);
14786 }
14787
14788 #[test]
14789 fn test_structural_tuple_cast_shape_carries_real_members() {
14790 let out = compile_and_emit("SELECT <tuple<street: str, zip: str>>$p");
14791 match &out.shape.root {
14792 crate::query::ShapeNode::NamedTuple { type_name, members, .. } => {
14793 assert_eq!(*type_name, None);
14794 let members = members.as_ref().expect("expected resolved members");
14795 assert_eq!(members.len(), 2);
14796 assert_eq!(members[0].key.as_deref(), Some("street"));
14797 assert_eq!(members[1].key.as_deref(), Some("zip"));
14798 }
14799 other => panic!("expected ShapeNode::NamedTuple, got {other:?}"),
14800 }
14801 }
14802
14803 #[test]
14804 fn test_tuple_cast_mixed_named_and_unnamed_elements_rejected() {
14805 match parse::parse("SELECT <tuple<x: float64, bool>>$p") {
14806 Ok(_) => panic!("expected parse error for mixed named/unnamed tuple elements"),
14807 Err(e) => assert!(e.to_string().contains("all named or all unnamed"), "got: {}", e),
14808 }
14809 }
14810
14811 #[test]
14812 fn test_is_with_tuple_type_rejected() {
14813 let schema = make_schema();
14814 let ast = parse::parse("SELECT Person FILTER Person is tuple<x: float64, y: float64>").unwrap();
14815 match ir::compile(&ast, &schema) {
14816 Ok(_) => panic!("expected error for IS with a tuple type"),
14817 Err(e) => {
14818 assert!(
14819 e.to_string().contains("cannot use IS with a tuple or array type"),
14820 "got: {}",
14821 e
14822 );
14823 }
14824 }
14825 }
14826
14827 #[test]
14828 fn test_is_with_array_type_rejected() {
14829 let schema = make_schema();
14830 let ast = parse::parse("SELECT Person FILTER Person is array<str>").unwrap();
14831 match ir::compile(&ast, &schema) {
14832 Ok(_) => panic!("expected error for IS with an array type"),
14833 Err(e) => {
14834 assert!(
14835 e.to_string().contains("cannot use IS with a tuple or array type"),
14836 "got: {}",
14837 e
14838 );
14839 }
14840 }
14841 }
14842
14843 #[test]
14844 fn test_tuple_index_on_non_literal_falls_back_to_jsonb_index() {
14845 let out = compile_and_emit("SELECT (<tuple<int64, str>>('1', 3)).1");
14849 assert!(out.sql.contains("->1"), "got:\n{}", out.sql);
14850 }
14851
14852 #[test]
14853 fn test_tuple_index_out_of_bounds_on_cast_target_errors_at_compile_time() {
14854 let ast = parse::parse("SELECT (<tuple<int64, str>>('1', 3)).2").unwrap();
14860 let schema = make_schema();
14861 match ir::compile(&ast, &schema) {
14862 Ok(_) => panic!("expected out-of-bounds tuple index error"),
14863 Err(e) => {
14864 assert!(
14865 e.to_string()
14866 .contains("2 is not a member of tuple<std::int64, std::str>"),
14867 "got: {}",
14868 e
14869 );
14870 }
14871 }
14872 }
14873
14874 #[test]
14875 fn test_positional_tuple_literal_cast_to_tuple_type_compiles() {
14876 let out = compile_and_emit("SELECT <tuple<int64, str>>(1, 'x')");
14880 assert!(
14881 out.sql.contains("jsonb_build_array((1)::int8, ('x')::text)"),
14882 "got:\n{}",
14883 out.sql
14884 );
14885 }
14886
14887 #[test]
14888 fn test_positional_tuple_literal_cast_coerces_mismatched_literal_types() {
14889 let out = compile_and_emit("SELECT <tuple<int64, str>>('1', 3)");
14893 assert!(
14894 out.sql.contains("jsonb_build_array(('1')::int8, (3)::text)"),
14895 "got:\n{}",
14896 out.sql
14897 );
14898 }
14899
14900 #[test]
14901 fn test_nested_tuple_literal_cast_applies_casts_recursively() {
14902 let out = compile_and_emit(
14903 "SELECT <tuple<point: tuple<x: float64, y: float64>, label: str>>(point := ('1', 2), label := 5)",
14904 );
14905 assert!(
14906 out.sql.contains(
14907 "jsonb_build_object('point', jsonb_build_object('x', ('1')::float8, 'y', (2)::float8), 'label', (5)::text)"
14908 ),
14909 "got:\n{}",
14910 out.sql
14911 );
14912 }
14913
14914 #[test]
14915 fn test_positional_tuple_literal_nested_inside_named_tuple_compiles() {
14916 let out = compile_and_emit("SELECT (point := (1, 2), label := 'origin')");
14917 assert!(out.sql.contains("jsonb_build_array(1, 2)"), "got:\n{}", out.sql);
14918 assert!(out.sql.contains("jsonb_build_object("), "got:\n{}", out.sql);
14919 }
14920
14921 #[test]
14922 fn test_positional_tuple_literal_in_schema_bound_shape_field_compiles() {
14923 let out = compile_and_emit("SELECT Person { name, pair := (1, 2) }");
14924 assert!(out.sql.contains("jsonb_build_array(1, 2)"), "got:\n{}", out.sql);
14925 }
14926
14927 fn make_schema_with_search_index(backend: crate::schema::SearchBackend) -> SchemaDescriptor {
14936 use crate::schema::{SearchIndexDescriptor, SearchPointerDescriptor, SearchWeight};
14937 let mut s = make_schema();
14938 if let Some(td) = s.types.iter_mut().find(|t| t.name == "Person") {
14939 td.search_indexes.push(SearchIndexDescriptor {
14940 index_name: None,
14941 backend,
14942 pointers: vec![SearchPointerDescriptor {
14943 name: "name".into(),
14944 weight: SearchWeight::A,
14945 }],
14946 });
14947 }
14948 s
14949 }
14950
14951 #[test]
14952 fn test_insert_enqueues_a_meilisearch_outbox_row() {
14953 let schema = make_schema_with_search_index(crate::schema::SearchBackend::Meilisearch);
14954 let out = compile_and_emit_with("INSERT Person { name := 'Alice', age := 30 }", &schema);
14955 assert!(
14956 out.sql.contains("'Meilisearch'::_pylon.\"IndexKind\""),
14957 "expected a Meilisearch outbox enqueue CTE, got:\n{}",
14958 out.sql,
14959 );
14960 assert!(
14961 out.sql.contains("INSERT INTO _pylon.\"IndexOutbox\""),
14962 "got:\n{}",
14963 out.sql
14964 );
14965 }
14966
14967 #[test]
14968 fn test_insert_enqueues_an_opensearch_outbox_row() {
14969 let schema = make_schema_with_search_index(crate::schema::SearchBackend::OpenSearch);
14970 let out = compile_and_emit_with("INSERT Person { name := 'Alice', age := 30 }", &schema);
14971 assert!(
14972 out.sql.contains("'OpenSearch'::_pylon.\"IndexKind\""),
14973 "expected an OpenSearch outbox enqueue CTE, got:\n{}",
14974 out.sql,
14975 );
14976 }
14977
14978 #[test]
14979 fn test_insert_does_not_enqueue_an_outbox_row_for_a_postgres_backed_search_index() {
14980 let schema = make_schema_with_search_index(crate::schema::SearchBackend::Postgres);
14983 let out = compile_and_emit_with("INSERT Person { name := 'Alice', age := 30 }", &schema);
14984 assert!(
14985 !out.sql.contains("_pylon.\"IndexOutbox\""),
14986 "did not expect an outbox enqueue, got:\n{}",
14987 out.sql
14988 );
14989 }
14990
14991 #[test]
14992 fn test_update_enqueues_a_meilisearch_outbox_row() {
14993 let schema = make_schema_with_search_index(crate::schema::SearchBackend::Meilisearch);
14994 let out = compile_and_emit_with("UPDATE Person FILTER .name = 'Alice' SET { age := 31 }", &schema);
14995 assert!(
14996 out.sql.contains("'Meilisearch'::_pylon.\"IndexKind\""),
14997 "expected a Meilisearch outbox enqueue CTE, got:\n{}",
14998 out.sql,
14999 );
15000 }
15001
15002 #[test]
15003 fn test_delete_enqueues_a_meilisearch_outbox_delete_job() {
15004 let schema = make_schema_with_search_index(crate::schema::SearchBackend::Meilisearch);
15005 let out = compile_and_emit_with("DELETE Person FILTER .name = 'Alice'", &schema);
15006 assert!(
15007 out.sql.contains("'Meilisearch'::_pylon.\"IndexKind\""),
15008 "expected a Meilisearch outbox enqueue CTE, got:\n{}",
15009 out.sql,
15010 );
15011 assert!(
15012 out.sql.contains("'delete'"),
15013 "expected the delete operation literal, got:\n{}",
15014 out.sql
15015 );
15016 }
15017
15018 #[test]
15019 fn test_range_intrinsic_resolves_int_literals_to_int8range() {
15020 let out = compile_and_emit("SELECT std::overlaps(std::range(1, 3), std::range(2, 5))");
15025 assert!(out.sql.contains("int8range(1, 3)"), "got:\n{}", out.sql);
15026 assert!(out.sql.contains("int8range(2, 5)"), "got:\n{}", out.sql);
15027 assert!(
15028 out.sql.contains(" && "),
15029 "expected infix && for overlaps, got:\n{}",
15030 out.sql
15031 );
15032 assert!(
15033 !out.sql.contains("\"std\""),
15034 "must not emit a literal std schema call, got:\n{}",
15035 out.sql
15036 );
15037 }
15038
15039 #[test]
15040 fn test_range_intrinsic_resolves_datetime_to_tstzrange() {
15041 let out =
15042 compile_and_emit("SELECT std::range(<datetime>'2024-01-01T00:00:00Z', <datetime>'2024-06-01T00:00:00Z')");
15043 assert!(out.sql.contains("tstzrange("), "got:\n{}", out.sql);
15044 }
15045
15046 #[test]
15047 fn test_range_intrinsic_four_arg_form_computes_bounds_string() {
15048 let out = compile_and_emit("SELECT std::range(1, 3, true, false)");
15049 assert!(out.sql.contains("int8range(1, 3,"), "got:\n{}", out.sql);
15050 assert!(
15051 out.sql.contains("CASE WHEN"),
15052 "expected a dynamic bounds-string CASE, got:\n{}",
15053 out.sql
15054 );
15055 }
15056
15057 #[test]
15058 fn test_multirange_intrinsic_resolves_from_range_element() {
15059 let out = compile_and_emit("SELECT std::multirange([std::range(1, 3), std::range(5, 7)])");
15060 assert!(out.sql.contains("int8multirange(VARIADIC "), "got:\n{}", out.sql);
15061 assert!(
15062 !out.sql.contains("\"std\""),
15063 "must not emit a literal std schema call, got:\n{}",
15064 out.sql
15065 );
15066 }
15067}