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(&binding_outer_key(cte_name)),
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(IrPathJoin::target)
450 .find(|target| target.alias == alias)
451}
452
453fn source_type_disc(src: &IrSource) -> String {
456 match source_fanout(src) {
457 Some(_) => format!("{}.\"__type__\"", qi(&src.alias)),
458 None => type_disc(&src.type_name),
459 }
460}
461
462fn emit_poly_union(implementors: &[IrPolyImplementor], columns: &[String]) -> String {
465 let col_list = columns.iter().map(|c| qi(c)).collect::<Vec<_>>().join(", ");
466 implementors
467 .iter()
468 .map(|imp| {
469 format!(
470 " SELECT {}::text AS \"__type__\", {} FROM {}",
471 sql_str(&imp.type_name),
472 col_list,
473 qn(&imp.module, &imp.table),
474 )
475 })
476 .collect::<Vec<_>>()
477 .join("\n UNION ALL\n")
478}
479
480fn emit_select_stmt(sel: &IrSelect, ctes: &[IrCteDef]) -> SqlOutput {
483 match sel.rows.as_slice() {
484 [IrRowSource::Bound { source, shape }] => emit_bound_select(sel, source, shape),
485 rows if rows.len() > 1 && rows.iter().all(|r| matches!(r, IrRowSource::Bound { .. })) => {
486 emit_bound_union_select(sel, rows)
487 }
488 rows => emit_free_rows(sel, rows, ctes),
489 }
490}
491
492fn bound_union_from_clause(rows: &[IrRowSource], common_columns: &[String]) -> String {
499 let columns = common_columns.iter().map(|c| qi(c)).collect::<Vec<_>>().join(", ");
500 rows.iter()
501 .map(|row| match row {
502 IrRowSource::Bound { source, .. } if !common_columns.is_empty() => {
503 let own_type = match source_fanout(source) {
504 Some(_) => "\"__type__\"".to_string(),
505 None => format!("{} AS \"__type__\"", type_disc(&source.type_name)),
506 };
507 format!(" SELECT {own_type}, {columns} FROM {}", source_ref(source))
508 }
509 IrRowSource::Bound { source, .. } => format!(" SELECT * FROM {}", source_ref(source)),
510 IrRowSource::Free(_) => unreachable!("caller checked every row is bound"),
511 })
512 .collect::<Vec<_>>()
513 .join("\n UNION ALL\n")
514}
515
516fn emit_bound_union_select(sel: &IrSelect, rows: &[IrRowSource]) -> SqlOutput {
517 let IrRowSource::Bound { source, shape } = &rows[0] else {
518 unreachable!("caller checked every row is bound")
519 };
520 let alias = &source.alias;
521 let (pointer_exprs, shape_pointers) = build_shape(shape, alias);
522 let row_type = if sel.poly_columns.is_empty() {
523 type_disc(&source.type_name)
524 } else {
525 format!("{}.\"__type__\"", qi(alias))
526 };
527 let mut parts = vec![row_type];
528 parts.extend(pointer_exprs);
529
530 let mut sql = format!(
531 "SELECT {}(\n {}\n) AS result\nFROM (\n{}\n) AS {}",
532 if sel.distinct { "DISTINCT " } else { "" },
533 parts.join(",\n "),
534 bound_union_from_clause(rows, &sel.poly_columns),
535 qi(alias),
536 );
537 append_filter(&mut sql, &sel.filter);
538 append_order_by(&mut sql, &sel.order_by);
539 append_offset_limit(&mut sql, &sel.offset, &sel.limit);
540
541 SqlOutput {
542 sql,
543 shape: ShapeDescriptor {
544 root: ShapeNode::Object {
545 name: String::new(),
546 type_name: Some(source.type_name.clone()),
547 position: 0,
548 cardinality: Cardinality::Many,
549 pointers: prepend_type(shape_pointers),
550 has_implicit_id: shape_has_implicit_id(shape),
551 },
552 },
553 inference_plan: None,
554 }
555}
556
557fn emit_bound_select(sel: &IrSelect, source: &IrSource, shape: &[IrShapePointer]) -> SqlOutput {
563 let alias = &source.alias;
564 let (pointer_exprs, shape_pointers) = build_shape(shape, alias);
565
566 let type_expr = if sel.polymorphic {
567 format!("{}.\"__type__\"", qi(alias))
568 } else {
569 type_disc(&source.type_name)
570 };
571 let mut parts = vec![type_expr];
572 parts.extend(pointer_exprs);
573 let tuple = parts.join(",\n ");
574
575 let distinct = if sel.distinct { "DISTINCT " } else { "" };
576
577 let from_clause = if let Some(dml) = &sel.dml_source {
579 let mut cte_parts = match dml.as_ref() {
580 IrStmt::Update(upd) if update_has_any_multilink(upd) => emit_update_multilink_ctes(upd, DML_CTE),
581 IrStmt::Insert(ins) if insert_has_any_multilink(ins) => emit_insert_multilink_ctes(ins, DML_CTE),
582 IrStmt::Insert(ins) if !ins.nested_ctes.is_empty() => {
585 let mut parts = emit_user_cte_parts(&ins.nested_ctes);
586 parts.push(format!("\"_dml\" AS (\n{}\n)", emit_dml_as_cte_source(dml)));
587 parts
588 }
589 IrStmt::Update(upd) if !upd.nested_ctes.is_empty() => {
590 let mut parts = emit_user_cte_parts(&upd.nested_ctes);
591 parts.push(format!("\"_dml\" AS (\n{}\n)", emit_dml_as_cte_source(dml)));
592 parts
593 }
594 IrStmt::Update(upd) if !upd.poly_implementors.is_empty() => emit_poly_update_dml_ctes(upd, "_dml"),
595 IrStmt::Delete(del) if !del.poly_implementors.is_empty() => emit_poly_delete_dml_ctes(del, "_dml"),
596 _ => vec![format!("\"_dml\" AS (\n{}\n)", emit_dml_as_cte_source(dml))],
597 };
598 let (enqueue_v, enqueue_s) = match dml.as_ref() {
599 IrStmt::Insert(ins) => (ins.enqueue_vector.as_slice(), ins.enqueue_search.as_slice()),
600 IrStmt::Update(upd) => (upd.enqueue_vector.as_slice(), upd.enqueue_search.as_slice()),
601 _ => (&[][..], &[][..]),
602 };
603 cte_parts.extend(enqueue_ctes(enqueue_v, "_dml"));
604 cte_parts.extend(enqueue_search_ctes(enqueue_s, "_dml", enqueue_v.len()));
605 format!(
606 "WITH\n{}\nSELECT {}(\n {}\n) AS result\nFROM \"_dml\" AS {}",
607 cte_parts.join(",\n"),
608 distinct,
609 tuple,
610 qi(alias)
611 )
612 } else if sel.polymorphic && !source.table.starts_with("@cte:") {
613 let union_sql = emit_poly_union(&sel.poly_implementors, &sel.poly_columns);
615 format!(
616 "SELECT {}(\n {}\n) AS result\nFROM (\n{}\n) AS {}",
617 distinct,
618 tuple,
619 union_sql,
620 qi(alias)
621 )
622 } else {
623 format!(
626 "SELECT {}(\n {}\n) AS result\nFROM {} AS {}",
627 distinct,
628 tuple,
629 source_ref(source),
630 qi(alias)
631 )
632 };
633
634 let mut sql = from_clause;
635 append_filter(&mut sql, &sel.filter);
636 append_order_by(&mut sql, &sel.order_by);
637 append_offset_limit(&mut sql, &sel.offset, &sel.limit);
638 append_lock_clause(&mut sql, &sel.lock);
639
640 let root_pointers = prepend_type(shape_pointers);
641 SqlOutput {
642 sql,
643 shape: ShapeDescriptor {
644 root: ShapeNode::Object {
645 name: String::new(),
646 type_name: Some(source.type_name.clone()),
647 position: 0,
648 cardinality: Cardinality::Many,
649 pointers: root_pointers,
650 has_implicit_id: shape_has_implicit_id(shape),
651 },
652 },
653 inference_plan: None,
654 }
655}
656
657fn nested_cte_from(ctes: &[IrCteDef], reading: &str, lead: &str) -> String {
664 let joined: Vec<String> = ctes
665 .iter()
666 .map(|c| qi(&c.name))
667 .filter(|name| reading.contains(&format!("{name}.")))
668 .collect();
669 if joined.is_empty() {
670 return String::new();
671 }
672 format!("{lead}FROM {}", joined.join(", "))
673}
674
675fn emit_scalar_union(branches: &[IrStmt]) -> String {
678 let operands: Vec<String> = branches
679 .iter()
680 .enumerate()
681 .map(|(i, branch)| {
682 format!(
683 "SELECT \"v\" FROM (\n{}\n) AS \"_u{i}\"",
684 emit_dml_as_cte_source(branch)
685 )
686 })
687 .collect();
688 format!(
689 "SELECT ROW(v) AS result, v FROM (\n{}\n) AS _scalar",
690 operands.join("\nUNION ALL\n")
691 )
692}
693
694fn emit_dml_as_cte_source(stmt: &IrStmt) -> String {
695 match stmt {
696 IrStmt::ScalarUnion(branches) => format!(" {}", emit_scalar_union(branches)),
697 IrStmt::Insert(ins) => {
698 let rewrite_cols: std::collections::HashSet<&str> =
699 ins.rewrites.iter().map(|r| r.column.as_str()).collect();
700 let cols: Vec<String> = ins
701 .assignments
702 .iter()
703 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
704 .map(|(c, _)| format!(" {}", qi(c)))
705 .chain(ins.rewrites.iter().map(|r| format!(" {}", qi(&r.column))))
706 .collect();
707 let vals: Vec<String> = ins
708 .assignments
709 .iter()
710 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
711 .map(|(_, e)| format!(" {}", emit_expr(e)))
712 .chain(ins.rewrites.iter().map(|r| format!(" {}", emit_expr(&r.expr))))
713 .collect();
714 let mut sql = if !ins.nested_ctes.is_empty() || ins.guard.is_some() {
715 format!(" {}", emit_insert_row_sql(ins))
722 } else if cols.is_empty() {
723 format!(" INSERT INTO {} DEFAULT VALUES", target_ref(&ins.target))
725 } else {
726 format!(
727 " INSERT INTO {} (\n{}\n ) VALUES (\n{}\n )",
728 target_ref(&ins.target),
729 cols.join(",\n"),
730 vals.join(",\n"),
731 )
732 };
733 if let Some(conflict) = &ins.unless_conflict {
734 emit_conflict(&mut sql, conflict);
735 }
736 sql.push_str("\n RETURNING *");
737 sql
738 }
739 IrStmt::Update(upd) => {
740 let alias = &upd.target.alias;
749 let sets = update_set_fragments(&upd.assignments, &upd.rewrites, " ");
750 let mut sql = format!(
751 " UPDATE {} AS {}\n SET\n{}",
752 target_ref(&upd.target),
753 qi(alias),
754 sets.join(",\n"),
755 );
756 sql.push_str(&nested_cte_from(&upd.nested_ctes, &sets.join(","), "\n "));
759 append_filter(&mut sql, &upd.filter);
760 sql.push_str(&format!("\n RETURNING {}.*", qi(alias)));
763 sql
764 }
765 IrStmt::Delete(del) => {
766 let alias = &del.target.alias;
767 let mut sql = format!(" DELETE FROM {} AS {}", target_ref(&del.target), qi(alias),);
768 append_filter(&mut sql, &del.filter);
769 sql.push_str("\n RETURNING *");
770 sql
771 }
772 IrStmt::Select(inner) => match inner.rows.as_slice() {
773 [IrRowSource::Bound { source, .. }]
778 if let Some(read) = inner.dml_source.as_deref()
779 && !matches!(read, IrStmt::Insert(_) | IrStmt::Update(_) | IrStmt::Delete(_)) =>
780 {
781 let mut sql = format!(
783 " SELECT {}* FROM (\n{}\n ) AS {}",
784 if inner.distinct { "DISTINCT " } else { "" },
785 emit_dml_as_cte_source(read),
786 qi(&source.alias),
787 );
788 append_filter(&mut sql, &inner.filter);
789 append_order_by(&mut sql, &inner.order_by);
790 append_offset_limit(&mut sql, &inner.offset, &inner.limit);
791 sql
792 }
793 [IrRowSource::Bound { source, .. }] => {
794 let from = if inner.polymorphic && !source.table.starts_with("@cte:") {
797 format!(
798 "(\n{}\n ) AS {}",
799 emit_poly_union(&inner.poly_implementors, &inner.poly_columns),
800 qi(&source.alias),
801 )
802 } else {
803 format!("{} AS {}", source_ref(source), qi(&source.alias))
804 };
805 let mut sql = format!(
806 " SELECT {}* FROM {}",
807 if inner.distinct { "DISTINCT " } else { "" },
808 from
809 );
810 append_filter(&mut sql, &inner.filter);
811 append_order_by(&mut sql, &inner.order_by);
812 append_offset_limit(&mut sql, &inner.offset, &inner.limit);
813 sql
814 }
815 rows if rows.len() > 1 && rows.iter().all(|r| matches!(r, IrRowSource::Bound { .. })) => {
818 let IrRowSource::Bound { source, .. } = &rows[0] else {
819 unreachable!("checked by the guard")
820 };
821 let mut sql = format!(
822 " SELECT {}* FROM (\n{}\n ) AS {}",
823 if inner.distinct { "DISTINCT " } else { "" },
824 bound_union_from_clause(rows, &inner.poly_columns),
825 qi(&source.alias),
826 );
827 append_filter(&mut sql, &inner.filter);
828 append_order_by(&mut sql, &inner.order_by);
829 append_offset_limit(&mut sql, &inner.offset, &inner.limit);
830 sql
831 }
832 _ => emit_select_stmt(inner, &[]).sql,
838 },
839 IrStmt::FunctionSelect(sel) => {
840 let args_sql = sel.fn_args.iter().map(emit_expr).collect::<Vec<_>>().join(", ");
843 let fn_call = format!("{}.{}({})", pg_schema(&sel.fn_module), qi(&sel.fn_name), args_sql);
844 let mut sql = format!(" SELECT * FROM {} AS {}", fn_call, qi(&sel.alias));
845 append_filter(&mut sql, &sel.filter);
846 append_order_by(&mut sql, &sel.order_by);
847 append_offset_limit(&mut sql, &sel.offset, &sel.limit);
848 sql
849 }
850 IrStmt::For(f) => {
851 if matches!(&f.iterator, IrForIterator::Values { exprs, .. } if exprs.is_empty()) {
856 return " SELECT NULL AS \"id\" WHERE FALSE".to_string();
857 }
858 let (values_from, _) = emit_for_iterator(&f.iterator, &format!("_for_{}", f.var_name));
859 let body =
860 prefix_body_ctes(emit_dml_as_cte_source(&f.body), &uncorrelated_body_ctes(f)).replace('\n', "\n ");
861 format!(
862 " SELECT \"_body\".*\n FROM {}\n CROSS JOIN LATERAL (\n {}\n ) AS \"_body\"",
863 values_from.replace('\n', "\n "),
864 body,
865 )
866 }
867 IrStmt::Group(grp) if matches!(grp.output, IrGroupOutput::Elements) => emit_group_elements_source(grp),
868 IrStmt::Group(_) | IrStmt::VectorSearch(_) | IrStmt::FtsSearch(_) => {
869 unreachable!("cannot appear as a CTE source")
870 }
871 IrStmt::PathSelect(ps) => {
872 let distinct = if ps.distinct { "DISTINCT " } else { "" };
878 let projection = match &ps.result {
879 IrPathResult::Scalar(e, _) => format!("{}{} AS v", distinct, emit_expr(e)),
883 IrPathResult::Object { alias, .. } => format!("{}{}.*", distinct, qi(alias)),
884 };
885 let mut sql = format!(
886 "SELECT {}\n FROM {}",
887 projection,
888 emit_path_joins(&ps.root, &ps.joins)
889 );
890 append_filter(&mut sql, &ps.filter);
891 append_order_by(&mut sql, &ps.order_by);
892 append_offset_limit(&mut sql, &ps.offset, &ps.limit);
893 match &ps.result {
894 IrPathResult::Scalar(e, _) => {
895 let row_value = if enum_type_of_expr(e).is_some() { "v::text" } else { "v" };
896 format!(" SELECT ROW({row_value}) AS result, v FROM ({sql}) AS _scalar")
897 }
898 IrPathResult::Object { .. } => format!(" {sql}"),
899 }
900 }
901 }
902}
903
904fn emit_poly_update_dml_ctes(upd: &IrUpdate, name: &str) -> Vec<String> {
939 let alias = &upd.target.alias;
940 let sets = update_set_fragments(&upd.assignments, &upd.rewrites, "");
941 let col_list = upd.poly_columns.iter().map(|c| qi(c)).collect::<Vec<_>>().join(", ");
942
943 let mut cte_parts = vec![];
944 let mut union_parts = vec![];
945 for (i, imp) in upd.poly_implementors.iter().enumerate() {
946 let cte_name = format!("{}__u{}", name, i);
947 let mut upd_sql = format!(
948 "UPDATE {} AS {}\nSET {}",
949 qn(&imp.module, &imp.table),
950 qi(alias),
951 sets.join(", "),
952 );
953 append_filter(&mut upd_sql, &upd.filter);
954 upd_sql.push_str(&format!("\nRETURNING {}", col_list));
955 cte_parts.push(format!("\"{}\" AS (\n{}\n)", cte_name, upd_sql));
956
957 union_parts.push(format!(
958 "SELECT {}::text AS \"__type__\", {} FROM \"{}\"",
959 sql_str(&imp.type_name),
960 col_list,
961 cte_name,
962 ));
963 }
964 cte_parts.push(format!("\"{}\" AS (\n{}\n)", name, union_parts.join("\nUNION ALL\n")));
965 cte_parts
966}
967
968fn concrete_poly_update(upd: &IrUpdate, imp: &IrPolyImplementor) -> IrUpdate {
973 let interface_prefix = format!("{}.", upd.target.table);
974 let own_prefix = format!("{}.", imp.table);
975 let own_junction = |table: &mut String, module: &mut String| {
976 if let Some(link) = table.strip_prefix(&interface_prefix) {
977 *table = format!("{own_prefix}{link}");
978 module.clone_from(&imp.module);
979 }
980 };
981 let mut concrete = upd.clone();
982 concrete.poly_implementors = vec![];
983 concrete.target.table = imp.table.clone();
984 concrete.target.type_name = imp.type_name.clone();
985 concrete.target.poly = None;
986 concrete
987 .multi_link_clears
988 .iter_mut()
989 .for_each(|c| own_junction(&mut c.junction_table, &mut c.module));
990 concrete
991 .multi_link_replaces
992 .iter_mut()
993 .chain(concrete.multi_link_appends.iter_mut())
994 .chain(concrete.multi_link_removals.iter_mut())
995 .for_each(|m| own_junction(&mut m.junction_table, &mut m.module));
996 concrete
997}
998
999fn emit_poly_update_multilink_ctes(upd: &IrUpdate, name: &str) -> Vec<String> {
1003 let col_list = upd.poly_columns.iter().map(|c| qi(c)).collect::<Vec<_>>().join(", ");
1004 let mut cte_parts = emit_user_cte_parts(&upd.nested_ctes);
1005 let mut union_parts = vec![];
1006 for (i, imp) in upd.poly_implementors.iter().enumerate() {
1007 let cte_name = format!("{}__u{}", name, i);
1008 cte_parts.extend(emit_update_multilink_cte_parts(
1009 &concrete_poly_update(upd, imp),
1010 &cte_name,
1011 false,
1012 ));
1013 union_parts.push(format!(
1014 "SELECT {}::text AS \"__type__\", {} FROM \"{}\"",
1015 sql_str(&imp.type_name),
1016 col_list,
1017 cte_name,
1018 ));
1019 }
1020 cte_parts.push(format!("\"{}\" AS (\n{}\n)", name, union_parts.join("\nUNION ALL\n")));
1021 cte_parts
1022}
1023
1024fn emit_poly_delete_dml_ctes(del: &IrDelete, name: &str) -> Vec<String> {
1025 let alias = &del.target.alias;
1026 let col_list = del.poly_columns.iter().map(|c| qi(c)).collect::<Vec<_>>().join(", ");
1027
1028 let mut cte_parts = vec![];
1029 let mut union_parts = vec![];
1030 for (i, imp) in del.poly_implementors.iter().enumerate() {
1031 let cte_name = format!("{}__d{}", name, i);
1032 let mut del_sql = format!("DELETE FROM {} AS {}", qn(&imp.module, &imp.table), qi(alias),);
1033 append_filter(&mut del_sql, &del.filter);
1034 del_sql.push_str(&format!("\nRETURNING {}", col_list));
1035 cte_parts.push(format!("\"{}\" AS (\n{}\n)", cte_name, del_sql));
1036
1037 union_parts.push(format!(
1038 "SELECT {}::text AS \"__type__\", {} FROM \"{}\"",
1039 sql_str(&imp.type_name),
1040 col_list,
1041 cte_name,
1042 ));
1043 }
1044 cte_parts.push(format!("\"{}\" AS (\n{}\n)", name, union_parts.join("\nUNION ALL\n")));
1045 cte_parts
1046}
1047
1048fn update_has_any_multilink(upd: &IrUpdate) -> bool {
1051 !upd.multi_link_clears.is_empty()
1052 || !upd.multi_link_replaces.is_empty()
1053 || !upd.multi_link_appends.is_empty()
1054 || !upd.multi_link_removals.is_empty()
1055}
1056
1057fn ml_clear_exclusion(rep: Option<&IrMultiLinkMutation>) -> String {
1075 match rep {
1076 Some(rep) => {
1077 let vals_ref = emit_multilink_values_subquery(&rep.values, &[]);
1078 format!(
1079 " AND {} NOT IN (SELECT \"_v\".\"id\" FROM {} AS \"_v\")",
1080 qi(&rep.target_col),
1081 vals_ref
1082 )
1083 }
1084 None => String::new(),
1085 }
1086}
1087
1088fn emit_update_multilink_ctes(upd: &IrUpdate, name: &str) -> Vec<String> {
1102 emit_update_multilink_cte_parts(upd, name, true)
1103}
1104
1105fn emit_update_multilink_cte_parts(upd: &IrUpdate, name: &str, emit_nested: bool) -> Vec<String> {
1110 let alias = &upd.target.alias;
1111 let has_scalar_changes = !upd.assignments.is_empty() || !upd.rewrites.is_empty();
1112 let ids_name = format!("{}__ids", name);
1113 let mut parts: Vec<String> = if emit_nested {
1115 emit_user_cte_parts(&upd.nested_ctes)
1116 } else {
1117 vec![]
1118 };
1119
1120 if has_scalar_changes {
1121 let sets = update_set_fragments(&upd.assignments, &upd.rewrites, "");
1122 let mut upd_sql = format!(
1123 "UPDATE {} AS {}\nSET {}",
1124 target_ref(&upd.target),
1125 qi(alias),
1126 sets.join(", "),
1127 );
1128 upd_sql.push_str(&nested_cte_from(&upd.nested_ctes, &sets.join(","), "\n"));
1131 append_filter(&mut upd_sql, &upd.filter);
1132 upd_sql.push_str(&format!("\nRETURNING {}.*", qi(alias)));
1135 parts.push(format!("\"{}\" AS (\n{}\n)", ids_name, upd_sql));
1136 } else {
1137 let mut sel = format!(
1138 "SELECT {}.* FROM {} AS {}",
1139 qi(alias),
1140 target_ref(&upd.target),
1141 qi(alias),
1142 );
1143 append_filter(&mut sel, &upd.filter);
1144 parts.push(format!("\"{}\" AS (\n{}\n)", ids_name, sel));
1145 }
1146
1147 for (i, clr) in upd.multi_link_clears.iter().enumerate() {
1148 let exclude = ml_clear_exclusion(upd.multi_link_replaces.get(i));
1149 let del = format!(
1150 "DELETE FROM {} WHERE {} IN (SELECT id FROM \"{}\"){}",
1151 qn(&clr.module, &clr.junction_table),
1152 qi(&clr.source_col),
1153 ids_name,
1154 exclude,
1155 );
1156 parts.push(format!("\"{}__clr_{}\" AS (\n{}\n)", name, i, del));
1157 }
1158 for (i, app) in upd.multi_link_appends.iter().enumerate() {
1159 parts.push(emit_ml_append_cte(
1160 app,
1161 &ids_name,
1162 &upd.target.alias,
1163 &format!("{}__ml_add_{}", name, i),
1164 ));
1165 }
1166 for (i, rem) in upd.multi_link_removals.iter().enumerate() {
1167 parts.push(emit_ml_remove_cte(
1168 rem,
1169 &ids_name,
1170 &upd.target.alias,
1171 &format!("{}__ml_rm_{}", name, i),
1172 ));
1173 }
1174 for (i, rep) in upd.multi_link_replaces.iter().enumerate() {
1175 parts.push(emit_ml_append_cte(
1176 rep,
1177 &ids_name,
1178 &upd.target.alias,
1179 &format!("{}__ml_rep_{}", name, i),
1180 ));
1181 }
1182
1183 parts.push(format!("\"{}\" AS (\n SELECT * FROM \"{}\"\n)", name, ids_name));
1184 parts
1185}
1186
1187fn insert_has_any_multilink(ins: &IrInsert) -> bool {
1188 !ins.multi_link_appends.is_empty()
1189}
1190
1191fn emit_insert_multilink_ctes(ins: &IrInsert, name: &str) -> Vec<String> {
1197 let ids_name = format!("{}__ids", name);
1198 let mut parts: Vec<String> = emit_user_cte_parts(&ins.nested_ctes);
1199
1200 let mut insert_sql = emit_insert_row_sql(ins);
1201 if let Some(conflict) = &ins.unless_conflict {
1202 emit_conflict(&mut insert_sql, conflict);
1203 }
1204 insert_sql.push_str("\nRETURNING *");
1205 parts.push(format!("\"{}\" AS (\n{}\n)", ids_name, insert_sql));
1206
1207 for (i, app) in ins.multi_link_appends.iter().enumerate() {
1208 parts.push(emit_ml_append_cte(
1209 app,
1210 &ids_name,
1211 &ins.target.alias,
1212 &format!("{}__ml_add_{}", name, i),
1213 ));
1214 }
1215
1216 parts.push(format!("\"{}\" AS (\n SELECT * FROM \"{}\"\n)", name, ids_name));
1217 parts
1218}
1219
1220fn append_insert_guard(sql: &mut String, ins: &IrInsert) {
1234 if let Some(guard) = &ins.guard {
1235 sql.push_str(&format!("\nWHERE {}", emit_expr(guard)));
1236 }
1237}
1238
1239fn emit_for_dml_ctes_within(f: &IrFor, name: &str, outer_alias: Option<&str>) -> Vec<String> {
1240 let iter_alias = format!("_for_{}", f.var_name);
1241 let (_, plain_iter_cte) = emit_for_iterator(&f.iterator, &iter_alias);
1242 let correlated = outer_alias;
1243 let iter_cte = match correlated {
1244 Some(outer) => emit_nested_for_iterator(&f.iterator, &iter_alias, outer),
1245 None => plain_iter_cte,
1246 };
1247 let mut parts: Vec<String> = vec![iter_cte];
1250 parts.extend(emit_user_cte_parts(&f.body_ctes));
1251 let ids_name = format!("{}__ids", name);
1252
1253 if correlated.is_some()
1256 && let IrStmt::Insert(ins) = f.body.as_ref()
1257 && ins.multi_link_appends.is_empty()
1258 {
1259 let rows_name = format!("{}__rows", name);
1260 let outer_source = format!("{}.{}", qi(&iter_alias), qi(OUTER_KEY));
1261 parts.push(emit_iteration_rows_cte(ins, &rows_name, &iter_alias, &outer_source));
1262 parts.push(emit_insert_from_rows_cte(ins, &ids_name, &rows_name));
1263 parts.push(format!("\"{}\" AS (\n SELECT * FROM \"{}\"\n)", name, rows_name));
1264 return parts;
1265 }
1266
1267 match f.body.as_ref() {
1268 IrStmt::Insert(ins) => {
1269 let rewrite_cols: std::collections::HashSet<&str> =
1270 ins.rewrites.iter().map(|r| r.column.as_str()).collect();
1271 let cols: Vec<String> = ins
1272 .assignments
1273 .iter()
1274 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
1275 .map(|(c, _)| qi(c))
1276 .chain(ins.rewrites.iter().map(|r| qi(&r.column)))
1277 .collect();
1278 let values: Vec<String> = ins
1279 .assignments
1280 .iter()
1281 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
1282 .map(|(_, e)| emit_expr(e))
1283 .chain(ins.rewrites.iter().map(|r| emit_expr(&r.expr)))
1284 .collect();
1285 let rows_name = format!("{}__rows", name);
1288 let correlated_targets =
1289 correlated_append_indices(&ins.multi_link_appends, &[&f.body_ctes, &ins.nested_ctes]);
1290 if correlated_targets.is_empty() {
1291 let mut sql = format!(
1292 "INSERT INTO {} ({})\nSELECT {} FROM {}",
1293 target_ref(&ins.target),
1294 cols.join(", "),
1295 values.join(", "),
1296 qi(&iter_alias),
1297 );
1298 append_insert_guard(&mut sql, ins);
1299 if let Some(conflict) = &ins.unless_conflict {
1300 emit_conflict(&mut sql, conflict);
1301 }
1302 sql.push_str("\nRETURNING *");
1303 parts.push(format!("\"{}\" AS (\n{}\n)", ids_name, sql));
1304 } else {
1305 let outer_source = match correlated {
1306 Some(_) => format!("{}.{}", qi(&iter_alias), qi(OUTER_KEY)),
1307 None => format!("{}.{}", qi(&iter_alias), qi("v")),
1308 };
1309 parts.push(emit_iteration_rows_cte(ins, &rows_name, &iter_alias, &outer_source));
1310 parts.push(emit_insert_from_rows_cte(ins, &ids_name, &rows_name));
1311 }
1312 for (i, append) in ins.multi_link_appends.iter().enumerate() {
1313 let cte_name = format!("{}__ml_add_{}", name, i);
1314 if correlated_targets.contains(&i) {
1315 let IrMultiLinkValueSource::CteRef(target) = &append.values.source else {
1316 unreachable!("only a CTE-backed value can carry an iteration key")
1317 };
1318 parts.push(emit_correlated_ml_append_cte(append, &rows_name, target, &cte_name));
1319 } else {
1320 parts.push(emit_ml_append_cte(append, &ids_name, &ins.target.alias, &cte_name));
1321 }
1322 }
1323 }
1324 IrStmt::Update(upd) => {
1325 let alias = &upd.target.alias;
1326 if upd.assignments.is_empty() && upd.rewrites.is_empty() && !upd.multi_link_appends.is_empty() {
1327 const ITER_COL: &str = "_iter";
1328 let generated: Vec<Option<&IrInsert>> = upd
1332 .multi_link_appends
1333 .iter()
1334 .map(|a| per_iteration_insert(a, &upd.nested_ctes))
1335 .collect();
1336 let new_cols: String = generated
1337 .iter()
1338 .enumerate()
1339 .filter_map(|(i, ins)| {
1340 ins.map(|ins| {
1341 format!(
1342 ", {} AS {}",
1343 ins.id_default_sql.as_deref().unwrap_or("uuidv7()"),
1344 qi(&format!("_new_{i}"))
1345 )
1346 })
1347 })
1348 .collect();
1349 let mut ids_sql = format!(
1350 "\"{}\" AS (\nSELECT {}.*, {}.\"v\" AS {}{} FROM {} AS {}, {}",
1351 ids_name,
1352 qi(alias),
1353 qi(&iter_alias),
1354 qi(ITER_COL),
1355 new_cols,
1356 target_ref(&upd.target),
1357 qi(alias),
1358 qi(&iter_alias),
1359 );
1360 append_filter(&mut ids_sql, &upd.filter);
1361 ids_sql.push_str("\n)");
1362 parts.push(ids_sql);
1363 for (i, append) in upd.multi_link_appends.iter().enumerate() {
1364 match generated[i] {
1365 Some(ins) => {
1366 parts.push(emit_for_nested_insert_cte(
1367 ins,
1368 &ids_name,
1369 &iter_alias,
1370 &format!("{}__ml_ins_{}", name, i),
1371 &format!("_new_{i}"),
1372 ));
1373 parts.push(emit_for_ml_append_cte(
1374 append,
1375 &ids_name,
1376 &format!("{}__ml_add_{}", name, i),
1377 &format!("_new_{i}"),
1378 ));
1379 }
1380 None => parts.push(emit_for_ml_append_cte(
1381 append,
1382 &ids_name,
1383 &format!("{}__ml_add_{}", name, i),
1384 ITER_COL,
1385 )),
1386 }
1387 }
1388 } else if !upd.poly_implementors.is_empty() {
1389 let sets = update_set_fragments(&upd.assignments, &upd.rewrites, "");
1393 let col_list = upd.poly_columns.iter().map(|c| qi(c)).collect::<Vec<_>>().join(", ");
1394 let ret_list = upd
1397 .poly_columns
1398 .iter()
1399 .map(|c| format!("{}.{}", qi(alias), qi(c)))
1400 .collect::<Vec<_>>()
1401 .join(", ");
1402 let mut union_parts = vec![];
1403 for (i, imp) in upd.poly_implementors.iter().enumerate() {
1404 let cte_name = format!("{}__u{}", ids_name, i);
1405 let mut upd_sql = format!(
1406 "UPDATE {} AS {}\nSET {}\nFROM {}",
1407 qn(&imp.module, &imp.table),
1408 qi(alias),
1409 sets.join(", "),
1410 qi(&iter_alias),
1411 );
1412 append_filter(&mut upd_sql, &upd.filter);
1413 upd_sql.push_str(&format!("\nRETURNING {}", ret_list));
1414 parts.push(format!("\"{}\" AS (\n{}\n)", cte_name, upd_sql));
1415 union_parts.push(format!(
1416 "SELECT {}::text AS \"__type__\", {} FROM \"{}\"",
1417 sql_str(&imp.type_name),
1418 col_list,
1419 cte_name,
1420 ));
1421 }
1422 parts.push(format!(
1423 "\"{}\" AS (\n{}\n)",
1424 ids_name,
1425 union_parts.join("\nUNION ALL\n")
1426 ));
1427 } else {
1428 let sets = update_set_fragments(&upd.assignments, &upd.rewrites, "");
1429 let mut sql = format!(
1430 "UPDATE {} AS {}\nSET {}\nFROM {}",
1431 target_ref(&upd.target),
1432 qi(alias),
1433 sets.join(", "),
1434 qi(&iter_alias),
1435 );
1436 append_filter(&mut sql, &upd.filter);
1437 sql.push_str(&format!("\nRETURNING {}.*", qi(alias)));
1438 parts.push(format!("\"{}\" AS (\n{}\n)", ids_name, sql));
1439 }
1440 }
1441 IrStmt::For(inner) => {
1445 let inner_alias = format!("_for_{}", inner.var_name);
1446 parts.push(emit_nested_for_iterator(&inner.iterator, &inner_alias, &iter_alias));
1447 parts.extend(emit_user_cte_parts(&inner.body_ctes));
1448 let IrStmt::Insert(ins) = inner.body.as_ref() else {
1449 unreachable!("a nested for-loop's own body is an insert: {:?}", inner.body)
1450 };
1451 let rewrite_cols: std::collections::HashSet<&str> =
1452 ins.rewrites.iter().map(|r| r.column.as_str()).collect();
1453 let cols: Vec<String> = ins
1454 .assignments
1455 .iter()
1456 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
1457 .map(|(c, _)| qi(c))
1458 .chain(ins.rewrites.iter().map(|r| qi(&r.column)))
1459 .collect();
1460 let values: Vec<String> = ins
1461 .assignments
1462 .iter()
1463 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
1464 .map(|(_, e)| emit_expr(e))
1465 .chain(ins.rewrites.iter().map(|r| emit_expr(&r.expr)))
1466 .collect();
1467 let mut sql = format!(
1468 "INSERT INTO {} ({})\nSELECT {} FROM {}",
1469 target_ref(&ins.target),
1470 cols.join(", "),
1471 values.join(", "),
1472 nested_for_from(&inner_alias, &iter_alias),
1473 );
1474 append_insert_guard(&mut sql, ins);
1475 if let Some(conflict) = &ins.unless_conflict {
1476 emit_conflict(&mut sql, conflict);
1477 }
1478 sql.push_str("\nRETURNING *");
1479 parts.push(format!("\"{}\" AS (\n{}\n)", ids_name, sql));
1480 for (i, append) in ins.multi_link_appends.iter().enumerate() {
1481 parts.push(emit_ml_append_cte(
1482 append,
1483 &ids_name,
1484 &ins.target.alias,
1485 &format!("{}__ml_add_{}", name, i),
1486 ));
1487 }
1488 }
1489 other => unreachable!("for-loop body is not a mutation: {other:?}"),
1491 }
1492
1493 parts.push(format!("\"{}\" AS (\n SELECT * FROM \"{}\"\n)", name, ids_name));
1494 parts
1495}
1496
1497fn emit_user_cte_parts(ctes: &[IrCteDef]) -> Vec<String> {
1506 let mut parts: Vec<String> = vec![];
1507 for c in ctes {
1508 let nested: &[IrCteDef] = match &c.stmt {
1514 IrStmt::Insert(ins) if insert_has_any_multilink(ins) => &[],
1515 IrStmt::Update(upd) if update_has_any_multilink(upd) => &[],
1516 IrStmt::Insert(ins) => &ins.nested_ctes,
1517 IrStmt::Update(upd) => &upd.nested_ctes,
1518 _ => &[],
1519 };
1520 if !nested.is_empty() {
1521 parts.extend(emit_user_cte_parts(nested));
1522 }
1523 if let IrStmt::Update(upd) = &c.stmt {
1524 if !upd.poly_implementors.is_empty() {
1525 parts.extend(if update_has_any_multilink(upd) {
1526 emit_poly_update_multilink_ctes(upd, &c.name)
1527 } else {
1528 emit_poly_update_dml_ctes(upd, &c.name)
1529 });
1530 continue;
1531 }
1532 if update_has_any_multilink(upd) {
1533 parts.extend(emit_update_multilink_ctes(upd, &c.name));
1534 continue;
1535 }
1536 }
1537 if let IrStmt::Insert(ins) = &c.stmt
1538 && insert_has_any_multilink(ins)
1539 {
1540 parts.extend(emit_insert_multilink_ctes(ins, &c.name));
1541 continue;
1542 }
1543 if let IrStmt::Delete(del) = &c.stmt
1544 && !del.poly_implementors.is_empty()
1545 {
1546 parts.extend(emit_poly_delete_dml_ctes(del, &c.name));
1547 continue;
1548 }
1549 if let IrStmt::For(f) = &c.stmt
1550 && matches!(f.body.as_ref(), IrStmt::Insert(_) | IrStmt::Update(_) | IrStmt::For(_))
1551 {
1552 parts.extend(emit_for_dml_ctes_within(f, &c.name, c.correlated_to.as_deref()));
1553 continue;
1554 }
1555 let body = emit_dml_as_cte_source(&c.stmt);
1556 parts.push(match &c.correlated_to {
1557 Some(iterator) => format!(
1560 "\"{}\" AS (\nSELECT {}.\"v\" AS {}, \"_row\".*\nFROM {}\nCROSS JOIN LATERAL (\n{}\n) AS \"_row\"\n)",
1561 c.name,
1562 qi(iterator),
1563 qi(&binding_outer_key(&c.name)),
1564 qi(iterator),
1565 body,
1566 ),
1567 None => format!("\"{}\" AS (\n{}\n)", c.name, body),
1568 });
1569 }
1570 parts
1571}
1572
1573fn emit_cte_prefix(ctes: &[IrCteDef]) -> String {
1575 format!("WITH\n{}\n", emit_user_cte_parts(ctes).join(",\n"))
1576}
1577
1578pub(crate) fn collect_link_prop_names(vals: &IrMultiLinkValues, names: &mut Vec<String>) {
1583 for (name, _) in &vals.link_props {
1584 if !names.contains(name) {
1585 names.push(name.clone());
1586 }
1587 }
1588 match &vals.source {
1589 IrMultiLinkValueSource::Union(a, b) => {
1590 collect_link_prop_names(a, names);
1591 collect_link_prop_names(b, names);
1592 }
1593 IrMultiLinkValueSource::Asserted { inner, .. } => collect_link_prop_names(inner, names),
1594 _ => {}
1595 }
1596}
1597
1598fn emit_link_prop_cols(vals: &IrMultiLinkValues, prop_names: &[String]) -> String {
1603 prop_names
1604 .iter()
1605 .map(|name| match vals.link_props.iter().find(|(n, _)| n == name) {
1606 Some((_, expr)) => format!(", {} AS {}", emit_expr(expr), qi(name)),
1607 None => format!(", NULL AS {}", qi(name)),
1608 })
1609 .collect()
1610}
1611
1612fn emit_multilink_values_subquery(vals: &IrMultiLinkValues, prop_names: &[String]) -> String {
1618 emit_multilink_values_inner(vals, prop_names, true)
1619}
1620
1621fn emit_multilink_values_inner(vals: &IrMultiLinkValues, prop_names: &[String], bare_cte_ok: bool) -> String {
1626 if let IrMultiLinkValueSource::Union(a, b) = &vals.source {
1627 return format!(
1628 "({}\nUNION ALL\n{})",
1629 emit_multilink_values_inner(a, prop_names, false),
1630 emit_multilink_values_inner(b, prop_names, false),
1631 );
1632 }
1633
1634 if let IrMultiLinkValueSource::Asserted {
1639 fn_name,
1640 inner,
1641 message,
1642 } = &vals.source
1643 {
1644 let inner_sql = emit_multilink_values_inner(inner, &[], false);
1645 let prop_cols = emit_link_prop_cols(vals, prop_names);
1646 return format!(
1647 "(SELECT unnest(\"_pylon\".{}(ARRAY(SELECT \"_v\".\"id\" FROM {} AS \"_v\"){})) AS \"id\"{})",
1648 qi(fn_name),
1649 inner_sql,
1650 assert_message_arg(message),
1651 prop_cols,
1652 );
1653 }
1654
1655 let prop_cols = emit_link_prop_cols(vals, prop_names);
1656
1657 match &vals.source {
1658 IrMultiLinkValueSource::CteRef(name) => {
1659 if prop_cols.is_empty() && bare_cte_ok {
1660 format!("\"{}\"", name)
1662 } else {
1663 format!("(SELECT \"_s\".\"id\"{} FROM \"{}\" AS \"_s\")", prop_cols, name)
1664 }
1665 }
1666 IrMultiLinkValueSource::Select(s) => {
1667 let [IrRowSource::Bound { source, .. }] = s.rows.as_slice() else {
1672 unreachable!("IrMultiLinkValueSource::Select is always schema-bound")
1673 };
1674 let alias = &source.alias;
1675 let mut sql = format!(
1676 "(SELECT {}.\"id\"{} FROM {} AS {}",
1677 qi(alias),
1678 prop_cols,
1679 source_ref(source),
1680 qi(alias)
1681 );
1682 append_filter(&mut sql, &s.filter);
1683 sql.push(')');
1684 sql
1685 }
1686 IrMultiLinkValueSource::PathSelect(ps) => {
1687 let root_alias = &ps.root.alias;
1688 let target_alias = match &ps.result {
1691 IrPathResult::Object { alias, .. } => alias,
1692 IrPathResult::Scalar(..) => root_alias,
1693 };
1694 let mut sql = format!(
1695 "(SELECT {}.\"id\"{} FROM {} AS {}",
1696 qi(target_alias),
1697 prop_cols,
1698 source_ref(&ps.root),
1699 qi(root_alias)
1700 );
1701 for join in &ps.joins {
1702 sql.push_str(&emit_path_join_sql(join));
1703 }
1704 append_filter(&mut sql, &ps.filter);
1705 sql.push(')');
1706 sql
1707 }
1708 IrMultiLinkValueSource::Function(fs) => {
1709 let args = fs.fn_args.iter().map(emit_expr).collect::<Vec<_>>().join(", ");
1710 let mut sql = format!(
1711 "(SELECT {}.\"id\"{} FROM {}.{}({}) AS {}",
1712 qi(&fs.alias),
1713 prop_cols,
1714 pg_schema(&fs.fn_module),
1715 qi(&fs.fn_name),
1716 args,
1717 qi(&fs.alias),
1718 );
1719 append_filter(&mut sql, &fs.filter);
1720 sql.push(')');
1721 sql
1722 }
1723 IrMultiLinkValueSource::Union(..) | IrMultiLinkValueSource::Asserted { .. } => {
1724 unreachable!("handled above")
1725 }
1726 }
1727}
1728
1729fn emit_path_join_sql(join: &IrPathJoin) -> String {
1731 match join {
1732 IrPathJoin::Lateral { inner, target } => {
1733 let projected = match &inner.result {
1734 IrPathResult::Object { alias, .. } => alias.clone(),
1735 IrPathResult::Scalar(..) => unreachable!("a lateral step is only built for an object-valued computed"),
1736 };
1737 let mut body = format!(
1738 "SELECT {}.* FROM {}",
1739 qi(&projected),
1740 emit_path_joins(&inner.root, &inner.joins)
1741 );
1742 append_filter(&mut body, &inner.filter);
1743 append_order_by(&mut body, &inner.order_by);
1744 append_offset_limit(&mut body, &inner.offset, &inner.limit);
1745 format!(" JOIN LATERAL (\n{}\n) AS {} ON TRUE", body, qi(&target.alias))
1746 }
1747 IrPathJoin::Function {
1748 fn_module,
1749 fn_name,
1750 args,
1751 target,
1752 } => {
1753 let args_sql = args.iter().map(emit_expr).collect::<Vec<_>>().join(", ");
1754 format!(
1755 " CROSS JOIN LATERAL {}.{}({}) AS {}",
1756 pg_schema(fn_module),
1757 qi(fn_name),
1758 args_sql,
1759 qi(&target.alias),
1760 )
1761 }
1762 IrPathJoin::Single {
1763 source_alias,
1764 fk_col,
1765 target,
1766 } => {
1767 format!(
1768 " JOIN {} AS {} ON {}.\"id\" = {}.{}",
1769 source_ref(target),
1770 qi(&target.alias),
1771 qi(&target.alias),
1772 qi(source_alias),
1773 qi(fk_col)
1774 )
1775 }
1776 IrPathJoin::Multi {
1777 source_alias,
1778 junction_alias,
1779 join: ml_join,
1780 target,
1781 } => {
1782 let (jt_ref, src_col, tgt_col) = match ml_join {
1783 IrMultiLinkJoin::Standard { junction_table, module } => (
1784 junction_ref(module, junction_table),
1785 "source".to_string(),
1786 "target".to_string(),
1787 ),
1788 IrMultiLinkJoin::Through {
1789 junction_table,
1790 module,
1791 source_col,
1792 target_col,
1793 } => (
1794 junction_ref(module, junction_table),
1795 source_col.clone(),
1796 target_col.clone(),
1797 ),
1798 IrMultiLinkJoin::BacklinkFk { .. } | IrMultiLinkJoin::BacklinkJunction { .. } => {
1803 unreachable!("a forward multi-link path step never uses a backlink join variant")
1804 }
1805 };
1806 format!(
1807 " JOIN {} AS {} ON {}.{} = {}.\"id\" JOIN {} AS {} ON {}.{} = {}.\"id\"",
1808 jt_ref,
1809 qi(junction_alias),
1810 qi(junction_alias),
1811 qi(&src_col),
1812 qi(source_alias),
1813 source_ref(target),
1814 qi(&target.alias),
1815 qi(junction_alias),
1816 qi(&tgt_col),
1817 qi(&target.alias),
1818 )
1819 }
1820 IrPathJoin::BacklinkSingle {
1821 source_alias,
1822 fk_col,
1823 target,
1824 } => {
1825 format!(
1826 " JOIN {} AS {} ON {}.{} = {}.\"id\"",
1827 source_ref(target),
1828 qi(&target.alias),
1829 qi(&target.alias),
1830 qi(fk_col),
1831 qi(source_alias),
1832 )
1833 }
1834 IrPathJoin::BacklinkMulti {
1835 source_alias,
1836 junction_alias,
1837 junction_table,
1838 module,
1839 owner_col,
1840 current_col,
1841 target,
1842 } => {
1843 format!(
1844 " JOIN {} AS {} ON {}.{} = {}.\"id\" JOIN {} AS {} ON {}.\"id\" = {}.{}",
1845 junction_ref(module, junction_table),
1846 qi(junction_alias),
1847 qi(junction_alias),
1848 qi(current_col),
1849 qi(source_alias),
1850 source_ref(target),
1851 qi(&target.alias),
1852 qi(&target.alias),
1853 qi(junction_alias),
1854 qi(owner_col),
1855 )
1856 }
1857 }
1858}
1859
1860pub(crate) fn append_value_is_the_loop_variable(values: &IrMultiLinkValues, var: &str) -> bool {
1872 let IrMultiLinkValueSource::Select(sel) = &values.source else {
1873 return false;
1874 };
1875 let Some(IrExpr::BinOp(cmp)) = sel.filter.as_ref() else {
1876 return false;
1877 };
1878 matches!(
1879 (&cmp.left, &cmp.op, &cmp.right),
1880 (
1881 IrExpr::ColumnRef { column, .. },
1882 crate::parse::ast::BinOpKind::Eq,
1883 IrExpr::ForVar { name, .. },
1884 ) if column == "id" && name == var
1885 ) && values.link_props.is_empty()
1886}
1887
1888pub(crate) fn per_iteration_insert<'c>(mutation: &IrMultiLinkMutation, nested: &'c [IrCteDef]) -> Option<&'c IrInsert> {
1894 let IrMultiLinkValueSource::CteRef(name) = &mutation.values.source else {
1895 return None;
1896 };
1897 if !mutation.values.link_props.is_empty() {
1898 return None;
1899 }
1900 nested.iter().find(|c| &c.name == name).and_then(|c| match &c.stmt {
1901 IrStmt::Insert(ins) if ins.id_default_sql.is_some() && ins.multi_link_appends.is_empty() => Some(ins),
1902 _ => None,
1903 })
1904}
1905
1906fn emit_for_nested_insert_cte(
1911 ins: &IrInsert,
1912 ids_name: &str,
1913 iter_alias: &str,
1914 cte_name: &str,
1915 id_col: &str,
1916) -> String {
1917 let mut cols = vec![qi("id")];
1918 let mut values = vec![format!("\"{}\".{}", ids_name, qi(id_col))];
1919 for (col, expr) in &ins.assignments {
1920 cols.push(qi(col));
1921 values.push(emit_expr(expr));
1922 }
1923 format!(
1924 "\"{}\" AS (\nINSERT INTO {} ({})\nSELECT {} FROM \"{}\", {}\nWHERE \"{}\".{} = {}.\"v\"\nRETURNING {}\n)",
1925 cte_name,
1926 target_ref(&ins.target),
1927 cols.join(", "),
1928 values.join(", "),
1929 ids_name,
1930 qi(iter_alias),
1931 ids_name,
1932 qi("_iter"),
1933 qi(iter_alias),
1934 qi("id"),
1935 )
1936}
1937
1938fn emit_for_ml_append_cte(mutation: &IrMultiLinkMutation, ids_name: &str, cte_name: &str, iter_col: &str) -> String {
1939 format!(
1940 "\"{}\" AS (\nINSERT INTO {} ({}, {})\nSELECT \"{}\".\"id\", \"{}\".{} FROM \"{}\"\nON CONFLICT DO NOTHING\nRETURNING {}, {}\n)",
1941 cte_name,
1942 qn(&mutation.module, &mutation.junction_table),
1943 qi(&mutation.source_col),
1944 qi(&mutation.target_col),
1945 ids_name,
1946 ids_name,
1947 qi(iter_col),
1948 ids_name,
1949 qi(&mutation.source_col),
1950 qi(&mutation.target_col),
1951 )
1952}
1953
1954fn emit_ml_append_cte(mutation: &IrMultiLinkMutation, ids_name: &str, ids_alias: &str, cte_name: &str) -> String {
1957 let mut prop_names = vec![];
1958 collect_link_prop_names(&mutation.values, &mut prop_names);
1959 let vals_ref = emit_multilink_values_subquery(&mutation.values, &prop_names);
1960
1961 let extra_cols: String = prop_names.iter().map(|n| format!(", {}", qi(n))).collect();
1962 let extra_select: String = prop_names.iter().map(|n| format!(", \"_v\".{}", qi(n))).collect();
1963
1964 let conflict_clause = if mutation.single {
1968 let mut sets = vec![format!(
1978 "{} = EXCLUDED.{}",
1979 qi(&mutation.target_col),
1980 qi(&mutation.target_col)
1981 )];
1982 sets.extend(prop_names.iter().map(|n| format!("{} = EXCLUDED.{}", qi(n), qi(n))));
1983 format!(
1984 "ON CONFLICT ({}) DO UPDATE SET {}",
1985 qi(&mutation.source_col),
1986 sets.join(", ")
1987 )
1988 } else if prop_names.is_empty() {
1989 "ON CONFLICT DO NOTHING".to_string()
1990 } else {
1991 let sets: Vec<String> = prop_names
1992 .iter()
1993 .map(|n| format!("{} = EXCLUDED.{}", qi(n), qi(n)))
1994 .collect();
1995 format!(
1996 "ON CONFLICT ({}, {}) DO UPDATE SET {}",
1997 qi(&mutation.source_col),
1998 qi(&mutation.target_col),
1999 sets.join(", "),
2000 )
2001 };
2002
2003 let lateral = if vals_ref.starts_with('(') { "LATERAL " } else { "" };
2006 let ins = format!(
2007 "INSERT INTO {} ({}, {}{})\nSELECT {}.\"id\", \"_v\".\"id\"{} FROM \"{}\" AS {} CROSS JOIN {}{} AS \"_v\"\n{}\nRETURNING {}, {}",
2008 qn(&mutation.module, &mutation.junction_table),
2009 qi(&mutation.source_col),
2010 qi(&mutation.target_col),
2011 extra_cols,
2012 qi(ids_alias),
2013 extra_select,
2014 ids_name,
2015 qi(ids_alias),
2016 lateral,
2017 vals_ref,
2018 conflict_clause,
2019 qi(&mutation.source_col),
2020 qi(&mutation.target_col),
2021 );
2022 format!("\"{}\" AS (\n{}\n)", cte_name, ins)
2023}
2024
2025fn emit_ml_remove_cte(mutation: &IrMultiLinkMutation, ids_name: &str, ids_alias: &str, cte_name: &str) -> String {
2028 let vals_ref = emit_multilink_values_subquery(&mutation.values, &[]);
2031 let del = format!(
2035 "DELETE FROM {} AS \"_jn\"\nUSING \"{}\" AS {}\nWHERE \"_jn\".{} = {}.\"id\"\n AND \"_jn\".{} IN (SELECT \"id\" FROM {})\nRETURNING \"_jn\".{}, \"_jn\".{}",
2036 qn(&mutation.module, &mutation.junction_table),
2037 ids_name,
2038 qi(ids_alias),
2039 qi(&mutation.source_col),
2040 qi(ids_alias),
2041 qi(&mutation.target_col),
2042 vals_ref,
2043 qi(&mutation.source_col),
2044 qi(&mutation.target_col),
2045 );
2046 format!("\"{}\" AS (\n{}\n)", cte_name, del)
2047}
2048
2049use crate::ir::IrConflict;
2052
2053fn emit_conflict(sql: &mut String, conflict: &IrConflict) {
2054 let on_sql = conflict.on.as_ref().map(|e| match e {
2058 IrExpr::Tuple(elements) | IrExpr::Row { elements, .. } => {
2059 format!("({})", elements.iter().map(emit_expr).collect::<Vec<_>>().join(", "))
2060 }
2061 other => format!("({})", emit_expr(other)),
2062 });
2063 match (&on_sql, &conflict.do_update) {
2064 (None, None) => sql.push_str(" ON CONFLICT DO NOTHING"),
2065 (Some(on), None) => sql.push_str(&format!(" ON CONFLICT {} DO NOTHING", on)),
2066 (None, Some(updates)) => {
2067 sql.push_str(&format!(" ON CONFLICT DO UPDATE SET {}", do_update_sets(updates)));
2068 }
2069 (Some(on), Some(updates)) => {
2070 sql.push_str(&format!(
2071 " ON CONFLICT {} DO UPDATE SET {}",
2072 on,
2073 do_update_sets(updates),
2074 ));
2075 }
2076 }
2077 if conflict.do_update.is_some()
2078 && let Some(predicate) = &conflict.do_update_where
2079 {
2080 sql.push_str(&format!(" WHERE {}", emit_expr(predicate)));
2081 }
2082}
2083
2084fn do_update_sets(updates: &[(String, IrExpr)]) -> String {
2085 updates
2086 .iter()
2087 .map(|(col, expr)| format!("{} = {}", qi(col), emit_expr(expr)))
2088 .collect::<Vec<_>>()
2089 .join(", ")
2090}
2091
2092fn is_integer_expr(expr: &IrExpr) -> bool {
2097 match expr {
2098 IrExpr::ColumnRef { pg_type, .. } => matches!(
2099 pg_type.as_str(),
2100 "int2" | "int4" | "int8" | "integer" | "bigint" | "smallint"
2101 ),
2102 IrExpr::Literal(crate::ir::IrLiteral::Int(_)) => true,
2103 IrExpr::BinOp(op) => is_integer_expr(&op.left) && is_integer_expr(&op.right),
2104 _ => false,
2105 }
2106}
2107
2108fn is_composite_tuple_cast(cast: &crate::ir::IrTypeCast) -> bool {
2113 cast.tuple_shape.is_some() && cast.pg_type != "jsonb" && cast.pg_type != "jsonb[]"
2114}
2115
2116fn is_raw_scalar(expr: &IrExpr) -> bool {
2117 matches!(expr, IrExpr::Array(items) if !matches!(items.first(), Some(IrExpr::Row { .. })))
2121 || matches!(expr, IrExpr::TypeCast(c) if c.pg_type == "jsonb")
2122 || matches!(expr, IrExpr::Row { .. })
2126 || matches!(expr, IrExpr::TypeCast(c) if is_composite_tuple_cast(c) && !c.pg_type.ends_with("[]"))
2129 || matches!(expr, IrExpr::JsonbField { .. })
2130 || matches!(expr, IrExpr::JsonbIndex { .. })
2131}
2132
2133fn emit_free_rows(sel: &IrSelect, rows: &[IrRowSource], ctes: &[IrCteDef]) -> SqlOutput {
2139 use crate::query::ShapeNode;
2140
2141 let items: Vec<&IrFreeExpr> = rows
2142 .iter()
2143 .map(|r| match r {
2144 IrRowSource::Free(item) => item,
2145 IrRowSource::Bound { .. } => unreachable!("mixed Bound/Free rows rejected at compile time"),
2146 })
2147 .collect();
2148
2149 if items.is_empty() {
2150 return SqlOutput {
2151 sql: "SELECT NULL AS result WHERE FALSE".to_string(),
2152 shape: ShapeDescriptor {
2153 root: ShapeNode::Scalar {
2154 name: String::new(),
2155 position: 0,
2156 },
2157 },
2158 inference_plan: None,
2159 };
2160 }
2161
2162 if items.len() == 1
2164 && let IrFreeExpr::AssertSet {
2165 fn_name,
2166 inner,
2167 message,
2168 } = items[0]
2169 {
2170 let array_sql = emit_array_source(inner) + &assert_message_arg(message);
2171 let mut sql = format!(
2172 "SELECT ROW(v) AS result FROM unnest(\"_pylon\".{}({})) AS _assert(v)",
2173 fn_name, array_sql,
2174 );
2175 if sel.distinct {
2176 sql = format!("SELECT DISTINCT * FROM ({}) AS \"_distinct\"", sql);
2177 }
2178 append_order_by(&mut sql, &sel.order_by);
2179 append_offset_limit(&mut sql, &sel.offset, &sel.limit);
2180 return SqlOutput {
2181 sql,
2182 shape: ShapeDescriptor {
2183 root: ShapeNode::Scalar {
2184 name: String::new(),
2185 position: 0,
2186 },
2187 },
2188 inference_plan: None,
2189 };
2190 }
2191
2192 let shape_root = free_item_shape(items.first().unwrap(), ctes);
2193
2194 let branches: Vec<String> = items
2195 .iter()
2196 .map(|item| match item {
2197 IrFreeExpr::Scalar(expr) => {
2198 if matches!(expr, IrExpr::ObjectSubquery(_)) {
2203 return format!("SELECT v AS result, v FROM (SELECT {} AS v) AS _obj", emit_expr(expr));
2204 }
2205 if is_raw_scalar(expr) {
2208 format!("SELECT v AS result, v FROM (SELECT {} AS v) AS _raw", emit_expr(expr))
2211 } else {
2212 let e = emit_expr(expr);
2219 let row_value = if enum_type_of_expr(expr).is_some() {
2220 "v::text"
2221 } else {
2222 "v"
2223 };
2224 let present = if items.len() > 1 { " WHERE v IS NOT NULL" } else { "" };
2228 format!("SELECT ROW({row_value}) AS result, v FROM (SELECT {e} AS v) AS _scalar{present}")
2229 }
2230 }
2231 IrFreeExpr::FreeObject(fields) => {
2232 let inner_cols: Vec<String> = fields
2238 .iter()
2239 .enumerate()
2240 .map(|(i, (_, e))| format!("{} AS \"_f{}\"", emit_expr(e), i))
2241 .collect();
2242 let row_items: Vec<String> = fields
2243 .iter()
2244 .enumerate()
2245 .map(|(i, (_, e))| {
2246 if enum_type_of_expr(e).is_some() {
2247 format!("\"_f{}\"::text", i)
2248 } else {
2249 format!("\"_f{}\"", i)
2250 }
2251 })
2252 .collect();
2253 let named_cols: Vec<String> = fields
2254 .iter()
2255 .enumerate()
2256 .map(|(i, (name, _))| format!("\"_f{}\" AS {}", i, qi(name)))
2257 .collect();
2258 format!(
2259 "SELECT ROW({}) AS result, {} FROM (SELECT {}) AS _obj",
2260 row_items.join(", "),
2261 named_cols.join(", "),
2262 inner_cols.join(", "),
2263 )
2264 }
2265 IrFreeExpr::Tuple(exprs) => {
2266 if exprs.len() == 1 {
2267 format!("SELECT ROW({}) AS result", emit_free_field_expr(&exprs[0]))
2268 } else {
2269 let parts: Vec<String> = exprs.iter().map(emit_free_field_expr).collect();
2270 format!("SELECT ({}) AS result", parts.join(", "))
2271 }
2272 }
2273 IrFreeExpr::NamedTupleRow(fields) => {
2274 let parts: Vec<String> = fields.iter().map(|(_, e)| emit_free_field_expr(e)).collect();
2275 if parts.len() == 1 {
2276 format!("SELECT ROW({}) AS result", parts[0])
2277 } else {
2278 format!("SELECT ({}) AS result", parts.join(", "))
2279 }
2280 }
2281 IrFreeExpr::AssertSet { .. } => unreachable!("AssertSet is handled by early return above"),
2282 IrFreeExpr::CtePassthrough(name) => format!("SELECT \"result\" FROM {}", qi(name)),
2283 })
2284 .collect();
2285
2286 let union_sql = branches.join("\nUNION ALL\n");
2287
2288 let mut sql = if sel.distinct {
2289 format!("SELECT DISTINCT * FROM (\n{}\n) AS \"_distinct\"", union_sql)
2291 } else {
2292 union_sql
2293 };
2294 if let Some(filter) = &sel.filter {
2298 sql = format!(
2299 "SELECT * FROM (\n{}\n) AS \"_filtered\"\nWHERE {}",
2300 sql,
2301 emit_expr(filter)
2302 );
2303 }
2304 append_order_by(&mut sql, &sel.order_by);
2305 append_offset_limit(&mut sql, &sel.offset, &sel.limit);
2306
2307 SqlOutput {
2308 sql,
2309 shape: ShapeDescriptor { root: shape_root },
2310 inference_plan: None,
2311 }
2312}
2313
2314fn enum_type_of_expr(expr: &IrExpr) -> Option<QualifiedPgType> {
2324 match expr {
2325 IrExpr::ColumnRef { pg_type, .. } => QualifiedPgType::of(pg_type),
2326 IrExpr::EnumLiteral { pg_type, .. } => Some(QualifiedPgType {
2327 name: pg_quoted_to_pylon(pg_type),
2328 is_array: false,
2329 }),
2330 _ => None,
2331 }
2332}
2333
2334fn enum_type_of_shape_expr(expr: &IrExpr) -> Option<QualifiedPgType> {
2343 if let Some(qualified) = enum_type_of_expr(expr) {
2344 return Some(qualified);
2345 }
2346 let IrExpr::Subquery(select) = expr else {
2347 return None;
2348 };
2349 let [IrRowSource::Bound { shape, .. }] = select.rows.as_slice() else {
2350 return None;
2351 };
2352 let [IrShapePointer::Scalar(scalar)] = shape.as_slice() else {
2353 return None;
2354 };
2355 QualifiedPgType::of(&scalar.pg_type)
2356}
2357
2358fn emit_free_field_expr(expr: &IrExpr) -> String {
2364 match enum_type_of_expr(expr) {
2365 Some(qualified) => format!("{}{}", emit_expr(expr), qualified.text_cast()),
2366 None => emit_expr(expr),
2367 }
2368}
2369
2370fn composite_tuple_shape(
2377 name: &str,
2378 position: usize,
2379 type_name: Option<String>,
2380 members: &[crate::query::JsonMember],
2381) -> crate::query::ShapeNode {
2382 use crate::query::ShapeNode;
2383 ShapeNode::Tuple {
2384 name: name.to_string(),
2385 position,
2386 elements: members
2387 .iter()
2388 .enumerate()
2389 .map(|(i, m)| composite_member_shape(i, m))
2390 .collect(),
2391 names: members.iter().map(|m| m.key.clone()).collect::<Option<Vec<_>>>(),
2393 type_name,
2394 }
2395}
2396
2397fn composite_member_shape(position: usize, member: &crate::query::JsonMember) -> crate::query::ShapeNode {
2398 use crate::query::{JsonMemberKind, ShapeNode};
2399 match &member.kind {
2400 JsonMemberKind::Scalar | JsonMemberKind::Decimal => ShapeNode::Scalar {
2403 name: String::new(),
2404 position,
2405 },
2406 JsonMemberKind::Enum { enum_type } => ShapeNode::Enum {
2407 name: String::new(),
2408 position,
2409 enum_type: enum_type.clone(),
2410 },
2411 JsonMemberKind::Tuple { type_name, members } => composite_tuple_shape("", position, type_name.clone(), members),
2412 }
2413}
2414
2415fn tuple_array_shape_node(name: &str, position: usize, items: &[IrExpr]) -> Option<crate::query::ShapeNode> {
2422 let IrExpr::Row {
2423 elements,
2424 names,
2425 is_free_object,
2426 } = items.first()?
2427 else {
2428 return None;
2429 };
2430 Some(crate::query::ShapeNode::Array {
2431 name: name.to_string(),
2432 position,
2433 element: Box::new(row_shape_node("", 0, elements, names, *is_free_object)),
2436 })
2437}
2438
2439fn row_shape_node(
2446 name: &str,
2447 position: usize,
2448 elements: &[IrExpr],
2449 names: &Option<Vec<String>>,
2450 is_free_object: bool,
2451) -> crate::query::ShapeNode {
2452 use crate::query::{Cardinality, ShapeNode};
2453 let members: Vec<ShapeNode> = elements
2454 .iter()
2455 .enumerate()
2456 .map(|(i, e)| {
2457 let member = names
2458 .as_ref()
2459 .and_then(|ns| ns.get(i))
2460 .map(String::as_str)
2461 .unwrap_or("");
2462 free_field_shape_node(member, i, e)
2463 })
2464 .collect();
2465 if is_free_object {
2470 return ShapeNode::Object {
2471 name: name.to_string(),
2472 type_name: None,
2473 position,
2474 cardinality: Cardinality::Many,
2475 pointers: members,
2476 has_implicit_id: false,
2477 };
2478 }
2479 ShapeNode::Tuple {
2480 name: name.to_string(),
2481 position,
2482 elements: members,
2483 names: names.clone(),
2484 type_name: None,
2487 }
2488}
2489
2490fn free_field_shape_node(name: &str, position: usize, expr: &IrExpr) -> crate::query::ShapeNode {
2493 use crate::query::{Cardinality, ShapeNode};
2494 if let IrExpr::Row {
2495 elements,
2496 names,
2497 is_free_object,
2498 } = expr
2499 {
2500 return row_shape_node(name, position, elements, names, *is_free_object);
2501 }
2502 if let IrExpr::Array(items) = expr
2503 && let Some(node) = tuple_array_shape_node(name, position, items)
2504 {
2505 return node;
2506 }
2507 if let IrExpr::ObjectSubquery(sel) = expr {
2508 let [IrRowSource::Bound { source, shape }] = sel.rows.as_slice() else {
2509 unreachable!("an object subquery is always schema-bound")
2510 };
2511 let (_, nodes) = build_shape(shape, &source.alias);
2512 return ShapeNode::Object {
2513 name: name.to_string(),
2514 type_name: Some(source.type_name.clone()),
2515 position,
2516 cardinality: Cardinality::Optional,
2517 pointers: prepend_type(nodes),
2518 has_implicit_id: shape_has_implicit_id(shape),
2519 };
2520 }
2521 if matches!(expr, IrExpr::ObjectPathSubquery(_) | IrExpr::ObjectPathUnion { .. }) {
2523 return expr_shape_node(name, position, expr);
2524 }
2525 if let IrExpr::ArrayFromSelect(source) = expr
2528 && matches!(
2529 source.as_ref(),
2530 IrArraySource::ObjectSelect(_)
2531 | IrArraySource::ObjectFunction(_)
2532 | IrArraySource::PathSelect(_)
2533 | IrArraySource::Group(_)
2534 )
2535 {
2536 return expr_shape_node(name, position, expr);
2537 }
2538 match enum_type_of_shape_expr(expr) {
2539 Some(qualified) => qualified.shape_node(name.to_string(), position),
2540 None => ShapeNode::Scalar {
2541 name: name.to_string(),
2542 position,
2543 },
2544 }
2545}
2546
2547fn expr_shape_node(name: &str, position: usize, expr: &IrExpr) -> crate::query::ShapeNode {
2554 use crate::query::{Cardinality, ShapeNode};
2555 match expr {
2556 IrExpr::Row {
2557 elements,
2558 names,
2559 is_free_object,
2560 } => row_shape_node(name, position, elements, names, *is_free_object),
2561 IrExpr::Array(items) if matches!(items.first(), Some(IrExpr::Row { .. })) => {
2562 tuple_array_shape_node(name, position, items).expect("matched as an array of rows just above")
2563 }
2564 IrExpr::ObjectPathUnion { branches, multi, .. } => {
2565 let first = branches.first().expect("a union has at least one branch");
2566 let IrPathResult::Object {
2567 alias,
2568 type_name,
2569 shape,
2570 } = &first.result
2571 else {
2572 unreachable!("an object path union's branches always land on objects")
2573 };
2574 let (_, pointer_nodes) = build_shape(shape, alias);
2575 let object = ShapeNode::Object {
2576 name: if *multi { String::new() } else { name.to_string() },
2577 type_name: Some(type_name.clone()),
2578 position: if *multi { 0 } else { position },
2579 cardinality: if *multi {
2580 Cardinality::Many
2581 } else {
2582 Cardinality::Optional
2583 },
2584 pointers: prepend_type(pointer_nodes),
2585 has_implicit_id: shape_has_implicit_id(shape),
2586 };
2587 if *multi {
2588 ShapeNode::Array {
2589 name: name.to_string(),
2590 position,
2591 element: Box::new(object),
2592 }
2593 } else {
2594 object
2595 }
2596 }
2597 IrExpr::ObjectPathSubquery(ps) => {
2598 let IrPathResult::Object {
2599 alias,
2600 type_name,
2601 shape,
2602 } = &ps.result
2603 else {
2604 unreachable!("an object path subquery always lands on an object")
2605 };
2606 let (_, pointer_nodes) = build_shape(shape, alias);
2607 ShapeNode::Object {
2608 name: name.to_string(),
2609 type_name: Some(type_name.clone()),
2610 position,
2611 cardinality: Cardinality::Optional,
2612 pointers: prepend_type(pointer_nodes),
2613 has_implicit_id: shape_has_implicit_id(shape),
2614 }
2615 }
2616 IrExpr::TypeCast(c) if is_composite_tuple_cast(c) => {
2619 let shape = c.tuple_shape.as_ref().expect("checked by `is_composite_tuple_cast`");
2620 let is_array = c.pg_type.ends_with("[]");
2623 let tuple = composite_tuple_shape(
2624 if is_array { "" } else { name },
2625 if is_array { 0 } else { position },
2626 shape.type_name.clone(),
2627 &shape.members,
2628 );
2629 match is_array {
2630 true => ShapeNode::Array {
2631 name: name.to_string(),
2632 position,
2633 element: Box::new(tuple),
2634 },
2635 false => tuple,
2636 }
2637 }
2638 IrExpr::TypeCast(c) if c.tuple_shape.is_some() => {
2639 let shape = c.tuple_shape.as_ref().unwrap();
2640 let is_array = c.pg_type.ends_with("[]");
2643 let tuple = ShapeNode::NamedTuple {
2644 name: if is_array { String::new() } else { name.to_string() },
2645 position: if is_array { 0 } else { position },
2646 type_name: shape.type_name.clone(),
2647 members: Some(shape.members.clone()),
2648 is_free_object: false,
2649 };
2650 if is_array {
2651 ShapeNode::Array {
2652 name: name.to_string(),
2653 position,
2654 element: Box::new(tuple),
2655 }
2656 } else {
2657 tuple
2658 }
2659 }
2660 IrExpr::TypeCast(c) if c.pg_type == "jsonb" => ShapeNode::Scalar {
2666 name: name.to_string(),
2667 position,
2668 },
2669 IrExpr::ArrayFromSelect(source) => match source.as_ref() {
2672 IrArraySource::ObjectFunction(fs) => {
2673 let (_, nodes) = build_shape(&fs.shape, &fs.alias);
2674 ShapeNode::Array {
2675 name: name.to_string(),
2676 position,
2677 element: Box::new(ShapeNode::Object {
2678 name: String::new(),
2679 type_name: Some(fs.type_name.clone()),
2680 position: 0,
2681 cardinality: Cardinality::Many,
2682 pointers: prepend_type(nodes),
2683 has_implicit_id: shape_has_implicit_id(&fs.shape),
2684 }),
2685 }
2686 }
2687 IrArraySource::Group(grp) => ShapeNode::Array {
2688 name: name.to_string(),
2689 position,
2690 element: Box::new(emit_group(grp).shape.root),
2691 },
2692 IrArraySource::ObjectSelect(sel) => {
2693 let [IrRowSource::Bound { source, shape }] = sel.rows.as_slice() else {
2694 unreachable!("IrArraySource::ObjectSelect is always schema-bound")
2695 };
2696 let (_, nodes) = build_shape(shape, &source.alias);
2697 ShapeNode::Array {
2698 name: name.to_string(),
2699 position,
2700 element: Box::new(ShapeNode::Object {
2701 name: String::new(),
2702 type_name: Some(source.type_name.clone()),
2703 position: 0,
2704 cardinality: Cardinality::Many,
2705 pointers: prepend_type(nodes),
2706 has_implicit_id: shape_has_implicit_id(shape),
2707 }),
2708 }
2709 }
2710 IrArraySource::PathSelect(ps) => match &ps.result {
2711 IrPathResult::Object {
2712 alias,
2713 type_name,
2714 shape,
2715 } => {
2716 let (_, pointer_nodes) = build_shape(shape, alias);
2717 ShapeNode::Array {
2718 name: name.to_string(),
2719 position,
2720 element: Box::new(ShapeNode::Object {
2721 name: String::new(),
2722 type_name: Some(type_name.clone()),
2723 position: 0,
2724 cardinality: Cardinality::Many,
2725 pointers: prepend_type(pointer_nodes),
2726 has_implicit_id: shape_has_implicit_id(shape),
2727 }),
2728 }
2729 }
2730 IrPathResult::Scalar(e, tuple_shape) if yields_array(e) => ShapeNode::Array {
2731 name: name.to_string(),
2732 position,
2733 element: Box::new(aggregated_array_element(e, tuple_shape.as_ref())),
2734 },
2735 IrPathResult::Scalar(..) => ShapeNode::Scalar {
2736 name: name.to_string(),
2737 position,
2738 },
2739 },
2740 _ => ShapeNode::Scalar {
2741 name: name.to_string(),
2742 position,
2743 },
2744 },
2745 e if is_raw_scalar(e) => ShapeNode::RawScalar,
2746 e => free_field_shape_node(name, position, e),
2747 }
2748}
2749
2750fn free_item_shape(item: &IrFreeExpr, ctes: &[IrCteDef]) -> crate::query::ShapeNode {
2751 use crate::query::{Cardinality, ShapeNode};
2752 match item {
2753 IrFreeExpr::Scalar(e) => match e {
2754 IrExpr::TypeCast(c) if c.pg_type == "jsonb" && c.tuple_shape.is_none() => ShapeNode::JsonScalar,
2758 _ => expr_shape_node("", 0, e),
2759 },
2760 IrFreeExpr::FreeObject(fields) => ShapeNode::Object {
2761 name: String::new(),
2762 type_name: None,
2763 position: 0,
2764 cardinality: Cardinality::Many,
2765 pointers: fields
2766 .iter()
2767 .enumerate()
2768 .map(|(i, (name, e))| free_field_shape_node(name, i, e))
2769 .collect(),
2770 has_implicit_id: false,
2771 },
2772 IrFreeExpr::Tuple(exprs) => ShapeNode::Tuple {
2773 name: String::new(),
2774 position: 0,
2775 elements: exprs
2776 .iter()
2777 .enumerate()
2778 .map(|(i, e)| free_field_shape_node("", i, e))
2779 .collect(),
2780 names: None,
2781 type_name: None,
2784 },
2785 IrFreeExpr::NamedTupleRow(fields) => ShapeNode::Tuple {
2786 name: String::new(),
2787 position: 0,
2788 elements: fields
2789 .iter()
2790 .enumerate()
2791 .map(|(i, (name, e))| free_field_shape_node(name, i, e))
2792 .collect(),
2793 names: Some(fields.iter().map(|(name, _)| name.clone()).collect()),
2794 type_name: None,
2795 },
2796 IrFreeExpr::AssertSet { .. } => ShapeNode::Scalar {
2797 name: String::new(),
2798 position: 0,
2799 },
2800 IrFreeExpr::CtePassthrough(name) => ctes
2806 .iter()
2807 .find(|c| &c.name == name)
2808 .and_then(|c| match &c.stmt {
2809 IrStmt::Select(sel) => match sel.rows.first() {
2810 Some(IrRowSource::Free(inner)) => Some(free_item_shape(inner, ctes)),
2811 _ => None,
2812 },
2813 IrStmt::PathSelect(ps) => Some(emit_path_select(ps).shape.root),
2818 _ => None,
2819 })
2820 .unwrap_or(ShapeNode::Scalar {
2821 name: String::new(),
2822 position: 0,
2823 }),
2824 }
2825}
2826
2827fn emit_path_joins(root: &IrSource, joins: &[IrPathJoin]) -> String {
2830 let mut parts = vec![format!("{} AS {}", source_ref(root), qi(&root.alias))];
2831 for join in joins {
2832 match join {
2833 IrPathJoin::Single {
2834 source_alias,
2835 fk_col,
2836 target,
2837 } => {
2838 parts.push(format!(
2839 "JOIN {} AS {} ON {}.{} = {}.\"id\"",
2840 source_ref(target),
2841 qi(&target.alias),
2842 qi(source_alias),
2843 qi(fk_col),
2844 qi(&target.alias),
2845 ));
2846 }
2847 IrPathJoin::Multi {
2848 source_alias,
2849 junction_alias,
2850 join,
2851 target,
2852 } => {
2853 match join {
2854 IrMultiLinkJoin::Standard { junction_table, module } => {
2855 parts.push(format!(
2856 "JOIN {} AS {} ON {}.\"source\" = {}.\"id\"",
2857 junction_ref(module, junction_table),
2858 qi(junction_alias),
2859 qi(junction_alias),
2860 qi(source_alias),
2861 ));
2862 parts.push(format!(
2863 "JOIN {} AS {} ON {}.\"id\" = {}.\"target\"",
2864 source_ref(target),
2865 qi(&target.alias),
2866 qi(&target.alias),
2867 qi(junction_alias),
2868 ));
2869 }
2870 IrMultiLinkJoin::Through {
2871 junction_table,
2872 module,
2873 source_col,
2874 target_col,
2875 } => {
2876 parts.push(format!(
2877 "JOIN {} AS {} ON {}.{} = {}.\"id\"",
2878 junction_ref(module, junction_table),
2879 qi(junction_alias),
2880 qi(junction_alias),
2881 qi(source_col),
2882 qi(source_alias),
2883 ));
2884 parts.push(format!(
2885 "JOIN {} AS {} ON {}.\"id\" = {}.{}",
2886 source_ref(target),
2887 qi(&target.alias),
2888 qi(&target.alias),
2889 qi(junction_alias),
2890 qi(target_col),
2891 ));
2892 }
2893 IrMultiLinkJoin::BacklinkFk { .. } | IrMultiLinkJoin::BacklinkJunction { .. } => {
2895 unreachable!("a forward multi-link path step never uses a backlink join variant")
2896 }
2897 }
2898 }
2899 IrPathJoin::BacklinkSingle {
2900 source_alias,
2901 fk_col,
2902 target,
2903 } => {
2904 parts.push(format!(
2905 "JOIN {} AS {} ON {}.{} = {}.\"id\"",
2906 source_ref(target),
2907 qi(&target.alias),
2908 qi(&target.alias),
2909 qi(fk_col),
2910 qi(source_alias),
2911 ));
2912 }
2913 IrPathJoin::Function {
2917 fn_module,
2918 fn_name,
2919 args,
2920 target,
2921 } => {
2922 let args_sql = args.iter().map(emit_expr).collect::<Vec<_>>().join(", ");
2923 parts.push(format!(
2924 "CROSS JOIN LATERAL {}.{}({}) AS {}",
2925 pg_schema(fn_module),
2926 qi(fn_name),
2927 args_sql,
2928 qi(&target.alias),
2929 ));
2930 }
2931 IrPathJoin::Lateral { inner, target } => {
2932 let projected = match &inner.result {
2933 IrPathResult::Object { alias, .. } => alias.clone(),
2934 IrPathResult::Scalar(..) => {
2935 unreachable!("a lateral step is only built for an object-valued computed")
2936 }
2937 };
2938 let mut body = format!(
2939 "SELECT {}.* FROM {}",
2940 qi(&projected),
2941 emit_path_joins(&inner.root, &inner.joins)
2942 );
2943 append_filter(&mut body, &inner.filter);
2944 append_order_by(&mut body, &inner.order_by);
2945 append_offset_limit(&mut body, &inner.offset, &inner.limit);
2946 parts.push(format!("JOIN LATERAL (\n{}\n) AS {} ON TRUE", body, qi(&target.alias)));
2947 }
2948 IrPathJoin::BacklinkMulti {
2949 source_alias,
2950 junction_alias,
2951 junction_table,
2952 module,
2953 owner_col,
2954 current_col,
2955 target,
2956 } => {
2957 parts.push(format!(
2958 "JOIN {} AS {} ON {}.{} = {}.\"id\"",
2959 junction_ref(module, junction_table),
2960 qi(junction_alias),
2961 qi(junction_alias),
2962 qi(current_col),
2963 qi(source_alias),
2964 ));
2965 parts.push(format!(
2966 "JOIN {} AS {} ON {}.\"id\" = {}.{}",
2967 source_ref(target),
2968 qi(&target.alias),
2969 qi(&target.alias),
2970 qi(junction_alias),
2971 qi(owner_col),
2972 ));
2973 }
2974 }
2975 }
2976 parts.join("\n")
2977}
2978
2979fn emit_array_source(src: &IrArraySource) -> String {
2981 match src {
2982 IrArraySource::Select(s) => {
2983 let [IrRowSource::Bound { source, shape }] = s.rows.as_slice() else {
2986 unreachable!("IrArraySource::Select is always schema-bound")
2987 };
2988 let scalar = match shape.first() {
2989 Some(IrShapePointer::Scalar(sf)) => format!("{}.{}", qi(&source.alias), qi(&sf.column)),
2990 _ => format!("{}.\"id\"", qi(&source.alias)),
2991 };
2992 let mut sql = format!("SELECT {} FROM {} AS {}", scalar, source_ref(source), qi(&source.alias));
2993 append_filter(&mut sql, &s.filter);
2994 format!("ARRAY({})", sql)
2995 }
2996 IrArraySource::ObjectFunction(fs) => {
2997 let (exprs, _) = build_shape(&fs.shape, &fs.alias);
2998 let mut parts = vec![sql_str(&fs.type_name) + "::text"];
2999 parts.extend(exprs);
3000 let args_sql = fs.fn_args.iter().map(emit_expr).collect::<Vec<_>>().join(", ");
3001 let mut sql = format!(
3002 "SELECT (\n {}\n) FROM {}.{}({}) AS {}",
3003 parts.join(",\n "),
3004 pg_schema(&fs.fn_module),
3005 qi(&fs.fn_name),
3006 args_sql,
3007 qi(&fs.alias),
3008 );
3009 append_filter(&mut sql, &fs.filter);
3010 append_order_by(&mut sql, &fs.order_by);
3011 append_offset_limit(&mut sql, &fs.offset, &fs.limit);
3012 format!("ARRAY({})", sql)
3013 }
3014 IrArraySource::ObjectSelect(s) => {
3015 let [IrRowSource::Bound { source, shape }] = s.rows.as_slice() else {
3016 unreachable!("IrArraySource::ObjectSelect is always schema-bound")
3017 };
3018 let (exprs, _) = build_shape(shape, &source.alias);
3019 let mut parts = vec![source_type_disc(source)];
3020 parts.extend(exprs);
3021 let mut sql = format!(
3022 "SELECT (\n {}\n) FROM {} AS {}",
3023 parts.join(",\n "),
3024 source_ref(source),
3025 qi(&source.alias)
3026 );
3027 append_filter(&mut sql, &s.filter);
3028 append_order_by(&mut sql, &s.order_by);
3029 append_offset_limit(&mut sql, &s.offset, &s.limit);
3030 format!("ARRAY({})", sql)
3031 }
3032 IrArraySource::Group(grp) => format!("ARRAY({})", emit_group(grp).sql),
3033 IrArraySource::StmtColumn { stmt, column } => format!(
3034 "ARRAY(SELECT {} FROM (\n{}\n) AS \"_rows\")",
3035 qi(column),
3036 emit_dml_as_cte_source(stmt),
3037 ),
3038 IrArraySource::PathSelect(ps) => {
3039 let scalar = match &ps.result {
3040 IrPathResult::Scalar(e, _) if yields_array(e) => {
3048 let cast = match e {
3052 IrExpr::ColumnRef { pg_type, .. } => {
3053 QualifiedPgType::of(pg_type).map(|qualified| qualified.text_cast())
3054 }
3055 _ => None,
3056 };
3057 format!("ROW({}{})", emit_expr(e), cast.unwrap_or(""))
3058 }
3059 IrPathResult::Scalar(e, _) => emit_expr(e),
3060 IrPathResult::Object {
3064 alias,
3065 type_name,
3066 shape,
3067 } => {
3068 let (pointer_exprs, _) = build_shape(shape, alias);
3069 let mut parts = vec![result_type_disc(ps, alias, type_name)];
3070 parts.extend(pointer_exprs);
3071 format!("(\n {}\n)", parts.join(",\n "))
3072 }
3073 };
3074 let from_sql = emit_path_joins(&ps.root, &ps.joins);
3075 let mut sql = format!(
3076 "SELECT {}{} FROM {}",
3077 if ps.distinct { "DISTINCT " } else { "" },
3078 scalar,
3079 from_sql
3080 );
3081 append_filter(&mut sql, &ps.filter);
3082 append_order_by(&mut sql, &ps.order_by);
3083 append_offset_limit(&mut sql, &ps.offset, &ps.limit);
3084 format!("ARRAY({})", sql)
3085 }
3086 IrArraySource::RawExpr {
3087 source,
3088 poly_implementors,
3089 poly_columns,
3090 expr,
3091 } => {
3092 let from_sql = if !poly_implementors.is_empty() {
3093 format!(
3094 "(\n{}\n) AS {}",
3095 emit_poly_union(poly_implementors, poly_columns),
3096 qi(&source.alias)
3097 )
3098 } else {
3099 format!("{} AS {}", source_ref(source), qi(&source.alias))
3100 };
3101 format!("ARRAY(SELECT {} FROM {})", emit_expr(expr), from_sql)
3102 }
3103 }
3104}
3105
3106fn emit_key_expr(expr: &IrExpr) -> String {
3109 if let IrExpr::ColumnRef { alias, column, pg_type } = expr
3110 && let Some(qualified) = QualifiedPgType::of(pg_type)
3111 {
3112 let col_ref = if alias.is_empty() {
3113 qi(column)
3114 } else {
3115 format!("{}.{}", qi(alias), qi(column))
3116 };
3117 return format!("{}{}", col_ref, qualified.text_cast());
3118 }
3119 emit_expr(expr)
3120}
3121
3122fn group_rows(grp: &IrGroup) -> (String, Option<String>) {
3126 let alias = &grp.source.alias;
3127 if grp.limit.is_none() && grp.offset.is_none() {
3128 return (
3129 format!("{} AS {}", source_ref(&grp.source), qi(alias)),
3130 grp.filter.as_ref().map(emit_expr),
3131 );
3132 }
3133 let ranked = format!(
3134 "(SELECT {}.*, row_number() OVER (PARTITION BY {}{}) AS \"__rk\"\n FROM {} AS {}{}) AS {}",
3135 qi(alias),
3136 group_by_sql(grp),
3137 group_element_order(grp),
3138 source_ref(&grp.source),
3139 qi(alias),
3140 grp.filter
3141 .as_ref()
3142 .map(|f| format!("\n WHERE {}", emit_expr(f)))
3143 .unwrap_or_default(),
3144 qi(alias),
3145 );
3146 let lower = grp.offset.as_ref().map(emit_expr).unwrap_or_else(|| "0".to_string());
3147 let mut conds = vec![format!("\"__rk\" > {}", lower)];
3148 if let Some(l) = &grp.limit {
3149 conds.push(format!("\"__rk\" <= {} + {}", lower, emit_expr(l)));
3150 }
3151 (ranked, Some(conds.join(" AND ")))
3152}
3153
3154fn group_by_sql(grp: &IrGroup) -> String {
3155 grp.keys
3156 .iter()
3157 .map(|(_, key_expr)| emit_expr(key_expr))
3158 .collect::<Vec<_>>()
3159 .join(", ")
3160}
3161
3162fn group_element_order(grp: &IrGroup) -> String {
3163 if grp.order_by.is_empty() {
3164 return String::new();
3165 }
3166 let s: Vec<_> = grp.order_by.iter().map(emit_sort_clause).collect();
3167 format!(" ORDER BY {}", s.join(", "))
3168}
3169
3170fn emit_group_elements_source(grp: &IrGroup) -> String {
3173 let (from_sql, cond) = group_rows(grp);
3174 let mut sql = format!("SELECT {}.*\n FROM {}", qi(&grp.source.alias), from_sql);
3175 if let Some(cond) = cond {
3176 sql.push_str(&format!("\n WHERE {}", cond));
3177 }
3178 sql
3179}
3180
3181fn emit_group(grp: &IrGroup) -> SqlOutput {
3182 match &grp.output {
3183 IrGroupOutput::Groups => emit_group_rows(grp),
3184 IrGroupOutput::Projection(projection) => emit_group_projection(
3185 grp,
3186 &projection.pointers,
3187 &projection.order_by,
3188 &projection.offset,
3189 &projection.limit,
3190 ),
3191 IrGroupOutput::Elements => emit_group_elements(grp),
3192 }
3193}
3194
3195fn emit_group_projection(
3196 grp: &IrGroup,
3197 pointers: &[IrShapePointer],
3198 order_by: &[IrSort],
3199 offset: &Option<IrExpr>,
3200 limit: &Option<IrExpr>,
3201) -> SqlOutput {
3202 let (exprs, nodes) = build_shape(pointers, &grp.source.alias);
3203 let mut parts = vec!["NULL::text".to_string()];
3204 parts.extend(exprs);
3205 let (from_sql, cond) = group_rows(grp);
3206 let mut sql = format!(
3207 "SELECT (\n {}\n) AS \"result\"\nFROM {}",
3208 parts.join(",\n "),
3209 from_sql
3210 );
3211 if let Some(cond) = cond {
3212 sql.push_str(&format!("\nWHERE {}", cond));
3213 }
3214 sql.push_str(&format!("\nGROUP BY {}", group_by_sql(grp)));
3215 if !order_by.is_empty() {
3216 let sorts: Vec<_> = order_by.iter().map(emit_sort_clause).collect();
3217 sql.push_str(&format!("\nORDER BY {}", sorts.join(", ")));
3218 }
3219 append_offset_limit(&mut sql, offset, limit);
3220 SqlOutput {
3221 sql,
3222 shape: ShapeDescriptor {
3223 root: ShapeNode::Object {
3224 name: String::new(),
3225 type_name: None,
3226 position: 0,
3227 cardinality: Cardinality::Many,
3228 pointers: nodes,
3229 has_implicit_id: false,
3230 },
3231 },
3232 inference_plan: None,
3233 }
3234}
3235
3236fn emit_group_elements(grp: &IrGroup) -> SqlOutput {
3237 let (exprs, nodes) = build_shape(&grp.shape, &grp.source.alias);
3238 let mut parts = vec![source_type_disc(&grp.source)];
3239 parts.extend(exprs);
3240 let (from_sql, cond) = group_rows(grp);
3241 let mut sql = format!(
3242 "SELECT (\n {}\n) AS \"result\"\nFROM {}",
3243 parts.join(",\n "),
3244 from_sql
3245 );
3246 if let Some(cond) = cond {
3247 sql.push_str(&format!("\nWHERE {}", cond));
3248 }
3249 SqlOutput {
3250 sql,
3251 shape: ShapeDescriptor {
3252 root: ShapeNode::Object {
3253 name: String::new(),
3254 type_name: Some(grp.source.type_name.clone()),
3255 position: 0,
3256 cardinality: Cardinality::Many,
3257 pointers: prepend_type(nodes),
3258 has_implicit_id: shape_has_implicit_id(&grp.shape),
3259 },
3260 },
3261 inference_plan: None,
3262 }
3263}
3264
3265fn emit_group_rows(grp: &IrGroup) -> SqlOutput {
3266 let alias = &grp.source.alias;
3267 let (shape_exprs, shape_nodes) = build_shape(&grp.shape, alias);
3268
3269 let mut elem_row_parts = vec![source_type_disc(&grp.source)];
3271 elem_row_parts.extend(shape_exprs);
3272 let elem_row = elem_row_parts.join(",\n ");
3273
3274 let n_keys = grp.keys.len();
3276 let grouping_pos = n_keys + 1;
3277 let elements_pos = n_keys + 2;
3278
3279 let mut key_exprs_sql: Vec<String> = vec![];
3281 let mut key_nodes: Vec<ShapeNode> = vec![];
3282 for (i, (key_name, key_expr)) in grp.keys.iter().enumerate() {
3283 let pos = i + 1;
3284 if let IrExpr::ColumnRef { pg_type, .. } = key_expr
3285 && let Some(qualified) = QualifiedPgType::of(pg_type)
3286 {
3287 key_exprs_sql.push(emit_key_expr(key_expr));
3288 key_nodes.push(qualified.shape_node(key_name.clone(), pos));
3289 continue;
3290 }
3291 key_exprs_sql.push(emit_expr(key_expr));
3292 key_nodes.push(ShapeNode::Scalar {
3293 name: key_name.clone(),
3294 position: pos,
3295 });
3296 }
3297
3298 let mut outer_parts = vec!["NULL::text".to_string()];
3300 outer_parts.extend(key_exprs_sql.clone());
3301 let key_names_sql = grp
3302 .keys
3303 .iter()
3304 .map(|(name, _)| format!("'{}'", name))
3305 .collect::<Vec<_>>()
3306 .join(", ");
3307 outer_parts.push(format!("ARRAY[{}]::text[]", key_names_sql));
3308 outer_parts.push(format!(
3309 "array_agg(ROW(\n {}\n )::record{})",
3310 elem_row,
3311 group_element_order(grp)
3312 ));
3313
3314 let outer_tuple = outer_parts.join(",\n ");
3315 let (from_sql, cond) = group_rows(grp);
3316 let mut sql = format!("SELECT (\n {}\n) AS \"result\"\nFROM {}", outer_tuple, from_sql,);
3317 if let Some(cond) = cond {
3318 sql.push_str(&format!("\nWHERE {}", cond));
3319 }
3320 sql.push_str(&format!("\nGROUP BY {}", group_by_sql(grp)));
3321
3322 let element_node = ShapeNode::Object {
3324 name: String::new(),
3325 type_name: Some(grp.source.type_name.clone()),
3326 position: 0,
3327 cardinality: Cardinality::Many,
3328 pointers: prepend_type(shape_nodes),
3329 has_implicit_id: shape_has_implicit_id(&grp.shape),
3330 };
3331
3332 let root = ShapeNode::Group {
3333 key_nodes,
3334 grouping_position: grouping_pos,
3335 elements_position: elements_pos,
3336 element: Box::new(element_node),
3337 };
3338
3339 SqlOutput {
3340 sql,
3341 shape: ShapeDescriptor { root },
3342 inference_plan: None,
3343 }
3344}
3345
3346fn emit_poly_union_type_only(implementors: &[IrPolyImplementor]) -> String {
3347 implementors
3348 .iter()
3349 .map(|imp| {
3350 format!(
3351 " SELECT {}::text AS \"__type__\" FROM {}",
3352 sql_str(&imp.type_name),
3353 qn(&imp.module, &imp.table),
3354 )
3355 })
3356 .collect::<Vec<_>>()
3357 .join("\n UNION ALL\n")
3358}
3359
3360fn emit_path_select(sel: &IrPathSelect) -> SqlOutput {
3361 let distinct = if sel.distinct { "DISTINCT " } else { "" };
3362 let from_sql = if !sel.poly_implementors.is_empty() {
3363 format!(
3364 "(\n{}\n) AS {}",
3365 emit_poly_union_type_only(&sel.poly_implementors),
3366 qi(&sel.root.alias),
3367 )
3368 } else {
3369 emit_path_joins(&sel.root, &sel.joins)
3370 };
3371
3372 let (result_expr, shape_root) = match &sel.result {
3373 IrPathResult::Scalar(ir_expr, tuple_shape) => {
3374 let is_nt = matches!(ir_expr, IrExpr::JsonbField { .. })
3377 || matches!(ir_expr, IrExpr::JsonbIndex { .. })
3378 || matches!(ir_expr, IrExpr::ColumnRef { pg_type, .. } if pg_type.starts_with("__nt__:"))
3379 || tuple_shape.is_some();
3380 let array_of_tuples = matches!(ir_expr, IrExpr::ColumnRef { pg_type, .. } if pg_type.ends_with("[]"));
3384 if let Some(shape) = tuple_shape.as_ref().filter(|_| array_of_tuples) {
3385 let node = ShapeNode::Array {
3386 name: String::new(),
3387 position: 0,
3388 element: Box::new(composite_tuple_shape("", 0, shape.type_name.clone(), &shape.members)),
3389 };
3390 (format!("ROW({}) AS result", emit_expr(ir_expr)), node)
3391 } else if is_nt {
3392 let expr_sql = format!("{} AS result", emit_expr(ir_expr));
3393 let shape = if matches!(ir_expr, IrExpr::JsonbField { .. } | IrExpr::JsonbIndex { .. }) {
3394 ShapeNode::RawScalar
3395 } else if let Some(shape) = tuple_shape {
3396 composite_tuple_shape("", 0, shape.type_name.clone(), &shape.members)
3403 } else {
3404 let type_name = match ir_expr {
3405 IrExpr::ColumnRef { pg_type, .. } => pg_type.strip_prefix("__nt__:").map(|s| s.to_string()),
3406 _ => None,
3407 };
3408 ShapeNode::NamedTuple {
3409 name: String::new(),
3410 position: 0,
3411 type_name,
3412 members: None,
3413 is_free_object: false,
3414 }
3415 };
3416 (expr_sql, shape)
3417 } else {
3418 if let IrExpr::ColumnRef { pg_type, .. } = ir_expr {
3421 if let Some(qualified) = QualifiedPgType::of(pg_type) {
3422 let expr = format!("ROW({}{}) AS result", emit_expr(ir_expr), qualified.text_cast());
3423 let shape = qualified.shape_node(String::new(), 0);
3424 (expr, shape)
3425 } else {
3426 let expr = format!("ROW({}) AS result", emit_expr(ir_expr));
3427 (
3428 expr,
3429 ShapeNode::Scalar {
3430 name: String::new(),
3431 position: 0,
3432 },
3433 )
3434 }
3435 } else {
3436 let expr = format!("ROW({}) AS result", emit_expr(ir_expr));
3437 (
3438 expr,
3439 ShapeNode::Scalar {
3440 name: String::new(),
3441 position: 0,
3442 },
3443 )
3444 }
3445 }
3446 }
3447 IrPathResult::Object {
3448 alias,
3449 type_name,
3450 shape,
3451 } => {
3452 let (pointer_exprs, pointer_nodes) = build_shape(shape, alias);
3453 let mut parts = vec![result_type_disc(sel, alias, type_name)];
3454 parts.extend(pointer_exprs);
3455 let expr = format!("(\n {}\n) AS result", parts.join(",\n "));
3456 let shape_root = ShapeNode::Object {
3457 name: String::new(),
3458 type_name: Some(type_name.clone()),
3459 position: 0,
3460 cardinality: Cardinality::Many,
3461 pointers: prepend_type(pointer_nodes),
3462 has_implicit_id: shape_has_implicit_id(shape),
3463 };
3464 (expr, shape_root)
3465 }
3466 };
3467
3468 let mut sql = format!("SELECT {}{}\nFROM {}", distinct, result_expr, from_sql);
3469 append_filter(&mut sql, &sel.filter);
3470 append_order_by(&mut sql, &sel.order_by);
3471 append_offset_limit(&mut sql, &sel.offset, &sel.limit);
3472
3473 SqlOutput {
3474 sql,
3475 shape: ShapeDescriptor { root: shape_root },
3476 inference_plan: None,
3477 }
3478}
3479
3480fn yields_jsonb(expr: &IrExpr) -> bool {
3487 match expr {
3488 IrExpr::JsonbField { .. } | IrExpr::JsonbIndex { .. } | IrExpr::NamedTuple { .. } => true,
3489 IrExpr::TypeCast(c) => c.pg_type == "jsonb",
3490 IrExpr::ColumnRef { pg_type, .. } | IrExpr::FnParam { pg_type, .. } => pg_type == "jsonb",
3491 IrExpr::CteRef { pg_type, .. } | IrExpr::ForVar { pg_type, .. } => pg_type.as_deref() == Some("jsonb"),
3492 IrExpr::FunctionCall(f) if f.return_pg_type.is_some() => f.return_pg_type.as_deref() == Some("jsonb"),
3496 IrExpr::FunctionCall(f) if f.schema.is_none() => {
3497 let mut overloads = crate::stdlib::registry().iter().filter(|d| d.name == f.name).peekable();
3498 overloads.peek().is_some() && overloads.all(|d| matches!(d.return_type, crate::stdlib::PylonType::Json))
3499 }
3500 _ => false,
3501 }
3502}
3503
3504fn yields_array(expr: &IrExpr) -> bool {
3507 match expr {
3508 IrExpr::Array(_) | IrExpr::ArrayFromSelect(_) => true,
3509 IrExpr::ColumnRef { pg_type, .. } | IrExpr::FnParam { pg_type, .. } => pg_type.ends_with("[]"),
3510 IrExpr::CteRef { pg_type, .. } | IrExpr::ForVar { pg_type, .. } => {
3511 pg_type.as_deref().is_some_and(|ty| ty.ends_with("[]"))
3512 }
3513 IrExpr::TypeCast(cast) => cast.pg_type.ends_with("[]"),
3514 _ => false,
3515 }
3516}
3517
3518fn aggregated_array_element(expr: &IrExpr, tuple_shape: Option<&crate::ir::TupleCastShape>) -> ShapeNode {
3525 if let Some(shape) = tuple_shape {
3526 return ShapeNode::Array {
3527 name: String::new(),
3528 position: 0,
3529 element: Box::new(composite_tuple_shape("", 0, shape.type_name.clone(), &shape.members)),
3530 };
3531 }
3532 if let IrExpr::ColumnRef { pg_type, .. } = expr
3533 && let Some(qualified) = QualifiedPgType::of(pg_type)
3534 {
3535 return qualified.shape_node(String::new(), 0);
3536 }
3537 ShapeNode::Scalar {
3538 name: String::new(),
3539 position: 0,
3540 }
3541}
3542
3543fn emit_for_iterator(it: &IrForIterator, iter_alias: &str) -> (String, String) {
3544 match it {
3545 IrForIterator::Values { exprs, pg_type } => {
3546 let rows: Vec<String> = exprs
3547 .iter()
3548 .map(|e| format!("({}::{})", emit_expr(e), pg_type))
3549 .collect();
3550 (
3551 format!("(VALUES {}) AS {}(\"v\")", rows.join(", "), qi(iter_alias)),
3552 format!("{}(\"v\") AS (VALUES {})", qi(iter_alias), rows.join(", ")),
3553 )
3554 }
3555 IrForIterator::Query { stmt, scalar } => {
3556 let inner = emit_dml_as_cte_source(stmt);
3557 let body = if *scalar {
3561 inner
3562 } else {
3563 format!(" SELECT \"id\" AS v FROM (\n{}\n ) AS _src", inner)
3564 };
3565 (
3566 format!("(\n{}\n) AS {}", body, qi(iter_alias)),
3567 format!("{} AS (\n{}\n)", qi(iter_alias), body),
3568 )
3569 }
3570 IrForIterator::SetReturning { expr, pg_type } => {
3571 let body = format!(" SELECT {}::{} AS v", emit_expr(expr), pg_type);
3572 (
3573 format!("(\n{}\n) AS {}", body, qi(iter_alias)),
3574 format!("{} AS (\n{}\n)", qi(iter_alias), body),
3575 )
3576 }
3577 }
3578}
3579
3580fn emit_nested_for_iterator(it: &IrForIterator, iter_alias: &str, outer_alias: &str) -> String {
3585 const OUTER: &str = "_outer";
3586 match it {
3587 IrForIterator::Values { exprs, pg_type } => {
3588 let rows: Vec<String> = exprs
3589 .iter()
3590 .map(|e| format!("({}::{})", emit_expr(e), pg_type))
3591 .collect();
3592 format!(
3593 "{} AS (\nSELECT {}.\"v\" AS {}, \"_vals\".\"v\" AS v\nFROM {}, (VALUES {}) AS \"_vals\"(\"v\")\n)",
3594 qi(iter_alias),
3595 qi(outer_alias),
3596 qi(OUTER),
3597 qi(outer_alias),
3598 rows.join(", "),
3599 )
3600 }
3601 IrForIterator::Query { stmt, scalar } => {
3602 let inner = emit_dml_as_cte_source(stmt);
3603 let projected = if *scalar { "\"_src\".\"v\"" } else { "\"_src\".\"id\"" };
3604 format!(
3605 "{} AS (\nSELECT {}.\"v\" AS {}, {} AS v\nFROM {}\nCROSS JOIN LATERAL (\n{}\n) AS \"_src\"\n)",
3606 qi(iter_alias),
3607 qi(outer_alias),
3608 qi(OUTER),
3609 projected,
3610 qi(outer_alias),
3611 inner,
3612 )
3613 }
3614 IrForIterator::SetReturning { expr, pg_type } => format!(
3615 "{} AS (\nSELECT {}.\"v\" AS {}, \"_vals\".\"v\" AS v\nFROM {}, LATERAL (SELECT {}::{} AS v) AS \"_vals\"\n)",
3616 qi(iter_alias),
3617 qi(outer_alias),
3618 qi(OUTER),
3619 qi(outer_alias),
3620 emit_expr(expr),
3621 pg_type,
3622 ),
3623 }
3624}
3625
3626fn nested_for_from(inner_alias: &str, outer_alias: &str) -> String {
3629 format!(
3630 "{} JOIN {} ON {}.\"v\" = {}.\"_outer\"",
3631 qi(inner_alias),
3632 qi(outer_alias),
3633 qi(outer_alias),
3634 qi(inner_alias),
3635 )
3636}
3637
3638fn uncorrelated_body_ctes(f: &IrFor) -> Vec<IrCteDef> {
3652 f.body_ctes
3653 .iter()
3654 .map(|cte| IrCteDef {
3655 correlated_to: None,
3656 ..cte.clone()
3657 })
3658 .collect()
3659}
3660
3661fn prefix_body_ctes(sql: String, body_ctes: &[IrCteDef]) -> String {
3662 if body_ctes.is_empty() {
3663 return sql;
3664 }
3665 merge_into_existing_with(&sql, &emit_user_cte_parts(body_ctes))
3666 .unwrap_or_else(|| format!("{}{}", emit_cte_prefix(body_ctes), sql))
3667}
3668
3669fn emit_for_stmt(f: &IrFor, user_ctes: &[IrCteDef]) -> SqlOutput {
3670 let iter_alias = format!("_for_{}", f.var_name);
3671
3672 if matches!(&f.iterator, IrForIterator::Values { exprs, .. } if exprs.is_empty()) {
3673 let empty = SqlOutput {
3674 sql: "SELECT NULL AS result WHERE FALSE".to_string(),
3675 shape: ShapeDescriptor {
3676 root: ShapeNode::Scalar {
3677 name: String::new(),
3678 position: 0,
3679 },
3680 },
3681 inference_plan: None,
3682 };
3683 return empty;
3684 }
3685
3686 let (values_from, iter_cte) = emit_for_iterator(&f.iterator, &iter_alias);
3687
3688 match f.body.as_ref() {
3689 IrStmt::Insert(ins) => emit_for_insert(ins, &iter_alias, &iter_cte, user_ctes, &f.body_ctes),
3690 IrStmt::Update(upd) => emit_for_update(upd, &iter_alias, &iter_cte, user_ctes, &f.body_ctes),
3691 body => {
3692 let body_out = match body {
3693 IrStmt::Select(sel) => emit_select_stmt(sel, user_ctes),
3694 IrStmt::PathSelect(sel) => emit_path_select(sel),
3695 other => unreachable!("for-loop body should have been rejected at compile time: {other:?}"),
3698 };
3699 let indent_body = prefix_body_ctes(body_out.sql, &uncorrelated_body_ctes(f)).replace('\n', "\n ");
3700 let cte_prefix = if !user_ctes.is_empty() {
3701 emit_cte_prefix(user_ctes)
3702 } else {
3703 String::new()
3704 };
3705 let sql = format!(
3706 "{}SELECT \"_body\".result\nFROM {}\nCROSS JOIN LATERAL (\n {}\n) AS \"_body\"",
3707 cte_prefix, values_from, indent_body,
3708 );
3709 SqlOutput {
3710 sql,
3711 shape: body_out.shape,
3712 inference_plan: None,
3713 }
3714 }
3715 }
3716}
3717
3718fn emit_for_update(
3723 upd: &IrUpdate,
3724 iter_alias: &str,
3725 iter_cte: &str,
3726 user_ctes: &[IrCteDef],
3727 body_ctes: &[IrCteDef],
3728) -> SqlOutput {
3729 let mut cte_parts: Vec<String> = emit_user_cte_parts(user_ctes);
3730 cte_parts.push(iter_cte.to_string());
3733 cte_parts.extend(emit_user_cte_parts(body_ctes));
3734
3735 let alias = &upd.target.alias;
3736 if upd.assignments.is_empty() && upd.rewrites.is_empty() && !upd.multi_link_appends.is_empty() {
3740 const ITER_COL: &str = "_iter";
3741 let ids_name = "_ids";
3742 let generated: Vec<Option<&IrInsert>> = upd
3746 .multi_link_appends
3747 .iter()
3748 .map(|a| per_iteration_insert(a, &upd.nested_ctes))
3749 .collect();
3750 let new_cols: String = generated
3751 .iter()
3752 .enumerate()
3753 .filter_map(|(i, ins)| {
3754 ins.map(|ins| {
3755 format!(
3756 ", {} AS {}",
3757 ins.id_default_sql.as_deref().unwrap_or("uuidv7()"),
3758 qi(&format!("_new_{i}"))
3759 )
3760 })
3761 })
3762 .collect();
3763 let mut ids_sql = format!(
3764 "\"{}\" AS (\nSELECT {}.*, {}.\"v\" AS {}{} FROM {} AS {}, {}",
3765 ids_name,
3766 qi(alias),
3767 qi(iter_alias),
3768 qi(ITER_COL),
3769 new_cols,
3770 target_ref(&upd.target),
3771 qi(alias),
3772 qi(iter_alias),
3773 );
3774 append_filter(&mut ids_sql, &upd.filter);
3775 ids_sql.push_str("\n)");
3776 cte_parts.push(ids_sql);
3777 for (i, append) in upd.multi_link_appends.iter().enumerate() {
3778 match generated[i] {
3779 Some(ins) => {
3780 cte_parts.push(emit_for_nested_insert_cte(
3781 ins,
3782 ids_name,
3783 iter_alias,
3784 &format!("_ml_ins_{i}"),
3785 &format!("_new_{i}"),
3786 ));
3787 cte_parts.push(emit_for_ml_append_cte(
3788 append,
3789 ids_name,
3790 &format!("_ml_add_{i}"),
3791 &format!("_new_{i}"),
3792 ));
3793 }
3794 None => cte_parts.push(emit_for_ml_append_cte(
3795 append,
3796 ids_name,
3797 &format!("_ml_add_{i}"),
3798 ITER_COL,
3799 )),
3800 }
3801 }
3802 let (pointer_exprs, shape_nodes) = build_shape(&upd.returning, ids_name);
3803 let mut parts = vec![type_disc(&upd.target.type_name)];
3804 parts.extend(pointer_exprs);
3805 let sql = format!(
3806 "WITH {}\nSELECT (\n {}\n) AS result\nFROM \"{}\"",
3807 cte_parts.join(",\n"),
3808 parts.join(",\n "),
3809 ids_name,
3810 );
3811 return SqlOutput {
3812 sql,
3813 shape: crate::query::ShapeDescriptor {
3814 root: ShapeNode::Object {
3815 name: String::new(),
3816 type_name: Some(upd.target.type_name.clone()),
3817 position: 0,
3818 cardinality: Cardinality::Many,
3819 pointers: prepend_type(shape_nodes),
3820 has_implicit_id: shape_has_implicit_id(&upd.returning),
3821 },
3822 },
3823 inference_plan: None,
3824 };
3825 }
3826 let sets: Vec<String> = upd
3827 .assignments
3828 .iter()
3829 .map(|(col, expr)| format!("{} = {}", qi(col), emit_expr(expr)))
3830 .chain(
3831 upd.rewrites
3832 .iter()
3833 .map(|rw| format!("{} = {}", qi(&rw.column), emit_expr(&rw.expr))),
3834 )
3835 .collect();
3836 if !upd.poly_implementors.is_empty() {
3837 return emit_for_poly_update(upd, iter_alias, &sets, cte_parts);
3838 }
3839 let mut sql = format!(
3840 "WITH {}\nUPDATE {} AS {}\nSET {}\nFROM {}",
3841 cte_parts.join(",\n"),
3842 target_ref(&upd.target),
3843 qi(alias),
3844 sets.join(", "),
3845 qi(iter_alias),
3846 );
3847 append_filter(&mut sql, &upd.filter);
3848 let mut parts = vec![type_disc(&upd.target.type_name)];
3849 let (pointer_exprs, shape_nodes) = build_shape(&upd.returning, alias);
3850 parts.extend(pointer_exprs);
3851 sql.push_str(&format!("\nRETURNING (\n {}\n) AS result", parts.join(",\n ")));
3852 SqlOutput {
3853 sql,
3854 shape: crate::query::ShapeDescriptor {
3855 root: ShapeNode::Object {
3856 name: String::new(),
3857 type_name: Some(upd.target.type_name.clone()),
3858 position: 0,
3859 cardinality: Cardinality::Many,
3860 pointers: prepend_type(shape_nodes),
3861 has_implicit_id: shape_has_implicit_id(&upd.returning),
3862 },
3863 },
3864 inference_plan: None,
3865 }
3866}
3867
3868const OUTER_KEY: &str = "_outer";
3873
3874fn binding_outer_key(cte_name: &str) -> String {
3879 format!("_outer__{cte_name}")
3880}
3881
3882fn emit_iteration_rows_cte(ins: &IrInsert, cte_name: &str, iter_alias: &str, outer_source: &str) -> String {
3887 let rewrite_cols: std::collections::HashSet<&str> = ins.rewrites.iter().map(|r| r.column.as_str()).collect();
3888 let mut selected = vec![
3889 format!(
3890 "{} AS {}",
3891 ins.id_default_sql.as_deref().unwrap_or("uuidv7()"),
3892 qi("id")
3893 ),
3894 format!("{} AS {}", outer_source, qi(OUTER_KEY)),
3895 ];
3896 selected.extend(
3897 ins.assignments
3898 .iter()
3899 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
3900 .map(|(c, e)| format!("{} AS {}", emit_expr(e), qi(c)))
3901 .chain(
3902 ins.rewrites
3903 .iter()
3904 .map(|r| format!("{} AS {}", emit_expr(&r.expr), qi(&r.column))),
3905 ),
3906 );
3907 let mut sql = format!(
3908 "\"{}\" AS (\nSELECT {}\nFROM {}",
3909 cte_name,
3910 selected.join(", "),
3911 qi(iter_alias),
3912 );
3913 append_insert_guard(&mut sql, ins);
3914 sql.push_str("\n)");
3915 sql
3916}
3917
3918fn emit_insert_from_rows_cte(ins: &IrInsert, cte_name: &str, rows_name: &str) -> String {
3920 let rewrite_cols: std::collections::HashSet<&str> = ins.rewrites.iter().map(|r| r.column.as_str()).collect();
3921 let cols: Vec<String> = std::iter::once(qi("id"))
3922 .chain(
3923 ins.assignments
3924 .iter()
3925 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
3926 .map(|(c, _)| qi(c)),
3927 )
3928 .chain(ins.rewrites.iter().map(|r| qi(&r.column)))
3929 .collect();
3930 let mut sql = format!(
3931 "INSERT INTO {} ({})\nSELECT {} FROM \"{}\"",
3932 target_ref(&ins.target),
3933 cols.join(", "),
3934 cols.join(", "),
3935 rows_name,
3936 );
3937 if let Some(conflict) = &ins.unless_conflict {
3938 emit_conflict(&mut sql, conflict);
3939 }
3940 sql.push_str("\nRETURNING *");
3941 format!("\"{}\" AS (\n{}\n)", cte_name, sql)
3942}
3943
3944fn emit_correlated_ml_append_cte(
3948 mutation: &IrMultiLinkMutation,
3949 rows_name: &str,
3950 targets_name: &str,
3951 cte_name: &str,
3952) -> String {
3953 format!(
3954 "\"{}\" AS (\nINSERT INTO {} ({}, {})\nSELECT \"_src\".{}, \"_tgt\".{} FROM \"{}\" AS \"_src\" JOIN \"{}\" AS \"_tgt\" ON \"_tgt\".{} = \"_src\".{}\nON CONFLICT DO NOTHING\nRETURNING {}, {}\n)",
3955 cte_name,
3956 qn(&mutation.module, &mutation.junction_table),
3957 qi(&mutation.source_col),
3958 qi(&mutation.target_col),
3959 qi("id"),
3960 qi("id"),
3961 rows_name,
3962 targets_name,
3963 qi(OUTER_KEY),
3964 qi(OUTER_KEY),
3965 qi(&mutation.source_col),
3966 qi(&mutation.target_col),
3967 )
3968}
3969
3970fn correlated_append_indices(appends: &[IrMultiLinkMutation], defs: &[&[IrCteDef]]) -> Vec<usize> {
3973 appends
3974 .iter()
3975 .enumerate()
3976 .filter(|(_, append)| match &append.values.source {
3977 IrMultiLinkValueSource::CteRef(target) => defs
3978 .iter()
3979 .flat_map(|group| group.iter())
3980 .any(|c| &c.name == target && c.correlated_to.is_some()),
3981 _ => false,
3982 })
3983 .map(|(i, _)| i)
3984 .collect()
3985}
3986
3987fn emit_for_poly_update(upd: &IrUpdate, iter_alias: &str, sets: &[String], mut cte_parts: Vec<String>) -> SqlOutput {
3991 let alias = &upd.target.alias;
3992 let mut union_parts = vec![];
3993 for (i, imp) in upd.poly_implementors.iter().enumerate() {
3994 let cte_name = format!("_u{}", i);
3995 let mut upd_sql = format!(
3996 "UPDATE {} AS {}\nSET {}\nFROM {}",
3997 qn(&imp.module, &imp.table),
3998 qi(alias),
3999 sets.join(", "),
4000 qi(iter_alias),
4001 );
4002 append_filter(&mut upd_sql, &upd.filter);
4003 upd_sql.push_str(&format!("\nRETURNING {}.\"id\"", qi(alias)));
4004 cte_parts.push(format!("\"{}\" AS (\n{}\n)", cte_name, upd_sql));
4005
4006 let r_alias = format!("_r{}", i);
4007 union_parts.push(format!(
4008 "SELECT ROW({}::text, {}.\"id\") AS result FROM \"{}\" AS {}",
4009 sql_str(&imp.type_name),
4010 qi(&r_alias),
4011 cte_name,
4012 qi(&r_alias),
4013 ));
4014 }
4015 let sql = format!("WITH {}\n{}", cte_parts.join(",\n"), union_parts.join("\nUNION ALL\n"));
4016 let (shape, _) = emit_returning_shape(&upd.target, &upd.returning, true);
4017 SqlOutput {
4018 sql,
4019 shape,
4020 inference_plan: None,
4021 }
4022}
4023
4024fn emit_for_insert(
4025 ins: &IrInsert,
4026 iter_alias: &str,
4027 iter_cte: &str,
4028 user_ctes: &[IrCteDef],
4029 body_ctes: &[IrCteDef],
4030) -> SqlOutput {
4031 let rewrite_cols: std::collections::HashSet<&str> = ins.rewrites.iter().map(|r| r.column.as_str()).collect();
4032
4033 let cols: Vec<String> = ins
4034 .assignments
4035 .iter()
4036 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
4037 .map(|(c, _)| qi(c))
4038 .chain(ins.rewrites.iter().map(|r| qi(&r.column)))
4039 .collect();
4040 let sel_exprs: Vec<String> = ins
4041 .assignments
4042 .iter()
4043 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
4044 .map(|(_, e)| emit_expr(e))
4045 .chain(ins.rewrites.iter().map(|r| emit_expr(&r.expr)))
4046 .collect();
4047
4048 let mut cte_parts: Vec<String> = emit_user_cte_parts(user_ctes);
4049 cte_parts.push(iter_cte.to_string());
4053 cte_parts.extend(emit_user_cte_parts(body_ctes));
4054 cte_parts.extend(emit_user_cte_parts(&ins.nested_ctes));
4055 let correlated_targets = correlated_append_indices(&ins.multi_link_appends, &[body_ctes, &ins.nested_ctes]);
4058
4059 let mut insert_sql = format!(
4060 "INSERT INTO {} ({})\nSELECT {} FROM {}",
4061 target_ref(&ins.target),
4062 cols.join(", "),
4063 sel_exprs.join(", "),
4064 qi(iter_alias),
4065 );
4066 append_insert_guard(&mut insert_sql, ins);
4067 if let Some(conflict) = &ins.unless_conflict {
4068 emit_conflict(&mut insert_sql, conflict);
4069 }
4070 let (shape, returning_sql) = emit_returning_shape(&ins.target, &ins.returning, false);
4071
4072 if ins.multi_link_appends.is_empty() {
4073 let mut sql = format!("WITH {}\n{}", cte_parts.join(",\n"), insert_sql);
4074 if let Some(r) = returning_sql {
4075 sql.push_str(&r);
4076 }
4077 return SqlOutput {
4078 sql,
4079 shape,
4080 inference_plan: None,
4081 };
4082 }
4083
4084 const IDS: &str = "_for_dml__ids";
4087 const ROWS: &str = "_for_dml__rows";
4088 if correlated_targets.is_empty() {
4089 insert_sql.push_str("\nRETURNING *");
4090 cte_parts.push(format!("\"{}\" AS (\n{}\n)", IDS, insert_sql));
4091 } else {
4092 let outer_source = format!("{}.{}", qi(iter_alias), qi("v"));
4093 cte_parts.push(emit_iteration_rows_cte(ins, ROWS, iter_alias, &outer_source));
4094 cte_parts.push(emit_insert_from_rows_cte(ins, IDS, ROWS));
4095 }
4096 for (i, append) in ins.multi_link_appends.iter().enumerate() {
4097 let cte_name = format!("_for_dml__ml_add_{}", i);
4098 if correlated_targets.contains(&i) {
4099 let IrMultiLinkValueSource::CteRef(target) = &append.values.source else {
4100 unreachable!("only a CTE-backed value can carry an iteration key")
4101 };
4102 cte_parts.push(emit_correlated_ml_append_cte(append, ROWS, target, &cte_name));
4103 } else {
4104 cte_parts.push(emit_ml_append_cte(append, IDS, &ins.target.alias, &cte_name));
4105 }
4106 }
4107
4108 let projection = match &returning_sql {
4109 Some(r) => r.trim_start_matches('\n').replacen("RETURNING ", "SELECT ", 1),
4112 None => "SELECT 1".to_string(),
4113 };
4114 SqlOutput {
4115 sql: format!("WITH {}\n{}\nFROM \"{}\"", cte_parts.join(",\n"), projection, IDS),
4116 shape,
4117 inference_plan: None,
4118 }
4119}
4120
4121fn enqueue_cte_sql(eq: &VectorEnqueueInfo, source_cte: &str, cte_name: &str) -> String {
4127 let index_name_sql = match &eq.index_name {
4128 None => "NULL".to_string(),
4129 Some(name) => sql_str(name),
4130 };
4131 format!(
4132 concat!(
4133 "\"{}\" AS (\n",
4134 " INSERT INTO _pylon.\"IndexOutbox\"\n",
4135 " (object_id, type_name, index_kind, index_name)\n",
4136 " SELECT \"id\", {}, 'Vector'::_pylon.\"IndexKind\", {}\n",
4137 " FROM \"{}\"\n",
4138 " ON CONFLICT (object_id, index_kind, index_name)\n",
4139 " DO UPDATE SET status = 'Pending', enqueued_at = now()\n",
4140 ")",
4141 ),
4142 cte_name,
4143 sql_str(&eq.type_name),
4144 index_name_sql,
4145 source_cte,
4146 )
4147}
4148
4149fn enqueue_ctes(enqueue: &[VectorEnqueueInfo], source_cte: &str) -> Vec<String> {
4151 enqueue
4152 .iter()
4153 .enumerate()
4154 .map(|(i, eq)| enqueue_cte_sql(eq, source_cte, &format!("_eq{}", i)))
4155 .collect()
4156}
4157
4158fn search_backend_index_kind(backend: &crate::schema::SearchBackend) -> &'static str {
4162 match backend {
4163 crate::schema::SearchBackend::OpenSearch => "OpenSearch",
4164 crate::schema::SearchBackend::Meilisearch => "Meilisearch",
4165 crate::schema::SearchBackend::Postgres => {
4166 unreachable!("Postgres-backed search indexes are never collected into SearchEnqueueInfo")
4167 }
4168 }
4169}
4170
4171fn enqueue_search_cte_sql(eq: &SearchEnqueueInfo, source_cte: &str, cte_name: &str) -> String {
4173 let index_name_sql = match &eq.index_name {
4174 None => "NULL".to_string(),
4175 Some(name) => sql_str(name),
4176 };
4177 format!(
4178 concat!(
4179 "\"{}\" AS (\n",
4180 " INSERT INTO _pylon.\"IndexOutbox\"\n",
4181 " (object_id, type_name, index_kind, index_name, operation)\n",
4182 " SELECT \"id\", {}, '{}'::_pylon.\"IndexKind\", {}, {}\n",
4183 " FROM \"{}\"\n",
4184 " ON CONFLICT (object_id, index_kind, index_name)\n",
4185 " DO UPDATE SET status = 'Pending', operation = EXCLUDED.operation, enqueued_at = now()\n",
4186 ")",
4187 ),
4188 cte_name,
4189 sql_str(&eq.type_name),
4190 search_backend_index_kind(&eq.backend),
4191 index_name_sql,
4192 sql_str(eq.operation),
4193 source_cte,
4194 )
4195}
4196
4197fn enqueue_search_ctes(enqueue: &[SearchEnqueueInfo], source_cte: &str, offset: usize) -> Vec<String> {
4199 enqueue
4200 .iter()
4201 .enumerate()
4202 .map(|(i, eq)| enqueue_search_cte_sql(eq, source_cte, &format!("_es{}", offset + i)))
4203 .collect()
4204}
4205
4206fn shape_select_from_cte(
4209 target: &IrSource,
4210 returning: &[IrShapePointer],
4211 cte_name: &str,
4212) -> (ShapeDescriptor, Option<String>) {
4213 if returning.is_empty() {
4214 return (
4215 ShapeDescriptor {
4216 root: ShapeNode::Scalar {
4217 name: String::new(),
4218 position: 0,
4219 },
4220 },
4221 None,
4222 );
4223 }
4224 let (pointer_exprs, shape_pointers) = build_shape(returning, "");
4225 let mut parts = vec![type_disc(&target.type_name)];
4226 parts.extend(pointer_exprs);
4227 let tuple = parts.join(",\n ");
4228 let sql = format!("SELECT (\n {}\n) AS result\nFROM {}", tuple, qi(cte_name));
4229 let root_pointers = prepend_type(shape_pointers);
4230 let shape = ShapeDescriptor {
4231 root: ShapeNode::Object {
4232 name: String::new(),
4233 type_name: Some(target.type_name.clone()),
4234 position: 0,
4235 cardinality: Cardinality::Required,
4236 pointers: root_pointers,
4237 has_implicit_id: shape_has_implicit_id(returning),
4238 },
4239 };
4240 (shape, Some(sql))
4241}
4242
4243fn emit_insert_row_sql(ins: &IrInsert) -> String {
4252 let rewrite_cols: std::collections::HashSet<&str> = ins.rewrites.iter().map(|r| r.column.as_str()).collect();
4253 let cols: Vec<String> = ins
4254 .assignments
4255 .iter()
4256 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
4257 .map(|(c, _)| qi(c))
4258 .chain(ins.rewrites.iter().map(|r| qi(&r.column)))
4259 .collect();
4260 let vals: Vec<String> = ins
4261 .assignments
4262 .iter()
4263 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
4264 .map(|(_, e)| emit_expr(e))
4265 .chain(ins.rewrites.iter().map(|r| emit_expr(&r.expr)))
4266 .collect();
4267 if let Some(guard) = &ins.guard {
4270 let from_ctes = nested_cte_from(&ins.nested_ctes, &vals.join(","), " ");
4271 let projection = if cols.is_empty() {
4272 String::new()
4273 } else {
4274 format!(" {}", vals.join(", "))
4275 };
4276 let column_list = if cols.is_empty() {
4277 String::new()
4278 } else {
4279 format!(" ({})", cols.join(", "))
4280 };
4281 return format!(
4282 "INSERT INTO {}{} SELECT{}{} WHERE {}",
4283 target_ref(&ins.target),
4284 column_list,
4285 projection,
4286 from_ctes,
4287 emit_expr(guard),
4288 );
4289 }
4290 if cols.is_empty() && ins.nested_ctes.is_empty() {
4291 format!("INSERT INTO {} DEFAULT VALUES", target_ref(&ins.target))
4294 } else if ins.nested_ctes.is_empty() {
4295 format!(
4296 "INSERT INTO {} ({}) VALUES ({})",
4297 target_ref(&ins.target),
4298 cols.join(", "),
4299 vals.join(", ")
4300 )
4301 } else {
4302 format!(
4303 "INSERT INTO {} ({}) SELECT {}{}",
4304 target_ref(&ins.target),
4305 cols.join(", "),
4306 vals.join(", "),
4307 nested_cte_from(&ins.nested_ctes, &vals.join(","), " "),
4308 )
4309 }
4310}
4311
4312fn emit_insert_stmt(ins: &IrInsert) -> SqlOutput {
4313 if ins.enqueue_vector.is_empty()
4314 && ins.enqueue_search.is_empty()
4315 && !insert_has_any_multilink(ins)
4316 && ins.nested_ctes.is_empty()
4317 {
4318 let mut sql = emit_insert_row_sql(ins);
4319 if let Some(conflict) = &ins.unless_conflict {
4320 emit_conflict(&mut sql, conflict);
4321 }
4322 let (shape, returning_sql) = emit_returning_shape(&ins.target, &ins.returning, false);
4323 if let Some(r) = returning_sql {
4324 sql.push_str(&r);
4325 }
4326 return SqlOutput {
4327 sql,
4328 shape,
4329 inference_plan: None,
4330 };
4331 }
4332
4333 let mut cte_parts = if insert_has_any_multilink(ins) {
4339 emit_insert_multilink_ctes(ins, "_w")
4340 } else {
4341 let mut cte_parts = emit_user_cte_parts(&ins.nested_ctes);
4342 let mut insert_sql = emit_insert_row_sql(ins);
4343 if let Some(conflict) = &ins.unless_conflict {
4344 emit_conflict(&mut insert_sql, conflict);
4345 }
4346 insert_sql.push_str("\nRETURNING \"id\"");
4347 cte_parts.push(format!("\"_w\" AS (\n{}\n)", insert_sql));
4348 cte_parts
4349 };
4350 cte_parts.extend(enqueue_ctes(&ins.enqueue_vector, "_w"));
4351 cte_parts.extend(enqueue_search_ctes(&ins.enqueue_search, "_w", ins.enqueue_vector.len()));
4352
4353 let (shape, select_sql) = shape_select_from_cte(&ins.target, &ins.returning, "_w");
4354 let sql = format!(
4355 "WITH\n{}\n{}",
4356 cte_parts.join(",\n"),
4357 select_sql.unwrap_or_else(|| "SELECT * FROM \"_w\"".to_string()),
4358 );
4359 SqlOutput {
4360 sql,
4361 shape,
4362 inference_plan: None,
4363 }
4364}
4365
4366fn emit_poly_update_stmt(upd: &IrUpdate, user_ctes: &[IrCteDef]) -> SqlOutput {
4369 let alias = &upd.target.alias;
4370 let sets = update_set_fragments(&upd.assignments, &upd.rewrites, "");
4371
4372 let mut cte_parts: Vec<String> = emit_user_cte_parts(user_ctes);
4378 cte_parts.extend(emit_user_cte_parts(&upd.nested_ctes));
4379 let mut union_parts = vec![];
4380 let from_ctes = nested_cte_from(&upd.nested_ctes, &sets.join(","), "\n");
4381
4382 let has_any_multilink = !upd.multi_link_clears.is_empty()
4383 || !upd.multi_link_replaces.is_empty()
4384 || !upd.multi_link_appends.is_empty()
4385 || !upd.multi_link_removals.is_empty();
4386 for (i, imp) in upd.poly_implementors.iter().enumerate() {
4387 let cte_name = format!("_u{}", i);
4388 if has_any_multilink {
4392 cte_parts.extend(emit_update_multilink_cte_parts(
4393 &concrete_poly_update(upd, imp),
4394 &cte_name,
4395 false,
4396 ));
4397 } else {
4398 let mut upd_sql = format!(
4399 "UPDATE {} AS {}\nSET {}{}",
4400 qn(&imp.module, &imp.table),
4401 qi(alias),
4402 sets.join(", "),
4403 from_ctes,
4404 );
4405 append_filter(&mut upd_sql, &upd.filter);
4406 upd_sql.push_str(&format!("\nRETURNING {}.\"id\"", qi(alias)));
4407 cte_parts.push(format!("\"{}\" AS (\n{}\n)", cte_name, upd_sql));
4408 }
4409
4410 let r_alias = format!("_r{}", i);
4411 union_parts.push(format!(
4412 "SELECT ROW({}::text, {}.\"id\") AS result FROM \"{}\" AS {}",
4413 sql_str(&imp.type_name),
4414 qi(&r_alias),
4415 cte_name,
4416 qi(&r_alias),
4417 ));
4418 }
4419
4420 let sql = format!("WITH\n{}\n{}", cte_parts.join(",\n"), union_parts.join("\nUNION ALL\n"),);
4421
4422 let (shape, _) = emit_returning_shape(&upd.target, &upd.returning, true);
4423 SqlOutput {
4424 sql,
4425 shape,
4426 inference_plan: None,
4427 }
4428}
4429
4430fn emit_update_stmt(upd: &IrUpdate, user_ctes: &[IrCteDef]) -> SqlOutput {
4431 if !upd.poly_implementors.is_empty() {
4432 return emit_poly_update_stmt(upd, user_ctes);
4433 }
4434 let alias = &upd.target.alias;
4435 let (shape, returning_sql) = emit_returning_shape(&upd.target, &upd.returning, true);
4436
4437 let has_any_multilink = !upd.multi_link_clears.is_empty()
4438 || !upd.multi_link_replaces.is_empty()
4439 || !upd.multi_link_appends.is_empty()
4440 || !upd.multi_link_removals.is_empty();
4441
4442 if !has_any_multilink && upd.enqueue_vector.is_empty() && upd.enqueue_search.is_empty() {
4443 let sets = update_set_fragments(&upd.assignments, &upd.rewrites, "");
4450 let mut sql = format!(
4451 "UPDATE {} AS {}\nSET {}",
4452 target_ref(&upd.target),
4453 qi(alias),
4454 sets.join(", "),
4455 );
4456 sql.push_str(&nested_cte_from(&upd.nested_ctes, &sets.join(","), "\n"));
4457 append_filter(&mut sql, &upd.filter);
4458 if let Some(r) = returning_sql {
4459 sql.push_str(&r);
4460 }
4461 let combined_ctes: Vec<IrCteDef> = user_ctes
4464 .iter()
4465 .cloned()
4466 .chain(upd.nested_ctes.iter().cloned())
4467 .collect();
4468 if !combined_ctes.is_empty() {
4469 sql = format!("{}{}", emit_cte_prefix(&combined_ctes), sql);
4470 }
4471 return SqlOutput {
4472 sql,
4473 shape,
4474 inference_plan: None,
4475 };
4476 }
4477
4478 if !has_any_multilink && (!upd.enqueue_vector.is_empty() || !upd.enqueue_search.is_empty()) {
4479 let sets = update_set_fragments(&upd.assignments, &upd.rewrites, "");
4481 let mut upd_sql = format!(
4482 " UPDATE {} AS {}\n SET {}",
4483 target_ref(&upd.target),
4484 qi(alias),
4485 sets.join(", "),
4486 );
4487 upd_sql.push_str(&nested_cte_from(&upd.nested_ctes, &sets.join(","), "\n "));
4488 append_filter(&mut upd_sql, &upd.filter);
4489 upd_sql.push_str("\n RETURNING \"id\"");
4490
4491 let mut cte_parts: Vec<String> = emit_user_cte_parts(user_ctes);
4492 cte_parts.extend(emit_user_cte_parts(&upd.nested_ctes));
4493 cte_parts.push(format!("\"_w\" AS (\n{}\n)", upd_sql));
4494 cte_parts.extend(enqueue_ctes(&upd.enqueue_vector, "_w"));
4495 cte_parts.extend(enqueue_search_ctes(&upd.enqueue_search, "_w", upd.enqueue_vector.len()));
4496
4497 let (shape2, select_sql) = shape_select_from_cte(&upd.target, &upd.returning, "_w");
4498 let sql = format!(
4499 "WITH\n{}\n{}",
4500 cte_parts.join(",\n"),
4501 select_sql.unwrap_or_else(|| "SELECT * FROM \"_w\"".to_string()),
4502 );
4503 return SqlOutput {
4504 sql,
4505 shape: shape2,
4506 inference_plan: None,
4507 };
4508 }
4509
4510 let result_expr = if !upd.returning.is_empty() {
4512 let (pointer_exprs, _) = build_shape(&upd.returning, alias);
4513 let mut parts = vec![type_disc(&upd.target.type_name)];
4514 parts.extend(pointer_exprs);
4515 parts.join(",\n ")
4516 } else {
4517 format!("{}.id", qi(alias))
4518 };
4519
4520 let has_scalar_changes = !upd.assignments.is_empty() || !upd.rewrites.is_empty();
4521
4522 let mut cte_parts: Vec<String> = emit_user_cte_parts(user_ctes);
4526 cte_parts.extend(emit_user_cte_parts(&upd.nested_ctes));
4527
4528 if has_scalar_changes {
4530 let sets = update_set_fragments(&upd.assignments, &upd.rewrites, "");
4531 let mut upd_sql = format!(
4532 "UPDATE {} AS {}\nSET {}",
4533 target_ref(&upd.target),
4534 qi(alias),
4535 sets.join(", "),
4536 );
4537 upd_sql.push_str(&nested_cte_from(&upd.nested_ctes, &sets.join(","), "\n"));
4538 append_filter(&mut upd_sql, &upd.filter);
4539 upd_sql.push_str("\nRETURNING *");
4540 cte_parts.push(format!("\"_ids\" AS (\n{}\n)", upd_sql));
4541 } else {
4542 let mut sel = format!(
4543 "SELECT {}.* FROM {} AS {}",
4544 qi(alias),
4545 target_ref(&upd.target),
4546 qi(alias),
4547 );
4548 append_filter(&mut sel, &upd.filter);
4549 cte_parts.push(format!("\"_ids\" AS (\n{}\n)", sel));
4550 }
4551
4552 for (i, clr) in upd.multi_link_clears.iter().enumerate() {
4554 let exclude = ml_clear_exclusion(upd.multi_link_replaces.get(i));
4555 let del = format!(
4556 "DELETE FROM {} WHERE {} IN (SELECT id FROM \"_ids\"){}",
4557 qn(&clr.module, &clr.junction_table),
4558 qi(&clr.source_col),
4559 exclude,
4560 );
4561 cte_parts.push(format!("\"_clr_{}\" AS (\n{}\n)", i, del));
4562 }
4563
4564 for (i, app) in upd.multi_link_appends.iter().enumerate() {
4566 cte_parts.push(emit_ml_append_cte(
4567 app,
4568 "_ids",
4569 &upd.target.alias,
4570 &format!("_ml_add_{}", i),
4571 ));
4572 }
4573
4574 for (i, rem) in upd.multi_link_removals.iter().enumerate() {
4576 cte_parts.push(emit_ml_remove_cte(
4577 rem,
4578 "_ids",
4579 &upd.target.alias,
4580 &format!("_ml_rm_{}", i),
4581 ));
4582 }
4583
4584 for (i, rep) in upd.multi_link_replaces.iter().enumerate() {
4586 cte_parts.push(emit_ml_append_cte(
4587 rep,
4588 "_ids",
4589 &upd.target.alias,
4590 &format!("_ml_rep_{}", i),
4591 ));
4592 }
4593
4594 cte_parts.extend(enqueue_ctes(&upd.enqueue_vector, "_ids"));
4596 cte_parts.extend(enqueue_search_ctes(
4597 &upd.enqueue_search,
4598 "_ids",
4599 upd.enqueue_vector.len(),
4600 ));
4601
4602 let sql = format!(
4603 "WITH\n{}\nSELECT (\n {}\n) AS result\nFROM \"_ids\" AS {}",
4604 cte_parts.join(",\n"),
4605 result_expr,
4606 qi(alias),
4607 );
4608 SqlOutput {
4609 sql,
4610 shape,
4611 inference_plan: None,
4612 }
4613}
4614
4615fn emit_delete_stmt(del: &IrDelete) -> SqlOutput {
4618 if !del.poly_implementors.is_empty() {
4619 return emit_poly_delete_stmt(del);
4620 }
4621 let alias = &del.target.alias;
4622
4623 if del.enqueue_search.is_empty() {
4624 let mut sql = format!("DELETE FROM {} AS {}", target_ref(&del.target), qi(alias),);
4625 append_filter(&mut sql, &del.filter);
4626 let (shape, returning_sql) = emit_returning_shape(&del.target, &del.returning, true);
4627 if let Some(r) = returning_sql {
4628 sql.push_str(&r);
4629 }
4630 return SqlOutput {
4631 sql,
4632 shape,
4633 inference_plan: None,
4634 };
4635 }
4636
4637 let mut del_sql = format!(" DELETE FROM {} AS {}", target_ref(&del.target), qi(alias),);
4639 append_filter(&mut del_sql, &del.filter);
4640 del_sql.push_str("\n RETURNING \"id\"");
4641
4642 let mut cte_parts = vec![format!("\"_del\" AS (\n{}\n)", del_sql)];
4643 cte_parts.extend(enqueue_search_ctes(&del.enqueue_search, "_del", 0));
4644
4645 let (shape, select_sql) = shape_select_from_cte(&del.target, &del.returning, "_del");
4646 let sql = format!(
4647 "WITH\n{}\n{}",
4648 cte_parts.join(",\n"),
4649 select_sql.unwrap_or_else(|| "SELECT * FROM \"_del\"".to_string()),
4650 );
4651 SqlOutput {
4652 sql,
4653 shape,
4654 inference_plan: None,
4655 }
4656}
4657
4658fn emit_poly_delete_stmt(del: &IrDelete) -> SqlOutput {
4659 let alias = &del.target.alias;
4660 let mut cte_parts = vec![];
4661 let mut union_parts = vec![];
4662
4663 for (i, imp) in del.poly_implementors.iter().enumerate() {
4664 let cte_name = format!("_d{}", i);
4665 let mut del_sql = format!("DELETE FROM {} AS {}", qn(&imp.module, &imp.table), qi(alias),);
4666 append_filter(&mut del_sql, &del.filter);
4667 del_sql.push_str(&format!("\nRETURNING {}.\"id\"", qi(alias)));
4668 cte_parts.push(format!("\"{}\" AS (\n{}\n)", cte_name, del_sql));
4669
4670 let r_alias = format!("_r{}", i);
4671 union_parts.push(format!(
4672 "SELECT ROW({}::text, {}.\"id\") AS result FROM \"{}\" AS {}",
4673 sql_str(&imp.type_name),
4674 qi(&r_alias),
4675 cte_name,
4676 qi(&r_alias),
4677 ));
4678 }
4679
4680 let sql = format!("WITH\n{}\n{}", cte_parts.join(",\n"), union_parts.join("\nUNION ALL\n"),);
4681
4682 let (shape, _) = emit_returning_shape(&del.target, &del.returning, true);
4683 SqlOutput {
4684 sql,
4685 shape,
4686 inference_plan: None,
4687 }
4688}
4689
4690fn emit_returning_shape(
4695 target: &IrSource,
4696 returning: &[IrShapePointer],
4697 with_alias: bool,
4698) -> (ShapeDescriptor, Option<String>) {
4699 if returning.is_empty() {
4700 return (
4701 ShapeDescriptor {
4702 root: ShapeNode::Scalar {
4703 name: String::new(),
4704 position: 0,
4705 },
4706 },
4707 None,
4708 );
4709 }
4710
4711 let alias = if with_alias { target.alias.as_str() } else { "" };
4712 let (pointer_exprs, shape_pointers) = build_shape(returning, alias);
4713
4714 let mut parts = vec![type_disc(&target.type_name)];
4715 parts.extend(pointer_exprs);
4716 let tuple = parts.join(",\n ");
4717 let sql = format!("\nRETURNING (\n {}\n) AS result", tuple);
4718
4719 let root_pointers = prepend_type(shape_pointers);
4720 let shape = ShapeDescriptor {
4721 root: ShapeNode::Object {
4722 name: String::new(),
4723 type_name: Some(target.type_name.clone()),
4724 position: 0,
4725 cardinality: Cardinality::Required,
4726 pointers: root_pointers,
4727 has_implicit_id: shape_has_implicit_id(returning),
4728 },
4729 };
4730 (shape, Some(sql))
4731}
4732
4733fn emit_scalar_set(f: &IrScalarSetPointer, pos: usize) -> (String, ShapeNode) {
4736 let from_sql = if !f.poly_implementors.is_empty() {
4737 format!(
4738 "(\n{}\n) AS {}",
4739 emit_poly_union(&f.poly_implementors, &f.poly_columns),
4740 qi(&f.source.alias)
4741 )
4742 } else {
4743 format!("{} AS {}", source_ref(&f.source), qi(&f.source.alias))
4744 };
4745 let sql = format!(
4746 "(SELECT COALESCE(array_agg(ROW({})::record), ARRAY[]::record[]) FROM {})",
4747 emit_expr(&f.bool_expr),
4748 from_sql,
4749 );
4750 let node = ShapeNode::Array {
4751 name: f.alias.clone(),
4752 position: pos,
4753 element: Box::new(ShapeNode::Scalar {
4754 name: String::new(),
4755 position: 0,
4756 }),
4757 };
4758 (sql, node)
4759}
4760
4761fn shape_has_implicit_id(pointers: &[IrShapePointer]) -> bool {
4766 matches!(pointers.first(), Some(IrShapePointer::Scalar(p)) if p.implicit_id)
4767}
4768
4769fn build_shape(pointers: &[IrShapePointer], table_alias: &str) -> (Vec<String>, Vec<ShapeNode>) {
4770 let mut exprs = Vec::new();
4771 let mut nodes = Vec::new();
4772
4773 for (i, pointer) in pointers.iter().enumerate() {
4774 let (sql, node) = emit_shape_pointer(pointer, table_alias, i + 1);
4775 exprs.push(sql);
4776 nodes.push(node);
4777 }
4778
4779 (exprs, nodes)
4780}
4781
4782fn fill_template(template: &str, args: &[String]) -> String {
4788 let mut out = String::with_capacity(template.len());
4789 let mut chars = template.char_indices().peekable();
4790 while let Some((start, c)) = chars.next() {
4791 if c != '$' {
4792 out.push(c);
4793 continue;
4794 }
4795 let mut end = start + 1;
4796 while let Some((i, d)) = chars.peek().copied()
4797 && d.is_ascii_digit()
4798 {
4799 end = i + d.len_utf8();
4800 chars.next();
4801 }
4802 match template[start + 1..end].parse::<usize>() {
4803 Ok(n) if (1..=args.len()).contains(&n) => out.push_str(&args[n - 1]),
4804 _ => out.push_str(&template[start..end]),
4805 }
4806 }
4807 out
4808}
4809
4810fn assert_message_arg(message: &Option<IrExpr>) -> String {
4812 message
4813 .as_ref()
4814 .map(|m| format!(", ({})::text", emit_expr(m)))
4815 .unwrap_or_default()
4816}
4817
4818fn emit_shape_pointer(pointer: &IrShapePointer, table_alias: &str, pos: usize) -> (String, ShapeNode) {
4819 match pointer {
4820 IrShapePointer::Scalar(f) => emit_scalar(f, table_alias, pos),
4821 IrShapePointer::SingleLink(f) => emit_single_link(f, table_alias, pos),
4822 IrShapePointer::MultiLink(f) => emit_multi_link(f, table_alias, pos),
4823 IrShapePointer::Computed(f) => (emit_expr(&f.expr), expr_shape_node(&f.alias, pos, &f.expr)),
4824 IrShapePointer::ScalarSet(f) => emit_scalar_set(f, pos),
4825 IrShapePointer::Asserted(a) => {
4826 let (sql, node) = emit_shape_pointer(&a.inner, table_alias, pos);
4827 let checked_set = match &a.check {
4838 Some(check) if emits_one_object(check) => {
4839 format!("ARRAY{}", emit_shape_pointer(check, table_alias, pos).0)
4840 }
4841 Some(check) => emit_shape_pointer(check, table_alias, pos).0,
4842 None if emits_one_object(&a.inner) => format!("ARRAY{sql}"),
4843 None => "\"_a\".\"v\"".to_string(),
4844 };
4845 let checked = format!(
4846 "(SELECT \"_a\".\"v\" FROM (SELECT {sql} AS \"v\") AS \"_a\"\n WHERE cardinality(\"_pylon\".{}({checked_set}::text[]{})) >= 0)",
4847 qi(&a.fn_name),
4848 assert_message_arg(&a.message),
4849 );
4850 (checked, node)
4851 }
4852 }
4853}
4854
4855fn emits_one_object(pointer: &IrShapePointer) -> bool {
4858 match pointer {
4859 IrShapePointer::SingleLink(_) => true,
4860 IrShapePointer::Computed(c) => matches!(
4861 c.expr,
4862 IrExpr::ObjectPathSubquery(_) | IrExpr::ObjectPathUnion { multi: false, .. }
4863 ),
4864 IrShapePointer::Asserted(a) => emits_one_object(&a.inner),
4865 _ => false,
4866 }
4867}
4868
4869fn pg_quoted_to_pylon(pg_type: &str) -> String {
4872 let inner = pg_type.trim_start_matches('"');
4873 if let Some(idx) = inner.find(r#""."#) {
4874 let module = &inner[..idx];
4875 let type_name = inner[idx + 3..].trim_end_matches('"');
4876 format!("{}::{}", module, type_name)
4877 } else {
4878 pg_type.to_string()
4879 }
4880}
4881
4882struct QualifiedPgType {
4893 name: String,
4895 is_array: bool,
4896}
4897
4898impl QualifiedPgType {
4899 fn of(pg_type: &str) -> Option<Self> {
4900 let (element, is_array) = match pg_type.strip_suffix("[]") {
4901 Some(element) => (element, true),
4902 None => (pg_type, false),
4903 };
4904 element.starts_with('"').then(|| Self {
4905 name: pg_quoted_to_pylon(element),
4906 is_array,
4907 })
4908 }
4909
4910 fn text_cast(&self) -> &'static str {
4912 if self.is_array { "::text[]" } else { "::text" }
4913 }
4914
4915 fn shape_node(&self, name: String, position: usize) -> ShapeNode {
4917 let element = ShapeNode::Enum {
4918 name: name.clone(),
4919 position,
4920 enum_type: self.name.clone(),
4921 };
4922 if !self.is_array {
4923 return element;
4924 }
4925 ShapeNode::Array {
4926 name,
4927 element: Box::new(ShapeNode::Enum {
4930 name: String::new(),
4931 position: 0,
4932 enum_type: self.name.clone(),
4933 }),
4934 position,
4935 }
4936 }
4937}
4938
4939fn emit_scalar(f: &IrScalarPointer, table_alias: &str, pos: usize) -> (String, ShapeNode) {
4940 let nominal_tuple = f
4947 .pg_type
4948 .strip_suffix("[]")
4949 .unwrap_or(&f.pg_type)
4950 .strip_prefix("__nt__:");
4951 if nominal_tuple.is_some() || f.tuple_shape.is_some() {
4952 let is_array = f.pg_type.ends_with("[]");
4953 let sql = if table_alias.is_empty() {
4954 qi(&f.column)
4955 } else {
4956 format!("{}.{}", qi(table_alias), qi(&f.column))
4957 };
4958 let type_name = nominal_tuple
4959 .map(str::to_string)
4960 .or_else(|| f.tuple_shape.as_ref().and_then(|s| s.type_name.clone()));
4961 let members = f.tuple_shape.as_ref().map(|s| s.members.as_slice()).unwrap_or(&[]);
4962 let tuple = composite_tuple_shape(
4965 if is_array { "" } else { &f.alias },
4966 if is_array { 0 } else { pos },
4967 type_name,
4968 members,
4969 );
4970 let node = if is_array {
4971 ShapeNode::Array {
4972 name: f.alias.clone(),
4973 position: pos,
4974 element: Box::new(tuple),
4975 }
4976 } else {
4977 tuple
4978 };
4979 return (sql, node);
4980 }
4981 if let Some(qualified) = QualifiedPgType::of(&f.pg_type) {
4984 let cast = qualified.text_cast();
4985 let sql = if table_alias.is_empty() {
4986 format!("{}{}", qi(&f.column), cast)
4987 } else {
4988 format!("{}.{}{}", qi(table_alias), qi(&f.column), cast)
4989 };
4990 return (sql, qualified.shape_node(f.alias.clone(), pos));
4991 }
4992 let sql = if table_alias.is_empty() {
4993 format!("{}::{}", qi(&f.column), f.pg_type)
4994 } else {
4995 format!("{}.{}::{}", qi(table_alias), qi(&f.column), f.pg_type)
4996 };
4997 (
4998 sql,
4999 ShapeNode::Scalar {
5000 name: f.alias.clone(),
5001 position: pos,
5002 },
5003 )
5004}
5005
5006fn emit_single_link(f: &IrSingleLinkPointer, parent_alias: &str, pos: usize) -> (String, ShapeNode) {
5007 let sub = &f.subquery;
5008 let [IrRowSource::Bound { source, shape }] = sub.rows.as_slice() else {
5009 unreachable!("single-link subquery is always schema-bound")
5010 };
5011 let sub_alias = &source.alias;
5012
5013 let (sub_exprs, mut sub_nodes) = build_shape(shape, sub_alias);
5014 let mut parts = vec![source_type_disc(source)];
5015 parts.extend(sub_exprs);
5016
5017 for lp in &f.link_properties {
5022 parts.push(format!("\"jt\".{}", qi(&lp.name)));
5023 let pos = sub_nodes.len() + 1;
5024 sub_nodes.push(ShapeNode::Scalar {
5025 name: format!("@{}", lp.name),
5026 position: pos,
5027 });
5028 }
5029
5030 let tuple = parts.join(",\n ");
5031
5032 let (from_sql, mut where_parts) = match &f.correlation {
5033 IrSingleLinkCorrelation::Fk { fk_column, target_pk } => {
5034 let from = format!("FROM {} AS {}", source_ref(source), qi(sub_alias));
5035 let cond = format!(
5036 "{}.{} = {}.{}",
5037 qi(parent_alias),
5038 qi(fk_column),
5039 qi(sub_alias),
5040 qi(target_pk),
5041 );
5042 (from, vec![cond])
5043 }
5044 IrSingleLinkCorrelation::Junction { join, target_pk: _ } => match join {
5048 IrMultiLinkJoin::Standard { junction_table, module } => {
5049 let from = format!(
5050 "FROM {} AS \"jt\"\n INNER JOIN {} AS {}\n ON {}.id = \"jt\".target",
5051 junction_ref(module, junction_table),
5052 source_ref(source),
5053 qi(sub_alias),
5054 qi(sub_alias),
5055 );
5056 let cond = format!("\"jt\".source = {}.id", qi(parent_alias));
5057 (from, vec![cond])
5058 }
5059 IrMultiLinkJoin::Through {
5060 junction_table,
5061 module,
5062 source_col,
5063 target_col,
5064 } => {
5065 let from = format!(
5066 "FROM {} AS \"jt\"\n INNER JOIN {} AS {}\n ON {}.id = \"jt\".{}",
5067 junction_ref(module, junction_table),
5068 source_ref(source),
5069 qi(sub_alias),
5070 qi(sub_alias),
5071 qi(target_col),
5072 );
5073 let cond = format!("\"jt\".{} = {}.id", qi(source_col), qi(parent_alias));
5074 (from, vec![cond])
5075 }
5076 IrMultiLinkJoin::BacklinkFk { .. } | IrMultiLinkJoin::BacklinkJunction { .. } => {
5077 unreachable!("a junction-backed single link's own forward join is always Standard or Through")
5078 }
5079 },
5080 };
5081 if let Some(filter) = &sub.filter {
5082 where_parts.push(emit_expr(filter));
5083 }
5084
5085 let mut sql = format!(
5086 "(SELECT (\n {}\n )\n {}\n WHERE {}",
5087 tuple,
5088 from_sql,
5089 where_parts.join(" AND "),
5090 );
5091 if !sub.order_by.is_empty() {
5092 let s: Vec<_> = sub.order_by.iter().map(emit_sort_clause).collect();
5093 sql.push_str(&format!("\n ORDER BY {}", s.join(", ")));
5094 }
5095 sql.push(')');
5096
5097 let node = ShapeNode::Object {
5098 name: f.alias.clone(),
5099 type_name: Some(source.type_name.clone()),
5100 position: pos,
5101 cardinality: Cardinality::Optional,
5102 pointers: prepend_type(sub_nodes),
5103 has_implicit_id: shape_has_implicit_id(shape),
5104 };
5105 (sql, node)
5106}
5107
5108fn emit_multi_link(f: &IrMultiLinkPointer, parent_alias: &str, pos: usize) -> (String, ShapeNode) {
5109 let sub = &f.subquery;
5110 let [IrRowSource::Bound { source, shape }] = sub.rows.as_slice() else {
5111 unreachable!("multi-link subquery is always schema-bound")
5112 };
5113 let sub_alias = &source.alias;
5114
5115 let (sub_exprs, mut sub_nodes) = build_shape(shape, sub_alias);
5116 let mut row_parts = vec![source_type_disc(source)];
5117 row_parts.extend(sub_exprs);
5118
5119 for lp in &f.link_properties {
5123 row_parts.push(format!("\"jt\".{}", qi(&lp.name)));
5124 let pos = sub_nodes.len() + 1;
5127 sub_nodes.push(ShapeNode::Scalar {
5128 name: format!("@{}", lp.name),
5129 position: pos,
5130 });
5131 }
5132
5133 let row = row_parts.join(",\n ");
5134
5135 let order_sql = if !sub.order_by.is_empty() {
5137 let s: Vec<_> = sub.order_by.iter().map(emit_sort_clause).collect();
5138 format!(" ORDER BY {}", s.join(", "))
5139 } else {
5140 String::new()
5141 };
5142
5143 let (from_sql, source_cond) = match &f.join {
5144 IrMultiLinkJoin::Standard { junction_table, module } => {
5145 let from = format!(
5146 "FROM {} AS \"jt\"\n INNER JOIN {} AS {}\n ON {}.id = \"jt\".target",
5147 junction_ref(module, junction_table),
5148 source_ref(source),
5149 qi(sub_alias),
5150 qi(sub_alias),
5151 );
5152 let cond = format!("\"jt\".source = {}.id", qi(parent_alias));
5153 (from, cond)
5154 }
5155 IrMultiLinkJoin::Through {
5156 junction_table,
5157 module,
5158 source_col,
5159 target_col,
5160 } => {
5161 let from = format!(
5162 "FROM {} AS \"jt\"\n INNER JOIN {} AS {}\n ON {}.id = \"jt\".{}",
5163 junction_ref(module, junction_table),
5164 source_ref(source),
5165 qi(sub_alias),
5166 qi(sub_alias),
5167 qi(target_col),
5168 );
5169 let cond = format!("\"jt\".{} = {}.id", qi(source_col), qi(parent_alias));
5170 (from, cond)
5171 }
5172 IrMultiLinkJoin::BacklinkFk { fk_col } => {
5175 let from = format!("FROM {} AS {}", source_ref(source), qi(sub_alias));
5176 let cond = format!("{}.{} = {}.id", qi(sub_alias), qi(fk_col), qi(parent_alias));
5177 (from, cond)
5178 }
5179 IrMultiLinkJoin::BacklinkJunction {
5184 junction_table,
5185 module,
5186 owner_col,
5187 current_col,
5188 } => {
5189 let from = format!(
5190 "FROM {} AS \"jt\"\n INNER JOIN {} AS {}\n ON {}.id = \"jt\".{}",
5191 junction_ref(module, junction_table),
5192 source_ref(source),
5193 qi(sub_alias),
5194 qi(sub_alias),
5195 qi(owner_col),
5196 );
5197 let cond = format!("\"jt\".{} = {}.id", qi(current_col), qi(parent_alias));
5198 (from, cond)
5199 }
5200 };
5201
5202 let mut where_parts = vec![source_cond];
5203 if let Some(filter) = &sub.filter {
5204 where_parts.push(emit_expr(filter));
5205 }
5206
5207 if f.single {
5208 let mut sql = format!(
5209 "(SELECT (\n {}\n )\n {}\n WHERE {}",
5210 row,
5211 from_sql,
5212 where_parts.join(" AND "),
5213 );
5214 append_order_by(&mut sql, &sub.order_by);
5215 append_offset_limit(&mut sql, &sub.offset, &sub.limit);
5216 sql.push(')');
5217 let node = ShapeNode::Object {
5218 name: f.alias.clone(),
5219 type_name: Some(source.type_name.clone()),
5220 position: pos,
5221 cardinality: Cardinality::Optional,
5222 pointers: prepend_type(sub_nodes),
5223 has_implicit_id: shape_has_implicit_id(shape),
5224 };
5225 return (sql, node);
5226 }
5227
5228 if f.single {
5229 let mut sql = format!(
5230 "(SELECT (\n {}\n )\n {}\n WHERE {}",
5231 row,
5232 from_sql,
5233 where_parts.join(" AND "),
5234 );
5235 append_order_by(&mut sql, &sub.order_by);
5236 append_offset_limit(&mut sql, &sub.offset, &sub.limit);
5237 sql.push(')');
5238 let node = ShapeNode::Object {
5239 name: f.alias.clone(),
5240 type_name: Some(source.type_name.clone()),
5241 position: pos,
5242 cardinality: Cardinality::Optional,
5243 pointers: prepend_type(sub_nodes),
5244 has_implicit_id: shape_has_implicit_id(shape),
5245 };
5246 return (sql, node);
5247 }
5248
5249 let sql = if sub.limit.is_some() || sub.offset.is_some() {
5250 let mut cols: Vec<String> = row_parts
5255 .iter()
5256 .enumerate()
5257 .map(|(i, part)| format!("{} AS \"c{}\"", part, i))
5258 .collect();
5259 let agg_row: Vec<String> = (0..row_parts.len()).map(|i| format!("\"__lim\".\"c{}\"", i)).collect();
5260 let mut agg_order: Vec<String> = vec![];
5261 for (i, s) in sub.order_by.iter().enumerate() {
5262 cols.push(format!("{} AS \"s{}\"", emit_expr(&s.expr), i));
5263 agg_order.push(emit_sort_clause(&IrSort {
5264 expr: IrExpr::ColumnRef {
5265 alias: "__lim".to_string(),
5266 column: format!("s{}", i),
5267 pg_type: String::new(),
5268 },
5269 direction: s.direction.clone(),
5270 nulls: s.nulls.clone(),
5271 }));
5272 }
5273 let mut inner = format!(
5274 "SELECT {}\n {}\n WHERE {}",
5275 cols.join(",\n "),
5276 from_sql,
5277 where_parts.join(" AND "),
5278 );
5279 append_order_by(&mut inner, &sub.order_by);
5280 append_offset_limit(&mut inner, &sub.offset, &sub.limit);
5281 let agg_order_sql = if agg_order.is_empty() {
5282 String::new()
5283 } else {
5284 format!(" ORDER BY {}", agg_order.join(", "))
5285 };
5286 format!(
5287 "(SELECT COALESCE(\n array_agg(ROW(\n {}\n )::record{}),\n ARRAY[]::record[]\n )\n FROM ({}) AS \"__lim\")",
5288 agg_row.join(",\n "),
5289 agg_order_sql,
5290 inner,
5291 )
5292 } else {
5293 format!(
5294 "(SELECT COALESCE(\n array_agg(ROW(\n {}\n )::record{}),\n ARRAY[]::record[]\n )\n {}\n WHERE {})",
5295 row,
5296 order_sql,
5297 from_sql,
5298 where_parts.join(" AND "),
5299 )
5300 };
5301
5302 let node = ShapeNode::Array {
5303 name: f.alias.clone(),
5304 position: pos,
5305 element: Box::new(ShapeNode::Object {
5306 name: String::new(),
5307 type_name: Some(source.type_name.clone()),
5308 position: 0,
5309 cardinality: Cardinality::Required,
5310 pointers: prepend_type(sub_nodes),
5311 has_implicit_id: shape_has_implicit_id(shape),
5312 }),
5313 };
5314 (sql, node)
5315}
5316
5317fn prepend_type(nodes: Vec<ShapeNode>) -> Vec<ShapeNode> {
5320 let mut out = vec![ShapeNode::Scalar {
5321 name: "__type__".into(),
5322 position: 0,
5323 }];
5324 out.extend(nodes);
5325 out
5326}
5327
5328fn append_filter(sql: &mut String, filter: &Option<IrExpr>) {
5331 if let Some(f) = filter {
5332 sql.push_str(&format!("\nWHERE {}", emit_expr(f)));
5333 }
5334}
5335
5336fn append_order_by(sql: &mut String, order_by: &[IrSort]) {
5337 if !order_by.is_empty() {
5338 let s: Vec<_> = order_by.iter().map(emit_sort_clause).collect();
5339 sql.push_str(&format!("\nORDER BY {}", s.join(", ")));
5340 }
5341}
5342
5343fn append_offset_limit(sql: &mut String, offset: &Option<IrExpr>, limit: &Option<IrExpr>) {
5344 if let Some(o) = offset {
5345 sql.push_str(&format!("\nOFFSET {}", emit_expr(o)));
5346 }
5347 if let Some(l) = limit {
5348 sql.push_str(&format!("\nLIMIT {}", emit_expr(l)));
5349 }
5350}
5351
5352fn append_lock_clause(sql: &mut String, lock: &Option<IrLockClause>) {
5359 let Some(lock) = lock else { return };
5360 let strength = match lock.strength {
5361 IrLockStrength::Update => "UPDATE",
5362 IrLockStrength::NoKeyUpdate => "NO KEY UPDATE",
5363 IrLockStrength::Share => "SHARE",
5364 IrLockStrength::KeyShare => "KEY SHARE",
5365 };
5366 sql.push_str(&format!("\nFOR {}", strength));
5367 match lock.wait {
5368 IrLockWait::Block => {}
5369 IrLockWait::NoWait => sql.push_str(" NOWAIT"),
5370 IrLockWait::SkipLocked => sql.push_str(" SKIP LOCKED"),
5371 }
5372}
5373
5374fn emit_sort_clause(s: &IrSort) -> String {
5375 let dir = match s.direction {
5376 IrSortDir::Asc => "ASC",
5377 IrSortDir::Desc => "DESC",
5378 };
5379 let nulls = match s.nulls {
5380 IrNulls::First => "NULLS FIRST",
5381 IrNulls::Last => "NULLS LAST",
5382 };
5383 format!("{} {} {}", emit_expr(&s.expr), dir, nulls)
5384}
5385
5386fn set_operand(expr: &IrExpr) -> String {
5391 match expr {
5392 IrExpr::ArrayFromSelect(_) => format!("SELECT unnest({})", emit_expr(expr)),
5393 IrExpr::CteRef { name, scalar, .. } => {
5394 format!("SELECT {} FROM {}", if *scalar { "\"v\"" } else { "\"id\"" }, qi(name))
5395 }
5396 _ => format!("SELECT {}", emit_expr(expr)),
5397 }
5398}
5399
5400pub fn emit_expr_with_fanouts(expr: &IrExpr, fanouts: &HashMap<(String, String), IrPolyFanout>) -> String {
5403 with_subtype_fanouts(fanouts, || emit_expr(expr))
5404}
5405
5406pub fn emit_expr(expr: &IrExpr) -> String {
5407 match expr {
5408 IrExpr::ColumnRef { alias, column, .. } => {
5409 if alias.is_empty() {
5410 qi(column)
5411 } else if alias == "NEW" || alias == "OLD" {
5412 format!("{alias}.{}", qi(column))
5423 } else {
5424 format!("{}.{}", qi(alias), qi(column))
5425 }
5426 }
5427 IrExpr::Param { index } => format!("${}", index + 1),
5428 IrExpr::Literal(lit) => emit_literal(lit),
5429 IrExpr::BinOp(op) => {
5430 let l = emit_expr(&op.left);
5431 let r = emit_expr(&op.right);
5432 match op.op {
5433 BinOpKind::Add => format!("({} + {})", l, r),
5434 BinOpKind::Sub => format!("({} - {})", l, r),
5435 BinOpKind::Mul => format!("({} * {})", l, r),
5436 BinOpKind::Div => format!("({} / {})", l, r),
5437 BinOpKind::FloorDiv => {
5438 if is_integer_expr(&op.left) && is_integer_expr(&op.right) {
5439 format!("({} / {})", l, r)
5440 } else {
5441 format!("floor(({}) / ({}))", l, r)
5442 }
5443 }
5444 BinOpKind::Mod => format!("({} % {})", l, r),
5445 BinOpKind::Pow => format!("power({}, {})", l, r),
5446 BinOpKind::Eq => format!("({} = {})", l, r),
5447 BinOpKind::Ne => format!("({} <> {})", l, r),
5448 BinOpKind::Lt => format!("({} < {})", l, r),
5449 BinOpKind::Le => format!("({} <= {})", l, r),
5450 BinOpKind::Gt => format!("({} > {})", l, r),
5451 BinOpKind::Ge => format!("({} >= {})", l, r),
5452 BinOpKind::And => format!("({} AND {})", l, r),
5453 BinOpKind::Or => format!("({} OR {})", l, r),
5454 BinOpKind::Like => format!("({} LIKE {})", l, r),
5455 BinOpKind::Ilike => format!("({} ILIKE {})", l, r),
5456 BinOpKind::NotLike => format!("({} NOT LIKE {})", l, r),
5457 BinOpKind::NotIlike => format!("({} NOT ILIKE {})", l, r),
5458 BinOpKind::In => format!("({} = ANY({}))", l, unwrap_unnest_for_any(&op.right, &r)),
5464 BinOpKind::NotIn => format!("({} <> ALL({}))", l, unwrap_unnest_for_any(&op.right, &r)),
5465 BinOpKind::Coalesce => format!("COALESCE({}, {})", l, r),
5466 BinOpKind::CoalesceEq => format!("({} IS NOT DISTINCT FROM {})", l, r),
5467 BinOpKind::CoalesceNe => format!("({} IS DISTINCT FROM {})", l, r),
5468 BinOpKind::Concat => format!("({} || {})", l, r),
5469 }
5470 }
5471 IrExpr::UnaryOp(op) => {
5472 let inner = emit_expr(&op.operand);
5473 match op.op {
5474 UnaryOpKind::Not => format!("(NOT {})", inner),
5475 UnaryOpKind::Minus => format!("(-{})", inner),
5476 UnaryOpKind::Exists => format!("EXISTS({})", inner),
5477 UnaryOpKind::Distinct => inner,
5482 }
5483 }
5484 IrExpr::FunctionCall(f) => {
5485 let args: Vec<_> = f.args.iter().map(emit_expr).collect();
5486 if let Some(tmpl) = &f.sql_template {
5487 return fill_template(tmpl, &args);
5488 }
5489 let name = match &f.schema {
5490 Some(s) => format!("{}.{}", pg_schema(s), qi(&f.name)),
5491 None => f.name.clone(),
5492 };
5493 format!("{}({})", name, args.join(", "))
5494 }
5495 IrExpr::TypeCast(c) => {
5496 if c.pg_type == "jsonb" {
5499 match &c.expr {
5500 IrExpr::Param { .. } => format!("({})::jsonb", emit_expr(&c.expr)),
5511 IrExpr::Literal(IrLiteral::Str(_)) => {
5512 format!("to_jsonb({}::text)", emit_expr(&c.expr))
5513 }
5514 _ => format!("to_jsonb({})", emit_expr(&c.expr)),
5515 }
5516 } else if yields_jsonb(&c.expr) {
5517 format!("(({}) #>> '{{}}')::{}", emit_expr(&c.expr), c.pg_type)
5521 } else {
5522 format!("({})::{}", emit_expr(&c.expr), c.pg_type)
5523 }
5524 }
5525 IrExpr::IfElse(ie) => format!(
5526 "CASE WHEN {} THEN {} ELSE {} END",
5527 emit_expr(&ie.condition),
5528 emit_expr(&ie.if_),
5529 emit_expr(&ie.else_),
5530 ),
5531 IrExpr::Array(elems) => {
5532 if elems.is_empty() {
5533 "ARRAY[]::text[]".to_string()
5534 } else {
5535 let parts: Vec<String> = elems.iter().map(emit_expr).collect();
5536 format!("ARRAY[{}]", parts.join(", "))
5537 }
5538 }
5539 IrExpr::Null => "NULL".to_string(),
5540 IrExpr::AggOverSet {
5541 fn_name,
5542 schema: _,
5543 elems,
5544 } => {
5545 let union_all = elems
5546 .iter()
5547 .map(|e| format!("SELECT {}", emit_expr(e)))
5548 .collect::<Vec<_>>()
5549 .join(" UNION ALL ");
5550 format!("(SELECT {}(v) FROM ({}) AS _set(v))", fn_name, union_all)
5551 }
5552 IrExpr::AggOverCte { fn_name, cte, column } => format!(
5553 "(SELECT {}({}) FROM {})",
5554 fn_name,
5555 column.as_deref().map(qi).unwrap_or_else(|| "*".to_string()),
5556 qi(cte),
5557 ),
5558 IrExpr::ExistsOverCte { cte, column } => match column {
5559 Some(column) => format!("EXISTS(SELECT 1 FROM {} WHERE {} IS NOT NULL)", qi(cte), qi(column)),
5560 None => format!("EXISTS(SELECT 1 FROM {})", qi(cte)),
5561 },
5562 IrExpr::AggOverQuery { fn_name, inner } => {
5563 let inner_sql = emit_select_stmt(inner, &[]).sql;
5564 format!("(SELECT {}(*) FROM ({}) _agg)", fn_name, inner_sql)
5565 }
5566 IrExpr::ArrayFromSelect(src) => emit_array_source(src),
5567 IrExpr::ScalarSubquery(sel) => {
5568 format!(
5569 "(SELECT \"v\" FROM (\n{}\n) AS \"_scalar_sub\")",
5570 emit_select_stmt(sel, &[]).sql
5571 )
5572 }
5573
5574 IrExpr::SetOp { op, left, right, mode } => {
5575 let set = format!("({}) {} ({})", set_operand(left), op.sql(), set_operand(right));
5576 match mode {
5577 crate::ir::SetOpMode::Exists => format!("EXISTS({set})"),
5578 crate::ir::SetOpMode::Array => format!("ARRAY({set})"),
5579 crate::ir::SetOpMode::Aggregate(f) => {
5580 format!("(SELECT {f}(\"_s\".\"v\") FROM ({set}) AS \"_s\"(\"v\"))")
5581 }
5582 }
5583 }
5584
5585 IrExpr::CteRef { name, scalar, .. } => {
5586 let col = if *scalar { "v" } else { "id" };
5589 format!("(SELECT \"{}\" FROM \"{}\")", col, name)
5590 }
5591
5592 IrExpr::CteFieldRef { name, field, .. } => {
5593 format!("(SELECT {} FROM {})", qi(field), qi(name))
5594 }
5595
5596 IrExpr::ForVar { name, .. } => format!("\"_for_{}\".\"v\"", name),
5597
5598 IrExpr::EnumLiteral { pg_type, variant } => {
5599 format!("'{}'::{}", variant.replace('\'', "''"), pg_type)
5600 }
5601
5602 IrExpr::GlobalParam { index, pg_type } => {
5603 format!("(${}::{})", index + 1, pg_type)
5604 }
5605
5606 IrExpr::GlobalRef { cte_name } => {
5607 format!("(SELECT \"value\" FROM \"{}\")", cte_name)
5608 }
5609
5610 IrExpr::NamedTuple { fields, .. } => {
5611 let pairs: Vec<String> = fields
5612 .iter()
5613 .flat_map(|(k, v)| [format!("'{}'", k.replace('\'', "''")), emit_expr(v)])
5614 .collect();
5615 format!("jsonb_build_object({})", pairs.join(", "))
5616 }
5617
5618 IrExpr::Tuple(elems) => {
5619 let items: Vec<String> = elems.iter().map(emit_expr).collect();
5620 format!("jsonb_build_array({})", items.join(", "))
5621 }
5622
5623 IrExpr::Row { elements, .. } => {
5624 let items: Vec<String> = elements.iter().map(emit_expr).collect();
5625 format!("ROW({})", items.join(", "))
5626 }
5627
5628 IrExpr::Subscript { expr, index, is_array } => {
5629 let e = emit_expr(expr);
5630 let i = emit_expr(index);
5631 if *is_array {
5632 format!("_pylon.array_subscript({}, ({})::bigint)", e, i)
5633 } else {
5634 format!("_pylon.str_subscript({}, ({})::bigint)", e, i)
5635 }
5636 }
5637
5638 IrExpr::Slice {
5639 expr,
5640 lower,
5641 upper,
5642 is_array,
5643 } => {
5644 let e = emit_expr(expr);
5645 if *is_array {
5646 let lo = lower
5647 .as_deref()
5648 .map(|x| format!("({}) + 1", emit_expr(x)))
5649 .unwrap_or_else(|| "1".to_string());
5650 let hi = upper.as_deref().map(emit_expr).unwrap_or_default();
5651 if hi.is_empty() {
5652 format!("({})[{}:]", e, lo)
5653 } else {
5654 format!("({})[{}:{}]", e, lo, hi)
5655 }
5656 } else {
5657 let start = lower
5659 .as_deref()
5660 .map(|x| format!("({}) + 1", emit_expr(x)))
5661 .unwrap_or_else(|| "1".to_string());
5662 match upper.as_deref() {
5663 Some(hi_expr) => {
5664 let lo_val = lower.as_deref().map(emit_expr).unwrap_or_else(|| "0".to_string());
5665 format!(
5667 "substr({}, {}, GREATEST(0, ({}) - ({})))",
5668 e,
5669 start,
5670 emit_expr(hi_expr),
5671 lo_val
5672 )
5673 }
5674 None => format!("substr({}, {})", e, start),
5675 }
5676 }
5677 }
5678
5679 IrExpr::JsonbField { expr, field } => {
5680 format!("({}->{})", emit_expr(expr), sql_str(field))
5681 }
5682
5683 IrExpr::CompositeField { expr, field, .. } => {
5686 format!("({}).{}", emit_expr(expr), qi(field))
5687 }
5688
5689 IrExpr::JsonbIndex { expr, index } => {
5690 format!("({}->{})", emit_expr(expr), index)
5691 }
5692
5693 IrExpr::FnParam { name, .. } => qi(name),
5694
5695 IrExpr::FnSubquery(fs) => {
5696 let scalar = match fs.shape.as_slice() {
5697 [IrShapePointer::Computed(c)] => emit_expr(&c.expr),
5698 _ => unreachable!("a function scalar subquery always projects exactly one computed pointer"),
5699 };
5700 let args_sql = fs.fn_args.iter().map(emit_expr).collect::<Vec<_>>().join(", ");
5701 let mut sql = format!(
5702 "(SELECT {}{}\nFROM {}.{}({}) AS {}",
5703 if fs.distinct { "DISTINCT " } else { "" },
5704 scalar,
5705 pg_schema(&fs.fn_module),
5706 qi(&fs.fn_name),
5707 args_sql,
5708 qi(&fs.alias),
5709 );
5710 append_filter(&mut sql, &fs.filter);
5711 append_order_by(&mut sql, &fs.order_by);
5712 append_offset_limit(&mut sql, &fs.offset, &fs.limit);
5713 sql.push(')');
5714 sql
5715 }
5716
5717 IrExpr::PathSubquery(ps) => {
5718 let scalar = match &ps.result {
5719 IrPathResult::Scalar(e, _) => emit_expr(e),
5720 IrPathResult::Object { alias, .. } => format!("{}.\"id\"", qi(alias)),
5721 };
5722 let from_sql = emit_path_joins(&ps.root, &ps.joins);
5723 let mut sql = format!(
5724 "(SELECT {}{}\nFROM {}",
5725 if ps.distinct { "DISTINCT " } else { "" },
5726 scalar,
5727 from_sql
5728 );
5729 append_filter(&mut sql, &ps.filter);
5730 append_order_by(&mut sql, &ps.order_by);
5731 append_offset_limit(&mut sql, &ps.offset, &ps.limit);
5732 sql.push(')');
5733 sql
5734 }
5735
5736 IrExpr::ObjectPathUnion { branches, limit, multi } => {
5737 let arms: Vec<String> = branches
5738 .iter()
5739 .map(|ps| {
5740 let IrPathResult::Object {
5741 alias,
5742 type_name,
5743 shape,
5744 } = &ps.result
5745 else {
5746 unreachable!("an object path union's branches always land on objects")
5747 };
5748 let (pointer_exprs, _) = build_shape(shape, alias);
5749 let mut parts = vec![result_type_disc(ps, alias, type_name)];
5750 parts.extend(pointer_exprs);
5751 let mut sql = format!(
5752 "SELECT (\n {}\n) AS \"r\"\nFROM {}",
5753 parts.join(",\n "),
5754 emit_path_joins(&ps.root, &ps.joins),
5755 );
5756 append_filter(&mut sql, &ps.filter);
5757 sql
5758 })
5759 .collect();
5760 let mut sql = format!(
5761 "{}(SELECT \"r\" FROM (\n{}\n) AS \"_u\"",
5762 if *multi { "ARRAY" } else { "" },
5763 arms.join("\nUNION ALL\n")
5764 );
5765 if let Some(limit) = limit {
5766 sql.push_str(&format!("\nLIMIT {}", emit_expr(limit)));
5767 }
5768 sql.push(')');
5769 sql
5770 }
5771
5772 IrExpr::ObjectPathSubquery(ps) => {
5773 let IrPathResult::Object {
5774 alias,
5775 type_name,
5776 shape,
5777 } = &ps.result
5778 else {
5779 unreachable!("an object path subquery always lands on an object")
5780 };
5781 let (pointer_exprs, _) = build_shape(shape, alias);
5782 let mut parts = vec![result_type_disc(ps, alias, type_name)];
5783 parts.extend(pointer_exprs);
5784 let mut sql = format!(
5785 "(SELECT (\n {}\n)\nFROM {}",
5786 parts.join(",\n "),
5787 emit_path_joins(&ps.root, &ps.joins),
5788 );
5789 append_filter(&mut sql, &ps.filter);
5790 append_order_by(&mut sql, &ps.order_by);
5791 append_offset_limit(&mut sql, &ps.offset, &ps.limit);
5792 sql.push(')');
5793 sql
5794 }
5795
5796 IrExpr::ObjectSubquery(sel) => {
5797 let [IrRowSource::Bound { source, shape }] = sel.rows.as_slice() else {
5798 unreachable!("an object subquery is always schema-bound")
5799 };
5800 let alias = &source.alias;
5801 let (sub_exprs, _) = build_shape(shape, alias);
5802 let mut row_parts = vec![source_type_disc(source)];
5803 row_parts.extend(sub_exprs);
5804 let mut sql = format!(
5805 "(SELECT (\n {}\n )\n FROM {} AS {}",
5806 row_parts.join(",\n "),
5807 source_ref(source),
5808 qi(alias),
5809 );
5810 append_filter(&mut sql, &sel.filter);
5811 append_order_by(&mut sql, &sel.order_by);
5812 append_offset_limit(&mut sql, &sel.offset, &sel.limit);
5813 sql.push(')');
5814 sql
5815 }
5816
5817 IrExpr::Subquery(sel) => {
5818 let [IrRowSource::Bound { source, shape }] = sel.rows.as_slice() else {
5819 unreachable!("scalar/exists subquery is always schema-bound")
5820 };
5821 let alias = &source.alias;
5822 let mut sql = if shape.is_empty() {
5823 format!("(SELECT 1\nFROM {} AS {}", source_ref(source), qi(alias))
5825 } else if let Some(c) = shape.iter().find_map(|f| {
5826 if let IrShapePointer::Computed(c) = f {
5827 Some(c)
5828 } else {
5829 None
5830 }
5831 }) {
5832 format!(
5837 "(SELECT {}\nFROM {} AS {}",
5838 emit_expr(&c.expr),
5839 source_ref(source),
5840 qi(alias)
5841 )
5842 } else {
5843 let pk_col = shape
5845 .iter()
5846 .find_map(|f| {
5847 if let IrShapePointer::Scalar(s) = f {
5848 Some(s.column.as_str())
5849 } else {
5850 None
5851 }
5852 })
5853 .unwrap_or("id");
5854 format!(
5855 "(SELECT {}.{}\nFROM {} AS {}",
5856 qi(alias),
5857 qi(pk_col),
5858 source_ref(source),
5859 qi(alias),
5860 )
5861 };
5862 append_filter(&mut sql, &sel.filter);
5863 append_order_by(&mut sql, &sel.order_by);
5864 append_offset_limit(&mut sql, &sel.offset, &sel.limit);
5865 sql.push(')');
5866 sql
5867 }
5868 IrExpr::RawSql(s) => format!("({s})"),
5869 }
5870}
5871
5872fn emit_vector_search(vs: &IrVectorSearch) -> SqlOutput {
5875 let alias = &vs.source.alias;
5876 let dist_sql = format!(
5877 "{}.{} {} {}",
5878 qi(alias),
5879 qi(&vs.vector_col),
5880 vs.distance_op,
5881 emit_expr(&vs.query_expr),
5882 );
5883
5884 let (obj_tuple, object_shape_nodes) = if vs.object_shape.is_empty() {
5889 let type_expr = type_disc(&vs.source.type_name);
5891 let id_expr = format!("{}.\"id\"", qi(alias));
5892 let tuple = format!("{},\n {}", type_expr, id_expr);
5893 let id_node = ShapeNode::Scalar {
5894 name: "id".to_string(),
5895 position: 1,
5896 };
5897 (tuple, vec![id_node])
5898 } else {
5899 let (pointer_exprs, shape_pointers) = build_shape(&vs.object_shape, alias);
5900 let mut parts = vec![type_disc(&vs.source.type_name)];
5901 parts.extend(pointer_exprs);
5902 (parts.join(",\n "), prepend_type(shape_pointers))
5903 };
5904
5905 let outer = format!(
5907 "NULL::text,\n ROW(\n {}\n )::record,\n {}",
5908 obj_tuple, dist_sql,
5909 );
5910 let mut sql = format!(
5911 "SELECT (\n {}\n) AS result\nFROM {} AS {}",
5912 outer,
5913 source_ref(&vs.source),
5914 qi(alias),
5915 );
5916 append_filter(&mut sql, &vs.filter);
5917
5918 if let Some(dir) = &vs.order_by_distance {
5920 let dir_sql = match dir {
5921 IrSortDir::Asc => "ASC",
5922 IrSortDir::Desc => "DESC",
5923 };
5924 sql.push_str(&format!("\nORDER BY {} {}", dist_sql, dir_sql));
5925 }
5926 append_offset_limit(&mut sql, &vs.offset, &vs.limit);
5927
5928 let object_node = ShapeNode::Object {
5929 name: "object".to_string(),
5930 type_name: Some(vs.source.type_name.clone()),
5931 position: 1,
5932 cardinality: Cardinality::Many,
5933 pointers: object_shape_nodes,
5934 has_implicit_id: shape_has_implicit_id(&vs.object_shape),
5935 };
5936 let shape = ShapeDescriptor {
5937 root: ShapeNode::VectorSearch {
5938 object_position: 1,
5939 distance_position: 2,
5940 object_node: Box::new(object_node),
5941 },
5942 };
5943 let inference_plan = vs.inference_model.as_ref().map(|model_name| InferencePlan::Embedding {
5944 model_name: model_name.clone(),
5945 type_name: vs.inference_type_name.clone().unwrap_or_default(),
5946 index_name: vs.inference_index_name.clone().unwrap_or(None),
5947 query_param_name: vs.inference_query_param_name.clone().unwrap_or_default(),
5948 query_literal: vs.inference_query_literal.clone(),
5949 });
5950 SqlOutput {
5951 sql,
5952 shape,
5953 inference_plan,
5954 }
5955}
5956
5957fn emit_fts_search(fs: &IrFtsSearch) -> SqlOutput {
5960 use crate::schema::SearchBackend;
5961 if fs.backend != SearchBackend::Postgres {
5962 return emit_fts_search_deferred(fs);
5963 }
5964
5965 let alias = &fs.source.alias;
5966 let search_col = format!("{}.{}", qi(alias), qi(&fs.search_col));
5967 let query_sql = emit_expr(&fs.query_expr);
5968 let tsquery = format!("{}('english', {})", fs.tsquery_fn, query_sql);
5969 let rank_sql = format!("ts_rank({}, {})", search_col, tsquery);
5970
5971 let (obj_tuple, object_shape_nodes) = if fs.object_shape.is_empty() {
5972 let type_expr = type_disc(&fs.source.type_name);
5973 let id_expr = format!("{}.\"id\"", qi(alias));
5974 let tuple = format!("{},\n {}", type_expr, id_expr);
5975 let id_node = ShapeNode::Scalar {
5976 name: "id".to_string(),
5977 position: 1,
5978 };
5979 (tuple, vec![id_node])
5980 } else {
5981 let (pointer_exprs, shape_pointers) = build_shape(&fs.object_shape, alias);
5982 let mut parts = vec![type_disc(&fs.source.type_name)];
5983 parts.extend(pointer_exprs);
5984 (parts.join(",\n "), prepend_type(shape_pointers))
5985 };
5986
5987 let outer = format!(
5988 "NULL::text,\n ROW(\n {}\n )::record,\n {}",
5989 obj_tuple, rank_sql,
5990 );
5991 let mut sql = format!(
5992 "SELECT (\n {}\n) AS result\nFROM {} AS {}\nWHERE {} @@ {}",
5993 outer,
5994 source_ref(&fs.source),
5995 qi(alias),
5996 search_col,
5997 tsquery,
5998 );
5999 if let Some(f) = &fs.filter {
6000 sql.push_str(&format!(" AND ({})", emit_expr(f)));
6001 }
6002 if let Some(dir) = &fs.order_by_rank {
6003 let dir_sql = match dir {
6004 IrSortDir::Asc => "ASC",
6005 IrSortDir::Desc => "DESC",
6006 };
6007 sql.push_str(&format!("\nORDER BY {} {}", rank_sql, dir_sql));
6008 }
6009 append_offset_limit(&mut sql, &fs.offset, &fs.limit);
6010
6011 let object_node = ShapeNode::Object {
6012 name: "object".to_string(),
6013 type_name: Some(fs.source.type_name.clone()),
6014 position: 1,
6015 cardinality: Cardinality::Many,
6016 pointers: object_shape_nodes,
6017 has_implicit_id: shape_has_implicit_id(&fs.object_shape),
6018 };
6019 let shape = ShapeDescriptor {
6020 root: ShapeNode::FtsSearch {
6021 object_position: 1,
6022 rank_position: 2,
6023 object_node: Box::new(object_node),
6024 },
6025 };
6026 SqlOutput {
6027 sql,
6028 shape,
6029 inference_plan: None,
6030 }
6031}
6032
6033fn emit_fts_search_deferred(fs: &IrFtsSearch) -> SqlOutput {
6034 let alias = &fs.source.alias;
6035 let ids_idx = fs
6036 .deferred_ids_param
6037 .expect("deferred_ids_param must be set for deferred backend");
6038 let scores_idx = fs
6039 .deferred_scores_param
6040 .expect("deferred_scores_param must be set for deferred backend");
6041 let ids_param = format!("${}", ids_idx + 1);
6042 let scores_param = format!("${}", scores_idx + 1);
6043
6044 let (obj_tuple, object_shape_nodes) = if fs.object_shape.is_empty() {
6045 let type_expr = type_disc(&fs.source.type_name);
6046 let id_expr = format!("{}.\"id\"", qi(alias));
6047 let tuple = format!("{},\n {}", type_expr, id_expr);
6048 let id_node = ShapeNode::Scalar {
6049 name: "id".to_string(),
6050 position: 1,
6051 };
6052 (tuple, vec![id_node])
6053 } else {
6054 let (pointer_exprs, shape_pointers) = build_shape(&fs.object_shape, alias);
6055 let mut parts = vec![type_disc(&fs.source.type_name)];
6056 parts.extend(pointer_exprs);
6057 (parts.join(",\n "), prepend_type(shape_pointers))
6058 };
6059
6060 let outer = format!(
6061 "NULL::text,\n ROW(\n {}\n )::record,\n \"_os\".\"score\"",
6062 obj_tuple,
6063 );
6064 let mut sql = format!(
6065 concat!(
6066 "SELECT (\n {}\n) AS result\n",
6067 "FROM {} AS {}\n",
6068 "JOIN UNNEST({}::uuid[], {}::float8[]) AS \"_os\"(\"id\", \"score\")\n",
6069 " ON \"_os\".\"id\" = {}.\"id\"",
6070 ),
6071 outer,
6072 source_ref(&fs.source),
6073 qi(alias),
6074 ids_param,
6075 scores_param,
6076 qi(alias),
6077 );
6078 if let Some(f) = &fs.filter {
6079 sql.push_str(&format!("\nWHERE ({})", emit_expr(f)));
6080 }
6081 if let Some(dir) = &fs.order_by_rank {
6082 let dir_sql = match dir {
6083 IrSortDir::Asc => "ASC",
6084 IrSortDir::Desc => "DESC",
6085 };
6086 sql.push_str(&format!("\nORDER BY \"_os\".\"score\" {}", dir_sql));
6087 }
6088 let size = fs.limit.as_ref().and_then(|lim| {
6090 if let IrExpr::Literal(IrLiteral::Int(n)) = lim {
6091 Some(*n as usize)
6092 } else {
6093 None
6094 }
6095 });
6096
6097 let object_node = ShapeNode::Object {
6098 name: "object".to_string(),
6099 type_name: Some(fs.source.type_name.clone()),
6100 position: 1,
6101 cardinality: Cardinality::Many,
6102 pointers: object_shape_nodes,
6103 has_implicit_id: shape_has_implicit_id(&fs.object_shape),
6104 };
6105 let shape = ShapeDescriptor {
6106 root: ShapeNode::FtsSearch {
6107 object_position: 1,
6108 rank_position: 2,
6109 object_node: Box::new(object_node),
6110 },
6111 };
6112 let backend_str = match fs.backend {
6113 crate::schema::SearchBackend::Meilisearch => "meilisearch",
6114 _ => "opensearch",
6115 };
6116 let inference_plan = Some(InferencePlan::Search {
6117 backend: backend_str.to_string(),
6118 index_name: fs.deferred_index_name.clone().unwrap_or_default(),
6119 query_param_name: fs.deferred_query_param_name.clone().unwrap_or_default(),
6120 query_literal: fs.deferred_query_literal.clone(),
6121 size,
6122 });
6123 SqlOutput {
6124 sql,
6125 shape,
6126 inference_plan,
6127 }
6128}
6129
6130fn emit_function_select(sel: &IrFunctionSelect) -> SqlOutput {
6133 let alias = &sel.alias;
6134 let (pointer_exprs, shape_pointers) = build_shape(&sel.shape, alias);
6135
6136 let type_expr = if sel.polymorphic {
6137 format!("{}.\"__type__\"", qi(alias))
6138 } else {
6139 type_disc(&sel.type_name)
6140 };
6141 let mut parts = vec![type_expr];
6142 parts.extend(pointer_exprs);
6143 let tuple = parts.join(",\n ");
6144 let distinct = if sel.distinct { "DISTINCT " } else { "" };
6145
6146 let args_sql = sel.fn_args.iter().map(emit_expr).collect::<Vec<_>>().join(", ");
6147 let fn_call = format!("{}.{}({})", pg_schema(&sel.fn_module), qi(&sel.fn_name), args_sql);
6148
6149 let from_clause = if sel.polymorphic {
6150 format!("{} AS {}", fn_call, qi(alias))
6153 } else {
6154 format!("{} AS {}", fn_call, qi(alias))
6155 };
6156
6157 let mut sql = format!("SELECT {}(\n {}\n) AS result\nFROM {}", distinct, tuple, from_clause,);
6158 append_filter(&mut sql, &sel.filter);
6159 append_order_by(&mut sql, &sel.order_by);
6160 append_offset_limit(&mut sql, &sel.offset, &sel.limit);
6161
6162 let root_pointers = prepend_type(shape_pointers);
6163 SqlOutput {
6164 sql,
6165 shape: ShapeDescriptor {
6166 root: ShapeNode::Object {
6167 name: String::new(),
6168 type_name: Some(sel.type_name.clone()),
6169 position: 0,
6170 cardinality: Cardinality::Many,
6171 pointers: root_pointers,
6172 has_implicit_id: shape_has_implicit_id(&sel.shape),
6173 },
6174 },
6175 inference_plan: None,
6176 }
6177}
6178
6179pub fn emit_fn_body(ir: &crate::ir::IrOutput) -> String {
6185 with_subtype_fanouts(&ir.subtype_fanouts, || emit_output_fn_body(ir))
6186}
6187
6188fn emit_output_fn_body(ir: &crate::ir::IrOutput) -> String {
6189 let body = match &ir.stmt {
6190 IrStmt::Select(sel) if matches!(sel.rows.as_slice(), [IrRowSource::Free(IrFreeExpr::Scalar(_))]) => {
6191 let IrRowSource::Free(IrFreeExpr::Scalar(e)) = &sel.rows[0] else {
6192 unreachable!()
6193 };
6194 format!("SELECT {}", emit_expr(e))
6195 }
6196 other => emit_dml_as_cte_source(other),
6197 };
6198 if ir.ctes.is_empty() {
6199 body
6200 } else {
6201 let cte_prefix = emit_cte_prefix(&ir.ctes);
6202 format!("{}{}", cte_prefix, body)
6203 }
6204}
6205
6206fn emit_literal(lit: &IrLiteral) -> String {
6207 match lit {
6208 IrLiteral::Str(s) => sql_str(s),
6209 IrLiteral::Int(i) => i.to_string(),
6210 IrLiteral::Float(f) => {
6211 let s = f.to_string();
6220 let s = if s.contains('.') || s.contains('e') {
6221 s
6222 } else {
6223 format!("{}.0", s)
6224 };
6225 format!("({}::float8)", s)
6226 }
6227 IrLiteral::Bool(b) => {
6228 if *b {
6229 "TRUE".into()
6230 } else {
6231 "FALSE".into()
6232 }
6233 }
6234 }
6235}
6236
6237#[cfg(test)]
6240mod tests {
6241 use super::*;
6242 use crate::ir;
6243 use crate::parse;
6244 use crate::schema::{
6245 FunctionDescriptor, FunctionParamDescriptor, GlobalDescriptor, LinkDescriptor, MultiLinkDescriptor,
6246 NamedTupleDescriptor, PropertyDescriptor, SchemaDescriptor, TypeDescriptor,
6247 };
6248
6249 #[test]
6250 fn a_template_argument_keeps_the_parameters_it_carries() {
6251 let filled = fill_template("f($1, $2, $10)", &["($2)::int".to_string(), "b".to_string()]);
6254 assert_eq!(filled, "f(($2)::int, b, $10)");
6255 }
6256
6257 fn make_schema() -> SchemaDescriptor {
6258 SchemaDescriptor {
6259 types: vec![
6260 TypeDescriptor {
6261 name: "Person".into(),
6262 module: "default".into(),
6263 table: "Person".into(),
6264 abstract_: false,
6265 materialized: false,
6266 description: None,
6267 parents: vec![],
6268 interfaces: vec![],
6269 bases: vec![],
6270 properties: vec![
6271 PropertyDescriptor {
6272 name: "id".into(),
6273 pg_type: "uuid".into(),
6274 nullable: false,
6275 default_sql: Some("uuidv7()".into()),
6276 default_pyql: None,
6277 description: None,
6278 check_constraints: vec![],
6279 is_exclusive: true,
6280 is_pk: true,
6281 is_readonly: true,
6282 rewrites: vec![],
6283 tuple_members: None,
6284 column_type: None,
6285 },
6286 PropertyDescriptor {
6287 name: "name".into(),
6288 pg_type: "text".into(),
6289 nullable: false,
6290 default_sql: None,
6291 default_pyql: None,
6292 description: None,
6293 check_constraints: vec![],
6294 is_exclusive: false,
6295 is_pk: false,
6296 is_readonly: false,
6297 rewrites: vec![],
6298 tuple_members: None,
6299 column_type: None,
6300 },
6301 PropertyDescriptor {
6302 name: "age".into(),
6303 pg_type: "int8".into(),
6304 nullable: true,
6305 default_sql: None,
6306 default_pyql: None,
6307 description: None,
6308 check_constraints: vec![],
6309 is_exclusive: false,
6310 is_pk: false,
6311 is_readonly: false,
6312 rewrites: vec![],
6313 tuple_members: None,
6314 column_type: None,
6315 },
6316 ],
6317 links: vec![LinkDescriptor {
6318 name: "company".into(),
6319 target: "default::Company".into(),
6320 nullable: true,
6321 through: None,
6322 description: None,
6323 default_pyql: None,
6324 is_exclusive: false,
6325 is_readonly: false,
6326 rewrites: vec![],
6327 on_delete: vec![],
6328 }],
6329 multilinks: vec![MultiLinkDescriptor {
6330 name: "posts".into(),
6331 target: "default::Post".into(),
6332 through: None,
6333 nullable: false,
6334 description: None,
6335 default_pyql: None,
6336 on_delete: vec![],
6337 is_exclusive: false,
6338 }],
6339 computed: vec![],
6340 constraints: vec![],
6341 indexes: vec![],
6342 partition: None,
6343 vector_indexes: vec![],
6344 search_indexes: vec![],
6345 triggers: vec![],
6346 junction: false,
6347 signals: vec![],
6348 },
6349 TypeDescriptor {
6350 name: "Company".into(),
6351 module: "default".into(),
6352 table: "Company".into(),
6353 abstract_: false,
6354 materialized: false,
6355 description: None,
6356 parents: vec![],
6357 interfaces: vec![],
6358 bases: vec![],
6359 properties: vec![PropertyDescriptor {
6360 name: "name".into(),
6361 pg_type: "text".into(),
6362 nullable: false,
6363 default_sql: None,
6364 default_pyql: None,
6365 description: None,
6366 check_constraints: vec![],
6367 is_exclusive: false,
6368 is_pk: false,
6369 is_readonly: false,
6370 rewrites: vec![],
6371 tuple_members: None,
6372 column_type: None,
6373 }],
6374 links: vec![],
6375 multilinks: vec![],
6376 computed: vec![],
6377 constraints: vec![],
6378 indexes: vec![],
6379 partition: None,
6380 vector_indexes: vec![],
6381 search_indexes: vec![],
6382 triggers: vec![],
6383 junction: false,
6384 signals: vec![],
6385 },
6386 TypeDescriptor {
6387 name: "Post".into(),
6388 module: "default".into(),
6389 table: "Post".into(),
6390 abstract_: false,
6391 materialized: false,
6392 description: None,
6393 parents: vec![],
6394 interfaces: vec![],
6395 bases: vec![],
6396 properties: vec![PropertyDescriptor {
6397 name: "title".into(),
6398 pg_type: "text".into(),
6399 nullable: false,
6400 default_sql: None,
6401 default_pyql: None,
6402 description: None,
6403 check_constraints: vec![],
6404 is_exclusive: false,
6405 is_pk: false,
6406 is_readonly: false,
6407 rewrites: vec![],
6408 tuple_members: None,
6409 column_type: None,
6410 }],
6411 links: vec![],
6412 multilinks: vec![],
6413 computed: vec![],
6414 constraints: vec![],
6415 indexes: vec![],
6416 partition: None,
6417 vector_indexes: vec![],
6418 search_indexes: vec![],
6419 triggers: vec![],
6420 junction: false,
6421 signals: vec![],
6422 },
6423 ],
6424 scalars: vec![],
6425 enums: vec![],
6426 named_tuples: vec![],
6427 globals: vec![],
6428 functions: vec![],
6429 aliases: vec![],
6430 channels: vec![],
6431 ..Default::default()
6432 }
6433 }
6434
6435 fn compile_and_emit(query: &str) -> SqlOutput {
6436 let schema = make_schema();
6437 compile_and_emit_with(query, &schema)
6438 }
6439
6440 fn compile_and_emit_with(query: &str, schema: &SchemaDescriptor) -> SqlOutput {
6441 let ast = parse::parse(query).expect("parse failed");
6442 let ir = ir::compile(&ast, schema).expect("IR compile failed");
6443 emit(&ir)
6444 }
6445
6446 fn compile_err_with(query: &str, schema: &SchemaDescriptor) -> String {
6448 let ast = parse::parse(query).expect("parse failed");
6449 match ir::compile(&ast, schema) {
6450 Ok(_) => panic!("expected this to be refused: {query}"),
6451 Err(e) => e.to_string(),
6452 }
6453 }
6454
6455 #[test]
6460 fn a_multilink_walk_can_be_tested_against_a_set_literal() {
6461 let out = compile_and_emit("SELECT Person FILTER .posts.title IN {'a', 'b'}");
6462 assert!(out.sql.contains("= ANY(ARRAY['a', 'b'])"), "{}", out.sql);
6463 }
6464
6465 #[test]
6469 fn all_over_a_multilink_is_not_the_same_question_as_any() {
6470 for condition in [
6471 ".posts.title IN {'a', 'b'}",
6472 ".posts IN ps",
6473 ".posts = ps",
6474 ".posts.title = 'x'",
6475 ] {
6476 let sql = |quantifier: &str| {
6477 compile_and_emit(&format!(
6478 "WITH ps := (SELECT Post) SELECT Person FILTER {quantifier}({condition})"
6479 ))
6480 .sql
6481 };
6482 assert_ne!(
6483 sql("all"),
6484 sql("any"),
6485 "all({condition}) must not compile to what any({condition}) does"
6486 );
6487 }
6488 }
6489
6490 #[test]
6494 fn all_over_a_multilink_answers_for_every_element() {
6495 let out = compile_and_emit("SELECT Person FILTER all(.posts.title IN {'a', 'b'})");
6496 assert!(
6497 out.sql.contains("NOT EXISTS(") && out.sql.contains("NOT (\"t2\".\"title\" = ANY("),
6498 "no element fails the test:\n{}",
6499 out.sql
6500 );
6501 let out = compile_and_emit("WITH ps := (SELECT Post) SELECT Person FILTER all(.posts IN ps)");
6502 assert!(
6503 out.sql
6504 .contains("coalesce(bool_and(\"_s\".\"v\"), true) FROM unnest(ARRAY(SELECT ("),
6505 "every answer, reduced:\n{}",
6506 out.sql
6507 );
6508 }
6509
6510 #[test]
6513 fn any_over_a_multilink_keeps_its_exists() {
6514 let out = compile_and_emit("WITH ps := (SELECT Post) SELECT Person FILTER any(.posts IN ps)");
6515 assert!(out.sql.contains("WHERE EXISTS("), "{}", out.sql);
6516 assert!(!out.sql.contains("bool_or"), "{}", out.sql);
6517 }
6518
6519 #[test]
6525 fn comparing_a_multilink_to_a_multi_row_binding_is_membership() {
6526 let out = compile_and_emit("WITH ps := (SELECT Post) SELECT Person FILTER .posts = ps");
6527 assert!(
6528 out.sql.contains("\"target\" = ANY((SELECT \"id\" FROM \"ps\"))"),
6529 "{}",
6530 out.sql
6531 );
6532 let out = compile_and_emit("WITH ps := (SELECT Post) SELECT Person FILTER .posts != ps");
6533 assert!(
6534 out.sql
6535 .contains("NOT (\"t2\".\"target\" = ANY((SELECT \"id\" FROM \"ps\")))"),
6536 "{}",
6537 out.sql
6538 );
6539 }
6540
6541 #[test]
6543 fn comparing_a_multilink_to_a_single_row_binding_stays_an_equality() {
6544 let out = compile_and_emit("WITH p := (SELECT Post LIMIT 1) SELECT Person FILTER .posts = p");
6545 assert!(
6546 out.sql.contains("\"target\" = (SELECT \"id\" FROM \"p\")"),
6547 "{}",
6548 out.sql
6549 );
6550 }
6551
6552 #[test]
6558 fn membership_of_one_set_in_another_answers_per_element() {
6559 let out = compile_and_emit("WITH a := (SELECT Person), b := (SELECT Person) SELECT a IN b");
6560 assert!(
6561 out.sql.contains("FROM \"a\""),
6562 "the left set is the source:\n{}",
6563 out.sql
6564 );
6565 assert!(
6566 out.sql.contains("\"id\" = ANY((SELECT \"id\" FROM \"b\"))"),
6567 "each row tests its own id:\n{}",
6568 out.sql
6569 );
6570 assert!(
6571 !out.sql.contains("(SELECT \"id\" FROM \"a\") = ANY"),
6572 "the left set must not collapse to one value:\n{}",
6573 out.sql
6574 );
6575 }
6576
6577 #[test]
6579 fn membership_of_a_type_in_a_set_answers_per_row() {
6580 let out = compile_and_emit("WITH b := (SELECT Person) SELECT Person NOT IN b");
6581 assert!(
6582 out.sql.contains("FROM \"public\".\"Person\""),
6583 "the type is the source:\n{}",
6584 out.sql
6585 );
6586 assert!(
6587 out.sql.contains("\"id\" <> ALL((SELECT \"id\" FROM \"b\"))"),
6588 "{}",
6589 out.sql
6590 );
6591 }
6592
6593 #[test]
6597 fn quantifying_membership_of_one_set_in_another_aggregates_the_elements() {
6598 for (query, aggregate) in [("SELECT all(a IN b)", "bool_and"), ("SELECT any(a IN b)", "bool_or")] {
6599 let out = compile_and_emit(&format!("WITH a := (SELECT Person), b := (SELECT Person) {query}"));
6600 assert!(
6601 out.sql.contains(&format!("{aggregate}(\"_s\".\"v\")")),
6602 "{query}:\n{}",
6603 out.sql
6604 );
6605 assert!(
6606 out.sql.contains("FROM unnest(ARRAY(SELECT ("),
6607 "one answer per element, then reduced — {query}:\n{}",
6608 out.sql
6609 );
6610 assert!(
6611 !out.sql.contains("(SELECT \"id\" FROM \"a\") = ANY"),
6612 "the left set must not collapse to one value — {query}:\n{}",
6613 out.sql
6614 );
6615 }
6616 }
6617
6618 #[test]
6622 fn membership_of_a_multilink_is_a_boolean_per_element() {
6623 let out = compile_and_emit("WITH ps := (SELECT Post) SELECT Person { t := .posts IN ps }");
6624 assert!(
6625 out.sql
6626 .contains("ARRAY(SELECT (\"t3\".\"id\" = ANY((SELECT \"id\" FROM \"ps\")))"),
6627 "one answer per post:\n{}",
6628 out.sql
6629 );
6630 assert!(
6631 !out.sql.contains("EXISTS("),
6632 "not one answer for the link:\n{}",
6633 out.sql
6634 );
6635 }
6636
6637 #[test]
6640 fn membership_of_a_multilink_in_a_filter_stays_one_answer() {
6641 let out = compile_and_emit("WITH ps := (SELECT Post) SELECT Person FILTER .posts IN ps");
6642 assert!(out.sql.contains("WHERE EXISTS("), "{}", out.sql);
6643 }
6644
6645 #[test]
6649 fn quantifying_membership_of_a_multilink_aggregates_its_elements() {
6650 let out = compile_and_emit("WITH ps := (SELECT Post) SELECT Person { t := all(.posts IN ps) }");
6651 assert!(
6652 out.sql
6653 .contains("coalesce(bool_and(\"_s\".\"v\"), true) FROM unnest(ARRAY(SELECT ("),
6654 "{}",
6655 out.sql
6656 );
6657 let out = compile_and_emit("WITH ps := (SELECT Post) SELECT Person { t := any(.posts IN ps) }");
6658 assert!(
6659 out.sql
6660 .contains("coalesce(bool_or(\"_s\".\"v\"), false) FROM unnest(ARRAY(SELECT ("),
6661 "{}",
6662 out.sql
6663 );
6664 }
6665
6666 #[test]
6669 fn membership_in_a_set_literal_answers_per_element() {
6670 let out = compile_and_emit("SELECT Person { t := .posts.title IN {'a', 'b'} }");
6671 assert!(
6672 out.sql
6673 .contains("ARRAY(SELECT (\"t2\".\"title\" = ANY(ARRAY['a', 'b']))"),
6674 "{}",
6675 out.sql
6676 );
6677 }
6678
6679 #[test]
6687 fn test_a_mutating_loop_as_a_select_source_is_bound_to_a_cte() {
6688 let out =
6689 compile_and_emit("SELECT (FOR p IN (SELECT Person) UNION (INSERT Post { title := p.name })) { title }");
6690 assert!(
6691 !out.sql.contains("LATERAL"),
6692 "the insert must not sit in a LATERAL, got:\n{}",
6693 out.sql
6694 );
6695 assert!(
6696 out.sql
6697 .contains("\"_nested_dml_0__ids\" AS (\nINSERT INTO \"public\".\"Post\""),
6698 "the loop's insert belongs in a top-level CTE, got:\n{}",
6699 out.sql
6700 );
6701 assert!(
6702 out.sql.contains("FROM \"_nested_dml_0\" AS "),
6703 "the select must read the bound loop back, got:\n{}",
6704 out.sql
6705 );
6706 }
6707
6708 #[test]
6712 fn test_a_loop_updating_its_own_variable_compiles_as_a_select_source() {
6713 let out = compile_and_emit("SELECT (FOR p IN (SELECT Person) UNION (UPDATE p SET { age := 1 })) { name }");
6714 assert!(
6715 out.sql
6716 .contains("\"_nested_dml_0__ids\" AS (\nUPDATE \"public\".\"Person\""),
6717 "got:\n{}",
6718 out.sql
6719 );
6720 assert!(!out.sql.contains("LATERAL"), "got:\n{}", out.sql);
6721 }
6722
6723 #[test]
6732 fn test_a_guarded_insert_reaches_a_binding_inside_a_nested_loop() {
6733 let out = compile_and_emit(
6734 "WITH existing := (SELECT Company FILTER .name = 'x'), \
6735 made := (INSERT Person { name := 'a', \
6736 posts := (FOR s IN {'p','q'} UNION ( \
6737 WITH extra := (INSERT Company { name := s }) \
6738 INSERT Post { title := s })) }) \
6739 IF NOT EXISTS existing ELSE {} \
6740 SELECT made",
6741 );
6742 let at = out
6743 .sql
6744 .find("INSERT INTO \"public\".\"Company\"")
6745 .unwrap_or_else(|| panic!("no Company insert in:\n{}", out.sql));
6746 let tail = &out.sql[at..];
6747 let stmt_end = tail.find("RETURNING").unwrap_or(tail.len());
6748 assert!(
6749 tail[..stmt_end].contains("NOT EXISTS(SELECT 1 FROM \"existing\")"),
6750 "a binding inside the loop must carry the condition too, got:\n{}",
6751 out.sql
6752 );
6753 }
6754
6755 #[test]
6760 fn test_a_guarded_insert_carries_its_condition_into_what_it_nests() {
6761 let out = compile_and_emit(
6762 "WITH existing := (SELECT Company FILTER .name = 'x'), \
6763 made := (INSERT Person { name := 'a', company := (INSERT Company { name := 'b' }), \
6764 posts := (FOR s IN {'p','q'} UNION (INSERT Post { title := s })) }) \
6765 IF NOT EXISTS existing ELSE {} \
6766 SELECT made",
6767 );
6768 for table in ["\"Person\"", "\"Company\" (", "\"Post\""] {
6769 let at = out
6770 .sql
6771 .find(&format!("INSERT INTO \"public\".{}", table))
6772 .unwrap_or_else(|| panic!("no insert into {} in:\n{}", table, out.sql));
6773 let tail = &out.sql[at..];
6774 let stmt_end = tail.find("RETURNING").unwrap_or(tail.len());
6775 assert!(
6776 tail[..stmt_end].contains("NOT EXISTS(SELECT 1 FROM \"existing\")"),
6777 "insert into {} writes whatever the condition says, got:\n{}",
6778 table,
6779 out.sql
6780 );
6781 }
6782 }
6783
6784 #[test]
6785 fn test_a_guarded_insert_keeps_its_guard_when_it_nests_another() {
6786 let out = compile_and_emit(
6787 "WITH existing := (SELECT Company FILTER .name = 'x'), \
6788 made := (INSERT Person { name := 'a', company := (INSERT Company { name := 'b' }) }) \
6789 IF NOT EXISTS existing ELSE {} \
6790 SELECT made",
6791 );
6792 let outer = out
6793 .sql
6794 .find("INSERT INTO \"public\".\"Person\"")
6795 .unwrap_or_else(|| panic!("no Person insert in:\n{}", out.sql));
6796 let tail = &out.sql[outer..];
6797 let stmt_end = tail.find("RETURNING").unwrap_or(tail.len());
6798 assert!(
6799 tail[..stmt_end].contains("NOT EXISTS(SELECT 1 FROM \"existing\")"),
6800 "the insert the conditional names must carry the condition, got:\n{}",
6801 out.sql
6802 );
6803 }
6804
6805 #[test]
6806 fn a_nested_read_inside_a_with_opens_no_second_with() {
6807 let out = compile_and_emit_with(
6808 "WITH a := (SELECT Person FILTER .age > 1), b := (SELECT Person LIMIT 1) SELECT (SELECT (a UNION b))",
6809 &make_schema(),
6810 );
6811 assert_eq!(out.sql.matches("WITH").count(), 1, "{}", out.sql);
6812 }
6813
6814 #[test]
6815 fn a_comparison_against_a_multi_row_with_binding_tests_membership() {
6816 let out = compile_and_emit_with(
6820 "WITH companies := (SELECT Company FILTER .name = 'x') SELECT Person FILTER .company = companies",
6821 &make_schema(),
6822 );
6823 assert!(
6824 out.sql.contains("= ANY((SELECT \"id\" FROM \"companies\"))"),
6825 "{}",
6826 out.sql
6827 );
6828
6829 let out = compile_and_emit_with(
6830 "WITH companies := (SELECT Company) SELECT Person FILTER any(.company = companies)",
6831 &make_schema(),
6832 );
6833 assert!(
6834 out.sql.contains("= ANY((SELECT \"id\" FROM \"companies\"))"),
6835 "{}",
6836 out.sql
6837 );
6838
6839 let out = compile_and_emit_with(
6840 "WITH companies := (SELECT Company) SELECT Person FILTER .company != companies",
6841 &make_schema(),
6842 );
6843 assert!(out.sql.contains("NOT (\"t1\".\"company_id\" = ANY("), "{}", out.sql);
6844 }
6845
6846 #[test]
6847 fn a_comparison_against_a_single_row_with_binding_stays_a_scalar_read() {
6848 let out = compile_and_emit_with(
6849 "WITH company := (SELECT Company LIMIT 1) SELECT Person FILTER .company = company",
6850 &make_schema(),
6851 );
6852 assert!(out.sql.contains("= (SELECT \"id\" FROM \"company\")"), "{}", out.sql);
6853 }
6854
6855 #[test]
6856 fn an_aggregate_over_a_backlink_counts_inside_a_subquery() {
6857 let out = compile_and_emit_with("SELECT Post { n := count(.<posts) }", &make_schema());
6858 assert!(
6859 !out.sql.contains("count((SELECT"),
6860 "an aggregate over a scalar subquery:\n{}",
6861 out.sql
6862 );
6863 assert!(out.sql.contains("count("), "{}", out.sql);
6864 }
6865
6866 #[test]
6867 fn an_aggregate_over_a_relative_subselect_counts_inside_a_subquery() {
6868 let out = compile_and_emit_with(
6869 "SELECT Post { n := count((SELECT .<posts FILTER .age > 1)) }",
6870 &make_schema(),
6871 );
6872 assert!(
6873 !out.sql.contains("count((SELECT"),
6874 "an aggregate over a scalar subquery:\n{}",
6875 out.sql
6876 );
6877 assert!(
6878 out.sql.contains(r#"(SELECT count("_s"."v") FROM unnest(ARRAY("#),
6879 "{}",
6880 out.sql
6881 );
6882 }
6883
6884 #[test]
6885 fn any_over_a_link_type_check_tests_each_element() {
6886 let out = compile_and_emit_with(
6887 "SELECT Company { name } FILTER any(.<company.posts IS Post)",
6888 &make_schema(),
6889 );
6890 assert!(
6891 !out.sql.contains("bool_or(ARRAY"),
6892 "an aggregate over outer rows:\n{}",
6893 out.sql
6894 );
6895 assert!(
6896 out.sql
6897 .contains("FROM unnest(ARRAY(SELECT ('default::Post' = 'default::Post')"),
6898 "{}",
6899 out.sql
6900 );
6901 }
6902
6903 #[test]
6904 fn a_link_type_check_reads_the_linked_object() {
6905 let out = compile_and_emit_with("SELECT Person { name } FILTER .company IS Company", &make_schema());
6906 assert!(!out.sql.contains("WHERE FALSE"), "{}", out.sql);
6907 assert!(out.sql.contains("'default::Company'"), "{}", out.sql);
6908 }
6909
6910 fn schema_with_a_subtype() -> SchemaDescriptor {
6912 let mut schema = make_schema();
6913 let mut admin = schema.types[0].clone();
6914 admin.name = "Admin".into();
6915 admin.table = "Admin".into();
6916 admin.bases = vec!["default::Person".into()];
6917 schema.types.push(admin);
6918 schema
6919 }
6920
6921 #[test]
6922 fn a_type_with_subtypes_reads_their_rows_too() {
6923 let out = compile_and_emit_with("SELECT Person { name }", &schema_with_a_subtype());
6924 assert!(
6925 out.sql.contains("'default::Admin'::text AS \"__type__\"") && out.sql.contains("FROM \"public\".\"Admin\""),
6926 "expected the subtype's table in the read:\n{}",
6927 out.sql
6928 );
6929 }
6930
6931 #[test]
6932 fn a_path_from_a_type_with_subtypes_reads_their_rows_too() {
6933 let out = compile_and_emit_with("SELECT Person.name", &schema_with_a_subtype());
6934 assert!(out.sql.contains("FROM \"public\".\"Admin\""), "{}", out.sql);
6935 }
6936
6937 #[test]
6938 fn a_multilink_of_a_type_with_subtypes_reads_their_junctions_too() {
6939 let out = compile_and_emit_with("SELECT Person { posts: { title } }", &schema_with_a_subtype());
6940 assert!(
6941 out.sql.contains(
6942 "(SELECT \"source\", \"target\" FROM \"public\".\"Person.posts\" UNION ALL \
6943 SELECT \"source\", \"target\" FROM \"public\".\"Admin.posts\")"
6944 ),
6945 "expected the junctions unioned:\n{}",
6946 out.sql
6947 );
6948 }
6949
6950 #[test]
6951 fn is_a_supertype_matches_its_subtypes() {
6952 let out = compile_and_emit_with(
6953 "SELECT Person { name } FILTER Person IS Admin",
6954 &schema_with_a_subtype(),
6955 );
6956 assert!(out.sql.contains("\"__type__\" = 'default::Admin'"), "{}", out.sql);
6957 }
6958
6959 #[test]
6960 fn a_write_to_a_type_with_subtypes_reaches_their_tables() {
6961 let out = compile_and_emit_with("UPDATE Person SET { age := 1 }", &schema_with_a_subtype());
6962 assert!(
6963 out.sql.contains("UPDATE \"public\".\"Person\"") && out.sql.contains("UPDATE \"public\".\"Admin\""),
6964 "expected both tables updated:\n{}",
6965 out.sql
6966 );
6967 assert!(
6968 !out.sql.contains("UPDATE (\n"),
6969 "a write never targets the union:\n{}",
6970 out.sql
6971 );
6972 }
6973
6974 #[test]
6975 fn test_comparing_a_multilink_itself_to_an_object() {
6976 let out = compile_and_emit_with(
6980 "WITH p := (SELECT Post LIMIT 1) SELECT Person { id } FILTER any(.posts = p)",
6981 &make_schema(),
6982 );
6983 assert!(
6984 out.sql.contains("EXISTS((SELECT 1\nFROM \"public\".\"Person.posts\""),
6985 "expected a junction EXISTS:\n{}",
6986 out.sql
6987 );
6988 assert!(
6989 out.sql.contains("\"target\" = (SELECT \"id\" FROM \"p\")"),
6990 "expected the junction target compared to the bound object's id:\n{}",
6991 out.sql
6992 );
6993 }
6994
6995 #[test]
6996 fn test_comparing_a_multilink_itself_warns_without_any() {
6997 let ast = parse::parse("WITH p := (SELECT Post LIMIT 1) SELECT Person { id } FILTER .posts = p").unwrap();
6998 let ir = ir::compile(&ast, &make_schema()).unwrap();
6999 assert!(
7000 ir.warnings.iter().any(|w| w.contains("multi-link '.posts'")),
7001 "expected the set-valued FILTER warning, got: {:?}",
7002 ir.warnings
7003 );
7004 }
7005
7006 #[test]
7007 fn test_detached_names_the_type_of_a_nested_select() {
7008 let out = compile_and_emit_with(
7012 "INSERT Person { name := $n, company := (SELECT detached Company FILTER .name = $c LIMIT 1) }",
7013 &make_schema(),
7014 );
7015 assert!(
7016 out.sql.contains("INSERT INTO \"public\".\"Person\""),
7017 "expected the insert to compile:\n{}",
7018 out.sql
7019 );
7020 assert!(
7021 out.sql.contains("FROM \"public\".\"Company\""),
7022 "expected the detached select to read the Company table:\n{}",
7023 out.sql
7024 );
7025 }
7026
7027 #[test]
7028 fn test_select_over_an_update_on_a_with_binding() {
7029 let out = compile_and_emit_with(
7034 "WITH p := (SELECT Person FILTER .id = $a LIMIT 1) SELECT (UPDATE p SET { name := $n }) { id }",
7035 &make_schema(),
7036 );
7037 assert!(
7038 out.sql.contains("UPDATE \"public\".\"Person\""),
7039 "expected the update to resolve to the bound type's table:\n{}",
7040 out.sql
7041 );
7042 }
7043
7044 #[test]
7045 fn test_a_mutation_as_a_free_object_field() {
7046 let out = compile_and_emit_with(
7050 "SELECT { renamed := (UPDATE Person FILTER .id = $a SET { name := $n }), n := 1 }",
7051 &make_schema(),
7052 );
7053 assert!(
7054 out.sql.contains("UPDATE \"public\".\"Person\""),
7055 "expected the mutation to become a data-modifying CTE:\n{}",
7056 out.sql
7057 );
7058 assert!(
7059 out.sql
7060 .contains("(SELECT \"t1\".\"id\"\nFROM \"_nested_dml_0\" AS \"t1\") AS \"_f0\""),
7061 "expected the field to carry the rows the mutation touched:\n{}",
7062 out.sql
7063 );
7064 }
7065
7066 #[test]
7067 fn test_a_sub_selects_filter_stays_on_its_own_subject_when_a_field_chain_projects_off_it() {
7068 let out = compile_and_emit_with("SELECT (SELECT Person FILTER .name = $n).company.name", &make_schema());
7072 assert!(
7073 out.sql.contains("WHERE (\"t0\".\"name\" = $1)"),
7074 "the inner filter belongs to the Person alias:\n{}",
7075 out.sql
7076 );
7077 }
7078
7079 #[test]
7080 fn test_reading_back_a_nested_insert_through_its_own_statement() {
7081 let out = compile_and_emit_with(
7082 "SELECT (INSERT Person { name := $n, posts := (INSERT Post { title := $t }) }) { id, posts: { title } }",
7083 &make_schema(),
7084 );
7085 assert!(out.sql.contains("__ml_add_0"), "got:\n{}", out.sql);
7086 }
7087
7088 #[test]
7089 fn test_reading_back_a_nested_insert_within_its_own_statement() {
7090 let out = compile_and_emit_with(
7094 "SELECT (INSERT Person { name := $n, posts := (INSERT Post { title := $t }) }) \
7095 { id, posts: { title } }",
7096 &make_schema(),
7097 );
7098 assert!(
7099 out.sql.contains("FROM \"_dml__ml_add_0\" AS \"jt\""),
7100 "the junction rows must come from the CTE that wrote them:\n{}",
7101 out.sql
7102 );
7103 assert!(
7104 out.sql.contains("\"_nested_dml_0\""),
7105 "and the targets from the nested insert's own CTE:\n{}",
7106 out.sql
7107 );
7108 }
7109
7110 #[test]
7111 fn test_a_nested_single_link_insert_reads_back_from_its_cte() {
7112 let out = compile_and_emit_with(
7113 "SELECT (INSERT Person { name := $n, company := (INSERT Company { name := $c }) }) \
7114 { id, company: { name } }",
7115 &make_schema(),
7116 );
7117 assert!(
7118 out.sql.contains("FROM \"_nested_dml_0\""),
7119 "the linked row must be read from the CTE that inserted it:\n{}",
7120 out.sql
7121 );
7122 }
7123
7124 #[test]
7125 fn test_a_free_object_field_holding_an_object_keeps_it_an_object() {
7126 let out = compile_and_emit_with(
7130 "WITH c := (SELECT Company LIMIT 1) SELECT { co := c { name }, n := 1 }",
7131 &make_schema(),
7132 );
7133 assert!(
7134 out.sql.contains("'default::Company'::text"),
7135 "the field must carry the object's own row:\n{}",
7136 out.sql
7137 );
7138 }
7139
7140 #[test]
7141 fn test_projecting_an_object_off_a_free_object() {
7142 let out = compile_and_emit_with(
7145 "WITH c := (SELECT Company LIMIT 1) SELECT { co := c { name }, n := 1 }.co",
7146 &make_schema(),
7147 );
7148 assert!(
7149 !out.sql.contains("jsonb_build_object"),
7150 "projecting a pointer must not go through jsonb:\n{}",
7151 out.sql
7152 );
7153 assert!(
7154 out.sql.contains("SELECT v AS result, v FROM"),
7155 "the object row is already the result row:\n{}",
7156 out.sql
7157 );
7158 }
7159
7160 #[test]
7161 fn test_a_shape_on_a_single_valued_object_path() {
7162 let out = compile_and_emit_with("SELECT Person { c := .company { name } }", &make_schema());
7167 assert!(
7168 out.sql.contains("'default::Company'::text"),
7169 "the pointer must carry the object's own row:\n{}",
7170 out.sql
7171 );
7172 assert!(
7173 !out.sql.contains("ARRAY(SELECT"),
7174 "a single-valued walk is one object, not an array of one:\n{}",
7175 out.sql
7176 );
7177 }
7178
7179 #[test]
7180 fn test_a_shapeless_single_link_still_reads_as_its_value() {
7181 let out = compile_and_emit_with("SELECT Person { c := .company }", &make_schema());
7184 assert!(
7185 !out.sql.contains("'default::Company'::text"),
7186 "a bare link reference must not grow an object row:\n{}",
7187 out.sql
7188 );
7189 }
7190
7191 #[test]
7192 fn test_a_tuple_element_holding_a_shaped_object() {
7193 let out = compile_and_emit_with(
7197 "WITH c := (SELECT Company LIMIT 1) SELECT (c { name }, 1)",
7198 &make_schema(),
7199 );
7200 assert!(
7201 out.sql.contains("'default::Company'::text"),
7202 "the element must carry the object's own row:\n{}",
7203 out.sql
7204 );
7205 }
7206
7207 #[test]
7208 fn test_an_object_returning_function_as_a_link_value() {
7209 let mut schema = make_schema();
7214 let company = schema
7216 .types
7217 .iter_mut()
7218 .find(|t| t.name == "Company")
7219 .expect("make_schema declares Company");
7220 company.properties.push(PropertyDescriptor {
7221 name: "id".into(),
7222 pg_type: "uuid".into(),
7223 nullable: false,
7224 default_sql: None,
7225 default_pyql: None,
7226 description: None,
7227 check_constraints: vec![],
7228 is_exclusive: true,
7229 is_pk: true,
7230 is_readonly: true,
7231 rewrites: vec![],
7232 tuple_members: None,
7233 column_type: None,
7234 });
7235 schema.functions.push(crate::schema::FunctionDescriptor {
7236 name: "current_company".into(),
7237 module: "default".into(),
7238 params: vec![],
7239 return_pg_type: "default::Company".into(),
7240 return_is_object: true,
7241 return_is_set: false,
7242 return_is_polymorphic: false,
7243 volatility: "stable".into(),
7244 body: "select Company limit 1".into(),
7245 });
7246 let out = compile_and_emit_with("INSERT Person { name := $n, company := current_company() }", &schema);
7247 assert!(
7248 out.sql.contains("FROM \"public\".\"current_company\"()"),
7249 "the function must supply the foreign key:\n{}",
7250 out.sql
7251 );
7252 }
7253
7254 fn make_schema_with_an_object_returning_default() -> SchemaDescriptor {
7258 let mut schema = make_schema();
7259 let company = schema
7260 .types
7261 .iter_mut()
7262 .find(|t| t.name == "Company")
7263 .expect("make_schema declares Company");
7264 company.properties.push(PropertyDescriptor {
7265 name: "id".into(),
7266 pg_type: "uuid".into(),
7267 nullable: false,
7268 default_sql: None,
7269 default_pyql: None,
7270 description: None,
7271 check_constraints: vec![],
7272 is_exclusive: true,
7273 is_pk: true,
7274 is_readonly: true,
7275 rewrites: vec![],
7276 tuple_members: None,
7277 column_type: None,
7278 });
7279 schema.functions.push(crate::schema::FunctionDescriptor {
7280 name: "current_company".into(),
7281 module: "default".into(),
7282 params: vec![],
7283 return_pg_type: "default::Company".into(),
7284 return_is_object: true,
7285 return_is_set: false,
7286 return_is_polymorphic: false,
7287 volatility: "stable".into(),
7288 body: "select Company limit 1".into(),
7289 });
7290 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
7291 let company_link = person.links.iter_mut().find(|l| l.name == "company").unwrap();
7292 company_link.default_pyql = Some("current_company()".into());
7293 schema
7294 }
7295
7296 #[test]
7297 fn an_object_returning_link_default_is_applied_by_the_insert() {
7298 let schema = make_schema_with_an_object_returning_default();
7302 let out = compile_and_emit_with("INSERT Person { name := $n }", &schema);
7303 assert!(
7304 out.sql.contains("\"company_id\"") && out.sql.contains("current_company"),
7305 "the default must supply the foreign key:\n{}",
7306 out.sql
7307 );
7308 }
7309
7310 #[test]
7311 fn an_explicit_value_wins_over_an_inlined_default() {
7312 let schema = make_schema_with_an_object_returning_default();
7313 let out = compile_and_emit_with("INSERT Person { name := $n, company := {} }", &schema);
7314 assert!(
7315 !out.sql.contains("current_company"),
7316 "the shape named the pointer, so the default does not apply:\n{}",
7317 out.sql
7318 );
7319 }
7320
7321 #[test]
7322 fn an_object_returning_link_default_gets_no_column_default() {
7323 let schema = make_schema_with_an_object_returning_default();
7326 let ddl = crate::export::export_schema(&schema).unwrap();
7327 assert!(
7328 !ddl.contains("\"company_id\" uuid DEFAULT"),
7329 "the column must carry no default:\n{ddl}"
7330 );
7331 crate::validate::validate_schema_types(&schema).expect("an inlined default is not an error");
7332 }
7333
7334 #[test]
7335 fn a_column_expressible_default_stays_in_the_ddl() {
7336 let mut schema = make_schema();
7338 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
7339 let age = person.properties.iter_mut().find(|p| p.name == "age").unwrap();
7340 age.default_pyql = Some("21".into());
7341 assert!(
7342 crate::ir::inlined_pointer_defaults(schema.types.iter().find(|t| t.name == "Person").unwrap(), &schema)
7343 .is_empty()
7344 );
7345 let ddl = crate::export::export_schema(&schema).unwrap();
7346 assert!(ddl.contains("DEFAULT 21"), "{ddl}");
7347 let out = compile_and_emit_with("INSERT Person { name := $n }", &schema);
7348 assert!(
7349 !out.sql.contains("21"),
7350 "the column DEFAULT still applies it:\n{}",
7351 out.sql
7352 );
7353 }
7354
7355 fn make_schema_with_an_inherited_self_naming_computed() -> SchemaDescriptor {
7359 let mut schema = make_schema();
7360 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
7361 person.computed.push(crate::schema::ComputedDescriptor {
7362 name: "own_name".into(),
7363 expression: "default::Person.name".into(),
7364 return_type: Some("text".into()),
7365 link_target: None,
7366 link_multi: false,
7367 });
7368 let mut employee = person.clone();
7369 employee.name = "Employee".into();
7370 employee.table = "Employee".into();
7371 employee.bases = vec!["default::Person".into()];
7372 schema.types.push(employee);
7373 schema
7374 }
7375
7376 #[test]
7377 fn an_inherited_computed_naming_its_declaring_type_still_means_the_row() {
7378 let schema = make_schema_with_an_inherited_self_naming_computed();
7383 let out = compile_and_emit_with("SELECT Employee { own_name }", &schema);
7384 assert!(
7385 !out.sql.contains("\"Person\""),
7386 "the computed must read the Employee row, not Person's table:\n{}",
7387 out.sql
7388 );
7389 crate::validate::validate_schema_types(&schema).expect("an inherited computed is single-valued");
7390 }
7391
7392 #[test]
7393 fn an_inherited_type_check_computed_tests_the_row_it_is_read_on() {
7394 let mut schema = make_schema();
7400 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
7401 person.computed.push(crate::schema::ComputedDescriptor {
7402 name: "employed".into(),
7403 expression: "(default::Person is default::Employee)".into(),
7404 return_type: Some("boolean".into()),
7405 link_target: None,
7406 link_multi: false,
7407 });
7408 let mut employee = person.clone();
7409 employee.name = "Employee".into();
7410 employee.table = "Employee".into();
7411 employee.bases = vec!["default::Person".into()];
7412 schema.types.push(employee);
7413
7414 crate::validate::validate_schema_types(&schema).expect("the check is single-valued on both types");
7415 let out = compile_and_emit_with("SELECT Employee { employed }", &schema);
7416 assert!(
7417 !out.sql.contains("\"Person\""),
7418 "the check must read the Employee row's own type:\n{}",
7419 out.sql
7420 );
7421 }
7422
7423 fn make_interface_schema_with_an_outsider() -> SchemaDescriptor {
7430 let mut schema = make_interface_schema();
7431 let individual = schema
7432 .types
7433 .iter()
7434 .find(|t| t.name == "Individual")
7435 .expect("Individual is in the interface schema")
7436 .clone();
7437 let mut company = individual.clone();
7438 company.name = "Company".into();
7439 company.table = "Company".into();
7440 company.interfaces = vec![];
7441 company.computed = vec![];
7442 schema.types.push(company);
7443 let mut organisation = individual;
7444 organisation.name = "Organisation".into();
7445 organisation.table = "Organisation".into();
7446 organisation.computed = vec![];
7447 schema.types.push(organisation);
7448 schema
7449 }
7450
7451 #[test]
7452 fn a_type_check_naming_another_type_reads_as_one_value_in_a_filter() {
7453 let out = compile_and_emit(
7461 "SELECT Person { name } FILTER (.name = 'a' if default::Company is default::Post else FALSE)",
7462 );
7463 assert!(
7464 !out.sql.contains("ARRAY(SELECT"),
7465 "an answer that is the same for every row has nothing to iterate:\n{}",
7466 out.sql
7467 );
7468 assert!(
7469 out.sql.contains("CASE WHEN FALSE"),
7470 "a Company is never a Post:\n{}",
7471 out.sql
7472 );
7473 }
7474
7475 #[test]
7476 fn a_polymorphic_type_is_never_one_from_a_hierarchy_it_shares_nothing_with() {
7477 let schema = make_interface_schema_with_an_outsider();
7484 let out = compile_and_emit_with(
7485 "SELECT Company { id } FILTER (.first_name = 'a' if default::Account is default::Company else FALSE)",
7486 &schema,
7487 );
7488 assert!(
7489 !out.sql.contains("assert_single") && !out.sql.contains("ARRAY(SELECT"),
7490 "nothing to ask per row when the hierarchies are disjoint:\n{}",
7491 out.sql
7492 );
7493 assert!(
7494 out.sql.contains("CASE WHEN FALSE"),
7495 "an Account is never a Company:\n{}",
7496 out.sql
7497 );
7498 }
7499
7500 #[test]
7501 fn a_check_covering_every_implementor_holds_without_reading_a_row() {
7502 let mut schema = make_interface_schema_with_an_outsider();
7506 schema.types.retain(|t| t.name != "Organisation");
7507 let out = compile_and_emit_with(
7508 "SELECT Company { id } FILTER (default::Account is default::Individual)",
7509 &schema,
7510 );
7511 assert!(
7512 !out.sql.contains("assert_single"),
7513 "every Account is an Individual when it is the only implementor:\n{}",
7514 out.sql
7515 );
7516 assert!(out.sql.contains("WHERE TRUE"), "{}", out.sql);
7517 }
7518
7519 #[test]
7520 fn a_type_check_over_a_polymorphic_outsider_collapses_to_one_value() {
7521 let schema = make_interface_schema_with_an_outsider();
7526 let out = compile_and_emit_with(
7527 "SELECT Company { id } FILTER (default::Account is default::Individual)",
7528 &schema,
7529 );
7530 assert!(
7531 out.sql.contains("\"_pylon\".\"assert_single\"(ARRAY(SELECT"),
7532 "one answer per Account row, read as a single value:\n{}",
7533 out.sql
7534 );
7535 assert!(
7536 out.sql
7537 .contains("is default::Individual'' is asked once for every default::Account object"),
7538 "the error has to name the check, not assert_single:\n{}",
7539 out.sql
7540 );
7541 }
7542
7543 #[test]
7544 fn a_type_check_naming_another_type_keeps_its_whole_set_in_a_shape() {
7545 let schema = make_interface_schema_with_an_outsider();
7548 let out = compile_and_emit_with(
7549 "SELECT Company { id, each := default::Account is default::Individual }",
7550 &schema,
7551 );
7552 assert!(
7553 out.sql.contains("array_agg") && !out.sql.contains("assert_single"),
7554 "a shape keeps every answer:\n{}",
7555 out.sql
7556 );
7557 }
7558
7559 #[test]
7560 fn a_shape_writing_the_computed_out_itself_does_not_get_the_anchor() {
7561 let schema = make_schema_with_an_inherited_self_naming_computed();
7569 let ast = parse::parse("SELECT Employee { own_name := default::Person.name }").expect("parse failed");
7570 let Err(error) = ir::compile(&ast, &schema) else {
7571 panic!("an absolute path has nothing to anchor to here")
7572 };
7573 assert!(
7574 format!("{error:?}").contains("absolute paths are not valid"),
7575 "{error:?}"
7576 );
7577 }
7578
7579 #[test]
7580 fn a_default_that_compiles_nowhere_is_still_an_error() {
7581 let mut schema = make_schema();
7584 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
7585 let age = person.properties.iter_mut().find(|p| p.name == "age").unwrap();
7586 age.default_pyql = Some("sequence_next(INTROSPECT default::AgeSeq)".into());
7587 let errs = crate::validate::validate_schema_types(&schema).unwrap_err();
7588 let (_, msg, _) = errs[0].class_name_message_position();
7589 assert!(msg.contains("default"), "{msg}");
7590 }
7591
7592 #[test]
7593 fn test_comparing_a_link_with_an_object_returning_function() {
7594 let mut schema = make_schema();
7598 let company = schema
7600 .types
7601 .iter_mut()
7602 .find(|t| t.name == "Company")
7603 .expect("make_schema declares Company");
7604 company.properties.push(PropertyDescriptor {
7605 name: "id".into(),
7606 pg_type: "uuid".into(),
7607 nullable: false,
7608 default_sql: None,
7609 default_pyql: None,
7610 description: None,
7611 check_constraints: vec![],
7612 is_exclusive: true,
7613 is_pk: true,
7614 is_readonly: true,
7615 rewrites: vec![],
7616 tuple_members: None,
7617 column_type: None,
7618 });
7619 schema.functions.push(crate::schema::FunctionDescriptor {
7620 name: "current_company".into(),
7621 module: "default".into(),
7622 params: vec![],
7623 return_pg_type: "default::Company".into(),
7624 return_is_object: true,
7625 return_is_set: false,
7626 return_is_polymorphic: false,
7627 volatility: "stable".into(),
7628 body: "select Company limit 1".into(),
7629 });
7630 let out = compile_and_emit_with("SELECT Person FILTER .company = current_company()", &schema);
7631 assert!(
7632 out.sql.contains("\"company_id\"") && out.sql.contains("current_company"),
7633 "the link's key against the function's row:\n{}",
7634 out.sql
7635 );
7636 }
7637
7638 #[test]
7639 fn test_choosing_between_two_object_sets() {
7640 let out = compile_and_emit_with(
7644 "WITH a := (SELECT Person FILTER .name = $n), b := (SELECT Person) \
7645 SELECT (a IF EXISTS a ELSE b) { name }",
7646 &make_schema(),
7647 );
7648 assert!(
7649 out.sql.contains("UNION ALL"),
7650 "expected one branch per side:\n{}",
7651 out.sql
7652 );
7653 }
7654
7655 #[test]
7656 fn test_a_union_operand_written_inline() {
7657 let out = compile_and_emit_with(
7661 "SELECT ((SELECT Person FILTER .name = $n) UNION (SELECT Person)) { name }",
7662 &make_schema(),
7663 );
7664 assert!(
7665 out.sql.starts_with("WITH"),
7666 "expected the operands hoisted:\n{}",
7667 out.sql
7668 );
7669 assert!(out.sql.contains("UNION ALL"), "{}", out.sql);
7670 }
7671
7672 #[test]
7673 fn test_a_for_loop_variable_over_objects() {
7674 let out = compile_and_emit_with("FOR p IN (SELECT Person) UNION (SELECT p.name)", &make_schema());
7678 assert!(
7679 out.sql.contains("FROM \"public\".\"Person\"") && out.sql.contains("\"_for_p\".\"v\""),
7680 "the body must read the row the variable names:\n{}",
7681 out.sql
7682 );
7683 }
7684
7685 #[test]
7686 fn test_a_shape_on_a_for_loop_variable() {
7687 let out = compile_and_emit_with("FOR p IN (SELECT Person) UNION (SELECT p { name })", &make_schema());
7688 assert!(
7689 out.sql.contains("'default::Person'::text"),
7690 "a shape on the variable yields its object:\n{}",
7691 out.sql
7692 );
7693 }
7694
7695 #[test]
7696 fn test_a_for_loop_variable_over_values_is_still_a_scalar() {
7697 let out = compile_and_emit_with("FOR n IN {1, 2} UNION (SELECT n)", &make_schema());
7698 assert!(out.sql.contains("VALUES"), "{}", out.sql);
7699 }
7700
7701 #[test]
7702 fn test_exists_over_a_sub_select_on_a_relative_path() {
7703 let out = compile_and_emit_with(
7708 "SELECT Person { id } FILTER EXISTS ((SELECT .posts FILTER .title = $t))",
7709 &make_schema(),
7710 );
7711 assert!(out.sql.contains("EXISTS("), "{}", out.sql);
7712 assert!(
7713 out.sql.contains("\"Person.posts\""),
7714 "the junction join must survive into the EXISTS:\n{}",
7715 out.sql
7716 );
7717 assert!(
7718 out.sql.contains("\"t0\".\"id\""),
7719 "and it must correlate to the enclosing row:\n{}",
7720 out.sql
7721 );
7722 }
7723
7724 #[test]
7725 fn test_counting_the_rows_a_mutation_touched() {
7726 let out = compile_and_emit_with("SELECT count((DELETE Person FILTER .name = $n))", &make_schema());
7730 assert!(out.sql.starts_with("WITH"), "{}", out.sql);
7731 assert!(out.sql.contains("DELETE FROM \"public\".\"Person\""), "{}", out.sql);
7732 assert!(out.sql.contains("count(*)"), "{}", out.sql);
7733 }
7734
7735 #[test]
7736 fn test_a_conflict_target_naming_several_columns() {
7737 let out = compile_and_emit_with(
7741 "INSERT Person { name := $n } UNLESS CONFLICT ON (.name, .id) ELSE (UPDATE Person SET { name := $n })",
7742 &make_schema(),
7743 );
7744 assert!(
7745 out.sql.contains("ON CONFLICT (\"name\", \"id\")"),
7746 "expected a column list:\n{}",
7747 out.sql
7748 );
7749 }
7750
7751 #[test]
7752 fn test_a_for_loop_over_a_path() {
7753 let out = compile_and_emit_with(
7757 "WITH p := (SELECT Person LIMIT 1) FOR post IN p.posts UNION (SELECT post.title)",
7758 &make_schema(),
7759 );
7760 assert!(out.sql.contains("CROSS JOIN LATERAL"), "{}", out.sql);
7761 }
7762
7763 #[test]
7764 fn test_a_relative_sub_select_keeps_its_enclosing_object() {
7765 let out = compile_and_emit_with(
7770 "SELECT Person { ps := assert_exists((SELECT .posts { title })) }",
7771 &make_schema(),
7772 );
7773 assert!(out.sql.contains("assert_exists"), "{}", out.sql);
7774 assert!(
7775 out.sql.contains("\"Person.posts\""),
7776 "the walk must reach the junction:\n{}",
7777 out.sql
7778 );
7779 }
7780
7781 #[test]
7782 fn test_a_relative_sub_select_as_a_multi_link_value() {
7783 let out = compile_and_emit_with(
7784 "UPDATE Person FILTER .name = $n SET { posts := (SELECT .posts FILTER .title = $t) }",
7785 &make_schema(),
7786 );
7787 assert!(
7788 out.sql.contains("\"Person.posts\""),
7789 "the value must read the row's own posts:\n{}",
7790 out.sql
7791 );
7792 }
7793
7794 #[test]
7795 fn test_a_shape_written_after_a_sub_select() {
7796 let out = compile_and_emit_with(
7800 "SELECT Person { ps := (SELECT Post FILTER .title = $t) { title } }",
7801 &make_schema(),
7802 );
7803 assert!(
7804 out.sql.contains("'default::Post'::text"),
7805 "the pointer must carry the object rows:\n{}",
7806 out.sql
7807 );
7808 }
7809
7810 #[test]
7811 fn test_a_shape_after_a_sub_select_with_a_with_block() {
7812 let out = compile_and_emit_with(
7813 "SELECT Person { ps := (WITH t := $t SELECT Post FILTER .title = t) { title } }",
7814 &make_schema(),
7815 );
7816 assert!(out.sql.contains("'default::Post'::text"), "{}", out.sql);
7817 }
7818
7819 #[test]
7820 fn test_a_computed_declared_as_an_object_returning_call() {
7821 let mut schema = make_schema();
7826 schema.functions.push(crate::schema::FunctionDescriptor {
7827 name: "recent".into(),
7828 module: "default".into(),
7829 params: vec![],
7830 return_pg_type: "default::Post".into(),
7831 return_is_object: true,
7832 return_is_set: true,
7833 return_is_polymorphic: false,
7834 volatility: "stable".into(),
7835 body: "select Post".into(),
7836 });
7837 schema.types[0].computed.push(crate::schema::ComputedDescriptor {
7838 name: "recent".into(),
7839 expression: "default::recent()".into(),
7840 return_type: None,
7841 link_target: None,
7842 link_multi: false,
7843 });
7844 let out = compile_and_emit_with("SELECT Person { recent }", &schema);
7845 assert!(
7846 out.sql.contains("ARRAY(SELECT") && out.sql.contains("'default::Post'::text"),
7847 "the pointer must carry the function's rows:\n{}",
7848 out.sql
7849 );
7850 }
7851
7852 #[test]
7853 fn test_one_branch_of_an_object_if_else_is_empty() {
7854 let out = compile_and_emit_with(
7858 "WITH p := (SELECT Person LIMIT 1) SELECT p { name } IF EXISTS p ELSE {}",
7859 &make_schema(),
7860 );
7861 assert!(!out.sql.contains("UNION ALL"), "nothing to union with:\n{}", out.sql);
7862 assert!(out.sql.contains("'default::Person'::text"), "{}", out.sql);
7863 }
7864
7865 #[test]
7866 fn test_a_conditional_insert_is_guarded_by_its_condition() {
7867 let out = compile_and_emit_with("SELECT (INSERT Person { name := $n }) IF FALSE ELSE {}", &make_schema());
7872 assert!(
7873 out.sql.contains("SELECT") && out.sql.contains("WHERE"),
7874 "a guarded insert selects its row rather than listing values:\n{}",
7875 out.sql
7876 );
7877 assert!(!out.sql.contains("VALUES"), "VALUES always yields a row:\n{}", out.sql);
7878 }
7879
7880 #[test]
7881 fn test_a_conditional_update_carries_its_condition() {
7882 let out = compile_and_emit(
7887 "SELECT (UPDATE Person FILTER .name = $n SET { name := $m }) IF EXISTS (SELECT Company) ELSE {}",
7888 );
7889 let update = out.sql.find("UPDATE").expect("an update is emitted");
7890 let where_clause = out.sql[update..].find("WHERE").expect("the update is filtered");
7891 assert!(
7892 out.sql[update + where_clause..].contains("EXISTS"),
7893 "the condition narrows the rows the update touches:\n{}",
7894 out.sql
7895 );
7896 }
7897
7898 #[test]
7899 fn test_a_conditional_delete_carries_its_condition() {
7900 let out = compile_and_emit("SELECT (DELETE Person FILTER .name = $n) IF FALSE ELSE {}");
7904 let delete = out
7905 .sql
7906 .split("DELETE FROM")
7907 .nth(1)
7908 .expect("the delete should still be emitted");
7909 let where_clause = delete.split("WHERE").nth(1).expect("the delete should be filtered");
7910 assert!(
7911 where_clause.contains("FALSE") || where_clause.contains("false"),
7912 "the condition must narrow the delete itself:\n{}",
7913 out.sql
7914 );
7915 }
7916
7917 #[test]
7918 fn test_a_pointer_a_binding_declared_in_its_own_shape() {
7919 let out = compile_and_emit_with(
7923 "WITH p := (SELECT Person { co := .company { name } } LIMIT 1) SELECT p { name, co: { name } }",
7924 &make_schema(),
7925 );
7926 assert!(
7927 out.sql.contains("'default::Company'::text"),
7928 "the declared pointer must be inlined:\n{}",
7929 out.sql
7930 );
7931 }
7932
7933 #[test]
7934 fn test_a_binding_declared_pointer_read_from_a_tuple_element() {
7935 let out = compile_and_emit_with(
7936 "WITH p := (SELECT Person { co := .company { name } } LIMIT 1) SELECT (p { co: { name } }, 1)",
7937 &make_schema(),
7938 );
7939 assert!(out.sql.contains("'default::Company'::text"), "{}", out.sql);
7940 }
7941
7942 #[test]
7943 fn test_a_for_loop_variable_shaped_inside_a_tuple() {
7944 let out = compile_and_emit_with(
7948 "WITH p := (SELECT Person LIMIT 1) FOR q IN p.posts UNION (SELECT (a := q { title }, b := 1))",
7949 &make_schema(),
7950 );
7951 assert!(
7952 out.sql.contains("\"_for_q\".\"v\""),
7953 "the element must be narrowed to the loop's row:\n{}",
7954 out.sql
7955 );
7956 }
7957
7958 #[test]
7959 fn test_choosing_between_two_walks() {
7960 let out = compile_and_emit_with(
7963 "WITH p := (SELECT Person LIMIT 1), q := (SELECT p.posts LIMIT 1) IF TRUE ELSE (SELECT p.posts) \
7964 SELECT q { title }",
7965 &make_schema(),
7966 );
7967 assert!(out.sql.contains("UNION ALL"), "one branch per side:\n{}", out.sql);
7968 }
7969
7970 #[test]
7971 fn test_a_bare_coalesce_of_correlated_walks_reaching_many_objects() {
7972 let out = compile_and_emit_with(
7975 "SELECT Post { owners := .<posts[is Person] ?? .<posts[is Person] }",
7976 &make_schema(),
7977 );
7978 assert!(
7979 !out.sql.contains("COALESCE("),
7980 "not a value-by-value choice:\n{}",
7981 out.sql
7982 );
7983 assert!(
7984 out.sql.contains("ARRAY(SELECT \"r\" FROM ("),
7985 "the arms are aggregated:\n{}",
7986 out.sql
7987 );
7988 }
7989
7990 #[test]
7991 fn test_a_union_of_correlated_walks_reaching_many() {
7992 let out = compile_and_emit_with(
7997 "SELECT Post { owners := (.<posts[is Person] UNION .<posts[is Person]) { name } }",
7998 &make_schema(),
7999 );
8000 assert!(
8001 out.sql.contains("ARRAY(SELECT \"r\" FROM ("),
8002 "the arms are aggregated:\n{}",
8003 out.sql
8004 );
8005 }
8006
8007 #[test]
8008 fn test_a_coalesce_of_correlated_walks_reaching_many() {
8009 let out = compile_and_emit_with(
8012 "SELECT Post { owners := (.<posts[is Person] ?? .<posts[is Person]) { name } }",
8013 &make_schema(),
8014 );
8015 assert!(
8016 out.sql.contains("ARRAY(SELECT \"r\" FROM ("),
8017 "the arms are aggregated:\n{}",
8018 out.sql
8019 );
8020 }
8021
8022 #[test]
8023 fn test_a_union_of_correlated_walks() {
8024 let out = compile_and_emit_with(
8028 "SELECT Post { owners := (SELECT (.<posts[is Person] UNION .<posts[is Person]) { name } LIMIT 1) }",
8029 &make_schema(),
8030 );
8031 assert!(out.sql.contains("UNION ALL"), "one arm per operand:\n{}", out.sql);
8032 assert!(
8033 out.sql.contains("AS \"_u\""),
8034 "the arms are read as one set:\n{}",
8035 out.sql
8036 );
8037 }
8038
8039 #[test]
8040 fn test_a_for_loop_whose_body_updates() {
8041 let out = compile_and_emit_with(
8045 "FOR p IN (SELECT Person) UNION (UPDATE Person FILTER .id = p.id SET { name := $n })",
8046 &make_schema(),
8047 );
8048 assert!(out.sql.contains("UPDATE \"public\".\"Person\""), "{}", out.sql);
8049 assert!(
8050 out.sql.contains("FROM \"_for_p\""),
8051 "driven from the iteration:\n{}",
8052 out.sql
8053 );
8054 assert!(
8055 !out.sql.contains("LATERAL"),
8056 "DML cannot sit in a LATERAL:\n{}",
8057 out.sql
8058 );
8059 }
8060
8061 #[test]
8062 fn test_a_for_loop_body_appends_the_row_it_is_iterating() {
8063 let out = compile_and_emit(
8064 "WITH t := (SELECT Post) FOR q IN t UNION (UPDATE Person FILTER .id = $i SET { posts += q })",
8065 );
8066 assert!(
8069 out.sql.contains("\"_iter\"") && out.sql.contains("FROM \"_ids\""),
8070 "junction rows driven from the iteration:\n{}",
8071 out.sql
8072 );
8073 assert!(
8074 !out.sql.contains("LATERAL"),
8075 "DML cannot sit in a LATERAL:\n{}",
8076 out.sql
8077 );
8078 }
8079
8080 #[test]
8081 fn test_select_on_a_for_loop_variable_reads_only_its_row() {
8082 let out = compile_and_emit("FOR p IN (SELECT Person) UNION (SELECT (SELECT p) { name })");
8083 assert!(
8084 out.sql.contains("= \"_for_p\".\"v\""),
8085 "narrowed to the row the variable holds:\n{}",
8086 out.sql
8087 );
8088 }
8089
8090 #[test]
8091 fn test_a_walk_off_a_for_loop_variable_reads_the_binding() {
8092 let out = compile_and_emit("WITH ps := (SELECT Post) FOR q IN ps UNION (SELECT Person FILTER .name = q.title)");
8095 assert!(
8096 out.sql.contains("FROM \"ps\""),
8097 "the walk reads the binding:\n{}",
8098 out.sql
8099 );
8100 }
8101
8102 #[test]
8103 fn test_a_mutating_for_loop_binding_becomes_sibling_ctes() {
8104 let out = compile_and_emit(
8105 "WITH made := (FOR n IN {'a', 'b'} UNION (INSERT Post { title := n })) SELECT count(made)",
8106 );
8107 assert!(
8108 out.sql.contains("\"made__ids\" AS (\nINSERT INTO"),
8109 "the insert is its own top-level CTE:\n{}",
8110 out.sql
8111 );
8112 assert!(
8113 !out.sql.contains("LATERAL"),
8114 "DML cannot sit in a LATERAL:\n{}",
8115 out.sql
8116 );
8117 }
8118
8119 #[test]
8120 fn test_count_of_a_binding_counts_its_rows() {
8121 let out = compile_and_emit("WITH people := (SELECT Person) SELECT count(people)");
8124 assert!(out.sql.contains("(SELECT count(*) FROM \"people\")"), "{}", out.sql);
8125 }
8126
8127 #[test]
8128 fn test_dividing_integers_yields_a_float() {
8129 for (query, expected) in [
8130 ("SELECT 100 / 133", "((100)::float8 / 133)"),
8131 ("SELECT Person { a := .age / 2 }", "::float8 / 2)"),
8132 ("SELECT Person.age / 2", "::float8 / 2)"),
8133 (
8134 "WITH total := max({133}), current := 38 - 1 SELECT 100 / total * current",
8135 "(((100)::float8 / (SELECT \"v\" FROM \"total\")) * (SELECT \"v\" FROM \"current\"))",
8136 ),
8137 (
8138 "WITH p := (SELECT Person LIMIT 1), index := p.age - 1, total := max(Person.age) SELECT 100 / total * index",
8139 "((100)::float8 / (SELECT \"v\" FROM \"total\"))",
8140 ),
8141 ] {
8142 let out = compile_and_emit(query);
8143 assert!(out.sql.contains(expected), "{query}:\n{}", out.sql);
8144 }
8145 }
8146
8147 #[test]
8148 fn test_dividing_anything_but_two_integers_is_left_to_postgres() {
8149 for query in [
8150 "SELECT 100 // 133",
8151 "SELECT 100 / 1.5",
8152 "SELECT <decimal>100 / <decimal>3",
8153 "SELECT 100 / <decimal>3",
8154 ] {
8155 let out = compile_and_emit(query);
8156 assert!(!out.sql.contains("::float8 /"), "{query}:\n{}", out.sql);
8157 }
8158 }
8159
8160 #[test]
8161 fn test_a_free_object_under_a_condition_stays_an_object() {
8162 for query in [
8163 "SELECT { a := 1 } IF true ELSE {}",
8164 "SELECT {} IF true ELSE { a := 1 }",
8165 "SELECT { a := 1 } IF true ELSE {} LIMIT 1",
8166 ] {
8167 let out = compile_and_emit(query);
8168 assert!(
8169 matches!(&out.shape.root, ShapeNode::Object { type_name: None, pointers, .. } if pointers.len() == 1),
8170 "{query}: {:?}\n{}",
8171 out.shape.root,
8172 out.sql
8173 );
8174 }
8175 }
8176
8177 #[test]
8178 fn test_exists_of_a_binding_asks_for_any_row() {
8179 let out = compile_and_emit("WITH people := (SELECT Person) SELECT exists people");
8180 assert!(out.sql.contains("EXISTS(SELECT 1 FROM \"people\")"), "{}", out.sql);
8181
8182 let out = compile_and_emit("WITH names := {'a', 'b'} SELECT exists names");
8183 assert!(
8184 out.sql
8185 .contains("EXISTS(SELECT 1 FROM \"names\" WHERE \"v\" IS NOT NULL)"),
8186 "{}",
8187 out.sql
8188 );
8189 }
8190
8191 #[test]
8192 fn test_a_conditional_insert_beside_a_read_branch_unions_both() {
8193 let out = compile_and_emit(
8194 "WITH existing := (SELECT Person FILTER .name = $n LIMIT 1) \
8195 SELECT (existing IF EXISTS existing ELSE (INSERT Person { name := $n })) { name }",
8196 );
8197 assert!(out.sql.contains("UNION ALL"), "{}", out.sql);
8198 assert!(
8199 out.sql.contains("INSERT INTO") && out.sql.contains("WHERE (NOT EXISTS(SELECT 1 FROM \"existing\"))"),
8200 "the insert carries the condition itself:\n{}",
8201 out.sql
8202 );
8203 }
8204
8205 fn make_schema_with_a_plain_abstract() -> SchemaDescriptor {
8210 let mut schema = make_schema();
8211 let mut archivable = schema
8212 .types
8213 .iter()
8214 .find(|t| t.name == "Person")
8215 .expect("Person is in the test schema")
8216 .clone();
8217 archivable.name = "Archivable".into();
8218 archivable.table = "Archivable".into();
8219 archivable.abstract_ = true;
8220 archivable.materialized = false;
8221 archivable.links = vec![];
8222 archivable.multilinks = vec![];
8223 schema.types.push(archivable);
8224 let person = schema
8225 .types
8226 .iter_mut()
8227 .find(|t| t.name == "Person")
8228 .expect("just found");
8229 person.parents.push("default::Archivable".into());
8230 schema
8231 }
8232
8233 #[test]
8234 fn test_an_intersection_with_a_plain_abstract_fans_out() {
8235 let schema = make_schema_with_a_plain_abstract();
8236 let out = compile_and_emit_with("SELECT Post { who := [is default::Archivable].name }", &schema);
8237 assert!(
8238 !out.sql.contains("\"Archivable\""),
8239 "a plain abstract backs no relation:\n{}",
8240 out.sql
8241 );
8242 assert!(
8243 out.sql.contains("\"public\".\"Person\""),
8244 "read from the type that carries the column:\n{}",
8245 out.sql
8246 );
8247 }
8248
8249 fn make_schema_with_a_linking_plain_abstract() -> SchemaDescriptor {
8253 let mut schema = make_schema_with_a_plain_abstract();
8254 let person_links = schema
8255 .types
8256 .iter()
8257 .find(|t| t.name == "Person")
8258 .expect("Person is in the test schema")
8259 .links
8260 .clone();
8261 let archivable = schema
8262 .types
8263 .iter_mut()
8264 .find(|t| t.name == "Archivable")
8265 .expect("just added");
8266 archivable.links = person_links;
8267 schema
8268 }
8269
8270 #[test]
8271 fn test_a_plain_abstract_in_a_subquery_fans_out() {
8272 let schema = make_schema_with_a_plain_abstract();
8273 let out = compile_and_emit_with(
8274 "SELECT Person FILTER .id IN (SELECT default::Archivable FILTER .age > 1).id",
8275 &schema,
8276 );
8277 assert!(
8278 !out.sql.contains("\"Archivable\""),
8279 "a plain abstract backs no relation:\n{}",
8280 out.sql
8281 );
8282 }
8283
8284 #[test]
8285 fn test_a_backlink_narrowed_to_a_plain_abstract_fans_out() {
8286 let schema = make_schema_with_a_linking_plain_abstract();
8287 let out = compile_and_emit_with(
8288 "SELECT Company FILTER .<company[is default::Archivable].name = 'a'",
8289 &schema,
8290 );
8291 assert!(
8292 !out.sql.contains("\"Archivable\""),
8293 "a plain abstract backs no relation:\n{}",
8294 out.sql
8295 );
8296 }
8297
8298 fn make_interface_schema_with_a_plain_abstract() -> SchemaDescriptor {
8302 let mut schema = make_interface_schema();
8303 let mut archivable = schema
8304 .types
8305 .iter()
8306 .find(|t| t.name == "Individual")
8307 .expect("Individual is in the interface schema")
8308 .clone();
8309 archivable.name = "Archivable".into();
8310 archivable.table = "Archivable".into();
8311 archivable.abstract_ = true;
8312 archivable.materialized = false;
8313 archivable.interfaces = vec![];
8314 archivable.links = vec![];
8315 archivable.multilinks = vec![];
8316 archivable.computed = vec![];
8317 schema.types.push(archivable);
8318 for t in schema.types.iter_mut() {
8319 match t.name.as_str() {
8320 "Account" => t.materialized = true,
8322 "Individual" => t.parents.push("default::Archivable".into()),
8323 _ => {}
8324 }
8325 }
8326 schema
8327 }
8328
8329 #[test]
8330 fn test_a_splat_over_a_plain_abstract_fans_out() {
8331 let schema = make_interface_schema_with_a_plain_abstract();
8332 let out = compile_and_emit_with("SELECT Account { id, [is default::Archivable].* }", &schema);
8333 assert!(
8334 !out.sql.contains("\"Archivable\""),
8335 "a plain abstract backs no relation:\n{}",
8336 out.sql
8337 );
8338 assert!(
8339 out.sql.contains("first_name"),
8340 "the splat still reaches the mixin's own pointers:\n{}",
8341 out.sql
8342 );
8343 }
8344
8345 #[test]
8346 fn test_an_aggregate_over_a_relative_walk_runs_inside_the_subquery() {
8347 let out = compile_and_emit("SELECT Person { latest := max(.posts.title) }");
8350 assert!(out.sql.contains("(SELECT max("), "{}", out.sql);
8351 assert!(!out.sql.contains("max(ARRAY("), "{}", out.sql);
8352 }
8353
8354 fn make_schema_with_a_computed_backlink() -> SchemaDescriptor {
8357 use crate::schema::ComputedDescriptor;
8358 let mut schema = make_schema();
8359 let company = schema
8360 .types
8361 .iter_mut()
8362 .find(|t| t.name == "Company")
8363 .expect("Company is in the test schema");
8364 company.computed = vec![ComputedDescriptor {
8365 name: "staff".into(),
8366 expression: ".<company[is default::Person]".into(),
8367 return_type: None,
8368 link_target: None,
8369 link_multi: false,
8370 }];
8371 schema
8372 }
8373
8374 #[test]
8375 fn test_a_computed_backlink_reads_the_objects_it_lands_on() {
8376 let schema = make_schema_with_a_computed_backlink();
8377 let out = compile_and_emit_with("SELECT Company { staff: { name } }", &schema);
8378 assert!(
8379 out.sql.contains("\"public\".\"Person\""),
8380 "the walk reaches Person:\n{}",
8381 out.sql
8382 );
8383 }
8384
8385 #[test]
8386 fn test_a_computed_backlinks_modifiers_belong_to_what_it_lands_on() {
8387 let schema = make_schema_with_a_computed_backlink();
8390 let out = compile_and_emit_with("SELECT Company.staff { name } FILTER .name = 'ada'", &schema);
8391 assert!(out.sql.contains("'ada'"), "{}", out.sql);
8392 assert!(
8393 out.sql.contains("\"public\".\"Person\""),
8394 "filtered on the Person the walk lands on:\n{}",
8395 out.sql
8396 );
8397 }
8398
8399 #[test]
8400 fn test_an_unreserved_keyword_names_a_free_shape_field() {
8401 let out = compile_and_emit("SELECT { last := 2, first := 1, order := 3 }");
8404 assert!(out.sql.contains("\"last\""), "{}", out.sql);
8405 assert!(out.sql.contains("\"first\""), "{}", out.sql);
8406 assert!(out.sql.contains("\"order\""), "{}", out.sql);
8407 }
8408
8409 #[test]
8410 fn test_a_shaped_sub_select_over_a_type_is_an_object() {
8411 let out = compile_and_emit("SELECT Person { latest := (SELECT Post { title } LIMIT 1) }");
8414 assert!(out.sql.contains("\"title\""), "{}", out.sql);
8415 }
8416
8417 #[test]
8418 fn test_a_walk_inside_a_path_selects_shape_finds_its_row() {
8419 let out = compile_and_emit("SELECT Person.posts { mine := (SELECT Post FILTER .title = .title LIMIT 1) }");
8422 assert!(out.sql.contains("\"public\".\"Post\""), "{}", out.sql);
8423 }
8424
8425 #[test]
8426 fn test_a_walk_off_a_binding_in_a_free_select() {
8427 let out = compile_and_emit("WITH p := (SELECT Person) SELECT { n := p.company.name }");
8430 assert!(out.sql.contains("\"p\""), "{}", out.sql);
8431 assert!(out.sql.contains("\"public\".\"Company\""), "{}", out.sql);
8432 }
8433
8434 #[test]
8435 fn test_a_free_selects_filter_can_root_at_a_binding() {
8436 let out = compile_and_emit("WITH p := (SELECT Person) SELECT { n := 1 } FILTER p.company.name = 'Acme'");
8437 assert!(out.sql.contains("'Acme'"), "{}", out.sql);
8438 }
8439
8440 #[test]
8441 fn test_a_trailing_shape_on_a_field_access_select() {
8442 let out = compile_and_emit("SELECT (SELECT Person).company { name }");
8443 assert!(out.sql.contains("\"name\""), "{}", out.sql);
8444 assert!(out.sql.contains("\"public\".\"Company\""), "{}", out.sql);
8445 }
8446
8447 #[test]
8448 fn test_a_union_of_walks_names_each_branch() {
8449 let out = compile_and_emit(
8452 "WITH p := (SELECT Person FILTER .name = $a), q := (SELECT Person FILTER .name = $b), \
8453 t := (SELECT p.posts UNION q.posts) SELECT t { title }",
8454 );
8455 assert!(out.sql.contains("UNION ALL"), "{}", out.sql);
8456 assert!(out.sql.contains("\"title\""), "{}", out.sql);
8457 }
8458
8459 #[test]
8460 fn test_sibling_for_loops_each_get_their_own_iterator() {
8461 let out = compile_and_emit(
8464 "WITH a := (SELECT Post), b := (SELECT Post), \
8465 x := (FOR p IN a UNION (INSERT Person { name := p.title, age := 1 })), \
8466 y := (FOR p IN b UNION (INSERT Person { name := p.title, age := 2 })) \
8467 SELECT x",
8468 );
8469 assert!(out.sql.contains("\"_for_p\" AS"), "{}", out.sql);
8470 assert!(out.sql.contains("\"_for_p__1\" AS"), "{}", out.sql);
8471 }
8472
8473 #[test]
8474 fn test_a_nested_for_carries_the_outer_loops_key() {
8475 let out = compile_and_emit(
8478 "WITH made := (FOR p IN (SELECT Person) UNION ( \
8479 FOR q IN p.posts UNION (INSERT Company { name := q.title }) \
8480 )) SELECT count(made)",
8481 );
8482 assert!(
8483 out.sql.contains("\"_outer\""),
8484 "the inner iterator carries the bond:\n{}",
8485 out.sql
8486 );
8487 assert!(
8488 out.sql.contains("JOIN \"_for_p\" ON"),
8489 "the insert reads both loop variables:\n{}",
8490 out.sql
8491 );
8492 assert!(
8493 !out.sql.contains("LATERAL (\nINSERT"),
8494 "DML cannot sit in a LATERAL:\n{}",
8495 out.sql
8496 );
8497 }
8498
8499 #[test]
8500 fn test_a_link_value_can_be_a_walk_off_a_binding() {
8501 let mut schema = make_schema();
8504 let company = schema
8505 .types
8506 .iter_mut()
8507 .find(|t| t.name == "Company")
8508 .expect("make_schema declares Company");
8509 company.properties.push(PropertyDescriptor {
8510 name: "id".into(),
8511 pg_type: "uuid".into(),
8512 nullable: false,
8513 default_sql: None,
8514 default_pyql: None,
8515 description: None,
8516 check_constraints: vec![],
8517 is_exclusive: true,
8518 is_pk: true,
8519 is_readonly: true,
8520 rewrites: vec![],
8521 tuple_members: None,
8522 column_type: None,
8523 });
8524 let out = compile_and_emit_with(
8525 "WITH others := (SELECT Person) \
8526 SELECT (INSERT Person { name := $n, age := 1, company := (SELECT others.company LIMIT 1) })",
8527 &schema,
8528 );
8529 assert!(out.sql.contains("\"company_id\""), "{}", out.sql);
8530 }
8531
8532 #[test]
8533 fn test_a_shape_over_a_coalesce_of_walks() {
8534 let schema = make_schema_with_computed_links();
8537 let out = compile_and_emit_with("SELECT Person { x := (.published ?? .plain) { title } }", &schema);
8538 assert!(out.sql.contains("UNION ALL"), "{}", out.sql);
8539 assert!(
8540 out.sql.contains("NOT EXISTS"),
8541 "the later branch only stands in when the first is empty:\n{}",
8542 out.sql
8543 );
8544 }
8545
8546 #[test]
8547 fn test_an_aggregate_over_a_walk_onto_objects_counts_its_rows() {
8548 let out = compile_and_emit("WITH c := (SELECT Company) SELECT c { n := count(.<company[is default::Person]) }");
8552 assert!(out.sql.contains("count("), "{}", out.sql);
8553 assert!(out.sql.contains("\"public\".\"Person\""), "{}", out.sql);
8554 }
8555
8556 #[test]
8557 fn an_aggregate_over_a_computed_set_counts_inside_a_subquery() {
8558 let schema = make_schema_with_computed_links();
8564 let out = compile_and_emit_with("SELECT Person { n := count(.plain) }", &schema);
8565 assert!(
8566 !out.sql.contains("count((SELECT"),
8567 "the aggregate belongs inside the walk's subquery, not around it:\n{}",
8568 out.sql
8569 );
8570 assert!(
8571 out.sql.contains("(SELECT count(") && out.sql.contains("\"public\".\"Post\""),
8572 "it counts the rows the computed's own path lands on:\n{}",
8573 out.sql
8574 );
8575 }
8576
8577 #[test]
8580 fn an_aggregate_over_a_stored_multi_link_still_counts_its_junction_rows() {
8581 let schema = make_schema_with_computed_links();
8582 let out = compile_and_emit_with("SELECT Person { n := count(.posts) }", &schema);
8583 assert!(out.sql.contains("\"public\".\"Person.posts\""), "{}", out.sql);
8584 assert!(
8585 !out.sql.contains("JOIN \"public\".\"Post\""),
8586 "counting junction rows needs no join onto the targets:\n{}",
8587 out.sql
8588 );
8589 }
8590
8591 #[test]
8592 fn test_a_select_can_name_its_own_result() {
8593 let out = compile_and_emit("SELECT oldest := max(Person.age)");
8596 assert!(out.sql.contains("max("), "{}", out.sql);
8597 let filtered = compile_and_emit("SELECT n := Person.age FILTER n > 18");
8598 assert!(
8599 filtered.sql.contains("18"),
8600 "the alias resolves to the result:\n{}",
8601 filtered.sql
8602 );
8603 }
8604
8605 #[test]
8606 fn test_exists_sees_a_computed_pointer() {
8607 let schema = make_schema_with_computed_links();
8611 let out = compile_and_emit_with("SELECT Person FILTER EXISTS .published", &schema);
8612 assert!(out.sql.contains("EXISTS"), "{}", out.sql);
8613 assert!(
8614 out.sql.contains("\"public\".\"Post\""),
8615 "the computed's own path:\n{}",
8616 out.sql
8617 );
8618 }
8619
8620 #[test]
8621 fn test_walking_through_a_computed_field_access() {
8622 use crate::schema::ComputedDescriptor;
8623 let mut schema = make_schema_with_computed_links();
8626 let person = schema.types.iter_mut().find(|t| t.name == "Person").expect("Person");
8627 person.computed.push(ComputedDescriptor {
8628 name: "latest_title".into(),
8629 expression: "(select .posts order by .title desc limit 1).title".into(),
8630 return_type: None,
8631 link_target: None,
8632 link_multi: false,
8633 });
8634 let out = compile_and_emit_with("SELECT Person { x := .latest_title }", &schema);
8635 assert!(out.sql.contains("\"title\""), "{}", out.sql);
8636 assert!(
8637 out.sql.contains("LIMIT"),
8638 "the computed's own limit survives:\n{}",
8639 out.sql
8640 );
8641 }
8642
8643 #[test]
8644 fn test_a_computed_field_access_onto_an_object_takes_a_shape() {
8645 use crate::schema::ComputedDescriptor;
8646 let mut schema = make_schema_with_computed_links();
8649 let post = schema.types.iter_mut().find(|t| t.name == "Post").expect("Post");
8650 post.links.push(LinkDescriptor {
8651 name: "author".into(),
8652 target: "default::Person".into(),
8653 nullable: true,
8654 through: None,
8655 description: None,
8656 default_pyql: None,
8657 is_exclusive: false,
8658 is_readonly: false,
8659 rewrites: vec![],
8660 on_delete: vec![],
8661 });
8662 let person = schema.types.iter_mut().find(|t| t.name == "Person").expect("Person");
8663 person.computed.retain(|c| c.name != "looper");
8664 person.computed.push(ComputedDescriptor {
8665 name: "latest_author".into(),
8666 expression: "((select .posts order by .title desc limit 1)).author".into(),
8667 return_type: None,
8668 link_target: None,
8669 link_multi: false,
8670 });
8671 let out = compile_and_emit_with("SELECT Person { latest_author: { name } }", &schema);
8672 assert!(out.sql.contains("\"name\""), "{}", out.sql);
8673 assert!(
8674 out.sql.contains("\"title\" DESC"),
8675 "the order stays on the posts:\n{}",
8676 out.sql
8677 );
8678 assert!(
8679 out.sql.contains("LIMIT"),
8680 "the computed's own limit survives:\n{}",
8681 out.sql
8682 );
8683 let splat = compile_and_emit_with("SELECT Person { * }", &schema);
8684 assert!(
8685 !splat.sql.contains("latest_author"),
8686 "`*` leaves links out:\n{}",
8687 splat.sql
8688 );
8689 }
8690
8691 #[test]
8692 fn test_a_narrowed_pointer_can_carry_a_shape() {
8693 let out = compile_and_emit("SELECT Company { x := 1, [is default::Person].posts: { title } }");
8697 assert!(out.sql.contains("\"title\""), "{}", out.sql);
8698 assert!(out.sql.contains("\"public\".\"Post\""), "{}", out.sql);
8699 }
8700
8701 #[test]
8702 fn test_select_type_name_as_a_path_step() {
8703 let out = compile_and_emit("SELECT Person.__type__");
8704 assert!(out.sql.contains("ROW('default::Person')"), "{}", out.sql);
8705 assert!(out.sql.contains("\"Person\" AS \"t0\""), "{}", out.sql);
8706 }
8707
8708 #[test]
8709 fn test_aggregate_over_a_with_bound_object_set() {
8710 let out = compile_and_emit("WITH people := (SELECT Person) SELECT max(people.age)");
8711 assert!(out.sql.contains("\"people\" AS ("));
8712 assert!(out.sql.contains("max("));
8713 assert!(!out.sql.contains("FROM \"default\".\"Person\" AS \"t1\""));
8715 }
8716
8717 #[test]
8718 fn test_select_union_of_two_object_bindings() {
8719 let out = compile_and_emit("WITH a := (SELECT Person LIMIT 1), b := (SELECT Person) SELECT (a UNION b)");
8720 assert!(out.sql.contains("SELECT * FROM \"a\""));
8721 assert!(out.sql.contains("UNION ALL"));
8722 assert!(out.sql.contains("SELECT * FROM \"b\""));
8723 }
8724
8725 #[test]
8726 fn test_select_union_of_different_object_types_is_rejected() {
8727 let schema = make_schema();
8728 let ast = parse::parse("WITH a := (SELECT Person), b := (SELECT Company) SELECT (a UNION b)").unwrap();
8729 let Err(err) = ir::compile(&ast, &schema) else {
8730 panic!("union of unrelated types must be rejected")
8731 };
8732 assert!(format!("{err}").contains("UNION"));
8733 }
8734
8735 #[test]
8736 fn test_for_union_as_a_function_body() {
8737 let schema = make_schema();
8738 let descriptor = FunctionDescriptor {
8739 name: "recent".into(),
8740 module: "default".into(),
8741 params: vec![],
8742 return_pg_type: "default::Person".into(),
8743 return_is_object: true,
8744 return_is_set: true,
8745 return_is_polymorphic: false,
8746 volatility: "stable".into(),
8747 body: "for n in {1, 2} union (select Person filter .age = n)".into(),
8748 };
8749 let ir = ir::compile_fn_body(&descriptor, &schema).expect("function body must compile");
8750 let body = emit_fn_body(&ir);
8751 assert!(body.contains("VALUES"), "{body}");
8752 assert!(body.contains("CROSS JOIN LATERAL"), "{body}");
8753 assert!(body.contains("\"_body\".*"), "{body}");
8754 }
8755
8756 #[test]
8757 fn test_with_binding_in_a_computed_reads_the_enclosing_object() {
8758 let out = compile_and_emit("SELECT Person { n := (WITH own := .name SELECT own) }");
8759 assert!(out.sql.contains("\"name\""), "{}", out.sql);
8760 }
8761
8762 #[test]
8763 fn test_subject_path_update_names_its_own_alias() {
8764 let mut schema = make_schema();
8765 let company = schema.types.iter_mut().find(|t| t.name == "Company").unwrap();
8766 company.properties.insert(
8767 0,
8768 PropertyDescriptor {
8769 name: "id".into(),
8770 pg_type: "uuid".into(),
8771 nullable: false,
8772 default_sql: Some("uuidv7()".into()),
8773 default_pyql: None,
8774 description: None,
8775 check_constraints: vec![],
8776 is_exclusive: true,
8777 is_pk: true,
8778 is_readonly: true,
8779 rewrites: vec![],
8780 tuple_members: None,
8781 column_type: None,
8782 },
8783 );
8784 let out = compile_and_emit_with("SELECT (UPDATE Person.company SET { name := 'x' }) { name }", &schema);
8788 assert!(out.sql.contains("\"t1\".\"id\" = ANY("), "{}", out.sql);
8789 }
8790
8791 #[test]
8792 fn test_an_update_past_a_from_returns_only_its_own_columns() {
8793 let out = compile_and_emit(
8797 "SELECT { (UPDATE Person FILTER .name = 'a' SET { company := (INSERT Company { name := 'c' }) }) }",
8798 );
8799 assert!(out.sql.contains("FROM \"_nested_dml_0\""), "{}", out.sql);
8800 assert!(
8801 out.sql.contains("RETURNING \"t0\".*"),
8802 "the update names its own target:\n{}",
8803 out.sql
8804 );
8805 }
8806
8807 #[test]
8808 fn test_nested_insert_in_a_mutation_written_as_a_free_set() {
8809 let out = compile_and_emit(
8810 "SELECT { (UPDATE Person FILTER .name = 'a' SET { company := (INSERT Company { name := 'c' }) }) }",
8811 );
8812 assert!(
8813 out.sql.contains("INSERT INTO \"public\".\"Company\""),
8814 "the hoisted insert is defined, not just referenced:\n{}",
8815 out.sql
8816 );
8817 }
8818
8819 #[test]
8820 fn test_nested_insert_as_a_link_value_in_an_update() {
8821 let out = compile_and_emit(
8825 "SELECT (UPDATE Person FILTER .name = 'a' SET { company := (INSERT Company { name := 'c' }) }) { name }",
8826 );
8827 assert!(out.sql.contains("INSERT INTO \"public\".\"Company\""), "{}", out.sql);
8828 assert!(out.sql.contains("_nested_dml_0"), "{}", out.sql);
8829 assert!(
8830 out.sql.contains("FROM \"_nested_dml_0\""),
8831 "the update reads the hoisted CTE:\n{}",
8832 out.sql
8833 );
8834 }
8835
8836 #[test]
8837 fn test_nested_insert_as_a_link_value() {
8838 let out = compile_and_emit(
8839 "SELECT (INSERT Person { name := 'a', company := (INSERT Company { name := 'c' }) }) { name }",
8840 );
8841 assert!(out.sql.contains("_nested_dml_0"), "{}", out.sql);
8842 assert!(out.sql.contains("INSERT INTO \"public\".\"Company\""), "{}", out.sql);
8843 }
8844
8845 #[test]
8846 fn test_nested_insert_in_a_one_element_set_is_that_insert() {
8847 let out = compile_and_emit(
8848 "SELECT (INSERT Person { name := 'a', company := { (INSERT Company { name := 'c' }) } }) { name }",
8849 );
8850 assert!(out.sql.contains("INSERT INTO \"public\".\"Company\""), "{}", out.sql);
8851 }
8852
8853 #[test]
8854 fn test_insert_with_no_assignments_uses_default_values() {
8855 let out = compile_and_emit("SELECT (INSERT Person { name := 'a', company := (INSERT Company {}) }) { name }");
8856 assert!(out.sql.contains("DEFAULT VALUES"), "{}", out.sql);
8857 }
8858
8859 #[test]
8860 fn test_backlink_narrowed_to_an_interface_its_implementor_declares() {
8861 use crate::schema::{LinkDescriptor, TypeDescriptor};
8864 let mut schema = make_interface_schema();
8865 let individual = schema.types.iter().find(|t| t.name == "Individual").unwrap().clone();
8866 schema.types.push(TypeDescriptor {
8867 name: "Note".into(),
8868 module: "default".into(),
8869 table: "Note".into(),
8870 abstract_: false,
8871 materialized: true,
8872 description: None,
8873 parents: vec![],
8874 interfaces: vec![],
8875 bases: vec![],
8876 properties: individual.properties[..1].to_vec(),
8877 links: vec![],
8878 multilinks: vec![],
8879 computed: vec![],
8880 constraints: vec![],
8881 indexes: vec![],
8882 partition: None,
8883 vector_indexes: vec![],
8884 search_indexes: vec![],
8885 triggers: vec![],
8886 junction: false,
8887 signals: vec![],
8888 });
8889 schema
8890 .types
8891 .iter_mut()
8892 .find(|t| t.name == "Individual")
8893 .unwrap()
8894 .links
8895 .push(LinkDescriptor {
8896 name: "note".into(),
8897 target: "default::Note".into(),
8898 nullable: true,
8899 description: None,
8900 default_pyql: None,
8901 is_exclusive: false,
8902 is_readonly: false,
8903 rewrites: vec![],
8904 on_delete: vec![],
8905 through: None,
8906 });
8907 let out = compile_and_emit_with("SELECT Note FILTER EXISTS .<note[is Account]", &schema);
8908 assert!(out.sql.contains("\"Individual\""), "{}", out.sql);
8909 }
8910
8911 #[test]
8912 fn test_subject_reads_the_row_a_constraint_checks() {
8913 let schema = make_schema();
8914 let sql = ir::compile_constraint_expr("__subject__.age > 18", "default::Person", &schema)
8915 .expect("__subject__ names the row under check");
8916 assert!(sql.contains("\"age\""), "{sql}");
8917 }
8918
8919 #[test]
8920 fn test_free_select_filter_gates_the_result_and_warns() {
8921 let schema = make_schema();
8922 let ast = parse::parse("SELECT count(Person) FILTER (Person.age > 18)").unwrap();
8923 let ir = ir::compile(&ast, &schema).unwrap();
8924 let out = emit(&ir);
8925 assert!(
8926 out.sql.contains("EXISTS("),
8927 "the condition gates the count:\n{}",
8928 out.sql
8929 );
8930 assert!(
8931 ir.warnings.iter().any(|w| w.contains("FILTER clause")),
8932 "a set-valued filter warns: {:?}",
8933 ir.warnings
8934 );
8935 }
8936
8937 #[test]
8938 fn test_free_sub_select_with_modifiers_reads_inline() {
8939 let out = compile_and_emit(
8942 "SELECT Person { n := (WITH mine := .name SELECT count(Post) FILTER (Post.title = mine)) }",
8943 );
8944 assert!(out.sql.contains("_scalar_sub"), "{}", out.sql);
8945 assert!(!out.sql.contains("WITH"), "nothing is hoisted:\n{}", out.sql);
8946 }
8947
8948 #[test]
8949 fn test_correlated_with_binding_is_not_hoisted() {
8950 let out = compile_and_emit("SELECT Person { n := (WITH own := .name SELECT own) }");
8951 assert!(
8952 !out.sql.contains("\"own\" AS ("),
8953 "the binding is inlined:\n{}",
8954 out.sql
8955 );
8956 assert!(out.sql.contains("\"name\""), "{}", out.sql);
8957 }
8958
8959 #[test]
8960 fn test_array_literal_binding_is_readable_and_typed() {
8961 let out = compile_and_emit("WITH order := ['a', 'b'] SELECT std::find(order, 'b')");
8962 assert!(out.sql.contains("array_position"), "its type is known:\n{}", out.sql);
8963 assert!(
8964 out.sql.contains("AS v"),
8965 "the binding exposes a value column:\n{}",
8966 out.sql
8967 );
8968 }
8969
8970 #[test]
8971 fn test_any_over_a_single_boolean_is_that_boolean() {
8972 let out = compile_and_emit("SELECT Person FILTER std::any(.age > 18)");
8973 assert!(!out.sql.contains("bool_or"), "no aggregate in WHERE:\n{}", out.sql);
8974 assert!(out.sql.contains("> 18"), "{}", out.sql);
8975 }
8976
8977 #[test]
8978 fn test_comparing_a_value_to_a_set_is_membership() {
8979 let schema = make_schema_with_computed_links();
8980 let out = compile_and_emit_with("SELECT Person FILTER .name = Person.posts.title", &schema);
8981 assert!(out.sql.contains("= ANY("), "{}", out.sql);
8982 }
8983
8984 #[test]
8985 fn test_for_over_a_with_binding_iterates_every_row() {
8986 let out = compile_and_emit(
8987 "WITH names := (SELECT Person.name) FOR n IN names UNION (SELECT Person FILTER .name = n)",
8988 );
8989 assert!(out.sql.contains("FROM \"names\""), "{}", out.sql);
8990 assert!(!out.sql.contains("VALUES"), "the whole set is iterated:\n{}", out.sql);
8991 }
8992
8993 #[test]
8994 fn test_for_body_bindings_stay_inside_the_body() {
8995 let out = compile_and_emit(
8998 "WITH names := (SELECT Person.name) \
8999 FOR n IN names UNION (WITH matching := (SELECT Person FILTER .name = n) SELECT matching)",
9000 );
9001 let lateral = out.sql.find("CROSS JOIN LATERAL").expect("a lateral body");
9002 let binding = out.sql.find("\"matching\" AS (").expect("the body's binding");
9003 assert!(binding > lateral, "the binding sits inside the body:\n{}", out.sql);
9004 }
9005
9006 #[test]
9007 fn test_array_valued_binding_picks_the_array_overload() {
9008 let out = compile_and_emit("WITH l := <array<std::str>>$0 SELECT std::find(l, 'a')");
9011 assert!(out.sql.contains("array_position"), "{}", out.sql);
9012 assert!(!out.sql.contains("strpos"), "{}", out.sql);
9013 }
9014
9015 #[test]
9016 fn test_aggregate_over_a_path_in_a_filter_is_a_subquery() {
9017 let out = compile_and_emit("SELECT Person FILTER .age = std::max(Person.age)");
9018 assert!(
9019 out.sql.contains("(SELECT max("),
9020 "an aggregate in WHERE needs its own query:\n{}",
9021 out.sql
9022 );
9023 }
9024
9025 #[test]
9026 fn test_distinct_over_a_single_value_is_that_value() {
9027 let out = compile_and_emit("SELECT Person { n := (DISTINCT .name) }");
9028 assert!(out.sql.contains("\"name\""), "{}", out.sql);
9029 assert!(!out.sql.contains("DISTINCT \""), "{}", out.sql);
9030 }
9031
9032 #[test]
9033 fn test_narrowing_an_interface_joins_the_implementor_table() {
9034 let mut schema = make_interface_schema();
9037 let account = schema.types.iter_mut().find(|t| t.name == "Account").unwrap();
9038 account.materialized = true;
9039 let out = compile_and_emit_with("SELECT Account[is Individual].first_name", &schema);
9040 assert!(
9041 out.sql.contains("JOIN \"public\".\"Individual\""),
9042 "the implementor's own table is joined:\n{}",
9043 out.sql
9044 );
9045 assert!(out.sql.contains("\"first_name\""), "{}", out.sql);
9046 }
9047
9048 #[test]
9049 fn test_sub_select_shape_declares_names_its_filter_and_projection_read() {
9050 let out = compile_and_emit("SELECT Person { n := (SELECT .posts { t := .title } FILTER .t = 'x' LIMIT 1).t }");
9051 assert!(out.sql.contains("'x'"), "{}", out.sql);
9052 assert!(out.sql.contains("\"title\""), "{}", out.sql);
9053 }
9054
9055 #[test]
9056 fn test_assert_over_an_object_set_returns_rows() {
9057 let out = compile_and_emit("SELECT std::assert_distinct((SELECT Person))");
9058 assert!(
9059 out.sql.contains("\"assert_distinct\"(ARRAY(SELECT \"id\""),
9060 "{}",
9061 out.sql
9062 );
9063 assert!(out.sql.contains("= ANY("), "{}", out.sql);
9064 assert!(out.sql.contains("\"Person\" AS \"t1\""), "{}", out.sql);
9065 assert!(!out.sql.contains("unnest("), "{}", out.sql);
9066 }
9067
9068 #[test]
9069 fn test_assert_over_a_for_union_reads_its_rows() {
9070 let out =
9071 compile_and_emit("SELECT std::assert_distinct((FOR n IN {1, 2} UNION (SELECT Person FILTER .age = n)))");
9072 assert!(out.sql.contains("CROSS JOIN LATERAL"), "{}", out.sql);
9073 assert!(out.sql.contains("= ANY("), "{}", out.sql);
9074 }
9075
9076 #[test]
9077 fn test_declared_computed_with_binding_reads_its_own_object() {
9078 use crate::schema::ComputedDescriptor;
9081 let schema = make_schema();
9082 let cd = ComputedDescriptor {
9083 name: "shouted".into(),
9084 expression: "(with own := .name select std::str_upper(own))".into(),
9085 return_type: None,
9086 link_target: None,
9087 link_multi: false,
9088 };
9089 let ir = ir::compile_computed_in_type(&cd, "default::Person", &schema)
9090 .expect("a computed may read the object it is declared on");
9091 assert!(ir.is_some());
9092 }
9093
9094 #[test]
9095 fn test_sub_select_modifiers_scope_to_its_own_subject() {
9096 let mut schema = make_schema();
9099 let post = schema
9100 .types
9101 .iter_mut()
9102 .find(|t| t.name == "Post")
9103 .expect("Post is in the test schema");
9104 post.links.push(LinkDescriptor {
9105 name: "author".into(),
9106 target: "default::Person".into(),
9107 nullable: true,
9108 description: None,
9109 default_pyql: None,
9110 is_exclusive: false,
9111 is_readonly: false,
9112 rewrites: vec![],
9113 on_delete: vec![],
9114 through: None,
9115 });
9116 let out = compile_and_emit_with(
9117 "SELECT Person { a := (SELECT .posts FILTER .title = 'x' LIMIT 1).author.name }",
9118 &schema,
9119 );
9120 assert!(out.sql.contains("'x'"), "{}", out.sql);
9121 }
9122
9123 #[test]
9124 fn test_free_select_set_literal() {
9125 let schema = make_schema();
9126 let ast = parse::parse("SELECT {1, 2, 3}").unwrap();
9127 let ir = ir::compile(&ast, &schema).unwrap();
9128 let out = emit(&ir);
9129 assert_eq!(out.sql.matches("UNION ALL").count(), 2);
9131 assert!(out.sql.contains("1 AS v"));
9132 assert!(out.sql.contains("2 AS v"));
9133 assert!(out.sql.contains("3 AS v"));
9134 assert!(out.sql.contains("ROW(v) AS result"));
9135 assert!(matches!(out.shape.root, crate::query::ShapeNode::Scalar { .. }));
9136 }
9137
9138 #[test]
9139 fn test_free_select_free_object() {
9140 let schema = make_schema();
9141 let ast = parse::parse("SELECT { foo := 'bar', n := 42 }").unwrap();
9142 let ir = ir::compile(&ast, &schema).unwrap();
9143 let out = emit(&ir);
9144 assert!(out.sql.contains("'bar'"));
9145 assert!(out.sql.contains("42"));
9146 assert!(out.sql.contains("AS result"));
9147 let crate::query::ShapeNode::Object {
9149 pointers, type_name, ..
9150 } = &out.shape.root
9151 else {
9152 panic!("expected Object shape")
9153 };
9154 assert!(type_name.is_none());
9155 assert_eq!(pointers.len(), 2);
9156 assert!(matches!(&pointers[0], crate::query::ShapeNode::Scalar { name, position: 0 } if name == "foo"));
9157 assert!(matches!(&pointers[1], crate::query::ShapeNode::Scalar { name, position: 1 } if name == "n"));
9158 }
9159
9160 #[test]
9161 fn test_free_select_object_with_enum_field_casts_to_text_and_tags_shape() {
9162 let mut schema = make_schema();
9170 schema.enums.push(crate::schema::EnumDescriptor {
9171 name: "Gender".into(),
9172 module: "default".into(),
9173 members: vec!["Male".into(), "Female".into()],
9174 });
9175 let out = compile_and_emit_with("select { gender := default::Gender.Male }", &schema);
9176 assert!(
9183 out.sql.contains("'Male'::\"public\".\"Gender\""),
9184 "expected the enum literal, got:\n{}",
9185 out.sql
9186 );
9187 assert!(
9188 out.sql.contains("ROW(\"_f0\"::text) AS result"),
9189 "expected the ROW composite to cast the enum field to text, got:\n{}",
9190 out.sql
9191 );
9192 let crate::query::ShapeNode::Object { pointers, .. } = &out.shape.root else {
9193 panic!("expected Object shape")
9194 };
9195 assert_eq!(pointers.len(), 1);
9196 assert!(
9197 matches!(
9198 &pointers[0],
9199 crate::query::ShapeNode::Enum { name, position: 0, enum_type }
9200 if name == "gender" && enum_type == "public::Gender"
9201 ),
9202 "expected Enum-tagged shape, got: {:?}",
9203 pointers[0]
9204 );
9205 }
9206
9207 #[test]
9208 fn test_free_select_bare_enum_literal_casts_to_text_inside_row() {
9209 let mut schema = make_schema();
9213 schema.enums.push(crate::schema::EnumDescriptor {
9214 name: "Gender".into(),
9215 module: "default".into(),
9216 members: vec!["Male".into(), "Female".into()],
9217 });
9218 let out = compile_and_emit_with("select default::Gender.Male", &schema);
9219 assert!(
9220 out.sql.contains("ROW(v::text) AS result"),
9221 "expected ROW(v::text), got:\n{}",
9222 out.sql
9223 );
9224 assert!(
9225 matches!(
9226 &out.shape.root,
9227 crate::query::ShapeNode::Enum { enum_type, .. } if enum_type == "public::Gender"
9228 ),
9229 "expected Enum-tagged shape, got: {:?}",
9230 out.shape.root
9231 );
9232 }
9233
9234 #[test]
9237 fn test_for_update_defaults_to_blocking() {
9238 let out = compile_and_emit("SELECT Person FOR UPDATE");
9239 assert!(out.sql.trim_end().ends_with("FOR UPDATE"), "got:\n{}", out.sql);
9240 }
9241
9242 #[test]
9243 fn test_for_update_skip_locked() {
9244 let out = compile_and_emit("SELECT Person FOR UPDATE SKIP LOCKED");
9245 assert!(
9246 out.sql.trim_end().ends_with("FOR UPDATE SKIP LOCKED"),
9247 "got:\n{}",
9248 out.sql
9249 );
9250 }
9251
9252 #[test]
9253 fn test_for_update_nowait() {
9254 let out = compile_and_emit("SELECT Person FOR UPDATE NOWAIT");
9255 assert!(out.sql.trim_end().ends_with("FOR UPDATE NOWAIT"), "got:\n{}", out.sql);
9256 }
9257
9258 #[test]
9259 fn test_for_no_key_update_skip_locked() {
9260 let out = compile_and_emit("SELECT Person FOR NO KEY UPDATE SKIP LOCKED");
9261 assert!(
9262 out.sql.trim_end().ends_with("FOR NO KEY UPDATE SKIP LOCKED"),
9263 "got:\n{}",
9264 out.sql
9265 );
9266 }
9267
9268 #[test]
9269 fn test_for_share() {
9270 let out = compile_and_emit("SELECT Person FOR SHARE");
9271 assert!(out.sql.trim_end().ends_with("FOR SHARE"), "got:\n{}", out.sql);
9272 }
9273
9274 #[test]
9275 fn test_for_key_share_nowait() {
9276 let out = compile_and_emit("SELECT Person FOR KEY SHARE NOWAIT");
9277 assert!(
9278 out.sql.trim_end().ends_with("FOR KEY SHARE NOWAIT"),
9279 "got:\n{}",
9280 out.sql
9281 );
9282 }
9283
9284 #[test]
9285 fn test_for_update_comes_after_order_by_limit_offset_in_emitted_sql() {
9286 let out = compile_and_emit("SELECT Person { name } ORDER BY .name OFFSET 1 LIMIT 5 FOR UPDATE SKIP LOCKED");
9289 let order_pos = out.sql.find("ORDER BY").unwrap();
9290 let offset_pos = out.sql.find("OFFSET").unwrap();
9291 let limit_pos = out.sql.find("LIMIT").unwrap();
9292 let for_pos = out.sql.find("FOR UPDATE").unwrap();
9293 assert!(
9294 order_pos < offset_pos && offset_pos < limit_pos && limit_pos < for_pos,
9295 "got:\n{}",
9296 out.sql
9297 );
9298 }
9299
9300 #[test]
9301 fn test_for_update_combined_with_distinct_is_rejected() {
9302 let ast = parse::parse("SELECT DISTINCT Person FOR UPDATE").expect("parse failed");
9303 let err = ir::compile(&ast, &make_schema())
9304 .err()
9305 .expect("expected a compile error");
9306 assert!(err.to_string().contains("DISTINCT"), "unexpected: {err}");
9307 }
9308
9309 #[test]
9310 fn test_for_update_combined_with_select_over_insert_is_rejected() {
9311 let ast = parse::parse("SELECT (INSERT Person { name := 'Alice' }) { name } FOR UPDATE").expect("parse failed");
9312 let err = ir::compile(&ast, &make_schema())
9313 .err()
9314 .expect("expected a compile error");
9315 assert!(err.to_string().contains("INSERT"), "unexpected: {err}");
9316 }
9317
9318 #[test]
9319 fn test_for_update_on_an_interface_type_is_rejected() {
9320 fn id_prop() -> PropertyDescriptor {
9321 PropertyDescriptor {
9322 name: "id".into(),
9323 pg_type: "uuid".into(),
9324 nullable: false,
9325 default_sql: Some("uuidv7()".into()),
9326 default_pyql: None,
9327 description: None,
9328 check_constraints: vec![],
9329 is_exclusive: true,
9330 is_pk: true,
9331 is_readonly: true,
9332 rewrites: vec![],
9333 tuple_members: None,
9334 column_type: None,
9335 }
9336 }
9337 let schema = SchemaDescriptor {
9338 types: vec![
9339 TypeDescriptor {
9340 name: "Account".into(),
9341 module: "default".into(),
9342 table: "Account".into(),
9343 abstract_: true,
9344 materialized: true,
9345 description: None,
9346 parents: vec![],
9347 interfaces: vec![],
9348 bases: vec![],
9349 properties: vec![id_prop()],
9350 links: vec![],
9351 multilinks: vec![],
9352 computed: vec![],
9353 constraints: vec![],
9354 indexes: vec![],
9355 partition: None,
9356 vector_indexes: vec![],
9357 search_indexes: vec![],
9358 triggers: vec![],
9359 junction: false,
9360 signals: vec![],
9361 },
9362 TypeDescriptor {
9363 name: "Individual".into(),
9364 module: "default".into(),
9365 table: "Individual".into(),
9366 abstract_: false,
9367 materialized: true,
9368 description: None,
9369 parents: vec![],
9370 interfaces: vec!["default::Account".into()],
9371 bases: vec![],
9372 properties: vec![id_prop()],
9373 links: vec![],
9374 multilinks: vec![],
9375 computed: vec![],
9376 constraints: vec![],
9377 indexes: vec![],
9378 partition: None,
9379 vector_indexes: vec![],
9380 search_indexes: vec![],
9381 triggers: vec![],
9382 junction: false,
9383 signals: vec![],
9384 },
9385 ],
9386 scalars: vec![],
9387 enums: vec![],
9388 named_tuples: vec![],
9389 globals: vec![],
9390 functions: vec![],
9391 aliases: vec![],
9392 channels: vec![],
9393 ..Default::default()
9394 };
9395 let ast = parse::parse("SELECT Account FOR UPDATE").expect("parse failed");
9396 let err = ir::compile(&ast, &schema).err().expect("expected a compile error");
9397 assert!(err.to_string().contains("interface"), "unexpected: {err}");
9398 }
9399
9400 #[test]
9401 fn test_schema_select_distinct_emits_distinct_keyword() {
9402 let out = compile_and_emit("SELECT DISTINCT Person { name }");
9407 assert!(out.sql.contains("DISTINCT"), "expected DISTINCT in SQL:\n{}", out.sql);
9408 }
9409
9410 #[test]
9411 fn test_schema_select_detached_compiles_as_ordinary_select() {
9412 let out = compile_and_emit("SELECT DETACHED Person { name }");
9416 assert!(
9417 out.sql.contains("\"name\""),
9418 "expected name column in SQL:\n{}",
9419 out.sql
9420 );
9421 }
9422
9423 #[test]
9424 fn test_detached_subquery_correlates_against_the_outer_row() {
9425 let out = compile_and_emit(
9432 "SELECT Person { name } FILTER NOT EXISTS (\
9433 SELECT DETACHED Person FILTER .name = Person.name AND .age > Person.age)",
9434 );
9435 assert!(
9436 out.sql.contains("\"t1\".\"name\" = \"t0\".\"name\""),
9437 "the inner row should be compared against the outer one:\n{}",
9438 out.sql
9439 );
9440 assert!(
9441 !out.sql.contains("\"t1\".\"name\" = \"t1\".\"name\""),
9442 "self-comparison means the correlation was lost:\n{}",
9443 out.sql
9444 );
9445 }
9446
9447 #[test]
9448 fn test_an_assert_on_a_pointer_checks_its_rows() {
9449 let out = compile_and_emit("SELECT Person { name, p := assert_exists(.posts { title }) }");
9453 assert!(
9454 out.sql
9455 .contains(r#"cardinality("_pylon"."assert_exists"("_a"."v"::text[]))"#),
9456 "the assert should read the aggregated rows:\n{}",
9457 out.sql
9458 );
9459 assert_eq!(
9460 out.sql.matches("array_agg(ROW(").count(),
9461 1,
9462 "the pointer's subquery should be evaluated once, not once per use:\n{}",
9463 out.sql
9464 );
9465 }
9466
9467 #[test]
9468 fn test_an_assert_on_a_scalar_stays_an_ordinary_call() {
9469 let out = compile_and_emit("SELECT Person { n := assert_exists(.name) }");
9472 assert!(
9473 !out.sql.contains(r#""_a"."v""#),
9474 "a scalar assert should not be wrapped as a pointer check:\n{}",
9475 out.sql
9476 );
9477 }
9478
9479 #[test]
9480 fn test_a_backlink_tail_walks_past_the_first_step() {
9481 let out = compile_and_emit("SELECT Post FILTER any(.<posts[is Person].posts.title = $t)");
9486 assert!(
9487 out.sql.contains(r#""public"."Person.posts""#),
9488 "the tail should walk the multi-link's junction:\n{}",
9489 out.sql
9490 );
9491 }
9492
9493 #[test]
9494 fn test_an_intersection_narrows_the_rest_of_a_multilink_walk() {
9495 let out = compile_and_emit("SELECT Person FILTER any(.posts[is Post].title = $t)");
9499 assert!(
9500 out.sql.contains(r#""public"."Post""#),
9501 "the walk should continue from the narrowed type:\n{}",
9502 out.sql
9503 );
9504 }
9505
9506 #[test]
9507 fn test_a_union_of_bindings_emits_selects_not_bare_names() {
9508 let out = compile_and_emit_with(
9512 "WITH a := (INSERT Post { title := $t1 }), b := (INSERT Post { title := $t2 }) \
9513 SELECT (INSERT Person { name := $n, posts := (a UNION b) })",
9514 &make_schema(),
9515 );
9516 assert!(
9517 out.sql.contains(r#"(SELECT "_s"."id" FROM "a" AS "_s")"#),
9518 "each union operand should be a select:\n{}",
9519 out.sql
9520 );
9521 }
9522
9523 #[test]
9524 fn test_a_bare_narrowing_reads_as_a_type_check_and_as_an_object() {
9525 for query in [
9529 "SELECT Person FILTER EXISTS [is Person]",
9530 "SELECT Person { p := [is Person] { name } }",
9531 ] {
9532 let ast = parse::parse(query).unwrap_or_else(|e| panic!("{query}: {e}"));
9533 assert!(ir::compile(&ast, &make_schema()).is_ok(), "{query} should compile");
9534 }
9535 }
9536
9537 #[test]
9538 fn test_a_multilink_takes_a_set_wrapped_in_a_select() {
9539 let out = compile_and_emit_with(
9544 "WITH a := (INSERT Post { title := $t1 }), b := (INSERT Post { title := $t2 }) \
9545 SELECT (INSERT Person { name := $n, posts := (SELECT { a, b }) })",
9546 &make_schema(),
9547 );
9548 assert!(
9549 out.sql.contains(r#""public"."Person.posts""#),
9550 "both targets should reach the junction:\n{}",
9551 out.sql
9552 );
9553 }
9554
9555 #[test]
9556 fn test_an_aggregate_over_a_subselected_path() {
9557 let out = compile_and_emit("SELECT array_agg((SELECT Person.name))");
9563 assert!(
9564 out.sql.contains("array_agg") && out.sql.contains(r#""public"."Person""#),
9565 "the aggregate should wrap the walk's column:\n{}",
9566 out.sql
9567 );
9568 }
9569
9570 #[test]
9571 fn test_a_distinct_aggregate_argument_reaches_the_aggregate() {
9572 for query in [
9578 "SELECT array_agg(DISTINCT Person.name)",
9579 "SELECT array_agg((SELECT DISTINCT Person.name))",
9580 ] {
9581 let out = compile_and_emit(query);
9582 assert!(
9583 out.sql.contains("array_agg(DISTINCT "),
9584 "{query} should deduplicate inside the aggregate:\n{}",
9585 out.sql
9586 );
9587 }
9588 }
9589
9590 #[test]
9591 fn test_is_not_negates_the_type_check() {
9592 let positive = compile_and_emit("SELECT Person FILTER Person IS Person");
9596 let negative = compile_and_emit("SELECT Person FILTER Person IS NOT Person");
9597 assert!(
9598 negative.sql.contains("NOT") && !positive.sql.contains("NOT"),
9599 "the negation should reach the SQL:\npositive:\n{}\nnegative:\n{}",
9600 positive.sql,
9601 negative.sql
9602 );
9603 }
9604
9605 #[test]
9606 fn test_a_cast_takes_a_prefix_operator_as_its_operand() {
9607 for query in ["SELECT <bool>EXISTS (SELECT Person LIMIT 1)", "SELECT <int64>-1"] {
9611 let ast = parse::parse(query).unwrap_or_else(|e| panic!("{query} should parse: {e}"));
9612 assert!(ir::compile(&ast, &make_schema()).is_ok(), "{query} should compile");
9613 }
9614 }
9615
9616 #[test]
9617 fn test_a_bare_multilink_reads_as_a_set() {
9618 let out =
9623 compile_and_emit("WITH p := (SELECT DETACHED Post LIMIT 1) SELECT Person { name } FILTER p IN .posts");
9624 assert!(
9625 out.sql.contains(r#""public"."Person.posts""#),
9626 "the membership test should read the link's junction:\n{}",
9627 out.sql
9628 );
9629 }
9630
9631 #[test]
9632 fn test_a_select_reads_its_own_shape_computed_in_its_clauses() {
9633 let out = compile_and_emit("SELECT Person { n := .name } ORDER BY .n ASC");
9637 assert!(
9638 out.sql.contains("ORDER BY"),
9639 "the order by should compile against the declared pointer:\n{}",
9640 out.sql
9641 );
9642 }
9643
9644 #[test]
9645 fn test_a_declared_pointer_can_be_walked_through() {
9646 let out = compile_and_emit_with(
9650 "WITH p := (SELECT Person { boss := .company } LIMIT 1) SELECT p { name, boss_name := .boss.name }",
9651 &make_schema(),
9652 );
9653 assert!(
9654 out.sql.contains(r#""public"."Company""#),
9655 "the walk should reach the declared pointer's own target table:\n{}",
9656 out.sql
9657 );
9658 }
9659
9660 #[test]
9661 fn test_a_replaced_subject_shape_stays_in_scope() {
9662 let out = compile_and_emit("SELECT Person { c := (SELECT .company { n := .name }) { n } }");
9667 assert!(
9668 out.sql.contains(r#"'default::Company'::text"#) && out.sql.contains(r#""t2"."name""#),
9669 "`n` should resolve to the company's own column:\n{}",
9670 out.sql
9671 );
9672 }
9673
9674 #[test]
9675 fn test_a_sibling_shape_pointer_is_not_in_scope() {
9676 let schema = make_schema();
9679 let ast = parse::parse("SELECT Person { n := .name, copy := .n }").unwrap();
9680 assert!(ir::compile(&ast, &schema).is_err());
9681 }
9682
9683 #[test]
9684 fn test_shape_over_a_with_binding_reads_the_binding() {
9685 let out = compile_and_emit("WITH c := (SELECT Company LIMIT 1) SELECT Person { name, employer := c { name } }");
9690 assert!(
9691 out.sql.contains("\"c\""),
9692 "the pointer should read the binding's CTE:\n{}",
9693 out.sql
9694 );
9695 }
9696
9697 #[test]
9698 fn test_shape_over_a_bare_type_name_is_still_rejected() {
9699 let schema = make_schema();
9702 let ast = parse::parse("SELECT Person { name, other := Company { name } }").unwrap();
9703 assert!(ir::compile(&ast, &schema).is_err());
9704 }
9705
9706 #[test]
9707 fn test_nested_shape_reads_a_prefix_bound_by_the_outer_select() {
9708 let out = compile_and_emit("SELECT Person { name, posts: { title, who := Person.name } }");
9713 assert!(
9714 out.sql.contains(r#""t0"."name""#),
9715 "the nested computed should read the outer Person alias:\n{}",
9716 out.sql
9717 );
9718 assert!(
9719 !out.sql.contains(r#"FROM "public"."Person" "t2""#),
9720 "the prefix should not open a second Person source:\n{}",
9721 out.sql
9722 );
9723 }
9724
9725 #[test]
9726 fn test_prefix_naming_a_type_out_of_scope_is_rejected() {
9727 let schema = make_schema();
9730 let ast = parse::parse("SELECT Person { name, who := Company.name }").unwrap();
9731 assert!(ir::compile(&ast, &schema).is_err());
9732 }
9733
9734 #[test]
9735 fn test_free_select_tuple() {
9736 let schema = make_schema();
9737 let ast = parse::parse("SELECT (1, 2)").unwrap();
9738 let ir = ir::compile(&ast, &schema).unwrap();
9739 let out = emit(&ir);
9740 assert!(out.sql.contains("1"));
9741 assert!(out.sql.contains("2"));
9742 assert!(out.sql.contains("AS result"));
9743 assert!(matches!(out.shape.root, crate::query::ShapeNode::Tuple { .. }));
9744 }
9745
9746 #[test]
9747 fn test_free_select_scalar_literal() {
9748 let schema = make_schema();
9749 let ast = parse::parse("SELECT 'hello'").unwrap();
9750 let ir = ir::compile(&ast, &schema).unwrap();
9751 let out = emit(&ir);
9752 assert!(out.sql.contains("SELECT 'hello' AS v"));
9753 assert!(out.sql.contains("ROW(v) AS result"));
9754 assert!(matches!(out.shape.root, crate::query::ShapeNode::Scalar { .. }));
9755 }
9756
9757 #[test]
9758 fn test_float_literal_casts_to_float8() {
9759 let out = compile_and_emit("SELECT 1.0");
9766 assert!(
9767 out.sql.contains("(1.0::float8)"),
9768 "expected explicit float8 cast, got:\n{}",
9769 out.sql
9770 );
9771 }
9772
9773 #[test]
9774 fn test_free_select_array_literal() {
9775 let schema = make_schema();
9776 let ast = parse::parse("SELECT [1, 2, 3]").unwrap();
9777 let ir = ir::compile(&ast, &schema).unwrap();
9778 let out = emit(&ir);
9779 assert!(out.sql.contains("SELECT ARRAY[1, 2, 3] AS v"), "{}", out.sql);
9780 assert!(out.sql.contains("SELECT v AS result, v FROM"), "{}", out.sql);
9781 assert!(matches!(out.shape.root, crate::query::ShapeNode::RawScalar));
9782 }
9783
9784 #[test]
9785 fn test_select_scalars() {
9786 let out = compile_and_emit("SELECT Person { name, age }");
9787 assert!(out.sql.contains("'default::Person'::text"));
9788 assert!(out.sql.contains("\"name\"::text"));
9789 assert!(out.sql.contains("\"age\"::int8"));
9790 assert!(out.sql.contains("FROM \"public\".\"Person\""));
9791 assert!(out.sql.contains(") AS result"));
9792 }
9793
9794 #[test]
9795 fn test_select_filter_param() {
9796 let out = compile_and_emit("SELECT Person { name } FILTER .name = $name");
9797 assert!(out.sql.contains("WHERE"));
9798 assert!(out.sql.contains("$1"));
9799 }
9800
9801 #[test]
9802 fn test_filter_in_set_literal_compiles_to_any_array() {
9803 let out = compile_and_emit("SELECT Person { name } FILTER .name IN {'Carol'}");
9804 assert!(out.sql.contains("= ANY(ARRAY['Carol'])"), "got:\n{}", out.sql);
9805 }
9806
9807 #[test]
9808 fn test_filter_in_multi_element_set_literal_compiles_to_any_array() {
9809 let out = compile_and_emit("SELECT Person { name } FILTER .name IN {'Carol', 'Bob'}");
9810 assert!(out.sql.contains("= ANY(ARRAY['Carol', 'Bob'])"), "got:\n{}", out.sql);
9811 }
9812
9813 #[test]
9820 fn test_filter_in_array_unpack_passes_the_array_straight_to_any() {
9821 let out = compile_and_emit("SELECT Person { name } FILTER .name IN std::array_unpack(<array<str>>$names)");
9822 assert!(
9823 !out.sql.contains("ANY(unnest("),
9824 "unnest inside ANY is invalid in WHERE:\n{}",
9825 out.sql
9826 );
9827 assert!(out.sql.contains("= ANY("), "got:\n{}", out.sql);
9828 }
9829
9830 #[test]
9831 fn test_filter_not_in_array_unpack_passes_the_array_straight_to_all() {
9832 let out = compile_and_emit("SELECT Person { name } FILTER .name NOT IN std::array_unpack(<array<str>>$names)");
9833 assert!(
9834 !out.sql.contains("ALL(unnest("),
9835 "unnest inside ALL is invalid in WHERE:\n{}",
9836 out.sql
9837 );
9838 assert!(out.sql.contains("<> ALL("), "got:\n{}", out.sql);
9839 }
9840
9841 #[test]
9846 fn test_all_over_unpacked_array_aggregates_the_array_not_the_multilink() {
9847 let out = compile_and_emit(
9848 "SELECT Person { name } FILTER all(std::array_unpack(<array<str>>$titles) IN .posts.title)",
9849 );
9850 assert!(out.sql.contains("bool_and("), "got:\n{}", out.sql);
9851 assert!(out.sql.contains("_unnested"), "got:\n{}", out.sql);
9852 }
9853
9854 #[test]
9858 fn test_any_over_a_multilink_against_an_unpacked_array_still_folds_into_any() {
9859 let out = compile_and_emit(
9860 "SELECT Person { name } FILTER any(.posts.title IN std::array_unpack(<array<str>>$titles))",
9861 );
9862 assert!(!out.sql.contains("unnest("), "got:\n{}", out.sql);
9863 assert!(out.sql.contains("= ANY("), "got:\n{}", out.sql);
9864 }
9865
9866 #[test]
9867 fn test_any_over_unpacked_array_aggregates_the_array_not_the_multilink() {
9868 let out = compile_and_emit(
9869 "SELECT Person { name } FILTER any(std::array_unpack(<array<str>>$titles) IN .posts.title)",
9870 );
9871 assert!(out.sql.contains("bool_or("), "got:\n{}", out.sql);
9872 assert!(out.sql.contains("_unnested"), "got:\n{}", out.sql);
9873 }
9874
9875 fn schema_with_array_property() -> SchemaDescriptor {
9877 let mut schema = make_schema();
9878 schema.types[0].properties.push(crate::schema::PropertyDescriptor {
9879 name: "perms".into(),
9880 pg_type: "text[]".into(),
9881 nullable: true,
9882 default_sql: None,
9883 default_pyql: None,
9884 description: None,
9885 check_constraints: vec![],
9886 is_exclusive: false,
9887 is_pk: false,
9888 is_readonly: false,
9889 rewrites: vec![],
9890 tuple_members: None,
9891 column_type: None,
9892 });
9893 schema
9894 }
9895
9896 #[test]
9902 fn test_aggregate_over_an_unpacked_walk_unnests_in_a_row_source() {
9903 let out = compile_and_emit_with(
9904 "WITH teams := (SELECT Person { perms } FILTER .age > 1) SELECT std::array_agg(std::array_unpack(teams.perms))",
9905 &schema_with_array_property(),
9906 );
9907 assert!(!out.sql.contains("array_agg(unnest("), "got:\n{}", out.sql);
9908 assert!(
9909 out.sql.contains(
9910 r#"coalesce(array_agg("_s"."v"), '{}') FROM unnest(ARRAY(SELECT unnest("t1"."perms") FROM "teams" AS "t1"#
9911 ),
9912 "got:\n{}",
9913 out.sql
9914 );
9915 }
9916
9917 #[test]
9918 fn test_aggregate_over_an_unpacked_type_rooted_path_unnests_in_a_row_source() {
9919 let out = compile_and_emit_with(
9920 "SELECT std::count(std::array_unpack(Person.perms))",
9921 &schema_with_array_property(),
9922 );
9923 assert!(!out.sql.contains("count(unnest("), "got:\n{}", out.sql);
9924 assert!(
9925 out.sql
9926 .contains(r#"FROM unnest(ARRAY(SELECT unnest("t0"."perms") FROM "public"."Person" AS "t0"#),
9927 "got:\n{}",
9928 out.sql
9929 );
9930 }
9931
9932 #[test]
9935 fn test_aggregate_over_an_unpacked_parameter_unnests_the_array_itself() {
9936 let out = compile_and_emit("SELECT std::array_agg(std::array_unpack(<array<str>>$names))");
9937 assert!(!out.sql.contains("array_agg(unnest("), "got:\n{}", out.sql);
9938 assert!(
9939 out.sql.contains(r#"FROM unnest(($1)::text[]) AS "_s"("v")"#),
9940 "got:\n{}",
9941 out.sql
9942 );
9943 }
9944
9945 #[test]
9946 fn test_aggregate_over_an_unpacked_property_unnests_that_row_s_array() {
9947 let out = compile_and_emit_with(
9948 "SELECT Person { n := std::array_agg(std::array_unpack(.perms)) }",
9949 &schema_with_array_property(),
9950 );
9951 assert!(!out.sql.contains("array_agg(unnest("), "got:\n{}", out.sql);
9952 assert!(
9953 out.sql.contains(r#"FROM unnest("t0"."perms") AS "_s"("v")"#),
9954 "got:\n{}",
9955 out.sql
9956 );
9957 }
9958
9959 #[test]
9962 fn test_array_agg_over_nothing_is_an_empty_array_not_null() {
9963 for query in [
9964 "SELECT std::array_agg(Person.name)",
9965 "SELECT std::array_agg((SELECT Person).name)",
9966 ] {
9967 let out = compile_and_emit(query);
9968 assert!(
9969 out.sql.contains("coalesce(") && out.sql.contains("'{}'"),
9970 "{query} got:\n{}",
9971 out.sql
9972 );
9973 }
9974 }
9975
9976 #[test]
9980 fn test_an_array_agg_binding_stays_an_array_through_its_coalesce() {
9981 let out = compile_and_emit(
9982 "WITH ids := std::array_agg(Person.id) SELECT Person { name } FILTER std::contains(ids, .id)",
9983 );
9984 assert!(
9985 out.sql.contains("@> ARRAY["),
9986 "expected the array overload, got:\n{}",
9987 out.sql
9988 );
9989 assert!(
9990 !out.sql.contains("strpos("),
9991 "the string overload must not win, got:\n{}",
9992 out.sql
9993 );
9994 }
9995
9996 #[test]
9999 fn test_aggregate_over_a_plain_walk_keeps_its_row_source() {
10000 let out = compile_and_emit("SELECT std::array_agg(Person.name)");
10001 assert!(
10002 out.sql.contains(r#"array_agg("t0"."name")"#) && out.sql.contains(r#"FROM "public"."Person""#),
10003 "got:\n{}",
10004 out.sql
10005 );
10006 }
10007
10008 #[test]
10011 fn test_the_cte_order_check_catches_a_forward_reference() {
10012 let bad = "WITH \"b\" AS (\n SELECT * FROM \"a\"\n),\n\"a\" AS (\n SELECT 1\n)\nSELECT 1";
10013 assert!(
10014 super::forward_cte_reference(bad).is_some(),
10015 "expected a forward reference"
10016 );
10017 let good = "WITH \"a\" AS (\n SELECT 1\n),\n\"b\" AS (\n SELECT * FROM \"a\"\n)\nSELECT 1";
10018 assert_eq!(super::forward_cte_reference(good), None, "ordered CTEs must pass");
10019 }
10020
10021 #[test]
10025 fn test_a_cast_unpacked_array_still_folds_into_any() {
10026 let out = compile_and_emit("SELECT Person { name } FILTER .name IN <str>std::array_unpack(<array<str>>$names)");
10027 assert!(
10028 !out.sql.contains("unnest("),
10029 "the unpack must fold into ANY, got:\n{}",
10030 out.sql
10031 );
10032 assert!(out.sql.contains("= ANY("), "got:\n{}", out.sql);
10033 }
10034
10035 #[test]
10038 fn test_array_unpack_outside_in_still_unnests() {
10039 let out = compile_and_emit("SELECT std::array_unpack(<array<str>>$names)");
10040 assert!(out.sql.contains("unnest("), "got:\n{}", out.sql);
10041 }
10042
10043 #[test]
10044 fn test_filter_not_in_set_literal_compiles_to_all_array() {
10045 let out = compile_and_emit("SELECT Person { name } FILTER .name NOT IN {'Carol'}");
10046 assert!(out.sql.contains("<> ALL(ARRAY['Carol'])"), "got:\n{}", out.sql);
10047 }
10048
10049 #[test]
10050 fn test_deep_splat_fetches_single_link_target_properties_not_just_id() {
10051 let out = compile_and_emit("SELECT Person { ** }");
10056 assert!(
10057 out.sql.contains("\"name\""),
10058 "expected Company.name pulled in via .company's ** expansion, got:\n{}",
10059 out.sql
10060 );
10061 }
10062
10063 #[test]
10064 fn test_deep_splat_fetches_multilink_target_properties_not_just_id() {
10065 let out = compile_and_emit("SELECT Person { ** }");
10066 assert!(
10067 out.sql.contains("\"title\""),
10068 "expected Post.title pulled in via .posts' ** expansion, got:\n{}",
10069 out.sql
10070 );
10071 }
10072
10073 #[test]
10074 fn test_schema_type_cast_select() {
10075 let out = compile_and_emit("SELECT <default::Person><uuid>'019ef1bb-0d42-7a9f-8f6b-b38d028a49ba'");
10076 assert!(out.sql.contains("FROM \"public\".\"Person\""));
10077 assert!(out.sql.contains("WHERE"));
10078 assert!(out.sql.contains("'019ef1bb-0d42-7a9f-8f6b-b38d028a49ba'"));
10079 }
10080
10081 #[test]
10082 fn test_select_single_link() {
10083 let out = compile_and_emit("SELECT Person { name, company { name } }");
10084 assert!(out.sql.contains("'default::Company'::text"));
10085 assert!(out.sql.contains("FROM \"public\".\"Company\""));
10086 assert!(out.sql.contains("\"company_id\" = "));
10088 }
10089
10090 #[test]
10091 fn test_select_multi_link() {
10092 let out = compile_and_emit("SELECT Person { name, posts { title } }");
10093 assert!(out.sql.contains("array_agg(ROW("));
10094 assert!(out.sql.contains("ARRAY[]::record[]"));
10095 assert!(out.sql.contains("'default::Post'::text"));
10096 assert!(out.sql.contains("\"Person.posts\""));
10097 }
10098
10099 fn compile_err(query: &str) -> String {
10100 let schema = make_schema();
10101 let ast = parse::parse(query).expect("parse failed");
10102 match ir::compile(&ast, &schema) {
10103 Ok(_) => panic!("expected a compile error"),
10104 Err(e) => format!("{e}"),
10105 }
10106 }
10107
10108 #[test]
10109 fn test_multi_link_limit_cuts_rows_before_aggregating() {
10110 let out = compile_and_emit("SELECT Person { posts: { title } filter .title = 'x' limit 1 }");
10111 assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
10114 assert!(out.sql.contains("AS \"__lim\""), "{}", out.sql);
10115 let agg = out.sql.find("array_agg").unwrap();
10116 let limit = out.sql.find("LIMIT 1").unwrap();
10117 assert!(agg < limit, "{}", out.sql);
10118 assert!(out.sql.contains("\"t1\".\"title\"::text AS \"c1\""), "{}", out.sql);
10119 }
10120
10121 #[test]
10122 fn test_multi_link_order_by_with_limit_orders_the_aggregate_too() {
10123 let out = compile_and_emit("SELECT Person { posts: { title } order by .title desc offset 1 limit 2 }");
10124 assert!(out.sql.contains("\"t1\".\"title\" AS \"s0\""), "{}", out.sql);
10127 assert!(
10128 out.sql.contains("ORDER BY \"__lim\".\"s0\" DESC NULLS LAST"),
10129 "{}",
10130 out.sql
10131 );
10132 assert!(out.sql.contains("OFFSET 1"), "{}", out.sql);
10133 assert!(out.sql.contains("LIMIT 2"), "{}", out.sql);
10134 }
10135
10136 #[test]
10137 fn test_multi_link_without_limit_keeps_the_flat_aggregate() {
10138 let out = compile_and_emit("SELECT Person { posts: { title } filter .title = 'x' }");
10139 assert!(!out.sql.contains("__lim"), "{}", out.sql);
10140 assert!(out.sql.contains("array_agg(ROW("), "{}", out.sql);
10141 }
10142
10143 #[test]
10144 fn test_sub_select_as_computed_pointer_is_a_link_pointer() {
10145 let out = compile_and_emit("SELECT Person { recent := (select .posts filter .title = 'x' limit 1) { title } }");
10146 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
10147 panic!()
10148 };
10149 let ShapeNode::Object {
10152 name,
10153 pointers: elem_pointers,
10154 ..
10155 } = &pointers[2]
10156 else {
10157 panic!("{:?}", pointers[2])
10158 };
10159 assert_eq!(name, "recent");
10160 assert!(matches!(&elem_pointers[1], ShapeNode::Scalar { name, .. } if name == "title"));
10161 assert!(out.sql.contains("\"jt\".source = \"t0\".id"), "{}", out.sql);
10162 assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
10163 }
10164
10165 #[test]
10166 fn test_sub_select_shape_inside_the_parens_is_equivalent() {
10167 let outer = compile_and_emit("SELECT Person { recent := (select .posts limit 1) { title } }");
10168 let inner = compile_and_emit("SELECT Person { recent := (select .posts { title } limit 1) }");
10169 assert_eq!(outer.sql, inner.sql);
10170 }
10171
10172 #[test]
10173 fn test_sub_select_field_access_is_a_correlated_scalar_subquery() {
10174 let out = compile_and_emit("SELECT Person { t := (select .posts filter .title = 'x' limit 1).title }");
10175 assert!(out.sql.contains("(\"t1\".\"id\" = \"t0\".\"id\")"), "{}", out.sql);
10178 assert!(out.sql.contains("\"t2\".\"title\""), "{}", out.sql);
10179 assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
10180 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
10181 panic!()
10182 };
10183 assert!(matches!(&pointers[2], ShapeNode::Scalar { name, .. } if name == "t"));
10184 }
10185
10186 #[test]
10187 fn test_sub_select_field_access_over_a_single_link() {
10188 let out = compile_and_emit("SELECT Person { c := (select .company).name }");
10189 assert!(out.sql.contains("\"t1\".\"company_id\" = \"t2\".\"id\""), "{}", out.sql);
10190 assert!(out.sql.contains("(\"t1\".\"id\" = \"t0\".\"id\")"), "{}", out.sql);
10191 }
10192
10193 #[test]
10194 fn test_sub_select_field_access_in_a_filter() {
10195 let out = compile_and_emit("SELECT Person { name } filter (select .posts limit 1).title = 'x'");
10196 assert!(out.sql.contains("WHERE ((SELECT \"t2\".\"title\""), "{}", out.sql);
10197 assert!(out.sql.contains("LIMIT 1) = 'x')"), "{}", out.sql);
10198 }
10199
10200 #[test]
10201 fn test_sub_select_over_an_absolute_path_is_not_correlated() {
10202 let out = compile_and_emit("SELECT Person { n := (select Company.name limit 1) }");
10203 assert!(out.sql.contains("FROM \"public\".\"Company\""), "{}", out.sql);
10204 assert!(!out.sql.contains("= \"t0\".\"id\""), "{}", out.sql);
10205 }
10206
10207 #[test]
10208 fn test_backlink_sub_select_carries_its_modifiers() {
10209 let out = compile_and_emit("SELECT Post { authors := (select .<posts[is Person] { name } limit 2) }");
10210 assert!(out.sql.contains("LIMIT 2"), "{}", out.sql);
10211 assert!(out.sql.contains("AS \"__lim\""), "{}", out.sql);
10212 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
10213 panic!()
10214 };
10215 assert!(matches!(&pointers[1], ShapeNode::Array { name, .. } if name == "authors"));
10216 }
10217
10218 #[test]
10219 fn test_with_bound_scalar_is_typed_for_overload_resolution() {
10220 let out = compile_and_emit("WITH xs := (select Person.name) SELECT contains(xs, 'a')");
10223 assert!(
10224 out.sql.contains("strpos((SELECT \"v\" FROM \"xs\"), 'a')"),
10225 "{}",
10226 out.sql
10227 );
10228 }
10229
10230 fn make_schema_with_computed_links() -> SchemaDescriptor {
10233 use crate::schema::ComputedDescriptor;
10234 let mut schema = make_schema();
10235 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
10236 person.computed = vec![
10237 ComputedDescriptor {
10238 name: "published".into(),
10239 expression: "(select .posts filter .title != '')".into(),
10240 return_type: None,
10241 link_target: None,
10242 link_multi: false,
10243 },
10244 ComputedDescriptor {
10245 name: "plain".into(),
10246 expression: ".posts".into(),
10247 return_type: None,
10248 link_target: None,
10249 link_multi: false,
10250 },
10251 ComputedDescriptor {
10252 name: "featured".into(),
10253 expression: "(select .published filter .title != 'draft')".into(),
10254 return_type: None,
10255 link_target: None,
10256 link_multi: false,
10257 },
10258 ComputedDescriptor {
10259 name: "looper".into(),
10260 expression: "(select .looper)".into(),
10261 return_type: None,
10262 link_target: None,
10263 link_multi: false,
10264 },
10265 ComputedDescriptor {
10266 name: "capped".into(),
10267 expression: "(select .posts limit 1)".into(),
10268 return_type: None,
10269 link_target: None,
10270 link_multi: false,
10271 },
10272 ];
10273 schema
10274 }
10275
10276 fn make_schema_with_object_fn_computed() -> SchemaDescriptor {
10279 use crate::schema::{ComputedDescriptor, FunctionDescriptor, FunctionParamDescriptor};
10280 let mut schema = make_schema();
10281 schema.functions = vec![FunctionDescriptor {
10282 name: "latest".into(),
10283 module: "default".into(),
10284 params: vec![FunctionParamDescriptor {
10285 name: "owner".into(),
10286 pg_type: "uuid".into(),
10287 }],
10288 return_pg_type: "default::Post".into(),
10289 return_is_object: true,
10290 return_is_set: true,
10291 return_is_polymorphic: false,
10292 volatility: "stable".into(),
10293 body: String::new(),
10294 }];
10295 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
10296 person.computed = vec![
10297 ComputedDescriptor {
10298 name: "recent".into(),
10299 expression: "latest(.id)".into(),
10300 return_type: None,
10301 link_target: None,
10302 link_multi: false,
10303 },
10304 ComputedDescriptor {
10305 name: "vetted".into(),
10306 expression: "(select latest(.id) filter .title != '')".into(),
10307 return_type: None,
10308 link_target: None,
10309 link_multi: false,
10310 },
10311 ];
10312 schema
10313 }
10314
10315 #[test]
10316 fn test_path_traverses_through_a_function_backed_computed() {
10317 let schema = make_schema_with_object_fn_computed();
10321 let out = compile_and_emit_with("SELECT Person { t := .recent.title }", &schema);
10322 assert!(
10323 out.sql
10324 .contains("CROSS JOIN LATERAL \"public\".\"latest\"(\"t1\".\"id\") AS \"t2\""),
10325 "{}",
10326 out.sql
10327 );
10328 assert!(out.sql.contains("ARRAY(SELECT \"t2\".\"title\""), "{}", out.sql);
10330 }
10331
10332 #[test]
10333 fn test_sub_select_over_a_function_backed_computed() {
10334 let schema = make_schema_with_object_fn_computed();
10337 let out = compile_and_emit_with(
10338 "SELECT Person { t := (select .recent filter .title = 'x' limit 1).title }",
10339 &schema,
10340 );
10341 assert!(
10342 out.sql.contains("CROSS JOIN LATERAL \"public\".\"latest\""),
10343 "{}",
10344 out.sql
10345 );
10346 assert!(out.sql.contains("\"t2\".\"title\" = 'x'"), "{}", out.sql);
10347 assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
10348 assert!(!out.sql.contains("ARRAY("), "{}", out.sql);
10349 }
10350
10351 #[test]
10352 fn test_function_backed_computed_carries_its_own_filter() {
10353 let schema = make_schema_with_object_fn_computed();
10354 let out = compile_and_emit_with(
10355 "SELECT Person { t := (select .vetted filter .title = 'x' limit 1).title }",
10356 &schema,
10357 );
10358 assert!(
10359 out.sql
10360 .contains("(\"t2\".\"title\" = 'x') AND (\"t2\".\"title\" <> '')"),
10361 "{}",
10362 out.sql
10363 );
10364 }
10365
10366 #[test]
10367 fn test_function_backed_computed_checks_its_argument_count() {
10368 use crate::schema::ComputedDescriptor;
10369 let mut schema = make_schema_with_object_fn_computed();
10370 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
10371 person.computed = vec![ComputedDescriptor {
10372 name: "recent".into(),
10373 expression: "latest()".into(),
10374 return_type: None,
10375 link_target: None,
10376 link_multi: false,
10377 }];
10378 let ast = parse::parse("SELECT Person { t := .recent.title }").unwrap();
10379 let err = match ir::compile(&ast, &schema) {
10380 Ok(_) => panic!("expected a compile error"),
10381 Err(e) => format!("{e}"),
10382 };
10383 assert!(err.contains("expects 1 argument(s), got 0"), "{err}");
10384 }
10385
10386 #[test]
10387 fn test_path_traverses_through_a_computed_pointer() {
10388 let schema = make_schema_with_computed_links();
10391 let out = compile_and_emit_with("SELECT Person { t := .published.title }", &schema);
10392 assert!(out.sql.contains("\"public\".\"Person.posts\""), "{}", out.sql);
10393 assert!(
10394 out.sql
10395 .contains("(\"t1\".\"id\" = \"t0\".\"id\") AND (\"t2\".\"title\" <> '')"),
10396 "{}",
10397 out.sql
10398 );
10399 }
10400
10401 #[test]
10402 fn test_sub_select_over_a_computed_pointer_projects_a_property() {
10403 let schema = make_schema_with_computed_links();
10406 let out = compile_and_emit_with(
10407 "SELECT Person { t := (select .published filter .title = 'a' limit 1).title }",
10408 &schema,
10409 );
10410 assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
10411 assert!(out.sql.contains("\"t2\".\"title\" = 'a'"), "{}", out.sql);
10413 assert!(out.sql.contains("\"t2\".\"title\" <> ''"), "{}", out.sql);
10414 }
10415
10416 #[test]
10417 fn test_chained_computed_pointers_each_contribute_their_filter() {
10418 let schema = make_schema_with_computed_links();
10419 let out = compile_and_emit_with("SELECT Person { t := .featured.title }", &schema);
10420 assert_eq!(out.sql.matches("\"public\".\"Person.posts\"").count(), 1, "{}", out.sql);
10421 assert!(out.sql.contains("\"t2\".\"title\" <> 'draft'"), "{}", out.sql);
10422 assert!(out.sql.contains("\"t2\".\"title\" <> ''"), "{}", out.sql);
10423 }
10424
10425 #[test]
10426 fn test_computed_pointer_naming_a_bare_link_is_traversable() {
10427 let schema = make_schema_with_computed_links();
10428 let out = compile_and_emit_with("SELECT Person { t := .plain.title }", &schema);
10429 assert!(out.sql.contains("ARRAY(SELECT \"t2\".\"title\""), "{}", out.sql);
10430 }
10431
10432 #[test]
10433 fn test_a_cycle_of_computed_pointers_is_reported_not_hung_on() {
10434 let schema = make_schema_with_computed_links();
10435 let ast = parse::parse("SELECT Person { t := .looper.title }").unwrap();
10436 let err = match ir::compile(&ast, &schema) {
10437 Ok(_) => panic!("expected a compile error"),
10438 Err(e) => format!("{e}"),
10439 };
10440 assert!(err.contains("expands into itself"), "{err}");
10441 }
10442
10443 #[test]
10444 fn test_a_cycle_of_computed_pointers_read_as_a_value_is_reported_not_hung_on() {
10445 let schema = make_schema_with_computed_links();
10448 for query in ["SELECT Person { looper }", "SELECT Person { * }"] {
10449 let ast = parse::parse(query).unwrap();
10450 let err = match ir::compile(&ast, &schema) {
10451 Ok(_) => panic!("expected a compile error for {query}"),
10452 Err(e) => format!("{e}"),
10453 };
10454 assert!(err.contains("expands into itself"), "{query}: {err}");
10455 }
10456 }
10457
10458 #[test]
10459 fn test_traversing_through_a_limited_computed_uses_a_lateral() {
10460 let schema = make_schema_with_computed_links();
10464 let out = compile_and_emit_with("SELECT Person { t := .capped.title }", &schema);
10465 assert!(out.sql.contains("JOIN LATERAL ("), "{}", out.sql);
10466 assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
10467 assert!(!out.sql.contains("ARRAY(SELECT"), "{}", out.sql);
10468 }
10469
10470 #[test]
10471 fn test_backlink_from_a_type_whose_interface_the_link_targets() {
10472 use crate::schema::{LinkDescriptor, TypeDescriptor};
10473 let mut schema = make_interface_schema();
10474 let individual = schema.types.iter().find(|t| t.name == "Individual").unwrap().clone();
10475 schema.types.push(TypeDescriptor {
10476 name: "Note".into(),
10477 module: "default".into(),
10478 table: "Note".into(),
10479 abstract_: false,
10480 materialized: true,
10481 description: None,
10482 parents: vec![],
10483 interfaces: vec![],
10484 bases: vec![],
10485 properties: individual.properties[..1].to_vec(),
10486 links: vec![LinkDescriptor {
10488 name: "owner".into(),
10489 target: "default::Account".into(),
10490 nullable: true,
10491 through: None,
10492 description: None,
10493 default_pyql: None,
10494 is_exclusive: false,
10495 is_readonly: false,
10496 rewrites: vec![],
10497 on_delete: vec![],
10498 }],
10499 multilinks: vec![],
10500 computed: vec![],
10501 constraints: vec![],
10502 indexes: vec![],
10503 partition: None,
10504 vector_indexes: vec![],
10505 search_indexes: vec![],
10506 triggers: vec![],
10507 junction: false,
10508 signals: vec![],
10509 });
10510 let out = compile_and_emit_with("SELECT Individual { notes := .<owner[is Note] }", &schema);
10513 assert!(out.sql.contains("\"public\".\"Note\""), "{}", out.sql);
10514 assert!(out.sql.contains("\"owner_id\" = \"t0\".id"), "{}", out.sql);
10515 }
10516
10517 #[test]
10518 fn test_with_block_in_expression_position_hoists_its_bindings() {
10519 let out = compile_and_emit("SELECT Person { t := (with c := (select Company.name limit 1) select c) }");
10522 assert!(out.sql.starts_with("WITH\n\"c\" AS ("), "{}", out.sql);
10523 assert!(out.sql.contains("(SELECT \"v\" FROM \"c\")"), "{}", out.sql);
10524 }
10525
10526 #[test]
10527 fn test_hoisted_binding_is_usable_in_the_expression_around_it() {
10528 let out =
10529 compile_and_emit("SELECT Person { t := (with c := (select Company.name limit 1) select .name ++ c) }");
10530 assert!(
10531 out.sql.contains("(\"t0\".\"name\" || (SELECT \"v\" FROM \"c\"))"),
10532 "{}",
10533 out.sql
10534 );
10535 }
10536
10537 #[test]
10538 fn test_hoisted_binding_joins_the_statements_own_with_clause() {
10539 let out = compile_and_emit(
10540 "WITH top := (select Person.name limit 1) \
10541 SELECT Person { t := (with c := (select Company.name limit 1) select c) }",
10542 );
10543 let top = out.sql.find("\"top\" AS (").expect("top-level binding");
10544 let inner = out.sql.find("\"c\" AS (").expect("hoisted binding");
10545 assert!(top < inner, "the statement's own bindings come first:\n{}", out.sql);
10546 }
10547
10548 #[test]
10549 fn test_a_binding_named_after_a_keyword_is_usable() {
10550 let out = compile_and_emit("WITH order := (select Person.name) SELECT Person { name } FILTER .name = order");
10555 assert!(out.sql.contains("(SELECT \"v\" FROM \"order\")"), "{}", out.sql);
10556 }
10557
10558 #[test]
10559 fn test_computed_declared_on_an_interface_is_visible_from_an_implementor() {
10560 use crate::schema::ComputedDescriptor;
10561 let mut schema = make_interface_schema();
10562 let account = schema.types.iter_mut().find(|t| t.name == "Account").unwrap();
10563 account.computed = vec![ComputedDescriptor {
10564 name: "tier".into(),
10565 expression: "'gold'".into(),
10566 return_type: None,
10567 link_target: None,
10568 link_multi: false,
10569 }];
10570 let individual = schema.types.iter_mut().find(|t| t.name == "Individual").unwrap();
10571 individual.computed = vec![];
10572 for q in ["SELECT Individual { tier }", "SELECT Individual { t := .tier }"] {
10575 let out = compile_and_emit_with(q, &schema);
10576 assert!(out.sql.contains("'gold'"), "{q}: {}", out.sql);
10577 }
10578 }
10579
10580 #[test]
10581 fn test_computed_may_carry_trailing_modifiers_without_a_select() {
10582 use crate::schema::ComputedDescriptor;
10583 let mut schema = make_schema();
10584 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
10585 person.computed = vec![ComputedDescriptor {
10586 name: "recent".into(),
10587 expression: ".posts order by .title desc limit 5".into(),
10588 return_type: None,
10589 link_target: None,
10590 link_multi: false,
10591 }];
10592 let out = compile_and_emit_with("SELECT Person { recent { title } }", &schema);
10595 assert!(out.sql.contains("LIMIT 5"), "{}", out.sql);
10596 assert!(out.sql.contains("ORDER BY \"t1\".\"title\" DESC"), "{}", out.sql);
10597 }
10598
10599 #[test]
10600 fn test_computed_may_lead_with_a_bare_select() {
10601 use crate::schema::ComputedDescriptor;
10602 let mut schema = make_schema();
10603 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
10604 person.computed = vec![ComputedDescriptor {
10605 name: "recent".into(),
10606 expression: "select .posts order by .title desc limit 5".into(),
10607 return_type: None,
10608 link_target: None,
10609 link_multi: false,
10610 }];
10611 let out = compile_and_emit_with("SELECT Person { recent { title } }", &schema);
10612 assert!(out.sql.contains("LIMIT 5"), "{}", out.sql);
10613 }
10614
10615 #[test]
10616 fn test_sub_select_with_a_shape_may_project_a_property_off_it() {
10617 let out = compile_and_emit("SELECT Person { t := (select .posts { title } limit 1).title }");
10618 assert!(out.sql.contains("\"t2\".\"title\""), "{}", out.sql);
10619 assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
10620 }
10621
10622 #[test]
10623 fn test_sub_statement_error_names_what_it_got() {
10624 let err = compile_err("SELECT Person { t := (insert Company { name := 'a' }) }");
10625 assert!(err.contains("an insert cannot stand in for a value"), "{err}");
10626 }
10627
10628 #[test]
10629 fn test_with_bound_scalar_path_select_emits_a_real_select() {
10630 let out = compile_and_emit("WITH xs := (select Person.name) SELECT Person { name }");
10633 assert!(
10634 out.sql
10635 .contains("SELECT ROW(v) AS result, v FROM (SELECT \"t0\".\"name\" AS v"),
10636 "{}",
10637 out.sql
10638 );
10639 }
10640
10641 #[test]
10642 fn test_with_bound_scalar_path_select_is_typed_as_its_result() {
10643 let out = compile_and_emit("WITH xs := (select Person.name) SELECT Person { name } FILTER .name IN xs");
10646 assert!(out.sql.contains("ANY((SELECT \"v\" FROM \"xs\"))"), "{}", out.sql);
10647 }
10648
10649 #[test]
10650 fn test_distinct_survives_a_with_binding() {
10651 let out = compile_and_emit("WITH xs := (select distinct Person.name) SELECT Person { name }");
10652 assert!(out.sql.contains("SELECT DISTINCT \"t0\".\"name\" AS v"), "{}", out.sql);
10653 }
10654
10655 #[test]
10656 fn test_for_over_a_derived_set_iterates_every_row() {
10657 let out = compile_and_emit("FOR x IN (select Person.name) UNION (SELECT Person { name } FILTER .name = x)");
10660 assert!(!out.sql.contains("VALUES"), "{}", out.sql);
10661 assert!(out.sql.contains(") AS \"_for_x\""), "{}", out.sql);
10662 assert!(out.sql.contains("CROSS JOIN LATERAL"), "{}", out.sql);
10663 assert!(out.sql.contains("\"t1\".\"name\" = \"_for_x\".\"v\""), "{}", out.sql);
10664 }
10665
10666 #[test]
10667 fn test_for_insert_over_a_derived_set_uses_it_as_the_cte() {
10668 let out = compile_and_emit("FOR x IN (select Person.name) UNION (INSERT Company { name := x })");
10669 assert!(out.sql.starts_with("WITH \"_for_x\" AS ("), "{}", out.sql);
10670 assert!(
10671 out.sql.contains("SELECT \"_for_x\".\"v\" FROM \"_for_x\""),
10672 "{}",
10673 out.sql
10674 );
10675 }
10676
10677 #[test]
10678 fn test_for_over_objects_binds_their_id() {
10679 let out = compile_and_emit("FOR p IN (select Person) UNION (INSERT Company { name := <str>p })");
10680 assert!(out.sql.contains("SELECT \"id\" AS v FROM ("), "{}", out.sql);
10681 }
10682
10683 #[test]
10684 fn test_for_over_a_non_select_statement_is_rejected() {
10685 let ast = parse::parse("FOR x IN (INSERT Company { name := 'a' }) UNION (SELECT Person { name })").unwrap();
10686 let schema = make_schema();
10687 let err = match ir::compile(&ast, &schema) {
10688 Ok(_) => panic!("expected a compile error"),
10689 Err(e) => format!("{e}"),
10690 };
10691 assert!(err.contains("only a select can be iterated"), "{err}");
10692 }
10693
10694 #[test]
10695 fn test_multi_link_path_outside_a_comparison_is_an_array() {
10696 let out = compile_and_emit("SELECT Person { t := .posts.title }");
10699 assert!(out.sql.contains("ARRAY(SELECT \"t2\".\"title\""), "{}", out.sql);
10700 assert!(out.sql.contains("(\"t1\".\"id\" = \"t0\".\"id\")"), "{}", out.sql);
10701 }
10702
10703 #[test]
10704 fn test_multi_link_path_inside_a_comparison_is_still_exists() {
10705 let out = compile_and_emit("SELECT Person { name } filter .posts.title = 'x'");
10706 assert!(out.sql.contains("WHERE EXISTS("), "{}", out.sql);
10707 assert!(!out.sql.contains("ARRAY("), "{}", out.sql);
10708 }
10709
10710 #[test]
10711 fn test_path_traversal_deeper_than_two_steps() {
10712 let schema = make_schema_with_through();
10713 let out = compile_and_emit_with("SELECT Person { t := .friends.friends.name }", &schema);
10714 assert_eq!(out.sql.matches("\"public\".\"PersonFriend\"").count(), 2, "{}", out.sql);
10716 assert!(out.sql.contains("ARRAY(SELECT \"t4\".\"name\""), "{}", out.sql);
10717 }
10718
10719 #[test]
10720 fn test_coalesce_over_backlinked_values_is_a_set_choice() {
10721 let out = compile_and_emit("SELECT Post ORDER BY max(.<posts[is Person].age ?? .<posts[is Person].age)");
10724 assert!(!out.sql.contains("EXISTS"), "{}", out.sql);
10725 assert!(!out.sql.contains("COALESCE("), "{}", out.sql);
10726 assert!(out.sql.contains("CASE WHEN (cardinality("), "{}", out.sql);
10727 assert!(
10728 out.sql
10729 .contains("(SELECT coalesce(max(\"_s\".\"v\"), NULL) FROM unnest("),
10730 "{}",
10731 out.sql
10732 );
10733 }
10734
10735 #[test]
10736 fn test_coalesce_over_multilink_values_is_a_set_choice() {
10737 let out = compile_and_emit("SELECT Person { t := .posts.title ?? .posts.title }");
10738 assert!(!out.sql.contains("EXISTS"), "{}", out.sql);
10739 assert!(out.sql.contains("CASE WHEN (cardinality("), "{}", out.sql);
10740 }
10741
10742 #[test]
10743 fn test_coalesce_of_a_set_with_a_single_value_falls_back_to_it() {
10744 let out = compile_and_emit("SELECT Post { a := .<posts[is Person].age ?? 0 }");
10745 assert!(out.sql.contains("ELSE array_remove(ARRAY[0], NULL) END"), "{}", out.sql);
10746 }
10747
10748 #[test]
10749 fn test_traversal_after_a_backlink() {
10750 let out = compile_and_emit("SELECT Post { t := .<posts[is Person].name }");
10751 assert!(out.sql.contains("ARRAY(SELECT \"t2\".\"name\""), "{}", out.sql);
10752 assert!(out.sql.contains("\"t3\".\"target\" = \"t1\".\"id\""), "{}", out.sql);
10753 }
10754
10755 #[test]
10756 fn test_computed_pointer_on_a_linked_type_is_reachable() {
10757 use crate::schema::ComputedDescriptor;
10758 let mut schema = make_schema();
10759 let company = schema.types.iter_mut().find(|t| t.name == "Company").unwrap();
10760 company.computed = vec![ComputedDescriptor {
10761 name: "shout".into(),
10762 expression: ".name ++ '!'".into(),
10763 return_type: Some("text".into()),
10764 link_target: None,
10765 link_multi: false,
10766 }];
10767 let out = compile_and_emit_with("SELECT Person { t := .company.shout }", &schema);
10771 assert!(out.sql.contains("(\"t2\".\"name\" || '!')"), "{}", out.sql);
10772 }
10773
10774 #[test]
10775 fn test_computed_pointer_behind_a_type_intersection() {
10776 let schema = make_interface_schema();
10777 let out = compile_and_emit_with("SELECT Account { n := [is Individual].full_name }", &schema);
10778 assert!(out.sql.contains("upper(\"t1\".\"first_name\")"), "{}", out.sql);
10779 assert!(out.sql.contains("FROM \"public\".\"Individual\""), "{}", out.sql);
10780 assert!(out.sql.contains("(\"t1\".\"id\" = \"t0\".\"id\")"), "{}", out.sql);
10781 }
10782
10783 #[test]
10784 fn test_computed_pointer_cannot_be_traversed_through() {
10785 use crate::schema::ComputedDescriptor;
10786 let mut schema = make_schema();
10787 let company = schema.types.iter_mut().find(|t| t.name == "Company").unwrap();
10788 company.computed = vec![ComputedDescriptor {
10789 name: "shout".into(),
10790 expression: ".name ++ '!'".into(),
10791 return_type: Some("text".into()),
10792 link_target: None,
10793 link_multi: false,
10794 }];
10795 let ast = parse::parse("SELECT Person { t := .company.shout.nope }").unwrap();
10796 let err = match ir::compile(&ast, &schema) {
10797 Ok(_) => panic!("expected a compile error"),
10798 Err(e) => format!("{e}"),
10799 };
10800 assert!(err.contains("is a computed pointer"), "{err}");
10801 }
10802
10803 #[test]
10804 fn test_sub_select_over_a_multi_link_path_keeps_its_modifiers() {
10805 let out = compile_and_emit("SELECT Person { t := (select .posts.title order by .title desc limit 2) }");
10806 assert!(out.sql.contains("ARRAY(SELECT"), "{}", out.sql);
10807 assert!(out.sql.contains("ORDER BY \"t2\".\"title\" DESC"), "{}", out.sql);
10808 assert!(out.sql.contains("LIMIT 2"), "{}", out.sql);
10809 }
10810
10811 #[test]
10812 fn test_limit_one_over_a_multi_link_path_stays_a_scalar() {
10813 let out = compile_and_emit("SELECT Person { t := (select .posts.title limit 1) }");
10816 assert!(!out.sql.contains("ARRAY("), "{}", out.sql);
10817 assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
10818 }
10819
10820 #[test]
10821 fn test_computed_multilink_can_carry_a_nested_shape() {
10822 let out = compile_and_emit("SELECT Person { p := .posts { title } }");
10823 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
10824 panic!()
10825 };
10826 let ShapeNode::Array { name, element, .. } = &pointers[2] else {
10827 panic!("{:?}", pointers[2])
10828 };
10829 assert_eq!(name, "p");
10830 let ShapeNode::Object {
10831 pointers: elem_pointers,
10832 ..
10833 } = element.as_ref()
10834 else {
10835 panic!()
10836 };
10837 assert!(matches!(&elem_pointers[1], ShapeNode::Scalar { name, .. } if name == "title"));
10838 }
10839
10840 #[test]
10841 fn test_schema_declared_backlink_computed_is_an_object_pointer() {
10842 use crate::schema::ComputedDescriptor;
10843 let mut schema = make_schema();
10844 let post = schema.types.iter_mut().find(|t| t.name == "Post").unwrap();
10845 post.computed = vec![ComputedDescriptor {
10846 name: "authors".into(),
10847 expression: ".<posts[is Person]".into(),
10848 return_type: None,
10849 link_target: None,
10850 link_multi: false,
10851 }];
10852 let out = compile_and_emit_with("SELECT Post { authors { name } }", &schema);
10855 assert!(out.sql.contains("array_agg(ROW("), "{}", out.sql);
10856 assert!(out.sql.contains("\"t1\".\"name\"::text"), "{}", out.sql);
10857 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
10858 panic!()
10859 };
10860 assert!(matches!(&pointers[1], ShapeNode::Array { name, .. } if name == "authors"));
10861 }
10862
10863 #[test]
10864 fn test_schema_declared_multilink_computed_defaults_to_ids_without_a_shape() {
10865 use crate::schema::ComputedDescriptor;
10866 let mut schema = make_schema();
10867 schema.types[0].computed = vec![ComputedDescriptor {
10868 name: "everything".into(),
10869 expression: ".posts".into(),
10870 return_type: None,
10871 link_target: None,
10872 link_multi: false,
10873 }];
10874 let out = compile_and_emit_with("SELECT Person { everything }", &schema);
10875 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
10876 panic!()
10877 };
10878 assert!(matches!(&pointers[2], ShapeNode::Array { name, .. } if name == "everything"));
10879 }
10880
10881 #[test]
10882 fn test_schema_declared_computed_sub_select_is_a_link_pointer() {
10883 use crate::schema::ComputedDescriptor;
10884 let mut schema = make_schema();
10885 schema.types[0].computed = vec![
10886 ComputedDescriptor {
10887 name: "recent".into(),
10888 expression: "(select .posts order by .title desc limit 1)".into(),
10889 return_type: None,
10890 link_target: None,
10891 link_multi: false,
10892 },
10893 ComputedDescriptor {
10894 name: "recent_title".into(),
10895 expression: "(select .posts order by .title desc limit 1).title".into(),
10896 return_type: Some("text".into()),
10897 link_target: None,
10898 link_multi: false,
10899 },
10900 ];
10901 let out = compile_and_emit_with("SELECT Person { recent { title }, recent_title }", &schema);
10902 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
10903 panic!()
10904 };
10905 assert!(matches!(&pointers[2], ShapeNode::Object { name, .. } if name == "recent"));
10908 assert!(matches!(&pointers[3], ShapeNode::Scalar { name, .. } if name == "recent_title"));
10909 assert!(out.sql.contains("\"jt\".source = \"t0\".id"), "{}", out.sql);
10910 assert_eq!(out.sql.matches("LIMIT 1").count(), 2, "{}", out.sql);
10911 crate::validate::validate_schema_types(&schema).expect("schema should validate");
10912 }
10913
10914 #[test]
10919 fn test_a_mutation_read_through_a_path_runs_as_a_hoisted_cte() {
10920 let out = compile_and_emit("SELECT (INSERT Person { name := 'x' }).name");
10921 assert!(
10922 out.sql.contains("\"_nested_dml_0\" AS (") && out.sql.contains("INSERT INTO"),
10923 "the mutation must run as a CTE:\n{}",
10924 out.sql
10925 );
10926 assert!(
10927 out.sql.contains("FROM \"_nested_dml_0\""),
10928 "the projection must read the mutation's own rows:\n{}",
10929 out.sql
10930 );
10931 }
10932
10933 #[test]
10934 fn test_dml_sub_statement_in_expression_position_still_rejected() {
10935 let err = compile_err("SELECT Person { x := (insert Company { name := 'c' }).name }");
10936 assert!(err.contains("an insert cannot stand in for a value"), "{err}");
10937 }
10938
10939 #[test]
10940 fn test_sub_select_with_a_shape_in_expression_position_is_rejected() {
10941 let err = compile_err("SELECT Person { name } filter (select .posts { title }) = 1");
10945 assert!(err.contains("sub-select with a shape"), "{err}");
10946 }
10947
10948 fn make_schema_with_through() -> SchemaDescriptor {
10949 let id_prop = || PropertyDescriptor {
10950 name: "id".into(),
10951 pg_type: "uuid".into(),
10952 nullable: false,
10953 default_sql: Some("gen_random_uuid()".into()),
10954 description: None,
10955 default_pyql: None,
10956 check_constraints: vec![],
10957 is_exclusive: true,
10958 is_pk: true,
10959 is_readonly: true,
10960 rewrites: vec![],
10961 tuple_members: None,
10962 column_type: None,
10963 };
10964 let name_prop = || PropertyDescriptor {
10965 name: "name".into(),
10966 pg_type: "text".into(),
10967 nullable: false,
10968 default_sql: None,
10969 description: None,
10970 check_constraints: vec![],
10971 default_pyql: None,
10972 is_exclusive: false,
10973 is_pk: false,
10974 is_readonly: false,
10975 rewrites: vec![],
10976 tuple_members: None,
10977 column_type: None,
10978 };
10979 SchemaDescriptor {
10980 types: vec![
10981 TypeDescriptor {
10982 name: "Person".into(),
10983 module: "default".into(),
10984 table: "Person".into(),
10985 abstract_: false,
10986 materialized: false,
10987 description: None,
10988 parents: vec![],
10989 interfaces: vec![],
10990 bases: vec![],
10991 properties: vec![id_prop(), name_prop()],
10992 links: vec![],
10993 multilinks: vec![MultiLinkDescriptor {
10994 name: "friends".into(),
10995 target: "default::Person".into(),
10996 through: Some("default::PersonFriend".into()),
10997 nullable: false,
10998 description: None,
10999 default_pyql: None,
11000 on_delete: vec![],
11001 is_exclusive: false,
11002 }],
11003 computed: vec![],
11004 constraints: vec![],
11005 indexes: vec![],
11006 partition: None,
11007 vector_indexes: vec![],
11008 search_indexes: vec![],
11009 triggers: vec![],
11010 junction: false,
11011 signals: vec![],
11012 },
11013 TypeDescriptor {
11014 name: "PersonFriend".into(),
11015 module: "default".into(),
11016 table: "PersonFriend".into(),
11017 abstract_: false,
11018 materialized: false,
11019 description: None,
11020 parents: vec![],
11021 interfaces: vec![],
11022 bases: vec![],
11023 properties: vec![id_prop()],
11024 links: vec![
11025 LinkDescriptor {
11026 name: "person".into(),
11027 target: "default::Person".into(),
11028 nullable: false,
11029 through: None,
11030 description: None,
11031 default_pyql: None,
11032 is_exclusive: false,
11033 is_readonly: false,
11034 rewrites: vec![],
11035 on_delete: vec![],
11036 },
11037 LinkDescriptor {
11038 name: "friend".into(),
11039 target: "default::Person".into(),
11040 nullable: false,
11041 through: None,
11042 description: None,
11043 default_pyql: None,
11044 is_exclusive: false,
11045 is_readonly: false,
11046 rewrites: vec![],
11047 on_delete: vec![],
11048 },
11049 ],
11050 multilinks: vec![],
11051 computed: vec![],
11052 constraints: vec![],
11053 indexes: vec![],
11054 partition: None,
11055 vector_indexes: vec![],
11056 search_indexes: vec![],
11057 triggers: vec![],
11058 junction: false,
11059 signals: vec![],
11060 },
11061 ],
11062 scalars: vec![],
11063 enums: vec![],
11064 named_tuples: vec![],
11065 globals: vec![],
11066 functions: vec![],
11067 aliases: vec![],
11068 channels: vec![],
11069 ..Default::default()
11070 }
11071 }
11072
11073 #[test]
11074 fn test_select_through_multi_link() {
11075 let schema = make_schema_with_through();
11076 let ast = crate::parse::parse("SELECT Person { name, friends { name } }").unwrap();
11077 let ir = crate::ir::compile(&ast, &schema).unwrap();
11078 let out = emit(&ir);
11079 assert!(out.sql.contains("\"public\".\"PersonFriend\""));
11081 assert!(out.sql.contains("\"friend\""));
11083 assert!(out.sql.contains("\"person\""));
11084 assert!(out.sql.contains("array_agg(ROW("));
11086 }
11087
11088 #[test]
11094 fn test_limited_multi_link_keeps_a_nested_multi_link_in_scope() {
11095 let schema = make_schema_with_through();
11098 let ast = crate::parse::parse("SELECT Person { friends: { name, friends { name } } limit 1 }").unwrap();
11099 let ir = crate::ir::compile(&ast, &schema).unwrap();
11100 let out = emit(&ir);
11101 assert_eq!(out.sql.matches("array_agg(ROW(").count(), 2, "{}", out.sql);
11102 let derived = out.sql.find("AS \"__lim\"").unwrap();
11103 let nested = out.sql.rfind("array_agg(ROW(").unwrap();
11104 assert!(
11105 nested < derived,
11106 "nested aggregate must sit inside the derived table:\n{}",
11107 out.sql
11108 );
11109 }
11110
11111 fn make_schema_with_junction_backed_link() -> SchemaDescriptor {
11112 let id_prop = || PropertyDescriptor {
11113 name: "id".into(),
11114 pg_type: "uuid".into(),
11115 nullable: false,
11116 default_sql: Some("gen_random_uuid()".into()),
11117 description: None,
11118 default_pyql: None,
11119 check_constraints: vec![],
11120 is_exclusive: true,
11121 is_pk: true,
11122 is_readonly: true,
11123 rewrites: vec![],
11124 tuple_members: None,
11125 column_type: None,
11126 };
11127 let name_prop = || PropertyDescriptor {
11128 name: "name".into(),
11129 pg_type: "text".into(),
11130 nullable: false,
11131 default_sql: None,
11132 description: None,
11133 check_constraints: vec![],
11134 default_pyql: None,
11135 is_exclusive: false,
11136 is_pk: false,
11137 is_readonly: false,
11138 rewrites: vec![],
11139 tuple_members: None,
11140 column_type: None,
11141 };
11142 SchemaDescriptor {
11143 types: vec![
11144 TypeDescriptor {
11145 name: "Person".into(),
11146 module: "default".into(),
11147 table: "Person".into(),
11148 abstract_: false,
11149 materialized: false,
11150 description: None,
11151 parents: vec![],
11152 interfaces: vec![],
11153 bases: vec![],
11154 properties: vec![id_prop(), name_prop()],
11155 links: vec![LinkDescriptor {
11156 name: "spouse".into(),
11157 target: "default::Org".into(),
11158 nullable: true,
11159 through: Some("default::Marriage".into()),
11160 description: None,
11161 default_pyql: None,
11162 is_exclusive: true,
11163 is_readonly: false,
11164 rewrites: vec![],
11165 on_delete: vec![],
11166 }],
11167 multilinks: vec![],
11168 computed: vec![],
11169 constraints: vec![],
11170 indexes: vec![],
11171 partition: None,
11172 vector_indexes: vec![],
11173 search_indexes: vec![],
11174 triggers: vec![],
11175 junction: false,
11176 signals: vec![],
11177 },
11178 TypeDescriptor {
11179 name: "Org".into(),
11180 module: "default".into(),
11181 table: "Org".into(),
11182 abstract_: false,
11183 materialized: false,
11184 description: None,
11185 parents: vec![],
11186 interfaces: vec![],
11187 bases: vec![],
11188 properties: vec![id_prop(), name_prop()],
11189 links: vec![],
11190 multilinks: vec![],
11191 computed: vec![],
11192 constraints: vec![],
11193 indexes: vec![],
11194 partition: None,
11195 vector_indexes: vec![],
11196 search_indexes: vec![],
11197 triggers: vec![],
11198 junction: false,
11199 signals: vec![],
11200 },
11201 TypeDescriptor {
11202 name: "Marriage".into(),
11203 module: "default".into(),
11204 table: "Person.spouse".into(),
11205 abstract_: false,
11206 materialized: false,
11207 description: None,
11208 parents: vec![],
11209 interfaces: vec![],
11210 bases: vec![],
11211 properties: vec![
11212 id_prop(),
11213 PropertyDescriptor {
11214 name: "since".into(),
11215 pg_type: "int8".into(),
11216 nullable: true,
11217 default_sql: None,
11218 description: None,
11219 check_constraints: vec![],
11220 default_pyql: None,
11221 is_exclusive: false,
11222 is_pk: false,
11223 is_readonly: false,
11224 rewrites: vec![],
11225 tuple_members: None,
11226 column_type: None,
11227 },
11228 ],
11229 links: vec![
11230 LinkDescriptor {
11231 name: "source".into(),
11232 target: "default::Person".into(),
11233 nullable: false,
11234 through: None,
11235 description: None,
11236 default_pyql: None,
11237 is_exclusive: false,
11238 is_readonly: false,
11239 rewrites: vec![],
11240 on_delete: vec![],
11241 },
11242 LinkDescriptor {
11243 name: "target".into(),
11244 target: "default::Org".into(),
11245 nullable: false,
11246 through: None,
11247 description: None,
11248 default_pyql: None,
11249 is_exclusive: false,
11250 is_readonly: false,
11251 rewrites: vec![],
11252 on_delete: vec![],
11253 },
11254 ],
11255 multilinks: vec![],
11256 computed: vec![],
11257 constraints: vec![],
11258 indexes: vec![],
11259 partition: None,
11260 vector_indexes: vec![],
11261 search_indexes: vec![],
11262 triggers: vec![],
11263 junction: true,
11264 signals: vec![],
11265 },
11266 ],
11267 scalars: vec![],
11268 enums: vec![],
11269 named_tuples: vec![],
11270 globals: vec![],
11271 functions: vec![],
11272 aliases: vec![],
11273 channels: vec![],
11274 ..Default::default()
11275 }
11276 }
11277
11278 #[test]
11279 fn test_select_shape_over_junction_backed_single_link() {
11280 let schema = make_schema_with_junction_backed_link();
11281 let ast = crate::parse::parse("SELECT Person { name, spouse { name } }").unwrap();
11282 let ir = crate::ir::compile(&ast, &schema).unwrap();
11283 let out = emit(&ir);
11284 assert!(out.sql.contains("\"public\".\"Person.spouse\""), "got:\n{}", out.sql);
11286 assert!(out.sql.contains("\"jt\".source"), "got:\n{}", out.sql);
11287 assert!(out.sql.contains("\"jt\".target"), "got:\n{}", out.sql);
11288 assert!(!out.sql.contains("array_agg"), "got:\n{}", out.sql);
11290 assert!(!out.sql.contains("spouse_id"), "got:\n{}", out.sql);
11291 }
11292
11293 #[test]
11294 fn test_select_shape_over_junction_backed_single_link_with_link_property() {
11295 let schema = make_schema_with_junction_backed_link();
11301 let ast = crate::parse::parse("SELECT Person { name, spouse { name, @since } }").unwrap();
11302 let ir = crate::ir::compile(&ast, &schema).unwrap();
11303 let out = emit(&ir);
11304 assert!(out.sql.contains("\"jt\".\"since\""), "got:\n{}", out.sql);
11305 assert!(!out.sql.contains("array_agg"), "got:\n{}", out.sql);
11306 }
11307
11308 #[test]
11309 fn test_select_path_over_junction_backed_single_link() {
11310 let schema = make_schema_with_junction_backed_link();
11311 let ast = crate::parse::parse("SELECT Person.spouse { name }").unwrap();
11312 let ir = crate::ir::compile(&ast, &schema).unwrap();
11313 let out = emit(&ir);
11314 assert!(out.sql.contains("\"public\".\"Person.spouse\""), "got:\n{}", out.sql);
11315 assert!(out.sql.contains("\"source\""), "got:\n{}", out.sql);
11316 assert!(out.sql.contains("\"target\""), "got:\n{}", out.sql);
11317 assert!(!out.sql.contains("spouse_id"), "got:\n{}", out.sql);
11318 }
11319
11320 #[test]
11321 fn test_insert_with_junction_backed_single_link() {
11322 let schema = make_schema_with_junction_backed_link();
11323 let out = compile_and_emit_with(
11324 "INSERT Person { name := $name, \
11325 spouse := (SELECT Org FILTER .id = $oid) { @since := <int64>$since } }",
11326 &schema,
11327 );
11328 assert!(out.sql.contains("\"public\".\"Person.spouse\""), "got:\n{}", out.sql);
11329 assert!(out.sql.contains("\"since\""), "got:\n{}", out.sql);
11330 assert!(!out.sql.contains("spouse_id"), "got:\n{}", out.sql);
11331 assert!(out.sql.contains("ON CONFLICT (\"source\") DO UPDATE SET \"target\" = EXCLUDED.\"target\", \"since\" = EXCLUDED.\"since\""), "got:\n{}", out.sql);
11338 }
11339
11340 #[test]
11341 fn test_update_replace_junction_backed_single_link() {
11342 let schema = make_schema_with_junction_backed_link();
11343 let out = compile_and_emit_with(
11344 "UPDATE Person FILTER .id = $id SET { \
11345 spouse := (SELECT Org FILTER .id = $oid) { @since := <int64>$since } }",
11346 &schema,
11347 );
11348 assert!(
11350 out.sql.contains("DELETE FROM \"public\".\"Person.spouse\""),
11351 "got:\n{}",
11352 out.sql
11353 );
11354 assert!(
11355 out.sql.contains("INSERT INTO \"public\".\"Person.spouse\""),
11356 "got:\n{}",
11357 out.sql
11358 );
11359 assert!(out.sql.contains("ON CONFLICT (\"source\") DO UPDATE SET \"target\" = EXCLUDED.\"target\", \"since\" = EXCLUDED.\"since\""), "got:\n{}", out.sql);
11360 assert!(out.sql.contains("NOT IN"), "got:\n{}", out.sql);
11365 }
11366
11367 #[test]
11372 fn test_unless_conflict_else_update_keeps_its_filter() {
11373 let out = compile_and_emit(
11374 "INSERT Person { name := 'a' } UNLESS CONFLICT ON .name \
11375 ELSE (UPDATE Person FILTER .age < 30 SET { name := 'b' })",
11376 );
11377 assert!(
11378 out.sql.contains("DO UPDATE SET") && out.sql.contains("WHERE"),
11379 "the ELSE UPDATE's filter must survive as a DO UPDATE predicate:\n{}",
11380 out.sql
11381 );
11382 assert!(
11383 out.sql.contains("\"Person\".\"age\""),
11384 "the predicate must read the existing row, not `excluded`:\n{}",
11385 out.sql
11386 );
11387 }
11388
11389 #[test]
11391 fn test_unless_conflict_else_update_without_a_filter_has_no_predicate() {
11392 let out = compile_and_emit(
11393 "INSERT Person { name := 'a' } UNLESS CONFLICT ON .name ELSE (UPDATE Person SET { name := 'b' })",
11394 );
11395 let after_set = out.sql.split("DO UPDATE SET").nth(1).unwrap_or_default();
11396 assert!(
11397 !after_set.contains("WHERE"),
11398 "got a predicate we never asked for:\n{}",
11399 out.sql
11400 );
11401 }
11402
11403 #[test]
11404 fn test_junction_backed_single_link_rejected_in_unless_conflict_else() {
11405 let schema = make_schema_with_junction_backed_link();
11412 let ast = parse::parse(
11413 "INSERT Person { name := $name } \
11414 UNLESS CONFLICT ON .name ELSE (UPDATE Person SET { \
11415 spouse := (SELECT Org FILTER .id = $oid) })",
11416 )
11417 .unwrap();
11418 assert!(ir::compile(&ast, &schema).is_err());
11419 }
11420
11421 #[test]
11422 fn test_update_clear_junction_backed_single_link() {
11423 let schema = make_schema_with_junction_backed_link();
11424 let out = compile_and_emit_with("UPDATE Person FILTER .id = $id SET { spouse := {} }", &schema);
11425 assert!(
11426 out.sql.contains("DELETE FROM \"public\".\"Person.spouse\""),
11427 "got:\n{}",
11428 out.sql
11429 );
11430 assert!(
11431 !out.sql.contains("INSERT INTO \"public\".\"Person.spouse\""),
11432 "clearing must not also insert:\n{}",
11433 out.sql
11434 );
11435 }
11436
11437 #[test]
11438 fn test_update_clear_junction_backed_single_link_with_cast_empty_set() {
11439 let schema = make_schema_with_junction_backed_link();
11447 let out = compile_and_emit_with("UPDATE Person FILTER .id = $id SET { spouse := <Org>{} }", &schema);
11448 assert!(
11449 out.sql.contains("DELETE FROM \"public\".\"Person.spouse\""),
11450 "got:\n{}",
11451 out.sql
11452 );
11453 assert!(
11454 !out.sql.contains("INSERT INTO \"public\".\"Person.spouse\""),
11455 "clearing must not also insert:\n{}",
11456 out.sql
11457 );
11458 }
11459
11460 fn make_schema_with_through_and_prop() -> SchemaDescriptor {
11468 let id_prop = || PropertyDescriptor {
11469 name: "id".into(),
11470 pg_type: "uuid".into(),
11471 nullable: false,
11472 default_sql: Some("gen_random_uuid()".into()),
11473 description: None,
11474 default_pyql: None,
11475 check_constraints: vec![],
11476 is_exclusive: true,
11477 is_pk: true,
11478 is_readonly: true,
11479 rewrites: vec![],
11480 tuple_members: None,
11481 column_type: None,
11482 };
11483 let name_prop = || PropertyDescriptor {
11484 name: "name".into(),
11485 pg_type: "text".into(),
11486 nullable: false,
11487 default_sql: None,
11488 description: None,
11489 check_constraints: vec![],
11490 default_pyql: None,
11491 is_exclusive: false,
11492 is_pk: false,
11493 is_readonly: false,
11494 rewrites: vec![],
11495 tuple_members: None,
11496 column_type: None,
11497 };
11498 SchemaDescriptor {
11499 types: vec![
11500 TypeDescriptor {
11501 name: "Product".into(),
11502 module: "default".into(),
11503 table: "Product".into(),
11504 abstract_: false,
11505 materialized: false,
11506 description: None,
11507 parents: vec![],
11508 interfaces: vec![],
11509 bases: vec![],
11510 properties: vec![id_prop(), name_prop()],
11511 links: vec![],
11512 multilinks: vec![MultiLinkDescriptor {
11513 name: "tags".into(),
11514 target: "default::Tag".into(),
11515 through: Some("default::ProductTag".into()),
11516 nullable: false,
11517 description: None,
11518 default_pyql: None,
11519 on_delete: vec![],
11520 is_exclusive: false,
11521 }],
11522 computed: vec![],
11523 constraints: vec![],
11524 indexes: vec![],
11525 partition: None,
11526 vector_indexes: vec![],
11527 search_indexes: vec![],
11528 triggers: vec![],
11529 junction: false,
11530 signals: vec![],
11531 },
11532 TypeDescriptor {
11533 name: "Tag".into(),
11534 module: "default".into(),
11535 table: "Tag".into(),
11536 abstract_: false,
11537 materialized: false,
11538 description: None,
11539 parents: vec![],
11540 interfaces: vec![],
11541 bases: vec![],
11542 properties: vec![id_prop(), name_prop()],
11543 links: vec![],
11544 multilinks: vec![],
11545 computed: vec![],
11546 constraints: vec![],
11547 indexes: vec![],
11548 partition: None,
11549 vector_indexes: vec![],
11550 search_indexes: vec![],
11551 triggers: vec![],
11552 junction: false,
11553 signals: vec![],
11554 },
11555 TypeDescriptor {
11556 name: "ProductTag".into(),
11557 module: "default".into(),
11558 table: "Product.tags".into(),
11559 abstract_: false,
11560 materialized: false,
11561 description: None,
11562 parents: vec![],
11563 interfaces: vec![],
11564 bases: vec![],
11565 properties: vec![
11566 id_prop(),
11567 PropertyDescriptor {
11568 name: "weight".into(),
11569 pg_type: "float8".into(),
11570 nullable: false,
11571 default_sql: None,
11572 default_pyql: None,
11573 description: None,
11574 check_constraints: vec![],
11575 is_exclusive: false,
11576 is_pk: false,
11577 is_readonly: false,
11578 rewrites: vec![],
11579 tuple_members: None,
11580 column_type: None,
11581 },
11582 ],
11583 links: vec![],
11587 multilinks: vec![],
11588 computed: vec![],
11589 constraints: vec![],
11590 indexes: vec![],
11591 partition: None,
11592 vector_indexes: vec![],
11593 search_indexes: vec![],
11594 triggers: vec![],
11595 junction: true,
11596 signals: vec![],
11597 },
11598 ],
11599 scalars: vec![],
11600 enums: vec![],
11601 named_tuples: vec![],
11602 globals: vec![],
11603 functions: vec![],
11604 aliases: vec![],
11605 channels: vec![],
11606 ..Default::default()
11607 }
11608 }
11609
11610 #[test]
11611 fn test_link_property_in_a_links_own_filter() {
11612 let schema = make_schema_with_through_and_prop();
11616 let out = compile_and_emit_with("SELECT Product { tags: { } filter @weight > 0.5 }", &schema);
11617 assert!(out.sql.contains("\"jt\".\"weight\" > (0.5::float8)"), "{}", out.sql);
11618 }
11619
11620 #[test]
11621 fn test_exists_on_a_link_property() {
11622 let schema = make_schema_with_through_and_prop();
11625 let out = compile_and_emit_with("SELECT Product { tags: { } filter exists @weight }", &schema);
11626 assert!(out.sql.contains("\"jt\".\"weight\" IS NOT NULL"), "{}", out.sql);
11627 }
11628
11629 #[test]
11630 fn test_link_property_in_a_computed_link_filter() {
11631 let schema = make_schema_with_through_and_prop();
11632 let out = compile_and_emit_with(
11633 "SELECT Product { best := (select .tags filter @weight > 0.5 limit 1) { @weight } }",
11634 &schema,
11635 );
11636 assert!(out.sql.contains("\"jt\".\"weight\" > (0.5::float8)"), "{}", out.sql);
11637 assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
11638 }
11639
11640 #[test]
11641 fn test_link_property_in_a_projected_sub_select_reads_the_junction() {
11642 let schema = make_schema_with_through_and_prop();
11645 let out = compile_and_emit_with(
11646 "SELECT Product { t := (select .tags filter @weight > 0.5 limit 1).id }",
11647 &schema,
11648 );
11649 assert!(out.sql.contains("\"t3\".\"weight\" > (0.5::float8)"), "{}", out.sql);
11650 }
11651
11652 #[test]
11653 fn test_unknown_link_property_names_the_through_type() {
11654 let schema = make_schema_with_through_and_prop();
11655 let ast = parse::parse("SELECT Product { tags: { } filter @nope > 0.5 }").unwrap();
11656 let err = match ir::compile(&ast, &schema) {
11657 Ok(_) => panic!("expected a compile error"),
11658 Err(e) => format!("{e}"),
11659 };
11660 assert!(err.contains("default::ProductTag") && err.contains("nope"), "{err}");
11661 }
11662
11663 #[test]
11664 fn test_link_property_outside_a_link_says_where_it_belongs() {
11665 let schema = make_schema_with_through_and_prop();
11666 let ast = parse::parse("SELECT Product { t := @weight }").unwrap();
11667 let err = match ir::compile(&ast, &schema) {
11668 Ok(_) => panic!("expected a compile error"),
11669 Err(e) => format!("{e}"),
11670 };
11671 assert!(err.contains("is a link property"), "{err}");
11672 }
11673
11674 #[test]
11675 fn test_multilink_append_with_link_property() {
11676 let schema = make_schema_with_through_and_prop();
11677 let out = compile_and_emit_with(
11678 "UPDATE Product FILTER .id = $id SET { tags += (SELECT Tag FILTER .id = $tid) { @weight := <float64>$w } }",
11679 &schema,
11680 );
11681 assert!(out.sql.contains("\"weight\""), "missing weight column:\n{}", out.sql);
11683 assert!(
11686 out.sql
11687 .contains("ON CONFLICT (\"source\", \"target\") DO UPDATE SET \"weight\" = EXCLUDED.\"weight\""),
11688 "missing upsert conflict clause:\n{}",
11689 out.sql
11690 );
11691 }
11692
11693 #[test]
11694 fn test_multilink_append_union_with_different_link_property_values() {
11695 let schema = make_schema_with_through_and_prop();
11699 let out = compile_and_emit_with(
11700 "UPDATE Product FILTER .id = $id SET { \
11701 tags += (SELECT Tag FILTER .id = $aid) { @weight := <float64>$w1 } \
11702 union (SELECT Tag FILTER .id = $bid) { @weight := <float64>$w2 } \
11703 }",
11704 &schema,
11705 );
11706 assert!(
11707 out.sql.contains("UNION ALL"),
11708 "expected a UNION ALL between the two shaped targets:\n{}",
11709 out.sql
11710 );
11711 assert_eq!(
11714 out.sql.matches("AS \"weight\"").count(),
11715 2,
11716 "each union branch must project its own weight:\n{}",
11717 out.sql
11718 );
11719 }
11720
11721 #[test]
11722 fn test_multilink_append_without_link_property_keeps_do_nothing() {
11723 let schema = make_schema_with_through_and_prop();
11726 let out = compile_and_emit_with(
11727 "UPDATE Product FILTER .id = $id SET { tags += (SELECT Tag FILTER .id = $tid) }",
11728 &schema,
11729 );
11730 assert!(
11731 out.sql.contains("ON CONFLICT DO NOTHING"),
11732 "expected plain DO NOTHING when no link properties are set:\n{}",
11733 out.sql
11734 );
11735 assert!(
11736 !out.sql.contains("\"weight\""),
11737 "unexpected weight column with no link property assignment:\n{}",
11738 out.sql
11739 );
11740 }
11741
11742 #[test]
11743 fn test_multilink_link_property_rejected_on_standard_junction() {
11744 let ast = crate::parse::parse(
11747 "UPDATE Person FILTER .id = $id SET { posts += (SELECT Post FILTER .title = $t) { @weight := <float64>$w } }",
11748 ).unwrap();
11749 match crate::ir::compile(&ast, &make_schema()) {
11750 Ok(_) => panic!("expected a compile error for link property on a Standard junction"),
11751 Err(e) => assert!(
11752 e.to_string().contains("Through"),
11753 "expected a Through[...]-related error, got: {e}"
11754 ),
11755 }
11756 }
11757
11758 #[test]
11759 fn test_multilink_link_property_rejected_on_remove() {
11760 let schema = make_schema_with_through_and_prop();
11761 let ast = crate::parse::parse(
11762 "UPDATE Product FILTER .id = $id SET { tags -= (SELECT Tag FILTER .id = $tid) { @weight := <float64>$w } }",
11763 )
11764 .unwrap();
11765 match crate::ir::compile(&ast, &schema) {
11766 Ok(_) => panic!("expected a compile error for link property on a remove (-=)"),
11767 Err(e) => assert!(
11768 e.to_string().contains("removing"),
11769 "expected a remove-related error, got: {e}"
11770 ),
11771 }
11772 }
11773
11774 #[test]
11775 fn test_insert_with_multilink_assignment() {
11776 let schema = make_schema_with_through_and_prop();
11781 let out = compile_and_emit_with(
11782 "INSERT Product { name := $name, tags := (SELECT Tag FILTER .id = $tid) { @weight := <float64>$w } }",
11783 &schema,
11784 );
11785 assert!(
11787 out.sql.contains("\"_w__ids\" AS (\nINSERT INTO"),
11788 "missing row-insert CTE:\n{}",
11789 out.sql
11790 );
11791 assert!(
11792 out.sql.contains("\"_w__ml_add_0\" AS ("),
11793 "missing junction-append CTE:\n{}",
11794 out.sql
11795 );
11796 assert!(out.sql.contains("\"weight\""), "missing weight column:\n{}", out.sql);
11797 assert!(
11798 out.sql.contains("\"_w\" AS (\n SELECT * FROM \"_w__ids\"\n)"),
11799 "missing _w passthrough:\n{}",
11800 out.sql
11801 );
11802 assert_eq!(
11803 out.sql.matches("WITH\n").count(),
11804 1,
11805 "must be a single flat top-level WITH block:\n{}",
11806 out.sql
11807 );
11808 }
11809
11810 #[test]
11818 fn test_user_with_binding_merges_into_a_statements_own_ctes() {
11819 let schema = make_schema_with_through_and_prop();
11820 for query in [
11821 "with t := (select Tag filter .id = $tid) insert Product { name := $name, tags := t }",
11822 "with t := (select Tag filter .id = $tid) \
11823 update Product filter .name = $name set { tags += t }",
11824 ] {
11825 let out = compile_and_emit_with(query, &schema);
11826 let flat: String = out.sql.split_whitespace().collect::<Vec<_>>().join(" ");
11827 assert!(
11828 !flat.contains(") WITH "),
11829 "a second WITH clause is invalid SQL:\n{}",
11830 out.sql
11831 );
11832 assert_eq!(
11833 out.sql.matches("WITH").count(),
11834 1,
11835 "exactly one WITH keyword expected:\n{}",
11836 out.sql
11837 );
11838 }
11839 }
11840
11841 #[test]
11842 fn test_with_bound_insert_with_multilink_assignment() {
11843 let schema = make_schema_with_through_and_prop();
11847 let out = compile_and_emit_with(
11848 "with insert0 := (insert Product { name := $name, tags := (select Tag filter .id = $tid) }) select insert0",
11849 &schema,
11850 );
11851 assert!(
11852 out.sql.contains("\"insert0__ids\" AS (\nINSERT INTO"),
11853 "missing row-insert CTE:\n{}",
11854 out.sql
11855 );
11856 assert!(
11857 out.sql.contains("\"insert0__ml_add_0\" AS ("),
11858 "missing junction-append CTE:\n{}",
11859 out.sql
11860 );
11861 assert!(
11862 out.sql
11863 .contains("\"insert0\" AS (\n SELECT * FROM \"insert0__ids\"\n)"),
11864 "missing insert0 passthrough:\n{}",
11865 out.sql
11866 );
11867 assert_eq!(
11868 out.sql.matches("WITH\n").count(),
11869 1,
11870 "must be a single flat top-level WITH block:\n{}",
11871 out.sql
11872 );
11873 }
11874
11875 #[test]
11876 fn test_with_block_cte_over_computed_global_merges_into_single_with_clause() {
11877 let mut schema = make_schema();
11886 schema.globals.push(GlobalDescriptor {
11887 name: "current_user_id".into(),
11888 module: "default".into(),
11889 scalar_type: "std::uuid".into(),
11890 required: false,
11891 default_expr: None,
11892 computed_expr: None,
11893 });
11894 schema.globals.push(GlobalDescriptor {
11895 name: "current_user".into(),
11896 module: "default".into(),
11897 scalar_type: "Person".into(),
11898 required: false,
11899 default_expr: None,
11900 computed_expr: Some("select default::Person filter .id = global current_user_id".into()),
11901 });
11902 let out = compile_and_emit_with("with\n user := (select global current_user)\nselect user;", &schema);
11903 assert_eq!(
11904 out.sql.matches("WITH").count(),
11905 1,
11906 "must be a single WITH clause, got:\n{}",
11907 out.sql
11908 );
11909 }
11910
11911 #[test]
11912 fn test_several_mutations_in_a_free_set_each_become_a_cte() {
11913 let schema = make_schema();
11914 let out = compile_and_emit_with(
11915 "select { (update Person filter .name = 'a' set { age := 1 }), \
11916 (update Company filter .name = 'b' set { name := 'c' }) }",
11917 &schema,
11918 );
11919 assert_eq!(
11920 out.sql.matches("UPDATE").count(),
11921 2,
11922 "both mutations have to run, got:\n{}",
11923 out.sql
11924 );
11925 assert!(out.sql.contains("WITH"), "each becomes a CTE, got:\n{}", out.sql);
11926 }
11927
11928 #[test]
11929 fn test_updating_a_binding_touches_only_its_own_rows() {
11930 let schema = make_schema();
11933 let out = compile_and_emit_with(
11934 "with mine := (select Person filter .name = 'a') select { (update mine set { age := 1 }) }",
11935 &schema,
11936 );
11937 assert!(
11938 out.sql.contains("UPDATE") && out.sql.contains("= ANY(ARRAY(SELECT"),
11939 "expected the update narrowed to the binding's rows, got:\n{}",
11940 out.sql
11941 );
11942 }
11943
11944 #[test]
11945 fn test_updating_a_traversal_touches_only_the_rows_it_lands_on() {
11946 let mut schema = make_schema();
11947 let company = schema
11950 .types
11951 .iter_mut()
11952 .find(|t| t.name == "Company")
11953 .expect("test schema has a Company type");
11954 company.properties.push(crate::schema::PropertyDescriptor {
11955 name: "id".into(),
11956 pg_type: "uuid".into(),
11957 nullable: false,
11958 default_sql: Some("uuidv7()".into()),
11959 default_pyql: None,
11960 description: None,
11961 check_constraints: vec![],
11962 is_exclusive: true,
11963 is_pk: true,
11964 is_readonly: true,
11965 rewrites: vec![],
11966 tuple_members: None,
11967 column_type: None,
11968 });
11969 let out = compile_and_emit_with(
11970 "with mine := (select Person filter .name = 'a') select { (update mine.company set { name := 'x' }) }",
11971 &schema,
11972 );
11973 assert!(
11974 out.sql.contains("= ANY(ARRAY(SELECT"),
11975 "expected the update narrowed to the traversal's rows, got:\n{}",
11976 out.sql
11977 );
11978 assert!(
11979 out.sql.contains("\"Company\""),
11980 "expected the table the traversal ends on, got:\n{}",
11981 out.sql
11982 );
11983 }
11984
11985 #[test]
11986 fn test_aggregate_over_a_multi_valued_path_keeps_the_set_flat() {
11987 let schema = make_schema();
11992 let out = compile_and_emit_with(
11993 "with a := (select Person limit 1) select { titles := array_agg(a.posts.title) }",
11994 &schema,
11995 );
11996 assert!(
11997 !out.sql.contains("array_agg(ARRAY("),
11998 "the aggregate must take the set, not the array standing for it, got:\n{}",
11999 out.sql
12000 );
12001 assert!(
12002 out.sql.contains("array_agg(") && out.sql.contains("\"title\""),
12003 "expected the aggregate over the traversal's own column, got:\n{}",
12004 out.sql
12005 );
12006 assert!(
12009 out.sql.contains("ROW(") && !out.sql.contains("jsonb_build_object"),
12010 "expected a composite row, got:\n{}",
12011 out.sql
12012 );
12013 }
12014
12015 #[test]
12016 fn test_with_bound_root_read_by_name_inside_a_nested_select() {
12017 let schema = make_schema();
12022 let out = compile_and_emit_with(
12023 "with
12024 owner := (select Person limit 1)
12025select owner { posts := (select owner.posts.title) };",
12026 &schema,
12027 );
12028 assert!(
12029 out.sql.contains("WITH") && out.sql.contains("\"title\""),
12030 "expected the nested select to traverse the binding, got:\n{}",
12031 out.sql
12032 );
12033 }
12034
12035 #[test]
12036 fn test_path_traversal_into_with_bound_cte_of_object_type() {
12037 let mut schema = make_schema();
12043 schema.globals.push(GlobalDescriptor {
12044 name: "current_user_id".into(),
12045 module: "default".into(),
12046 scalar_type: "std::uuid".into(),
12047 required: false,
12048 default_expr: None,
12049 computed_expr: None,
12050 });
12051 schema.globals.push(GlobalDescriptor {
12052 name: "current_user".into(),
12053 module: "default".into(),
12054 scalar_type: "Person".into(),
12055 required: false,
12056 default_expr: None,
12057 computed_expr: Some("select default::Person filter .id = global current_user_id".into()),
12058 });
12059 let out = compile_and_emit_with(
12060 "with\n user := (select global current_user)\nselect user.name;",
12061 &schema,
12062 );
12063 assert!(
12064 out.sql.contains("FROM \"user\""),
12065 "expected path traversal from the CTE, got:\n{}",
12066 out.sql
12067 );
12068 assert_eq!(
12069 out.sql.matches("WITH").count(),
12070 1,
12071 "must be a single WITH clause, got:\n{}",
12072 out.sql
12073 );
12074 }
12075
12076 #[test]
12077 fn test_with_bound_schema_object_property_access_in_filter_expression() {
12078 let schema = make_schema();
12086 let out = compile_and_emit_with(
12087 "with\n person := (select detached Person filter .id = <uuid>$id)\n\
12088 select Person filter .id not in person.id;",
12089 &schema,
12090 );
12091 assert!(
12092 out.sql.contains("<> ALL((SELECT") && out.sql.contains(".\"id\"") && out.sql.contains("FROM \"person\""),
12093 "got:\n{}",
12094 out.sql
12095 );
12096 }
12097
12098 #[test]
12099 fn test_with_bound_schema_object_scalar_property_access() {
12100 let schema = make_schema();
12104 let out = compile_and_emit_with(
12105 "with\n person := (select detached Person filter .id = <uuid>$id)\n\
12106 select Person filter .name = person.name;",
12107 &schema,
12108 );
12109 assert!(
12110 out.sql.contains("(SELECT") && out.sql.contains(".\"name\"") && out.sql.contains("FROM \"person\""),
12111 "got:\n{}",
12112 out.sql
12113 );
12114 }
12115
12116 #[test]
12117 fn test_with_bound_schema_object_link_traversal_reaches_nested_property() {
12118 let schema = make_schema();
12125 let out = compile_and_emit_with(
12126 "with\n person := (select detached Person filter .id = <uuid>$id)\n\
12127 select Company filter .name = person.company.name;",
12128 &schema,
12129 );
12130 assert!(
12131 out.sql.contains("(SELECT") && out.sql.contains("\"name\"") && out.sql.contains("\"Company\""),
12132 "got:\n{}",
12133 out.sql
12134 );
12135 }
12136
12137 #[test]
12138 fn test_with_bound_schema_object_bare_link_reduces_to_id() {
12139 let schema = make_schema();
12143 let out = compile_and_emit_with(
12144 "with\n person := (select detached Person filter .id = <uuid>$id)\n\
12145 select Person filter .id = person.company;",
12146 &schema,
12147 );
12148 assert!(
12149 out.sql.contains("(SELECT") && out.sql.contains("\"id\""),
12150 "got:\n{}",
12151 out.sql
12152 );
12153 }
12154
12155 #[test]
12156 fn test_with_bound_schema_object_unknown_property_suggests_a_close_match() {
12157 let schema = make_schema();
12158 let ast = parse::parse(
12159 "with\n person := (select detached Person filter .id = <uuid>$id)\n\
12160 select Person filter .name = person.nam;",
12161 )
12162 .unwrap();
12163 match ir::compile(&ast, &schema) {
12164 Err(err) => assert!(format!("{err}").contains("Did you mean 'name'"), "got: {err}"),
12165 Ok(_) => panic!("expected a compile error"),
12166 }
12167 }
12168
12169 #[test]
12170 fn test_with_bound_free_object_passthrough_preserves_all_fields() {
12171 let out = compile_and_emit("with\n test := { test2 := 1.0, test3 := 'str' }\nselect test;");
12177 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
12178 panic!("expected Object shape, got {:?}", out.shape.root)
12179 };
12180 assert_eq!(pointers.len(), 2);
12181 assert!(matches!(&pointers[0], ShapeNode::Scalar { name, .. } if name == "test2"));
12182 assert!(matches!(&pointers[1], ShapeNode::Scalar { name, .. } if name == "test3"));
12183 }
12184
12185 #[test]
12186 fn test_with_bound_free_object_field_access() {
12187 let out = compile_and_emit("with\n test := { test2 := 1.0, test3 := 'str' }\nselect test.test2;");
12192 assert!(out.sql.contains("\"test2\" FROM \"test\""), "got:\n{}", out.sql);
12193 }
12194
12195 #[test]
12196 fn test_with_bound_free_object_nested_field_access_chain() {
12197 let out =
12202 compile_and_emit("with\n test := { test2 := 1.0, test3 := { foo := 'bar' } }\nselect test.test3.foo;");
12203 assert!(out.sql.contains("\"test3\" FROM \"test\""), "got:\n{}", out.sql);
12204 assert!(
12205 out.sql.contains("->'foo'"),
12206 "expected jsonb field extraction, got:\n{}",
12207 out.sql
12208 );
12209 }
12210
12211 #[test]
12212 fn test_with_bound_free_object_nested_field_access_wrong_field_errors() {
12213 let schema = make_schema();
12216 let ast = parse::parse("with\n test := { test2 := 1.0, test3 := { foo := 'bar' } }\nselect test.test3.nope;")
12217 .unwrap();
12218 assert!(ir::compile(&ast, &schema).is_err());
12219 }
12220
12221 #[test]
12222 fn test_nested_free_object_literal_in_computed_shape_element() {
12223 let out = compile_and_emit("select default::Person { id, test := { foo := 'bar' } };");
12229 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
12230 panic!("expected Object shape")
12231 };
12232 let test_node = pointers
12233 .iter()
12234 .find(|p| matches!(p, ShapeNode::Object { name, .. } if name == "test"))
12235 .unwrap_or_else(|| panic!("expected an Object shape node for 'test', got {:?}", pointers));
12236 let ShapeNode::Object {
12239 type_name, pointers, ..
12240 } = test_node
12241 else {
12242 unreachable!("matched as an object just above")
12243 };
12244 assert_eq!(*type_name, None);
12245 assert!(
12246 matches!(&pointers[..], [ShapeNode::Scalar { name, .. }] if name == "foo"),
12247 "got {pointers:?}"
12248 );
12249 }
12250
12251 #[test]
12252 fn test_bare_free_cte_reference_in_computed_shape_collapses_to_empty() {
12253 let out = compile_and_emit(
12258 "with\n test := { test2 := 1.0, test3 := 'str' }\n\
12259 select default::Person { id, test := test };",
12260 );
12261 assert!(
12262 out.sql.contains("ROW()"),
12263 "expected an empty free object, got:\n{}",
12264 out.sql
12265 );
12266 }
12267
12268 #[test]
12269 fn test_shaped_free_cte_reference_projects_fields() {
12270 let out = compile_and_emit(
12274 "with\n test := { test2 := 1.0, test3 := 'str' }\n\
12275 select default::Person { id, test := test { test2 } };",
12276 );
12277 assert!(
12279 out.sql.contains("ROW((SELECT \"test2\" FROM \"test\"))"),
12280 "got:\n{}",
12281 out.sql
12282 );
12283 assert!(
12284 !out.sql.contains("'test3'"),
12285 "test3 should not be projected, got:\n{}",
12286 out.sql
12287 );
12288 }
12289
12290 #[test]
12291 fn test_insert_multilink_remove_rejected() {
12292 let schema = make_schema_with_through_and_prop();
12293 let ast =
12294 crate::parse::parse("INSERT Product { name := $name, tags -= (SELECT Tag FILTER .id = $tid) }").unwrap();
12295 match crate::ir::compile(&ast, &schema) {
12296 Ok(_) => panic!("expected a compile error for `-=` on a multi-link at insert time"),
12297 Err(e) => assert!(
12298 e.to_string().contains("nothing to remove"),
12299 "expected a 'nothing to remove yet' error, got: {e}"
12300 ),
12301 }
12302 }
12303
12304 #[test]
12305 fn test_multilink_junction_info_disambiguates_self_referencing_through_type() {
12306 let schema = make_schema_with_through();
12313 let out = compile_and_emit_with(
12314 "UPDATE Person FILTER .id = $id SET { friends += (SELECT Person FILTER .id = $fid) }",
12315 &schema,
12316 );
12317 assert!(
12318 out.sql.contains("(\"person\", \"friend\")"),
12319 "expected two distinct FK columns:\n{}",
12320 out.sql
12321 );
12322 assert!(
12323 !out.sql.contains("(\"person\", \"person\")"),
12324 "source/target collapsed to the same column:\n{}",
12325 out.sql
12326 );
12327 }
12328
12329 #[test]
12330 fn test_shape_descriptor_scalars() {
12331 let out = compile_and_emit("SELECT Person { name, age }");
12332 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
12333 panic!()
12334 };
12335 assert_eq!(pointers.len(), 4); assert!(matches!(&pointers[0], ShapeNode::Scalar { name, position: 0 } if name == "__type__"));
12337 assert!(matches!(&pointers[1], ShapeNode::Scalar { name, position: 1 } if name == "id"));
12338 assert!(matches!(&pointers[2], ShapeNode::Scalar { name, position: 2 } if name == "name"));
12339 assert!(matches!(&pointers[3], ShapeNode::Scalar { name, position: 3 } if name == "age"));
12340 }
12341
12342 #[test]
12343 fn test_shape_descriptor_multi_link() {
12344 let out = compile_and_emit("SELECT Person { name, posts { title } }");
12345 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
12346 panic!()
12347 };
12348 assert_eq!(pointers.len(), 4);
12350 let ShapeNode::Array {
12351 name,
12352 position,
12353 element,
12354 } = &pointers[3]
12355 else {
12356 panic!()
12357 };
12358 assert_eq!(name, "posts");
12359 assert_eq!(*position, 3);
12360 let ShapeNode::Object {
12361 pointers: elem_pointers,
12362 ..
12363 } = element.as_ref()
12364 else {
12365 panic!()
12366 };
12367 assert_eq!(elem_pointers.len(), 2);
12370 }
12371
12372 #[test]
12373 fn test_select_order_by_limit() {
12374 let out = compile_and_emit("SELECT Person { name } ORDER BY .name ASC LIMIT 10");
12375 assert!(out.sql.contains("ORDER BY"));
12376 assert!(out.sql.contains("LIMIT 10"));
12377 }
12378
12379 #[test]
12380 fn test_insert_returning() {
12381 let out = compile_and_emit("INSERT Person { name := 'Alice', age := 30 }");
12382 assert!(out.sql.contains("INSERT INTO \"public\".\"Person\""));
12383 assert!(out.sql.contains("RETURNING"));
12384 assert!(out.sql.contains("'default::Person'::text"));
12385 assert!(out.sql.contains(") AS result"));
12386 let ShapeNode::Object {
12388 cardinality, pointers, ..
12389 } = &out.shape.root
12390 else {
12391 panic!()
12392 };
12393 assert_eq!(*cardinality, Cardinality::Required);
12394 assert!(
12396 pointers
12397 .iter()
12398 .any(|f| matches!(f, ShapeNode::Scalar { name, .. } if name == "id"))
12399 );
12400 assert!(
12401 !pointers
12402 .iter()
12403 .any(|f| matches!(f, ShapeNode::Scalar { name, .. } if name == "name"))
12404 );
12405 }
12406
12407 #[test]
12408 fn test_update_returning() {
12409 let out = compile_and_emit("UPDATE Person FILTER .name = $name SET { age := 31 }");
12410 assert!(out.sql.contains("UPDATE \"public\".\"Person\""));
12411 assert!(out.sql.contains("SET"));
12412 assert!(out.sql.contains("RETURNING"));
12414 assert!(out.sql.contains("'default::Person'::text"));
12415 assert!(!out.sql.contains("\"name\"::text"), "bare UPDATE must not return name");
12416 }
12417
12418 #[test]
12419 fn test_update_set_tuple_param_cast_uses_direct_jsonb_cast_not_to_jsonb() {
12420 let out = compile_and_emit("UPDATE Person FILTER .id = $id SET { age := <tuple<x: float64>>$val }");
12427 assert!(
12428 out.sql.contains(")::jsonb"),
12429 "expected a direct ::jsonb cast, got:\n{}",
12430 out.sql
12431 );
12432 assert!(
12433 !out.sql.contains("to_jsonb($"),
12434 "must not pass a bare param straight into to_jsonb(): got:\n{}",
12435 out.sql
12436 );
12437 }
12438
12439 #[test]
12440 fn test_empty_set_cast_to_object_type_clears_optional_link() {
12441 let out = compile_and_emit("UPDATE Person FILTER .id = $id SET { company := <default::Company>{} }");
12450 assert!(out.sql.contains("\"company_id\" = NULL"), "got:\n{}", out.sql);
12451 }
12452
12453 #[test]
12454 fn test_an_aggregate_over_a_set_literal_inside_a_shape() {
12455 let out = compile_and_emit("UPDATE Person FILTER .id = $id SET { age := min({<int64>100, .age}) }");
12461 assert!(out.sql.contains("min(v)"), "got:\n{}", out.sql);
12462 assert!(
12463 out.sql.contains("UNION ALL"),
12464 "both operands must reach the aggregate:\n{}",
12465 out.sql
12466 );
12467 }
12468
12469 #[test]
12470 fn test_an_aggregate_over_a_set_literal_reads_the_subject() {
12471 let out = compile_and_emit("SELECT Person { capped := min({<int64>100, .age}) }");
12474 assert!(out.sql.contains("min(v)"), "got:\n{}", out.sql);
12475 assert!(
12476 out.sql.contains("\"age\""),
12477 "the relative path must reach the column:\n{}",
12478 out.sql
12479 );
12480 }
12481
12482 #[test]
12483 fn test_an_aggregate_over_a_union_inside_a_shape() {
12484 let out = compile_and_emit("SELECT Person { capped := min((<int64>100 union .age)) }");
12487 assert!(out.sql.contains("min(v)"), "got:\n{}", out.sql);
12488 assert!(
12489 out.sql.contains("UNION ALL"),
12490 "both arms must reach the aggregate:\n{}",
12491 out.sql
12492 );
12493 }
12494
12495 #[test]
12496 fn test_an_aggregate_over_a_union_of_three_arms_flattens() {
12497 let out = compile_and_emit("SELECT Person { capped := min((<int64>1 union <int64>2 union .age)) }");
12498 assert_eq!(
12499 out.sql.matches("UNION ALL").count(),
12500 2,
12501 "three arms means two joins:\n{}",
12502 out.sql
12503 );
12504 }
12505
12506 #[test]
12507 fn test_an_aggregate_over_an_empty_set_emits_no_row_source() {
12508 for query in ["SELECT Person { capped := min({}) }", "SELECT min({})"] {
12512 let ast = parse::parse(query).expect("parse failed");
12513 let sql = ir::compile(&ast, &make_schema())
12514 .map(|ir| emit(&ir).sql)
12515 .unwrap_or_default();
12516 assert!(
12517 !sql.contains("FROM () AS"),
12518 "{query} emitted an empty row source:\n{sql}"
12519 );
12520 }
12521 }
12522
12523 #[test]
12524 fn test_delete_returning() {
12525 let out = compile_and_emit("DELETE Person FILTER .id = $id");
12526 assert!(out.sql.contains("DELETE FROM \"public\".\"Person\""));
12527 assert!(out.sql.contains("RETURNING"));
12529 assert!(out.sql.contains("'default::Person'::text"));
12530 assert!(!out.sql.contains("\"name\"::text"), "bare DELETE must not return name");
12531 }
12532
12533 #[test]
12534 fn test_select_over_insert() {
12535 let out = compile_and_emit("SELECT (INSERT Person { name := $name, age := $age }) { id, name }");
12536 assert!(out.sql.contains("WITH\n\"_dml\" AS ("));
12538 assert!(out.sql.contains("INSERT INTO"));
12539 assert!(out.sql.contains("RETURNING *"));
12540 assert!(out.sql.contains("'default::Person'::text"));
12542 assert!(out.sql.contains("\"name\"::text"));
12543 }
12544
12545 #[test]
12546 fn test_select_over_update() {
12547 let out = compile_and_emit("SELECT (UPDATE Person FILTER .id = $id SET { name := $name }) { id, name }");
12548 assert!(out.sql.contains("WITH\n\"_dml\" AS ("));
12549 assert!(out.sql.contains("UPDATE"));
12550 assert!(out.sql.contains("RETURNING \"t1\".*"), "{}", out.sql);
12551 assert!(out.sql.contains("\"name\"::text"));
12552 }
12553
12554 #[test]
12555 fn test_insert_user_specified_id_denied_by_default() {
12556 let schema = make_schema();
12557 let ast = parse::parse("INSERT Person { id := <uuid>$id, name := $name, age := $age }").unwrap();
12558 match ir::compile_with_config(&ast, &schema, &ir::SessionConfig::default()) {
12559 Err(err) => assert!(err.to_string().contains("cannot assign to property 'id'"), "got: {err}"),
12560 Ok(_) => panic!("expected id assignment to be denied by default"),
12561 }
12562 }
12563
12564 #[test]
12565 fn test_insert_user_specified_id_allowed_when_configured() {
12566 let schema = make_schema();
12567 let ast = parse::parse("INSERT Person { id := <uuid>$id, name := $name, age := $age }").unwrap();
12568 let config = ir::SessionConfig {
12569 allow_user_specified_id: true,
12570 };
12571 let ir_out = ir::compile_with_config(&ast, &schema, &config)
12572 .expect("expected id assignment to be allowed with allow_user_specified_id");
12573 let out = emit(&ir_out);
12574 assert!(out.sql.contains("INSERT INTO"));
12575 }
12576
12577 #[test]
12578 fn test_update_user_specified_id_denied_even_when_configured() {
12579 let schema = make_schema();
12580 let ast = parse::parse("UPDATE Person FILTER .name = $name SET { id := <uuid>$id }").unwrap();
12581 let config = ir::SessionConfig {
12582 allow_user_specified_id: true,
12583 };
12584 match ir::compile_with_config(&ast, &schema, &config) {
12585 Err(err) => assert!(err.to_string().contains("cannot assign to property 'id'"), "got: {err}"),
12586 Ok(_) => panic!("expected UPDATE to always deny reassigning id"),
12587 }
12588 }
12589
12590 #[test]
12591 fn test_select_over_update_multilink_only() {
12592 let out = compile_and_emit(
12602 "SELECT (UPDATE Person FILTER .id = $id SET { posts += (SELECT Post FILTER .title = $title) }) { id, name }",
12603 );
12604 assert!(
12605 out.sql.contains("\"_dml__ml_add_0\""),
12606 "missing junction-append CTE:\n{}",
12607 out.sql
12608 );
12609 assert!(out.sql.contains("INSERT INTO"), "missing junction INSERT:\n{}", out.sql);
12610 assert!(
12613 out.sql.contains("\"_dml__ids\" AS (\nSELECT"),
12614 "expected SELECT-based _ids CTE:\n{}",
12615 out.sql
12616 );
12617 assert!(
12618 !out.sql.contains("SET\n\nWHERE") && !out.sql.contains("SET \nWHERE"),
12619 "empty SET clause regression:\n{}",
12620 out.sql
12621 );
12622 assert_eq!(
12625 out.sql.matches("WITH\n").count(),
12626 1,
12627 "junction CTE must not be nested in a second WITH:\n{}",
12628 out.sql
12629 );
12630 assert!(
12632 out.sql.contains("\"_dml\" AS (\n SELECT * FROM \"_dml__ids\"\n)"),
12633 "missing _dml passthrough:\n{}",
12634 out.sql
12635 );
12636 }
12637
12638 #[test]
12639 fn test_select_over_update_scalar_and_multilink() {
12640 let out = compile_and_emit(
12644 "SELECT (UPDATE Person FILTER .id = $id SET { name := $name, posts += (SELECT Post FILTER .title = $title) }) { id, name }",
12645 );
12646 assert!(
12647 out.sql.contains("\"_dml__ml_add_0\""),
12648 "missing junction-append CTE:\n{}",
12649 out.sql
12650 );
12651 assert!(
12652 out.sql.contains("\"_dml__ids\" AS (\nUPDATE"),
12653 "expected UPDATE-based _ids CTE:\n{}",
12654 out.sql
12655 );
12656 assert!(
12657 out.sql.contains("\"name\" = "),
12658 "missing scalar SET assignment:\n{}",
12659 out.sql
12660 );
12661 assert_eq!(
12662 out.sql.matches("WITH\n").count(),
12663 1,
12664 "junction CTE must not be nested in a second WITH:\n{}",
12665 out.sql
12666 );
12667 }
12668
12669 #[test]
12670 fn test_with_bound_insert_and_multilink_update_forward_ref() {
12671 let out = compile_and_emit(
12679 "with insert0 := (insert Post { title := $title }), update0 := (update Person filter .id = $id set { posts += (select insert0) }) select { insert0, update0 }",
12680 );
12681 assert!(
12682 out.sql.contains("\"insert0\" AS (\n INSERT INTO"),
12683 "missing insert0 CTE:\n{}",
12684 out.sql
12685 );
12686 assert!(
12687 out.sql.contains("\"update0__ml_add_0\""),
12688 "missing junction-append CTE for update0:\n{}",
12689 out.sql
12690 );
12691 assert!(
12692 out.sql.contains("\"update0__ids\" AS (\nSELECT"),
12693 "expected SELECT-based update0 ids CTE (no scalar changes):\n{}",
12694 out.sql
12695 );
12696 assert!(
12697 out.sql
12698 .contains("\"update0\" AS (\n SELECT * FROM \"update0__ids\"\n)"),
12699 "missing update0 passthrough:\n{}",
12700 out.sql
12701 );
12702 assert_eq!(
12705 out.sql.matches("WITH\n").count(),
12706 1,
12707 "must be a single flat top-level WITH block:\n{}",
12708 out.sql
12709 );
12710 }
12711
12712 #[test]
12713 fn test_select_over_delete() {
12714 let out = compile_and_emit("SELECT (DELETE Person FILTER .id = $id) { id, name }");
12715 assert!(out.sql.contains("WITH\n\"_dml\" AS ("));
12716 assert!(out.sql.contains("DELETE FROM"));
12717 assert!(out.sql.contains("RETURNING *"));
12718 assert!(out.sql.contains("\"name\"::text"));
12719 }
12720
12721 fn make_schema_with_rewrite() -> SchemaDescriptor {
12722 use crate::schema::RewriteEntry;
12723 let mut schema = make_schema();
12724 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
12726 person.properties.push(PropertyDescriptor {
12727 name: "slug".into(),
12728 pg_type: "text".into(),
12729 nullable: true,
12730 default_sql: None,
12731 default_pyql: None,
12732 description: None,
12733 check_constraints: vec![],
12734 is_exclusive: false,
12735 is_pk: false,
12736 is_readonly: false,
12737 rewrites: vec![
12738 RewriteEntry {
12739 on: 1,
12740 handler: "str_lower(.name)".into(),
12741 }, RewriteEntry {
12743 on: 2,
12744 handler: "str_lower(.name)".into(),
12745 }, ],
12747 tuple_members: None,
12748 column_type: None,
12749 });
12750 schema
12751 }
12752
12753 #[test]
12754 fn a_link_assigned_from_a_select_keeps_its_clauses() {
12755 let out = compile_and_emit(
12756 "INSERT Person { name := 'a', age := 1, company := (SELECT Company ORDER BY .name LIMIT 1) }",
12757 );
12758 assert!(
12759 out.sql.contains("ORDER BY") && out.sql.contains("LIMIT 1"),
12760 "{}",
12761 out.sql
12762 );
12763 }
12764
12765 #[test]
12766 fn a_rewrite_leaves_the_statement_alone() {
12767 let schema = make_schema_with_rewrite();
12769 for query in [
12770 "INSERT Person { name := $name, age := 30 }",
12771 "UPDATE Person FILTER .id = $id SET { name := $name }",
12772 ] {
12773 let out = compile_and_emit_with(query, &schema);
12774 assert!(!out.sql.contains("\"slug\""), "{}", out.sql);
12775 }
12776 }
12777
12778 #[test]
12779 fn a_rewrite_reads_the_row_being_written() {
12780 let schema = make_schema_with_rewrite();
12781 let assignments = crate::ir::compile_rewrite_assignments("default::Person", 1, &schema).unwrap();
12782 let [assignment] = assignments.as_slice() else {
12783 panic!("one rewrite on insert")
12784 };
12785 assert_eq!(assignment.column, "slug");
12786 assert!(assignment.sql.contains("NEW.\"name\""), "{}", assignment.sql);
12787 }
12788
12789 #[test]
12790 fn rewrites_become_before_triggers() {
12791 let ddl = crate::export::export_schema(&make_schema_with_rewrite()).unwrap();
12792 for event in ["BEFORE INSERT", "BEFORE UPDATE"] {
12793 assert!(
12794 ddl.contains(&format!("{event} ON \"public\".\"Person\"")),
12795 "{event}:\n{ddl}"
12796 );
12797 }
12798 assert!(ddl.contains("NEW.\"slug\" := _pylon_rewrites.\"v0\";"), "{ddl}");
12799 }
12800
12801 #[test]
12802 fn a_rewrite_walking_a_link_starts_from_the_new_row() {
12803 use crate::schema::RewriteEntry;
12805 let mut schema = make_schema_with_rewrite();
12806 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
12807 let slug = person.properties.iter_mut().find(|p| p.name == "slug").unwrap();
12808 slug.rewrites = vec![RewriteEntry {
12809 on: 1,
12810 handler: "<str>(.company is Company) ++ .company.name".into(),
12811 }];
12812 let assignments = crate::ir::compile_rewrite_assignments("default::Person", 1, &schema).unwrap();
12813 let sql = &assignments[0].sql;
12814 assert!(!sql.contains("\"public\".\"Person\""), "{sql}");
12815 assert!(
12816 sql.contains("(SELECT (NEW).*)") && sql.contains("NEW.\"company_id\""),
12817 "{sql}"
12818 );
12819 }
12820
12821 #[test]
12822 fn test_unless_conflict_do_nothing() {
12823 let out = compile_and_emit("INSERT Person { name := $name } UNLESS CONFLICT");
12824 assert!(out.sql.contains("ON CONFLICT DO NOTHING"));
12825 }
12826
12827 #[test]
12828 fn test_unless_conflict_on_do_nothing() {
12829 let out = compile_and_emit("INSERT Person { name := $name } UNLESS CONFLICT ON .name");
12830 assert!(out.sql.contains("ON CONFLICT (\"name\") DO NOTHING"));
12831 }
12832
12833 #[test]
12834 fn test_unless_conflict_do_update() {
12835 let out = compile_and_emit(
12836 "INSERT Person { name := $name, age := $age } \
12837 UNLESS CONFLICT ON .name \
12838 ELSE (UPDATE Person SET { age := $age })",
12839 );
12840 assert!(out.sql.contains("ON CONFLICT (\"name\") DO UPDATE SET"));
12841 assert!(out.sql.contains("\"age\" = $2"));
12842 assert!(!out.sql.contains("DO NOTHING"));
12844 }
12845
12846 #[test]
12847 fn test_unless_conflict_else_update_self_reference_is_qualified() {
12848 let out = compile_and_emit(
12856 "INSERT Person { name := $name, age := 0 } \
12857 UNLESS CONFLICT ON .name \
12858 ELSE (UPDATE Person SET { age := .age + 1 })",
12859 );
12860 assert!(
12861 out.sql.contains("\"Person\".\"age\""),
12862 "self-reference must be qualified with the table's own name, got:\n{}",
12863 out.sql
12864 );
12865 assert!(
12866 !out.sql.contains("SET \"age\" = (\"age\""),
12867 "must not emit an unqualified (ambiguous) self-reference, got:\n{}",
12868 out.sql
12869 );
12870 }
12871
12872 #[test]
12873 fn test_unless_conflict_do_update_no_on() {
12874 let out = compile_and_emit(
12875 "INSERT Person { name := $name } \
12876 UNLESS CONFLICT \
12877 ELSE (UPDATE Person SET { age := 0 })",
12878 );
12879 assert!(out.sql.contains("ON CONFLICT DO UPDATE SET"));
12880 assert!(out.sql.contains("\"age\" = 0"));
12881 }
12882
12883 #[test]
12884 fn test_select_over_select() {
12885 let out = compile_and_emit("SELECT (SELECT Person FILTER .age > 18) { name }");
12886 assert!(out.sql.contains("WITH\n\"_dml\" AS ("));
12888 assert!(out.sql.contains("SELECT *"));
12890 assert!(out.sql.contains("FROM \"public\".\"Person\""));
12891 assert!(out.sql.contains("WHERE"));
12893 assert!(out.sql.contains("'default::Person'::text"));
12895 assert!(out.sql.contains("\"name\"::text"));
12896 }
12897
12898 #[test]
12899 fn test_select_over_select_with_outer_filter() {
12900 let out = compile_and_emit("SELECT (SELECT Person FILTER .age > 18) { name } FILTER .name = $name");
12901 assert!(out.sql.contains("WITH\n\"_dml\" AS ("));
12902 assert!(out.sql.contains("SELECT *"));
12903 assert_eq!(out.sql.matches("WHERE").count(), 2);
12905 assert!(out.sql.contains("$1"));
12906 }
12907
12908 #[test]
12909 fn test_insert_link_subquery() {
12910 let out = compile_and_emit("INSERT Person { name := $name, company := (SELECT Company FILTER .name = $co) }");
12911 assert!(out.sql.contains("\"company_id\""));
12913 assert!(out.sql.contains("SELECT"));
12914 assert!(out.sql.contains("\"id\""));
12916 assert!(out.sql.contains("FROM \"public\".\"Company\""));
12917 assert!(out.sql.contains("$2")); }
12920
12921 #[test]
12922 fn test_update_link_subquery() {
12923 let out =
12924 compile_and_emit("UPDATE Person FILTER .id = $id SET { company := (SELECT Company FILTER .name = $co) }");
12925 assert!(out.sql.contains("\"company_id\""));
12926 assert!(out.sql.contains("SELECT"));
12927 assert!(out.sql.contains("FROM \"public\".\"Company\""));
12928 }
12929
12930 #[test]
12931 fn test_insert_link_value_from_nested_insert_hoists_a_with_cte() {
12932 let out = compile_and_emit(
12940 "INSERT Person { name := 'Alice', company := (select (insert Company { name := 'Acme' }) { id }) }",
12941 );
12942 assert!(
12943 out.sql.starts_with("WITH"),
12944 "expected a WITH-hoisted CTE, got:\n{}",
12945 out.sql
12946 );
12947 assert!(
12948 out.sql.contains("INSERT INTO \"public\".\"Company\""),
12949 "expected the nested insert to be its own CTE, got:\n{}",
12950 out.sql,
12951 );
12952 assert!(
12955 out.sql.contains("\"company_id\") SELECT") && out.sql.contains(".\"id\" FROM"),
12956 "expected the outer insert to switch from VALUES to SELECT ... FROM <cte>, got:\n{}",
12957 out.sql,
12958 );
12959 assert!(
12960 !out.sql.contains("FROM \"public\".\"Company\" AS"),
12961 "must not select from the real Company table, got:\n{}",
12962 out.sql
12963 );
12964 }
12965
12966 #[test]
12967 fn test_update_link_value_from_nested_insert_hoists_a_with_cte_and_adds_from_clause() {
12968 let out = compile_and_emit(
12969 "UPDATE Person FILTER .id = $id SET { company := (select (insert Company { name := 'Acme' }) { id }) }",
12970 );
12971 assert!(
12972 out.sql.starts_with("WITH"),
12973 "expected a WITH-hoisted CTE, got:\n{}",
12974 out.sql
12975 );
12976 assert!(
12977 out.sql.contains("INSERT INTO \"public\".\"Company\""),
12978 "expected the nested insert to be its own CTE, got:\n{}",
12979 out.sql,
12980 );
12981 assert!(
12984 out.sql.contains("\nFROM \""),
12985 "expected a FROM clause referencing the nested CTE, got:\n{}",
12986 out.sql
12987 );
12988 assert!(out.sql.contains("SET \"company_id\" = "), "got:\n{}", out.sql);
12989 }
12990
12991 #[test]
12992 fn test_update_link_value_from_nested_insert_combines_with_multilink_mutation() {
12993 let out = compile_and_emit(
12998 "UPDATE Person FILTER .id = $id SET { \
12999 company := (select (insert Company { name := 'Acme' }) { id }), \
13000 posts += (SELECT Post FILTER .title = $t) \
13001 }",
13002 );
13003 assert!(out.sql.starts_with("WITH"), "got:\n{}", out.sql);
13004 assert!(
13005 out.sql.contains("INSERT INTO \"public\".\"Company\""),
13006 "expected the nested insert to be its own CTE, got:\n{}",
13007 out.sql,
13008 );
13009 assert!(
13010 out.sql.contains("\"_ids\" AS (\nUPDATE") && out.sql.contains("\nFROM \""),
13011 "expected the _ids UPDATE to gain a FROM clause referencing the nested CTE, got:\n{}",
13012 out.sql,
13013 );
13014 assert!(
13015 out.sql.contains("\"_ml_add_0\""),
13016 "expected the junction-append CTE to still be present, got:\n{}",
13017 out.sql
13018 );
13019 }
13020
13021 #[test]
13022 fn test_update_link_value_from_nested_insert_combines_with_interface_target() {
13023 fn id_prop() -> PropertyDescriptor {
13029 PropertyDescriptor {
13030 name: "id".into(),
13031 pg_type: "uuid".into(),
13032 nullable: false,
13033 default_sql: Some("uuidv7()".into()),
13034 default_pyql: None,
13035 description: None,
13036 check_constraints: vec![],
13037 is_exclusive: true,
13038 is_pk: true,
13039 is_readonly: true,
13040 rewrites: vec![],
13041 tuple_members: None,
13042 column_type: None,
13043 }
13044 }
13045 fn text_prop(name: &str) -> PropertyDescriptor {
13046 PropertyDescriptor {
13047 name: name.into(),
13048 pg_type: "text".into(),
13049 nullable: false,
13050 default_sql: None,
13051 default_pyql: None,
13052 description: None,
13053 check_constraints: vec![],
13054 is_exclusive: false,
13055 is_pk: false,
13056 is_readonly: false,
13057 rewrites: vec![],
13058 tuple_members: None,
13059 column_type: None,
13060 }
13061 }
13062 fn company_link() -> LinkDescriptor {
13063 LinkDescriptor {
13064 name: "company".into(),
13065 target: "default::Company".into(),
13066 nullable: true,
13067 through: None,
13068 description: None,
13069 default_pyql: None,
13070 is_exclusive: false,
13071 is_readonly: false,
13072 rewrites: vec![],
13073 on_delete: vec![],
13074 }
13075 }
13076 let schema = SchemaDescriptor {
13077 types: vec![
13078 TypeDescriptor {
13079 name: "Company".into(),
13080 module: "default".into(),
13081 table: "Company".into(),
13082 abstract_: false,
13083 materialized: true,
13084 description: None,
13085 parents: vec![],
13086 interfaces: vec![],
13087 bases: vec![],
13088 properties: vec![id_prop(), text_prop("name")],
13089 links: vec![],
13090 multilinks: vec![],
13091 computed: vec![],
13092 constraints: vec![],
13093 indexes: vec![],
13094 partition: None,
13095 vector_indexes: vec![],
13096 search_indexes: vec![],
13097 triggers: vec![],
13098 junction: false,
13099 signals: vec![],
13100 },
13101 TypeDescriptor {
13102 name: "Account".into(),
13103 module: "default".into(),
13104 table: "Account".into(),
13105 abstract_: true,
13106 materialized: true,
13107 description: None,
13108 parents: vec![],
13109 interfaces: vec![],
13110 bases: vec![],
13111 properties: vec![id_prop(), text_prop("email")],
13112 links: vec![company_link()],
13113 multilinks: vec![],
13114 computed: vec![],
13115 constraints: vec![],
13116 indexes: vec![],
13117 partition: None,
13118 vector_indexes: vec![],
13119 search_indexes: vec![],
13120 triggers: vec![],
13121 junction: false,
13122 signals: vec![],
13123 },
13124 TypeDescriptor {
13125 name: "Individual".into(),
13126 module: "default".into(),
13127 table: "Individual".into(),
13128 abstract_: false,
13129 materialized: true,
13130 description: None,
13131 parents: vec![],
13132 interfaces: vec!["default::Account".into()],
13133 bases: vec![],
13134 properties: vec![id_prop(), text_prop("email"), text_prop("first_name")],
13135 links: vec![company_link()],
13136 multilinks: vec![],
13137 computed: vec![],
13138 constraints: vec![],
13139 indexes: vec![],
13140 partition: None,
13141 vector_indexes: vec![],
13142 search_indexes: vec![],
13143 triggers: vec![],
13144 junction: false,
13145 signals: vec![],
13146 },
13147 ],
13148 scalars: vec![],
13149 enums: vec![],
13150 named_tuples: vec![],
13151 globals: vec![],
13152 functions: vec![],
13153 aliases: vec![],
13154 channels: vec![],
13155 ..Default::default()
13156 };
13157 let out = compile_and_emit_with(
13158 "UPDATE Account FILTER .email = $email \
13159 SET { company := (select (insert Company { name := 'Acme' }) { id }) }",
13160 &schema,
13161 );
13162 assert!(out.sql.starts_with("WITH"), "got:\n{}", out.sql);
13163 assert!(
13164 out.sql.contains("INSERT INTO \"public\".\"Company\""),
13165 "expected the nested insert to be its own CTE, got:\n{}",
13166 out.sql,
13167 );
13168 assert!(
13169 out.sql.contains("UPDATE \"public\".\"Individual\"") && out.sql.contains("\nFROM \"_nested_dml_0\""),
13170 "expected the per-implementor UPDATE to gain a FROM clause referencing the nested CTE, got:\n{}",
13171 out.sql,
13172 );
13173 }
13174
13175 #[test]
13176 fn test_a_backlink_narrowed_to_a_supertype() {
13177 let mut schema = interface_link_schema();
13181 schema
13184 .types
13185 .iter_mut()
13186 .find(|t| t.name == "Account")
13187 .expect("the helper declares Account")
13188 .links
13189 .clear();
13190 let out = compile_and_emit_with("SELECT Company { owners := .<company[is Account] { email } }", &schema);
13191 assert!(
13192 out.sql.contains("\"public\".\"Individual\""),
13193 "the backlink must read the type that declares it:\n{}",
13194 out.sql
13195 );
13196 }
13197
13198 #[test]
13199 fn a_path_through_a_backlink_narrowed_to_a_supertype() {
13200 let mut schema = interface_link_schema();
13201 schema
13202 .types
13203 .iter_mut()
13204 .find(|t| t.name == "Account")
13205 .expect("the helper declares Account")
13206 .links
13207 .clear();
13208 let out = compile_and_emit_with(
13209 "SELECT Company { name } FILTER EXISTS (SELECT .<company[is Account])",
13210 &schema,
13211 );
13212 assert!(out.sql.contains("\"public\".\"Individual\""), "{}", out.sql);
13213 }
13214
13215 fn interface_link_schema() -> SchemaDescriptor {
13219 fn id_prop() -> PropertyDescriptor {
13225 PropertyDescriptor {
13226 name: "id".into(),
13227 pg_type: "uuid".into(),
13228 nullable: false,
13229 default_sql: None,
13230 default_pyql: None,
13231 description: None,
13232 check_constraints: vec![],
13233 is_exclusive: true,
13234 is_pk: true,
13235 is_readonly: true,
13236 rewrites: vec![],
13237 tuple_members: None,
13238 column_type: None,
13239 }
13240 }
13241 fn text_prop(name: &str) -> PropertyDescriptor {
13242 PropertyDescriptor {
13243 name: name.into(),
13244 pg_type: "text".into(),
13245 nullable: false,
13246 default_sql: None,
13247 default_pyql: None,
13248 description: None,
13249 check_constraints: vec![],
13250 is_exclusive: false,
13251 is_pk: false,
13252 is_readonly: false,
13253 rewrites: vec![],
13254 tuple_members: None,
13255 column_type: None,
13256 }
13257 }
13258 fn link(name: &str, target: &str) -> LinkDescriptor {
13259 LinkDescriptor {
13260 name: name.into(),
13261 target: target.into(),
13262 nullable: true,
13263 through: None,
13264 description: None,
13265 default_pyql: None,
13266 is_exclusive: false,
13267 is_readonly: false,
13268 rewrites: vec![],
13269 on_delete: vec![],
13270 }
13271 }
13272 fn ty(
13273 name: &str,
13274 abstract_: bool,
13275 interfaces: Vec<String>,
13276 properties: Vec<PropertyDescriptor>,
13277 links: Vec<LinkDescriptor>,
13278 ) -> TypeDescriptor {
13279 TypeDescriptor {
13280 name: name.into(),
13281 module: "default".into(),
13282 table: name.into(),
13283 abstract_,
13284 materialized: true,
13285 description: None,
13286 parents: vec![],
13287 interfaces,
13288 bases: vec![],
13289 properties,
13290 links,
13291 multilinks: vec![],
13292 computed: vec![],
13293 constraints: vec![],
13294 indexes: vec![],
13295 partition: None,
13296 vector_indexes: vec![],
13297 search_indexes: vec![],
13298 triggers: vec![],
13299 junction: false,
13300 signals: vec![],
13301 }
13302 }
13303 SchemaDescriptor {
13304 types: vec![
13305 ty("Company", false, vec![], vec![id_prop(), text_prop("name")], vec![]),
13306 ty(
13307 "Account",
13308 true,
13309 vec![],
13310 vec![id_prop(), text_prop("email")],
13311 vec![link("company", "default::Company")],
13312 ),
13313 ty(
13314 "Individual",
13315 false,
13316 vec!["default::Account".into()],
13317 vec![id_prop(), text_prop("email")],
13318 vec![link("company", "default::Company")],
13319 ),
13320 ty(
13321 "Token",
13322 false,
13323 vec![],
13324 vec![id_prop(), text_prop("value")],
13325 vec![link("account", "default::Account")],
13326 ),
13327 ],
13328 scalars: vec![],
13329 enums: vec![],
13330 named_tuples: vec![],
13331 globals: vec![],
13332 functions: vec![],
13333 aliases: vec![],
13334 channels: vec![],
13335 ..Default::default()
13336 }
13337 }
13338
13339 fn two_hop_interface_schema() -> SchemaDescriptor {
13343 let mut schema = interface_link_schema();
13344 let token = schema
13345 .types
13346 .iter()
13347 .find(|t| t.name == "Token")
13348 .expect("the helper declares Token")
13349 .clone();
13350 let mut connector = token.clone();
13351 connector.name = "Connector".into();
13352 connector.table = "Connector".into();
13353 connector.links[0].name = "provider".into();
13354 let mut installation = token;
13355 installation.name = "Installation".into();
13356 installation.table = "Installation".into();
13357 installation.links[0].name = "connector".into();
13358 installation.links[0].target = "default::Connector".into();
13359 schema.types.push(connector);
13360 schema.types.push(installation);
13361 schema
13362 }
13363
13364 #[test]
13365 fn a_narrowed_two_hop_walk_off_a_pinned_row_is_one_value() {
13366 let out = compile_and_emit_with(
13372 "SELECT { s := ((SELECT Installation FILTER .id = <uuid>$0).connector.provider[is Individual].email) ?? 'y' }",
13373 &two_hop_interface_schema(),
13374 );
13375 assert!(
13376 !out.sql.contains("ARRAY(SELECT"),
13377 "expected a value, not a set:\n{}",
13378 out.sql
13379 );
13380 assert!(
13381 out.sql.contains("COALESCE("),
13382 "the default must stay a scalar one:\n{}",
13383 out.sql
13384 );
13385 }
13386
13387 #[test]
13388 fn a_narrowed_two_hop_walk_off_an_unpinned_row_stays_a_set() {
13389 let out = compile_and_emit_with(
13390 "SELECT { s := (SELECT Installation FILTER .value = 'x').connector.provider[is Individual].email }",
13391 &two_hop_interface_schema(),
13392 );
13393 assert!(
13394 out.sql.contains("ARRAY(SELECT"),
13395 "a walk off many rows is a set:\n{}",
13396 out.sql
13397 );
13398 }
13399
13400 #[test]
13407 fn a_walk_ending_in_a_type_intersection_is_valid_in_a_filter() {
13408 let out = compile_and_emit_with(
13409 "SELECT Token { id } FILTER EXISTS .account[is Individual]",
13410 &interface_link_schema(),
13411 );
13412 assert!(
13413 out.sql.contains("\"public\".\"Individual\""),
13414 "the narrowing must reach the implementor's table:\n{}",
13415 out.sql
13416 );
13417 assert!(
13418 out.sql.contains("IS NOT NULL"),
13419 "exists over the narrowed walk is a nullness test:\n{}",
13420 out.sql
13421 );
13422 }
13423
13424 #[test]
13425 fn a_walk_ending_in_a_type_intersection_is_valid_off_a_binding() {
13426 let out = compile_and_emit_with(
13427 "WITH t := (SELECT Token LIMIT 1) SELECT { e := EXISTS t.account[is Individual] }",
13428 &interface_link_schema(),
13429 );
13430 assert!(
13431 out.sql.contains("\"public\".\"Individual\""),
13432 "the narrowing must reach the implementor's table:\n{}",
13433 out.sql
13434 );
13435 }
13436
13437 #[test]
13438 fn test_reading_a_link_through_an_interface_target() {
13439 let schema = interface_link_schema();
13440 let out = compile_and_emit_with("SELECT Token { account: { email, company: { name } } }", &schema);
13441 assert!(
13442 out.sql
13443 .contains("\"email\", \"company_id\" FROM \"public\".\"Individual\""),
13444 "the fanned-out interface must carry its own link columns:\n{}",
13445 out.sql
13446 );
13447 }
13448
13449 #[test]
13450 fn test_computed_pointer_in_shape_emits_expression() {
13451 let mut schema = make_schema();
13452 schema.types[0].computed.push(crate::schema::ComputedDescriptor {
13453 name: "upper_name".into(),
13454 expression: "str_upper(.name)".into(),
13455 return_type: Some("text".into()),
13456 link_target: None,
13457 link_multi: false,
13458 });
13459 let out = compile_and_emit_with("SELECT Person { upper_name }", &schema);
13460 assert!(
13461 out.sql.to_lowercase().contains("upper"),
13462 "expected upper() in SQL, got:\n{}",
13463 out.sql
13464 );
13465 }
13466
13467 fn make_interface_schema() -> SchemaDescriptor {
13470 fn id_prop() -> PropertyDescriptor {
13471 PropertyDescriptor {
13472 name: "id".into(),
13473 pg_type: "uuid".into(),
13474 nullable: false,
13475 default_sql: Some("uuidv7()".into()),
13476 default_pyql: None,
13477 description: None,
13478 check_constraints: vec![],
13479 is_exclusive: true,
13480 is_pk: true,
13481 is_readonly: true,
13482 rewrites: vec![],
13483 tuple_members: None,
13484 column_type: None,
13485 }
13486 }
13487 SchemaDescriptor {
13488 types: vec![
13489 TypeDescriptor {
13490 name: "Account".into(),
13491 module: "default".into(),
13492 table: "Account".into(),
13493 abstract_: true,
13494 materialized: false,
13495 description: None,
13496 parents: vec![],
13497 interfaces: vec![],
13498 bases: vec![],
13499 properties: vec![
13500 id_prop(),
13501 PropertyDescriptor {
13502 name: "email".into(),
13503 pg_type: "text".into(),
13504 nullable: false,
13505 default_sql: None,
13506 default_pyql: None,
13507 description: None,
13508 check_constraints: vec![],
13509 is_exclusive: false,
13510 is_pk: false,
13511 is_readonly: false,
13512 rewrites: vec![],
13513 tuple_members: None,
13514 column_type: None,
13515 },
13516 ],
13517 links: vec![],
13518 multilinks: vec![],
13519 computed: vec![],
13520 constraints: vec![],
13521 indexes: vec![],
13522 partition: None,
13523 vector_indexes: vec![],
13524 search_indexes: vec![],
13525 triggers: vec![],
13526 junction: false,
13527 signals: vec![],
13528 },
13529 TypeDescriptor {
13530 name: "Individual".into(),
13531 module: "default".into(),
13532 table: "Individual".into(),
13533 abstract_: false,
13534 materialized: true,
13535 description: None,
13536 parents: vec![],
13537 interfaces: vec!["default::Account".into()],
13538 bases: vec![],
13539 properties: vec![
13540 id_prop(),
13541 PropertyDescriptor {
13542 name: "first_name".into(),
13543 pg_type: "text".into(),
13544 nullable: false,
13545 default_sql: None,
13546 default_pyql: None,
13547 description: None,
13548 check_constraints: vec![],
13549 is_exclusive: false,
13550 is_pk: false,
13551 is_readonly: false,
13552 rewrites: vec![],
13553 tuple_members: None,
13554 column_type: None,
13555 },
13556 ],
13557 links: vec![],
13558 multilinks: vec![],
13559 computed: vec![crate::schema::ComputedDescriptor {
13560 name: "full_name".into(),
13561 expression: "str_upper(.first_name)".into(),
13562 return_type: Some("text".into()),
13563 link_target: None,
13564 link_multi: false,
13565 }],
13566 constraints: vec![],
13567 indexes: vec![],
13568 partition: None,
13569 vector_indexes: vec![],
13570 search_indexes: vec![],
13571 triggers: vec![],
13572 junction: false,
13573 signals: vec![],
13574 },
13575 ],
13576 scalars: vec![],
13577 enums: vec![],
13578 named_tuples: vec![],
13579 globals: vec![],
13580 functions: vec![],
13581 aliases: vec![],
13582 channels: vec![],
13583 ..Default::default()
13584 }
13585 }
13586
13587 #[test]
13588 fn test_type_intersection_splat_includes_concrete_computed_pointers() {
13589 let schema = make_interface_schema();
13594 let out = compile_and_emit_with("SELECT Account { *, [is Individual].* }", &schema);
13595 assert!(
13596 out.sql.to_lowercase().contains("upper"),
13597 "expected the concrete type's computed pointer (str_upper(...)) in the shape, got:\n{}",
13598 out.sql
13599 );
13600 assert!(
13601 out.sql.contains("\"first_name\""),
13602 "expected the concrete type's stored property too, got:\n{}",
13603 out.sql
13604 );
13605 }
13606
13607 #[test]
13608 fn test_enum_array_property_casts_to_text_array_and_shapes_as_a_list() {
13609 let mut schema = make_schema();
13616 schema.enums.push(crate::schema::EnumDescriptor {
13617 name: "Gender".into(),
13618 module: "default".into(),
13619 members: vec!["Male".into(), "Female".into()],
13620 });
13621 schema.types[0].properties.push(crate::schema::PropertyDescriptor {
13622 name: "genders".into(),
13623 pg_type: r#""default"."Gender"[]"#.into(),
13624 nullable: true,
13625 default_sql: None,
13626 default_pyql: None,
13627 description: None,
13628 check_constraints: vec![],
13629 is_exclusive: false,
13630 is_pk: false,
13631 is_readonly: false,
13632 rewrites: vec![],
13633 tuple_members: None,
13634 column_type: None,
13635 });
13636
13637 let out = compile_and_emit_with("SELECT Person { genders }", &schema);
13638 assert!(
13639 out.sql.contains(r#""genders"::text[]"#),
13640 "expected a text[] cast, got:\n{}",
13641 out.sql
13642 );
13643
13644 let crate::query::ShapeNode::Object { pointers, .. } = &out.shape.root else {
13645 panic!("expected Object shape, got {:?}", out.shape.root)
13646 };
13647 let genders = pointers
13648 .iter()
13649 .find(|node| matches!(node, crate::query::ShapeNode::Array { name, .. } if name == "genders"))
13650 .unwrap_or_else(|| panic!("expected an Array-shaped pointer, got {pointers:?}"));
13651 let crate::query::ShapeNode::Array { element, .. } = genders else {
13652 unreachable!()
13653 };
13654 assert!(
13655 matches!(
13656 element.as_ref(),
13657 crate::query::ShapeNode::Enum { enum_type, position: 0, .. }
13658 if enum_type == "default::Gender"
13659 ),
13660 "expected the elements to be enum-shaped, got {element:?}",
13661 );
13662 }
13663
13664 #[test]
13665 fn test_scalar_enum_property_still_casts_to_a_single_text() {
13666 let mut schema = make_schema();
13667 schema.enums.push(crate::schema::EnumDescriptor {
13668 name: "Gender".into(),
13669 module: "default".into(),
13670 members: vec!["Male".into(), "Female".into()],
13671 });
13672 schema.types[0].properties.push(crate::schema::PropertyDescriptor {
13673 name: "gender".into(),
13674 pg_type: r#""default"."Gender""#.into(),
13675 nullable: true,
13676 default_sql: None,
13677 default_pyql: None,
13678 description: None,
13679 check_constraints: vec![],
13680 is_exclusive: false,
13681 is_pk: false,
13682 is_readonly: false,
13683 rewrites: vec![],
13684 tuple_members: None,
13685 column_type: None,
13686 });
13687
13688 let out = compile_and_emit_with("SELECT Person { gender }", &schema);
13689 assert!(
13690 out.sql.contains(r#""gender"::text"#) && !out.sql.contains(r#""gender"::text[]"#),
13691 "expected a plain text cast, got:\n{}",
13692 out.sql
13693 );
13694 let crate::query::ShapeNode::Object { pointers, .. } = &out.shape.root else {
13695 panic!("expected Object shape")
13696 };
13697 assert!(
13698 pointers.iter().any(|node| matches!(
13699 node,
13700 crate::query::ShapeNode::Enum { name, enum_type, .. }
13701 if name == "gender" && enum_type == "default::Gender"
13702 )),
13703 "expected an Enum-shaped pointer, got {pointers:?}",
13704 );
13705 }
13706
13707 #[test]
13708 fn test_type_intersection_splat_keeps_enum_pointers_enum_shaped() {
13709 let mut schema = make_interface_schema();
13714 schema.enums.push(crate::schema::EnumDescriptor {
13715 name: "Gender".into(),
13716 module: "default".into(),
13717 members: vec!["Male".into(), "Female".into()],
13718 });
13719 let individual = schema
13722 .types
13723 .iter_mut()
13724 .find(|t| t.name == "Individual")
13725 .expect("the interface schema has an Individual type");
13726 individual.properties.push(crate::schema::PropertyDescriptor {
13727 name: "gender".into(),
13728 pg_type: r#""default"."Gender""#.into(),
13729 nullable: true,
13730 default_sql: None,
13731 default_pyql: None,
13732 description: None,
13733 check_constraints: vec![],
13734 is_exclusive: false,
13735 is_pk: false,
13736 is_readonly: false,
13737 rewrites: vec![],
13738 tuple_members: None,
13739 column_type: None,
13740 });
13741
13742 let out = compile_and_emit_with("SELECT Account { [is Individual].* }", &schema);
13743 let crate::query::ShapeNode::Object { pointers, .. } = &out.shape.root else {
13744 panic!("expected Object shape, got {:?}", out.shape.root)
13745 };
13746 assert!(
13747 pointers.iter().any(|node| matches!(
13748 node,
13749 crate::query::ShapeNode::Enum { name, enum_type, .. }
13750 if name == "gender" && enum_type == "default::Gender"
13751 )),
13752 "expected the enum property to stay enum-shaped through the intersection, got {pointers:?}",
13753 );
13754 }
13755
13756 #[test]
13757 fn test_shallow_splat_leaves_object_valued_computeds_to_the_deep_form() {
13758 let mut schema = make_schema();
13763 schema.types[0].computed.push(crate::schema::ComputedDescriptor {
13764 name: "authors".into(),
13765 expression: ".posts".into(),
13766 return_type: None,
13767 link_target: None,
13768 link_multi: false,
13769 });
13770 schema.types[0].computed.push(crate::schema::ComputedDescriptor {
13771 name: "age_next".into(),
13772 expression: ".age + 1".into(),
13773 return_type: Some("int8".into()),
13774 link_target: None,
13775 link_multi: false,
13776 });
13777
13778 let shallow = compile_and_emit_with("SELECT Person { * }", &schema);
13779 let crate::query::ShapeNode::Object { pointers, .. } = &shallow.shape.root else {
13780 panic!("expected Object shape")
13781 };
13782 let names: Vec<&str> = pointers.iter().map(shape_pointer_name).collect();
13783 assert!(
13784 names.contains(&"age_next"),
13785 "a computed property belongs in `*`: {names:?}"
13786 );
13787 assert!(!names.contains(&"authors"), "a computed link does not: {names:?}");
13788
13789 let deep = compile_and_emit_with("SELECT Person { ** }", &schema);
13790 let crate::query::ShapeNode::Object { pointers, .. } = &deep.shape.root else {
13791 panic!("expected Object shape")
13792 };
13793 let names: Vec<&str> = pointers.iter().map(shape_pointer_name).collect();
13794 assert!(
13795 names.contains(&"authors"),
13796 "`**` includes links, computed ones included: {names:?}"
13797 );
13798 }
13799
13800 fn shape_pointer_name(node: &crate::query::ShapeNode) -> &str {
13801 use crate::query::ShapeNode;
13802 match node {
13803 ShapeNode::Scalar { name, .. }
13804 | ShapeNode::Enum { name, .. }
13805 | ShapeNode::NamedTuple { name, .. }
13806 | ShapeNode::Object { name, .. }
13807 | ShapeNode::Array { name, .. } => name,
13808 _ => "",
13809 }
13810 }
13811
13812 #[test]
13813 fn test_any_over_a_multilink_comparison_does_not_warn() {
13814 let schema = make_schema();
13819 let ast = parse::parse("SELECT Person FILTER any(.posts.title = 'x')").unwrap();
13820 let ir = ir::compile(&ast, &schema).unwrap();
13821 assert!(
13822 !ir.warnings.iter().any(|w| w.contains("FILTER clause")),
13823 "any() states the intent, so nothing should warn: {:?}",
13824 ir.warnings
13825 );
13826 }
13827
13828 #[test]
13829 fn test_bare_multilink_comparison_still_warns() {
13830 let schema = make_schema();
13831 let ast = parse::parse("SELECT Person FILTER .posts.title = 'x'").unwrap();
13832 let ir = ir::compile(&ast, &schema).unwrap();
13833 assert!(
13834 ir.warnings.iter().any(|w| w.contains("FILTER clause")),
13835 "an unwrapped set-valued comparison still warns: {:?}",
13836 ir.warnings
13837 );
13838 }
13839
13840 #[test]
13841 fn test_link_to_an_interface_expands_over_its_implementors() {
13842 let mut schema = make_interface_schema();
13848 let account = schema
13849 .types
13850 .iter_mut()
13851 .find(|t| t.name == "Account")
13852 .expect("the interface schema has an Account type");
13853 account.materialized = true;
13854 let mut organization = schema
13856 .types
13857 .iter()
13858 .find(|t| t.name == "Individual")
13859 .expect("the interface schema has an Individual type")
13860 .clone();
13861 organization.name = "Organization".into();
13862 organization.table = "Organization".into();
13863 organization.computed.clear();
13864 schema.types.push(organization);
13865
13866 let owner = TypeDescriptor {
13867 name: "Note".into(),
13868 module: "default".into(),
13869 table: "Note".into(),
13870 abstract_: false,
13871 materialized: false,
13872 description: None,
13873 parents: vec![],
13874 interfaces: vec![],
13875 bases: vec![],
13876 properties: vec![],
13877 links: vec![LinkDescriptor {
13878 name: "owner".into(),
13879 target: "default::Account".into(),
13880 nullable: true,
13881 description: None,
13882 default_pyql: None,
13883 is_exclusive: false,
13884 is_readonly: false,
13885 rewrites: vec![],
13886 on_delete: vec![],
13887 through: None,
13888 }],
13889 multilinks: vec![],
13890 computed: vec![],
13891 constraints: vec![],
13892 indexes: vec![],
13893 partition: None,
13894 vector_indexes: vec![],
13895 search_indexes: vec![],
13896 triggers: vec![],
13897 junction: false,
13898 signals: vec![],
13899 };
13900 schema.types.push(owner);
13901
13902 let out = compile_and_emit_with("SELECT Note { owner: { id } }", &schema);
13903 assert!(
13904 out.sql.contains("UNION ALL"),
13905 "the link's target should expand over its implementors, got:\n{}",
13906 out.sql
13907 );
13908 assert!(
13909 out.sql.contains("'default::Individual'::text AS \"__type__\""),
13910 "each branch should carry its own discriminator, got:\n{}",
13911 out.sql
13912 );
13913 assert!(
13914 !out.sql.contains("FROM \"default\".\"Account\" AS"),
13915 "the interface's own view should no longer be read directly, got:\n{}",
13916 out.sql
13917 );
13918 }
13919
13920 fn make_two_implementor_schema() -> SchemaDescriptor {
13923 let mut schema = make_interface_schema();
13924 let mut organization = schema
13925 .types
13926 .iter()
13927 .find(|t| t.name == "Individual")
13928 .expect("the interface schema has an Individual type")
13929 .clone();
13930 organization.name = "Organization".into();
13931 organization.table = "Organization".into();
13932 organization.computed.clear();
13933 schema.types.push(organization);
13934 schema
13935 }
13936
13937 fn assert_rows_carry_their_concrete_type(query: &str) {
13940 let out = compile_and_emit_with(query, &make_two_implementor_schema());
13941 assert!(
13942 !out.sql.contains("'default::Account'::text"),
13943 "rows are labelled with the interface instead of their own type:\n{}",
13944 out.sql
13945 );
13946 assert!(
13947 out.sql.contains("'default::Individual'::text AS \"__type__\""),
13948 "the rows should come from the implementors, each tagging its own:\n{}",
13949 out.sql
13950 );
13951 }
13952
13953 #[test]
13954 fn an_aggregated_select_of_an_interface_carries_the_concrete_type() {
13955 assert_rows_carry_their_concrete_type("SELECT (xs := array_agg((SELECT Account { id })))");
13956 }
13957
13958 #[test]
13959 fn an_aggregated_binding_of_an_interface_carries_the_concrete_type() {
13960 assert_rows_carry_their_concrete_type("WITH a := (SELECT Account) SELECT (xs := array_agg((SELECT a { id })))");
13961 }
13962
13963 #[test]
13964 fn a_single_object_read_off_a_binding_carries_the_concrete_type() {
13965 assert_rows_carry_their_concrete_type("WITH a := (SELECT Account) SELECT (x := (SELECT a { id } LIMIT 1))");
13966 }
13967
13968 #[test]
13969 fn grouped_elements_of_an_interface_carry_the_concrete_type() {
13970 assert_rows_carry_their_concrete_type("group Account { id } by .email");
13971 }
13972
13973 fn make_note_schema() -> SchemaDescriptor {
13976 fn link_to(name: &str, target: &str) -> LinkDescriptor {
13977 LinkDescriptor {
13978 name: name.into(),
13979 target: target.into(),
13980 nullable: true,
13981 description: None,
13982 default_pyql: None,
13983 is_exclusive: false,
13984 is_readonly: false,
13985 rewrites: vec![],
13986 on_delete: vec![],
13987 through: None,
13988 }
13989 }
13990 let mut schema = make_two_implementor_schema();
13991 for account in schema
13992 .types
13993 .iter_mut()
13994 .filter(|t| ["Account", "Individual", "Organization"].contains(&t.name.as_str()))
13995 {
13996 account.links.push(link_to("pinned", "default::Note"));
13997 }
13998 schema.types.push(TypeDescriptor {
13999 name: "Note".into(),
14000 module: "default".into(),
14001 table: "Note".into(),
14002 abstract_: false,
14003 materialized: false,
14004 description: None,
14005 parents: vec![],
14006 interfaces: vec![],
14007 bases: vec![],
14008 properties: vec![],
14009 links: vec![link_to("owner", "default::Account")],
14010 multilinks: vec![],
14011 computed: vec![],
14012 constraints: vec![],
14013 indexes: vec![],
14014 partition: None,
14015 vector_indexes: vec![],
14016 search_indexes: vec![],
14017 triggers: vec![],
14018 junction: false,
14019 signals: vec![],
14020 });
14021 schema
14022 }
14023
14024 #[test]
14025 fn a_backlink_to_an_interface_carries_the_concrete_type() {
14026 let out = compile_and_emit_with(
14027 "SELECT Note { pinners := .<pinned[is Account] { id } }",
14028 &make_note_schema(),
14029 );
14030 assert!(
14031 !out.sql.contains("'default::Account'::text"),
14032 "the backlink's rows are labelled with the interface instead of their own type:\n{}",
14033 out.sql
14034 );
14035 }
14036
14037 #[test]
14038 fn a_link_reached_through_a_deep_splat_carries_the_concrete_type() {
14039 let out = compile_and_emit_with("SELECT Note { ** }", &make_note_schema());
14040 assert!(
14041 !out.sql.contains("'default::Account'::text"),
14042 "the splatted link's rows are labelled with the interface instead of their own type:\n{}",
14043 out.sql
14044 );
14045 }
14046
14047 #[test]
14048 fn test_path_traversal_onto_an_interface_carries_the_concrete_type() {
14049 let mut schema = make_interface_schema();
14054 let account = schema
14055 .types
14056 .iter_mut()
14057 .find(|t| t.name == "Account")
14058 .expect("the interface schema has an Account type");
14059 account.materialized = true;
14060 let mut organization = schema
14061 .types
14062 .iter()
14063 .find(|t| t.name == "Individual")
14064 .expect("the interface schema has an Individual type")
14065 .clone();
14066 organization.name = "Organization".into();
14067 organization.table = "Organization".into();
14068 organization.computed.clear();
14069 schema.types.push(organization);
14070 schema.types.push(TypeDescriptor {
14071 name: "Note".into(),
14072 module: "default".into(),
14073 table: "Note".into(),
14074 abstract_: false,
14075 materialized: false,
14076 description: None,
14077 parents: vec![],
14078 interfaces: vec![],
14079 bases: vec![],
14080 properties: vec![],
14081 links: vec![LinkDescriptor {
14082 name: "owner".into(),
14083 target: "default::Account".into(),
14084 nullable: true,
14085 description: None,
14086 default_pyql: None,
14087 is_exclusive: false,
14088 is_readonly: false,
14089 rewrites: vec![],
14090 on_delete: vec![],
14091 through: None,
14092 }],
14093 multilinks: vec![],
14094 computed: vec![],
14095 constraints: vec![],
14096 indexes: vec![],
14097 partition: None,
14098 vector_indexes: vec![],
14099 search_indexes: vec![],
14100 triggers: vec![],
14101 junction: false,
14102 signals: vec![],
14103 });
14104
14105 let out = compile_and_emit_with("SELECT Note.owner { id }", &schema);
14106 assert!(
14107 out.sql.contains("UNION ALL") && out.sql.contains("'default::Individual'::text AS \"__type__\""),
14108 "the traversal's target should fan out over implementors, got:\n{}",
14109 out.sql
14110 );
14111 assert!(
14112 !out.sql.contains("'default::Account'::text,"),
14113 "the row's type should be read off the row, not fixed to the interface, got:\n{}",
14114 out.sql
14115 );
14116 }
14117
14118 #[test]
14119 fn test_computed_multilink_chain_comes_back_as_rows() {
14120 let mut schema = make_schema();
14125 schema.types[0].computed.push(crate::schema::ComputedDescriptor {
14126 name: "coauthors".into(),
14127 expression: ".posts.author".into(),
14128 return_type: None,
14129 link_target: None,
14130 link_multi: false,
14131 });
14132 let post = schema
14133 .types
14134 .iter_mut()
14135 .find(|t| t.name == "Post")
14136 .expect("test schema has a Post type");
14137 post.links.push(LinkDescriptor {
14138 name: "author".into(),
14139 target: "default::Person".into(),
14140 nullable: true,
14141 description: None,
14142 default_pyql: None,
14143 is_exclusive: false,
14144 is_readonly: false,
14145 rewrites: vec![],
14146 on_delete: vec![],
14147 through: None,
14148 });
14149
14150 let out = compile_and_emit_with("SELECT Person { coauthors: { name } }", &schema);
14151 assert!(
14152 out.sql.contains("ARRAY(SELECT"),
14153 "the chain should aggregate into an array, got:\n{}",
14154 out.sql
14155 );
14156 assert!(
14157 out.sql.contains("\"name\""),
14158 "the requested sub-shape should survive, got:\n{}",
14159 out.sql
14160 );
14161
14162 let crate::query::ShapeNode::Object { pointers, .. } = &out.shape.root else {
14163 panic!("expected Object shape, got {:?}", out.shape.root)
14164 };
14165 let coauthors = pointers
14166 .iter()
14167 .find(|node| matches!(node, crate::query::ShapeNode::Array { name, .. } if name == "coauthors"))
14168 .unwrap_or_else(|| panic!("expected an Array-shaped pointer, got {pointers:?}"));
14169 let crate::query::ShapeNode::Array { element, .. } = coauthors else {
14170 unreachable!()
14171 };
14172 assert!(
14173 matches!(
14174 element.as_ref(),
14175 crate::query::ShapeNode::Object { type_name: Some(t), .. } if t == "default::Person"
14176 ),
14177 "the elements should hydrate as objects, got {element:?}",
14178 );
14179 }
14180
14181 #[test]
14182 fn test_multi_sort_with_then_emits_two_order_keys() {
14183 let out = compile_and_emit("SELECT Person { name } ORDER BY .name THEN .age DESC");
14184 assert!(out.sql.contains("ORDER BY"), "expected ORDER BY");
14185 assert!(out.sql.contains("\"name\""));
14187 assert!(out.sql.contains("\"age\""));
14188 assert!(out.sql.contains("DESC"));
14189 }
14190
14191 #[test]
14192 fn test_string_index_emits_str_subscript() {
14193 let out = compile_and_emit("SELECT 'hello'[1]");
14194 assert!(
14195 out.sql.contains("_pylon.str_subscript"),
14196 "expected _pylon.str_subscript() for string index, got:\n{}",
14197 out.sql
14198 );
14199 }
14200
14201 #[test]
14204 fn test_for_over_a_set_returning_call_is_not_a_values_clause() {
14205 let out = compile_and_emit("FOR n IN array_unpack(<array<int64>>$ns) UNION (SELECT n)");
14206 assert!(
14207 !out.sql.contains("VALUES (unnest"),
14208 "a set-returning iterator must not land in VALUES, got:\n{}",
14209 out.sql
14210 );
14211 assert!(
14212 out.sql.contains("SELECT unnest"),
14213 "expected the iterator in a select list, got:\n{}",
14214 out.sql
14215 );
14216 }
14217
14218 #[test]
14221 fn test_for_over_array_unpack_binds_the_element_type() {
14222 let out = compile_and_emit("FOR j IN array_unpack(<array<json>>$rows) UNION (SELECT <str>j['k'])");
14223 assert!(
14224 out.sql.contains("::jsonb AS v"),
14225 "the loop variable should carry the element type, got:\n{}",
14226 out.sql
14227 );
14228 }
14229
14230 #[test]
14233 fn test_a_binding_keeps_its_array_type_through_an_aggregate_and_a_condition() {
14234 for query in [
14235 "WITH ids := ([<uuid>$a] IF true ELSE [<uuid>$b]) SELECT Person FILTER contains(ids, .id)",
14236 "WITH ns := array_agg((SELECT Person.name)) SELECT Person FILTER contains(ns, .name)",
14237 ] {
14238 let out = compile_and_emit(query);
14239 assert!(
14240 out.sql.contains("@> ARRAY["),
14241 "expected the array overload of contains for `{query}`, got:\n{}",
14242 out.sql
14243 );
14244 }
14245 }
14246
14247 #[test]
14251 fn test_an_array_literals_walk_element_is_read_as_one_value() {
14252 let out = compile_and_emit("WITH p := (SELECT Person LIMIT 1) SELECT [<uuid>$a, p.id]");
14253 assert!(
14254 !out.sql.contains("ARRAY(SELECT"),
14255 "the element should be a scalar subquery, not an array of its own:\n{}",
14256 out.sql
14257 );
14258 }
14259
14260 #[test]
14264 fn test_ordering_a_walk_leaves_the_inner_filter_on_its_own_subject() {
14265 let out = compile_and_emit("SELECT (SELECT Person FILTER .name = 'x' LIMIT 1).posts { title } ORDER BY .title");
14266 assert!(
14267 out.sql.contains("WHERE (\"t0\".\"name\" = 'x')"),
14268 "the filter belongs to the Person the walk starts from, got:\n{}",
14269 out.sql
14270 );
14271 assert!(
14272 out.sql.contains("\"title\" ASC"),
14273 "the ordering belongs to the Post the walk lands on, got:\n{}",
14274 out.sql
14275 );
14276 }
14277
14278 #[test]
14282 fn test_a_walks_head_keeps_its_own_limit() {
14283 let out = compile_and_emit("SELECT (SELECT Person FILTER .name = 'x' LIMIT 1).posts { title }");
14284 let (head, walk) = out.sql.split_once(")\nSELECT").expect("expected a bound head");
14285 assert!(
14286 head.contains("LIMIT 1"),
14287 "the head keeps its own row count, got:\n{}",
14288 out.sql
14289 );
14290 assert!(
14291 !walk.contains("LIMIT"),
14292 "the walk keeps every row it reaches, got:\n{}",
14293 out.sql
14294 );
14295 }
14296
14297 #[test]
14301 fn test_for_over_json_array_unpack_binds_json() {
14302 let out = compile_and_emit(
14303 "FOR entry IN json_array_unpack(to_json(<str>$rows)) UNION (SELECT <str>json_get(entry, 'k'))",
14304 );
14305 assert!(
14306 out.sql.contains("::jsonb AS v"),
14307 "the loop variable should carry json, got:\n{}",
14308 out.sql
14309 );
14310 }
14311
14312 #[test]
14315 fn test_casting_json_get_reads_the_value_not_its_json_spelling() {
14316 let out = compile_and_emit("SELECT <uuid><str>json_get(<json>$p, 'id')");
14317 assert!(
14318 out.sql.contains("#>> '{}'"),
14319 "expected the json value to be read out, got:\n{}",
14320 out.sql
14321 );
14322 }
14323
14324 #[test]
14327 fn test_indexing_json_emits_a_jsonb_accessor() {
14328 let out = compile_and_emit("SELECT (<json>$payload)['label']");
14329 assert!(
14330 out.sql.contains("->'label'"),
14331 "expected a jsonb field access, got:\n{}",
14332 out.sql
14333 );
14334 assert!(
14335 !out.sql.contains("str_subscript"),
14336 "json must not be subscripted as a string, got:\n{}",
14337 out.sql
14338 );
14339 }
14340
14341 #[test]
14344 fn test_casting_json_to_a_scalar_unwraps_it() {
14345 let out = compile_and_emit("SELECT <str>to_json(<str>$s)");
14346 assert!(
14347 out.sql.contains("#>> '{}'"),
14348 "expected the json value to be extracted, got:\n{}",
14349 out.sql
14350 );
14351 }
14352
14353 #[test]
14357 fn test_a_binding_that_reads_the_loop_variable_is_keyed_by_iteration() {
14358 let out = compile_and_emit(
14359 "WITH made := (FOR p IN (SELECT Person) UNION ( \
14360 WITH mine := (SELECT p.posts LIMIT 1) \
14361 INSERT Company { name := mine.title } \
14362 )) SELECT count(made)",
14363 );
14364 assert!(
14365 out.sql
14366 .contains("\"mine\" AS (\nSELECT \"_for_p\".\"v\" AS \"_outer__mine\""),
14367 "the binding must be evaluated per iteration, got:\n{}",
14368 out.sql
14369 );
14370 assert!(
14371 out.sql.contains("\"mine\".\"_outer__mine\" = \"_for_p\".\"v\""),
14372 "a read of it must pin to the iteration in scope, got:\n{}",
14373 out.sql
14374 );
14375 }
14376
14377 #[test]
14381 fn test_a_loop_nested_in_a_loop_pairs_its_rows_by_iteration() {
14382 let out = compile_and_emit(
14383 "FOR n IN {'a', 'b'} UNION (INSERT Person { name := n, posts := (FOR t IN {n} UNION (INSERT Post { title := t })) })",
14384 );
14385 assert!(
14386 out.sql.contains("\"_outer\""),
14387 "the nested rows must carry an iteration key, got:\n{}",
14388 out.sql
14389 );
14390 assert!(
14391 !out.sql.contains("CROSS JOIN \"_nested_dml_0\""),
14392 "they must not be cross-joined onto every row, got:\n{}",
14393 out.sql
14394 );
14395 }
14396
14397 #[test]
14400 fn test_for_bodied_insert_writes_its_multi_link() {
14401 let out = compile_and_emit("FOR n IN {'a', 'b'} UNION (INSERT Person { name := n, posts := (SELECT Post) })");
14402 assert!(
14403 out.sql.contains("INSERT INTO \"public\".\"Person.posts\""),
14404 "the junction rows must be written too, got:\n{}",
14405 out.sql
14406 );
14407 }
14408
14409 #[test]
14410 fn test_string_slice_emits_substr() {
14411 let out = compile_and_emit("SELECT 'hello'[1:3]");
14412 assert!(
14413 out.sql.contains("substr"),
14414 "expected substr() for string slice, got:\n{}",
14415 out.sql
14416 );
14417 }
14418
14419 #[test]
14420 fn test_array_index_emits_subscript() {
14421 let out = compile_and_emit("SELECT [1, 2, 3][1]");
14422 assert!(
14423 out.sql.contains("_pylon.array_subscript"),
14424 "expected _pylon.array_subscript() for array index, got:\n{}",
14425 out.sql
14426 );
14427 }
14428
14429 #[test]
14430 fn test_index_into_array_returning_function_emits_array_subscript() {
14431 let out = compile_and_emit("SELECT str_split('a::b', '::')[-1]");
14432 assert!(
14433 out.sql.contains("_pylon.array_subscript(string_to_array"),
14434 "expected _pylon.array_subscript() over str_split, got:\n{}",
14435 out.sql
14436 );
14437 }
14438
14439 #[test]
14440 fn test_array_slice_emits_subscript() {
14441 let out = compile_and_emit("SELECT [1, 2, 3][0:2]");
14442 assert!(
14443 !out.sql.contains("substr"),
14444 "should not use substr for array, got:\n{}",
14445 out.sql
14446 );
14447 assert!(out.sql.contains(")["), "expected array slice syntax, got:\n{}", out.sql);
14448 }
14449
14450 #[test]
14451 fn test_open_ended_string_slice_emits_substr_no_length() {
14452 let out = compile_and_emit("SELECT 'hello'[2:]");
14453 assert!(out.sql.contains("substr"), "expected substr(), got:\n{}", out.sql);
14455 let substr_idx = out.sql.find("substr").unwrap();
14457 let after = &out.sql[substr_idx..];
14458 let commas = after.chars().take_while(|&c| c != ')').filter(|&c| c == ',').count();
14459 assert_eq!(commas, 1, "open-ended slice should use 2-arg substr, got:\n{}", out.sql);
14460 }
14461
14462 #[test]
14463 fn test_group_by_single_key() {
14464 let out = compile_and_emit("group Person { name } by .age");
14465 assert!(out.sql.contains("GROUP BY"), "expected GROUP BY, got:\n{}", out.sql);
14467 assert!(out.sql.contains("\"age\""), "expected age column, got:\n{}", out.sql);
14469 assert!(
14471 out.sql.contains("array_agg(ROW("),
14472 "expected array_agg, got:\n{}",
14473 out.sql
14474 );
14475 assert!(
14477 out.sql.contains("ARRAY['age']"),
14478 "expected grouping array, got:\n{}",
14479 out.sql
14480 );
14481 assert!(matches!(out.shape.root, crate::query::ShapeNode::Group { .. }));
14483 if let crate::query::ShapeNode::Group {
14484 key_nodes,
14485 grouping_position,
14486 elements_position,
14487 ..
14488 } = &out.shape.root
14489 {
14490 assert_eq!(key_nodes.len(), 1);
14491 assert!(matches!(&key_nodes[0], crate::query::ShapeNode::Scalar { name, position: 1 } if name == "age"));
14492 assert_eq!(*grouping_position, 2);
14493 assert_eq!(*elements_position, 3);
14494 }
14495 }
14496
14497 #[test]
14498 fn test_group_using_alias() {
14499 let out = compile_and_emit("group Person using decade := .age // 10 by decade");
14500 assert!(out.sql.contains("GROUP BY"), "expected GROUP BY, got:\n{}", out.sql);
14501 assert!(
14502 out.sql.contains("ARRAY['decade']"),
14503 "expected grouping array, got:\n{}",
14504 out.sql
14505 );
14506 if let crate::query::ShapeNode::Group { key_nodes, .. } = &out.shape.root {
14507 assert_eq!(key_nodes.len(), 1);
14508 assert!(matches!(&key_nodes[0], crate::query::ShapeNode::Scalar { name, .. } if name == "decade"));
14509 }
14510 }
14511
14512 #[test]
14513 fn test_group_orders_elements_within_each_group() {
14514 let out = compile_and_emit("group Person { name } by .age order by .name desc");
14515 assert!(
14516 out.sql.contains(")::record ORDER BY \"t0\".\"name\" DESC NULLS LAST)"),
14517 "{}",
14518 out.sql
14519 );
14520 assert!(!out.sql.contains("row_number()"), "{}", out.sql);
14521 }
14522
14523 #[test]
14524 fn test_group_limit_trims_each_group_not_the_result() {
14525 let out = compile_and_emit("group Person { name } by .age order by .name desc limit 1");
14528 assert!(
14529 out.sql
14530 .contains("row_number() OVER (PARTITION BY \"t0\".\"age\" ORDER BY \"t0\".\"name\" DESC NULLS LAST)"),
14531 "{}",
14532 out.sql
14533 );
14534 assert!(
14535 out.sql.contains("WHERE \"__rk\" > 0 AND \"__rk\" <= 0 + 1"),
14536 "{}",
14537 out.sql
14538 );
14539 assert!(!out.sql.contains("\nLIMIT"), "{}", out.sql);
14540 }
14541
14542 #[test]
14543 fn test_group_by_two_keys_with_a_per_group_limit() {
14544 let out = compile_and_emit(
14545 "group Person { name } by .age, .name filter .age > 18 order by .name desc offset 1 limit 2",
14546 );
14547 assert!(
14548 out.sql.contains("PARTITION BY \"t0\".\"age\", \"t0\".\"name\""),
14549 "{}",
14550 out.sql
14551 );
14552 let where_pos = out.sql.find("WHERE (\"t0\".\"age\" > 18)").expect("filter");
14555 let rank_pos = out.sql.find("WHERE \"__rk\"").expect("rank filter");
14556 assert!(where_pos < rank_pos, "{}", out.sql);
14557 assert!(
14558 out.sql.contains("GROUP BY \"t0\".\"age\", \"t0\".\"name\""),
14559 "{}",
14560 out.sql
14561 );
14562 }
14563
14564 #[test]
14565 fn test_group_filter_without_a_limit_is_a_plain_where() {
14566 let out = compile_and_emit("group Person { name } by .age filter .age > 18");
14567 assert!(out.sql.contains("WHERE (\"t0\".\"age\" > 18)"), "{}", out.sql);
14568 assert!(!out.sql.contains("row_number()"), "{}", out.sql);
14569 }
14570
14571 #[test]
14572 fn test_abs_path_concat_same_type() {
14573 let out = compile_and_emit("SELECT Person.name ++ ' ' ++ Person.name");
14574 assert!(out.sql.contains("\"name\""), "expected name column, got:\n{}", out.sql);
14575 assert!(out.sql.contains("||"), "expected concat operator, got:\n{}", out.sql);
14576 assert!(out.sql.contains("FROM"), "expected FROM clause, got:\n{}", out.sql);
14577 }
14578
14579 #[test]
14580 fn test_abs_path_single_property() {
14581 let out = compile_and_emit("SELECT Person.name");
14582 assert!(out.sql.contains("\"name\""), "expected name column, got:\n{}", out.sql);
14583 assert!(out.sql.contains("FROM"), "expected FROM clause, got:\n{}", out.sql);
14584 }
14585
14586 #[test]
14587 fn test_pgvector_cast_emits_vector_type() {
14588 let out = compile_and_emit("SELECT <pgvector::vector>[1.0, 2.0, 3.0]");
14589 assert!(
14590 out.sql.contains("::vector"),
14591 "expected ::vector cast, got:\n{}",
14592 out.sql
14593 );
14594 assert!(out.sql.contains("ARRAY["), "expected ARRAY literal, got:\n{}", out.sql);
14595 }
14596
14597 #[test]
14598 fn test_pgvector_euclidean_distance_emits_l2_operator() {
14599 let out = compile_and_emit(
14600 "SELECT pgvector::euclidean_distance(<pgvector::vector>[1.0, 2.0], <pgvector::vector>[3.0, 4.0])",
14601 );
14602 assert!(out.sql.contains("<->"), "expected <-> operator, got:\n{}", out.sql);
14603 }
14604
14605 #[test]
14606 fn test_pgvector_cosine_distance_emits_cosine_operator() {
14607 let out = compile_and_emit(
14608 "SELECT pgvector::cosine_distance(<pgvector::vector>[1.0, 2.0], <pgvector::vector>[3.0, 4.0])",
14609 );
14610 assert!(out.sql.contains("<=>"), "expected <=> operator, got:\n{}", out.sql);
14611 }
14612
14613 #[test]
14614 fn test_pgvector_neg_inner_product_emits_ip_operator() {
14615 let out = compile_and_emit(
14616 "SELECT pgvector::neg_inner_product(<pgvector::vector>[1.0, 2.0], <pgvector::vector>[3.0, 4.0])",
14617 );
14618 assert!(out.sql.contains("<#>"), "expected <#> operator, got:\n{}", out.sql);
14619 }
14620
14621 #[test]
14622 fn test_pgvector_inner_product_negates_ip_operator() {
14623 let out = compile_and_emit(
14624 "SELECT pgvector::inner_product(<pgvector::vector>[1.0, 2.0], <pgvector::vector>[3.0, 4.0])",
14625 );
14626 assert!(out.sql.contains("<#>"), "expected <#> operator, got:\n{}", out.sql);
14627 assert!(out.sql.contains("0.0"), "expected negation of <#>, got:\n{}", out.sql);
14628 }
14629
14630 #[test]
14631 fn test_crypto_digest_str_and_bytes_overloads_both_use_pgcrypto_digest() {
14632 let out = compile_and_emit("SELECT crypto::digest('hello', 'sha256')");
14633 assert!(
14634 out.sql.contains("digest("),
14635 "expected pgcrypto's digest(), got:\n{}",
14636 out.sql
14637 );
14638
14639 let out = compile_and_emit("SELECT crypto::digest(std::from_hex('68656c6c6f'), 'sha256')");
14640 assert!(
14641 out.sql.contains("digest("),
14642 "expected pgcrypto's digest(), got:\n{}",
14643 out.sql
14644 );
14645 }
14646
14647 #[test]
14648 fn test_crypto_hmac_str_and_bytes_overloads_both_use_pgcrypto_hmac() {
14649 let out = compile_and_emit("SELECT crypto::hmac('hello', 'key', 'sha256')");
14650 assert!(
14651 out.sql.contains("hmac("),
14652 "expected pgcrypto's hmac(), got:\n{}",
14653 out.sql
14654 );
14655
14656 let out =
14657 compile_and_emit("SELECT crypto::hmac(std::from_hex('68656c6c6f'), std::from_hex('6b6579'), 'sha256')");
14658 assert!(
14659 out.sql.contains("hmac("),
14660 "expected pgcrypto's hmac(), got:\n{}",
14661 out.sql
14662 );
14663 }
14664
14665 #[test]
14666 fn test_crypto_gen_salt_zero_arg_defaults_to_blowfish() {
14667 let out = compile_and_emit("SELECT crypto::gen_salt()");
14668 assert!(
14669 out.sql.contains("gen_salt('bf')"),
14670 "expected default 'bf' salt type, got:\n{}",
14671 out.sql
14672 );
14673 }
14674
14675 #[test]
14676 fn test_crypto_gen_salt_one_arg_passes_type_through() {
14677 let out = compile_and_emit("SELECT crypto::gen_salt('xdes')");
14678 assert!(
14679 out.sql.contains("gen_salt("),
14680 "expected gen_salt() call, got:\n{}",
14681 out.sql
14682 );
14683 }
14684
14685 #[test]
14686 fn test_crypto_gen_salt_iter_count_casts_to_int4() {
14687 let out = compile_and_emit("SELECT crypto::gen_salt('xdes', 5)");
14688 assert!(
14689 out.sql.contains("::int4"),
14690 "expected int8 -> int4 narrowing cast, got:\n{}",
14691 out.sql
14692 );
14693 }
14694
14695 #[test]
14696 fn test_crypto_crypt_uses_pgcrypto_crypt() {
14697 let out = compile_and_emit("SELECT crypto::crypt('hunter2', crypto::gen_salt())");
14698 assert!(
14699 out.sql.contains("crypt("),
14700 "expected pgcrypto's crypt(), got:\n{}",
14701 out.sql
14702 );
14703 }
14704
14705 #[test]
14706 fn test_postgis_cast_emits_geometry_type() {
14707 let out = compile_and_emit("SELECT <postgis::geometry>'POINT(1 2)'");
14708 assert!(
14709 out.sql.contains("::geometry"),
14710 "expected ::geometry cast, got:\n{}",
14711 out.sql
14712 );
14713 }
14714
14715 #[test]
14716 fn test_postgis_x_uses_st_x_builtin() {
14717 let out = compile_and_emit("SELECT postgis::x(<postgis::geometry>'POINT(1 2)')");
14718 assert!(out.sql.contains("st_x("), "expected st_x() call, got:\n{}", out.sql);
14719 }
14720
14721 #[test]
14722 fn test_postgis_area_geometry_and_geography_overloads() {
14723 let out = compile_and_emit("SELECT postgis::area(<postgis::geometry>'POINT(1 2)')");
14724 assert!(
14725 out.sql.contains("st_area("),
14726 "expected st_area() call, got:\n{}",
14727 out.sql
14728 );
14729
14730 let out = compile_and_emit("SELECT postgis::area(<postgis::geography>'POINT(1 2)', true)");
14731 assert!(
14732 out.sql.contains("st_area("),
14733 "expected st_area() call, got:\n{}",
14734 out.sql
14735 );
14736 }
14737
14738 #[test]
14739 fn test_postgis_setsrid_casts_int64_arg_to_int4() {
14740 let out = compile_and_emit("SELECT postgis::setsrid(<postgis::geometry>'POINT(1 2)', 4326)");
14741 assert!(
14742 out.sql.contains("st_setsrid("),
14743 "expected st_setsrid() call, got:\n{}",
14744 out.sql
14745 );
14746 assert!(
14747 out.sql.contains("::int4"),
14748 "expected int8 -> int4 narrowing cast, got:\n{}",
14749 out.sql
14750 );
14751 }
14752
14753 #[test]
14754 fn test_postgis_quantizecoordinates_default_arity_variants_compile() {
14755 let out = compile_and_emit("SELECT postgis::quantizecoordinates(<postgis::geometry>'POINT(1 2)', 5)");
14760 assert!(out.sql.contains("st_quantizecoordinates("), "got:\n{}", out.sql);
14761
14762 let out = compile_and_emit("SELECT postgis::quantizecoordinates(<postgis::geometry>'POINT(1 2)', 5, 5, 5)");
14763 assert!(out.sql.contains("st_quantizecoordinates("), "got:\n{}", out.sql);
14764 }
14765
14766 #[test]
14767 fn test_postgis_op_contains_emits_infix_operator_not_function_call() {
14768 let out = compile_and_emit(
14773 "SELECT postgis::op_contains(<postgis::geometry>'POINT(1 2)', <postgis::geometry>'POINT(3 4)')",
14774 );
14775 assert!(out.sql.contains(" ~ "), "expected infix ~ operator, got:\n{}", out.sql);
14776 assert!(
14777 !out.sql.contains("op_contains("),
14778 "must not call a literal op_contains function, got:\n{}",
14779 out.sql
14780 );
14781 }
14782
14783 #[test]
14784 fn test_postgis_op_overlaps_geometry_and_geography_overloads() {
14785 let out = compile_and_emit(
14786 "SELECT postgis::op_overlaps(<postgis::geometry>'POINT(1 2)', <postgis::geometry>'POINT(3 4)')",
14787 );
14788 assert!(
14789 out.sql.contains(" && "),
14790 "expected infix && operator, got:\n{}",
14791 out.sql
14792 );
14793
14794 let out = compile_and_emit(
14795 "SELECT postgis::op_overlaps(<postgis::geography>'POINT(1 2)', <postgis::geography>'POINT(3 4)')",
14796 );
14797 assert!(
14798 out.sql.contains(" && "),
14799 "expected infix && operator, got:\n{}",
14800 out.sql
14801 );
14802 }
14803
14804 fn make_schema_with_fns() -> SchemaDescriptor {
14807 let mut s = make_schema();
14808 s.functions = vec![
14809 FunctionDescriptor {
14810 name: "mysum".into(),
14811 module: "default".into(),
14812 params: vec![
14813 FunctionParamDescriptor {
14814 name: "a".into(),
14815 pg_type: "int8".into(),
14816 },
14817 FunctionParamDescriptor {
14818 name: "b".into(),
14819 pg_type: "int8".into(),
14820 },
14821 ],
14822 return_pg_type: "int8".into(),
14823 return_is_object: false,
14824 return_is_set: false,
14825 return_is_polymorphic: false,
14826 volatility: "immutable".into(),
14827 body: "a + b".into(),
14828 },
14829 FunctionDescriptor {
14830 name: "adults".into(),
14831 module: "default".into(),
14832 params: vec![],
14833 return_pg_type: "default::Person".into(),
14834 return_is_object: true,
14835 return_is_set: true,
14836 return_is_polymorphic: false,
14837 volatility: "stable".into(),
14838 body: "select Person filter .age > 18".into(),
14839 },
14840 FunctionDescriptor {
14847 name: "mysum".into(),
14848 module: "default".into(),
14849 params: vec![
14850 FunctionParamDescriptor {
14851 name: "a".into(),
14852 pg_type: "int8".into(),
14853 },
14854 FunctionParamDescriptor {
14855 name: "b".into(),
14856 pg_type: "int8".into(),
14857 },
14858 FunctionParamDescriptor {
14859 name: "c".into(),
14860 pg_type: "int8".into(),
14861 },
14862 ],
14863 return_pg_type: "int8".into(),
14864 return_is_object: false,
14865 return_is_set: false,
14866 return_is_polymorphic: false,
14867 volatility: "immutable".into(),
14868 body: "a + b + c".into(),
14869 },
14870 ];
14871 s
14872 }
14873
14874 #[test]
14875 fn test_user_fn_scalar_call() {
14876 let schema = make_schema_with_fns();
14877 let out = compile_and_emit_with("SELECT mysum(1, 2)", &schema);
14878 assert!(out.sql.contains("\"public\".\"mysum\""), "got:\n{}", out.sql);
14879 }
14880
14881 #[test]
14882 fn test_user_fn_overload_resolved_by_argument_count() {
14883 let schema = make_schema_with_fns();
14888 let out = compile_and_emit_with("SELECT mysum(1, 2, 3)", &schema);
14889 assert!(
14890 out.sql
14891 .contains("\"public\".\"mysum\"((1)::int8, (2)::int8, (3)::int8)"),
14892 "got:\n{}",
14893 out.sql,
14894 );
14895
14896 let out = compile_and_emit_with("SELECT mysum(1, 2)", &schema);
14898 assert!(
14899 out.sql.contains("\"public\".\"mysum\"((1)::int8, (2)::int8)"),
14900 "got:\n{}",
14901 out.sql,
14902 );
14903 }
14904
14905 #[test]
14906 fn test_user_fn_object_select_no_shape() {
14907 let schema = make_schema_with_fns();
14908 let out = compile_and_emit_with("SELECT adults()", &schema);
14909 assert!(out.sql.contains("\"public\".\"adults\"()"), "got:\n{}", out.sql);
14910 assert!(out.sql.contains("FROM"), "got:\n{}", out.sql);
14911 }
14912
14913 #[test]
14914 fn test_user_fn_object_select_with_shape() {
14915 let schema = make_schema_with_fns();
14916 let out = compile_and_emit_with("SELECT adults() { name }", &schema);
14917 assert!(out.sql.contains("\"public\".\"adults\"()"), "got:\n{}", out.sql);
14918 assert!(out.sql.contains("\"name\""), "got:\n{}", out.sql);
14919 }
14920
14921 #[test]
14922 fn test_object_fn_projected_to_a_property_inside_an_expression() {
14923 let schema = make_schema_with_fns();
14924 let out = compile_and_emit_with("SELECT Person { n := adults().name }", &schema);
14925 assert!(out.sql.contains("FROM \"public\".\"adults\"() AS"), "got:\n{}", out.sql);
14926 assert!(out.sql.contains("\".\"name\"\n"), "got:\n{}", out.sql);
14927 }
14928
14929 #[test]
14930 fn test_object_fn_sub_select_carries_its_modifiers() {
14931 let schema = make_schema_with_fns();
14932 let out = compile_and_emit_with(
14933 "SELECT Person { n := (select adults() filter .age > 21 limit 1).name }",
14934 &schema,
14935 );
14936 assert!(out.sql.contains("FROM \"public\".\"adults\"() AS"), "got:\n{}", out.sql);
14937 assert!(out.sql.contains("\"age\" > 21"), "got:\n{}", out.sql);
14938 assert!(out.sql.contains("LIMIT 1"), "got:\n{}", out.sql);
14939 }
14940
14941 #[test]
14942 fn test_object_fn_without_a_property_still_explains_the_restriction() {
14943 let schema = make_schema_with_fns();
14944 let ast = parse::parse("SELECT Person { n := adults() + 1 }").unwrap();
14945 let err = match ir::compile(&ast, &schema) {
14946 Ok(_) => panic!("expected a compile error"),
14947 Err(e) => format!("{e}"),
14948 };
14949 assert!(err.contains("returns objects"), "{err}");
14950 }
14951
14952 #[test]
14953 fn test_object_fn_projected_to_an_unknown_property_is_rejected() {
14954 let schema = make_schema_with_fns();
14955 let ast = parse::parse("SELECT Person { n := adults().nope }").unwrap();
14956 let err = match ir::compile(&ast, &schema) {
14957 Ok(_) => panic!("expected a compile error"),
14958 Err(e) => format!("{e}"),
14959 };
14960 assert!(err.contains("nope"), "{err}");
14961 }
14962
14963 #[test]
14964 fn test_user_fn_in_cte_exposes_raw_columns() {
14965 let schema = make_schema_with_fns();
14968 let out = compile_and_emit_with("WITH persons := adults() SELECT persons FILTER .age > 25", &schema);
14969 assert!(
14970 out.sql.contains("SELECT * FROM \"public\".\"adults\"()"),
14971 "CTE source must be SELECT * FROM fn(), got:\n{}",
14972 out.sql,
14973 );
14974 assert!(
14975 out.sql.contains("\"age\""),
14976 "outer filter must reference raw column, got:\n{}",
14977 out.sql
14978 );
14979 }
14980
14981 fn make_schema_with_vector() -> SchemaDescriptor {
14984 use crate::schema::VectorIndexDescriptor;
14985 let mut s = make_schema();
14986 if let Some(td) = s.types.iter_mut().find(|t| t.name == "Person") {
14987 td.vector_indexes.push(VectorIndexDescriptor {
14988 index_name: None,
14989 pointers: vec!["name".into()],
14990 model: "test-embed".into(),
14991 metric: "cosine".into(),
14992 dimensions: 4,
14993 });
14994 }
14995 s
14996 }
14997
14998 #[test]
14999 fn test_vector_search_bare_type_name() {
15000 let schema = make_schema_with_vector();
15001 let out = compile_and_emit_with(
15002 "WITH search := vector::search(Person, <pgvector::vector>[1.0, 2.0, 3.0, 4.0]) \
15003 SELECT search { object { name }, distance }",
15004 &schema,
15005 );
15006 assert!(
15007 out.sql.contains("\"Person\""),
15008 "expected Person table, got:\n{}",
15009 out.sql
15010 );
15011 assert!(out.sql.contains("<=>"), "expected cosine operator, got:\n{}", out.sql);
15012 }
15013
15014 #[test]
15015 fn test_vector_search_qualified_type_name() {
15016 let schema = make_schema_with_vector();
15017 let out = compile_and_emit_with(
15018 "WITH search := vector::search(default::Person, <pgvector::vector>[1.0, 2.0, 3.0, 4.0]) \
15019 SELECT search { object { name }, distance }",
15020 &schema,
15021 );
15022 assert!(
15023 out.sql.contains("\"Person\""),
15024 "expected Person table, got:\n{}",
15025 out.sql
15026 );
15027 assert!(out.sql.contains("<=>"), "expected cosine operator, got:\n{}", out.sql);
15028 }
15029
15030 #[test]
15031 fn test_vector_search_subquery_filter_included_in_where() {
15032 let schema = make_schema_with_vector();
15033 let out = compile_and_emit_with(
15034 "WITH search := vector::search((select Person filter .name = 'Alice'), <pgvector::vector>[1.0, 2.0, 3.0, 4.0]) \
15035 SELECT search { object { name }, distance }",
15036 &schema,
15037 );
15038 assert!(
15039 out.sql.contains("\"Person\""),
15040 "expected Person table, got:\n{}",
15041 out.sql
15042 );
15043 assert!(out.sql.contains("\"name\""), "expected name filter, got:\n{}", out.sql);
15044 assert!(out.sql.contains("Alice"), "expected filter value, got:\n{}", out.sql);
15045 assert!(out.sql.contains("<=>"), "expected cosine operator, got:\n{}", out.sql);
15046 }
15047
15048 #[test]
15049 fn test_vector_search_subquery_filter_combined_with_outer_property_filter() {
15050 let schema = make_schema_with_vector();
15051 let out = compile_and_emit_with(
15052 "WITH search := vector::search((select Person filter .age > 18), <pgvector::vector>[1.0, 2.0, 3.0, 4.0]) \
15053 SELECT search { object { name }, distance }",
15054 &schema,
15055 );
15056 assert!(
15057 out.sql.contains("\"age\""),
15058 "expected age pre-filter, got:\n{}",
15059 out.sql
15060 );
15061 assert!(out.sql.contains("18"), "expected filter value 18, got:\n{}", out.sql);
15062 assert!(out.sql.contains("<=>"), "expected cosine operator, got:\n{}", out.sql);
15063 }
15064
15065 #[test]
15066 fn test_vector_search_text_overload_with_subquery_filter() {
15067 let schema = make_schema_with_vector();
15068 let out = compile_and_emit_with(
15069 "WITH search := vector::search((select Person filter .name = 'Alice'), query := $q) \
15070 SELECT search { object { name }, distance }",
15071 &schema,
15072 );
15073 assert!(
15074 out.sql.contains("\"Person\""),
15075 "expected Person table, got:\n{}",
15076 out.sql
15077 );
15078 assert!(
15079 out.sql.contains("Alice"),
15080 "expected pre-filter value, got:\n{}",
15081 out.sql
15082 );
15083 assert!(
15084 out.sql.contains("float8[]"),
15085 "expected float8[] cast for deferred vec param, got:\n{}",
15086 out.sql
15087 );
15088 assert!(out.sql.contains("<=>"), "expected cosine operator, got:\n{}", out.sql);
15089 }
15090
15091 #[test]
15092 fn test_count_type_ref_compiles_to_agg_over_query() {
15093 let out = compile_and_emit("SELECT count(Person)");
15094 assert!(out.sql.contains("count(*)"), "expected count(*), got:\n{}", out.sql);
15095 assert!(
15096 out.sql.contains("\"Person\""),
15097 "expected Person table, got:\n{}",
15098 out.sql
15099 );
15100 }
15101
15102 #[test]
15103 fn test_count_qualified_type_ref_compiles_to_agg_over_query() {
15104 let out = compile_and_emit("SELECT count(default::Person)");
15105 assert!(out.sql.contains("count(*)"), "expected count(*), got:\n{}", out.sql);
15106 assert!(
15107 out.sql.contains("\"Person\""),
15108 "expected Person table, got:\n{}",
15109 out.sql
15110 );
15111 }
15112
15113 #[test]
15114 fn test_count_subquery_compiles_to_agg_over_query() {
15115 let out = compile_and_emit("SELECT count((select Person))");
15116 assert!(out.sql.contains("count(*)"), "expected count(*), got:\n{}", out.sql);
15117 assert!(
15118 out.sql.contains("\"Person\""),
15119 "expected Person table, got:\n{}",
15120 out.sql
15121 );
15122 }
15123
15124 #[test]
15125 fn test_count_subquery_with_filter() {
15126 let out = compile_and_emit("SELECT count((select Person filter .name = 'Alice'))");
15127 assert!(out.sql.contains("count(*)"), "expected count(*), got:\n{}", out.sql);
15128 assert!(
15129 out.sql.contains("\"name\""),
15130 "expected filter on name, got:\n{}",
15131 out.sql
15132 );
15133 }
15134
15135 #[test]
15136 fn test_positional_param_compiles_to_dollar_n() {
15137 let out = compile_and_emit("SELECT Person FILTER .name = $0");
15138 assert!(out.sql.contains("$1"), "expected $1 placeholder, got:\n{}", out.sql);
15139 }
15140
15141 #[test]
15142 fn test_multiple_positional_params_compile_in_order() {
15143 let out = compile_and_emit("SELECT Person FILTER .name = $0 AND .age > $1");
15144 assert!(out.sql.contains("$1"), "expected $1, got:\n{}", out.sql);
15145 assert!(out.sql.contains("$2"), "expected $2, got:\n{}", out.sql);
15146 }
15147
15148 #[test]
15149 fn test_repeated_positional_param_reuses_slot() {
15150 let out = compile_and_emit("SELECT Person FILTER .name = $0 OR .name = $0");
15151 assert_eq!(
15152 out.sql.matches("$1").count(),
15153 2,
15154 "both uses must reference $1, got:\n{}",
15155 out.sql
15156 );
15157 }
15158
15159 #[test]
15160 fn test_cast_to_nonexistent_type_names_full_type() {
15161 let schema = make_schema();
15162 let ast = parse::parse("SELECT Person FILTER .name = <default::Ghost>$name").unwrap();
15163 match ir::compile(&ast, &schema) {
15164 Ok(_) => panic!("expected compile error for unknown type"),
15165 Err(e) => {
15166 let msg = e.to_string();
15167 assert!(
15168 msg.contains("unknown type 'default::Ghost'"),
15169 "expected full type name in error, got: {msg}",
15170 );
15171 }
15172 }
15173 }
15174
15175 #[test]
15176 fn test_cast_to_nonexistent_unqualified_type_names_type() {
15177 let schema = make_schema();
15178 let ast = parse::parse("SELECT Person FILTER .name = <Ghost>$name").unwrap();
15179 match ir::compile(&ast, &schema) {
15180 Ok(_) => panic!("expected compile error for unknown type"),
15181 Err(e) => {
15182 let msg = e.to_string();
15183 assert!(
15184 msg.contains("unknown type 'Ghost'"),
15185 "expected type name in error, got: {msg}",
15186 );
15187 }
15188 }
15189 }
15190
15191 #[test]
15192 fn test_top_level_cast_to_registered_scalar_uses_its_own_domain() {
15193 use crate::schema::ScalarDescriptor;
15199 let mut schema = make_schema();
15200 schema.scalars.push(ScalarDescriptor {
15201 name: "Email".into(),
15202 module: "default".into(),
15203 base: "Str".into(),
15204 pg_type: "text".into(),
15205 check_constraints: vec!["value ~ '^[^@]+@[^@]+\\.[^@]+$'".into()],
15206 is_sequence: false,
15207 });
15208 let out = compile_and_emit_with("SELECT <default::Email>'test@test.de'", &schema);
15209 assert!(
15210 out.sql.contains("\"public\".\"Email\""),
15211 "expected a cast to the scalar's own domain, got:\n{}",
15212 out.sql
15213 );
15214 assert!(
15215 !out.sql.to_lowercase().contains("\"person\""),
15216 "must not be misrouted to an object-type lookup, got:\n{}",
15217 out.sql
15218 );
15219 }
15220
15221 #[test]
15222 fn test_top_level_cast_to_unknown_type_names_full_type() {
15223 let schema = make_schema();
15228 let ast = parse::parse("SELECT <default::Ghost>$name").unwrap();
15229 match ir::compile(&ast, &schema) {
15230 Ok(_) => panic!("expected compile error for unknown type"),
15231 Err(e) => {
15232 let msg = e.to_string();
15233 assert!(
15234 msg.contains("unknown type 'default::Ghost'"),
15235 "expected full type name in error, got: {msg}",
15236 );
15237 }
15238 }
15239 }
15240
15241 #[test]
15242 fn test_structural_tuple_cast_unnamed_resolves_to_jsonb() {
15243 let out = compile_and_emit("SELECT <tuple<str, bool>>$p");
15244 assert!(out.sql.contains("($1)::jsonb"), "got:\n{}", out.sql);
15245 }
15246
15247 #[test]
15248 fn test_jsonb_to_uuid_cast_extracts_via_text() {
15249 let out = compile_and_emit("SELECT <uuid>(<json>$p)");
15251 assert!(out.sql.contains("#>> '{}'"), "got:\n{}", out.sql);
15252 assert!(out.sql.contains("::uuid"), "got:\n{}", out.sql);
15253 }
15254
15255 #[test]
15256 fn test_jsonb_to_datetime_cast_extracts_via_text() {
15257 let out = compile_and_emit("SELECT <datetime>(<json>$p)");
15258 assert!(out.sql.contains("#>> '{}'"), "got:\n{}", out.sql);
15259 assert!(out.sql.contains("::timestamptz"), "got:\n{}", out.sql);
15260 }
15261
15262 #[test]
15263 fn test_jsonb_to_duration_cast_extracts_via_text() {
15264 let out = compile_and_emit("SELECT <duration>(<json>$p)");
15265 assert!(out.sql.contains("#>> '{}'"), "got:\n{}", out.sql);
15266 assert!(out.sql.contains("::interval"), "got:\n{}", out.sql);
15267 }
15268
15269 #[test]
15270 fn test_jsonb_to_array_cast_unpacks_each_element() {
15271 let out = compile_and_emit("SELECT <array<int64>>(<json>$p)");
15272 assert!(out.sql.contains("jsonb_array_elements("), "got:\n{}", out.sql);
15273 assert!(out.sql.contains("#>> '{}'"), "got:\n{}", out.sql);
15274 assert!(out.sql.contains("::int8"), "got:\n{}", out.sql);
15275 }
15276
15277 #[test]
15278 fn test_non_jsonb_cast_is_unaffected_by_jsonb_extraction() {
15279 let out = compile_and_emit("SELECT <uuid>$p");
15282 assert!(!out.sql.contains("#>>"), "got:\n{}", out.sql);
15283 assert!(out.sql.contains("::uuid"), "got:\n{}", out.sql);
15284 }
15285
15286 #[test]
15287 fn test_datetime_plus_duration_type_checks() {
15288 let out = compile_and_emit("SELECT <datetime>$p + <duration>$d");
15293 assert!(out.sql.contains(" + "), "got:\n{}", out.sql);
15294 }
15295
15296 #[test]
15297 fn test_duration_plus_datetime_type_checks_either_order() {
15298 let out = compile_and_emit("SELECT <duration>$d + <datetime>$p");
15299 assert!(out.sql.contains(" + "), "got:\n{}", out.sql);
15300 }
15301
15302 #[test]
15303 fn test_local_date_plus_duration_type_checks() {
15304 let out = compile_and_emit("SELECT <cal::local_date>$p + <duration>$d");
15305 assert!(out.sql.contains(" + "), "got:\n{}", out.sql);
15306 }
15307
15308 #[test]
15309 fn test_datetime_minus_duration_type_checks() {
15310 let out = compile_and_emit("SELECT <datetime>$p - <duration>$d");
15311 assert!(out.sql.contains(" - "), "got:\n{}", out.sql);
15312 }
15313
15314 #[test]
15315 fn test_local_time_plus_duration_type_checks() {
15316 let out = compile_and_emit("SELECT <cal::local_time>$p + <duration>$d");
15317 assert!(out.sql.contains(" + "), "got:\n{}", out.sql);
15318 }
15319
15320 #[test]
15321 fn test_datetime_plus_duration_only_allowed_for_add_and_sub() {
15322 let schema = make_schema();
15325 let ast = parse::parse("SELECT <datetime>$p = <duration>$d").unwrap();
15326 match ir::compile(&ast, &schema) {
15327 Err(err) => assert!(format!("{err}").contains("cannot be applied"), "got: {err}"),
15328 Ok(_) => panic!("expected a compile error"),
15329 }
15330 }
15331
15332 #[test]
15333 fn test_int_and_float_mix_is_allowed() {
15334 let out = compile_and_emit("SELECT <int16>1 + <float32>2.0");
15338 assert!(out.sql.contains(" + "), "got:\n{}", out.sql);
15339 }
15340
15341 #[test]
15342 fn test_int_and_decimal_mix_is_allowed() {
15343 let out = compile_and_emit("SELECT <int64>1 + <decimal>2.0");
15347 assert!(out.sql.contains(" + "), "got:\n{}", out.sql);
15348 }
15349
15350 #[test]
15351 fn test_float_and_decimal_mix_still_rejected() {
15352 let schema = make_schema();
15356 let ast = parse::parse("SELECT <decimal>$p + <float64>$n").unwrap();
15357 match ir::compile(&ast, &schema) {
15358 Err(err) => assert!(format!("{err}").contains("cannot be applied"), "got: {err}"),
15359 Ok(_) => panic!("expected a compile error"),
15360 }
15361 }
15362
15363 #[test]
15364 fn test_unknown_pointer_suggests_a_close_match() {
15365 let schema = make_schema();
15369 let ast = parse::parse("SELECT Person { nam }").unwrap();
15370 match ir::compile(&ast, &schema) {
15371 Err(err) => assert_eq!(
15372 format!("{err}"),
15373 "object type 'default::Person' has no link or property 'nam'. Did you mean 'name'?"
15374 ),
15375 Ok(_) => panic!("expected a compile error"),
15376 }
15377 }
15378
15379 #[test]
15380 fn test_unrelated_unknown_pointer_gets_no_suggestion() {
15381 let schema = make_schema();
15384 let ast = parse::parse("SELECT Person { xyzxyzxyz }").unwrap();
15385 match ir::compile(&ast, &schema) {
15386 Err(err) => {
15387 let msg = format!("{err}");
15388 assert!(msg.contains("has no link or property 'xyzxyzxyz'"), "got: {msg}");
15389 assert!(!msg.contains("Did you mean"), "got: {msg}");
15390 }
15391 Ok(_) => panic!("expected a compile error"),
15392 }
15393 }
15394
15395 #[test]
15396 fn test_multilink_sourced_backlink_exists_filter_compiles() {
15397 let out = compile_and_emit("SELECT Post filter exists .<posts[is Person]");
15407 assert!(out.sql.contains("EXISTS"), "got:\n{}", out.sql);
15408 }
15409
15410 #[test]
15411 fn test_backlink_as_computed_shape_pointer_compiles() {
15412 let out = compile_and_emit("SELECT Post { title, authors := .<posts[is Person] { name } }");
15425 assert!(out.sql.contains("array_agg(ROW("), "got:\n{}", out.sql);
15426 }
15427
15428 fn schema_with_exclusive_posts() -> SchemaDescriptor {
15429 let mut schema = make_schema();
15430 for td in schema.types.iter_mut().filter(|td| td.name == "Person") {
15431 for ml in td.multilinks.iter_mut().filter(|ml| ml.name == "posts") {
15432 ml.is_exclusive = true;
15433 }
15434 }
15435 schema
15436 }
15437
15438 #[test]
15439 fn test_backlink_through_an_exclusive_multilink_is_single() {
15440 let out = compile_and_emit_with(
15441 "SELECT Post { title, author := .<posts[is Person] { name } }",
15442 &schema_with_exclusive_posts(),
15443 );
15444 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
15445 panic!("expected an object root, got {:?}", out.shape.root)
15446 };
15447 assert!(
15448 matches!(pointers.last(), Some(ShapeNode::Object { name, .. }) if name == "author"),
15449 "got {pointers:?}"
15450 );
15451 }
15452
15453 #[test]
15454 fn test_a_path_through_an_exclusive_backlink_is_single() {
15455 let out = compile_and_emit_with(
15456 "SELECT Post { title, author_name := .<posts[is Person].name }",
15457 &schema_with_exclusive_posts(),
15458 );
15459 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
15460 panic!("expected an object root, got {:?}", out.shape.root)
15461 };
15462 assert!(
15463 matches!(pointers.last(), Some(ShapeNode::Scalar { name, .. }) if name == "author_name"),
15464 "got {pointers:?}"
15465 );
15466 assert!(!out.sql.contains("ARRAY("), "got:\n{}", out.sql);
15467 }
15468
15469 #[test]
15470 fn test_structural_tuple_cast_named_resolves_to_jsonb() {
15471 let out = compile_and_emit("SELECT <tuple<x: float64, y: float64>>$p");
15472 assert!(out.sql.contains("($1)::jsonb"), "got:\n{}", out.sql);
15473 }
15474
15475 #[test]
15476 fn test_structural_tuple_cast_nested_resolves_to_jsonb() {
15477 let out = compile_and_emit("SELECT <tuple<point: tuple<x: float64, y: float64>, label: str>>$p");
15478 assert!(out.sql.contains("($1)::jsonb"), "got:\n{}", out.sql);
15479 }
15480
15481 #[test]
15482 fn test_a_tuple_parameter_binds_as_the_shape_a_property_declares() {
15483 let schema = make_schema_with_a_structural_tuple_property();
15486 let name =
15487 crate::schema::tuple_type::structural_name(schema.types[0].properties[0].tuple_members.as_ref().unwrap());
15488 let out = compile_and_emit_with("SELECT <tuple<street: str, zip: str>>$p", &schema);
15489 assert!(
15490 out.sql.contains(&format!("($1)::\"public\".\"{name}\"")),
15491 "got:\n{}",
15492 out.sql
15493 );
15494 }
15495
15496 #[test]
15497 fn test_a_tuple_parameter_no_one_declares_keeps_the_json_it_travelled_as() {
15498 let schema = make_schema_with_a_structural_tuple_property();
15501 let out = compile_and_emit_with("SELECT <tuple<other: str, n: int64>>$p", &schema);
15502 assert!(out.sql.contains("($1)::jsonb"), "got:\n{}", out.sql);
15503 }
15504
15505 #[test]
15506 fn test_a_tuple_parameter_json_cannot_carry_is_refused() {
15507 let schema = make_schema_with_a_structural_tuple_property();
15510 let err = compile_err_with("SELECT <tuple<raw: bytes, n: int64>>$p", &schema);
15511 assert!(err.contains("std::bytes"), "got: {err}");
15512 assert!(err.contains("nothing declares it"), "got: {err}");
15513 assert!(err.contains("named_tuple"), "the error has to say what to do: {err}");
15514 }
15515
15516 #[test]
15517 fn test_nominal_named_tuple_cast_resolves_to_the_declared_composite() {
15518 let mut schema = make_schema();
15522 schema.named_tuples.push(NamedTupleDescriptor {
15523 name: "Point".into(),
15524 module: "default".into(),
15525 members: vec![],
15526 });
15527 let out = compile_and_emit_with("SELECT <default::Point>$p", &schema);
15528 assert!(out.sql.contains("($1)::\"public\".\"Point_t\""), "got:\n{}", out.sql);
15529 assert!(!out.sql.contains("jsonb"), "got:\n{}", out.sql);
15530 }
15531
15532 #[test]
15533 fn test_array_literal_cast_resolves_to_native_pg_array_not_jsonb() {
15534 let out = compile_and_emit("SELECT <array<str>>['foo', 'bar']");
15538 assert!(
15539 out.sql.contains("::text[]") || out.sql.contains("ARRAY["),
15540 "got:\n{}",
15541 out.sql
15542 );
15543 assert!(
15544 !out.sql.contains("jsonb"),
15545 "arrays must not use jsonb, got:\n{}",
15546 out.sql
15547 );
15548 }
15549
15550 #[test]
15551 fn test_array_literal_cast_applies_per_element_cast() {
15552 let out = compile_and_emit("SELECT <array<int64>>['1', '2']");
15556 assert!(out.sql.contains("ARRAY[('1')::int8, ('2')::int8]"), "got:\n{}", out.sql);
15557 }
15558
15559 #[test]
15560 fn test_array_param_cast_uses_direct_suffix_cast() {
15561 let out = compile_and_emit("SELECT <array<int64>>$p");
15562 assert!(out.sql.contains("::int8[]"), "got:\n{}", out.sql);
15563 }
15564
15565 #[test]
15566 fn test_array_of_named_tuple_element_casts_to_jsonb_array() {
15567 let out = compile_and_emit("SELECT <array<tuple<x: float64, y: float64>>>$p");
15571 assert!(out.sql.contains("::jsonb[]"), "got:\n{}", out.sql);
15572 }
15573
15574 #[test]
15575 fn test_contains_on_array_literal_cast_uses_array_overload_not_strpos() {
15576 let out = compile_and_emit("SELECT contains(<array<str>>[1, 2], '2')");
15582 assert!(out.sql.contains("@> ARRAY["), "got:\n{}", out.sql);
15583 assert!(
15584 !out.sql.contains("strpos"),
15585 "must not fall back to the str/str overload, got:\n{}",
15586 out.sql
15587 );
15588 }
15589
15590 #[test]
15591 fn test_nested_array_type_rejected_at_parse_time() {
15592 match parse::parse("SELECT <array<array<str>>>$p") {
15593 Ok(_) => panic!("expected parse error for nested array type"),
15594 Err(e) => assert!(e.to_string().contains("nested arrays are not supported"), "got: {}", e),
15595 }
15596 }
15597
15598 #[test]
15599 fn test_array_cast_in_computed_shape_field_schema_bound_context() {
15600 let out = compile_and_emit("SELECT Person { name, tags := <array<str>>['a', 'b'] }");
15603 assert!(out.sql.contains("ARRAY[('a')::text, ('b')::text]"), "got:\n{}", out.sql);
15604 }
15605
15606 #[test]
15607 fn test_nominal_named_tuple_cast_shape_carries_real_members() {
15608 use crate::schema::{TupleMemberDescriptor, TupleMemberKind};
15609 let mut schema = make_schema();
15610 schema.named_tuples.push(NamedTupleDescriptor {
15611 name: "Point".into(),
15612 module: "default".into(),
15613 members: vec![
15614 TupleMemberDescriptor {
15615 name: Some("x".into()),
15616 kind: TupleMemberKind::Scalar {
15617 pg_type: "float8".into(),
15618 },
15619 },
15620 TupleMemberDescriptor {
15621 name: Some("y".into()),
15622 kind: TupleMemberKind::Scalar {
15623 pg_type: "float8".into(),
15624 },
15625 },
15626 ],
15627 });
15628 let out = compile_and_emit_with("SELECT <default::Point>$p", &schema);
15629 match &out.shape.root {
15630 crate::query::ShapeNode::Tuple { type_name, names, .. } => {
15631 assert_eq!(type_name.as_deref(), Some("default::Point"));
15632 assert_eq!(names.as_ref().unwrap(), &vec!["x".to_string(), "y".to_string()]);
15633 }
15634 other => panic!("expected ShapeNode::Tuple, got {other:?}"),
15635 }
15636 }
15637
15638 #[test]
15639 fn test_structural_tuple_property_read_shape_carries_real_members() {
15640 use crate::schema::{TupleMemberDescriptor, TupleMemberKind};
15641 let schema = SchemaDescriptor {
15642 types: vec![TypeDescriptor {
15643 name: "Person".into(),
15644 module: "default".into(),
15645 table: "Person".into(),
15646 abstract_: false,
15647 materialized: false,
15648 description: None,
15649 parents: vec![],
15650 interfaces: vec![],
15651 bases: vec![],
15652 properties: vec![PropertyDescriptor {
15653 name: "address".into(),
15654 pg_type: "jsonb".into(),
15655 nullable: true,
15656 default_sql: None,
15657 default_pyql: None,
15658 description: None,
15659 check_constraints: vec![],
15660 is_exclusive: false,
15661 is_pk: false,
15662 is_readonly: false,
15663 rewrites: vec![],
15664 tuple_members: Some(vec![
15665 TupleMemberDescriptor {
15666 name: Some("street".into()),
15667 kind: TupleMemberKind::Scalar { pg_type: "text".into() },
15668 },
15669 TupleMemberDescriptor {
15670 name: Some("zip".into()),
15671 kind: TupleMemberKind::Scalar { pg_type: "text".into() },
15672 },
15673 ]),
15674 column_type: None,
15675 }],
15676 links: vec![],
15677 multilinks: vec![],
15678 computed: vec![],
15679 constraints: vec![],
15680 indexes: vec![],
15681 partition: None,
15682 vector_indexes: vec![],
15683 search_indexes: vec![],
15684 triggers: vec![],
15685 junction: false,
15686 signals: vec![],
15687 }],
15688 scalars: vec![],
15689 enums: vec![],
15690 named_tuples: vec![],
15691 globals: vec![],
15692 functions: vec![],
15693 aliases: vec![],
15694 channels: vec![],
15695 ..Default::default()
15696 };
15697 let out = compile_and_emit_with("SELECT Person { address }", &schema);
15698 assert!(!out.sql.contains("::jsonb"), "got:\n{}", out.sql);
15701 match &out.shape.root {
15702 crate::query::ShapeNode::Object { pointers, .. } => {
15703 let address = pointers
15704 .iter()
15705 .find(|p| matches!(p, crate::query::ShapeNode::Tuple { name, .. } if name == "address"))
15706 .expect("expected address pointer in shape");
15707 match address {
15708 crate::query::ShapeNode::Tuple {
15709 type_name,
15710 names,
15711 elements,
15712 ..
15713 } => {
15714 assert_eq!(*type_name, None);
15715 let names = names.as_ref().expect("a named tuple's members name themselves");
15716 assert_eq!(names, &vec!["street".to_string(), "zip".to_string()]);
15717 assert_eq!(elements.len(), 2);
15720 assert!(
15721 matches!(&elements[0], crate::query::ShapeNode::Scalar { position: 0, .. })
15722 && matches!(&elements[1], crate::query::ShapeNode::Scalar { position: 1, .. }),
15723 "got {elements:?}"
15724 );
15725 }
15726 other => panic!("expected Tuple, got {other:?}"),
15727 }
15728 }
15729 other => panic!("expected ShapeNode::Object, got {other:?}"),
15730 }
15731 }
15732
15733 #[test]
15734 fn test_bare_path_select_structural_tuple_property_shape_carries_real_members() {
15735 use crate::schema::{TupleMemberDescriptor, TupleMemberKind};
15745 let schema = SchemaDescriptor {
15746 types: vec![TypeDescriptor {
15747 name: "Person".into(),
15748 module: "default".into(),
15749 table: "Person".into(),
15750 abstract_: false,
15751 materialized: false,
15752 description: None,
15753 parents: vec![],
15754 interfaces: vec![],
15755 bases: vec![],
15756 properties: vec![PropertyDescriptor {
15757 name: "address".into(),
15758 pg_type: "jsonb".into(),
15759 nullable: true,
15760 default_sql: None,
15761 default_pyql: None,
15762 description: None,
15763 check_constraints: vec![],
15764 is_exclusive: false,
15765 is_pk: false,
15766 is_readonly: false,
15767 rewrites: vec![],
15768 tuple_members: Some(vec![
15769 TupleMemberDescriptor {
15770 name: Some("street".into()),
15771 kind: TupleMemberKind::Scalar { pg_type: "text".into() },
15772 },
15773 TupleMemberDescriptor {
15774 name: Some("zip".into()),
15775 kind: TupleMemberKind::Scalar { pg_type: "text".into() },
15776 },
15777 ]),
15778 column_type: None,
15779 }],
15780 links: vec![],
15781 multilinks: vec![],
15782 computed: vec![],
15783 constraints: vec![],
15784 indexes: vec![],
15785 partition: None,
15786 vector_indexes: vec![],
15787 search_indexes: vec![],
15788 triggers: vec![],
15789 junction: false,
15790 signals: vec![],
15791 }],
15792 scalars: vec![],
15793 enums: vec![],
15794 named_tuples: vec![],
15795 globals: vec![],
15796 functions: vec![],
15797 aliases: vec![],
15798 channels: vec![],
15799 ..Default::default()
15800 };
15801 let out = compile_and_emit_with("SELECT Person.address", &schema);
15802 match &out.shape.root {
15803 crate::query::ShapeNode::Tuple { type_name, names, .. } => {
15804 assert_eq!(*type_name, None);
15805 let names = names.as_ref().expect("a named tuple's members name themselves");
15806 assert_eq!(names, &vec!["street".to_string(), "zip".to_string()]);
15807 }
15808 other => panic!("expected ShapeNode::Tuple, got {other:?}"),
15809 }
15810 }
15811
15812 #[test]
15813 fn test_path_traversal_into_structural_tuple_property() {
15814 use crate::schema::{TupleMemberDescriptor, TupleMemberKind};
15815 let schema = SchemaDescriptor {
15816 types: vec![TypeDescriptor {
15817 name: "Person".into(),
15818 module: "default".into(),
15819 table: "Person".into(),
15820 abstract_: false,
15821 materialized: false,
15822 description: None,
15823 parents: vec![],
15824 interfaces: vec![],
15825 bases: vec![],
15826 properties: vec![PropertyDescriptor {
15827 name: "address".into(),
15828 pg_type: "jsonb".into(),
15829 nullable: true,
15830 default_sql: None,
15831 default_pyql: None,
15832 description: None,
15833 check_constraints: vec![],
15834 is_exclusive: false,
15835 is_pk: false,
15836 is_readonly: false,
15837 rewrites: vec![],
15838 tuple_members: Some(vec![
15839 TupleMemberDescriptor {
15840 name: Some("street".into()),
15841 kind: TupleMemberKind::Scalar { pg_type: "text".into() },
15842 },
15843 TupleMemberDescriptor {
15844 name: Some("zip".into()),
15845 kind: TupleMemberKind::Scalar { pg_type: "text".into() },
15846 },
15847 ]),
15848 column_type: None,
15849 }],
15850 links: vec![],
15851 multilinks: vec![],
15852 computed: vec![],
15853 constraints: vec![],
15854 indexes: vec![],
15855 partition: None,
15856 vector_indexes: vec![],
15857 search_indexes: vec![],
15858 triggers: vec![],
15859 junction: false,
15860 signals: vec![],
15861 }],
15862 scalars: vec![],
15863 enums: vec![],
15864 named_tuples: vec![],
15865 globals: vec![],
15866 functions: vec![],
15867 aliases: vec![],
15868 channels: vec![],
15869 ..Default::default()
15870 };
15871 let out = compile_and_emit_with("SELECT default::Person.address.street", &schema);
15877 assert!(out.sql.contains(r#"("t0"."address")."street""#), "got:\n{}", out.sql);
15880 assert!(
15882 matches!(out.shape.root, crate::query::ShapeNode::Scalar { .. }),
15883 "got {:?}",
15884 out.shape.root
15885 );
15886 }
15887
15888 fn make_schema_with_a_structural_tuple_property() -> SchemaDescriptor {
15894 use crate::schema::{TupleMemberDescriptor, TupleMemberKind};
15895 let member = |name: &str| TupleMemberDescriptor {
15896 name: Some(name.to_string()),
15897 kind: TupleMemberKind::Scalar { pg_type: "text".into() },
15898 };
15899 SchemaDescriptor {
15900 types: vec![TypeDescriptor {
15901 name: "Person".into(),
15902 module: "default".into(),
15903 table: "Person".into(),
15904 abstract_: false,
15905 materialized: false,
15906 description: None,
15907 parents: vec![],
15908 interfaces: vec![],
15909 bases: vec![],
15910 properties: vec![PropertyDescriptor {
15911 name: "address".into(),
15912 pg_type: "jsonb".into(),
15913 nullable: true,
15914 default_sql: None,
15915 default_pyql: None,
15916 description: None,
15917 check_constraints: vec![],
15918 is_exclusive: false,
15919 is_pk: false,
15920 is_readonly: false,
15921 rewrites: vec![],
15922 tuple_members: Some(vec![member("street"), member("zip")]),
15923 column_type: None,
15924 }],
15925 links: vec![],
15926 multilinks: vec![],
15927 computed: vec![],
15928 constraints: vec![],
15929 indexes: vec![],
15930 partition: None,
15931 vector_indexes: vec![],
15932 search_indexes: vec![],
15933 triggers: vec![],
15934 junction: false,
15935 signals: vec![],
15936 }],
15937 scalars: vec![],
15938 enums: vec![],
15939 named_tuples: vec![],
15940 globals: vec![],
15941 functions: vec![],
15942 aliases: vec![],
15943 channels: vec![],
15944 ..Default::default()
15945 }
15946 }
15947
15948 #[test]
15949 fn test_a_tuple_literal_assigned_to_a_column_is_a_composite_row() {
15950 let schema = make_schema_with_a_structural_tuple_property();
15956 let out = compile_and_emit_with(
15957 "INSERT Person { address := (street := 'Main', zip := '1000') }",
15958 &schema,
15959 );
15960 let name =
15961 crate::schema::tuple_type::structural_name(schema.types[0].properties[0].tuple_members.as_ref().unwrap());
15962 assert!(
15963 out.sql
15964 .contains(&format!("VALUES ((ROW('Main', '1000'))::\"public\".\"{name}\")")),
15965 "got:\n{}",
15966 out.sql
15967 );
15968 assert!(
15972 !out.sql.contains("populate_tuple") && !out.sql.contains("jsonb"),
15973 "a tuple literal on its way into a column is a row: got:\n{}",
15974 out.sql
15975 );
15976 }
15977
15978 #[test]
15979 fn test_a_tuple_column_compared_to_a_literal_gets_the_columns_own_type() {
15980 let schema = make_schema_with_a_structural_tuple_property();
15984 let out = compile_and_emit_with(
15985 "SELECT Person { address } FILTER .address = (street := 'Main', zip := '1000')",
15986 &schema,
15987 );
15988 let name =
15989 crate::schema::tuple_type::structural_name(schema.types[0].properties[0].tuple_members.as_ref().unwrap());
15990 assert!(
15991 out.sql
15992 .contains(&format!("(ROW('Main', '1000'))::\"public\".\"{name}\"")),
15993 "got:\n{}",
15994 out.sql
15995 );
15996 }
15997
15998 #[test]
15999 fn test_a_tuple_column_compared_to_a_parameter_is_read_into_the_columns_type() {
16000 let schema = make_schema_with_a_structural_tuple_property();
16001 let out = compile_and_emit_with(
16002 "SELECT Person { address } FILTER .address = <tuple<street: str, zip: str>>$a",
16003 &schema,
16004 );
16005 assert!(out.sql.contains("\"_pylon\".\"populate_tuple\""), "got:\n{}", out.sql);
16006 }
16007
16008 #[test]
16009 fn test_a_tuple_property_under_another_name_keeps_its_members() {
16010 let schema = make_schema_with_a_structural_tuple_property();
16014 let out = compile_and_emit_with("SELECT Person { p := .address }", &schema);
16015 let crate::query::ShapeNode::Object { pointers, .. } = &out.shape.root else {
16016 panic!("expected ShapeNode::Object, got {:?}", out.shape.root)
16017 };
16018 let p = pointers
16019 .iter()
16020 .find(|ptr| matches!(ptr, crate::query::ShapeNode::Tuple { name, .. } if name == "p"))
16021 .unwrap_or_else(|| panic!("expected a tuple pointer named 'p', got {pointers:?}"));
16022 let crate::query::ShapeNode::Tuple { names, .. } = p else {
16023 unreachable!("matched as a tuple just above")
16024 };
16025 assert_eq!(
16026 names.as_ref().expect("a named tuple's members name themselves"),
16027 &vec!["street".to_string(), "zip".to_string()]
16028 );
16029 }
16030
16031 #[test]
16032 fn test_a_tuple_parameter_assigned_to_a_column_is_read_into_it() {
16033 let schema = make_schema_with_a_structural_tuple_property();
16037 let out = compile_and_emit_with("INSERT Person { address := <tuple<street: str, zip: str>>$a }", &schema);
16038 assert!(
16039 out.sql.contains("\"_pylon\".\"populate_tuple\"") && out.sql.contains("to_jsonb("),
16040 "got:\n{}",
16041 out.sql
16042 );
16043 }
16044
16045 #[test]
16046 fn test_an_array_of_tuples_parameter_is_read_element_wise() {
16047 let schema = make_schema_with_an_array_of_tuples_property();
16048 let out = compile_and_emit_with(
16049 "INSERT Person { addresses := <array<tuple<street: str, zip: str>>>$a }",
16050 &schema,
16051 );
16052 assert!(
16053 out.sql.contains("\"_pylon\".\"populate_tuples\"") && out.sql.contains("to_jsonb("),
16054 "got:\n{}",
16055 out.sql
16056 );
16057 }
16058
16059 fn make_schema_with_an_array_of_tuples_property() -> SchemaDescriptor {
16060 use crate::schema::{TupleMemberDescriptor, TupleMemberKind};
16061 SchemaDescriptor {
16062 types: vec![TypeDescriptor {
16063 name: "Person".into(),
16064 module: "default".into(),
16065 table: "Person".into(),
16066 abstract_: false,
16067 materialized: false,
16068 description: None,
16069 parents: vec![],
16070 interfaces: vec![],
16071 bases: vec![],
16072 properties: vec![PropertyDescriptor {
16073 name: "addresses".into(),
16074 pg_type: "jsonb[]".into(),
16075 nullable: true,
16076 default_sql: None,
16077 default_pyql: None,
16078 description: None,
16079 check_constraints: vec![],
16080 is_exclusive: false,
16081 is_pk: false,
16082 is_readonly: false,
16083 rewrites: vec![],
16084 tuple_members: Some(vec![
16085 TupleMemberDescriptor {
16086 name: Some("street".into()),
16087 kind: TupleMemberKind::Scalar { pg_type: "text".into() },
16088 },
16089 TupleMemberDescriptor {
16090 name: Some("zip".into()),
16091 kind: TupleMemberKind::Scalar { pg_type: "text".into() },
16092 },
16093 ]),
16094 column_type: None,
16095 }],
16096 links: vec![],
16097 multilinks: vec![],
16098 computed: vec![],
16099 constraints: vec![],
16100 indexes: vec![],
16101 partition: None,
16102 vector_indexes: vec![],
16103 search_indexes: vec![],
16104 triggers: vec![],
16105 junction: false,
16106 signals: vec![],
16107 }],
16108 scalars: vec![],
16109 enums: vec![],
16110 named_tuples: vec![],
16111 globals: vec![],
16112 functions: vec![],
16113 aliases: vec![],
16114 channels: vec![],
16115 ..Default::default()
16116 }
16117 }
16118
16119 #[test]
16120 fn test_array_of_tuples_property_read_shape_is_an_array_of_named_tuples() {
16121 let schema = make_schema_with_an_array_of_tuples_property();
16127 let out = compile_and_emit_with("SELECT Person { addresses }", &schema);
16128 assert!(!out.sql.contains("::jsonb"), "got:\n{}", out.sql);
16131 let crate::query::ShapeNode::Object { pointers, .. } = &out.shape.root else {
16132 panic!("expected ShapeNode::Object, got {:?}", out.shape.root)
16133 };
16134 let addresses = pointers
16135 .iter()
16136 .find(|p| matches!(p, crate::query::ShapeNode::Array { name, .. } if name == "addresses"))
16137 .expect("expected addresses pointer in shape");
16138 let crate::query::ShapeNode::Array { element, .. } = addresses else {
16139 unreachable!("matched as an array just above")
16140 };
16141 match element.as_ref() {
16142 crate::query::ShapeNode::Tuple {
16143 type_name,
16144 names,
16145 position,
16146 ..
16147 } => {
16148 assert_eq!(*type_name, None);
16149 assert_eq!(*position, 0, "an element is the whole of its own value");
16150 let names = names.as_ref().expect("a named tuple's members name themselves");
16151 assert_eq!(names, &vec!["street".to_string(), "zip".to_string()]);
16152 }
16153 other => panic!("expected Tuple element, got {other:?}"),
16154 }
16155 }
16156
16157 #[test]
16158 fn test_a_path_to_an_optional_property_drops_the_rows_without_one() {
16159 let out = compile_and_emit("SELECT Person.age");
16163 assert!(out.sql.contains(r#"("t0"."age" IS NOT NULL)"#), "got:\n{}", out.sql);
16164 }
16165
16166 #[test]
16167 fn test_a_path_to_a_required_property_needs_no_guard() {
16168 let out = compile_and_emit("SELECT Person.name");
16171 assert!(!out.sql.contains("IS NOT NULL"), "got:\n{}", out.sql);
16172 }
16173
16174 #[test]
16175 fn test_a_path_to_an_optional_property_keeps_its_own_filter_too() {
16176 let out = compile_and_emit("SELECT Person.age FILTER Person.name = $n");
16177 assert!(
16178 out.sql
16179 .contains(r#"WHERE (("t0"."name" = $1) AND ("t0"."age" IS NOT NULL))"#),
16180 "got:\n{}",
16181 out.sql
16182 );
16183 }
16184
16185 #[test]
16186 fn test_a_set_of_array_valued_properties_aggregates_as_records() {
16187 let out = compile_and_emit_with(
16192 "SELECT Company { permissions := .<company[is Person].perms }",
16193 &schema_with_array_property(),
16194 );
16195 assert!(out.sql.contains(r#"ROW("t2"."perms")"#), "got:\n{}", out.sql);
16196 let crate::query::ShapeNode::Object { pointers, .. } = &out.shape.root else {
16197 panic!("expected ShapeNode::Object, got {:?}", out.shape.root)
16198 };
16199 let permissions = pointers
16200 .iter()
16201 .find(|p| matches!(p, crate::query::ShapeNode::Array { name, .. } if name == "permissions"))
16202 .expect("expected the permissions pointer");
16203 let crate::query::ShapeNode::Array { element, .. } = permissions else {
16204 unreachable!("matched as an array just above")
16205 };
16206 match element.as_ref() {
16208 crate::query::ShapeNode::Scalar { position, .. } => assert_eq!(*position, 0),
16209 other => panic!("expected the array at position 0 of its record, got {other:?}"),
16210 }
16211 }
16212
16213 #[test]
16214 fn test_a_set_of_array_of_tuples_properties_keeps_the_member_shape() {
16215 let mut schema = make_schema_with_an_array_of_tuples_property();
16216 schema.types.push(TypeDescriptor {
16218 name: "Company".into(),
16219 module: "default".into(),
16220 table: "Company".into(),
16221 abstract_: false,
16222 materialized: false,
16223 description: None,
16224 parents: vec![],
16225 interfaces: vec![],
16226 bases: vec![],
16227 properties: vec![],
16228 links: vec![],
16229 multilinks: vec![],
16230 computed: vec![],
16231 constraints: vec![],
16232 indexes: vec![],
16233 partition: None,
16234 vector_indexes: vec![],
16235 search_indexes: vec![],
16236 triggers: vec![],
16237 junction: false,
16238 signals: vec![],
16239 });
16240 schema.types[0].links.push(crate::schema::LinkDescriptor {
16241 name: "company".into(),
16242 target: "default::Company".into(),
16243 nullable: true,
16244 description: None,
16245 default_pyql: None,
16246 is_exclusive: false,
16247 is_readonly: false,
16248 rewrites: vec![],
16249 on_delete: vec![],
16250 through: None,
16251 });
16252 let out = compile_and_emit_with(
16253 "SELECT Company { addresses := .<company[is Person].addresses }",
16254 &schema,
16255 );
16256 assert!(out.sql.contains(r#"ROW("t2"."addresses")"#), "got:\n{}", out.sql);
16257 let crate::query::ShapeNode::Object { pointers, .. } = &out.shape.root else {
16258 panic!("expected ShapeNode::Object, got {:?}", out.shape.root)
16259 };
16260 let addresses = pointers
16261 .iter()
16262 .find(|p| matches!(p, crate::query::ShapeNode::Array { name, .. } if name == "addresses"))
16263 .expect("expected the addresses pointer");
16264 let crate::query::ShapeNode::Array { element, .. } = addresses else {
16265 unreachable!("matched as an array just above")
16266 };
16267 match element.as_ref() {
16270 crate::query::ShapeNode::Array { element, position, .. } => {
16271 assert_eq!(*position, 0);
16272 assert!(
16273 matches!(element.as_ref(), crate::query::ShapeNode::Tuple { names: Some(_), .. }),
16274 "expected named-tuple elements, got {element:?}"
16275 );
16276 }
16277 other => panic!("expected an array per element, got {other:?}"),
16278 }
16279 }
16280
16281 #[test]
16282 fn test_an_optional_property_aggregated_into_an_array_drops_its_empties() {
16283 let out = compile_and_emit("SELECT Company { ages := .<company[is Person].age }");
16287 assert!(out.sql.contains(r#"("t2"."age" IS NOT NULL)"#), "got:\n{}", out.sql);
16288 }
16289
16290 #[test]
16291 fn test_bare_path_select_array_of_tuples_property_shape() {
16292 let schema = make_schema_with_an_array_of_tuples_property();
16296 let out = compile_and_emit_with("SELECT Person.addresses", &schema);
16297 assert!(out.sql.contains("ROW("), "got:\n{}", out.sql);
16298 match &out.shape.root {
16299 crate::query::ShapeNode::Array { element, position, .. } => {
16300 assert_eq!(*position, 0);
16301 match element.as_ref() {
16302 crate::query::ShapeNode::Tuple { names, .. } => {
16303 let names = names.as_ref().expect("a named tuple's members name themselves");
16304 assert_eq!(names[0], "street");
16305 }
16306 other => panic!("expected Tuple element, got {other:?}"),
16307 }
16308 }
16309 other => panic!("expected ShapeNode::Array, got {other:?}"),
16310 }
16311 }
16312
16313 #[test]
16314 fn test_a_with_bound_path_select_keeps_the_shape_the_path_itself_has() {
16315 let schema = make_schema_with_an_array_of_tuples_property();
16319 let out = compile_and_emit_with("WITH entries := Person.addresses SELECT entries", &schema);
16320 match &out.shape.root {
16321 crate::query::ShapeNode::Array { element, .. } => assert!(
16322 matches!(element.as_ref(), crate::query::ShapeNode::Tuple { names: Some(_), .. }),
16323 "expected a named-tuple element, got {element:?}"
16324 ),
16325 other => panic!("expected ShapeNode::Array, got {other:?}"),
16326 }
16327 }
16328
16329 #[test]
16330 fn test_path_traversal_into_array_of_tuples_property_rejected() {
16331 let schema = make_schema_with_an_array_of_tuples_property();
16334 let ast = parse::parse("SELECT default::Person.addresses.street").unwrap();
16335 match ir::compile(&ast, &schema) {
16336 Ok(_) => panic!("expected error for a field access on an array of tuples"),
16337 Err(e) => assert!(
16338 e.to_string()
16339 .contains("'addresses' is a scalar property, not a link — cannot traverse further"),
16340 "got: {}",
16341 e
16342 ),
16343 }
16344 }
16345
16346 #[test]
16347 fn test_a_literal_cast_to_a_declared_tuple_type_is_a_value_of_it() {
16348 let mut schema = make_schema();
16352 schema.named_tuples.push(crate::schema::NamedTupleDescriptor {
16353 name: "Money".into(),
16354 module: "default".into(),
16355 members: vec![
16356 crate::schema::TupleMemberDescriptor {
16357 name: Some("amount".into()),
16358 kind: crate::schema::TupleMemberKind::Scalar {
16359 pg_type: "numeric".into(),
16360 },
16361 },
16362 crate::schema::TupleMemberDescriptor {
16363 name: Some("currency".into()),
16364 kind: crate::schema::TupleMemberKind::Scalar { pg_type: "text".into() },
16365 },
16366 ],
16367 });
16368 let out = compile_and_emit_with("SELECT <default::Money>(amount := 1.5, currency := 'EUR')", &schema);
16369 assert!(
16370 out.sql.contains("ROW((1.5::float8), 'EUR'))::\"public\".\"Money_t\""),
16371 "the composite cast coerces each member to its attribute: got:\n{}",
16372 out.sql
16373 );
16374 assert!(!out.sql.contains("jsonb"), "got:\n{}", out.sql);
16375 match &out.shape.root {
16377 crate::query::ShapeNode::Tuple { type_name, names, .. } => {
16378 assert_eq!(type_name.as_deref(), Some("default::Money"));
16379 assert_eq!(
16380 names.as_ref().unwrap(),
16381 &vec!["amount".to_string(), "currency".to_string()]
16382 );
16383 }
16384 other => panic!("expected ShapeNode::Tuple, got {other:?}"),
16385 }
16386 }
16387
16388 #[test]
16389 fn test_an_array_literal_of_tuples_is_described_by_its_elements() {
16390 let out = compile_and_emit("SELECT [(a := 1), (a := 2)]");
16394 let crate::query::ShapeNode::Array { element, position, .. } = &out.shape.root else {
16395 panic!("expected ShapeNode::Array, got {:?}", out.shape.root)
16396 };
16397 assert_eq!(*position, 0);
16398 match element.as_ref() {
16399 crate::query::ShapeNode::Tuple { names, position, .. } => {
16400 assert_eq!(*position, 0, "an element is the whole of its own value");
16401 assert_eq!(names.as_ref().unwrap(), &vec!["a".to_string()]);
16402 }
16403 other => panic!("expected a tuple element, got {other:?}"),
16404 }
16405 let scalars = compile_and_emit("SELECT [1, 2]");
16407 assert!(
16408 matches!(scalars.shape.root, crate::query::ShapeNode::RawScalar),
16409 "got {:?}",
16410 scalars.shape.root
16411 );
16412 }
16413
16414 #[test]
16415 fn test_a_tuple_on_its_way_into_json_is_built_as_json() {
16416 let named = compile_and_emit("SELECT <json>(a := 1, b := 'x')");
16421 assert!(
16422 named.sql.contains("jsonb_build_object('a', 1, 'b', 'x')"),
16423 "got:\n{}",
16424 named.sql
16425 );
16426 let positional = compile_and_emit("SELECT <json>(1, 'x')");
16429 assert!(
16430 positional.sql.contains("jsonb_build_array(1, 'x')"),
16431 "got:\n{}",
16432 positional.sql
16433 );
16434 let nested = compile_and_emit("SELECT <json>(deep := (a := 1), b := 'x')");
16437 assert!(
16438 nested.sql.contains("jsonb_build_object('a', 1)"),
16439 "got:\n{}",
16440 nested.sql
16441 );
16442 assert!(!nested.sql.contains("ROW("), "got:\n{}", nested.sql);
16443 }
16444
16445 #[test]
16446 fn test_a_tuple_literal_carries_the_members_it_names() {
16447 let out = compile_and_emit("SELECT (amount := 9.99, note := 'x')");
16451 match &out.shape.root {
16452 crate::query::ShapeNode::Tuple { names, elements, .. } => {
16453 let names = names.as_ref().expect("a literal names its own members");
16454 assert_eq!(names, &vec!["amount".to_string(), "note".to_string()]);
16455 assert_eq!(elements.len(), 2);
16456 }
16457 other => panic!("expected ShapeNode::Tuple, got {other:?}"),
16458 }
16459 }
16460
16461 #[test]
16462 fn test_a_tuple_literal_member_that_is_itself_a_tuple_recurses() {
16463 let out = compile_and_emit("SELECT (origin := (x := 1, y := 2), label := 'a')");
16464 let crate::query::ShapeNode::Tuple { elements, .. } = &out.shape.root else {
16465 panic!("expected ShapeNode::Tuple, got {:?}", out.shape.root)
16466 };
16467 match &elements[0] {
16468 crate::query::ShapeNode::Tuple { name, names, .. } => {
16469 assert_eq!(name, "origin");
16470 let names = names.as_ref().expect("a nested literal names its members too");
16471 assert_eq!(names, &vec!["x".to_string(), "y".to_string()]);
16472 }
16473 other => panic!("expected a nested tuple member, got {other:?}"),
16474 }
16475 assert!(
16478 out.sql.contains("ROW(1, 2)") || out.sql.contains("(1, 2)"),
16479 "got:\n{}",
16480 out.sql
16481 );
16482 assert!(!out.sql.contains("jsonb_build"), "got:\n{}", out.sql);
16483 }
16484
16485 #[test]
16486 fn test_a_tuple_literal_member_holding_an_enum_keeps_its_type() {
16487 let mut schema = make_schema();
16489 schema.enums.push(crate::schema::EnumDescriptor {
16490 name: "Gender".into(),
16491 module: "default".into(),
16492 members: vec!["Male".into(), "Female".into()],
16493 });
16494 let out = compile_and_emit_with("SELECT (shade := default::Gender.Male, label := 'a')", &schema);
16495 let crate::query::ShapeNode::Tuple { elements, .. } = &out.shape.root else {
16496 panic!("expected ShapeNode::Tuple, got {:?}", out.shape.root)
16497 };
16498 match &elements[0] {
16499 crate::query::ShapeNode::Enum { enum_type, .. } => assert_eq!(enum_type, "public::Gender"),
16502 other => panic!("expected an enum member, got {other:?}"),
16503 }
16504 }
16505
16506 #[test]
16507 fn test_a_free_object_literal_carries_no_member_plan() {
16508 let out = compile_and_emit("SELECT Person { meta := { foo := 'bar' } }");
16512 let crate::query::ShapeNode::Object { pointers, .. } = &out.shape.root else {
16513 panic!("expected ShapeNode::Object, got {:?}", out.shape.root)
16514 };
16515 let meta = pointers
16516 .iter()
16517 .find(|p| matches!(p, crate::query::ShapeNode::Object { name, .. } if name == "meta"))
16518 .expect("expected the meta pointer");
16519 match meta {
16520 crate::query::ShapeNode::Object {
16521 type_name, pointers, ..
16522 } => {
16523 assert_eq!(*type_name, None, "a free object names no declared type");
16524 assert!(
16525 matches!(&pointers[..], [crate::query::ShapeNode::Scalar { name, .. }] if name == "foo"),
16526 "got {pointers:?}"
16527 );
16528 }
16529 other => panic!("expected ShapeNode::Object, got {other:?}"),
16530 }
16531 }
16532
16533 #[test]
16534 fn test_structural_tuple_cast_shape_carries_real_members() {
16535 let out = compile_and_emit("SELECT <tuple<street: str, zip: str>>$p");
16536 match &out.shape.root {
16537 crate::query::ShapeNode::NamedTuple { type_name, members, .. } => {
16538 assert_eq!(*type_name, None);
16539 let members = members.as_ref().expect("expected resolved members");
16540 assert_eq!(members.len(), 2);
16541 assert_eq!(members[0].key.as_deref(), Some("street"));
16542 assert_eq!(members[1].key.as_deref(), Some("zip"));
16543 }
16544 other => panic!("expected ShapeNode::NamedTuple, got {other:?}"),
16545 }
16546 }
16547
16548 #[test]
16549 fn test_array_of_tuples_cast_shape_is_an_array_of_named_tuples() {
16550 let out = compile_and_emit("SELECT <array<tuple<street: str, zip: str>>>$p");
16554 assert!(out.sql.contains("::jsonb[]"), "got:\n{}", out.sql);
16555 match &out.shape.root {
16556 crate::query::ShapeNode::Array { element, position, .. } => {
16557 assert_eq!(*position, 0);
16558 match element.as_ref() {
16559 crate::query::ShapeNode::NamedTuple { members, position, .. } => {
16560 assert_eq!(*position, 0);
16561 let members = members.as_ref().expect("expected resolved members");
16562 assert_eq!(members.len(), 2);
16563 assert_eq!(members[0].key.as_deref(), Some("street"));
16564 assert_eq!(members[1].key.as_deref(), Some("zip"));
16565 }
16566 other => panic!("expected NamedTuple element, got {other:?}"),
16567 }
16568 }
16569 other => panic!("expected ShapeNode::Array, got {other:?}"),
16570 }
16571 }
16572
16573 #[test]
16574 fn test_tuple_cast_mixed_named_and_unnamed_elements_rejected() {
16575 match parse::parse("SELECT <tuple<x: float64, bool>>$p") {
16576 Ok(_) => panic!("expected parse error for mixed named/unnamed tuple elements"),
16577 Err(e) => assert!(e.to_string().contains("all named or all unnamed"), "got: {}", e),
16578 }
16579 }
16580
16581 #[test]
16582 fn test_is_with_tuple_type_rejected() {
16583 let schema = make_schema();
16584 let ast = parse::parse("SELECT Person FILTER Person is tuple<x: float64, y: float64>").unwrap();
16585 match ir::compile(&ast, &schema) {
16586 Ok(_) => panic!("expected error for IS with a tuple type"),
16587 Err(e) => {
16588 assert!(
16589 e.to_string().contains("cannot use IS with a tuple or array type"),
16590 "got: {}",
16591 e
16592 );
16593 }
16594 }
16595 }
16596
16597 #[test]
16598 fn test_is_with_array_type_rejected() {
16599 let schema = make_schema();
16600 let ast = parse::parse("SELECT Person FILTER Person is array<str>").unwrap();
16601 match ir::compile(&ast, &schema) {
16602 Ok(_) => panic!("expected error for IS with an array type"),
16603 Err(e) => {
16604 assert!(
16605 e.to_string().contains("cannot use IS with a tuple or array type"),
16606 "got: {}",
16607 e
16608 );
16609 }
16610 }
16611 }
16612
16613 #[test]
16614 fn test_tuple_index_on_non_literal_falls_back_to_jsonb_index() {
16615 let out = compile_and_emit("SELECT (<tuple<int64, str>>('1', 3)).1");
16619 assert!(out.sql.contains("->1"), "got:\n{}", out.sql);
16620 }
16621
16622 #[test]
16623 fn test_tuple_index_out_of_bounds_on_cast_target_errors_at_compile_time() {
16624 let ast = parse::parse("SELECT (<tuple<int64, str>>('1', 3)).2").unwrap();
16630 let schema = make_schema();
16631 match ir::compile(&ast, &schema) {
16632 Ok(_) => panic!("expected out-of-bounds tuple index error"),
16633 Err(e) => {
16634 assert!(
16635 e.to_string()
16636 .contains("2 is not a member of tuple<std::int64, std::str>"),
16637 "got: {}",
16638 e
16639 );
16640 }
16641 }
16642 }
16643
16644 #[test]
16645 fn test_positional_tuple_literal_cast_to_tuple_type_compiles() {
16646 let out = compile_and_emit("SELECT <tuple<int64, str>>(1, 'x')");
16650 assert!(out.sql.contains("ROW((1)::int8, ('x')::text)"), "got:\n{}", out.sql);
16651 assert!(!out.sql.contains("jsonb"), "got:\n{}", out.sql);
16652 }
16653
16654 #[test]
16655 fn test_positional_tuple_literal_cast_coerces_mismatched_literal_types() {
16656 let out = compile_and_emit("SELECT <tuple<int64, str>>('1', 3)");
16660 assert!(out.sql.contains("ROW(('1')::int8, (3)::text)"), "got:\n{}", out.sql);
16661 }
16662
16663 #[test]
16664 fn test_nested_tuple_literal_cast_applies_casts_recursively() {
16665 let out = compile_and_emit(
16666 "SELECT <tuple<point: tuple<x: float64, y: float64>, label: str>>(point := ('1', 2), label := 5)",
16667 );
16668 assert!(
16669 out.sql.contains("ROW(ROW(('1')::float8, (2)::float8), (5)::text)"),
16670 "got:\n{}",
16671 out.sql
16672 );
16673 let crate::query::ShapeNode::Tuple { names, elements, .. } = &out.shape.root else {
16676 panic!("expected ShapeNode::Tuple, got {:?}", out.shape.root)
16677 };
16678 assert_eq!(names.as_ref().unwrap(), &vec!["point".to_string(), "label".to_string()]);
16679 assert!(
16680 matches!(&elements[0], crate::query::ShapeNode::Tuple { names: Some(n), .. } if n == &vec!["x".to_string(), "y".to_string()]),
16681 "got {:?}",
16682 elements[0]
16683 );
16684 }
16685
16686 #[test]
16687 fn test_positional_tuple_literal_nested_inside_named_tuple_compiles() {
16688 let out = compile_and_emit("SELECT (point := (1, 2), label := 'origin')");
16689 assert!(out.sql.contains("ROW(1, 2)"), "got:\n{}", out.sql);
16692 assert!(!out.sql.contains("jsonb_build"), "got:\n{}", out.sql);
16693 let crate::query::ShapeNode::Tuple { elements, names, .. } = &out.shape.root else {
16694 panic!("expected ShapeNode::Tuple, got {:?}", out.shape.root)
16695 };
16696 assert_eq!(names.as_ref().unwrap(), &vec!["point".to_string(), "label".to_string()]);
16697 assert!(
16699 matches!(&elements[0], crate::query::ShapeNode::Tuple { names: None, .. }),
16700 "got {:?}",
16701 elements[0]
16702 );
16703 }
16704
16705 #[test]
16706 fn test_positional_tuple_literal_in_schema_bound_shape_field_compiles() {
16707 let out = compile_and_emit("SELECT Person { name, pair := (1, 2) }");
16708 assert!(out.sql.contains("ROW(1, 2)"), "got:\n{}", out.sql);
16709 assert!(!out.sql.contains("jsonb_build"), "got:\n{}", out.sql);
16710 }
16711
16712 fn make_schema_with_search_index(backend: crate::schema::SearchBackend) -> SchemaDescriptor {
16721 use crate::schema::{SearchIndexDescriptor, SearchPointerDescriptor, SearchWeight};
16722 let mut s = make_schema();
16723 if let Some(td) = s.types.iter_mut().find(|t| t.name == "Person") {
16724 td.search_indexes.push(SearchIndexDescriptor {
16725 index_name: None,
16726 backend,
16727 pointers: vec![SearchPointerDescriptor {
16728 name: "name".into(),
16729 weight: SearchWeight::A,
16730 }],
16731 });
16732 }
16733 s
16734 }
16735
16736 #[test]
16737 fn test_insert_enqueues_a_meilisearch_outbox_row() {
16738 let schema = make_schema_with_search_index(crate::schema::SearchBackend::Meilisearch);
16739 let out = compile_and_emit_with("INSERT Person { name := 'Alice', age := 30 }", &schema);
16740 assert!(
16741 out.sql.contains("'Meilisearch'::_pylon.\"IndexKind\""),
16742 "expected a Meilisearch outbox enqueue CTE, got:\n{}",
16743 out.sql,
16744 );
16745 assert!(
16746 out.sql.contains("INSERT INTO _pylon.\"IndexOutbox\""),
16747 "got:\n{}",
16748 out.sql
16749 );
16750 }
16751
16752 #[test]
16753 fn test_insert_enqueues_an_opensearch_outbox_row() {
16754 let schema = make_schema_with_search_index(crate::schema::SearchBackend::OpenSearch);
16755 let out = compile_and_emit_with("INSERT Person { name := 'Alice', age := 30 }", &schema);
16756 assert!(
16757 out.sql.contains("'OpenSearch'::_pylon.\"IndexKind\""),
16758 "expected an OpenSearch outbox enqueue CTE, got:\n{}",
16759 out.sql,
16760 );
16761 }
16762
16763 #[test]
16764 fn test_insert_does_not_enqueue_an_outbox_row_for_a_postgres_backed_search_index() {
16765 let schema = make_schema_with_search_index(crate::schema::SearchBackend::Postgres);
16768 let out = compile_and_emit_with("INSERT Person { name := 'Alice', age := 30 }", &schema);
16769 assert!(
16770 !out.sql.contains("_pylon.\"IndexOutbox\""),
16771 "did not expect an outbox enqueue, got:\n{}",
16772 out.sql
16773 );
16774 }
16775
16776 #[test]
16777 fn test_update_enqueues_a_meilisearch_outbox_row() {
16778 let schema = make_schema_with_search_index(crate::schema::SearchBackend::Meilisearch);
16779 let out = compile_and_emit_with("UPDATE Person FILTER .name = 'Alice' SET { age := 31 }", &schema);
16780 assert!(
16781 out.sql.contains("'Meilisearch'::_pylon.\"IndexKind\""),
16782 "expected a Meilisearch outbox enqueue CTE, got:\n{}",
16783 out.sql,
16784 );
16785 }
16786
16787 #[test]
16788 fn test_delete_enqueues_a_meilisearch_outbox_delete_job() {
16789 let schema = make_schema_with_search_index(crate::schema::SearchBackend::Meilisearch);
16790 let out = compile_and_emit_with("DELETE Person FILTER .name = 'Alice'", &schema);
16791 assert!(
16792 out.sql.contains("'Meilisearch'::_pylon.\"IndexKind\""),
16793 "expected a Meilisearch outbox enqueue CTE, got:\n{}",
16794 out.sql,
16795 );
16796 assert!(
16797 out.sql.contains("'delete'"),
16798 "expected the delete operation literal, got:\n{}",
16799 out.sql
16800 );
16801 }
16802
16803 #[test]
16804 fn test_range_intrinsic_resolves_int_literals_to_int8range() {
16805 let out = compile_and_emit("SELECT std::overlaps(std::range(1, 3), std::range(2, 5))");
16810 assert!(out.sql.contains("int8range(1, 3)"), "got:\n{}", out.sql);
16811 assert!(out.sql.contains("int8range(2, 5)"), "got:\n{}", out.sql);
16812 assert!(
16813 out.sql.contains(" && "),
16814 "expected infix && for overlaps, got:\n{}",
16815 out.sql
16816 );
16817 assert!(
16818 !out.sql.contains("\"std\""),
16819 "must not emit a literal std schema call, got:\n{}",
16820 out.sql
16821 );
16822 }
16823
16824 #[test]
16825 fn test_range_intrinsic_resolves_datetime_to_tstzrange() {
16826 let out =
16827 compile_and_emit("SELECT std::range(<datetime>'2024-01-01T00:00:00Z', <datetime>'2024-06-01T00:00:00Z')");
16828 assert!(out.sql.contains("tstzrange("), "got:\n{}", out.sql);
16829 }
16830
16831 #[test]
16834 fn test_range_intrinsic_named_bounds_become_a_bounds_string() {
16835 let out = compile_and_emit("SELECT std::range(1, 3, inc_lower := true, inc_upper := true)");
16836 assert!(out.sql.contains("int8range(1, 3, '[]')"), "got:\n{}", out.sql);
16837 let out = compile_and_emit("SELECT std::range(1, 3, inc_lower := false)");
16838 assert!(out.sql.contains("int8range(1, 3, '()')"), "got:\n{}", out.sql);
16839 }
16840
16841 #[test]
16843 fn test_range_intrinsic_computes_a_bounds_string_from_an_expression() {
16844 let out = compile_and_emit("SELECT std::range(1, 3, inc_upper := <bool>$closed)");
16845 assert!(out.sql.contains("CASE WHEN"), "got:\n{}", out.sql);
16846 }
16847
16848 #[test]
16850 fn test_range_intrinsic_leaves_a_missing_upper_bound_unbounded() {
16851 let out = compile_and_emit("SELECT std::range(1)");
16852 assert!(out.sql.contains("int8range(1, NULL)"), "got:\n{}", out.sql);
16853 }
16854
16855 #[test]
16858 fn test_a_named_only_parameter_is_reported_as_named_only() {
16859 assert!(
16860 compile_err("SELECT std::range(1, 5, true, false)").contains("takes 'inc_lower' as a named argument only"),
16861 "{}",
16862 compile_err("SELECT std::range(1, 5, true, false)")
16863 );
16864 assert!(
16865 compile_err("SELECT std::json_set(std::to_json('{}'), 'a', std::to_json('1'))")
16866 .contains("requires the named argument 'value'"),
16867 "{}",
16868 compile_err("SELECT std::json_set(std::to_json('{}'), 'a', std::to_json('1'))")
16869 );
16870 assert!(
16872 compile_err("SELECT std::range()").contains("takes 1 or 2 argument(s), got 0"),
16873 "{}",
16874 compile_err("SELECT std::range()")
16875 );
16876 let message = compile_err("SELECT std::json_get(std::to_json('{}'), 'a', 1, 2)");
16878 assert!(message.contains("default := optional<json>"), "{message}");
16879 }
16880
16881 #[test]
16882 fn test_range_intrinsic_builds_the_empty_range() {
16883 let out = compile_and_emit("SELECT std::range(1, 3, empty := true)");
16884 assert!(out.sql.contains("'empty'::int8range"), "got:\n{}", out.sql);
16885 }
16886
16887 #[test]
16888 fn test_multirange_intrinsic_resolves_from_range_element() {
16889 let out = compile_and_emit("SELECT std::multirange([std::range(1, 3), std::range(5, 7)])");
16890 assert!(out.sql.contains("int8multirange(VARIADIC "), "got:\n{}", out.sql);
16891 assert!(
16892 !out.sql.contains("\"std\""),
16893 "must not emit a literal std schema call, got:\n{}",
16894 out.sql
16895 );
16896 }
16897}