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) => format!("({})", elements.iter().map(emit_expr).collect::<Vec<_>>().join(", ")),
2059 other => format!("({})", emit_expr(other)),
2060 });
2061 match (&on_sql, &conflict.do_update) {
2062 (None, None) => sql.push_str(" ON CONFLICT DO NOTHING"),
2063 (Some(on), None) => sql.push_str(&format!(" ON CONFLICT {} DO NOTHING", on)),
2064 (None, Some(updates)) => {
2065 sql.push_str(&format!(" ON CONFLICT DO UPDATE SET {}", do_update_sets(updates)));
2066 }
2067 (Some(on), Some(updates)) => {
2068 sql.push_str(&format!(
2069 " ON CONFLICT {} DO UPDATE SET {}",
2070 on,
2071 do_update_sets(updates),
2072 ));
2073 }
2074 }
2075 if conflict.do_update.is_some()
2076 && let Some(predicate) = &conflict.do_update_where
2077 {
2078 sql.push_str(&format!(" WHERE {}", emit_expr(predicate)));
2079 }
2080}
2081
2082fn do_update_sets(updates: &[(String, IrExpr)]) -> String {
2083 updates
2084 .iter()
2085 .map(|(col, expr)| format!("{} = {}", qi(col), emit_expr(expr)))
2086 .collect::<Vec<_>>()
2087 .join(", ")
2088}
2089
2090fn is_integer_expr(expr: &IrExpr) -> bool {
2095 match expr {
2096 IrExpr::ColumnRef { pg_type, .. } => matches!(
2097 pg_type.as_str(),
2098 "int2" | "int4" | "int8" | "integer" | "bigint" | "smallint"
2099 ),
2100 IrExpr::Literal(crate::ir::IrLiteral::Int(_)) => true,
2101 IrExpr::BinOp(op) => is_integer_expr(&op.left) && is_integer_expr(&op.right),
2102 _ => false,
2103 }
2104}
2105
2106fn is_raw_scalar(expr: &IrExpr) -> bool {
2107 matches!(expr, IrExpr::Array(_))
2108 || matches!(expr, IrExpr::TypeCast(c) if c.pg_type == "jsonb")
2109 || matches!(expr, IrExpr::NamedTuple { .. })
2110 || matches!(expr, IrExpr::Tuple(_))
2111 || matches!(expr, IrExpr::JsonbField { .. })
2112 || matches!(expr, IrExpr::JsonbIndex { .. })
2113}
2114
2115fn emit_free_rows(sel: &IrSelect, rows: &[IrRowSource], ctes: &[IrCteDef]) -> SqlOutput {
2121 use crate::query::ShapeNode;
2122
2123 let items: Vec<&IrFreeExpr> = rows
2124 .iter()
2125 .map(|r| match r {
2126 IrRowSource::Free(item) => item,
2127 IrRowSource::Bound { .. } => unreachable!("mixed Bound/Free rows rejected at compile time"),
2128 })
2129 .collect();
2130
2131 if items.is_empty() {
2132 return SqlOutput {
2133 sql: "SELECT NULL AS result WHERE FALSE".to_string(),
2134 shape: ShapeDescriptor {
2135 root: ShapeNode::Scalar {
2136 name: String::new(),
2137 position: 0,
2138 },
2139 },
2140 inference_plan: None,
2141 };
2142 }
2143
2144 if items.len() == 1
2146 && let IrFreeExpr::AssertSet {
2147 fn_name,
2148 inner,
2149 message,
2150 } = items[0]
2151 {
2152 let array_sql = emit_array_source(inner) + &assert_message_arg(message);
2153 let mut sql = format!(
2154 "SELECT ROW(v) AS result FROM unnest(\"_pylon\".{}({})) AS _assert(v)",
2155 fn_name, array_sql,
2156 );
2157 if sel.distinct {
2158 sql = format!("SELECT DISTINCT * FROM ({}) AS \"_distinct\"", sql);
2159 }
2160 append_order_by(&mut sql, &sel.order_by);
2161 append_offset_limit(&mut sql, &sel.offset, &sel.limit);
2162 return SqlOutput {
2163 sql,
2164 shape: ShapeDescriptor {
2165 root: ShapeNode::Scalar {
2166 name: String::new(),
2167 position: 0,
2168 },
2169 },
2170 inference_plan: None,
2171 };
2172 }
2173
2174 let shape_root = free_item_shape(items.first().unwrap(), ctes);
2175
2176 let branches: Vec<String> = items
2177 .iter()
2178 .map(|item| match item {
2179 IrFreeExpr::Scalar(expr) => {
2180 if matches!(expr, IrExpr::ObjectSubquery(_)) {
2185 return format!("SELECT v AS result, v FROM (SELECT {} AS v) AS _obj", emit_expr(expr));
2186 }
2187 if is_raw_scalar(expr) {
2190 format!("SELECT v AS result, v FROM (SELECT {} AS v) AS _raw", emit_expr(expr))
2193 } else {
2194 let e = emit_expr(expr);
2201 let row_value = if enum_type_of_expr(expr).is_some() {
2202 "v::text"
2203 } else {
2204 "v"
2205 };
2206 let present = if items.len() > 1 { " WHERE v IS NOT NULL" } else { "" };
2210 format!("SELECT ROW({row_value}) AS result, v FROM (SELECT {e} AS v) AS _scalar{present}")
2211 }
2212 }
2213 IrFreeExpr::FreeObject(fields) => {
2214 let inner_cols: Vec<String> = fields
2220 .iter()
2221 .enumerate()
2222 .map(|(i, (_, e))| format!("{} AS \"_f{}\"", emit_expr(e), i))
2223 .collect();
2224 let row_items: Vec<String> = fields
2225 .iter()
2226 .enumerate()
2227 .map(|(i, (_, e))| {
2228 if enum_type_of_expr(e).is_some() {
2229 format!("\"_f{}\"::text", i)
2230 } else {
2231 format!("\"_f{}\"", i)
2232 }
2233 })
2234 .collect();
2235 let named_cols: Vec<String> = fields
2236 .iter()
2237 .enumerate()
2238 .map(|(i, (name, _))| format!("\"_f{}\" AS {}", i, qi(name)))
2239 .collect();
2240 format!(
2241 "SELECT ROW({}) AS result, {} FROM (SELECT {}) AS _obj",
2242 row_items.join(", "),
2243 named_cols.join(", "),
2244 inner_cols.join(", "),
2245 )
2246 }
2247 IrFreeExpr::Tuple(exprs) => {
2248 if exprs.len() == 1 {
2249 format!("SELECT ROW({}) AS result", emit_free_field_expr(&exprs[0]))
2250 } else {
2251 let parts: Vec<String> = exprs.iter().map(emit_free_field_expr).collect();
2252 format!("SELECT ({}) AS result", parts.join(", "))
2253 }
2254 }
2255 IrFreeExpr::NamedTupleRow(fields) => {
2256 let parts: Vec<String> = fields.iter().map(|(_, e)| emit_free_field_expr(e)).collect();
2257 if parts.len() == 1 {
2258 format!("SELECT ROW({}) AS result", parts[0])
2259 } else {
2260 format!("SELECT ({}) AS result", parts.join(", "))
2261 }
2262 }
2263 IrFreeExpr::AssertSet { .. } => unreachable!("AssertSet is handled by early return above"),
2264 IrFreeExpr::CtePassthrough(name) => format!("SELECT \"result\" FROM {}", qi(name)),
2265 })
2266 .collect();
2267
2268 let union_sql = branches.join("\nUNION ALL\n");
2269
2270 let mut sql = if sel.distinct {
2271 format!("SELECT DISTINCT * FROM (\n{}\n) AS \"_distinct\"", union_sql)
2273 } else {
2274 union_sql
2275 };
2276 if let Some(filter) = &sel.filter {
2280 sql = format!(
2281 "SELECT * FROM (\n{}\n) AS \"_filtered\"\nWHERE {}",
2282 sql,
2283 emit_expr(filter)
2284 );
2285 }
2286 append_order_by(&mut sql, &sel.order_by);
2287 append_offset_limit(&mut sql, &sel.offset, &sel.limit);
2288
2289 SqlOutput {
2290 sql,
2291 shape: ShapeDescriptor { root: shape_root },
2292 inference_plan: None,
2293 }
2294}
2295
2296fn enum_type_of_expr(expr: &IrExpr) -> Option<QualifiedPgType> {
2306 match expr {
2307 IrExpr::ColumnRef { pg_type, .. } => QualifiedPgType::of(pg_type),
2308 IrExpr::EnumLiteral { pg_type, .. } => Some(QualifiedPgType {
2309 name: pg_quoted_to_pylon(pg_type),
2310 is_array: false,
2311 }),
2312 _ => None,
2313 }
2314}
2315
2316fn enum_type_of_shape_expr(expr: &IrExpr) -> Option<QualifiedPgType> {
2325 if let Some(qualified) = enum_type_of_expr(expr) {
2326 return Some(qualified);
2327 }
2328 let IrExpr::Subquery(select) = expr else {
2329 return None;
2330 };
2331 let [IrRowSource::Bound { shape, .. }] = select.rows.as_slice() else {
2332 return None;
2333 };
2334 let [IrShapePointer::Scalar(scalar)] = shape.as_slice() else {
2335 return None;
2336 };
2337 QualifiedPgType::of(&scalar.pg_type)
2338}
2339
2340fn emit_free_field_expr(expr: &IrExpr) -> String {
2346 match enum_type_of_expr(expr) {
2347 Some(qualified) => format!("{}{}", emit_expr(expr), qualified.text_cast()),
2348 None => emit_expr(expr),
2349 }
2350}
2351
2352fn literal_tuple_member(key: &str, value: &IrExpr) -> crate::query::JsonMember {
2356 use crate::query::{JsonMember, JsonMemberKind};
2357
2358 let kind = match value {
2359 IrExpr::NamedTuple { fields, .. } => JsonMemberKind::Tuple {
2360 type_name: None,
2361 members: fields
2362 .iter()
2363 .map(|(key, value)| literal_tuple_member(key, value))
2364 .collect(),
2365 },
2366 _ => match enum_type_of_expr(value) {
2367 Some(qualified) => JsonMemberKind::Enum {
2368 enum_type: pg_schema_qualified_to_pylon(&qualified.name),
2371 },
2372 None if crate::ir::infer_ir_type(value) == Some("numeric") => JsonMemberKind::Decimal,
2373 None => JsonMemberKind::Scalar,
2374 },
2375 };
2376 JsonMember {
2377 key: Some(key.to_string()),
2378 kind,
2379 }
2380}
2381
2382fn pg_schema_qualified_to_pylon(qualified: &str) -> String {
2385 match qualified.split_once("::") {
2386 Some(("public", name)) => format!("default::{name}"),
2387 _ => qualified.to_string(),
2388 }
2389}
2390
2391fn free_field_shape_node(name: &str, position: usize, expr: &IrExpr) -> crate::query::ShapeNode {
2394 use crate::query::{Cardinality, ShapeNode};
2395 if let IrExpr::ObjectSubquery(sel) = expr {
2396 let [IrRowSource::Bound { source, shape }] = sel.rows.as_slice() else {
2397 unreachable!("an object subquery is always schema-bound")
2398 };
2399 let (_, nodes) = build_shape(shape, &source.alias);
2400 return ShapeNode::Object {
2401 name: name.to_string(),
2402 type_name: Some(source.type_name.clone()),
2403 position,
2404 cardinality: Cardinality::Optional,
2405 pointers: prepend_type(nodes),
2406 has_implicit_id: shape_has_implicit_id(shape),
2407 };
2408 }
2409 if matches!(expr, IrExpr::ObjectPathSubquery(_) | IrExpr::ObjectPathUnion { .. }) {
2411 return expr_shape_node(name, position, expr);
2412 }
2413 if let IrExpr::ArrayFromSelect(source) = expr
2416 && matches!(
2417 source.as_ref(),
2418 IrArraySource::ObjectSelect(_)
2419 | IrArraySource::ObjectFunction(_)
2420 | IrArraySource::PathSelect(_)
2421 | IrArraySource::Group(_)
2422 )
2423 {
2424 return expr_shape_node(name, position, expr);
2425 }
2426 match enum_type_of_shape_expr(expr) {
2427 Some(qualified) => qualified.shape_node(name.to_string(), position),
2428 None => ShapeNode::Scalar {
2429 name: name.to_string(),
2430 position,
2431 },
2432 }
2433}
2434
2435fn expr_shape_node(name: &str, position: usize, expr: &IrExpr) -> crate::query::ShapeNode {
2442 use crate::query::{Cardinality, ShapeNode};
2443 match expr {
2444 IrExpr::ObjectPathUnion { branches, multi, .. } => {
2445 let first = branches.first().expect("a union has at least one branch");
2446 let IrPathResult::Object {
2447 alias,
2448 type_name,
2449 shape,
2450 } = &first.result
2451 else {
2452 unreachable!("an object path union's branches always land on objects")
2453 };
2454 let (_, pointer_nodes) = build_shape(shape, alias);
2455 let object = ShapeNode::Object {
2456 name: if *multi { String::new() } else { name.to_string() },
2457 type_name: Some(type_name.clone()),
2458 position: if *multi { 0 } else { position },
2459 cardinality: if *multi {
2460 Cardinality::Many
2461 } else {
2462 Cardinality::Optional
2463 },
2464 pointers: prepend_type(pointer_nodes),
2465 has_implicit_id: shape_has_implicit_id(shape),
2466 };
2467 if *multi {
2468 ShapeNode::Array {
2469 name: name.to_string(),
2470 position,
2471 element: Box::new(object),
2472 }
2473 } else {
2474 object
2475 }
2476 }
2477 IrExpr::ObjectPathSubquery(ps) => {
2478 let IrPathResult::Object {
2479 alias,
2480 type_name,
2481 shape,
2482 } = &ps.result
2483 else {
2484 unreachable!("an object path subquery always lands on an object")
2485 };
2486 let (_, pointer_nodes) = build_shape(shape, alias);
2487 ShapeNode::Object {
2488 name: name.to_string(),
2489 type_name: Some(type_name.clone()),
2490 position,
2491 cardinality: Cardinality::Optional,
2492 pointers: prepend_type(pointer_nodes),
2493 has_implicit_id: shape_has_implicit_id(shape),
2494 }
2495 }
2496 IrExpr::TypeCast(c) if c.tuple_shape.is_some() => {
2497 let shape = c.tuple_shape.as_ref().unwrap();
2498 let is_array = c.pg_type.ends_with("[]");
2501 let tuple = ShapeNode::NamedTuple {
2502 name: if is_array { String::new() } else { name.to_string() },
2503 position: if is_array { 0 } else { position },
2504 type_name: shape.type_name.clone(),
2505 members: Some(shape.members.clone()),
2506 is_free_object: false,
2507 };
2508 if is_array {
2509 ShapeNode::Array {
2510 name: name.to_string(),
2511 position,
2512 element: Box::new(tuple),
2513 }
2514 } else {
2515 tuple
2516 }
2517 }
2518 IrExpr::TypeCast(c) if c.pg_type == "jsonb" => ShapeNode::Scalar {
2524 name: name.to_string(),
2525 position,
2526 },
2527 IrExpr::NamedTuple { fields, is_free_object } => ShapeNode::NamedTuple {
2528 name: name.to_string(),
2529 position,
2530 type_name: None,
2531 members: (!*is_free_object).then(|| {
2542 fields
2543 .iter()
2544 .map(|(key, value)| literal_tuple_member(key, value))
2545 .collect()
2546 }),
2547 is_free_object: *is_free_object,
2548 },
2549 IrExpr::ArrayFromSelect(source) => match source.as_ref() {
2552 IrArraySource::ObjectFunction(fs) => {
2553 let (_, nodes) = build_shape(&fs.shape, &fs.alias);
2554 ShapeNode::Array {
2555 name: name.to_string(),
2556 position,
2557 element: Box::new(ShapeNode::Object {
2558 name: String::new(),
2559 type_name: Some(fs.type_name.clone()),
2560 position: 0,
2561 cardinality: Cardinality::Many,
2562 pointers: prepend_type(nodes),
2563 has_implicit_id: shape_has_implicit_id(&fs.shape),
2564 }),
2565 }
2566 }
2567 IrArraySource::Group(grp) => ShapeNode::Array {
2568 name: name.to_string(),
2569 position,
2570 element: Box::new(emit_group(grp).shape.root),
2571 },
2572 IrArraySource::ObjectSelect(sel) => {
2573 let [IrRowSource::Bound { source, shape }] = sel.rows.as_slice() else {
2574 unreachable!("IrArraySource::ObjectSelect is always schema-bound")
2575 };
2576 let (_, nodes) = build_shape(shape, &source.alias);
2577 ShapeNode::Array {
2578 name: name.to_string(),
2579 position,
2580 element: Box::new(ShapeNode::Object {
2581 name: String::new(),
2582 type_name: Some(source.type_name.clone()),
2583 position: 0,
2584 cardinality: Cardinality::Many,
2585 pointers: prepend_type(nodes),
2586 has_implicit_id: shape_has_implicit_id(shape),
2587 }),
2588 }
2589 }
2590 IrArraySource::PathSelect(ps) => match &ps.result {
2591 IrPathResult::Object {
2592 alias,
2593 type_name,
2594 shape,
2595 } => {
2596 let (_, pointer_nodes) = build_shape(shape, alias);
2597 ShapeNode::Array {
2598 name: name.to_string(),
2599 position,
2600 element: Box::new(ShapeNode::Object {
2601 name: String::new(),
2602 type_name: Some(type_name.clone()),
2603 position: 0,
2604 cardinality: Cardinality::Many,
2605 pointers: prepend_type(pointer_nodes),
2606 has_implicit_id: shape_has_implicit_id(shape),
2607 }),
2608 }
2609 }
2610 IrPathResult::Scalar(e, tuple_shape) if yields_array(e) => ShapeNode::Array {
2611 name: name.to_string(),
2612 position,
2613 element: Box::new(aggregated_array_element(e, tuple_shape.as_ref())),
2614 },
2615 IrPathResult::Scalar(..) => ShapeNode::Scalar {
2616 name: name.to_string(),
2617 position,
2618 },
2619 },
2620 _ => ShapeNode::Scalar {
2621 name: name.to_string(),
2622 position,
2623 },
2624 },
2625 e if is_raw_scalar(e) => ShapeNode::RawScalar,
2626 e => free_field_shape_node(name, position, e),
2627 }
2628}
2629
2630fn free_item_shape(item: &IrFreeExpr, ctes: &[IrCteDef]) -> crate::query::ShapeNode {
2631 use crate::query::{Cardinality, ShapeNode};
2632 match item {
2633 IrFreeExpr::Scalar(e) => match e {
2634 IrExpr::TypeCast(c) if c.pg_type == "jsonb" && c.tuple_shape.is_none() => ShapeNode::JsonScalar,
2638 _ => expr_shape_node("", 0, e),
2639 },
2640 IrFreeExpr::FreeObject(fields) => ShapeNode::Object {
2641 name: String::new(),
2642 type_name: None,
2643 position: 0,
2644 cardinality: Cardinality::Many,
2645 pointers: fields
2646 .iter()
2647 .enumerate()
2648 .map(|(i, (name, e))| free_field_shape_node(name, i, e))
2649 .collect(),
2650 has_implicit_id: false,
2651 },
2652 IrFreeExpr::Tuple(exprs) => ShapeNode::Tuple {
2653 position: 0,
2654 elements: exprs
2655 .iter()
2656 .enumerate()
2657 .map(|(i, e)| free_field_shape_node("", i, e))
2658 .collect(),
2659 names: None,
2660 },
2661 IrFreeExpr::NamedTupleRow(fields) => ShapeNode::Tuple {
2662 position: 0,
2663 elements: fields
2664 .iter()
2665 .enumerate()
2666 .map(|(i, (name, e))| free_field_shape_node(name, i, e))
2667 .collect(),
2668 names: Some(fields.iter().map(|(name, _)| name.clone()).collect()),
2669 },
2670 IrFreeExpr::AssertSet { .. } => ShapeNode::Scalar {
2671 name: String::new(),
2672 position: 0,
2673 },
2674 IrFreeExpr::CtePassthrough(name) => ctes
2680 .iter()
2681 .find(|c| &c.name == name)
2682 .and_then(|c| match &c.stmt {
2683 IrStmt::Select(sel) => match sel.rows.first() {
2684 Some(IrRowSource::Free(inner)) => Some(free_item_shape(inner, ctes)),
2685 _ => None,
2686 },
2687 IrStmt::PathSelect(ps) => Some(emit_path_select(ps).shape.root),
2692 _ => None,
2693 })
2694 .unwrap_or(ShapeNode::Scalar {
2695 name: String::new(),
2696 position: 0,
2697 }),
2698 }
2699}
2700
2701fn emit_path_joins(root: &IrSource, joins: &[IrPathJoin]) -> String {
2704 let mut parts = vec![format!("{} AS {}", source_ref(root), qi(&root.alias))];
2705 for join in joins {
2706 match join {
2707 IrPathJoin::Single {
2708 source_alias,
2709 fk_col,
2710 target,
2711 } => {
2712 parts.push(format!(
2713 "JOIN {} AS {} ON {}.{} = {}.\"id\"",
2714 source_ref(target),
2715 qi(&target.alias),
2716 qi(source_alias),
2717 qi(fk_col),
2718 qi(&target.alias),
2719 ));
2720 }
2721 IrPathJoin::Multi {
2722 source_alias,
2723 junction_alias,
2724 join,
2725 target,
2726 } => {
2727 match join {
2728 IrMultiLinkJoin::Standard { junction_table, module } => {
2729 parts.push(format!(
2730 "JOIN {} AS {} ON {}.\"source\" = {}.\"id\"",
2731 junction_ref(module, junction_table),
2732 qi(junction_alias),
2733 qi(junction_alias),
2734 qi(source_alias),
2735 ));
2736 parts.push(format!(
2737 "JOIN {} AS {} ON {}.\"id\" = {}.\"target\"",
2738 source_ref(target),
2739 qi(&target.alias),
2740 qi(&target.alias),
2741 qi(junction_alias),
2742 ));
2743 }
2744 IrMultiLinkJoin::Through {
2745 junction_table,
2746 module,
2747 source_col,
2748 target_col,
2749 } => {
2750 parts.push(format!(
2751 "JOIN {} AS {} ON {}.{} = {}.\"id\"",
2752 junction_ref(module, junction_table),
2753 qi(junction_alias),
2754 qi(junction_alias),
2755 qi(source_col),
2756 qi(source_alias),
2757 ));
2758 parts.push(format!(
2759 "JOIN {} AS {} ON {}.\"id\" = {}.{}",
2760 source_ref(target),
2761 qi(&target.alias),
2762 qi(&target.alias),
2763 qi(junction_alias),
2764 qi(target_col),
2765 ));
2766 }
2767 IrMultiLinkJoin::BacklinkFk { .. } | IrMultiLinkJoin::BacklinkJunction { .. } => {
2769 unreachable!("a forward multi-link path step never uses a backlink join variant")
2770 }
2771 }
2772 }
2773 IrPathJoin::BacklinkSingle {
2774 source_alias,
2775 fk_col,
2776 target,
2777 } => {
2778 parts.push(format!(
2779 "JOIN {} AS {} ON {}.{} = {}.\"id\"",
2780 source_ref(target),
2781 qi(&target.alias),
2782 qi(&target.alias),
2783 qi(fk_col),
2784 qi(source_alias),
2785 ));
2786 }
2787 IrPathJoin::Function {
2791 fn_module,
2792 fn_name,
2793 args,
2794 target,
2795 } => {
2796 let args_sql = args.iter().map(emit_expr).collect::<Vec<_>>().join(", ");
2797 parts.push(format!(
2798 "CROSS JOIN LATERAL {}.{}({}) AS {}",
2799 pg_schema(fn_module),
2800 qi(fn_name),
2801 args_sql,
2802 qi(&target.alias),
2803 ));
2804 }
2805 IrPathJoin::Lateral { inner, target } => {
2806 let projected = match &inner.result {
2807 IrPathResult::Object { alias, .. } => alias.clone(),
2808 IrPathResult::Scalar(..) => {
2809 unreachable!("a lateral step is only built for an object-valued computed")
2810 }
2811 };
2812 let mut body = format!(
2813 "SELECT {}.* FROM {}",
2814 qi(&projected),
2815 emit_path_joins(&inner.root, &inner.joins)
2816 );
2817 append_filter(&mut body, &inner.filter);
2818 append_order_by(&mut body, &inner.order_by);
2819 append_offset_limit(&mut body, &inner.offset, &inner.limit);
2820 parts.push(format!("JOIN LATERAL (\n{}\n) AS {} ON TRUE", body, qi(&target.alias)));
2821 }
2822 IrPathJoin::BacklinkMulti {
2823 source_alias,
2824 junction_alias,
2825 junction_table,
2826 module,
2827 owner_col,
2828 current_col,
2829 target,
2830 } => {
2831 parts.push(format!(
2832 "JOIN {} AS {} ON {}.{} = {}.\"id\"",
2833 junction_ref(module, junction_table),
2834 qi(junction_alias),
2835 qi(junction_alias),
2836 qi(current_col),
2837 qi(source_alias),
2838 ));
2839 parts.push(format!(
2840 "JOIN {} AS {} ON {}.\"id\" = {}.{}",
2841 source_ref(target),
2842 qi(&target.alias),
2843 qi(&target.alias),
2844 qi(junction_alias),
2845 qi(owner_col),
2846 ));
2847 }
2848 }
2849 }
2850 parts.join("\n")
2851}
2852
2853fn emit_array_source(src: &IrArraySource) -> String {
2855 match src {
2856 IrArraySource::Select(s) => {
2857 let [IrRowSource::Bound { source, shape }] = s.rows.as_slice() else {
2860 unreachable!("IrArraySource::Select is always schema-bound")
2861 };
2862 let scalar = match shape.first() {
2863 Some(IrShapePointer::Scalar(sf)) => format!("{}.{}", qi(&source.alias), qi(&sf.column)),
2864 _ => format!("{}.\"id\"", qi(&source.alias)),
2865 };
2866 let mut sql = format!("SELECT {} FROM {} AS {}", scalar, source_ref(source), qi(&source.alias));
2867 append_filter(&mut sql, &s.filter);
2868 format!("ARRAY({})", sql)
2869 }
2870 IrArraySource::ObjectFunction(fs) => {
2871 let (exprs, _) = build_shape(&fs.shape, &fs.alias);
2872 let mut parts = vec![sql_str(&fs.type_name) + "::text"];
2873 parts.extend(exprs);
2874 let args_sql = fs.fn_args.iter().map(emit_expr).collect::<Vec<_>>().join(", ");
2875 let mut sql = format!(
2876 "SELECT (\n {}\n) FROM {}.{}({}) AS {}",
2877 parts.join(",\n "),
2878 pg_schema(&fs.fn_module),
2879 qi(&fs.fn_name),
2880 args_sql,
2881 qi(&fs.alias),
2882 );
2883 append_filter(&mut sql, &fs.filter);
2884 append_order_by(&mut sql, &fs.order_by);
2885 append_offset_limit(&mut sql, &fs.offset, &fs.limit);
2886 format!("ARRAY({})", sql)
2887 }
2888 IrArraySource::ObjectSelect(s) => {
2889 let [IrRowSource::Bound { source, shape }] = s.rows.as_slice() else {
2890 unreachable!("IrArraySource::ObjectSelect is always schema-bound")
2891 };
2892 let (exprs, _) = build_shape(shape, &source.alias);
2893 let mut parts = vec![source_type_disc(source)];
2894 parts.extend(exprs);
2895 let mut sql = format!(
2896 "SELECT (\n {}\n) FROM {} AS {}",
2897 parts.join(",\n "),
2898 source_ref(source),
2899 qi(&source.alias)
2900 );
2901 append_filter(&mut sql, &s.filter);
2902 append_order_by(&mut sql, &s.order_by);
2903 append_offset_limit(&mut sql, &s.offset, &s.limit);
2904 format!("ARRAY({})", sql)
2905 }
2906 IrArraySource::Group(grp) => format!("ARRAY({})", emit_group(grp).sql),
2907 IrArraySource::StmtColumn { stmt, column } => format!(
2908 "ARRAY(SELECT {} FROM (\n{}\n) AS \"_rows\")",
2909 qi(column),
2910 emit_dml_as_cte_source(stmt),
2911 ),
2912 IrArraySource::PathSelect(ps) => {
2913 let scalar = match &ps.result {
2914 IrPathResult::Scalar(e, _) if yields_array(e) => {
2922 let cast = match e {
2926 IrExpr::ColumnRef { pg_type, .. } => {
2927 QualifiedPgType::of(pg_type).map(|qualified| qualified.text_cast())
2928 }
2929 _ => None,
2930 };
2931 format!("ROW({}{})", emit_expr(e), cast.unwrap_or(""))
2932 }
2933 IrPathResult::Scalar(e, _) => emit_expr(e),
2934 IrPathResult::Object {
2938 alias,
2939 type_name,
2940 shape,
2941 } => {
2942 let (pointer_exprs, _) = build_shape(shape, alias);
2943 let mut parts = vec![result_type_disc(ps, alias, type_name)];
2944 parts.extend(pointer_exprs);
2945 format!("(\n {}\n)", parts.join(",\n "))
2946 }
2947 };
2948 let from_sql = emit_path_joins(&ps.root, &ps.joins);
2949 let mut sql = format!(
2950 "SELECT {}{} FROM {}",
2951 if ps.distinct { "DISTINCT " } else { "" },
2952 scalar,
2953 from_sql
2954 );
2955 append_filter(&mut sql, &ps.filter);
2956 append_order_by(&mut sql, &ps.order_by);
2957 append_offset_limit(&mut sql, &ps.offset, &ps.limit);
2958 format!("ARRAY({})", sql)
2959 }
2960 IrArraySource::RawExpr {
2961 source,
2962 poly_implementors,
2963 poly_columns,
2964 expr,
2965 } => {
2966 let from_sql = if !poly_implementors.is_empty() {
2967 format!(
2968 "(\n{}\n) AS {}",
2969 emit_poly_union(poly_implementors, poly_columns),
2970 qi(&source.alias)
2971 )
2972 } else {
2973 format!("{} AS {}", source_ref(source), qi(&source.alias))
2974 };
2975 format!("ARRAY(SELECT {} FROM {})", emit_expr(expr), from_sql)
2976 }
2977 }
2978}
2979
2980fn emit_key_expr(expr: &IrExpr) -> String {
2983 if let IrExpr::ColumnRef { alias, column, pg_type } = expr
2984 && let Some(qualified) = QualifiedPgType::of(pg_type)
2985 {
2986 let col_ref = if alias.is_empty() {
2987 qi(column)
2988 } else {
2989 format!("{}.{}", qi(alias), qi(column))
2990 };
2991 return format!("{}{}", col_ref, qualified.text_cast());
2992 }
2993 emit_expr(expr)
2994}
2995
2996fn group_rows(grp: &IrGroup) -> (String, Option<String>) {
3000 let alias = &grp.source.alias;
3001 if grp.limit.is_none() && grp.offset.is_none() {
3002 return (
3003 format!("{} AS {}", source_ref(&grp.source), qi(alias)),
3004 grp.filter.as_ref().map(emit_expr),
3005 );
3006 }
3007 let ranked = format!(
3008 "(SELECT {}.*, row_number() OVER (PARTITION BY {}{}) AS \"__rk\"\n FROM {} AS {}{}) AS {}",
3009 qi(alias),
3010 group_by_sql(grp),
3011 group_element_order(grp),
3012 source_ref(&grp.source),
3013 qi(alias),
3014 grp.filter
3015 .as_ref()
3016 .map(|f| format!("\n WHERE {}", emit_expr(f)))
3017 .unwrap_or_default(),
3018 qi(alias),
3019 );
3020 let lower = grp.offset.as_ref().map(emit_expr).unwrap_or_else(|| "0".to_string());
3021 let mut conds = vec![format!("\"__rk\" > {}", lower)];
3022 if let Some(l) = &grp.limit {
3023 conds.push(format!("\"__rk\" <= {} + {}", lower, emit_expr(l)));
3024 }
3025 (ranked, Some(conds.join(" AND ")))
3026}
3027
3028fn group_by_sql(grp: &IrGroup) -> String {
3029 grp.keys
3030 .iter()
3031 .map(|(_, key_expr)| emit_expr(key_expr))
3032 .collect::<Vec<_>>()
3033 .join(", ")
3034}
3035
3036fn group_element_order(grp: &IrGroup) -> String {
3037 if grp.order_by.is_empty() {
3038 return String::new();
3039 }
3040 let s: Vec<_> = grp.order_by.iter().map(emit_sort_clause).collect();
3041 format!(" ORDER BY {}", s.join(", "))
3042}
3043
3044fn emit_group_elements_source(grp: &IrGroup) -> String {
3047 let (from_sql, cond) = group_rows(grp);
3048 let mut sql = format!("SELECT {}.*\n FROM {}", qi(&grp.source.alias), from_sql);
3049 if let Some(cond) = cond {
3050 sql.push_str(&format!("\n WHERE {}", cond));
3051 }
3052 sql
3053}
3054
3055fn emit_group(grp: &IrGroup) -> SqlOutput {
3056 match &grp.output {
3057 IrGroupOutput::Groups => emit_group_rows(grp),
3058 IrGroupOutput::Projection(projection) => emit_group_projection(
3059 grp,
3060 &projection.pointers,
3061 &projection.order_by,
3062 &projection.offset,
3063 &projection.limit,
3064 ),
3065 IrGroupOutput::Elements => emit_group_elements(grp),
3066 }
3067}
3068
3069fn emit_group_projection(
3070 grp: &IrGroup,
3071 pointers: &[IrShapePointer],
3072 order_by: &[IrSort],
3073 offset: &Option<IrExpr>,
3074 limit: &Option<IrExpr>,
3075) -> SqlOutput {
3076 let (exprs, nodes) = build_shape(pointers, &grp.source.alias);
3077 let mut parts = vec!["NULL::text".to_string()];
3078 parts.extend(exprs);
3079 let (from_sql, cond) = group_rows(grp);
3080 let mut sql = format!(
3081 "SELECT (\n {}\n) AS \"result\"\nFROM {}",
3082 parts.join(",\n "),
3083 from_sql
3084 );
3085 if let Some(cond) = cond {
3086 sql.push_str(&format!("\nWHERE {}", cond));
3087 }
3088 sql.push_str(&format!("\nGROUP BY {}", group_by_sql(grp)));
3089 if !order_by.is_empty() {
3090 let sorts: Vec<_> = order_by.iter().map(emit_sort_clause).collect();
3091 sql.push_str(&format!("\nORDER BY {}", sorts.join(", ")));
3092 }
3093 append_offset_limit(&mut sql, offset, limit);
3094 SqlOutput {
3095 sql,
3096 shape: ShapeDescriptor {
3097 root: ShapeNode::Object {
3098 name: String::new(),
3099 type_name: None,
3100 position: 0,
3101 cardinality: Cardinality::Many,
3102 pointers: nodes,
3103 has_implicit_id: false,
3104 },
3105 },
3106 inference_plan: None,
3107 }
3108}
3109
3110fn emit_group_elements(grp: &IrGroup) -> SqlOutput {
3111 let (exprs, nodes) = build_shape(&grp.shape, &grp.source.alias);
3112 let mut parts = vec![source_type_disc(&grp.source)];
3113 parts.extend(exprs);
3114 let (from_sql, cond) = group_rows(grp);
3115 let mut sql = format!(
3116 "SELECT (\n {}\n) AS \"result\"\nFROM {}",
3117 parts.join(",\n "),
3118 from_sql
3119 );
3120 if let Some(cond) = cond {
3121 sql.push_str(&format!("\nWHERE {}", cond));
3122 }
3123 SqlOutput {
3124 sql,
3125 shape: ShapeDescriptor {
3126 root: ShapeNode::Object {
3127 name: String::new(),
3128 type_name: Some(grp.source.type_name.clone()),
3129 position: 0,
3130 cardinality: Cardinality::Many,
3131 pointers: prepend_type(nodes),
3132 has_implicit_id: shape_has_implicit_id(&grp.shape),
3133 },
3134 },
3135 inference_plan: None,
3136 }
3137}
3138
3139fn emit_group_rows(grp: &IrGroup) -> SqlOutput {
3140 let alias = &grp.source.alias;
3141 let (shape_exprs, shape_nodes) = build_shape(&grp.shape, alias);
3142
3143 let mut elem_row_parts = vec![source_type_disc(&grp.source)];
3145 elem_row_parts.extend(shape_exprs);
3146 let elem_row = elem_row_parts.join(",\n ");
3147
3148 let n_keys = grp.keys.len();
3150 let grouping_pos = n_keys + 1;
3151 let elements_pos = n_keys + 2;
3152
3153 let mut key_exprs_sql: Vec<String> = vec![];
3155 let mut key_nodes: Vec<ShapeNode> = vec![];
3156 for (i, (key_name, key_expr)) in grp.keys.iter().enumerate() {
3157 let pos = i + 1;
3158 if let IrExpr::ColumnRef { pg_type, .. } = key_expr
3159 && let Some(qualified) = QualifiedPgType::of(pg_type)
3160 {
3161 key_exprs_sql.push(emit_key_expr(key_expr));
3162 key_nodes.push(qualified.shape_node(key_name.clone(), pos));
3163 continue;
3164 }
3165 key_exprs_sql.push(emit_expr(key_expr));
3166 key_nodes.push(ShapeNode::Scalar {
3167 name: key_name.clone(),
3168 position: pos,
3169 });
3170 }
3171
3172 let mut outer_parts = vec!["NULL::text".to_string()];
3174 outer_parts.extend(key_exprs_sql.clone());
3175 let key_names_sql = grp
3176 .keys
3177 .iter()
3178 .map(|(name, _)| format!("'{}'", name))
3179 .collect::<Vec<_>>()
3180 .join(", ");
3181 outer_parts.push(format!("ARRAY[{}]::text[]", key_names_sql));
3182 outer_parts.push(format!(
3183 "array_agg(ROW(\n {}\n )::record{})",
3184 elem_row,
3185 group_element_order(grp)
3186 ));
3187
3188 let outer_tuple = outer_parts.join(",\n ");
3189 let (from_sql, cond) = group_rows(grp);
3190 let mut sql = format!("SELECT (\n {}\n) AS \"result\"\nFROM {}", outer_tuple, from_sql,);
3191 if let Some(cond) = cond {
3192 sql.push_str(&format!("\nWHERE {}", cond));
3193 }
3194 sql.push_str(&format!("\nGROUP BY {}", group_by_sql(grp)));
3195
3196 let element_node = ShapeNode::Object {
3198 name: String::new(),
3199 type_name: Some(grp.source.type_name.clone()),
3200 position: 0,
3201 cardinality: Cardinality::Many,
3202 pointers: prepend_type(shape_nodes),
3203 has_implicit_id: shape_has_implicit_id(&grp.shape),
3204 };
3205
3206 let root = ShapeNode::Group {
3207 key_nodes,
3208 grouping_position: grouping_pos,
3209 elements_position: elements_pos,
3210 element: Box::new(element_node),
3211 };
3212
3213 SqlOutput {
3214 sql,
3215 shape: ShapeDescriptor { root },
3216 inference_plan: None,
3217 }
3218}
3219
3220fn emit_poly_union_type_only(implementors: &[IrPolyImplementor]) -> String {
3221 implementors
3222 .iter()
3223 .map(|imp| {
3224 format!(
3225 " SELECT {}::text AS \"__type__\" FROM {}",
3226 sql_str(&imp.type_name),
3227 qn(&imp.module, &imp.table),
3228 )
3229 })
3230 .collect::<Vec<_>>()
3231 .join("\n UNION ALL\n")
3232}
3233
3234fn emit_path_select(sel: &IrPathSelect) -> SqlOutput {
3235 let distinct = if sel.distinct { "DISTINCT " } else { "" };
3236 let from_sql = if !sel.poly_implementors.is_empty() {
3237 format!(
3238 "(\n{}\n) AS {}",
3239 emit_poly_union_type_only(&sel.poly_implementors),
3240 qi(&sel.root.alias),
3241 )
3242 } else {
3243 emit_path_joins(&sel.root, &sel.joins)
3244 };
3245
3246 let (result_expr, shape_root) = match &sel.result {
3247 IrPathResult::Scalar(ir_expr, tuple_shape) => {
3248 let is_nt = matches!(ir_expr, IrExpr::NamedTuple { .. })
3250 || matches!(ir_expr, IrExpr::Tuple(_))
3251 || matches!(ir_expr, IrExpr::JsonbField { .. })
3252 || matches!(ir_expr, IrExpr::JsonbIndex { .. })
3253 || matches!(ir_expr, IrExpr::ColumnRef { pg_type, .. } if pg_type.starts_with("__nt__:"))
3254 || tuple_shape.is_some();
3255 let array_of_tuples = matches!(ir_expr, IrExpr::ColumnRef { pg_type, .. } if pg_type.ends_with("[]"));
3259 if let Some(shape) = tuple_shape.as_ref().filter(|_| array_of_tuples) {
3260 let node = ShapeNode::Array {
3261 name: String::new(),
3262 position: 0,
3263 element: Box::new(ShapeNode::NamedTuple {
3264 name: String::new(),
3265 position: 0,
3266 type_name: shape.type_name.clone(),
3267 members: Some(shape.members.clone()),
3268 is_free_object: false,
3269 }),
3270 };
3271 (format!("ROW({}) AS result", emit_expr(ir_expr)), node)
3272 } else if is_nt {
3273 let expr_sql = format!("{} AS result", emit_expr(ir_expr));
3274 let shape = if matches!(
3275 ir_expr,
3276 IrExpr::JsonbField { .. } | IrExpr::JsonbIndex { .. } | IrExpr::Tuple(_)
3277 ) {
3278 ShapeNode::RawScalar
3279 } else if let Some(shape) = tuple_shape {
3280 ShapeNode::NamedTuple {
3286 name: String::new(),
3287 position: 0,
3288 type_name: shape.type_name.clone(),
3289 members: Some(shape.members.clone()),
3290 is_free_object: false,
3291 }
3292 } else {
3293 let type_name = match ir_expr {
3294 IrExpr::ColumnRef { pg_type, .. } => pg_type.strip_prefix("__nt__:").map(|s| s.to_string()),
3295 _ => None,
3296 };
3297 ShapeNode::NamedTuple {
3298 name: String::new(),
3299 position: 0,
3300 type_name,
3301 members: None,
3302 is_free_object: false,
3303 }
3304 };
3305 (expr_sql, shape)
3306 } else {
3307 if let IrExpr::ColumnRef { pg_type, .. } = ir_expr {
3310 if let Some(qualified) = QualifiedPgType::of(pg_type) {
3311 let expr = format!("ROW({}{}) AS result", emit_expr(ir_expr), qualified.text_cast());
3312 let shape = qualified.shape_node(String::new(), 0);
3313 (expr, shape)
3314 } else {
3315 let expr = format!("ROW({}) AS result", emit_expr(ir_expr));
3316 (
3317 expr,
3318 ShapeNode::Scalar {
3319 name: String::new(),
3320 position: 0,
3321 },
3322 )
3323 }
3324 } else {
3325 let expr = format!("ROW({}) AS result", emit_expr(ir_expr));
3326 (
3327 expr,
3328 ShapeNode::Scalar {
3329 name: String::new(),
3330 position: 0,
3331 },
3332 )
3333 }
3334 }
3335 }
3336 IrPathResult::Object {
3337 alias,
3338 type_name,
3339 shape,
3340 } => {
3341 let (pointer_exprs, pointer_nodes) = build_shape(shape, alias);
3342 let mut parts = vec![result_type_disc(sel, alias, type_name)];
3343 parts.extend(pointer_exprs);
3344 let expr = format!("(\n {}\n) AS result", parts.join(",\n "));
3345 let shape_root = ShapeNode::Object {
3346 name: String::new(),
3347 type_name: Some(type_name.clone()),
3348 position: 0,
3349 cardinality: Cardinality::Many,
3350 pointers: prepend_type(pointer_nodes),
3351 has_implicit_id: shape_has_implicit_id(shape),
3352 };
3353 (expr, shape_root)
3354 }
3355 };
3356
3357 let mut sql = format!("SELECT {}{}\nFROM {}", distinct, result_expr, from_sql);
3358 append_filter(&mut sql, &sel.filter);
3359 append_order_by(&mut sql, &sel.order_by);
3360 append_offset_limit(&mut sql, &sel.offset, &sel.limit);
3361
3362 SqlOutput {
3363 sql,
3364 shape: ShapeDescriptor { root: shape_root },
3365 inference_plan: None,
3366 }
3367}
3368
3369fn yields_jsonb(expr: &IrExpr) -> bool {
3376 match expr {
3377 IrExpr::JsonbField { .. } | IrExpr::JsonbIndex { .. } | IrExpr::NamedTuple { .. } => true,
3378 IrExpr::TypeCast(c) => c.pg_type == "jsonb",
3379 IrExpr::ColumnRef { pg_type, .. } | IrExpr::FnParam { pg_type, .. } => pg_type == "jsonb",
3380 IrExpr::CteRef { pg_type, .. } | IrExpr::ForVar { pg_type, .. } => pg_type.as_deref() == Some("jsonb"),
3381 IrExpr::FunctionCall(f) if f.return_pg_type.is_some() => f.return_pg_type.as_deref() == Some("jsonb"),
3385 IrExpr::FunctionCall(f) if f.schema.is_none() => {
3386 let mut overloads = crate::stdlib::registry().iter().filter(|d| d.name == f.name).peekable();
3387 overloads.peek().is_some() && overloads.all(|d| matches!(d.return_type, crate::stdlib::PylonType::Json))
3388 }
3389 _ => false,
3390 }
3391}
3392
3393fn yields_array(expr: &IrExpr) -> bool {
3396 match expr {
3397 IrExpr::Array(_) | IrExpr::ArrayFromSelect(_) => true,
3398 IrExpr::ColumnRef { pg_type, .. } | IrExpr::FnParam { pg_type, .. } => pg_type.ends_with("[]"),
3399 IrExpr::CteRef { pg_type, .. } | IrExpr::ForVar { pg_type, .. } => {
3400 pg_type.as_deref().is_some_and(|ty| ty.ends_with("[]"))
3401 }
3402 IrExpr::TypeCast(cast) => cast.pg_type.ends_with("[]"),
3403 _ => false,
3404 }
3405}
3406
3407fn aggregated_array_element(expr: &IrExpr, tuple_shape: Option<&crate::ir::TupleCastShape>) -> ShapeNode {
3414 if let Some(shape) = tuple_shape {
3415 return ShapeNode::Array {
3416 name: String::new(),
3417 position: 0,
3418 element: Box::new(ShapeNode::NamedTuple {
3419 name: String::new(),
3420 position: 0,
3421 type_name: shape.type_name.clone(),
3422 members: Some(shape.members.clone()),
3423 is_free_object: false,
3424 }),
3425 };
3426 }
3427 if let IrExpr::ColumnRef { pg_type, .. } = expr
3428 && let Some(qualified) = QualifiedPgType::of(pg_type)
3429 {
3430 return qualified.shape_node(String::new(), 0);
3431 }
3432 ShapeNode::Scalar {
3433 name: String::new(),
3434 position: 0,
3435 }
3436}
3437
3438fn emit_for_iterator(it: &IrForIterator, iter_alias: &str) -> (String, String) {
3439 match it {
3440 IrForIterator::Values { exprs, pg_type } => {
3441 let rows: Vec<String> = exprs
3442 .iter()
3443 .map(|e| format!("({}::{})", emit_expr(e), pg_type))
3444 .collect();
3445 (
3446 format!("(VALUES {}) AS {}(\"v\")", rows.join(", "), qi(iter_alias)),
3447 format!("{}(\"v\") AS (VALUES {})", qi(iter_alias), rows.join(", ")),
3448 )
3449 }
3450 IrForIterator::Query { stmt, scalar } => {
3451 let inner = emit_dml_as_cte_source(stmt);
3452 let body = if *scalar {
3456 inner
3457 } else {
3458 format!(" SELECT \"id\" AS v FROM (\n{}\n ) AS _src", inner)
3459 };
3460 (
3461 format!("(\n{}\n) AS {}", body, qi(iter_alias)),
3462 format!("{} AS (\n{}\n)", qi(iter_alias), body),
3463 )
3464 }
3465 IrForIterator::SetReturning { expr, pg_type } => {
3466 let body = format!(" SELECT {}::{} AS v", emit_expr(expr), pg_type);
3467 (
3468 format!("(\n{}\n) AS {}", body, qi(iter_alias)),
3469 format!("{} AS (\n{}\n)", qi(iter_alias), body),
3470 )
3471 }
3472 }
3473}
3474
3475fn emit_nested_for_iterator(it: &IrForIterator, iter_alias: &str, outer_alias: &str) -> String {
3480 const OUTER: &str = "_outer";
3481 match it {
3482 IrForIterator::Values { exprs, pg_type } => {
3483 let rows: Vec<String> = exprs
3484 .iter()
3485 .map(|e| format!("({}::{})", emit_expr(e), pg_type))
3486 .collect();
3487 format!(
3488 "{} AS (\nSELECT {}.\"v\" AS {}, \"_vals\".\"v\" AS v\nFROM {}, (VALUES {}) AS \"_vals\"(\"v\")\n)",
3489 qi(iter_alias),
3490 qi(outer_alias),
3491 qi(OUTER),
3492 qi(outer_alias),
3493 rows.join(", "),
3494 )
3495 }
3496 IrForIterator::Query { stmt, scalar } => {
3497 let inner = emit_dml_as_cte_source(stmt);
3498 let projected = if *scalar { "\"_src\".\"v\"" } else { "\"_src\".\"id\"" };
3499 format!(
3500 "{} AS (\nSELECT {}.\"v\" AS {}, {} AS v\nFROM {}\nCROSS JOIN LATERAL (\n{}\n) AS \"_src\"\n)",
3501 qi(iter_alias),
3502 qi(outer_alias),
3503 qi(OUTER),
3504 projected,
3505 qi(outer_alias),
3506 inner,
3507 )
3508 }
3509 IrForIterator::SetReturning { expr, pg_type } => format!(
3510 "{} AS (\nSELECT {}.\"v\" AS {}, \"_vals\".\"v\" AS v\nFROM {}, LATERAL (SELECT {}::{} AS v) AS \"_vals\"\n)",
3511 qi(iter_alias),
3512 qi(outer_alias),
3513 qi(OUTER),
3514 qi(outer_alias),
3515 emit_expr(expr),
3516 pg_type,
3517 ),
3518 }
3519}
3520
3521fn nested_for_from(inner_alias: &str, outer_alias: &str) -> String {
3524 format!(
3525 "{} JOIN {} ON {}.\"v\" = {}.\"_outer\"",
3526 qi(inner_alias),
3527 qi(outer_alias),
3528 qi(outer_alias),
3529 qi(inner_alias),
3530 )
3531}
3532
3533fn uncorrelated_body_ctes(f: &IrFor) -> Vec<IrCteDef> {
3547 f.body_ctes
3548 .iter()
3549 .map(|cte| IrCteDef {
3550 correlated_to: None,
3551 ..cte.clone()
3552 })
3553 .collect()
3554}
3555
3556fn prefix_body_ctes(sql: String, body_ctes: &[IrCteDef]) -> String {
3557 if body_ctes.is_empty() {
3558 return sql;
3559 }
3560 merge_into_existing_with(&sql, &emit_user_cte_parts(body_ctes))
3561 .unwrap_or_else(|| format!("{}{}", emit_cte_prefix(body_ctes), sql))
3562}
3563
3564fn emit_for_stmt(f: &IrFor, user_ctes: &[IrCteDef]) -> SqlOutput {
3565 let iter_alias = format!("_for_{}", f.var_name);
3566
3567 if matches!(&f.iterator, IrForIterator::Values { exprs, .. } if exprs.is_empty()) {
3568 let empty = SqlOutput {
3569 sql: "SELECT NULL AS result WHERE FALSE".to_string(),
3570 shape: ShapeDescriptor {
3571 root: ShapeNode::Scalar {
3572 name: String::new(),
3573 position: 0,
3574 },
3575 },
3576 inference_plan: None,
3577 };
3578 return empty;
3579 }
3580
3581 let (values_from, iter_cte) = emit_for_iterator(&f.iterator, &iter_alias);
3582
3583 match f.body.as_ref() {
3584 IrStmt::Insert(ins) => emit_for_insert(ins, &iter_alias, &iter_cte, user_ctes, &f.body_ctes),
3585 IrStmt::Update(upd) => emit_for_update(upd, &iter_alias, &iter_cte, user_ctes, &f.body_ctes),
3586 body => {
3587 let body_out = match body {
3588 IrStmt::Select(sel) => emit_select_stmt(sel, user_ctes),
3589 IrStmt::PathSelect(sel) => emit_path_select(sel),
3590 other => unreachable!("for-loop body should have been rejected at compile time: {other:?}"),
3593 };
3594 let indent_body = prefix_body_ctes(body_out.sql, &uncorrelated_body_ctes(f)).replace('\n', "\n ");
3595 let cte_prefix = if !user_ctes.is_empty() {
3596 emit_cte_prefix(user_ctes)
3597 } else {
3598 String::new()
3599 };
3600 let sql = format!(
3601 "{}SELECT \"_body\".result\nFROM {}\nCROSS JOIN LATERAL (\n {}\n) AS \"_body\"",
3602 cte_prefix, values_from, indent_body,
3603 );
3604 SqlOutput {
3605 sql,
3606 shape: body_out.shape,
3607 inference_plan: None,
3608 }
3609 }
3610 }
3611}
3612
3613fn emit_for_update(
3618 upd: &IrUpdate,
3619 iter_alias: &str,
3620 iter_cte: &str,
3621 user_ctes: &[IrCteDef],
3622 body_ctes: &[IrCteDef],
3623) -> SqlOutput {
3624 let mut cte_parts: Vec<String> = emit_user_cte_parts(user_ctes);
3625 cte_parts.push(iter_cte.to_string());
3628 cte_parts.extend(emit_user_cte_parts(body_ctes));
3629
3630 let alias = &upd.target.alias;
3631 if upd.assignments.is_empty() && upd.rewrites.is_empty() && !upd.multi_link_appends.is_empty() {
3635 const ITER_COL: &str = "_iter";
3636 let ids_name = "_ids";
3637 let generated: Vec<Option<&IrInsert>> = upd
3641 .multi_link_appends
3642 .iter()
3643 .map(|a| per_iteration_insert(a, &upd.nested_ctes))
3644 .collect();
3645 let new_cols: String = generated
3646 .iter()
3647 .enumerate()
3648 .filter_map(|(i, ins)| {
3649 ins.map(|ins| {
3650 format!(
3651 ", {} AS {}",
3652 ins.id_default_sql.as_deref().unwrap_or("uuidv7()"),
3653 qi(&format!("_new_{i}"))
3654 )
3655 })
3656 })
3657 .collect();
3658 let mut ids_sql = format!(
3659 "\"{}\" AS (\nSELECT {}.*, {}.\"v\" AS {}{} FROM {} AS {}, {}",
3660 ids_name,
3661 qi(alias),
3662 qi(iter_alias),
3663 qi(ITER_COL),
3664 new_cols,
3665 target_ref(&upd.target),
3666 qi(alias),
3667 qi(iter_alias),
3668 );
3669 append_filter(&mut ids_sql, &upd.filter);
3670 ids_sql.push_str("\n)");
3671 cte_parts.push(ids_sql);
3672 for (i, append) in upd.multi_link_appends.iter().enumerate() {
3673 match generated[i] {
3674 Some(ins) => {
3675 cte_parts.push(emit_for_nested_insert_cte(
3676 ins,
3677 ids_name,
3678 iter_alias,
3679 &format!("_ml_ins_{i}"),
3680 &format!("_new_{i}"),
3681 ));
3682 cte_parts.push(emit_for_ml_append_cte(
3683 append,
3684 ids_name,
3685 &format!("_ml_add_{i}"),
3686 &format!("_new_{i}"),
3687 ));
3688 }
3689 None => cte_parts.push(emit_for_ml_append_cte(
3690 append,
3691 ids_name,
3692 &format!("_ml_add_{i}"),
3693 ITER_COL,
3694 )),
3695 }
3696 }
3697 let (pointer_exprs, shape_nodes) = build_shape(&upd.returning, ids_name);
3698 let mut parts = vec![type_disc(&upd.target.type_name)];
3699 parts.extend(pointer_exprs);
3700 let sql = format!(
3701 "WITH {}\nSELECT (\n {}\n) AS result\nFROM \"{}\"",
3702 cte_parts.join(",\n"),
3703 parts.join(",\n "),
3704 ids_name,
3705 );
3706 return SqlOutput {
3707 sql,
3708 shape: crate::query::ShapeDescriptor {
3709 root: ShapeNode::Object {
3710 name: String::new(),
3711 type_name: Some(upd.target.type_name.clone()),
3712 position: 0,
3713 cardinality: Cardinality::Many,
3714 pointers: prepend_type(shape_nodes),
3715 has_implicit_id: shape_has_implicit_id(&upd.returning),
3716 },
3717 },
3718 inference_plan: None,
3719 };
3720 }
3721 let sets: Vec<String> = upd
3722 .assignments
3723 .iter()
3724 .map(|(col, expr)| format!("{} = {}", qi(col), emit_expr(expr)))
3725 .chain(
3726 upd.rewrites
3727 .iter()
3728 .map(|rw| format!("{} = {}", qi(&rw.column), emit_expr(&rw.expr))),
3729 )
3730 .collect();
3731 if !upd.poly_implementors.is_empty() {
3732 return emit_for_poly_update(upd, iter_alias, &sets, cte_parts);
3733 }
3734 let mut sql = format!(
3735 "WITH {}\nUPDATE {} AS {}\nSET {}\nFROM {}",
3736 cte_parts.join(",\n"),
3737 target_ref(&upd.target),
3738 qi(alias),
3739 sets.join(", "),
3740 qi(iter_alias),
3741 );
3742 append_filter(&mut sql, &upd.filter);
3743 let mut parts = vec![type_disc(&upd.target.type_name)];
3744 let (pointer_exprs, shape_nodes) = build_shape(&upd.returning, alias);
3745 parts.extend(pointer_exprs);
3746 sql.push_str(&format!("\nRETURNING (\n {}\n) AS result", parts.join(",\n ")));
3747 SqlOutput {
3748 sql,
3749 shape: crate::query::ShapeDescriptor {
3750 root: ShapeNode::Object {
3751 name: String::new(),
3752 type_name: Some(upd.target.type_name.clone()),
3753 position: 0,
3754 cardinality: Cardinality::Many,
3755 pointers: prepend_type(shape_nodes),
3756 has_implicit_id: shape_has_implicit_id(&upd.returning),
3757 },
3758 },
3759 inference_plan: None,
3760 }
3761}
3762
3763const OUTER_KEY: &str = "_outer";
3768
3769fn binding_outer_key(cte_name: &str) -> String {
3774 format!("_outer__{cte_name}")
3775}
3776
3777fn emit_iteration_rows_cte(ins: &IrInsert, cte_name: &str, iter_alias: &str, outer_source: &str) -> String {
3782 let rewrite_cols: std::collections::HashSet<&str> = ins.rewrites.iter().map(|r| r.column.as_str()).collect();
3783 let mut selected = vec![
3784 format!(
3785 "{} AS {}",
3786 ins.id_default_sql.as_deref().unwrap_or("uuidv7()"),
3787 qi("id")
3788 ),
3789 format!("{} AS {}", outer_source, qi(OUTER_KEY)),
3790 ];
3791 selected.extend(
3792 ins.assignments
3793 .iter()
3794 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
3795 .map(|(c, e)| format!("{} AS {}", emit_expr(e), qi(c)))
3796 .chain(
3797 ins.rewrites
3798 .iter()
3799 .map(|r| format!("{} AS {}", emit_expr(&r.expr), qi(&r.column))),
3800 ),
3801 );
3802 let mut sql = format!(
3803 "\"{}\" AS (\nSELECT {}\nFROM {}",
3804 cte_name,
3805 selected.join(", "),
3806 qi(iter_alias),
3807 );
3808 append_insert_guard(&mut sql, ins);
3809 sql.push_str("\n)");
3810 sql
3811}
3812
3813fn emit_insert_from_rows_cte(ins: &IrInsert, cte_name: &str, rows_name: &str) -> String {
3815 let rewrite_cols: std::collections::HashSet<&str> = ins.rewrites.iter().map(|r| r.column.as_str()).collect();
3816 let cols: Vec<String> = std::iter::once(qi("id"))
3817 .chain(
3818 ins.assignments
3819 .iter()
3820 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
3821 .map(|(c, _)| qi(c)),
3822 )
3823 .chain(ins.rewrites.iter().map(|r| qi(&r.column)))
3824 .collect();
3825 let mut sql = format!(
3826 "INSERT INTO {} ({})\nSELECT {} FROM \"{}\"",
3827 target_ref(&ins.target),
3828 cols.join(", "),
3829 cols.join(", "),
3830 rows_name,
3831 );
3832 if let Some(conflict) = &ins.unless_conflict {
3833 emit_conflict(&mut sql, conflict);
3834 }
3835 sql.push_str("\nRETURNING *");
3836 format!("\"{}\" AS (\n{}\n)", cte_name, sql)
3837}
3838
3839fn emit_correlated_ml_append_cte(
3843 mutation: &IrMultiLinkMutation,
3844 rows_name: &str,
3845 targets_name: &str,
3846 cte_name: &str,
3847) -> String {
3848 format!(
3849 "\"{}\" AS (\nINSERT INTO {} ({}, {})\nSELECT \"_src\".{}, \"_tgt\".{} FROM \"{}\" AS \"_src\" JOIN \"{}\" AS \"_tgt\" ON \"_tgt\".{} = \"_src\".{}\nON CONFLICT DO NOTHING\nRETURNING {}, {}\n)",
3850 cte_name,
3851 qn(&mutation.module, &mutation.junction_table),
3852 qi(&mutation.source_col),
3853 qi(&mutation.target_col),
3854 qi("id"),
3855 qi("id"),
3856 rows_name,
3857 targets_name,
3858 qi(OUTER_KEY),
3859 qi(OUTER_KEY),
3860 qi(&mutation.source_col),
3861 qi(&mutation.target_col),
3862 )
3863}
3864
3865fn correlated_append_indices(appends: &[IrMultiLinkMutation], defs: &[&[IrCteDef]]) -> Vec<usize> {
3868 appends
3869 .iter()
3870 .enumerate()
3871 .filter(|(_, append)| match &append.values.source {
3872 IrMultiLinkValueSource::CteRef(target) => defs
3873 .iter()
3874 .flat_map(|group| group.iter())
3875 .any(|c| &c.name == target && c.correlated_to.is_some()),
3876 _ => false,
3877 })
3878 .map(|(i, _)| i)
3879 .collect()
3880}
3881
3882fn emit_for_poly_update(upd: &IrUpdate, iter_alias: &str, sets: &[String], mut cte_parts: Vec<String>) -> SqlOutput {
3886 let alias = &upd.target.alias;
3887 let mut union_parts = vec![];
3888 for (i, imp) in upd.poly_implementors.iter().enumerate() {
3889 let cte_name = format!("_u{}", i);
3890 let mut upd_sql = format!(
3891 "UPDATE {} AS {}\nSET {}\nFROM {}",
3892 qn(&imp.module, &imp.table),
3893 qi(alias),
3894 sets.join(", "),
3895 qi(iter_alias),
3896 );
3897 append_filter(&mut upd_sql, &upd.filter);
3898 upd_sql.push_str(&format!("\nRETURNING {}.\"id\"", qi(alias)));
3899 cte_parts.push(format!("\"{}\" AS (\n{}\n)", cte_name, upd_sql));
3900
3901 let r_alias = format!("_r{}", i);
3902 union_parts.push(format!(
3903 "SELECT ROW({}::text, {}.\"id\") AS result FROM \"{}\" AS {}",
3904 sql_str(&imp.type_name),
3905 qi(&r_alias),
3906 cte_name,
3907 qi(&r_alias),
3908 ));
3909 }
3910 let sql = format!("WITH {}\n{}", cte_parts.join(",\n"), union_parts.join("\nUNION ALL\n"));
3911 let (shape, _) = emit_returning_shape(&upd.target, &upd.returning, true);
3912 SqlOutput {
3913 sql,
3914 shape,
3915 inference_plan: None,
3916 }
3917}
3918
3919fn emit_for_insert(
3920 ins: &IrInsert,
3921 iter_alias: &str,
3922 iter_cte: &str,
3923 user_ctes: &[IrCteDef],
3924 body_ctes: &[IrCteDef],
3925) -> SqlOutput {
3926 let rewrite_cols: std::collections::HashSet<&str> = ins.rewrites.iter().map(|r| r.column.as_str()).collect();
3927
3928 let cols: Vec<String> = ins
3929 .assignments
3930 .iter()
3931 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
3932 .map(|(c, _)| qi(c))
3933 .chain(ins.rewrites.iter().map(|r| qi(&r.column)))
3934 .collect();
3935 let sel_exprs: Vec<String> = ins
3936 .assignments
3937 .iter()
3938 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
3939 .map(|(_, e)| emit_expr(e))
3940 .chain(ins.rewrites.iter().map(|r| emit_expr(&r.expr)))
3941 .collect();
3942
3943 let mut cte_parts: Vec<String> = emit_user_cte_parts(user_ctes);
3944 cte_parts.push(iter_cte.to_string());
3948 cte_parts.extend(emit_user_cte_parts(body_ctes));
3949 cte_parts.extend(emit_user_cte_parts(&ins.nested_ctes));
3950 let correlated_targets = correlated_append_indices(&ins.multi_link_appends, &[body_ctes, &ins.nested_ctes]);
3953
3954 let mut insert_sql = format!(
3955 "INSERT INTO {} ({})\nSELECT {} FROM {}",
3956 target_ref(&ins.target),
3957 cols.join(", "),
3958 sel_exprs.join(", "),
3959 qi(iter_alias),
3960 );
3961 append_insert_guard(&mut insert_sql, ins);
3962 if let Some(conflict) = &ins.unless_conflict {
3963 emit_conflict(&mut insert_sql, conflict);
3964 }
3965 let (shape, returning_sql) = emit_returning_shape(&ins.target, &ins.returning, false);
3966
3967 if ins.multi_link_appends.is_empty() {
3968 let mut sql = format!("WITH {}\n{}", cte_parts.join(",\n"), insert_sql);
3969 if let Some(r) = returning_sql {
3970 sql.push_str(&r);
3971 }
3972 return SqlOutput {
3973 sql,
3974 shape,
3975 inference_plan: None,
3976 };
3977 }
3978
3979 const IDS: &str = "_for_dml__ids";
3982 const ROWS: &str = "_for_dml__rows";
3983 if correlated_targets.is_empty() {
3984 insert_sql.push_str("\nRETURNING *");
3985 cte_parts.push(format!("\"{}\" AS (\n{}\n)", IDS, insert_sql));
3986 } else {
3987 let outer_source = format!("{}.{}", qi(iter_alias), qi("v"));
3988 cte_parts.push(emit_iteration_rows_cte(ins, ROWS, iter_alias, &outer_source));
3989 cte_parts.push(emit_insert_from_rows_cte(ins, IDS, ROWS));
3990 }
3991 for (i, append) in ins.multi_link_appends.iter().enumerate() {
3992 let cte_name = format!("_for_dml__ml_add_{}", i);
3993 if correlated_targets.contains(&i) {
3994 let IrMultiLinkValueSource::CteRef(target) = &append.values.source else {
3995 unreachable!("only a CTE-backed value can carry an iteration key")
3996 };
3997 cte_parts.push(emit_correlated_ml_append_cte(append, ROWS, target, &cte_name));
3998 } else {
3999 cte_parts.push(emit_ml_append_cte(append, IDS, &ins.target.alias, &cte_name));
4000 }
4001 }
4002
4003 let projection = match &returning_sql {
4004 Some(r) => r.trim_start_matches('\n').replacen("RETURNING ", "SELECT ", 1),
4007 None => "SELECT 1".to_string(),
4008 };
4009 SqlOutput {
4010 sql: format!("WITH {}\n{}\nFROM \"{}\"", cte_parts.join(",\n"), projection, IDS),
4011 shape,
4012 inference_plan: None,
4013 }
4014}
4015
4016fn enqueue_cte_sql(eq: &VectorEnqueueInfo, source_cte: &str, cte_name: &str) -> String {
4022 let index_name_sql = match &eq.index_name {
4023 None => "NULL".to_string(),
4024 Some(name) => sql_str(name),
4025 };
4026 format!(
4027 concat!(
4028 "\"{}\" AS (\n",
4029 " INSERT INTO _pylon.\"IndexOutbox\"\n",
4030 " (object_id, type_name, index_kind, index_name)\n",
4031 " SELECT \"id\", {}, 'Vector'::_pylon.\"IndexKind\", {}\n",
4032 " FROM \"{}\"\n",
4033 " ON CONFLICT (object_id, index_kind, index_name)\n",
4034 " DO UPDATE SET status = 'Pending', enqueued_at = now()\n",
4035 ")",
4036 ),
4037 cte_name,
4038 sql_str(&eq.type_name),
4039 index_name_sql,
4040 source_cte,
4041 )
4042}
4043
4044fn enqueue_ctes(enqueue: &[VectorEnqueueInfo], source_cte: &str) -> Vec<String> {
4046 enqueue
4047 .iter()
4048 .enumerate()
4049 .map(|(i, eq)| enqueue_cte_sql(eq, source_cte, &format!("_eq{}", i)))
4050 .collect()
4051}
4052
4053fn search_backend_index_kind(backend: &crate::schema::SearchBackend) -> &'static str {
4057 match backend {
4058 crate::schema::SearchBackend::OpenSearch => "OpenSearch",
4059 crate::schema::SearchBackend::Meilisearch => "Meilisearch",
4060 crate::schema::SearchBackend::Postgres => {
4061 unreachable!("Postgres-backed search indexes are never collected into SearchEnqueueInfo")
4062 }
4063 }
4064}
4065
4066fn enqueue_search_cte_sql(eq: &SearchEnqueueInfo, source_cte: &str, cte_name: &str) -> String {
4068 let index_name_sql = match &eq.index_name {
4069 None => "NULL".to_string(),
4070 Some(name) => sql_str(name),
4071 };
4072 format!(
4073 concat!(
4074 "\"{}\" AS (\n",
4075 " INSERT INTO _pylon.\"IndexOutbox\"\n",
4076 " (object_id, type_name, index_kind, index_name, operation)\n",
4077 " SELECT \"id\", {}, '{}'::_pylon.\"IndexKind\", {}, {}\n",
4078 " FROM \"{}\"\n",
4079 " ON CONFLICT (object_id, index_kind, index_name)\n",
4080 " DO UPDATE SET status = 'Pending', operation = EXCLUDED.operation, enqueued_at = now()\n",
4081 ")",
4082 ),
4083 cte_name,
4084 sql_str(&eq.type_name),
4085 search_backend_index_kind(&eq.backend),
4086 index_name_sql,
4087 sql_str(eq.operation),
4088 source_cte,
4089 )
4090}
4091
4092fn enqueue_search_ctes(enqueue: &[SearchEnqueueInfo], source_cte: &str, offset: usize) -> Vec<String> {
4094 enqueue
4095 .iter()
4096 .enumerate()
4097 .map(|(i, eq)| enqueue_search_cte_sql(eq, source_cte, &format!("_es{}", offset + i)))
4098 .collect()
4099}
4100
4101fn shape_select_from_cte(
4104 target: &IrSource,
4105 returning: &[IrShapePointer],
4106 cte_name: &str,
4107) -> (ShapeDescriptor, Option<String>) {
4108 if returning.is_empty() {
4109 return (
4110 ShapeDescriptor {
4111 root: ShapeNode::Scalar {
4112 name: String::new(),
4113 position: 0,
4114 },
4115 },
4116 None,
4117 );
4118 }
4119 let (pointer_exprs, shape_pointers) = build_shape(returning, "");
4120 let mut parts = vec![type_disc(&target.type_name)];
4121 parts.extend(pointer_exprs);
4122 let tuple = parts.join(",\n ");
4123 let sql = format!("SELECT (\n {}\n) AS result\nFROM {}", tuple, qi(cte_name));
4124 let root_pointers = prepend_type(shape_pointers);
4125 let shape = ShapeDescriptor {
4126 root: ShapeNode::Object {
4127 name: String::new(),
4128 type_name: Some(target.type_name.clone()),
4129 position: 0,
4130 cardinality: Cardinality::Required,
4131 pointers: root_pointers,
4132 has_implicit_id: shape_has_implicit_id(returning),
4133 },
4134 };
4135 (shape, Some(sql))
4136}
4137
4138fn emit_insert_row_sql(ins: &IrInsert) -> String {
4147 let rewrite_cols: std::collections::HashSet<&str> = ins.rewrites.iter().map(|r| r.column.as_str()).collect();
4148 let cols: Vec<String> = ins
4149 .assignments
4150 .iter()
4151 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
4152 .map(|(c, _)| qi(c))
4153 .chain(ins.rewrites.iter().map(|r| qi(&r.column)))
4154 .collect();
4155 let vals: Vec<String> = ins
4156 .assignments
4157 .iter()
4158 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
4159 .map(|(_, e)| emit_expr(e))
4160 .chain(ins.rewrites.iter().map(|r| emit_expr(&r.expr)))
4161 .collect();
4162 if let Some(guard) = &ins.guard {
4165 let from_ctes = nested_cte_from(&ins.nested_ctes, &vals.join(","), " ");
4166 let projection = if cols.is_empty() {
4167 String::new()
4168 } else {
4169 format!(" {}", vals.join(", "))
4170 };
4171 let column_list = if cols.is_empty() {
4172 String::new()
4173 } else {
4174 format!(" ({})", cols.join(", "))
4175 };
4176 return format!(
4177 "INSERT INTO {}{} SELECT{}{} WHERE {}",
4178 target_ref(&ins.target),
4179 column_list,
4180 projection,
4181 from_ctes,
4182 emit_expr(guard),
4183 );
4184 }
4185 if cols.is_empty() && ins.nested_ctes.is_empty() {
4186 format!("INSERT INTO {} DEFAULT VALUES", target_ref(&ins.target))
4189 } else if ins.nested_ctes.is_empty() {
4190 format!(
4191 "INSERT INTO {} ({}) VALUES ({})",
4192 target_ref(&ins.target),
4193 cols.join(", "),
4194 vals.join(", ")
4195 )
4196 } else {
4197 format!(
4198 "INSERT INTO {} ({}) SELECT {}{}",
4199 target_ref(&ins.target),
4200 cols.join(", "),
4201 vals.join(", "),
4202 nested_cte_from(&ins.nested_ctes, &vals.join(","), " "),
4203 )
4204 }
4205}
4206
4207fn emit_insert_stmt(ins: &IrInsert) -> SqlOutput {
4208 if ins.enqueue_vector.is_empty()
4209 && ins.enqueue_search.is_empty()
4210 && !insert_has_any_multilink(ins)
4211 && ins.nested_ctes.is_empty()
4212 {
4213 let mut sql = emit_insert_row_sql(ins);
4214 if let Some(conflict) = &ins.unless_conflict {
4215 emit_conflict(&mut sql, conflict);
4216 }
4217 let (shape, returning_sql) = emit_returning_shape(&ins.target, &ins.returning, false);
4218 if let Some(r) = returning_sql {
4219 sql.push_str(&r);
4220 }
4221 return SqlOutput {
4222 sql,
4223 shape,
4224 inference_plan: None,
4225 };
4226 }
4227
4228 let mut cte_parts = if insert_has_any_multilink(ins) {
4234 emit_insert_multilink_ctes(ins, "_w")
4235 } else {
4236 let mut cte_parts = emit_user_cte_parts(&ins.nested_ctes);
4237 let mut insert_sql = emit_insert_row_sql(ins);
4238 if let Some(conflict) = &ins.unless_conflict {
4239 emit_conflict(&mut insert_sql, conflict);
4240 }
4241 insert_sql.push_str("\nRETURNING \"id\"");
4242 cte_parts.push(format!("\"_w\" AS (\n{}\n)", insert_sql));
4243 cte_parts
4244 };
4245 cte_parts.extend(enqueue_ctes(&ins.enqueue_vector, "_w"));
4246 cte_parts.extend(enqueue_search_ctes(&ins.enqueue_search, "_w", ins.enqueue_vector.len()));
4247
4248 let (shape, select_sql) = shape_select_from_cte(&ins.target, &ins.returning, "_w");
4249 let sql = format!(
4250 "WITH\n{}\n{}",
4251 cte_parts.join(",\n"),
4252 select_sql.unwrap_or_else(|| "SELECT * FROM \"_w\"".to_string()),
4253 );
4254 SqlOutput {
4255 sql,
4256 shape,
4257 inference_plan: None,
4258 }
4259}
4260
4261fn emit_poly_update_stmt(upd: &IrUpdate, user_ctes: &[IrCteDef]) -> SqlOutput {
4264 let alias = &upd.target.alias;
4265 let sets = update_set_fragments(&upd.assignments, &upd.rewrites, "");
4266
4267 let mut cte_parts: Vec<String> = emit_user_cte_parts(user_ctes);
4273 cte_parts.extend(emit_user_cte_parts(&upd.nested_ctes));
4274 let mut union_parts = vec![];
4275 let from_ctes = nested_cte_from(&upd.nested_ctes, &sets.join(","), "\n");
4276
4277 let has_any_multilink = !upd.multi_link_clears.is_empty()
4278 || !upd.multi_link_replaces.is_empty()
4279 || !upd.multi_link_appends.is_empty()
4280 || !upd.multi_link_removals.is_empty();
4281 for (i, imp) in upd.poly_implementors.iter().enumerate() {
4282 let cte_name = format!("_u{}", i);
4283 if has_any_multilink {
4287 cte_parts.extend(emit_update_multilink_cte_parts(
4288 &concrete_poly_update(upd, imp),
4289 &cte_name,
4290 false,
4291 ));
4292 } else {
4293 let mut upd_sql = format!(
4294 "UPDATE {} AS {}\nSET {}{}",
4295 qn(&imp.module, &imp.table),
4296 qi(alias),
4297 sets.join(", "),
4298 from_ctes,
4299 );
4300 append_filter(&mut upd_sql, &upd.filter);
4301 upd_sql.push_str(&format!("\nRETURNING {}.\"id\"", qi(alias)));
4302 cte_parts.push(format!("\"{}\" AS (\n{}\n)", cte_name, upd_sql));
4303 }
4304
4305 let r_alias = format!("_r{}", i);
4306 union_parts.push(format!(
4307 "SELECT ROW({}::text, {}.\"id\") AS result FROM \"{}\" AS {}",
4308 sql_str(&imp.type_name),
4309 qi(&r_alias),
4310 cte_name,
4311 qi(&r_alias),
4312 ));
4313 }
4314
4315 let sql = format!("WITH\n{}\n{}", cte_parts.join(",\n"), union_parts.join("\nUNION ALL\n"),);
4316
4317 let (shape, _) = emit_returning_shape(&upd.target, &upd.returning, true);
4318 SqlOutput {
4319 sql,
4320 shape,
4321 inference_plan: None,
4322 }
4323}
4324
4325fn emit_update_stmt(upd: &IrUpdate, user_ctes: &[IrCteDef]) -> SqlOutput {
4326 if !upd.poly_implementors.is_empty() {
4327 return emit_poly_update_stmt(upd, user_ctes);
4328 }
4329 let alias = &upd.target.alias;
4330 let (shape, returning_sql) = emit_returning_shape(&upd.target, &upd.returning, true);
4331
4332 let has_any_multilink = !upd.multi_link_clears.is_empty()
4333 || !upd.multi_link_replaces.is_empty()
4334 || !upd.multi_link_appends.is_empty()
4335 || !upd.multi_link_removals.is_empty();
4336
4337 if !has_any_multilink && upd.enqueue_vector.is_empty() && upd.enqueue_search.is_empty() {
4338 let sets = update_set_fragments(&upd.assignments, &upd.rewrites, "");
4345 let mut sql = format!(
4346 "UPDATE {} AS {}\nSET {}",
4347 target_ref(&upd.target),
4348 qi(alias),
4349 sets.join(", "),
4350 );
4351 sql.push_str(&nested_cte_from(&upd.nested_ctes, &sets.join(","), "\n"));
4352 append_filter(&mut sql, &upd.filter);
4353 if let Some(r) = returning_sql {
4354 sql.push_str(&r);
4355 }
4356 let combined_ctes: Vec<IrCteDef> = user_ctes
4359 .iter()
4360 .cloned()
4361 .chain(upd.nested_ctes.iter().cloned())
4362 .collect();
4363 if !combined_ctes.is_empty() {
4364 sql = format!("{}{}", emit_cte_prefix(&combined_ctes), sql);
4365 }
4366 return SqlOutput {
4367 sql,
4368 shape,
4369 inference_plan: None,
4370 };
4371 }
4372
4373 if !has_any_multilink && (!upd.enqueue_vector.is_empty() || !upd.enqueue_search.is_empty()) {
4374 let sets = update_set_fragments(&upd.assignments, &upd.rewrites, "");
4376 let mut upd_sql = format!(
4377 " UPDATE {} AS {}\n SET {}",
4378 target_ref(&upd.target),
4379 qi(alias),
4380 sets.join(", "),
4381 );
4382 upd_sql.push_str(&nested_cte_from(&upd.nested_ctes, &sets.join(","), "\n "));
4383 append_filter(&mut upd_sql, &upd.filter);
4384 upd_sql.push_str("\n RETURNING \"id\"");
4385
4386 let mut cte_parts: Vec<String> = emit_user_cte_parts(user_ctes);
4387 cte_parts.extend(emit_user_cte_parts(&upd.nested_ctes));
4388 cte_parts.push(format!("\"_w\" AS (\n{}\n)", upd_sql));
4389 cte_parts.extend(enqueue_ctes(&upd.enqueue_vector, "_w"));
4390 cte_parts.extend(enqueue_search_ctes(&upd.enqueue_search, "_w", upd.enqueue_vector.len()));
4391
4392 let (shape2, select_sql) = shape_select_from_cte(&upd.target, &upd.returning, "_w");
4393 let sql = format!(
4394 "WITH\n{}\n{}",
4395 cte_parts.join(",\n"),
4396 select_sql.unwrap_or_else(|| "SELECT * FROM \"_w\"".to_string()),
4397 );
4398 return SqlOutput {
4399 sql,
4400 shape: shape2,
4401 inference_plan: None,
4402 };
4403 }
4404
4405 let result_expr = if !upd.returning.is_empty() {
4407 let (pointer_exprs, _) = build_shape(&upd.returning, alias);
4408 let mut parts = vec![type_disc(&upd.target.type_name)];
4409 parts.extend(pointer_exprs);
4410 parts.join(",\n ")
4411 } else {
4412 format!("{}.id", qi(alias))
4413 };
4414
4415 let has_scalar_changes = !upd.assignments.is_empty() || !upd.rewrites.is_empty();
4416
4417 let mut cte_parts: Vec<String> = emit_user_cte_parts(user_ctes);
4421 cte_parts.extend(emit_user_cte_parts(&upd.nested_ctes));
4422
4423 if has_scalar_changes {
4425 let sets = update_set_fragments(&upd.assignments, &upd.rewrites, "");
4426 let mut upd_sql = format!(
4427 "UPDATE {} AS {}\nSET {}",
4428 target_ref(&upd.target),
4429 qi(alias),
4430 sets.join(", "),
4431 );
4432 upd_sql.push_str(&nested_cte_from(&upd.nested_ctes, &sets.join(","), "\n"));
4433 append_filter(&mut upd_sql, &upd.filter);
4434 upd_sql.push_str("\nRETURNING *");
4435 cte_parts.push(format!("\"_ids\" AS (\n{}\n)", upd_sql));
4436 } else {
4437 let mut sel = format!(
4438 "SELECT {}.* FROM {} AS {}",
4439 qi(alias),
4440 target_ref(&upd.target),
4441 qi(alias),
4442 );
4443 append_filter(&mut sel, &upd.filter);
4444 cte_parts.push(format!("\"_ids\" AS (\n{}\n)", sel));
4445 }
4446
4447 for (i, clr) in upd.multi_link_clears.iter().enumerate() {
4449 let exclude = ml_clear_exclusion(upd.multi_link_replaces.get(i));
4450 let del = format!(
4451 "DELETE FROM {} WHERE {} IN (SELECT id FROM \"_ids\"){}",
4452 qn(&clr.module, &clr.junction_table),
4453 qi(&clr.source_col),
4454 exclude,
4455 );
4456 cte_parts.push(format!("\"_clr_{}\" AS (\n{}\n)", i, del));
4457 }
4458
4459 for (i, app) in upd.multi_link_appends.iter().enumerate() {
4461 cte_parts.push(emit_ml_append_cte(
4462 app,
4463 "_ids",
4464 &upd.target.alias,
4465 &format!("_ml_add_{}", i),
4466 ));
4467 }
4468
4469 for (i, rem) in upd.multi_link_removals.iter().enumerate() {
4471 cte_parts.push(emit_ml_remove_cte(
4472 rem,
4473 "_ids",
4474 &upd.target.alias,
4475 &format!("_ml_rm_{}", i),
4476 ));
4477 }
4478
4479 for (i, rep) in upd.multi_link_replaces.iter().enumerate() {
4481 cte_parts.push(emit_ml_append_cte(
4482 rep,
4483 "_ids",
4484 &upd.target.alias,
4485 &format!("_ml_rep_{}", i),
4486 ));
4487 }
4488
4489 cte_parts.extend(enqueue_ctes(&upd.enqueue_vector, "_ids"));
4491 cte_parts.extend(enqueue_search_ctes(
4492 &upd.enqueue_search,
4493 "_ids",
4494 upd.enqueue_vector.len(),
4495 ));
4496
4497 let sql = format!(
4498 "WITH\n{}\nSELECT (\n {}\n) AS result\nFROM \"_ids\" AS {}",
4499 cte_parts.join(",\n"),
4500 result_expr,
4501 qi(alias),
4502 );
4503 SqlOutput {
4504 sql,
4505 shape,
4506 inference_plan: None,
4507 }
4508}
4509
4510fn emit_delete_stmt(del: &IrDelete) -> SqlOutput {
4513 if !del.poly_implementors.is_empty() {
4514 return emit_poly_delete_stmt(del);
4515 }
4516 let alias = &del.target.alias;
4517
4518 if del.enqueue_search.is_empty() {
4519 let mut sql = format!("DELETE FROM {} AS {}", target_ref(&del.target), qi(alias),);
4520 append_filter(&mut sql, &del.filter);
4521 let (shape, returning_sql) = emit_returning_shape(&del.target, &del.returning, true);
4522 if let Some(r) = returning_sql {
4523 sql.push_str(&r);
4524 }
4525 return SqlOutput {
4526 sql,
4527 shape,
4528 inference_plan: None,
4529 };
4530 }
4531
4532 let mut del_sql = format!(" DELETE FROM {} AS {}", target_ref(&del.target), qi(alias),);
4534 append_filter(&mut del_sql, &del.filter);
4535 del_sql.push_str("\n RETURNING \"id\"");
4536
4537 let mut cte_parts = vec![format!("\"_del\" AS (\n{}\n)", del_sql)];
4538 cte_parts.extend(enqueue_search_ctes(&del.enqueue_search, "_del", 0));
4539
4540 let (shape, select_sql) = shape_select_from_cte(&del.target, &del.returning, "_del");
4541 let sql = format!(
4542 "WITH\n{}\n{}",
4543 cte_parts.join(",\n"),
4544 select_sql.unwrap_or_else(|| "SELECT * FROM \"_del\"".to_string()),
4545 );
4546 SqlOutput {
4547 sql,
4548 shape,
4549 inference_plan: None,
4550 }
4551}
4552
4553fn emit_poly_delete_stmt(del: &IrDelete) -> SqlOutput {
4554 let alias = &del.target.alias;
4555 let mut cte_parts = vec![];
4556 let mut union_parts = vec![];
4557
4558 for (i, imp) in del.poly_implementors.iter().enumerate() {
4559 let cte_name = format!("_d{}", i);
4560 let mut del_sql = format!("DELETE FROM {} AS {}", qn(&imp.module, &imp.table), qi(alias),);
4561 append_filter(&mut del_sql, &del.filter);
4562 del_sql.push_str(&format!("\nRETURNING {}.\"id\"", qi(alias)));
4563 cte_parts.push(format!("\"{}\" AS (\n{}\n)", cte_name, del_sql));
4564
4565 let r_alias = format!("_r{}", i);
4566 union_parts.push(format!(
4567 "SELECT ROW({}::text, {}.\"id\") AS result FROM \"{}\" AS {}",
4568 sql_str(&imp.type_name),
4569 qi(&r_alias),
4570 cte_name,
4571 qi(&r_alias),
4572 ));
4573 }
4574
4575 let sql = format!("WITH\n{}\n{}", cte_parts.join(",\n"), union_parts.join("\nUNION ALL\n"),);
4576
4577 let (shape, _) = emit_returning_shape(&del.target, &del.returning, true);
4578 SqlOutput {
4579 sql,
4580 shape,
4581 inference_plan: None,
4582 }
4583}
4584
4585fn emit_returning_shape(
4590 target: &IrSource,
4591 returning: &[IrShapePointer],
4592 with_alias: bool,
4593) -> (ShapeDescriptor, Option<String>) {
4594 if returning.is_empty() {
4595 return (
4596 ShapeDescriptor {
4597 root: ShapeNode::Scalar {
4598 name: String::new(),
4599 position: 0,
4600 },
4601 },
4602 None,
4603 );
4604 }
4605
4606 let alias = if with_alias { target.alias.as_str() } else { "" };
4607 let (pointer_exprs, shape_pointers) = build_shape(returning, alias);
4608
4609 let mut parts = vec![type_disc(&target.type_name)];
4610 parts.extend(pointer_exprs);
4611 let tuple = parts.join(",\n ");
4612 let sql = format!("\nRETURNING (\n {}\n) AS result", tuple);
4613
4614 let root_pointers = prepend_type(shape_pointers);
4615 let shape = ShapeDescriptor {
4616 root: ShapeNode::Object {
4617 name: String::new(),
4618 type_name: Some(target.type_name.clone()),
4619 position: 0,
4620 cardinality: Cardinality::Required,
4621 pointers: root_pointers,
4622 has_implicit_id: shape_has_implicit_id(returning),
4623 },
4624 };
4625 (shape, Some(sql))
4626}
4627
4628fn emit_scalar_set(f: &IrScalarSetPointer, pos: usize) -> (String, ShapeNode) {
4631 let from_sql = if !f.poly_implementors.is_empty() {
4632 format!(
4633 "(\n{}\n) AS {}",
4634 emit_poly_union(&f.poly_implementors, &f.poly_columns),
4635 qi(&f.source.alias)
4636 )
4637 } else {
4638 format!("{} AS {}", source_ref(&f.source), qi(&f.source.alias))
4639 };
4640 let sql = format!(
4641 "(SELECT COALESCE(array_agg(ROW({})::record), ARRAY[]::record[]) FROM {})",
4642 emit_expr(&f.bool_expr),
4643 from_sql,
4644 );
4645 let node = ShapeNode::Array {
4646 name: f.alias.clone(),
4647 position: pos,
4648 element: Box::new(ShapeNode::Scalar {
4649 name: String::new(),
4650 position: 0,
4651 }),
4652 };
4653 (sql, node)
4654}
4655
4656fn shape_has_implicit_id(pointers: &[IrShapePointer]) -> bool {
4661 matches!(pointers.first(), Some(IrShapePointer::Scalar(p)) if p.implicit_id)
4662}
4663
4664fn build_shape(pointers: &[IrShapePointer], table_alias: &str) -> (Vec<String>, Vec<ShapeNode>) {
4665 let mut exprs = Vec::new();
4666 let mut nodes = Vec::new();
4667
4668 for (i, pointer) in pointers.iter().enumerate() {
4669 let (sql, node) = emit_shape_pointer(pointer, table_alias, i + 1);
4670 exprs.push(sql);
4671 nodes.push(node);
4672 }
4673
4674 (exprs, nodes)
4675}
4676
4677fn fill_template(template: &str, args: &[String]) -> String {
4683 let mut out = String::with_capacity(template.len());
4684 let mut chars = template.char_indices().peekable();
4685 while let Some((start, c)) = chars.next() {
4686 if c != '$' {
4687 out.push(c);
4688 continue;
4689 }
4690 let mut end = start + 1;
4691 while let Some((i, d)) = chars.peek().copied()
4692 && d.is_ascii_digit()
4693 {
4694 end = i + d.len_utf8();
4695 chars.next();
4696 }
4697 match template[start + 1..end].parse::<usize>() {
4698 Ok(n) if (1..=args.len()).contains(&n) => out.push_str(&args[n - 1]),
4699 _ => out.push_str(&template[start..end]),
4700 }
4701 }
4702 out
4703}
4704
4705fn assert_message_arg(message: &Option<IrExpr>) -> String {
4707 message
4708 .as_ref()
4709 .map(|m| format!(", ({})::text", emit_expr(m)))
4710 .unwrap_or_default()
4711}
4712
4713fn emit_shape_pointer(pointer: &IrShapePointer, table_alias: &str, pos: usize) -> (String, ShapeNode) {
4714 match pointer {
4715 IrShapePointer::Scalar(f) => emit_scalar(f, table_alias, pos),
4716 IrShapePointer::SingleLink(f) => emit_single_link(f, table_alias, pos),
4717 IrShapePointer::MultiLink(f) => emit_multi_link(f, table_alias, pos),
4718 IrShapePointer::Computed(f) => (emit_expr(&f.expr), expr_shape_node(&f.alias, pos, &f.expr)),
4719 IrShapePointer::ScalarSet(f) => emit_scalar_set(f, pos),
4720 IrShapePointer::Asserted(a) => {
4721 let (sql, node) = emit_shape_pointer(&a.inner, table_alias, pos);
4722 let checked_set = match &a.check {
4733 Some(check) if emits_one_object(check) => {
4734 format!("ARRAY{}", emit_shape_pointer(check, table_alias, pos).0)
4735 }
4736 Some(check) => emit_shape_pointer(check, table_alias, pos).0,
4737 None if emits_one_object(&a.inner) => format!("ARRAY{sql}"),
4738 None => "\"_a\".\"v\"".to_string(),
4739 };
4740 let checked = format!(
4741 "(SELECT \"_a\".\"v\" FROM (SELECT {sql} AS \"v\") AS \"_a\"\n WHERE cardinality(\"_pylon\".{}({checked_set}::text[]{})) >= 0)",
4742 qi(&a.fn_name),
4743 assert_message_arg(&a.message),
4744 );
4745 (checked, node)
4746 }
4747 }
4748}
4749
4750fn emits_one_object(pointer: &IrShapePointer) -> bool {
4753 match pointer {
4754 IrShapePointer::SingleLink(_) => true,
4755 IrShapePointer::Computed(c) => matches!(
4756 c.expr,
4757 IrExpr::ObjectPathSubquery(_) | IrExpr::ObjectPathUnion { multi: false, .. }
4758 ),
4759 IrShapePointer::Asserted(a) => emits_one_object(&a.inner),
4760 _ => false,
4761 }
4762}
4763
4764fn pg_quoted_to_pylon(pg_type: &str) -> String {
4767 let inner = pg_type.trim_start_matches('"');
4768 if let Some(idx) = inner.find(r#""."#) {
4769 let module = &inner[..idx];
4770 let type_name = inner[idx + 3..].trim_end_matches('"');
4771 format!("{}::{}", module, type_name)
4772 } else {
4773 pg_type.to_string()
4774 }
4775}
4776
4777struct QualifiedPgType {
4788 name: String,
4790 is_array: bool,
4791}
4792
4793impl QualifiedPgType {
4794 fn of(pg_type: &str) -> Option<Self> {
4795 let (element, is_array) = match pg_type.strip_suffix("[]") {
4796 Some(element) => (element, true),
4797 None => (pg_type, false),
4798 };
4799 element.starts_with('"').then(|| Self {
4800 name: pg_quoted_to_pylon(element),
4801 is_array,
4802 })
4803 }
4804
4805 fn text_cast(&self) -> &'static str {
4807 if self.is_array { "::text[]" } else { "::text" }
4808 }
4809
4810 fn shape_node(&self, name: String, position: usize) -> ShapeNode {
4812 let element = ShapeNode::Enum {
4813 name: name.clone(),
4814 position,
4815 enum_type: self.name.clone(),
4816 };
4817 if !self.is_array {
4818 return element;
4819 }
4820 ShapeNode::Array {
4821 name,
4822 element: Box::new(ShapeNode::Enum {
4825 name: String::new(),
4826 position: 0,
4827 enum_type: self.name.clone(),
4828 }),
4829 position,
4830 }
4831 }
4832}
4833
4834fn emit_scalar(f: &IrScalarPointer, table_alias: &str, pos: usize) -> (String, ShapeNode) {
4835 let nominal_tuple = f
4840 .pg_type
4841 .strip_suffix("[]")
4842 .unwrap_or(&f.pg_type)
4843 .strip_prefix("__nt__:");
4844 if nominal_tuple.is_some() || f.tuple_shape.is_some() {
4845 let is_array = f.pg_type.ends_with("[]");
4846 let cast = if is_array { "::jsonb[]" } else { "::jsonb" };
4847 let sql = if table_alias.is_empty() {
4848 format!("{}{}", qi(&f.column), cast)
4849 } else {
4850 format!("{}.{}{}", qi(table_alias), qi(&f.column), cast)
4851 };
4852 let tuple = ShapeNode::NamedTuple {
4853 name: if is_array { String::new() } else { f.alias.clone() },
4857 position: if is_array { 0 } else { pos },
4858 type_name: nominal_tuple
4859 .map(str::to_string)
4860 .or_else(|| f.tuple_shape.as_ref().and_then(|s| s.type_name.clone())),
4861 members: f.tuple_shape.as_ref().map(|s| s.members.clone()),
4862 is_free_object: false,
4863 };
4864 let node = if is_array {
4865 ShapeNode::Array {
4866 name: f.alias.clone(),
4867 position: pos,
4868 element: Box::new(tuple),
4869 }
4870 } else {
4871 tuple
4872 };
4873 return (sql, node);
4874 }
4875 if let Some(qualified) = QualifiedPgType::of(&f.pg_type) {
4878 let cast = qualified.text_cast();
4879 let sql = if table_alias.is_empty() {
4880 format!("{}{}", qi(&f.column), cast)
4881 } else {
4882 format!("{}.{}{}", qi(table_alias), qi(&f.column), cast)
4883 };
4884 return (sql, qualified.shape_node(f.alias.clone(), pos));
4885 }
4886 let sql = if table_alias.is_empty() {
4887 format!("{}::{}", qi(&f.column), f.pg_type)
4888 } else {
4889 format!("{}.{}::{}", qi(table_alias), qi(&f.column), f.pg_type)
4890 };
4891 (
4892 sql,
4893 ShapeNode::Scalar {
4894 name: f.alias.clone(),
4895 position: pos,
4896 },
4897 )
4898}
4899
4900fn emit_single_link(f: &IrSingleLinkPointer, parent_alias: &str, pos: usize) -> (String, ShapeNode) {
4901 let sub = &f.subquery;
4902 let [IrRowSource::Bound { source, shape }] = sub.rows.as_slice() else {
4903 unreachable!("single-link subquery is always schema-bound")
4904 };
4905 let sub_alias = &source.alias;
4906
4907 let (sub_exprs, mut sub_nodes) = build_shape(shape, sub_alias);
4908 let mut parts = vec![source_type_disc(source)];
4909 parts.extend(sub_exprs);
4910
4911 for lp in &f.link_properties {
4916 parts.push(format!("\"jt\".{}", qi(&lp.name)));
4917 let pos = sub_nodes.len() + 1;
4918 sub_nodes.push(ShapeNode::Scalar {
4919 name: format!("@{}", lp.name),
4920 position: pos,
4921 });
4922 }
4923
4924 let tuple = parts.join(",\n ");
4925
4926 let (from_sql, mut where_parts) = match &f.correlation {
4927 IrSingleLinkCorrelation::Fk { fk_column, target_pk } => {
4928 let from = format!("FROM {} AS {}", source_ref(source), qi(sub_alias));
4929 let cond = format!(
4930 "{}.{} = {}.{}",
4931 qi(parent_alias),
4932 qi(fk_column),
4933 qi(sub_alias),
4934 qi(target_pk),
4935 );
4936 (from, vec![cond])
4937 }
4938 IrSingleLinkCorrelation::Junction { join, target_pk: _ } => match join {
4942 IrMultiLinkJoin::Standard { junction_table, module } => {
4943 let from = format!(
4944 "FROM {} AS \"jt\"\n INNER JOIN {} AS {}\n ON {}.id = \"jt\".target",
4945 junction_ref(module, junction_table),
4946 source_ref(source),
4947 qi(sub_alias),
4948 qi(sub_alias),
4949 );
4950 let cond = format!("\"jt\".source = {}.id", qi(parent_alias));
4951 (from, vec![cond])
4952 }
4953 IrMultiLinkJoin::Through {
4954 junction_table,
4955 module,
4956 source_col,
4957 target_col,
4958 } => {
4959 let from = format!(
4960 "FROM {} AS \"jt\"\n INNER JOIN {} AS {}\n ON {}.id = \"jt\".{}",
4961 junction_ref(module, junction_table),
4962 source_ref(source),
4963 qi(sub_alias),
4964 qi(sub_alias),
4965 qi(target_col),
4966 );
4967 let cond = format!("\"jt\".{} = {}.id", qi(source_col), qi(parent_alias));
4968 (from, vec![cond])
4969 }
4970 IrMultiLinkJoin::BacklinkFk { .. } | IrMultiLinkJoin::BacklinkJunction { .. } => {
4971 unreachable!("a junction-backed single link's own forward join is always Standard or Through")
4972 }
4973 },
4974 };
4975 if let Some(filter) = &sub.filter {
4976 where_parts.push(emit_expr(filter));
4977 }
4978
4979 let mut sql = format!(
4980 "(SELECT (\n {}\n )\n {}\n WHERE {}",
4981 tuple,
4982 from_sql,
4983 where_parts.join(" AND "),
4984 );
4985 if !sub.order_by.is_empty() {
4986 let s: Vec<_> = sub.order_by.iter().map(emit_sort_clause).collect();
4987 sql.push_str(&format!("\n ORDER BY {}", s.join(", ")));
4988 }
4989 sql.push(')');
4990
4991 let node = ShapeNode::Object {
4992 name: f.alias.clone(),
4993 type_name: Some(source.type_name.clone()),
4994 position: pos,
4995 cardinality: Cardinality::Optional,
4996 pointers: prepend_type(sub_nodes),
4997 has_implicit_id: shape_has_implicit_id(shape),
4998 };
4999 (sql, node)
5000}
5001
5002fn emit_multi_link(f: &IrMultiLinkPointer, parent_alias: &str, pos: usize) -> (String, ShapeNode) {
5003 let sub = &f.subquery;
5004 let [IrRowSource::Bound { source, shape }] = sub.rows.as_slice() else {
5005 unreachable!("multi-link subquery is always schema-bound")
5006 };
5007 let sub_alias = &source.alias;
5008
5009 let (sub_exprs, mut sub_nodes) = build_shape(shape, sub_alias);
5010 let mut row_parts = vec![source_type_disc(source)];
5011 row_parts.extend(sub_exprs);
5012
5013 for lp in &f.link_properties {
5017 row_parts.push(format!("\"jt\".{}", qi(&lp.name)));
5018 let pos = sub_nodes.len() + 1;
5021 sub_nodes.push(ShapeNode::Scalar {
5022 name: format!("@{}", lp.name),
5023 position: pos,
5024 });
5025 }
5026
5027 let row = row_parts.join(",\n ");
5028
5029 let order_sql = if !sub.order_by.is_empty() {
5031 let s: Vec<_> = sub.order_by.iter().map(emit_sort_clause).collect();
5032 format!(" ORDER BY {}", s.join(", "))
5033 } else {
5034 String::new()
5035 };
5036
5037 let (from_sql, source_cond) = match &f.join {
5038 IrMultiLinkJoin::Standard { junction_table, module } => {
5039 let from = format!(
5040 "FROM {} AS \"jt\"\n INNER JOIN {} AS {}\n ON {}.id = \"jt\".target",
5041 junction_ref(module, junction_table),
5042 source_ref(source),
5043 qi(sub_alias),
5044 qi(sub_alias),
5045 );
5046 let cond = format!("\"jt\".source = {}.id", qi(parent_alias));
5047 (from, cond)
5048 }
5049 IrMultiLinkJoin::Through {
5050 junction_table,
5051 module,
5052 source_col,
5053 target_col,
5054 } => {
5055 let from = format!(
5056 "FROM {} AS \"jt\"\n INNER JOIN {} AS {}\n ON {}.id = \"jt\".{}",
5057 junction_ref(module, junction_table),
5058 source_ref(source),
5059 qi(sub_alias),
5060 qi(sub_alias),
5061 qi(target_col),
5062 );
5063 let cond = format!("\"jt\".{} = {}.id", qi(source_col), qi(parent_alias));
5064 (from, cond)
5065 }
5066 IrMultiLinkJoin::BacklinkFk { fk_col } => {
5069 let from = format!("FROM {} AS {}", source_ref(source), qi(sub_alias));
5070 let cond = format!("{}.{} = {}.id", qi(sub_alias), qi(fk_col), qi(parent_alias));
5071 (from, cond)
5072 }
5073 IrMultiLinkJoin::BacklinkJunction {
5078 junction_table,
5079 module,
5080 owner_col,
5081 current_col,
5082 } => {
5083 let from = format!(
5084 "FROM {} AS \"jt\"\n INNER JOIN {} AS {}\n ON {}.id = \"jt\".{}",
5085 junction_ref(module, junction_table),
5086 source_ref(source),
5087 qi(sub_alias),
5088 qi(sub_alias),
5089 qi(owner_col),
5090 );
5091 let cond = format!("\"jt\".{} = {}.id", qi(current_col), qi(parent_alias));
5092 (from, cond)
5093 }
5094 };
5095
5096 let mut where_parts = vec![source_cond];
5097 if let Some(filter) = &sub.filter {
5098 where_parts.push(emit_expr(filter));
5099 }
5100
5101 if f.single {
5102 let mut sql = format!(
5103 "(SELECT (\n {}\n )\n {}\n WHERE {}",
5104 row,
5105 from_sql,
5106 where_parts.join(" AND "),
5107 );
5108 append_order_by(&mut sql, &sub.order_by);
5109 append_offset_limit(&mut sql, &sub.offset, &sub.limit);
5110 sql.push(')');
5111 let node = ShapeNode::Object {
5112 name: f.alias.clone(),
5113 type_name: Some(source.type_name.clone()),
5114 position: pos,
5115 cardinality: Cardinality::Optional,
5116 pointers: prepend_type(sub_nodes),
5117 has_implicit_id: shape_has_implicit_id(shape),
5118 };
5119 return (sql, node);
5120 }
5121
5122 if f.single {
5123 let mut sql = format!(
5124 "(SELECT (\n {}\n )\n {}\n WHERE {}",
5125 row,
5126 from_sql,
5127 where_parts.join(" AND "),
5128 );
5129 append_order_by(&mut sql, &sub.order_by);
5130 append_offset_limit(&mut sql, &sub.offset, &sub.limit);
5131 sql.push(')');
5132 let node = ShapeNode::Object {
5133 name: f.alias.clone(),
5134 type_name: Some(source.type_name.clone()),
5135 position: pos,
5136 cardinality: Cardinality::Optional,
5137 pointers: prepend_type(sub_nodes),
5138 has_implicit_id: shape_has_implicit_id(shape),
5139 };
5140 return (sql, node);
5141 }
5142
5143 let sql = if sub.limit.is_some() || sub.offset.is_some() {
5144 let mut cols: Vec<String> = row_parts
5149 .iter()
5150 .enumerate()
5151 .map(|(i, part)| format!("{} AS \"c{}\"", part, i))
5152 .collect();
5153 let agg_row: Vec<String> = (0..row_parts.len()).map(|i| format!("\"__lim\".\"c{}\"", i)).collect();
5154 let mut agg_order: Vec<String> = vec![];
5155 for (i, s) in sub.order_by.iter().enumerate() {
5156 cols.push(format!("{} AS \"s{}\"", emit_expr(&s.expr), i));
5157 agg_order.push(emit_sort_clause(&IrSort {
5158 expr: IrExpr::ColumnRef {
5159 alias: "__lim".to_string(),
5160 column: format!("s{}", i),
5161 pg_type: String::new(),
5162 },
5163 direction: s.direction.clone(),
5164 nulls: s.nulls.clone(),
5165 }));
5166 }
5167 let mut inner = format!(
5168 "SELECT {}\n {}\n WHERE {}",
5169 cols.join(",\n "),
5170 from_sql,
5171 where_parts.join(" AND "),
5172 );
5173 append_order_by(&mut inner, &sub.order_by);
5174 append_offset_limit(&mut inner, &sub.offset, &sub.limit);
5175 let agg_order_sql = if agg_order.is_empty() {
5176 String::new()
5177 } else {
5178 format!(" ORDER BY {}", agg_order.join(", "))
5179 };
5180 format!(
5181 "(SELECT COALESCE(\n array_agg(ROW(\n {}\n )::record{}),\n ARRAY[]::record[]\n )\n FROM ({}) AS \"__lim\")",
5182 agg_row.join(",\n "),
5183 agg_order_sql,
5184 inner,
5185 )
5186 } else {
5187 format!(
5188 "(SELECT COALESCE(\n array_agg(ROW(\n {}\n )::record{}),\n ARRAY[]::record[]\n )\n {}\n WHERE {})",
5189 row,
5190 order_sql,
5191 from_sql,
5192 where_parts.join(" AND "),
5193 )
5194 };
5195
5196 let node = ShapeNode::Array {
5197 name: f.alias.clone(),
5198 position: pos,
5199 element: Box::new(ShapeNode::Object {
5200 name: String::new(),
5201 type_name: Some(source.type_name.clone()),
5202 position: 0,
5203 cardinality: Cardinality::Required,
5204 pointers: prepend_type(sub_nodes),
5205 has_implicit_id: shape_has_implicit_id(shape),
5206 }),
5207 };
5208 (sql, node)
5209}
5210
5211fn prepend_type(nodes: Vec<ShapeNode>) -> Vec<ShapeNode> {
5214 let mut out = vec![ShapeNode::Scalar {
5215 name: "__type__".into(),
5216 position: 0,
5217 }];
5218 out.extend(nodes);
5219 out
5220}
5221
5222fn append_filter(sql: &mut String, filter: &Option<IrExpr>) {
5225 if let Some(f) = filter {
5226 sql.push_str(&format!("\nWHERE {}", emit_expr(f)));
5227 }
5228}
5229
5230fn append_order_by(sql: &mut String, order_by: &[IrSort]) {
5231 if !order_by.is_empty() {
5232 let s: Vec<_> = order_by.iter().map(emit_sort_clause).collect();
5233 sql.push_str(&format!("\nORDER BY {}", s.join(", ")));
5234 }
5235}
5236
5237fn append_offset_limit(sql: &mut String, offset: &Option<IrExpr>, limit: &Option<IrExpr>) {
5238 if let Some(o) = offset {
5239 sql.push_str(&format!("\nOFFSET {}", emit_expr(o)));
5240 }
5241 if let Some(l) = limit {
5242 sql.push_str(&format!("\nLIMIT {}", emit_expr(l)));
5243 }
5244}
5245
5246fn append_lock_clause(sql: &mut String, lock: &Option<IrLockClause>) {
5253 let Some(lock) = lock else { return };
5254 let strength = match lock.strength {
5255 IrLockStrength::Update => "UPDATE",
5256 IrLockStrength::NoKeyUpdate => "NO KEY UPDATE",
5257 IrLockStrength::Share => "SHARE",
5258 IrLockStrength::KeyShare => "KEY SHARE",
5259 };
5260 sql.push_str(&format!("\nFOR {}", strength));
5261 match lock.wait {
5262 IrLockWait::Block => {}
5263 IrLockWait::NoWait => sql.push_str(" NOWAIT"),
5264 IrLockWait::SkipLocked => sql.push_str(" SKIP LOCKED"),
5265 }
5266}
5267
5268fn emit_sort_clause(s: &IrSort) -> String {
5269 let dir = match s.direction {
5270 IrSortDir::Asc => "ASC",
5271 IrSortDir::Desc => "DESC",
5272 };
5273 let nulls = match s.nulls {
5274 IrNulls::First => "NULLS FIRST",
5275 IrNulls::Last => "NULLS LAST",
5276 };
5277 format!("{} {} {}", emit_expr(&s.expr), dir, nulls)
5278}
5279
5280fn set_operand(expr: &IrExpr) -> String {
5285 match expr {
5286 IrExpr::ArrayFromSelect(_) => format!("SELECT unnest({})", emit_expr(expr)),
5287 IrExpr::CteRef { name, scalar, .. } => {
5288 format!("SELECT {} FROM {}", if *scalar { "\"v\"" } else { "\"id\"" }, qi(name))
5289 }
5290 _ => format!("SELECT {}", emit_expr(expr)),
5291 }
5292}
5293
5294pub fn emit_expr_with_fanouts(expr: &IrExpr, fanouts: &HashMap<(String, String), IrPolyFanout>) -> String {
5297 with_subtype_fanouts(fanouts, || emit_expr(expr))
5298}
5299
5300pub fn emit_expr(expr: &IrExpr) -> String {
5301 match expr {
5302 IrExpr::ColumnRef { alias, column, .. } => {
5303 if alias.is_empty() {
5304 qi(column)
5305 } else if alias == "NEW" || alias == "OLD" {
5306 format!("{alias}.{}", qi(column))
5317 } else {
5318 format!("{}.{}", qi(alias), qi(column))
5319 }
5320 }
5321 IrExpr::Param { index } => format!("${}", index + 1),
5322 IrExpr::Literal(lit) => emit_literal(lit),
5323 IrExpr::BinOp(op) => {
5324 let l = emit_expr(&op.left);
5325 let r = emit_expr(&op.right);
5326 match op.op {
5327 BinOpKind::Add => format!("({} + {})", l, r),
5328 BinOpKind::Sub => format!("({} - {})", l, r),
5329 BinOpKind::Mul => format!("({} * {})", l, r),
5330 BinOpKind::Div => format!("({} / {})", l, r),
5331 BinOpKind::FloorDiv => {
5332 if is_integer_expr(&op.left) && is_integer_expr(&op.right) {
5333 format!("({} / {})", l, r)
5334 } else {
5335 format!("floor(({}) / ({}))", l, r)
5336 }
5337 }
5338 BinOpKind::Mod => format!("({} % {})", l, r),
5339 BinOpKind::Pow => format!("power({}, {})", l, r),
5340 BinOpKind::Eq => format!("({} = {})", l, r),
5341 BinOpKind::Ne => format!("({} <> {})", l, r),
5342 BinOpKind::Lt => format!("({} < {})", l, r),
5343 BinOpKind::Le => format!("({} <= {})", l, r),
5344 BinOpKind::Gt => format!("({} > {})", l, r),
5345 BinOpKind::Ge => format!("({} >= {})", l, r),
5346 BinOpKind::And => format!("({} AND {})", l, r),
5347 BinOpKind::Or => format!("({} OR {})", l, r),
5348 BinOpKind::Like => format!("({} LIKE {})", l, r),
5349 BinOpKind::Ilike => format!("({} ILIKE {})", l, r),
5350 BinOpKind::NotLike => format!("({} NOT LIKE {})", l, r),
5351 BinOpKind::NotIlike => format!("({} NOT ILIKE {})", l, r),
5352 BinOpKind::In => format!("({} = ANY({}))", l, unwrap_unnest_for_any(&op.right, &r)),
5358 BinOpKind::NotIn => format!("({} <> ALL({}))", l, unwrap_unnest_for_any(&op.right, &r)),
5359 BinOpKind::Coalesce => format!("COALESCE({}, {})", l, r),
5360 BinOpKind::CoalesceEq => format!("({} IS NOT DISTINCT FROM {})", l, r),
5361 BinOpKind::CoalesceNe => format!("({} IS DISTINCT FROM {})", l, r),
5362 BinOpKind::Concat => format!("({} || {})", l, r),
5363 }
5364 }
5365 IrExpr::UnaryOp(op) => {
5366 let inner = emit_expr(&op.operand);
5367 match op.op {
5368 UnaryOpKind::Not => format!("(NOT {})", inner),
5369 UnaryOpKind::Minus => format!("(-{})", inner),
5370 UnaryOpKind::Exists => format!("EXISTS({})", inner),
5371 UnaryOpKind::Distinct => inner,
5376 }
5377 }
5378 IrExpr::FunctionCall(f) => {
5379 let args: Vec<_> = f.args.iter().map(emit_expr).collect();
5380 if let Some(tmpl) = &f.sql_template {
5381 return fill_template(tmpl, &args);
5382 }
5383 let name = match &f.schema {
5384 Some(s) => format!("{}.{}", pg_schema(s), qi(&f.name)),
5385 None => f.name.clone(),
5386 };
5387 format!("{}({})", name, args.join(", "))
5388 }
5389 IrExpr::TypeCast(c) => {
5390 if c.pg_type == "jsonb" {
5393 match &c.expr {
5394 IrExpr::Param { .. } => format!("({})::jsonb", emit_expr(&c.expr)),
5405 IrExpr::Literal(IrLiteral::Str(_)) => {
5406 format!("to_jsonb({}::text)", emit_expr(&c.expr))
5407 }
5408 _ => format!("to_jsonb({})", emit_expr(&c.expr)),
5409 }
5410 } else if yields_jsonb(&c.expr) {
5411 format!("(({}) #>> '{{}}')::{}", emit_expr(&c.expr), c.pg_type)
5415 } else {
5416 format!("({})::{}", emit_expr(&c.expr), c.pg_type)
5417 }
5418 }
5419 IrExpr::IfElse(ie) => format!(
5420 "CASE WHEN {} THEN {} ELSE {} END",
5421 emit_expr(&ie.condition),
5422 emit_expr(&ie.if_),
5423 emit_expr(&ie.else_),
5424 ),
5425 IrExpr::Array(elems) => {
5426 if elems.is_empty() {
5427 "ARRAY[]::text[]".to_string()
5428 } else {
5429 let parts: Vec<String> = elems.iter().map(emit_expr).collect();
5430 format!("ARRAY[{}]", parts.join(", "))
5431 }
5432 }
5433 IrExpr::Null => "NULL".to_string(),
5434 IrExpr::AggOverSet {
5435 fn_name,
5436 schema: _,
5437 elems,
5438 } => {
5439 let union_all = elems
5440 .iter()
5441 .map(|e| format!("SELECT {}", emit_expr(e)))
5442 .collect::<Vec<_>>()
5443 .join(" UNION ALL ");
5444 format!("(SELECT {}(v) FROM ({}) AS _set(v))", fn_name, union_all)
5445 }
5446 IrExpr::AggOverCte { fn_name, cte, column } => format!(
5447 "(SELECT {}({}) FROM {})",
5448 fn_name,
5449 column.as_deref().map(qi).unwrap_or_else(|| "*".to_string()),
5450 qi(cte),
5451 ),
5452 IrExpr::ExistsOverCte { cte, column } => match column {
5453 Some(column) => format!("EXISTS(SELECT 1 FROM {} WHERE {} IS NOT NULL)", qi(cte), qi(column)),
5454 None => format!("EXISTS(SELECT 1 FROM {})", qi(cte)),
5455 },
5456 IrExpr::AggOverQuery { fn_name, inner } => {
5457 let inner_sql = emit_select_stmt(inner, &[]).sql;
5458 format!("(SELECT {}(*) FROM ({}) _agg)", fn_name, inner_sql)
5459 }
5460 IrExpr::ArrayFromSelect(src) => emit_array_source(src),
5461 IrExpr::ScalarSubquery(sel) => {
5462 format!(
5463 "(SELECT \"v\" FROM (\n{}\n) AS \"_scalar_sub\")",
5464 emit_select_stmt(sel, &[]).sql
5465 )
5466 }
5467
5468 IrExpr::SetOp { op, left, right, mode } => {
5469 let set = format!("({}) {} ({})", set_operand(left), op.sql(), set_operand(right));
5470 match mode {
5471 crate::ir::SetOpMode::Exists => format!("EXISTS({set})"),
5472 crate::ir::SetOpMode::Array => format!("ARRAY({set})"),
5473 crate::ir::SetOpMode::Aggregate(f) => {
5474 format!("(SELECT {f}(\"_s\".\"v\") FROM ({set}) AS \"_s\"(\"v\"))")
5475 }
5476 }
5477 }
5478
5479 IrExpr::CteRef { name, scalar, .. } => {
5480 let col = if *scalar { "v" } else { "id" };
5483 format!("(SELECT \"{}\" FROM \"{}\")", col, name)
5484 }
5485
5486 IrExpr::CteFieldRef { name, field, .. } => {
5487 format!("(SELECT {} FROM {})", qi(field), qi(name))
5488 }
5489
5490 IrExpr::ForVar { name, .. } => format!("\"_for_{}\".\"v\"", name),
5491
5492 IrExpr::EnumLiteral { pg_type, variant } => {
5493 format!("'{}'::{}", variant.replace('\'', "''"), pg_type)
5494 }
5495
5496 IrExpr::GlobalParam { index, pg_type } => {
5497 format!("(${}::{})", index + 1, pg_type)
5498 }
5499
5500 IrExpr::GlobalRef { cte_name } => {
5501 format!("(SELECT \"value\" FROM \"{}\")", cte_name)
5502 }
5503
5504 IrExpr::NamedTuple { fields, .. } => {
5505 let pairs: Vec<String> = fields
5506 .iter()
5507 .flat_map(|(k, v)| [format!("'{}'", k.replace('\'', "''")), emit_expr(v)])
5508 .collect();
5509 format!("jsonb_build_object({})", pairs.join(", "))
5510 }
5511
5512 IrExpr::Tuple(elems) => {
5513 let items: Vec<String> = elems.iter().map(emit_expr).collect();
5514 format!("jsonb_build_array({})", items.join(", "))
5515 }
5516
5517 IrExpr::Subscript { expr, index, is_array } => {
5518 let e = emit_expr(expr);
5519 let i = emit_expr(index);
5520 if *is_array {
5521 format!("_pylon.array_subscript({}, ({})::bigint)", e, i)
5522 } else {
5523 format!("_pylon.str_subscript({}, ({})::bigint)", e, i)
5524 }
5525 }
5526
5527 IrExpr::Slice {
5528 expr,
5529 lower,
5530 upper,
5531 is_array,
5532 } => {
5533 let e = emit_expr(expr);
5534 if *is_array {
5535 let lo = lower
5536 .as_deref()
5537 .map(|x| format!("({}) + 1", emit_expr(x)))
5538 .unwrap_or_else(|| "1".to_string());
5539 let hi = upper.as_deref().map(emit_expr).unwrap_or_default();
5540 if hi.is_empty() {
5541 format!("({})[{}:]", e, lo)
5542 } else {
5543 format!("({})[{}:{}]", e, lo, hi)
5544 }
5545 } else {
5546 let start = lower
5548 .as_deref()
5549 .map(|x| format!("({}) + 1", emit_expr(x)))
5550 .unwrap_or_else(|| "1".to_string());
5551 match upper.as_deref() {
5552 Some(hi_expr) => {
5553 let lo_val = lower.as_deref().map(emit_expr).unwrap_or_else(|| "0".to_string());
5554 format!(
5556 "substr({}, {}, GREATEST(0, ({}) - ({})))",
5557 e,
5558 start,
5559 emit_expr(hi_expr),
5560 lo_val
5561 )
5562 }
5563 None => format!("substr({}, {})", e, start),
5564 }
5565 }
5566 }
5567
5568 IrExpr::JsonbField { expr, field } => {
5569 format!("({}->{})", emit_expr(expr), sql_str(field))
5570 }
5571
5572 IrExpr::JsonbIndex { expr, index } => {
5573 format!("({}->{})", emit_expr(expr), index)
5574 }
5575
5576 IrExpr::FnParam { name, .. } => qi(name),
5577
5578 IrExpr::FnSubquery(fs) => {
5579 let scalar = match fs.shape.as_slice() {
5580 [IrShapePointer::Computed(c)] => emit_expr(&c.expr),
5581 _ => unreachable!("a function scalar subquery always projects exactly one computed pointer"),
5582 };
5583 let args_sql = fs.fn_args.iter().map(emit_expr).collect::<Vec<_>>().join(", ");
5584 let mut sql = format!(
5585 "(SELECT {}{}\nFROM {}.{}({}) AS {}",
5586 if fs.distinct { "DISTINCT " } else { "" },
5587 scalar,
5588 pg_schema(&fs.fn_module),
5589 qi(&fs.fn_name),
5590 args_sql,
5591 qi(&fs.alias),
5592 );
5593 append_filter(&mut sql, &fs.filter);
5594 append_order_by(&mut sql, &fs.order_by);
5595 append_offset_limit(&mut sql, &fs.offset, &fs.limit);
5596 sql.push(')');
5597 sql
5598 }
5599
5600 IrExpr::PathSubquery(ps) => {
5601 let scalar = match &ps.result {
5602 IrPathResult::Scalar(e, _) => emit_expr(e),
5603 IrPathResult::Object { alias, .. } => format!("{}.\"id\"", qi(alias)),
5604 };
5605 let from_sql = emit_path_joins(&ps.root, &ps.joins);
5606 let mut sql = format!(
5607 "(SELECT {}{}\nFROM {}",
5608 if ps.distinct { "DISTINCT " } else { "" },
5609 scalar,
5610 from_sql
5611 );
5612 append_filter(&mut sql, &ps.filter);
5613 append_order_by(&mut sql, &ps.order_by);
5614 append_offset_limit(&mut sql, &ps.offset, &ps.limit);
5615 sql.push(')');
5616 sql
5617 }
5618
5619 IrExpr::ObjectPathUnion { branches, limit, multi } => {
5620 let arms: Vec<String> = branches
5621 .iter()
5622 .map(|ps| {
5623 let IrPathResult::Object {
5624 alias,
5625 type_name,
5626 shape,
5627 } = &ps.result
5628 else {
5629 unreachable!("an object path union's branches always land on objects")
5630 };
5631 let (pointer_exprs, _) = build_shape(shape, alias);
5632 let mut parts = vec![result_type_disc(ps, alias, type_name)];
5633 parts.extend(pointer_exprs);
5634 let mut sql = format!(
5635 "SELECT (\n {}\n) AS \"r\"\nFROM {}",
5636 parts.join(",\n "),
5637 emit_path_joins(&ps.root, &ps.joins),
5638 );
5639 append_filter(&mut sql, &ps.filter);
5640 sql
5641 })
5642 .collect();
5643 let mut sql = format!(
5644 "{}(SELECT \"r\" FROM (\n{}\n) AS \"_u\"",
5645 if *multi { "ARRAY" } else { "" },
5646 arms.join("\nUNION ALL\n")
5647 );
5648 if let Some(limit) = limit {
5649 sql.push_str(&format!("\nLIMIT {}", emit_expr(limit)));
5650 }
5651 sql.push(')');
5652 sql
5653 }
5654
5655 IrExpr::ObjectPathSubquery(ps) => {
5656 let IrPathResult::Object {
5657 alias,
5658 type_name,
5659 shape,
5660 } = &ps.result
5661 else {
5662 unreachable!("an object path subquery always lands on an object")
5663 };
5664 let (pointer_exprs, _) = build_shape(shape, alias);
5665 let mut parts = vec![result_type_disc(ps, alias, type_name)];
5666 parts.extend(pointer_exprs);
5667 let mut sql = format!(
5668 "(SELECT (\n {}\n)\nFROM {}",
5669 parts.join(",\n "),
5670 emit_path_joins(&ps.root, &ps.joins),
5671 );
5672 append_filter(&mut sql, &ps.filter);
5673 append_order_by(&mut sql, &ps.order_by);
5674 append_offset_limit(&mut sql, &ps.offset, &ps.limit);
5675 sql.push(')');
5676 sql
5677 }
5678
5679 IrExpr::ObjectSubquery(sel) => {
5680 let [IrRowSource::Bound { source, shape }] = sel.rows.as_slice() else {
5681 unreachable!("an object subquery is always schema-bound")
5682 };
5683 let alias = &source.alias;
5684 let (sub_exprs, _) = build_shape(shape, alias);
5685 let mut row_parts = vec![source_type_disc(source)];
5686 row_parts.extend(sub_exprs);
5687 let mut sql = format!(
5688 "(SELECT (\n {}\n )\n FROM {} AS {}",
5689 row_parts.join(",\n "),
5690 source_ref(source),
5691 qi(alias),
5692 );
5693 append_filter(&mut sql, &sel.filter);
5694 append_order_by(&mut sql, &sel.order_by);
5695 append_offset_limit(&mut sql, &sel.offset, &sel.limit);
5696 sql.push(')');
5697 sql
5698 }
5699
5700 IrExpr::Subquery(sel) => {
5701 let [IrRowSource::Bound { source, shape }] = sel.rows.as_slice() else {
5702 unreachable!("scalar/exists subquery is always schema-bound")
5703 };
5704 let alias = &source.alias;
5705 let mut sql = if shape.is_empty() {
5706 format!("(SELECT 1\nFROM {} AS {}", source_ref(source), qi(alias))
5708 } else if let Some(c) = shape.iter().find_map(|f| {
5709 if let IrShapePointer::Computed(c) = f {
5710 Some(c)
5711 } else {
5712 None
5713 }
5714 }) {
5715 format!(
5720 "(SELECT {}\nFROM {} AS {}",
5721 emit_expr(&c.expr),
5722 source_ref(source),
5723 qi(alias)
5724 )
5725 } else {
5726 let pk_col = shape
5728 .iter()
5729 .find_map(|f| {
5730 if let IrShapePointer::Scalar(s) = f {
5731 Some(s.column.as_str())
5732 } else {
5733 None
5734 }
5735 })
5736 .unwrap_or("id");
5737 format!(
5738 "(SELECT {}.{}\nFROM {} AS {}",
5739 qi(alias),
5740 qi(pk_col),
5741 source_ref(source),
5742 qi(alias),
5743 )
5744 };
5745 append_filter(&mut sql, &sel.filter);
5746 append_order_by(&mut sql, &sel.order_by);
5747 append_offset_limit(&mut sql, &sel.offset, &sel.limit);
5748 sql.push(')');
5749 sql
5750 }
5751 IrExpr::RawSql(s) => format!("({s})"),
5752 }
5753}
5754
5755fn emit_vector_search(vs: &IrVectorSearch) -> SqlOutput {
5758 let alias = &vs.source.alias;
5759 let dist_sql = format!(
5760 "{}.{} {} {}",
5761 qi(alias),
5762 qi(&vs.vector_col),
5763 vs.distance_op,
5764 emit_expr(&vs.query_expr),
5765 );
5766
5767 let (obj_tuple, object_shape_nodes) = if vs.object_shape.is_empty() {
5772 let type_expr = type_disc(&vs.source.type_name);
5774 let id_expr = format!("{}.\"id\"", qi(alias));
5775 let tuple = format!("{},\n {}", type_expr, id_expr);
5776 let id_node = ShapeNode::Scalar {
5777 name: "id".to_string(),
5778 position: 1,
5779 };
5780 (tuple, vec![id_node])
5781 } else {
5782 let (pointer_exprs, shape_pointers) = build_shape(&vs.object_shape, alias);
5783 let mut parts = vec![type_disc(&vs.source.type_name)];
5784 parts.extend(pointer_exprs);
5785 (parts.join(",\n "), prepend_type(shape_pointers))
5786 };
5787
5788 let outer = format!(
5790 "NULL::text,\n ROW(\n {}\n )::record,\n {}",
5791 obj_tuple, dist_sql,
5792 );
5793 let mut sql = format!(
5794 "SELECT (\n {}\n) AS result\nFROM {} AS {}",
5795 outer,
5796 source_ref(&vs.source),
5797 qi(alias),
5798 );
5799 append_filter(&mut sql, &vs.filter);
5800
5801 if let Some(dir) = &vs.order_by_distance {
5803 let dir_sql = match dir {
5804 IrSortDir::Asc => "ASC",
5805 IrSortDir::Desc => "DESC",
5806 };
5807 sql.push_str(&format!("\nORDER BY {} {}", dist_sql, dir_sql));
5808 }
5809 append_offset_limit(&mut sql, &vs.offset, &vs.limit);
5810
5811 let object_node = ShapeNode::Object {
5812 name: "object".to_string(),
5813 type_name: Some(vs.source.type_name.clone()),
5814 position: 1,
5815 cardinality: Cardinality::Many,
5816 pointers: object_shape_nodes,
5817 has_implicit_id: shape_has_implicit_id(&vs.object_shape),
5818 };
5819 let shape = ShapeDescriptor {
5820 root: ShapeNode::VectorSearch {
5821 object_position: 1,
5822 distance_position: 2,
5823 object_node: Box::new(object_node),
5824 },
5825 };
5826 let inference_plan = vs.inference_model.as_ref().map(|model_name| InferencePlan::Embedding {
5827 model_name: model_name.clone(),
5828 type_name: vs.inference_type_name.clone().unwrap_or_default(),
5829 index_name: vs.inference_index_name.clone().unwrap_or(None),
5830 query_param_name: vs.inference_query_param_name.clone().unwrap_or_default(),
5831 query_literal: vs.inference_query_literal.clone(),
5832 });
5833 SqlOutput {
5834 sql,
5835 shape,
5836 inference_plan,
5837 }
5838}
5839
5840fn emit_fts_search(fs: &IrFtsSearch) -> SqlOutput {
5843 use crate::schema::SearchBackend;
5844 if fs.backend != SearchBackend::Postgres {
5845 return emit_fts_search_deferred(fs);
5846 }
5847
5848 let alias = &fs.source.alias;
5849 let search_col = format!("{}.{}", qi(alias), qi(&fs.search_col));
5850 let query_sql = emit_expr(&fs.query_expr);
5851 let tsquery = format!("{}('english', {})", fs.tsquery_fn, query_sql);
5852 let rank_sql = format!("ts_rank({}, {})", search_col, tsquery);
5853
5854 let (obj_tuple, object_shape_nodes) = if fs.object_shape.is_empty() {
5855 let type_expr = type_disc(&fs.source.type_name);
5856 let id_expr = format!("{}.\"id\"", qi(alias));
5857 let tuple = format!("{},\n {}", type_expr, id_expr);
5858 let id_node = ShapeNode::Scalar {
5859 name: "id".to_string(),
5860 position: 1,
5861 };
5862 (tuple, vec![id_node])
5863 } else {
5864 let (pointer_exprs, shape_pointers) = build_shape(&fs.object_shape, alias);
5865 let mut parts = vec![type_disc(&fs.source.type_name)];
5866 parts.extend(pointer_exprs);
5867 (parts.join(",\n "), prepend_type(shape_pointers))
5868 };
5869
5870 let outer = format!(
5871 "NULL::text,\n ROW(\n {}\n )::record,\n {}",
5872 obj_tuple, rank_sql,
5873 );
5874 let mut sql = format!(
5875 "SELECT (\n {}\n) AS result\nFROM {} AS {}\nWHERE {} @@ {}",
5876 outer,
5877 source_ref(&fs.source),
5878 qi(alias),
5879 search_col,
5880 tsquery,
5881 );
5882 if let Some(f) = &fs.filter {
5883 sql.push_str(&format!(" AND ({})", emit_expr(f)));
5884 }
5885 if let Some(dir) = &fs.order_by_rank {
5886 let dir_sql = match dir {
5887 IrSortDir::Asc => "ASC",
5888 IrSortDir::Desc => "DESC",
5889 };
5890 sql.push_str(&format!("\nORDER BY {} {}", rank_sql, dir_sql));
5891 }
5892 append_offset_limit(&mut sql, &fs.offset, &fs.limit);
5893
5894 let object_node = ShapeNode::Object {
5895 name: "object".to_string(),
5896 type_name: Some(fs.source.type_name.clone()),
5897 position: 1,
5898 cardinality: Cardinality::Many,
5899 pointers: object_shape_nodes,
5900 has_implicit_id: shape_has_implicit_id(&fs.object_shape),
5901 };
5902 let shape = ShapeDescriptor {
5903 root: ShapeNode::FtsSearch {
5904 object_position: 1,
5905 rank_position: 2,
5906 object_node: Box::new(object_node),
5907 },
5908 };
5909 SqlOutput {
5910 sql,
5911 shape,
5912 inference_plan: None,
5913 }
5914}
5915
5916fn emit_fts_search_deferred(fs: &IrFtsSearch) -> SqlOutput {
5917 let alias = &fs.source.alias;
5918 let ids_idx = fs
5919 .deferred_ids_param
5920 .expect("deferred_ids_param must be set for deferred backend");
5921 let scores_idx = fs
5922 .deferred_scores_param
5923 .expect("deferred_scores_param must be set for deferred backend");
5924 let ids_param = format!("${}", ids_idx + 1);
5925 let scores_param = format!("${}", scores_idx + 1);
5926
5927 let (obj_tuple, object_shape_nodes) = if fs.object_shape.is_empty() {
5928 let type_expr = type_disc(&fs.source.type_name);
5929 let id_expr = format!("{}.\"id\"", qi(alias));
5930 let tuple = format!("{},\n {}", type_expr, id_expr);
5931 let id_node = ShapeNode::Scalar {
5932 name: "id".to_string(),
5933 position: 1,
5934 };
5935 (tuple, vec![id_node])
5936 } else {
5937 let (pointer_exprs, shape_pointers) = build_shape(&fs.object_shape, alias);
5938 let mut parts = vec![type_disc(&fs.source.type_name)];
5939 parts.extend(pointer_exprs);
5940 (parts.join(",\n "), prepend_type(shape_pointers))
5941 };
5942
5943 let outer = format!(
5944 "NULL::text,\n ROW(\n {}\n )::record,\n \"_os\".\"score\"",
5945 obj_tuple,
5946 );
5947 let mut sql = format!(
5948 concat!(
5949 "SELECT (\n {}\n) AS result\n",
5950 "FROM {} AS {}\n",
5951 "JOIN UNNEST({}::uuid[], {}::float8[]) AS \"_os\"(\"id\", \"score\")\n",
5952 " ON \"_os\".\"id\" = {}.\"id\"",
5953 ),
5954 outer,
5955 source_ref(&fs.source),
5956 qi(alias),
5957 ids_param,
5958 scores_param,
5959 qi(alias),
5960 );
5961 if let Some(f) = &fs.filter {
5962 sql.push_str(&format!("\nWHERE ({})", emit_expr(f)));
5963 }
5964 if let Some(dir) = &fs.order_by_rank {
5965 let dir_sql = match dir {
5966 IrSortDir::Asc => "ASC",
5967 IrSortDir::Desc => "DESC",
5968 };
5969 sql.push_str(&format!("\nORDER BY \"_os\".\"score\" {}", dir_sql));
5970 }
5971 let size = fs.limit.as_ref().and_then(|lim| {
5973 if let IrExpr::Literal(IrLiteral::Int(n)) = lim {
5974 Some(*n as usize)
5975 } else {
5976 None
5977 }
5978 });
5979
5980 let object_node = ShapeNode::Object {
5981 name: "object".to_string(),
5982 type_name: Some(fs.source.type_name.clone()),
5983 position: 1,
5984 cardinality: Cardinality::Many,
5985 pointers: object_shape_nodes,
5986 has_implicit_id: shape_has_implicit_id(&fs.object_shape),
5987 };
5988 let shape = ShapeDescriptor {
5989 root: ShapeNode::FtsSearch {
5990 object_position: 1,
5991 rank_position: 2,
5992 object_node: Box::new(object_node),
5993 },
5994 };
5995 let backend_str = match fs.backend {
5996 crate::schema::SearchBackend::Meilisearch => "meilisearch",
5997 _ => "opensearch",
5998 };
5999 let inference_plan = Some(InferencePlan::Search {
6000 backend: backend_str.to_string(),
6001 index_name: fs.deferred_index_name.clone().unwrap_or_default(),
6002 query_param_name: fs.deferred_query_param_name.clone().unwrap_or_default(),
6003 query_literal: fs.deferred_query_literal.clone(),
6004 size,
6005 });
6006 SqlOutput {
6007 sql,
6008 shape,
6009 inference_plan,
6010 }
6011}
6012
6013fn emit_function_select(sel: &IrFunctionSelect) -> SqlOutput {
6016 let alias = &sel.alias;
6017 let (pointer_exprs, shape_pointers) = build_shape(&sel.shape, alias);
6018
6019 let type_expr = if sel.polymorphic {
6020 format!("{}.\"__type__\"", qi(alias))
6021 } else {
6022 type_disc(&sel.type_name)
6023 };
6024 let mut parts = vec![type_expr];
6025 parts.extend(pointer_exprs);
6026 let tuple = parts.join(",\n ");
6027 let distinct = if sel.distinct { "DISTINCT " } else { "" };
6028
6029 let args_sql = sel.fn_args.iter().map(emit_expr).collect::<Vec<_>>().join(", ");
6030 let fn_call = format!("{}.{}({})", pg_schema(&sel.fn_module), qi(&sel.fn_name), args_sql);
6031
6032 let from_clause = if sel.polymorphic {
6033 format!("{} AS {}", fn_call, qi(alias))
6036 } else {
6037 format!("{} AS {}", fn_call, qi(alias))
6038 };
6039
6040 let mut sql = format!("SELECT {}(\n {}\n) AS result\nFROM {}", distinct, tuple, from_clause,);
6041 append_filter(&mut sql, &sel.filter);
6042 append_order_by(&mut sql, &sel.order_by);
6043 append_offset_limit(&mut sql, &sel.offset, &sel.limit);
6044
6045 let root_pointers = prepend_type(shape_pointers);
6046 SqlOutput {
6047 sql,
6048 shape: ShapeDescriptor {
6049 root: ShapeNode::Object {
6050 name: String::new(),
6051 type_name: Some(sel.type_name.clone()),
6052 position: 0,
6053 cardinality: Cardinality::Many,
6054 pointers: root_pointers,
6055 has_implicit_id: shape_has_implicit_id(&sel.shape),
6056 },
6057 },
6058 inference_plan: None,
6059 }
6060}
6061
6062pub fn emit_fn_body(ir: &crate::ir::IrOutput) -> String {
6068 with_subtype_fanouts(&ir.subtype_fanouts, || emit_output_fn_body(ir))
6069}
6070
6071fn emit_output_fn_body(ir: &crate::ir::IrOutput) -> String {
6072 let body = match &ir.stmt {
6073 IrStmt::Select(sel) if matches!(sel.rows.as_slice(), [IrRowSource::Free(IrFreeExpr::Scalar(_))]) => {
6074 let IrRowSource::Free(IrFreeExpr::Scalar(e)) = &sel.rows[0] else {
6075 unreachable!()
6076 };
6077 format!("SELECT {}", emit_expr(e))
6078 }
6079 other => emit_dml_as_cte_source(other),
6080 };
6081 if ir.ctes.is_empty() {
6082 body
6083 } else {
6084 let cte_prefix = emit_cte_prefix(&ir.ctes);
6085 format!("{}{}", cte_prefix, body)
6086 }
6087}
6088
6089fn emit_literal(lit: &IrLiteral) -> String {
6090 match lit {
6091 IrLiteral::Str(s) => sql_str(s),
6092 IrLiteral::Int(i) => i.to_string(),
6093 IrLiteral::Float(f) => {
6094 let s = f.to_string();
6103 let s = if s.contains('.') || s.contains('e') {
6104 s
6105 } else {
6106 format!("{}.0", s)
6107 };
6108 format!("({}::float8)", s)
6109 }
6110 IrLiteral::Bool(b) => {
6111 if *b {
6112 "TRUE".into()
6113 } else {
6114 "FALSE".into()
6115 }
6116 }
6117 }
6118}
6119
6120#[cfg(test)]
6123mod tests {
6124 use super::*;
6125 use crate::ir;
6126 use crate::parse;
6127 use crate::schema::{
6128 FunctionDescriptor, FunctionParamDescriptor, GlobalDescriptor, LinkDescriptor, MultiLinkDescriptor,
6129 NamedTupleDescriptor, PropertyDescriptor, SchemaDescriptor, TypeDescriptor,
6130 };
6131
6132 #[test]
6133 fn a_template_argument_keeps_the_parameters_it_carries() {
6134 let filled = fill_template("f($1, $2, $10)", &["($2)::int".to_string(), "b".to_string()]);
6137 assert_eq!(filled, "f(($2)::int, b, $10)");
6138 }
6139
6140 fn make_schema() -> SchemaDescriptor {
6141 SchemaDescriptor {
6142 types: vec![
6143 TypeDescriptor {
6144 name: "Person".into(),
6145 module: "default".into(),
6146 table: "Person".into(),
6147 abstract_: false,
6148 materialized: false,
6149 description: None,
6150 parents: vec![],
6151 interfaces: vec![],
6152 bases: vec![],
6153 properties: vec![
6154 PropertyDescriptor {
6155 name: "id".into(),
6156 pg_type: "uuid".into(),
6157 nullable: false,
6158 default_sql: Some("uuidv7()".into()),
6159 default_pyql: None,
6160 description: None,
6161 check_constraints: vec![],
6162 is_exclusive: true,
6163 is_pk: true,
6164 is_readonly: true,
6165 rewrites: vec![],
6166 tuple_members: None,
6167 column_type: None,
6168 },
6169 PropertyDescriptor {
6170 name: "name".into(),
6171 pg_type: "text".into(),
6172 nullable: false,
6173 default_sql: None,
6174 default_pyql: None,
6175 description: None,
6176 check_constraints: vec![],
6177 is_exclusive: false,
6178 is_pk: false,
6179 is_readonly: false,
6180 rewrites: vec![],
6181 tuple_members: None,
6182 column_type: None,
6183 },
6184 PropertyDescriptor {
6185 name: "age".into(),
6186 pg_type: "int8".into(),
6187 nullable: true,
6188 default_sql: None,
6189 default_pyql: None,
6190 description: None,
6191 check_constraints: vec![],
6192 is_exclusive: false,
6193 is_pk: false,
6194 is_readonly: false,
6195 rewrites: vec![],
6196 tuple_members: None,
6197 column_type: None,
6198 },
6199 ],
6200 links: vec![LinkDescriptor {
6201 name: "company".into(),
6202 target: "default::Company".into(),
6203 nullable: true,
6204 through: None,
6205 description: None,
6206 default_pyql: None,
6207 is_exclusive: false,
6208 is_readonly: false,
6209 rewrites: vec![],
6210 on_delete: vec![],
6211 }],
6212 multilinks: vec![MultiLinkDescriptor {
6213 name: "posts".into(),
6214 target: "default::Post".into(),
6215 through: None,
6216 nullable: false,
6217 description: None,
6218 default_pyql: None,
6219 on_delete: vec![],
6220 is_exclusive: false,
6221 }],
6222 computed: vec![],
6223 constraints: vec![],
6224 indexes: vec![],
6225 partition: None,
6226 vector_indexes: vec![],
6227 search_indexes: vec![],
6228 triggers: vec![],
6229 junction: false,
6230 signals: vec![],
6231 },
6232 TypeDescriptor {
6233 name: "Company".into(),
6234 module: "default".into(),
6235 table: "Company".into(),
6236 abstract_: false,
6237 materialized: false,
6238 description: None,
6239 parents: vec![],
6240 interfaces: vec![],
6241 bases: vec![],
6242 properties: vec![PropertyDescriptor {
6243 name: "name".into(),
6244 pg_type: "text".into(),
6245 nullable: false,
6246 default_sql: None,
6247 default_pyql: None,
6248 description: None,
6249 check_constraints: vec![],
6250 is_exclusive: false,
6251 is_pk: false,
6252 is_readonly: false,
6253 rewrites: vec![],
6254 tuple_members: None,
6255 column_type: None,
6256 }],
6257 links: vec![],
6258 multilinks: vec![],
6259 computed: vec![],
6260 constraints: vec![],
6261 indexes: vec![],
6262 partition: None,
6263 vector_indexes: vec![],
6264 search_indexes: vec![],
6265 triggers: vec![],
6266 junction: false,
6267 signals: vec![],
6268 },
6269 TypeDescriptor {
6270 name: "Post".into(),
6271 module: "default".into(),
6272 table: "Post".into(),
6273 abstract_: false,
6274 materialized: false,
6275 description: None,
6276 parents: vec![],
6277 interfaces: vec![],
6278 bases: vec![],
6279 properties: vec![PropertyDescriptor {
6280 name: "title".into(),
6281 pg_type: "text".into(),
6282 nullable: false,
6283 default_sql: None,
6284 default_pyql: None,
6285 description: None,
6286 check_constraints: vec![],
6287 is_exclusive: false,
6288 is_pk: false,
6289 is_readonly: false,
6290 rewrites: vec![],
6291 tuple_members: None,
6292 column_type: None,
6293 }],
6294 links: vec![],
6295 multilinks: vec![],
6296 computed: vec![],
6297 constraints: vec![],
6298 indexes: vec![],
6299 partition: None,
6300 vector_indexes: vec![],
6301 search_indexes: vec![],
6302 triggers: vec![],
6303 junction: false,
6304 signals: vec![],
6305 },
6306 ],
6307 scalars: vec![],
6308 enums: vec![],
6309 named_tuples: vec![],
6310 globals: vec![],
6311 functions: vec![],
6312 aliases: vec![],
6313 channels: vec![],
6314 ..Default::default()
6315 }
6316 }
6317
6318 fn compile_and_emit(query: &str) -> SqlOutput {
6319 let schema = make_schema();
6320 compile_and_emit_with(query, &schema)
6321 }
6322
6323 fn compile_and_emit_with(query: &str, schema: &SchemaDescriptor) -> SqlOutput {
6324 let ast = parse::parse(query).expect("parse failed");
6325 let ir = ir::compile(&ast, schema).expect("IR compile failed");
6326 emit(&ir)
6327 }
6328
6329 #[test]
6334 fn a_multilink_walk_can_be_tested_against_a_set_literal() {
6335 let out = compile_and_emit("SELECT Person FILTER .posts.title IN {'a', 'b'}");
6336 assert!(out.sql.contains("= ANY(ARRAY['a', 'b'])"), "{}", out.sql);
6337 }
6338
6339 #[test]
6343 fn all_over_a_multilink_is_not_the_same_question_as_any() {
6344 for condition in [
6345 ".posts.title IN {'a', 'b'}",
6346 ".posts IN ps",
6347 ".posts = ps",
6348 ".posts.title = 'x'",
6349 ] {
6350 let sql = |quantifier: &str| {
6351 compile_and_emit(&format!(
6352 "WITH ps := (SELECT Post) SELECT Person FILTER {quantifier}({condition})"
6353 ))
6354 .sql
6355 };
6356 assert_ne!(
6357 sql("all"),
6358 sql("any"),
6359 "all({condition}) must not compile to what any({condition}) does"
6360 );
6361 }
6362 }
6363
6364 #[test]
6368 fn all_over_a_multilink_answers_for_every_element() {
6369 let out = compile_and_emit("SELECT Person FILTER all(.posts.title IN {'a', 'b'})");
6370 assert!(
6371 out.sql.contains("NOT EXISTS(") && out.sql.contains("NOT (\"t2\".\"title\" = ANY("),
6372 "no element fails the test:\n{}",
6373 out.sql
6374 );
6375 let out = compile_and_emit("WITH ps := (SELECT Post) SELECT Person FILTER all(.posts IN ps)");
6376 assert!(
6377 out.sql
6378 .contains("coalesce(bool_and(\"_s\".\"v\"), true) FROM unnest(ARRAY(SELECT ("),
6379 "every answer, reduced:\n{}",
6380 out.sql
6381 );
6382 }
6383
6384 #[test]
6387 fn any_over_a_multilink_keeps_its_exists() {
6388 let out = compile_and_emit("WITH ps := (SELECT Post) SELECT Person FILTER any(.posts IN ps)");
6389 assert!(out.sql.contains("WHERE EXISTS("), "{}", out.sql);
6390 assert!(!out.sql.contains("bool_or"), "{}", out.sql);
6391 }
6392
6393 #[test]
6399 fn comparing_a_multilink_to_a_multi_row_binding_is_membership() {
6400 let out = compile_and_emit("WITH ps := (SELECT Post) SELECT Person FILTER .posts = ps");
6401 assert!(
6402 out.sql.contains("\"target\" = ANY((SELECT \"id\" FROM \"ps\"))"),
6403 "{}",
6404 out.sql
6405 );
6406 let out = compile_and_emit("WITH ps := (SELECT Post) SELECT Person FILTER .posts != ps");
6407 assert!(
6408 out.sql
6409 .contains("NOT (\"t2\".\"target\" = ANY((SELECT \"id\" FROM \"ps\")))"),
6410 "{}",
6411 out.sql
6412 );
6413 }
6414
6415 #[test]
6417 fn comparing_a_multilink_to_a_single_row_binding_stays_an_equality() {
6418 let out = compile_and_emit("WITH p := (SELECT Post LIMIT 1) SELECT Person FILTER .posts = p");
6419 assert!(
6420 out.sql.contains("\"target\" = (SELECT \"id\" FROM \"p\")"),
6421 "{}",
6422 out.sql
6423 );
6424 }
6425
6426 #[test]
6432 fn membership_of_one_set_in_another_answers_per_element() {
6433 let out = compile_and_emit("WITH a := (SELECT Person), b := (SELECT Person) SELECT a IN b");
6434 assert!(
6435 out.sql.contains("FROM \"a\""),
6436 "the left set is the source:\n{}",
6437 out.sql
6438 );
6439 assert!(
6440 out.sql.contains("\"id\" = ANY((SELECT \"id\" FROM \"b\"))"),
6441 "each row tests its own id:\n{}",
6442 out.sql
6443 );
6444 assert!(
6445 !out.sql.contains("(SELECT \"id\" FROM \"a\") = ANY"),
6446 "the left set must not collapse to one value:\n{}",
6447 out.sql
6448 );
6449 }
6450
6451 #[test]
6453 fn membership_of_a_type_in_a_set_answers_per_row() {
6454 let out = compile_and_emit("WITH b := (SELECT Person) SELECT Person NOT IN b");
6455 assert!(
6456 out.sql.contains("FROM \"public\".\"Person\""),
6457 "the type is the source:\n{}",
6458 out.sql
6459 );
6460 assert!(
6461 out.sql.contains("\"id\" <> ALL((SELECT \"id\" FROM \"b\"))"),
6462 "{}",
6463 out.sql
6464 );
6465 }
6466
6467 #[test]
6471 fn quantifying_membership_of_one_set_in_another_aggregates_the_elements() {
6472 for (query, aggregate) in [("SELECT all(a IN b)", "bool_and"), ("SELECT any(a IN b)", "bool_or")] {
6473 let out = compile_and_emit(&format!("WITH a := (SELECT Person), b := (SELECT Person) {query}"));
6474 assert!(
6475 out.sql.contains(&format!("{aggregate}(\"_s\".\"v\")")),
6476 "{query}:\n{}",
6477 out.sql
6478 );
6479 assert!(
6480 out.sql.contains("FROM unnest(ARRAY(SELECT ("),
6481 "one answer per element, then reduced — {query}:\n{}",
6482 out.sql
6483 );
6484 assert!(
6485 !out.sql.contains("(SELECT \"id\" FROM \"a\") = ANY"),
6486 "the left set must not collapse to one value — {query}:\n{}",
6487 out.sql
6488 );
6489 }
6490 }
6491
6492 #[test]
6496 fn membership_of_a_multilink_is_a_boolean_per_element() {
6497 let out = compile_and_emit("WITH ps := (SELECT Post) SELECT Person { t := .posts IN ps }");
6498 assert!(
6499 out.sql
6500 .contains("ARRAY(SELECT (\"t3\".\"id\" = ANY((SELECT \"id\" FROM \"ps\")))"),
6501 "one answer per post:\n{}",
6502 out.sql
6503 );
6504 assert!(
6505 !out.sql.contains("EXISTS("),
6506 "not one answer for the link:\n{}",
6507 out.sql
6508 );
6509 }
6510
6511 #[test]
6514 fn membership_of_a_multilink_in_a_filter_stays_one_answer() {
6515 let out = compile_and_emit("WITH ps := (SELECT Post) SELECT Person FILTER .posts IN ps");
6516 assert!(out.sql.contains("WHERE EXISTS("), "{}", out.sql);
6517 }
6518
6519 #[test]
6523 fn quantifying_membership_of_a_multilink_aggregates_its_elements() {
6524 let out = compile_and_emit("WITH ps := (SELECT Post) SELECT Person { t := all(.posts IN ps) }");
6525 assert!(
6526 out.sql
6527 .contains("coalesce(bool_and(\"_s\".\"v\"), true) FROM unnest(ARRAY(SELECT ("),
6528 "{}",
6529 out.sql
6530 );
6531 let out = compile_and_emit("WITH ps := (SELECT Post) SELECT Person { t := any(.posts IN ps) }");
6532 assert!(
6533 out.sql
6534 .contains("coalesce(bool_or(\"_s\".\"v\"), false) FROM unnest(ARRAY(SELECT ("),
6535 "{}",
6536 out.sql
6537 );
6538 }
6539
6540 #[test]
6543 fn membership_in_a_set_literal_answers_per_element() {
6544 let out = compile_and_emit("SELECT Person { t := .posts.title IN {'a', 'b'} }");
6545 assert!(
6546 out.sql
6547 .contains("ARRAY(SELECT (\"t2\".\"title\" = ANY(ARRAY['a', 'b']))"),
6548 "{}",
6549 out.sql
6550 );
6551 }
6552
6553 #[test]
6561 fn test_a_mutating_loop_as_a_select_source_is_bound_to_a_cte() {
6562 let out =
6563 compile_and_emit("SELECT (FOR p IN (SELECT Person) UNION (INSERT Post { title := p.name })) { title }");
6564 assert!(
6565 !out.sql.contains("LATERAL"),
6566 "the insert must not sit in a LATERAL, got:\n{}",
6567 out.sql
6568 );
6569 assert!(
6570 out.sql
6571 .contains("\"_nested_dml_0__ids\" AS (\nINSERT INTO \"public\".\"Post\""),
6572 "the loop's insert belongs in a top-level CTE, got:\n{}",
6573 out.sql
6574 );
6575 assert!(
6576 out.sql.contains("FROM \"_nested_dml_0\" AS "),
6577 "the select must read the bound loop back, got:\n{}",
6578 out.sql
6579 );
6580 }
6581
6582 #[test]
6586 fn test_a_loop_updating_its_own_variable_compiles_as_a_select_source() {
6587 let out = compile_and_emit("SELECT (FOR p IN (SELECT Person) UNION (UPDATE p SET { age := 1 })) { name }");
6588 assert!(
6589 out.sql
6590 .contains("\"_nested_dml_0__ids\" AS (\nUPDATE \"public\".\"Person\""),
6591 "got:\n{}",
6592 out.sql
6593 );
6594 assert!(!out.sql.contains("LATERAL"), "got:\n{}", out.sql);
6595 }
6596
6597 #[test]
6606 fn test_a_guarded_insert_reaches_a_binding_inside_a_nested_loop() {
6607 let out = compile_and_emit(
6608 "WITH existing := (SELECT Company FILTER .name = 'x'), \
6609 made := (INSERT Person { name := 'a', \
6610 posts := (FOR s IN {'p','q'} UNION ( \
6611 WITH extra := (INSERT Company { name := s }) \
6612 INSERT Post { title := s })) }) \
6613 IF NOT EXISTS existing ELSE {} \
6614 SELECT made",
6615 );
6616 let at = out
6617 .sql
6618 .find("INSERT INTO \"public\".\"Company\"")
6619 .unwrap_or_else(|| panic!("no Company insert in:\n{}", out.sql));
6620 let tail = &out.sql[at..];
6621 let stmt_end = tail.find("RETURNING").unwrap_or(tail.len());
6622 assert!(
6623 tail[..stmt_end].contains("NOT EXISTS(SELECT 1 FROM \"existing\")"),
6624 "a binding inside the loop must carry the condition too, got:\n{}",
6625 out.sql
6626 );
6627 }
6628
6629 #[test]
6634 fn test_a_guarded_insert_carries_its_condition_into_what_it_nests() {
6635 let out = compile_and_emit(
6636 "WITH existing := (SELECT Company FILTER .name = 'x'), \
6637 made := (INSERT Person { name := 'a', company := (INSERT Company { name := 'b' }), \
6638 posts := (FOR s IN {'p','q'} UNION (INSERT Post { title := s })) }) \
6639 IF NOT EXISTS existing ELSE {} \
6640 SELECT made",
6641 );
6642 for table in ["\"Person\"", "\"Company\" (", "\"Post\""] {
6643 let at = out
6644 .sql
6645 .find(&format!("INSERT INTO \"public\".{}", table))
6646 .unwrap_or_else(|| panic!("no insert into {} in:\n{}", table, out.sql));
6647 let tail = &out.sql[at..];
6648 let stmt_end = tail.find("RETURNING").unwrap_or(tail.len());
6649 assert!(
6650 tail[..stmt_end].contains("NOT EXISTS(SELECT 1 FROM \"existing\")"),
6651 "insert into {} writes whatever the condition says, got:\n{}",
6652 table,
6653 out.sql
6654 );
6655 }
6656 }
6657
6658 #[test]
6659 fn test_a_guarded_insert_keeps_its_guard_when_it_nests_another() {
6660 let out = compile_and_emit(
6661 "WITH existing := (SELECT Company FILTER .name = 'x'), \
6662 made := (INSERT Person { name := 'a', company := (INSERT Company { name := 'b' }) }) \
6663 IF NOT EXISTS existing ELSE {} \
6664 SELECT made",
6665 );
6666 let outer = out
6667 .sql
6668 .find("INSERT INTO \"public\".\"Person\"")
6669 .unwrap_or_else(|| panic!("no Person insert in:\n{}", out.sql));
6670 let tail = &out.sql[outer..];
6671 let stmt_end = tail.find("RETURNING").unwrap_or(tail.len());
6672 assert!(
6673 tail[..stmt_end].contains("NOT EXISTS(SELECT 1 FROM \"existing\")"),
6674 "the insert the conditional names must carry the condition, got:\n{}",
6675 out.sql
6676 );
6677 }
6678
6679 #[test]
6680 fn a_nested_read_inside_a_with_opens_no_second_with() {
6681 let out = compile_and_emit_with(
6682 "WITH a := (SELECT Person FILTER .age > 1), b := (SELECT Person LIMIT 1) SELECT (SELECT (a UNION b))",
6683 &make_schema(),
6684 );
6685 assert_eq!(out.sql.matches("WITH").count(), 1, "{}", out.sql);
6686 }
6687
6688 #[test]
6689 fn a_comparison_against_a_multi_row_with_binding_tests_membership() {
6690 let out = compile_and_emit_with(
6694 "WITH companies := (SELECT Company FILTER .name = 'x') SELECT Person FILTER .company = companies",
6695 &make_schema(),
6696 );
6697 assert!(
6698 out.sql.contains("= ANY((SELECT \"id\" FROM \"companies\"))"),
6699 "{}",
6700 out.sql
6701 );
6702
6703 let out = compile_and_emit_with(
6704 "WITH companies := (SELECT Company) SELECT Person FILTER any(.company = companies)",
6705 &make_schema(),
6706 );
6707 assert!(
6708 out.sql.contains("= ANY((SELECT \"id\" FROM \"companies\"))"),
6709 "{}",
6710 out.sql
6711 );
6712
6713 let out = compile_and_emit_with(
6714 "WITH companies := (SELECT Company) SELECT Person FILTER .company != companies",
6715 &make_schema(),
6716 );
6717 assert!(out.sql.contains("NOT (\"t1\".\"company_id\" = ANY("), "{}", out.sql);
6718 }
6719
6720 #[test]
6721 fn a_comparison_against_a_single_row_with_binding_stays_a_scalar_read() {
6722 let out = compile_and_emit_with(
6723 "WITH company := (SELECT Company LIMIT 1) SELECT Person FILTER .company = company",
6724 &make_schema(),
6725 );
6726 assert!(out.sql.contains("= (SELECT \"id\" FROM \"company\")"), "{}", out.sql);
6727 }
6728
6729 #[test]
6730 fn an_aggregate_over_a_backlink_counts_inside_a_subquery() {
6731 let out = compile_and_emit_with("SELECT Post { n := count(.<posts) }", &make_schema());
6732 assert!(
6733 !out.sql.contains("count((SELECT"),
6734 "an aggregate over a scalar subquery:\n{}",
6735 out.sql
6736 );
6737 assert!(out.sql.contains("count("), "{}", out.sql);
6738 }
6739
6740 #[test]
6741 fn an_aggregate_over_a_relative_subselect_counts_inside_a_subquery() {
6742 let out = compile_and_emit_with(
6743 "SELECT Post { n := count((SELECT .<posts FILTER .age > 1)) }",
6744 &make_schema(),
6745 );
6746 assert!(
6747 !out.sql.contains("count((SELECT"),
6748 "an aggregate over a scalar subquery:\n{}",
6749 out.sql
6750 );
6751 assert!(
6752 out.sql.contains(r#"(SELECT count("_s"."v") FROM unnest(ARRAY("#),
6753 "{}",
6754 out.sql
6755 );
6756 }
6757
6758 #[test]
6759 fn any_over_a_link_type_check_tests_each_element() {
6760 let out = compile_and_emit_with(
6761 "SELECT Company { name } FILTER any(.<company.posts IS Post)",
6762 &make_schema(),
6763 );
6764 assert!(
6765 !out.sql.contains("bool_or(ARRAY"),
6766 "an aggregate over outer rows:\n{}",
6767 out.sql
6768 );
6769 assert!(
6770 out.sql
6771 .contains("FROM unnest(ARRAY(SELECT ('default::Post' = 'default::Post')"),
6772 "{}",
6773 out.sql
6774 );
6775 }
6776
6777 #[test]
6778 fn a_link_type_check_reads_the_linked_object() {
6779 let out = compile_and_emit_with("SELECT Person { name } FILTER .company IS Company", &make_schema());
6780 assert!(!out.sql.contains("WHERE FALSE"), "{}", out.sql);
6781 assert!(out.sql.contains("'default::Company'"), "{}", out.sql);
6782 }
6783
6784 fn schema_with_a_subtype() -> SchemaDescriptor {
6786 let mut schema = make_schema();
6787 let mut admin = schema.types[0].clone();
6788 admin.name = "Admin".into();
6789 admin.table = "Admin".into();
6790 admin.bases = vec!["default::Person".into()];
6791 schema.types.push(admin);
6792 schema
6793 }
6794
6795 #[test]
6796 fn a_type_with_subtypes_reads_their_rows_too() {
6797 let out = compile_and_emit_with("SELECT Person { name }", &schema_with_a_subtype());
6798 assert!(
6799 out.sql.contains("'default::Admin'::text AS \"__type__\"") && out.sql.contains("FROM \"public\".\"Admin\""),
6800 "expected the subtype's table in the read:\n{}",
6801 out.sql
6802 );
6803 }
6804
6805 #[test]
6806 fn a_path_from_a_type_with_subtypes_reads_their_rows_too() {
6807 let out = compile_and_emit_with("SELECT Person.name", &schema_with_a_subtype());
6808 assert!(out.sql.contains("FROM \"public\".\"Admin\""), "{}", out.sql);
6809 }
6810
6811 #[test]
6812 fn a_multilink_of_a_type_with_subtypes_reads_their_junctions_too() {
6813 let out = compile_and_emit_with("SELECT Person { posts: { title } }", &schema_with_a_subtype());
6814 assert!(
6815 out.sql.contains(
6816 "(SELECT \"source\", \"target\" FROM \"public\".\"Person.posts\" UNION ALL \
6817 SELECT \"source\", \"target\" FROM \"public\".\"Admin.posts\")"
6818 ),
6819 "expected the junctions unioned:\n{}",
6820 out.sql
6821 );
6822 }
6823
6824 #[test]
6825 fn is_a_supertype_matches_its_subtypes() {
6826 let out = compile_and_emit_with(
6827 "SELECT Person { name } FILTER Person IS Admin",
6828 &schema_with_a_subtype(),
6829 );
6830 assert!(out.sql.contains("\"__type__\" = 'default::Admin'"), "{}", out.sql);
6831 }
6832
6833 #[test]
6834 fn a_write_to_a_type_with_subtypes_reaches_their_tables() {
6835 let out = compile_and_emit_with("UPDATE Person SET { age := 1 }", &schema_with_a_subtype());
6836 assert!(
6837 out.sql.contains("UPDATE \"public\".\"Person\"") && out.sql.contains("UPDATE \"public\".\"Admin\""),
6838 "expected both tables updated:\n{}",
6839 out.sql
6840 );
6841 assert!(
6842 !out.sql.contains("UPDATE (\n"),
6843 "a write never targets the union:\n{}",
6844 out.sql
6845 );
6846 }
6847
6848 #[test]
6849 fn test_comparing_a_multilink_itself_to_an_object() {
6850 let out = compile_and_emit_with(
6854 "WITH p := (SELECT Post LIMIT 1) SELECT Person { id } FILTER any(.posts = p)",
6855 &make_schema(),
6856 );
6857 assert!(
6858 out.sql.contains("EXISTS((SELECT 1\nFROM \"public\".\"Person.posts\""),
6859 "expected a junction EXISTS:\n{}",
6860 out.sql
6861 );
6862 assert!(
6863 out.sql.contains("\"target\" = (SELECT \"id\" FROM \"p\")"),
6864 "expected the junction target compared to the bound object's id:\n{}",
6865 out.sql
6866 );
6867 }
6868
6869 #[test]
6870 fn test_comparing_a_multilink_itself_warns_without_any() {
6871 let ast = parse::parse("WITH p := (SELECT Post LIMIT 1) SELECT Person { id } FILTER .posts = p").unwrap();
6872 let ir = ir::compile(&ast, &make_schema()).unwrap();
6873 assert!(
6874 ir.warnings.iter().any(|w| w.contains("multi-link '.posts'")),
6875 "expected the set-valued FILTER warning, got: {:?}",
6876 ir.warnings
6877 );
6878 }
6879
6880 #[test]
6881 fn test_detached_names_the_type_of_a_nested_select() {
6882 let out = compile_and_emit_with(
6886 "INSERT Person { name := $n, company := (SELECT detached Company FILTER .name = $c LIMIT 1) }",
6887 &make_schema(),
6888 );
6889 assert!(
6890 out.sql.contains("INSERT INTO \"public\".\"Person\""),
6891 "expected the insert to compile:\n{}",
6892 out.sql
6893 );
6894 assert!(
6895 out.sql.contains("FROM \"public\".\"Company\""),
6896 "expected the detached select to read the Company table:\n{}",
6897 out.sql
6898 );
6899 }
6900
6901 #[test]
6902 fn test_select_over_an_update_on_a_with_binding() {
6903 let out = compile_and_emit_with(
6908 "WITH p := (SELECT Person FILTER .id = $a LIMIT 1) SELECT (UPDATE p SET { name := $n }) { id }",
6909 &make_schema(),
6910 );
6911 assert!(
6912 out.sql.contains("UPDATE \"public\".\"Person\""),
6913 "expected the update to resolve to the bound type's table:\n{}",
6914 out.sql
6915 );
6916 }
6917
6918 #[test]
6919 fn test_a_mutation_as_a_free_object_field() {
6920 let out = compile_and_emit_with(
6924 "SELECT { renamed := (UPDATE Person FILTER .id = $a SET { name := $n }), n := 1 }",
6925 &make_schema(),
6926 );
6927 assert!(
6928 out.sql.contains("UPDATE \"public\".\"Person\""),
6929 "expected the mutation to become a data-modifying CTE:\n{}",
6930 out.sql
6931 );
6932 assert!(
6933 out.sql
6934 .contains("(SELECT \"t1\".\"id\"\nFROM \"_nested_dml_0\" AS \"t1\") AS \"_f0\""),
6935 "expected the field to carry the rows the mutation touched:\n{}",
6936 out.sql
6937 );
6938 }
6939
6940 #[test]
6941 fn test_a_sub_selects_filter_stays_on_its_own_subject_when_a_field_chain_projects_off_it() {
6942 let out = compile_and_emit_with("SELECT (SELECT Person FILTER .name = $n).company.name", &make_schema());
6946 assert!(
6947 out.sql.contains("WHERE (\"t0\".\"name\" = $1)"),
6948 "the inner filter belongs to the Person alias:\n{}",
6949 out.sql
6950 );
6951 }
6952
6953 #[test]
6954 fn test_reading_back_a_nested_insert_through_its_own_statement() {
6955 let out = compile_and_emit_with(
6956 "SELECT (INSERT Person { name := $n, posts := (INSERT Post { title := $t }) }) { id, posts: { title } }",
6957 &make_schema(),
6958 );
6959 assert!(out.sql.contains("__ml_add_0"), "got:\n{}", out.sql);
6960 }
6961
6962 #[test]
6963 fn test_reading_back_a_nested_insert_within_its_own_statement() {
6964 let out = compile_and_emit_with(
6968 "SELECT (INSERT Person { name := $n, posts := (INSERT Post { title := $t }) }) \
6969 { id, posts: { title } }",
6970 &make_schema(),
6971 );
6972 assert!(
6973 out.sql.contains("FROM \"_dml__ml_add_0\" AS \"jt\""),
6974 "the junction rows must come from the CTE that wrote them:\n{}",
6975 out.sql
6976 );
6977 assert!(
6978 out.sql.contains("\"_nested_dml_0\""),
6979 "and the targets from the nested insert's own CTE:\n{}",
6980 out.sql
6981 );
6982 }
6983
6984 #[test]
6985 fn test_a_nested_single_link_insert_reads_back_from_its_cte() {
6986 let out = compile_and_emit_with(
6987 "SELECT (INSERT Person { name := $n, company := (INSERT Company { name := $c }) }) \
6988 { id, company: { name } }",
6989 &make_schema(),
6990 );
6991 assert!(
6992 out.sql.contains("FROM \"_nested_dml_0\""),
6993 "the linked row must be read from the CTE that inserted it:\n{}",
6994 out.sql
6995 );
6996 }
6997
6998 #[test]
6999 fn test_a_free_object_field_holding_an_object_keeps_it_an_object() {
7000 let out = compile_and_emit_with(
7004 "WITH c := (SELECT Company LIMIT 1) SELECT { co := c { name }, n := 1 }",
7005 &make_schema(),
7006 );
7007 assert!(
7008 out.sql.contains("'default::Company'::text"),
7009 "the field must carry the object's own row:\n{}",
7010 out.sql
7011 );
7012 }
7013
7014 #[test]
7015 fn test_projecting_an_object_off_a_free_object() {
7016 let out = compile_and_emit_with(
7019 "WITH c := (SELECT Company LIMIT 1) SELECT { co := c { name }, n := 1 }.co",
7020 &make_schema(),
7021 );
7022 assert!(
7023 !out.sql.contains("jsonb_build_object"),
7024 "projecting a pointer must not go through jsonb:\n{}",
7025 out.sql
7026 );
7027 assert!(
7028 out.sql.contains("SELECT v AS result, v FROM"),
7029 "the object row is already the result row:\n{}",
7030 out.sql
7031 );
7032 }
7033
7034 #[test]
7035 fn test_a_shape_on_a_single_valued_object_path() {
7036 let out = compile_and_emit_with("SELECT Person { c := .company { name } }", &make_schema());
7041 assert!(
7042 out.sql.contains("'default::Company'::text"),
7043 "the pointer must carry the object's own row:\n{}",
7044 out.sql
7045 );
7046 assert!(
7047 !out.sql.contains("ARRAY(SELECT"),
7048 "a single-valued walk is one object, not an array of one:\n{}",
7049 out.sql
7050 );
7051 }
7052
7053 #[test]
7054 fn test_a_shapeless_single_link_still_reads_as_its_value() {
7055 let out = compile_and_emit_with("SELECT Person { c := .company }", &make_schema());
7058 assert!(
7059 !out.sql.contains("'default::Company'::text"),
7060 "a bare link reference must not grow an object row:\n{}",
7061 out.sql
7062 );
7063 }
7064
7065 #[test]
7066 fn test_a_tuple_element_holding_a_shaped_object() {
7067 let out = compile_and_emit_with(
7071 "WITH c := (SELECT Company LIMIT 1) SELECT (c { name }, 1)",
7072 &make_schema(),
7073 );
7074 assert!(
7075 out.sql.contains("'default::Company'::text"),
7076 "the element must carry the object's own row:\n{}",
7077 out.sql
7078 );
7079 }
7080
7081 #[test]
7082 fn test_an_object_returning_function_as_a_link_value() {
7083 let mut schema = make_schema();
7088 let company = schema
7090 .types
7091 .iter_mut()
7092 .find(|t| t.name == "Company")
7093 .expect("make_schema declares Company");
7094 company.properties.push(PropertyDescriptor {
7095 name: "id".into(),
7096 pg_type: "uuid".into(),
7097 nullable: false,
7098 default_sql: None,
7099 default_pyql: None,
7100 description: None,
7101 check_constraints: vec![],
7102 is_exclusive: true,
7103 is_pk: true,
7104 is_readonly: true,
7105 rewrites: vec![],
7106 tuple_members: None,
7107 column_type: None,
7108 });
7109 schema.functions.push(crate::schema::FunctionDescriptor {
7110 name: "current_company".into(),
7111 module: "default".into(),
7112 params: vec![],
7113 return_pg_type: "default::Company".into(),
7114 return_is_object: true,
7115 return_is_set: false,
7116 return_is_polymorphic: false,
7117 volatility: "stable".into(),
7118 body: "select Company limit 1".into(),
7119 });
7120 let out = compile_and_emit_with("INSERT Person { name := $n, company := current_company() }", &schema);
7121 assert!(
7122 out.sql.contains("FROM \"public\".\"current_company\"()"),
7123 "the function must supply the foreign key:\n{}",
7124 out.sql
7125 );
7126 }
7127
7128 fn make_schema_with_an_object_returning_default() -> SchemaDescriptor {
7132 let mut schema = make_schema();
7133 let company = schema
7134 .types
7135 .iter_mut()
7136 .find(|t| t.name == "Company")
7137 .expect("make_schema declares Company");
7138 company.properties.push(PropertyDescriptor {
7139 name: "id".into(),
7140 pg_type: "uuid".into(),
7141 nullable: false,
7142 default_sql: None,
7143 default_pyql: None,
7144 description: None,
7145 check_constraints: vec![],
7146 is_exclusive: true,
7147 is_pk: true,
7148 is_readonly: true,
7149 rewrites: vec![],
7150 tuple_members: None,
7151 column_type: None,
7152 });
7153 schema.functions.push(crate::schema::FunctionDescriptor {
7154 name: "current_company".into(),
7155 module: "default".into(),
7156 params: vec![],
7157 return_pg_type: "default::Company".into(),
7158 return_is_object: true,
7159 return_is_set: false,
7160 return_is_polymorphic: false,
7161 volatility: "stable".into(),
7162 body: "select Company limit 1".into(),
7163 });
7164 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
7165 let company_link = person.links.iter_mut().find(|l| l.name == "company").unwrap();
7166 company_link.default_pyql = Some("current_company()".into());
7167 schema
7168 }
7169
7170 #[test]
7171 fn an_object_returning_link_default_is_applied_by_the_insert() {
7172 let schema = make_schema_with_an_object_returning_default();
7176 let out = compile_and_emit_with("INSERT Person { name := $n }", &schema);
7177 assert!(
7178 out.sql.contains("\"company_id\"") && out.sql.contains("current_company"),
7179 "the default must supply the foreign key:\n{}",
7180 out.sql
7181 );
7182 }
7183
7184 #[test]
7185 fn an_explicit_value_wins_over_an_inlined_default() {
7186 let schema = make_schema_with_an_object_returning_default();
7187 let out = compile_and_emit_with("INSERT Person { name := $n, company := {} }", &schema);
7188 assert!(
7189 !out.sql.contains("current_company"),
7190 "the shape named the pointer, so the default does not apply:\n{}",
7191 out.sql
7192 );
7193 }
7194
7195 #[test]
7196 fn an_object_returning_link_default_gets_no_column_default() {
7197 let schema = make_schema_with_an_object_returning_default();
7200 let ddl = crate::export::export_schema(&schema).unwrap();
7201 assert!(
7202 !ddl.contains("\"company_id\" uuid DEFAULT"),
7203 "the column must carry no default:\n{ddl}"
7204 );
7205 crate::validate::validate_schema_types(&schema).expect("an inlined default is not an error");
7206 }
7207
7208 #[test]
7209 fn a_column_expressible_default_stays_in_the_ddl() {
7210 let mut schema = make_schema();
7212 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
7213 let age = person.properties.iter_mut().find(|p| p.name == "age").unwrap();
7214 age.default_pyql = Some("21".into());
7215 assert!(
7216 crate::ir::inlined_pointer_defaults(schema.types.iter().find(|t| t.name == "Person").unwrap(), &schema)
7217 .is_empty()
7218 );
7219 let ddl = crate::export::export_schema(&schema).unwrap();
7220 assert!(ddl.contains("DEFAULT 21"), "{ddl}");
7221 let out = compile_and_emit_with("INSERT Person { name := $n }", &schema);
7222 assert!(
7223 !out.sql.contains("21"),
7224 "the column DEFAULT still applies it:\n{}",
7225 out.sql
7226 );
7227 }
7228
7229 fn make_schema_with_an_inherited_self_naming_computed() -> SchemaDescriptor {
7233 let mut schema = make_schema();
7234 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
7235 person.computed.push(crate::schema::ComputedDescriptor {
7236 name: "own_name".into(),
7237 expression: "default::Person.name".into(),
7238 return_type: Some("text".into()),
7239 link_target: None,
7240 link_multi: false,
7241 });
7242 let mut employee = person.clone();
7243 employee.name = "Employee".into();
7244 employee.table = "Employee".into();
7245 employee.bases = vec!["default::Person".into()];
7246 schema.types.push(employee);
7247 schema
7248 }
7249
7250 #[test]
7251 fn an_inherited_computed_naming_its_declaring_type_still_means_the_row() {
7252 let schema = make_schema_with_an_inherited_self_naming_computed();
7257 let out = compile_and_emit_with("SELECT Employee { own_name }", &schema);
7258 assert!(
7259 !out.sql.contains("\"Person\""),
7260 "the computed must read the Employee row, not Person's table:\n{}",
7261 out.sql
7262 );
7263 crate::validate::validate_schema_types(&schema).expect("an inherited computed is single-valued");
7264 }
7265
7266 #[test]
7267 fn an_inherited_type_check_computed_tests_the_row_it_is_read_on() {
7268 let mut schema = make_schema();
7274 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
7275 person.computed.push(crate::schema::ComputedDescriptor {
7276 name: "employed".into(),
7277 expression: "(default::Person is default::Employee)".into(),
7278 return_type: Some("boolean".into()),
7279 link_target: None,
7280 link_multi: false,
7281 });
7282 let mut employee = person.clone();
7283 employee.name = "Employee".into();
7284 employee.table = "Employee".into();
7285 employee.bases = vec!["default::Person".into()];
7286 schema.types.push(employee);
7287
7288 crate::validate::validate_schema_types(&schema).expect("the check is single-valued on both types");
7289 let out = compile_and_emit_with("SELECT Employee { employed }", &schema);
7290 assert!(
7291 !out.sql.contains("\"Person\""),
7292 "the check must read the Employee row's own type:\n{}",
7293 out.sql
7294 );
7295 }
7296
7297 fn make_interface_schema_with_an_outsider() -> SchemaDescriptor {
7304 let mut schema = make_interface_schema();
7305 let individual = schema
7306 .types
7307 .iter()
7308 .find(|t| t.name == "Individual")
7309 .expect("Individual is in the interface schema")
7310 .clone();
7311 let mut company = individual.clone();
7312 company.name = "Company".into();
7313 company.table = "Company".into();
7314 company.interfaces = vec![];
7315 company.computed = vec![];
7316 schema.types.push(company);
7317 let mut organisation = individual;
7318 organisation.name = "Organisation".into();
7319 organisation.table = "Organisation".into();
7320 organisation.computed = vec![];
7321 schema.types.push(organisation);
7322 schema
7323 }
7324
7325 #[test]
7326 fn a_type_check_naming_another_type_reads_as_one_value_in_a_filter() {
7327 let out = compile_and_emit(
7335 "SELECT Person { name } FILTER (.name = 'a' if default::Company is default::Post else FALSE)",
7336 );
7337 assert!(
7338 !out.sql.contains("ARRAY(SELECT"),
7339 "an answer that is the same for every row has nothing to iterate:\n{}",
7340 out.sql
7341 );
7342 assert!(
7343 out.sql.contains("CASE WHEN FALSE"),
7344 "a Company is never a Post:\n{}",
7345 out.sql
7346 );
7347 }
7348
7349 #[test]
7350 fn a_polymorphic_type_is_never_one_from_a_hierarchy_it_shares_nothing_with() {
7351 let schema = make_interface_schema_with_an_outsider();
7358 let out = compile_and_emit_with(
7359 "SELECT Company { id } FILTER (.first_name = 'a' if default::Account is default::Company else FALSE)",
7360 &schema,
7361 );
7362 assert!(
7363 !out.sql.contains("assert_single") && !out.sql.contains("ARRAY(SELECT"),
7364 "nothing to ask per row when the hierarchies are disjoint:\n{}",
7365 out.sql
7366 );
7367 assert!(
7368 out.sql.contains("CASE WHEN FALSE"),
7369 "an Account is never a Company:\n{}",
7370 out.sql
7371 );
7372 }
7373
7374 #[test]
7375 fn a_check_covering_every_implementor_holds_without_reading_a_row() {
7376 let mut schema = make_interface_schema_with_an_outsider();
7380 schema.types.retain(|t| t.name != "Organisation");
7381 let out = compile_and_emit_with(
7382 "SELECT Company { id } FILTER (default::Account is default::Individual)",
7383 &schema,
7384 );
7385 assert!(
7386 !out.sql.contains("assert_single"),
7387 "every Account is an Individual when it is the only implementor:\n{}",
7388 out.sql
7389 );
7390 assert!(out.sql.contains("WHERE TRUE"), "{}", out.sql);
7391 }
7392
7393 #[test]
7394 fn a_type_check_over_a_polymorphic_outsider_collapses_to_one_value() {
7395 let schema = make_interface_schema_with_an_outsider();
7400 let out = compile_and_emit_with(
7401 "SELECT Company { id } FILTER (default::Account is default::Individual)",
7402 &schema,
7403 );
7404 assert!(
7405 out.sql.contains("\"_pylon\".\"assert_single\"(ARRAY(SELECT"),
7406 "one answer per Account row, read as a single value:\n{}",
7407 out.sql
7408 );
7409 assert!(
7410 out.sql
7411 .contains("is default::Individual'' is asked once for every default::Account object"),
7412 "the error has to name the check, not assert_single:\n{}",
7413 out.sql
7414 );
7415 }
7416
7417 #[test]
7418 fn a_type_check_naming_another_type_keeps_its_whole_set_in_a_shape() {
7419 let schema = make_interface_schema_with_an_outsider();
7422 let out = compile_and_emit_with(
7423 "SELECT Company { id, each := default::Account is default::Individual }",
7424 &schema,
7425 );
7426 assert!(
7427 out.sql.contains("array_agg") && !out.sql.contains("assert_single"),
7428 "a shape keeps every answer:\n{}",
7429 out.sql
7430 );
7431 }
7432
7433 #[test]
7434 fn a_shape_writing_the_computed_out_itself_does_not_get_the_anchor() {
7435 let schema = make_schema_with_an_inherited_self_naming_computed();
7443 let ast = parse::parse("SELECT Employee { own_name := default::Person.name }").expect("parse failed");
7444 let Err(error) = ir::compile(&ast, &schema) else {
7445 panic!("an absolute path has nothing to anchor to here")
7446 };
7447 assert!(
7448 format!("{error:?}").contains("absolute paths are not valid"),
7449 "{error:?}"
7450 );
7451 }
7452
7453 #[test]
7454 fn a_default_that_compiles_nowhere_is_still_an_error() {
7455 let mut schema = make_schema();
7458 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
7459 let age = person.properties.iter_mut().find(|p| p.name == "age").unwrap();
7460 age.default_pyql = Some("sequence_next(INTROSPECT default::AgeSeq)".into());
7461 let errs = crate::validate::validate_schema_types(&schema).unwrap_err();
7462 let (_, msg, _) = errs[0].class_name_message_position();
7463 assert!(msg.contains("default"), "{msg}");
7464 }
7465
7466 #[test]
7467 fn test_comparing_a_link_with_an_object_returning_function() {
7468 let mut schema = make_schema();
7472 let company = schema
7474 .types
7475 .iter_mut()
7476 .find(|t| t.name == "Company")
7477 .expect("make_schema declares Company");
7478 company.properties.push(PropertyDescriptor {
7479 name: "id".into(),
7480 pg_type: "uuid".into(),
7481 nullable: false,
7482 default_sql: None,
7483 default_pyql: None,
7484 description: None,
7485 check_constraints: vec![],
7486 is_exclusive: true,
7487 is_pk: true,
7488 is_readonly: true,
7489 rewrites: vec![],
7490 tuple_members: None,
7491 column_type: None,
7492 });
7493 schema.functions.push(crate::schema::FunctionDescriptor {
7494 name: "current_company".into(),
7495 module: "default".into(),
7496 params: vec![],
7497 return_pg_type: "default::Company".into(),
7498 return_is_object: true,
7499 return_is_set: false,
7500 return_is_polymorphic: false,
7501 volatility: "stable".into(),
7502 body: "select Company limit 1".into(),
7503 });
7504 let out = compile_and_emit_with("SELECT Person FILTER .company = current_company()", &schema);
7505 assert!(
7506 out.sql.contains("\"company_id\"") && out.sql.contains("current_company"),
7507 "the link's key against the function's row:\n{}",
7508 out.sql
7509 );
7510 }
7511
7512 #[test]
7513 fn test_choosing_between_two_object_sets() {
7514 let out = compile_and_emit_with(
7518 "WITH a := (SELECT Person FILTER .name = $n), b := (SELECT Person) \
7519 SELECT (a IF EXISTS a ELSE b) { name }",
7520 &make_schema(),
7521 );
7522 assert!(
7523 out.sql.contains("UNION ALL"),
7524 "expected one branch per side:\n{}",
7525 out.sql
7526 );
7527 }
7528
7529 #[test]
7530 fn test_a_union_operand_written_inline() {
7531 let out = compile_and_emit_with(
7535 "SELECT ((SELECT Person FILTER .name = $n) UNION (SELECT Person)) { name }",
7536 &make_schema(),
7537 );
7538 assert!(
7539 out.sql.starts_with("WITH"),
7540 "expected the operands hoisted:\n{}",
7541 out.sql
7542 );
7543 assert!(out.sql.contains("UNION ALL"), "{}", out.sql);
7544 }
7545
7546 #[test]
7547 fn test_a_for_loop_variable_over_objects() {
7548 let out = compile_and_emit_with("FOR p IN (SELECT Person) UNION (SELECT p.name)", &make_schema());
7552 assert!(
7553 out.sql.contains("FROM \"public\".\"Person\"") && out.sql.contains("\"_for_p\".\"v\""),
7554 "the body must read the row the variable names:\n{}",
7555 out.sql
7556 );
7557 }
7558
7559 #[test]
7560 fn test_a_shape_on_a_for_loop_variable() {
7561 let out = compile_and_emit_with("FOR p IN (SELECT Person) UNION (SELECT p { name })", &make_schema());
7562 assert!(
7563 out.sql.contains("'default::Person'::text"),
7564 "a shape on the variable yields its object:\n{}",
7565 out.sql
7566 );
7567 }
7568
7569 #[test]
7570 fn test_a_for_loop_variable_over_values_is_still_a_scalar() {
7571 let out = compile_and_emit_with("FOR n IN {1, 2} UNION (SELECT n)", &make_schema());
7572 assert!(out.sql.contains("VALUES"), "{}", out.sql);
7573 }
7574
7575 #[test]
7576 fn test_exists_over_a_sub_select_on_a_relative_path() {
7577 let out = compile_and_emit_with(
7582 "SELECT Person { id } FILTER EXISTS ((SELECT .posts FILTER .title = $t))",
7583 &make_schema(),
7584 );
7585 assert!(out.sql.contains("EXISTS("), "{}", out.sql);
7586 assert!(
7587 out.sql.contains("\"Person.posts\""),
7588 "the junction join must survive into the EXISTS:\n{}",
7589 out.sql
7590 );
7591 assert!(
7592 out.sql.contains("\"t0\".\"id\""),
7593 "and it must correlate to the enclosing row:\n{}",
7594 out.sql
7595 );
7596 }
7597
7598 #[test]
7599 fn test_counting_the_rows_a_mutation_touched() {
7600 let out = compile_and_emit_with("SELECT count((DELETE Person FILTER .name = $n))", &make_schema());
7604 assert!(out.sql.starts_with("WITH"), "{}", out.sql);
7605 assert!(out.sql.contains("DELETE FROM \"public\".\"Person\""), "{}", out.sql);
7606 assert!(out.sql.contains("count(*)"), "{}", out.sql);
7607 }
7608
7609 #[test]
7610 fn test_a_conflict_target_naming_several_columns() {
7611 let out = compile_and_emit_with(
7615 "INSERT Person { name := $n } UNLESS CONFLICT ON (.name, .id) ELSE (UPDATE Person SET { name := $n })",
7616 &make_schema(),
7617 );
7618 assert!(
7619 out.sql.contains("ON CONFLICT (\"name\", \"id\")"),
7620 "expected a column list:\n{}",
7621 out.sql
7622 );
7623 }
7624
7625 #[test]
7626 fn test_a_for_loop_over_a_path() {
7627 let out = compile_and_emit_with(
7631 "WITH p := (SELECT Person LIMIT 1) FOR post IN p.posts UNION (SELECT post.title)",
7632 &make_schema(),
7633 );
7634 assert!(out.sql.contains("CROSS JOIN LATERAL"), "{}", out.sql);
7635 }
7636
7637 #[test]
7638 fn test_a_relative_sub_select_keeps_its_enclosing_object() {
7639 let out = compile_and_emit_with(
7644 "SELECT Person { ps := assert_exists((SELECT .posts { title })) }",
7645 &make_schema(),
7646 );
7647 assert!(out.sql.contains("assert_exists"), "{}", out.sql);
7648 assert!(
7649 out.sql.contains("\"Person.posts\""),
7650 "the walk must reach the junction:\n{}",
7651 out.sql
7652 );
7653 }
7654
7655 #[test]
7656 fn test_a_relative_sub_select_as_a_multi_link_value() {
7657 let out = compile_and_emit_with(
7658 "UPDATE Person FILTER .name = $n SET { posts := (SELECT .posts FILTER .title = $t) }",
7659 &make_schema(),
7660 );
7661 assert!(
7662 out.sql.contains("\"Person.posts\""),
7663 "the value must read the row's own posts:\n{}",
7664 out.sql
7665 );
7666 }
7667
7668 #[test]
7669 fn test_a_shape_written_after_a_sub_select() {
7670 let out = compile_and_emit_with(
7674 "SELECT Person { ps := (SELECT Post FILTER .title = $t) { title } }",
7675 &make_schema(),
7676 );
7677 assert!(
7678 out.sql.contains("'default::Post'::text"),
7679 "the pointer must carry the object rows:\n{}",
7680 out.sql
7681 );
7682 }
7683
7684 #[test]
7685 fn test_a_shape_after_a_sub_select_with_a_with_block() {
7686 let out = compile_and_emit_with(
7687 "SELECT Person { ps := (WITH t := $t SELECT Post FILTER .title = t) { title } }",
7688 &make_schema(),
7689 );
7690 assert!(out.sql.contains("'default::Post'::text"), "{}", out.sql);
7691 }
7692
7693 #[test]
7694 fn test_a_computed_declared_as_an_object_returning_call() {
7695 let mut schema = make_schema();
7700 schema.functions.push(crate::schema::FunctionDescriptor {
7701 name: "recent".into(),
7702 module: "default".into(),
7703 params: vec![],
7704 return_pg_type: "default::Post".into(),
7705 return_is_object: true,
7706 return_is_set: true,
7707 return_is_polymorphic: false,
7708 volatility: "stable".into(),
7709 body: "select Post".into(),
7710 });
7711 schema.types[0].computed.push(crate::schema::ComputedDescriptor {
7712 name: "recent".into(),
7713 expression: "default::recent()".into(),
7714 return_type: None,
7715 link_target: None,
7716 link_multi: false,
7717 });
7718 let out = compile_and_emit_with("SELECT Person { recent }", &schema);
7719 assert!(
7720 out.sql.contains("ARRAY(SELECT") && out.sql.contains("'default::Post'::text"),
7721 "the pointer must carry the function's rows:\n{}",
7722 out.sql
7723 );
7724 }
7725
7726 #[test]
7727 fn test_one_branch_of_an_object_if_else_is_empty() {
7728 let out = compile_and_emit_with(
7732 "WITH p := (SELECT Person LIMIT 1) SELECT p { name } IF EXISTS p ELSE {}",
7733 &make_schema(),
7734 );
7735 assert!(!out.sql.contains("UNION ALL"), "nothing to union with:\n{}", out.sql);
7736 assert!(out.sql.contains("'default::Person'::text"), "{}", out.sql);
7737 }
7738
7739 #[test]
7740 fn test_a_conditional_insert_is_guarded_by_its_condition() {
7741 let out = compile_and_emit_with("SELECT (INSERT Person { name := $n }) IF FALSE ELSE {}", &make_schema());
7746 assert!(
7747 out.sql.contains("SELECT") && out.sql.contains("WHERE"),
7748 "a guarded insert selects its row rather than listing values:\n{}",
7749 out.sql
7750 );
7751 assert!(!out.sql.contains("VALUES"), "VALUES always yields a row:\n{}", out.sql);
7752 }
7753
7754 #[test]
7755 fn test_a_conditional_update_carries_its_condition() {
7756 let out = compile_and_emit(
7761 "SELECT (UPDATE Person FILTER .name = $n SET { name := $m }) IF EXISTS (SELECT Company) ELSE {}",
7762 );
7763 let update = out.sql.find("UPDATE").expect("an update is emitted");
7764 let where_clause = out.sql[update..].find("WHERE").expect("the update is filtered");
7765 assert!(
7766 out.sql[update + where_clause..].contains("EXISTS"),
7767 "the condition narrows the rows the update touches:\n{}",
7768 out.sql
7769 );
7770 }
7771
7772 #[test]
7773 fn test_a_conditional_delete_carries_its_condition() {
7774 let out = compile_and_emit("SELECT (DELETE Person FILTER .name = $n) IF FALSE ELSE {}");
7778 let delete = out
7779 .sql
7780 .split("DELETE FROM")
7781 .nth(1)
7782 .expect("the delete should still be emitted");
7783 let where_clause = delete.split("WHERE").nth(1).expect("the delete should be filtered");
7784 assert!(
7785 where_clause.contains("FALSE") || where_clause.contains("false"),
7786 "the condition must narrow the delete itself:\n{}",
7787 out.sql
7788 );
7789 }
7790
7791 #[test]
7792 fn test_a_pointer_a_binding_declared_in_its_own_shape() {
7793 let out = compile_and_emit_with(
7797 "WITH p := (SELECT Person { co := .company { name } } LIMIT 1) SELECT p { name, co: { name } }",
7798 &make_schema(),
7799 );
7800 assert!(
7801 out.sql.contains("'default::Company'::text"),
7802 "the declared pointer must be inlined:\n{}",
7803 out.sql
7804 );
7805 }
7806
7807 #[test]
7808 fn test_a_binding_declared_pointer_read_from_a_tuple_element() {
7809 let out = compile_and_emit_with(
7810 "WITH p := (SELECT Person { co := .company { name } } LIMIT 1) SELECT (p { co: { name } }, 1)",
7811 &make_schema(),
7812 );
7813 assert!(out.sql.contains("'default::Company'::text"), "{}", out.sql);
7814 }
7815
7816 #[test]
7817 fn test_a_for_loop_variable_shaped_inside_a_tuple() {
7818 let out = compile_and_emit_with(
7822 "WITH p := (SELECT Person LIMIT 1) FOR q IN p.posts UNION (SELECT (a := q { title }, b := 1))",
7823 &make_schema(),
7824 );
7825 assert!(
7826 out.sql.contains("\"_for_q\".\"v\""),
7827 "the element must be narrowed to the loop's row:\n{}",
7828 out.sql
7829 );
7830 }
7831
7832 #[test]
7833 fn test_choosing_between_two_walks() {
7834 let out = compile_and_emit_with(
7837 "WITH p := (SELECT Person LIMIT 1), q := (SELECT p.posts LIMIT 1) IF TRUE ELSE (SELECT p.posts) \
7838 SELECT q { title }",
7839 &make_schema(),
7840 );
7841 assert!(out.sql.contains("UNION ALL"), "one branch per side:\n{}", out.sql);
7842 }
7843
7844 #[test]
7845 fn test_a_bare_coalesce_of_correlated_walks_reaching_many_objects() {
7846 let out = compile_and_emit_with(
7849 "SELECT Post { owners := .<posts[is Person] ?? .<posts[is Person] }",
7850 &make_schema(),
7851 );
7852 assert!(
7853 !out.sql.contains("COALESCE("),
7854 "not a value-by-value choice:\n{}",
7855 out.sql
7856 );
7857 assert!(
7858 out.sql.contains("ARRAY(SELECT \"r\" FROM ("),
7859 "the arms are aggregated:\n{}",
7860 out.sql
7861 );
7862 }
7863
7864 #[test]
7865 fn test_a_union_of_correlated_walks_reaching_many() {
7866 let out = compile_and_emit_with(
7871 "SELECT Post { owners := (.<posts[is Person] UNION .<posts[is Person]) { name } }",
7872 &make_schema(),
7873 );
7874 assert!(
7875 out.sql.contains("ARRAY(SELECT \"r\" FROM ("),
7876 "the arms are aggregated:\n{}",
7877 out.sql
7878 );
7879 }
7880
7881 #[test]
7882 fn test_a_coalesce_of_correlated_walks_reaching_many() {
7883 let out = compile_and_emit_with(
7886 "SELECT Post { owners := (.<posts[is Person] ?? .<posts[is Person]) { name } }",
7887 &make_schema(),
7888 );
7889 assert!(
7890 out.sql.contains("ARRAY(SELECT \"r\" FROM ("),
7891 "the arms are aggregated:\n{}",
7892 out.sql
7893 );
7894 }
7895
7896 #[test]
7897 fn test_a_union_of_correlated_walks() {
7898 let out = compile_and_emit_with(
7902 "SELECT Post { owners := (SELECT (.<posts[is Person] UNION .<posts[is Person]) { name } LIMIT 1) }",
7903 &make_schema(),
7904 );
7905 assert!(out.sql.contains("UNION ALL"), "one arm per operand:\n{}", out.sql);
7906 assert!(
7907 out.sql.contains("AS \"_u\""),
7908 "the arms are read as one set:\n{}",
7909 out.sql
7910 );
7911 }
7912
7913 #[test]
7914 fn test_a_for_loop_whose_body_updates() {
7915 let out = compile_and_emit_with(
7919 "FOR p IN (SELECT Person) UNION (UPDATE Person FILTER .id = p.id SET { name := $n })",
7920 &make_schema(),
7921 );
7922 assert!(out.sql.contains("UPDATE \"public\".\"Person\""), "{}", out.sql);
7923 assert!(
7924 out.sql.contains("FROM \"_for_p\""),
7925 "driven from the iteration:\n{}",
7926 out.sql
7927 );
7928 assert!(
7929 !out.sql.contains("LATERAL"),
7930 "DML cannot sit in a LATERAL:\n{}",
7931 out.sql
7932 );
7933 }
7934
7935 #[test]
7936 fn test_a_for_loop_body_appends_the_row_it_is_iterating() {
7937 let out = compile_and_emit(
7938 "WITH t := (SELECT Post) FOR q IN t UNION (UPDATE Person FILTER .id = $i SET { posts += q })",
7939 );
7940 assert!(
7943 out.sql.contains("\"_iter\"") && out.sql.contains("FROM \"_ids\""),
7944 "junction rows driven from the iteration:\n{}",
7945 out.sql
7946 );
7947 assert!(
7948 !out.sql.contains("LATERAL"),
7949 "DML cannot sit in a LATERAL:\n{}",
7950 out.sql
7951 );
7952 }
7953
7954 #[test]
7955 fn test_select_on_a_for_loop_variable_reads_only_its_row() {
7956 let out = compile_and_emit("FOR p IN (SELECT Person) UNION (SELECT (SELECT p) { name })");
7957 assert!(
7958 out.sql.contains("= \"_for_p\".\"v\""),
7959 "narrowed to the row the variable holds:\n{}",
7960 out.sql
7961 );
7962 }
7963
7964 #[test]
7965 fn test_a_walk_off_a_for_loop_variable_reads_the_binding() {
7966 let out = compile_and_emit("WITH ps := (SELECT Post) FOR q IN ps UNION (SELECT Person FILTER .name = q.title)");
7969 assert!(
7970 out.sql.contains("FROM \"ps\""),
7971 "the walk reads the binding:\n{}",
7972 out.sql
7973 );
7974 }
7975
7976 #[test]
7977 fn test_a_mutating_for_loop_binding_becomes_sibling_ctes() {
7978 let out = compile_and_emit(
7979 "WITH made := (FOR n IN {'a', 'b'} UNION (INSERT Post { title := n })) SELECT count(made)",
7980 );
7981 assert!(
7982 out.sql.contains("\"made__ids\" AS (\nINSERT INTO"),
7983 "the insert is its own top-level CTE:\n{}",
7984 out.sql
7985 );
7986 assert!(
7987 !out.sql.contains("LATERAL"),
7988 "DML cannot sit in a LATERAL:\n{}",
7989 out.sql
7990 );
7991 }
7992
7993 #[test]
7994 fn test_count_of_a_binding_counts_its_rows() {
7995 let out = compile_and_emit("WITH people := (SELECT Person) SELECT count(people)");
7998 assert!(out.sql.contains("(SELECT count(*) FROM \"people\")"), "{}", out.sql);
7999 }
8000
8001 #[test]
8002 fn test_dividing_integers_yields_a_float() {
8003 for (query, expected) in [
8004 ("SELECT 100 / 133", "((100)::float8 / 133)"),
8005 ("SELECT Person { a := .age / 2 }", "::float8 / 2)"),
8006 ("SELECT Person.age / 2", "::float8 / 2)"),
8007 (
8008 "WITH total := max({133}), current := 38 - 1 SELECT 100 / total * current",
8009 "(((100)::float8 / (SELECT \"v\" FROM \"total\")) * (SELECT \"v\" FROM \"current\"))",
8010 ),
8011 (
8012 "WITH p := (SELECT Person LIMIT 1), index := p.age - 1, total := max(Person.age) SELECT 100 / total * index",
8013 "((100)::float8 / (SELECT \"v\" FROM \"total\"))",
8014 ),
8015 ] {
8016 let out = compile_and_emit(query);
8017 assert!(out.sql.contains(expected), "{query}:\n{}", out.sql);
8018 }
8019 }
8020
8021 #[test]
8022 fn test_dividing_anything_but_two_integers_is_left_to_postgres() {
8023 for query in [
8024 "SELECT 100 // 133",
8025 "SELECT 100 / 1.5",
8026 "SELECT <decimal>100 / <decimal>3",
8027 "SELECT 100 / <decimal>3",
8028 ] {
8029 let out = compile_and_emit(query);
8030 assert!(!out.sql.contains("::float8 /"), "{query}:\n{}", out.sql);
8031 }
8032 }
8033
8034 #[test]
8035 fn test_a_free_object_under_a_condition_stays_an_object() {
8036 for query in [
8037 "SELECT { a := 1 } IF true ELSE {}",
8038 "SELECT {} IF true ELSE { a := 1 }",
8039 "SELECT { a := 1 } IF true ELSE {} LIMIT 1",
8040 ] {
8041 let out = compile_and_emit(query);
8042 assert!(
8043 matches!(&out.shape.root, ShapeNode::Object { type_name: None, pointers, .. } if pointers.len() == 1),
8044 "{query}: {:?}\n{}",
8045 out.shape.root,
8046 out.sql
8047 );
8048 }
8049 }
8050
8051 #[test]
8052 fn test_exists_of_a_binding_asks_for_any_row() {
8053 let out = compile_and_emit("WITH people := (SELECT Person) SELECT exists people");
8054 assert!(out.sql.contains("EXISTS(SELECT 1 FROM \"people\")"), "{}", out.sql);
8055
8056 let out = compile_and_emit("WITH names := {'a', 'b'} SELECT exists names");
8057 assert!(
8058 out.sql
8059 .contains("EXISTS(SELECT 1 FROM \"names\" WHERE \"v\" IS NOT NULL)"),
8060 "{}",
8061 out.sql
8062 );
8063 }
8064
8065 #[test]
8066 fn test_a_conditional_insert_beside_a_read_branch_unions_both() {
8067 let out = compile_and_emit(
8068 "WITH existing := (SELECT Person FILTER .name = $n LIMIT 1) \
8069 SELECT (existing IF EXISTS existing ELSE (INSERT Person { name := $n })) { name }",
8070 );
8071 assert!(out.sql.contains("UNION ALL"), "{}", out.sql);
8072 assert!(
8073 out.sql.contains("INSERT INTO") && out.sql.contains("WHERE (NOT EXISTS(SELECT 1 FROM \"existing\"))"),
8074 "the insert carries the condition itself:\n{}",
8075 out.sql
8076 );
8077 }
8078
8079 fn make_schema_with_a_plain_abstract() -> SchemaDescriptor {
8084 let mut schema = make_schema();
8085 let mut archivable = schema
8086 .types
8087 .iter()
8088 .find(|t| t.name == "Person")
8089 .expect("Person is in the test schema")
8090 .clone();
8091 archivable.name = "Archivable".into();
8092 archivable.table = "Archivable".into();
8093 archivable.abstract_ = true;
8094 archivable.materialized = false;
8095 archivable.links = vec![];
8096 archivable.multilinks = vec![];
8097 schema.types.push(archivable);
8098 let person = schema
8099 .types
8100 .iter_mut()
8101 .find(|t| t.name == "Person")
8102 .expect("just found");
8103 person.parents.push("default::Archivable".into());
8104 schema
8105 }
8106
8107 #[test]
8108 fn test_an_intersection_with_a_plain_abstract_fans_out() {
8109 let schema = make_schema_with_a_plain_abstract();
8110 let out = compile_and_emit_with("SELECT Post { who := [is default::Archivable].name }", &schema);
8111 assert!(
8112 !out.sql.contains("\"Archivable\""),
8113 "a plain abstract backs no relation:\n{}",
8114 out.sql
8115 );
8116 assert!(
8117 out.sql.contains("\"public\".\"Person\""),
8118 "read from the type that carries the column:\n{}",
8119 out.sql
8120 );
8121 }
8122
8123 fn make_schema_with_a_linking_plain_abstract() -> SchemaDescriptor {
8127 let mut schema = make_schema_with_a_plain_abstract();
8128 let person_links = schema
8129 .types
8130 .iter()
8131 .find(|t| t.name == "Person")
8132 .expect("Person is in the test schema")
8133 .links
8134 .clone();
8135 let archivable = schema
8136 .types
8137 .iter_mut()
8138 .find(|t| t.name == "Archivable")
8139 .expect("just added");
8140 archivable.links = person_links;
8141 schema
8142 }
8143
8144 #[test]
8145 fn test_a_plain_abstract_in_a_subquery_fans_out() {
8146 let schema = make_schema_with_a_plain_abstract();
8147 let out = compile_and_emit_with(
8148 "SELECT Person FILTER .id IN (SELECT default::Archivable FILTER .age > 1).id",
8149 &schema,
8150 );
8151 assert!(
8152 !out.sql.contains("\"Archivable\""),
8153 "a plain abstract backs no relation:\n{}",
8154 out.sql
8155 );
8156 }
8157
8158 #[test]
8159 fn test_a_backlink_narrowed_to_a_plain_abstract_fans_out() {
8160 let schema = make_schema_with_a_linking_plain_abstract();
8161 let out = compile_and_emit_with(
8162 "SELECT Company FILTER .<company[is default::Archivable].name = 'a'",
8163 &schema,
8164 );
8165 assert!(
8166 !out.sql.contains("\"Archivable\""),
8167 "a plain abstract backs no relation:\n{}",
8168 out.sql
8169 );
8170 }
8171
8172 fn make_interface_schema_with_a_plain_abstract() -> SchemaDescriptor {
8176 let mut schema = make_interface_schema();
8177 let mut archivable = schema
8178 .types
8179 .iter()
8180 .find(|t| t.name == "Individual")
8181 .expect("Individual is in the interface schema")
8182 .clone();
8183 archivable.name = "Archivable".into();
8184 archivable.table = "Archivable".into();
8185 archivable.abstract_ = true;
8186 archivable.materialized = false;
8187 archivable.interfaces = vec![];
8188 archivable.links = vec![];
8189 archivable.multilinks = vec![];
8190 archivable.computed = vec![];
8191 schema.types.push(archivable);
8192 for t in schema.types.iter_mut() {
8193 match t.name.as_str() {
8194 "Account" => t.materialized = true,
8196 "Individual" => t.parents.push("default::Archivable".into()),
8197 _ => {}
8198 }
8199 }
8200 schema
8201 }
8202
8203 #[test]
8204 fn test_a_splat_over_a_plain_abstract_fans_out() {
8205 let schema = make_interface_schema_with_a_plain_abstract();
8206 let out = compile_and_emit_with("SELECT Account { id, [is default::Archivable].* }", &schema);
8207 assert!(
8208 !out.sql.contains("\"Archivable\""),
8209 "a plain abstract backs no relation:\n{}",
8210 out.sql
8211 );
8212 assert!(
8213 out.sql.contains("first_name"),
8214 "the splat still reaches the mixin's own pointers:\n{}",
8215 out.sql
8216 );
8217 }
8218
8219 #[test]
8220 fn test_an_aggregate_over_a_relative_walk_runs_inside_the_subquery() {
8221 let out = compile_and_emit("SELECT Person { latest := max(.posts.title) }");
8224 assert!(out.sql.contains("(SELECT max("), "{}", out.sql);
8225 assert!(!out.sql.contains("max(ARRAY("), "{}", out.sql);
8226 }
8227
8228 fn make_schema_with_a_computed_backlink() -> SchemaDescriptor {
8231 use crate::schema::ComputedDescriptor;
8232 let mut schema = make_schema();
8233 let company = schema
8234 .types
8235 .iter_mut()
8236 .find(|t| t.name == "Company")
8237 .expect("Company is in the test schema");
8238 company.computed = vec![ComputedDescriptor {
8239 name: "staff".into(),
8240 expression: ".<company[is default::Person]".into(),
8241 return_type: None,
8242 link_target: None,
8243 link_multi: false,
8244 }];
8245 schema
8246 }
8247
8248 #[test]
8249 fn test_a_computed_backlink_reads_the_objects_it_lands_on() {
8250 let schema = make_schema_with_a_computed_backlink();
8251 let out = compile_and_emit_with("SELECT Company { staff: { name } }", &schema);
8252 assert!(
8253 out.sql.contains("\"public\".\"Person\""),
8254 "the walk reaches Person:\n{}",
8255 out.sql
8256 );
8257 }
8258
8259 #[test]
8260 fn test_a_computed_backlinks_modifiers_belong_to_what_it_lands_on() {
8261 let schema = make_schema_with_a_computed_backlink();
8264 let out = compile_and_emit_with("SELECT Company.staff { name } FILTER .name = 'ada'", &schema);
8265 assert!(out.sql.contains("'ada'"), "{}", out.sql);
8266 assert!(
8267 out.sql.contains("\"public\".\"Person\""),
8268 "filtered on the Person the walk lands on:\n{}",
8269 out.sql
8270 );
8271 }
8272
8273 #[test]
8274 fn test_an_unreserved_keyword_names_a_free_shape_field() {
8275 let out = compile_and_emit("SELECT { last := 2, first := 1, order := 3 }");
8278 assert!(out.sql.contains("\"last\""), "{}", out.sql);
8279 assert!(out.sql.contains("\"first\""), "{}", out.sql);
8280 assert!(out.sql.contains("\"order\""), "{}", out.sql);
8281 }
8282
8283 #[test]
8284 fn test_a_shaped_sub_select_over_a_type_is_an_object() {
8285 let out = compile_and_emit("SELECT Person { latest := (SELECT Post { title } LIMIT 1) }");
8288 assert!(out.sql.contains("\"title\""), "{}", out.sql);
8289 }
8290
8291 #[test]
8292 fn test_a_walk_inside_a_path_selects_shape_finds_its_row() {
8293 let out = compile_and_emit("SELECT Person.posts { mine := (SELECT Post FILTER .title = .title LIMIT 1) }");
8296 assert!(out.sql.contains("\"public\".\"Post\""), "{}", out.sql);
8297 }
8298
8299 #[test]
8300 fn test_a_walk_off_a_binding_in_a_free_select() {
8301 let out = compile_and_emit("WITH p := (SELECT Person) SELECT { n := p.company.name }");
8304 assert!(out.sql.contains("\"p\""), "{}", out.sql);
8305 assert!(out.sql.contains("\"public\".\"Company\""), "{}", out.sql);
8306 }
8307
8308 #[test]
8309 fn test_a_free_selects_filter_can_root_at_a_binding() {
8310 let out = compile_and_emit("WITH p := (SELECT Person) SELECT { n := 1 } FILTER p.company.name = 'Acme'");
8311 assert!(out.sql.contains("'Acme'"), "{}", out.sql);
8312 }
8313
8314 #[test]
8315 fn test_a_trailing_shape_on_a_field_access_select() {
8316 let out = compile_and_emit("SELECT (SELECT Person).company { name }");
8317 assert!(out.sql.contains("\"name\""), "{}", out.sql);
8318 assert!(out.sql.contains("\"public\".\"Company\""), "{}", out.sql);
8319 }
8320
8321 #[test]
8322 fn test_a_union_of_walks_names_each_branch() {
8323 let out = compile_and_emit(
8326 "WITH p := (SELECT Person FILTER .name = $a), q := (SELECT Person FILTER .name = $b), \
8327 t := (SELECT p.posts UNION q.posts) SELECT t { title }",
8328 );
8329 assert!(out.sql.contains("UNION ALL"), "{}", out.sql);
8330 assert!(out.sql.contains("\"title\""), "{}", out.sql);
8331 }
8332
8333 #[test]
8334 fn test_sibling_for_loops_each_get_their_own_iterator() {
8335 let out = compile_and_emit(
8338 "WITH a := (SELECT Post), b := (SELECT Post), \
8339 x := (FOR p IN a UNION (INSERT Person { name := p.title, age := 1 })), \
8340 y := (FOR p IN b UNION (INSERT Person { name := p.title, age := 2 })) \
8341 SELECT x",
8342 );
8343 assert!(out.sql.contains("\"_for_p\" AS"), "{}", out.sql);
8344 assert!(out.sql.contains("\"_for_p__1\" AS"), "{}", out.sql);
8345 }
8346
8347 #[test]
8348 fn test_a_nested_for_carries_the_outer_loops_key() {
8349 let out = compile_and_emit(
8352 "WITH made := (FOR p IN (SELECT Person) UNION ( \
8353 FOR q IN p.posts UNION (INSERT Company { name := q.title }) \
8354 )) SELECT count(made)",
8355 );
8356 assert!(
8357 out.sql.contains("\"_outer\""),
8358 "the inner iterator carries the bond:\n{}",
8359 out.sql
8360 );
8361 assert!(
8362 out.sql.contains("JOIN \"_for_p\" ON"),
8363 "the insert reads both loop variables:\n{}",
8364 out.sql
8365 );
8366 assert!(
8367 !out.sql.contains("LATERAL (\nINSERT"),
8368 "DML cannot sit in a LATERAL:\n{}",
8369 out.sql
8370 );
8371 }
8372
8373 #[test]
8374 fn test_a_link_value_can_be_a_walk_off_a_binding() {
8375 let mut schema = make_schema();
8378 let company = schema
8379 .types
8380 .iter_mut()
8381 .find(|t| t.name == "Company")
8382 .expect("make_schema declares Company");
8383 company.properties.push(PropertyDescriptor {
8384 name: "id".into(),
8385 pg_type: "uuid".into(),
8386 nullable: false,
8387 default_sql: None,
8388 default_pyql: None,
8389 description: None,
8390 check_constraints: vec![],
8391 is_exclusive: true,
8392 is_pk: true,
8393 is_readonly: true,
8394 rewrites: vec![],
8395 tuple_members: None,
8396 column_type: None,
8397 });
8398 let out = compile_and_emit_with(
8399 "WITH others := (SELECT Person) \
8400 SELECT (INSERT Person { name := $n, age := 1, company := (SELECT others.company LIMIT 1) })",
8401 &schema,
8402 );
8403 assert!(out.sql.contains("\"company_id\""), "{}", out.sql);
8404 }
8405
8406 #[test]
8407 fn test_a_shape_over_a_coalesce_of_walks() {
8408 let schema = make_schema_with_computed_links();
8411 let out = compile_and_emit_with("SELECT Person { x := (.published ?? .plain) { title } }", &schema);
8412 assert!(out.sql.contains("UNION ALL"), "{}", out.sql);
8413 assert!(
8414 out.sql.contains("NOT EXISTS"),
8415 "the later branch only stands in when the first is empty:\n{}",
8416 out.sql
8417 );
8418 }
8419
8420 #[test]
8421 fn test_an_aggregate_over_a_walk_onto_objects_counts_its_rows() {
8422 let out = compile_and_emit("WITH c := (SELECT Company) SELECT c { n := count(.<company[is default::Person]) }");
8426 assert!(out.sql.contains("count("), "{}", out.sql);
8427 assert!(out.sql.contains("\"public\".\"Person\""), "{}", out.sql);
8428 }
8429
8430 #[test]
8431 fn an_aggregate_over_a_computed_set_counts_inside_a_subquery() {
8432 let schema = make_schema_with_computed_links();
8438 let out = compile_and_emit_with("SELECT Person { n := count(.plain) }", &schema);
8439 assert!(
8440 !out.sql.contains("count((SELECT"),
8441 "the aggregate belongs inside the walk's subquery, not around it:\n{}",
8442 out.sql
8443 );
8444 assert!(
8445 out.sql.contains("(SELECT count(") && out.sql.contains("\"public\".\"Post\""),
8446 "it counts the rows the computed's own path lands on:\n{}",
8447 out.sql
8448 );
8449 }
8450
8451 #[test]
8454 fn an_aggregate_over_a_stored_multi_link_still_counts_its_junction_rows() {
8455 let schema = make_schema_with_computed_links();
8456 let out = compile_and_emit_with("SELECT Person { n := count(.posts) }", &schema);
8457 assert!(out.sql.contains("\"public\".\"Person.posts\""), "{}", out.sql);
8458 assert!(
8459 !out.sql.contains("JOIN \"public\".\"Post\""),
8460 "counting junction rows needs no join onto the targets:\n{}",
8461 out.sql
8462 );
8463 }
8464
8465 #[test]
8466 fn test_a_select_can_name_its_own_result() {
8467 let out = compile_and_emit("SELECT oldest := max(Person.age)");
8470 assert!(out.sql.contains("max("), "{}", out.sql);
8471 let filtered = compile_and_emit("SELECT n := Person.age FILTER n > 18");
8472 assert!(
8473 filtered.sql.contains("18"),
8474 "the alias resolves to the result:\n{}",
8475 filtered.sql
8476 );
8477 }
8478
8479 #[test]
8480 fn test_exists_sees_a_computed_pointer() {
8481 let schema = make_schema_with_computed_links();
8485 let out = compile_and_emit_with("SELECT Person FILTER EXISTS .published", &schema);
8486 assert!(out.sql.contains("EXISTS"), "{}", out.sql);
8487 assert!(
8488 out.sql.contains("\"public\".\"Post\""),
8489 "the computed's own path:\n{}",
8490 out.sql
8491 );
8492 }
8493
8494 #[test]
8495 fn test_walking_through_a_computed_field_access() {
8496 use crate::schema::ComputedDescriptor;
8497 let mut schema = make_schema_with_computed_links();
8500 let person = schema.types.iter_mut().find(|t| t.name == "Person").expect("Person");
8501 person.computed.push(ComputedDescriptor {
8502 name: "latest_title".into(),
8503 expression: "(select .posts order by .title desc limit 1).title".into(),
8504 return_type: None,
8505 link_target: None,
8506 link_multi: false,
8507 });
8508 let out = compile_and_emit_with("SELECT Person { x := .latest_title }", &schema);
8509 assert!(out.sql.contains("\"title\""), "{}", out.sql);
8510 assert!(
8511 out.sql.contains("LIMIT"),
8512 "the computed's own limit survives:\n{}",
8513 out.sql
8514 );
8515 }
8516
8517 #[test]
8518 fn test_a_computed_field_access_onto_an_object_takes_a_shape() {
8519 use crate::schema::ComputedDescriptor;
8520 let mut schema = make_schema_with_computed_links();
8523 let post = schema.types.iter_mut().find(|t| t.name == "Post").expect("Post");
8524 post.links.push(LinkDescriptor {
8525 name: "author".into(),
8526 target: "default::Person".into(),
8527 nullable: true,
8528 through: None,
8529 description: None,
8530 default_pyql: None,
8531 is_exclusive: false,
8532 is_readonly: false,
8533 rewrites: vec![],
8534 on_delete: vec![],
8535 });
8536 let person = schema.types.iter_mut().find(|t| t.name == "Person").expect("Person");
8537 person.computed.retain(|c| c.name != "looper");
8538 person.computed.push(ComputedDescriptor {
8539 name: "latest_author".into(),
8540 expression: "((select .posts order by .title desc limit 1)).author".into(),
8541 return_type: None,
8542 link_target: None,
8543 link_multi: false,
8544 });
8545 let out = compile_and_emit_with("SELECT Person { latest_author: { name } }", &schema);
8546 assert!(out.sql.contains("\"name\""), "{}", out.sql);
8547 assert!(
8548 out.sql.contains("\"title\" DESC"),
8549 "the order stays on the posts:\n{}",
8550 out.sql
8551 );
8552 assert!(
8553 out.sql.contains("LIMIT"),
8554 "the computed's own limit survives:\n{}",
8555 out.sql
8556 );
8557 let splat = compile_and_emit_with("SELECT Person { * }", &schema);
8558 assert!(
8559 !splat.sql.contains("latest_author"),
8560 "`*` leaves links out:\n{}",
8561 splat.sql
8562 );
8563 }
8564
8565 #[test]
8566 fn test_a_narrowed_pointer_can_carry_a_shape() {
8567 let out = compile_and_emit("SELECT Company { x := 1, [is default::Person].posts: { title } }");
8571 assert!(out.sql.contains("\"title\""), "{}", out.sql);
8572 assert!(out.sql.contains("\"public\".\"Post\""), "{}", out.sql);
8573 }
8574
8575 #[test]
8576 fn test_select_type_name_as_a_path_step() {
8577 let out = compile_and_emit("SELECT Person.__type__");
8578 assert!(out.sql.contains("ROW('default::Person')"), "{}", out.sql);
8579 assert!(out.sql.contains("\"Person\" AS \"t0\""), "{}", out.sql);
8580 }
8581
8582 #[test]
8583 fn test_aggregate_over_a_with_bound_object_set() {
8584 let out = compile_and_emit("WITH people := (SELECT Person) SELECT max(people.age)");
8585 assert!(out.sql.contains("\"people\" AS ("));
8586 assert!(out.sql.contains("max("));
8587 assert!(!out.sql.contains("FROM \"default\".\"Person\" AS \"t1\""));
8589 }
8590
8591 #[test]
8592 fn test_select_union_of_two_object_bindings() {
8593 let out = compile_and_emit("WITH a := (SELECT Person LIMIT 1), b := (SELECT Person) SELECT (a UNION b)");
8594 assert!(out.sql.contains("SELECT * FROM \"a\""));
8595 assert!(out.sql.contains("UNION ALL"));
8596 assert!(out.sql.contains("SELECT * FROM \"b\""));
8597 }
8598
8599 #[test]
8600 fn test_select_union_of_different_object_types_is_rejected() {
8601 let schema = make_schema();
8602 let ast = parse::parse("WITH a := (SELECT Person), b := (SELECT Company) SELECT (a UNION b)").unwrap();
8603 let Err(err) = ir::compile(&ast, &schema) else {
8604 panic!("union of unrelated types must be rejected")
8605 };
8606 assert!(format!("{err}").contains("UNION"));
8607 }
8608
8609 #[test]
8610 fn test_for_union_as_a_function_body() {
8611 let schema = make_schema();
8612 let descriptor = FunctionDescriptor {
8613 name: "recent".into(),
8614 module: "default".into(),
8615 params: vec![],
8616 return_pg_type: "default::Person".into(),
8617 return_is_object: true,
8618 return_is_set: true,
8619 return_is_polymorphic: false,
8620 volatility: "stable".into(),
8621 body: "for n in {1, 2} union (select Person filter .age = n)".into(),
8622 };
8623 let ir = ir::compile_fn_body(&descriptor, &schema).expect("function body must compile");
8624 let body = emit_fn_body(&ir);
8625 assert!(body.contains("VALUES"), "{body}");
8626 assert!(body.contains("CROSS JOIN LATERAL"), "{body}");
8627 assert!(body.contains("\"_body\".*"), "{body}");
8628 }
8629
8630 #[test]
8631 fn test_with_binding_in_a_computed_reads_the_enclosing_object() {
8632 let out = compile_and_emit("SELECT Person { n := (WITH own := .name SELECT own) }");
8633 assert!(out.sql.contains("\"name\""), "{}", out.sql);
8634 }
8635
8636 #[test]
8637 fn test_subject_path_update_names_its_own_alias() {
8638 let mut schema = make_schema();
8639 let company = schema.types.iter_mut().find(|t| t.name == "Company").unwrap();
8640 company.properties.insert(
8641 0,
8642 PropertyDescriptor {
8643 name: "id".into(),
8644 pg_type: "uuid".into(),
8645 nullable: false,
8646 default_sql: Some("uuidv7()".into()),
8647 default_pyql: None,
8648 description: None,
8649 check_constraints: vec![],
8650 is_exclusive: true,
8651 is_pk: true,
8652 is_readonly: true,
8653 rewrites: vec![],
8654 tuple_members: None,
8655 column_type: None,
8656 },
8657 );
8658 let out = compile_and_emit_with("SELECT (UPDATE Person.company SET { name := 'x' }) { name }", &schema);
8662 assert!(out.sql.contains("\"t1\".\"id\" = ANY("), "{}", out.sql);
8663 }
8664
8665 #[test]
8666 fn test_an_update_past_a_from_returns_only_its_own_columns() {
8667 let out = compile_and_emit(
8671 "SELECT { (UPDATE Person FILTER .name = 'a' SET { company := (INSERT Company { name := 'c' }) }) }",
8672 );
8673 assert!(out.sql.contains("FROM \"_nested_dml_0\""), "{}", out.sql);
8674 assert!(
8675 out.sql.contains("RETURNING \"t0\".*"),
8676 "the update names its own target:\n{}",
8677 out.sql
8678 );
8679 }
8680
8681 #[test]
8682 fn test_nested_insert_in_a_mutation_written_as_a_free_set() {
8683 let out = compile_and_emit(
8684 "SELECT { (UPDATE Person FILTER .name = 'a' SET { company := (INSERT Company { name := 'c' }) }) }",
8685 );
8686 assert!(
8687 out.sql.contains("INSERT INTO \"public\".\"Company\""),
8688 "the hoisted insert is defined, not just referenced:\n{}",
8689 out.sql
8690 );
8691 }
8692
8693 #[test]
8694 fn test_nested_insert_as_a_link_value_in_an_update() {
8695 let out = compile_and_emit(
8699 "SELECT (UPDATE Person FILTER .name = 'a' SET { company := (INSERT Company { name := 'c' }) }) { name }",
8700 );
8701 assert!(out.sql.contains("INSERT INTO \"public\".\"Company\""), "{}", out.sql);
8702 assert!(out.sql.contains("_nested_dml_0"), "{}", out.sql);
8703 assert!(
8704 out.sql.contains("FROM \"_nested_dml_0\""),
8705 "the update reads the hoisted CTE:\n{}",
8706 out.sql
8707 );
8708 }
8709
8710 #[test]
8711 fn test_nested_insert_as_a_link_value() {
8712 let out = compile_and_emit(
8713 "SELECT (INSERT Person { name := 'a', company := (INSERT Company { name := 'c' }) }) { name }",
8714 );
8715 assert!(out.sql.contains("_nested_dml_0"), "{}", out.sql);
8716 assert!(out.sql.contains("INSERT INTO \"public\".\"Company\""), "{}", out.sql);
8717 }
8718
8719 #[test]
8720 fn test_nested_insert_in_a_one_element_set_is_that_insert() {
8721 let out = compile_and_emit(
8722 "SELECT (INSERT Person { name := 'a', company := { (INSERT Company { name := 'c' }) } }) { name }",
8723 );
8724 assert!(out.sql.contains("INSERT INTO \"public\".\"Company\""), "{}", out.sql);
8725 }
8726
8727 #[test]
8728 fn test_insert_with_no_assignments_uses_default_values() {
8729 let out = compile_and_emit("SELECT (INSERT Person { name := 'a', company := (INSERT Company {}) }) { name }");
8730 assert!(out.sql.contains("DEFAULT VALUES"), "{}", out.sql);
8731 }
8732
8733 #[test]
8734 fn test_backlink_narrowed_to_an_interface_its_implementor_declares() {
8735 use crate::schema::{LinkDescriptor, TypeDescriptor};
8738 let mut schema = make_interface_schema();
8739 let individual = schema.types.iter().find(|t| t.name == "Individual").unwrap().clone();
8740 schema.types.push(TypeDescriptor {
8741 name: "Note".into(),
8742 module: "default".into(),
8743 table: "Note".into(),
8744 abstract_: false,
8745 materialized: true,
8746 description: None,
8747 parents: vec![],
8748 interfaces: vec![],
8749 bases: vec![],
8750 properties: individual.properties[..1].to_vec(),
8751 links: vec![],
8752 multilinks: vec![],
8753 computed: vec![],
8754 constraints: vec![],
8755 indexes: vec![],
8756 partition: None,
8757 vector_indexes: vec![],
8758 search_indexes: vec![],
8759 triggers: vec![],
8760 junction: false,
8761 signals: vec![],
8762 });
8763 schema
8764 .types
8765 .iter_mut()
8766 .find(|t| t.name == "Individual")
8767 .unwrap()
8768 .links
8769 .push(LinkDescriptor {
8770 name: "note".into(),
8771 target: "default::Note".into(),
8772 nullable: true,
8773 description: None,
8774 default_pyql: None,
8775 is_exclusive: false,
8776 is_readonly: false,
8777 rewrites: vec![],
8778 on_delete: vec![],
8779 through: None,
8780 });
8781 let out = compile_and_emit_with("SELECT Note FILTER EXISTS .<note[is Account]", &schema);
8782 assert!(out.sql.contains("\"Individual\""), "{}", out.sql);
8783 }
8784
8785 #[test]
8786 fn test_subject_reads_the_row_a_constraint_checks() {
8787 let schema = make_schema();
8788 let sql = ir::compile_constraint_expr("__subject__.age > 18", "default::Person", &schema)
8789 .expect("__subject__ names the row under check");
8790 assert!(sql.contains("\"age\""), "{sql}");
8791 }
8792
8793 #[test]
8794 fn test_free_select_filter_gates_the_result_and_warns() {
8795 let schema = make_schema();
8796 let ast = parse::parse("SELECT count(Person) FILTER (Person.age > 18)").unwrap();
8797 let ir = ir::compile(&ast, &schema).unwrap();
8798 let out = emit(&ir);
8799 assert!(
8800 out.sql.contains("EXISTS("),
8801 "the condition gates the count:\n{}",
8802 out.sql
8803 );
8804 assert!(
8805 ir.warnings.iter().any(|w| w.contains("FILTER clause")),
8806 "a set-valued filter warns: {:?}",
8807 ir.warnings
8808 );
8809 }
8810
8811 #[test]
8812 fn test_free_sub_select_with_modifiers_reads_inline() {
8813 let out = compile_and_emit(
8816 "SELECT Person { n := (WITH mine := .name SELECT count(Post) FILTER (Post.title = mine)) }",
8817 );
8818 assert!(out.sql.contains("_scalar_sub"), "{}", out.sql);
8819 assert!(!out.sql.contains("WITH"), "nothing is hoisted:\n{}", out.sql);
8820 }
8821
8822 #[test]
8823 fn test_correlated_with_binding_is_not_hoisted() {
8824 let out = compile_and_emit("SELECT Person { n := (WITH own := .name SELECT own) }");
8825 assert!(
8826 !out.sql.contains("\"own\" AS ("),
8827 "the binding is inlined:\n{}",
8828 out.sql
8829 );
8830 assert!(out.sql.contains("\"name\""), "{}", out.sql);
8831 }
8832
8833 #[test]
8834 fn test_array_literal_binding_is_readable_and_typed() {
8835 let out = compile_and_emit("WITH order := ['a', 'b'] SELECT std::find(order, 'b')");
8836 assert!(out.sql.contains("array_position"), "its type is known:\n{}", out.sql);
8837 assert!(
8838 out.sql.contains("AS v"),
8839 "the binding exposes a value column:\n{}",
8840 out.sql
8841 );
8842 }
8843
8844 #[test]
8845 fn test_any_over_a_single_boolean_is_that_boolean() {
8846 let out = compile_and_emit("SELECT Person FILTER std::any(.age > 18)");
8847 assert!(!out.sql.contains("bool_or"), "no aggregate in WHERE:\n{}", out.sql);
8848 assert!(out.sql.contains("> 18"), "{}", out.sql);
8849 }
8850
8851 #[test]
8852 fn test_comparing_a_value_to_a_set_is_membership() {
8853 let schema = make_schema_with_computed_links();
8854 let out = compile_and_emit_with("SELECT Person FILTER .name = Person.posts.title", &schema);
8855 assert!(out.sql.contains("= ANY("), "{}", out.sql);
8856 }
8857
8858 #[test]
8859 fn test_for_over_a_with_binding_iterates_every_row() {
8860 let out = compile_and_emit(
8861 "WITH names := (SELECT Person.name) FOR n IN names UNION (SELECT Person FILTER .name = n)",
8862 );
8863 assert!(out.sql.contains("FROM \"names\""), "{}", out.sql);
8864 assert!(!out.sql.contains("VALUES"), "the whole set is iterated:\n{}", out.sql);
8865 }
8866
8867 #[test]
8868 fn test_for_body_bindings_stay_inside_the_body() {
8869 let out = compile_and_emit(
8872 "WITH names := (SELECT Person.name) \
8873 FOR n IN names UNION (WITH matching := (SELECT Person FILTER .name = n) SELECT matching)",
8874 );
8875 let lateral = out.sql.find("CROSS JOIN LATERAL").expect("a lateral body");
8876 let binding = out.sql.find("\"matching\" AS (").expect("the body's binding");
8877 assert!(binding > lateral, "the binding sits inside the body:\n{}", out.sql);
8878 }
8879
8880 #[test]
8881 fn test_array_valued_binding_picks_the_array_overload() {
8882 let out = compile_and_emit("WITH l := <array<std::str>>$0 SELECT std::find(l, 'a')");
8885 assert!(out.sql.contains("array_position"), "{}", out.sql);
8886 assert!(!out.sql.contains("strpos"), "{}", out.sql);
8887 }
8888
8889 #[test]
8890 fn test_aggregate_over_a_path_in_a_filter_is_a_subquery() {
8891 let out = compile_and_emit("SELECT Person FILTER .age = std::max(Person.age)");
8892 assert!(
8893 out.sql.contains("(SELECT max("),
8894 "an aggregate in WHERE needs its own query:\n{}",
8895 out.sql
8896 );
8897 }
8898
8899 #[test]
8900 fn test_distinct_over_a_single_value_is_that_value() {
8901 let out = compile_and_emit("SELECT Person { n := (DISTINCT .name) }");
8902 assert!(out.sql.contains("\"name\""), "{}", out.sql);
8903 assert!(!out.sql.contains("DISTINCT \""), "{}", out.sql);
8904 }
8905
8906 #[test]
8907 fn test_narrowing_an_interface_joins_the_implementor_table() {
8908 let mut schema = make_interface_schema();
8911 let account = schema.types.iter_mut().find(|t| t.name == "Account").unwrap();
8912 account.materialized = true;
8913 let out = compile_and_emit_with("SELECT Account[is Individual].first_name", &schema);
8914 assert!(
8915 out.sql.contains("JOIN \"public\".\"Individual\""),
8916 "the implementor's own table is joined:\n{}",
8917 out.sql
8918 );
8919 assert!(out.sql.contains("\"first_name\""), "{}", out.sql);
8920 }
8921
8922 #[test]
8923 fn test_sub_select_shape_declares_names_its_filter_and_projection_read() {
8924 let out = compile_and_emit("SELECT Person { n := (SELECT .posts { t := .title } FILTER .t = 'x' LIMIT 1).t }");
8925 assert!(out.sql.contains("'x'"), "{}", out.sql);
8926 assert!(out.sql.contains("\"title\""), "{}", out.sql);
8927 }
8928
8929 #[test]
8930 fn test_assert_over_an_object_set_returns_rows() {
8931 let out = compile_and_emit("SELECT std::assert_distinct((SELECT Person))");
8932 assert!(
8933 out.sql.contains("\"assert_distinct\"(ARRAY(SELECT \"id\""),
8934 "{}",
8935 out.sql
8936 );
8937 assert!(out.sql.contains("= ANY("), "{}", out.sql);
8938 assert!(out.sql.contains("\"Person\" AS \"t1\""), "{}", out.sql);
8939 assert!(!out.sql.contains("unnest("), "{}", out.sql);
8940 }
8941
8942 #[test]
8943 fn test_assert_over_a_for_union_reads_its_rows() {
8944 let out =
8945 compile_and_emit("SELECT std::assert_distinct((FOR n IN {1, 2} UNION (SELECT Person FILTER .age = n)))");
8946 assert!(out.sql.contains("CROSS JOIN LATERAL"), "{}", out.sql);
8947 assert!(out.sql.contains("= ANY("), "{}", out.sql);
8948 }
8949
8950 #[test]
8951 fn test_declared_computed_with_binding_reads_its_own_object() {
8952 use crate::schema::ComputedDescriptor;
8955 let schema = make_schema();
8956 let cd = ComputedDescriptor {
8957 name: "shouted".into(),
8958 expression: "(with own := .name select std::str_upper(own))".into(),
8959 return_type: None,
8960 link_target: None,
8961 link_multi: false,
8962 };
8963 let ir = ir::compile_computed_in_type(&cd, "default::Person", &schema)
8964 .expect("a computed may read the object it is declared on");
8965 assert!(ir.is_some());
8966 }
8967
8968 #[test]
8969 fn test_sub_select_modifiers_scope_to_its_own_subject() {
8970 let mut schema = make_schema();
8973 let post = schema
8974 .types
8975 .iter_mut()
8976 .find(|t| t.name == "Post")
8977 .expect("Post is in the test schema");
8978 post.links.push(LinkDescriptor {
8979 name: "author".into(),
8980 target: "default::Person".into(),
8981 nullable: true,
8982 description: None,
8983 default_pyql: None,
8984 is_exclusive: false,
8985 is_readonly: false,
8986 rewrites: vec![],
8987 on_delete: vec![],
8988 through: None,
8989 });
8990 let out = compile_and_emit_with(
8991 "SELECT Person { a := (SELECT .posts FILTER .title = 'x' LIMIT 1).author.name }",
8992 &schema,
8993 );
8994 assert!(out.sql.contains("'x'"), "{}", out.sql);
8995 }
8996
8997 #[test]
8998 fn test_free_select_set_literal() {
8999 let schema = make_schema();
9000 let ast = parse::parse("SELECT {1, 2, 3}").unwrap();
9001 let ir = ir::compile(&ast, &schema).unwrap();
9002 let out = emit(&ir);
9003 assert_eq!(out.sql.matches("UNION ALL").count(), 2);
9005 assert!(out.sql.contains("1 AS v"));
9006 assert!(out.sql.contains("2 AS v"));
9007 assert!(out.sql.contains("3 AS v"));
9008 assert!(out.sql.contains("ROW(v) AS result"));
9009 assert!(matches!(out.shape.root, crate::query::ShapeNode::Scalar { .. }));
9010 }
9011
9012 #[test]
9013 fn test_free_select_free_object() {
9014 let schema = make_schema();
9015 let ast = parse::parse("SELECT { foo := 'bar', n := 42 }").unwrap();
9016 let ir = ir::compile(&ast, &schema).unwrap();
9017 let out = emit(&ir);
9018 assert!(out.sql.contains("'bar'"));
9019 assert!(out.sql.contains("42"));
9020 assert!(out.sql.contains("AS result"));
9021 let crate::query::ShapeNode::Object {
9023 pointers, type_name, ..
9024 } = &out.shape.root
9025 else {
9026 panic!("expected Object shape")
9027 };
9028 assert!(type_name.is_none());
9029 assert_eq!(pointers.len(), 2);
9030 assert!(matches!(&pointers[0], crate::query::ShapeNode::Scalar { name, position: 0 } if name == "foo"));
9031 assert!(matches!(&pointers[1], crate::query::ShapeNode::Scalar { name, position: 1 } if name == "n"));
9032 }
9033
9034 #[test]
9035 fn test_free_select_object_with_enum_field_casts_to_text_and_tags_shape() {
9036 let mut schema = make_schema();
9044 schema.enums.push(crate::schema::EnumDescriptor {
9045 name: "Gender".into(),
9046 module: "default".into(),
9047 members: vec!["Male".into(), "Female".into()],
9048 });
9049 let out = compile_and_emit_with("select { gender := default::Gender.Male }", &schema);
9050 assert!(
9057 out.sql.contains("'Male'::\"public\".\"Gender\""),
9058 "expected the enum literal, got:\n{}",
9059 out.sql
9060 );
9061 assert!(
9062 out.sql.contains("ROW(\"_f0\"::text) AS result"),
9063 "expected the ROW composite to cast the enum field to text, got:\n{}",
9064 out.sql
9065 );
9066 let crate::query::ShapeNode::Object { pointers, .. } = &out.shape.root else {
9067 panic!("expected Object shape")
9068 };
9069 assert_eq!(pointers.len(), 1);
9070 assert!(
9071 matches!(
9072 &pointers[0],
9073 crate::query::ShapeNode::Enum { name, position: 0, enum_type }
9074 if name == "gender" && enum_type == "public::Gender"
9075 ),
9076 "expected Enum-tagged shape, got: {:?}",
9077 pointers[0]
9078 );
9079 }
9080
9081 #[test]
9082 fn test_free_select_bare_enum_literal_casts_to_text_inside_row() {
9083 let mut schema = make_schema();
9087 schema.enums.push(crate::schema::EnumDescriptor {
9088 name: "Gender".into(),
9089 module: "default".into(),
9090 members: vec!["Male".into(), "Female".into()],
9091 });
9092 let out = compile_and_emit_with("select default::Gender.Male", &schema);
9093 assert!(
9094 out.sql.contains("ROW(v::text) AS result"),
9095 "expected ROW(v::text), got:\n{}",
9096 out.sql
9097 );
9098 assert!(
9099 matches!(
9100 &out.shape.root,
9101 crate::query::ShapeNode::Enum { enum_type, .. } if enum_type == "public::Gender"
9102 ),
9103 "expected Enum-tagged shape, got: {:?}",
9104 out.shape.root
9105 );
9106 }
9107
9108 #[test]
9111 fn test_for_update_defaults_to_blocking() {
9112 let out = compile_and_emit("SELECT Person FOR UPDATE");
9113 assert!(out.sql.trim_end().ends_with("FOR UPDATE"), "got:\n{}", out.sql);
9114 }
9115
9116 #[test]
9117 fn test_for_update_skip_locked() {
9118 let out = compile_and_emit("SELECT Person FOR UPDATE SKIP LOCKED");
9119 assert!(
9120 out.sql.trim_end().ends_with("FOR UPDATE SKIP LOCKED"),
9121 "got:\n{}",
9122 out.sql
9123 );
9124 }
9125
9126 #[test]
9127 fn test_for_update_nowait() {
9128 let out = compile_and_emit("SELECT Person FOR UPDATE NOWAIT");
9129 assert!(out.sql.trim_end().ends_with("FOR UPDATE NOWAIT"), "got:\n{}", out.sql);
9130 }
9131
9132 #[test]
9133 fn test_for_no_key_update_skip_locked() {
9134 let out = compile_and_emit("SELECT Person FOR NO KEY UPDATE SKIP LOCKED");
9135 assert!(
9136 out.sql.trim_end().ends_with("FOR NO KEY UPDATE SKIP LOCKED"),
9137 "got:\n{}",
9138 out.sql
9139 );
9140 }
9141
9142 #[test]
9143 fn test_for_share() {
9144 let out = compile_and_emit("SELECT Person FOR SHARE");
9145 assert!(out.sql.trim_end().ends_with("FOR SHARE"), "got:\n{}", out.sql);
9146 }
9147
9148 #[test]
9149 fn test_for_key_share_nowait() {
9150 let out = compile_and_emit("SELECT Person FOR KEY SHARE NOWAIT");
9151 assert!(
9152 out.sql.trim_end().ends_with("FOR KEY SHARE NOWAIT"),
9153 "got:\n{}",
9154 out.sql
9155 );
9156 }
9157
9158 #[test]
9159 fn test_for_update_comes_after_order_by_limit_offset_in_emitted_sql() {
9160 let out = compile_and_emit("SELECT Person { name } ORDER BY .name OFFSET 1 LIMIT 5 FOR UPDATE SKIP LOCKED");
9163 let order_pos = out.sql.find("ORDER BY").unwrap();
9164 let offset_pos = out.sql.find("OFFSET").unwrap();
9165 let limit_pos = out.sql.find("LIMIT").unwrap();
9166 let for_pos = out.sql.find("FOR UPDATE").unwrap();
9167 assert!(
9168 order_pos < offset_pos && offset_pos < limit_pos && limit_pos < for_pos,
9169 "got:\n{}",
9170 out.sql
9171 );
9172 }
9173
9174 #[test]
9175 fn test_for_update_combined_with_distinct_is_rejected() {
9176 let ast = parse::parse("SELECT DISTINCT Person FOR UPDATE").expect("parse failed");
9177 let err = ir::compile(&ast, &make_schema())
9178 .err()
9179 .expect("expected a compile error");
9180 assert!(err.to_string().contains("DISTINCT"), "unexpected: {err}");
9181 }
9182
9183 #[test]
9184 fn test_for_update_combined_with_select_over_insert_is_rejected() {
9185 let ast = parse::parse("SELECT (INSERT Person { name := 'Alice' }) { name } FOR UPDATE").expect("parse failed");
9186 let err = ir::compile(&ast, &make_schema())
9187 .err()
9188 .expect("expected a compile error");
9189 assert!(err.to_string().contains("INSERT"), "unexpected: {err}");
9190 }
9191
9192 #[test]
9193 fn test_for_update_on_an_interface_type_is_rejected() {
9194 fn id_prop() -> PropertyDescriptor {
9195 PropertyDescriptor {
9196 name: "id".into(),
9197 pg_type: "uuid".into(),
9198 nullable: false,
9199 default_sql: Some("uuidv7()".into()),
9200 default_pyql: None,
9201 description: None,
9202 check_constraints: vec![],
9203 is_exclusive: true,
9204 is_pk: true,
9205 is_readonly: true,
9206 rewrites: vec![],
9207 tuple_members: None,
9208 column_type: None,
9209 }
9210 }
9211 let schema = SchemaDescriptor {
9212 types: vec![
9213 TypeDescriptor {
9214 name: "Account".into(),
9215 module: "default".into(),
9216 table: "Account".into(),
9217 abstract_: true,
9218 materialized: true,
9219 description: None,
9220 parents: vec![],
9221 interfaces: vec![],
9222 bases: vec![],
9223 properties: vec![id_prop()],
9224 links: vec![],
9225 multilinks: vec![],
9226 computed: vec![],
9227 constraints: vec![],
9228 indexes: vec![],
9229 partition: None,
9230 vector_indexes: vec![],
9231 search_indexes: vec![],
9232 triggers: vec![],
9233 junction: false,
9234 signals: vec![],
9235 },
9236 TypeDescriptor {
9237 name: "Individual".into(),
9238 module: "default".into(),
9239 table: "Individual".into(),
9240 abstract_: false,
9241 materialized: true,
9242 description: None,
9243 parents: vec![],
9244 interfaces: vec!["default::Account".into()],
9245 bases: vec![],
9246 properties: vec![id_prop()],
9247 links: vec![],
9248 multilinks: vec![],
9249 computed: vec![],
9250 constraints: vec![],
9251 indexes: vec![],
9252 partition: None,
9253 vector_indexes: vec![],
9254 search_indexes: vec![],
9255 triggers: vec![],
9256 junction: false,
9257 signals: vec![],
9258 },
9259 ],
9260 scalars: vec![],
9261 enums: vec![],
9262 named_tuples: vec![],
9263 globals: vec![],
9264 functions: vec![],
9265 aliases: vec![],
9266 channels: vec![],
9267 ..Default::default()
9268 };
9269 let ast = parse::parse("SELECT Account FOR UPDATE").expect("parse failed");
9270 let err = ir::compile(&ast, &schema).err().expect("expected a compile error");
9271 assert!(err.to_string().contains("interface"), "unexpected: {err}");
9272 }
9273
9274 #[test]
9275 fn test_schema_select_distinct_emits_distinct_keyword() {
9276 let out = compile_and_emit("SELECT DISTINCT Person { name }");
9281 assert!(out.sql.contains("DISTINCT"), "expected DISTINCT in SQL:\n{}", out.sql);
9282 }
9283
9284 #[test]
9285 fn test_schema_select_detached_compiles_as_ordinary_select() {
9286 let out = compile_and_emit("SELECT DETACHED Person { name }");
9290 assert!(
9291 out.sql.contains("\"name\""),
9292 "expected name column in SQL:\n{}",
9293 out.sql
9294 );
9295 }
9296
9297 #[test]
9298 fn test_detached_subquery_correlates_against_the_outer_row() {
9299 let out = compile_and_emit(
9306 "SELECT Person { name } FILTER NOT EXISTS (\
9307 SELECT DETACHED Person FILTER .name = Person.name AND .age > Person.age)",
9308 );
9309 assert!(
9310 out.sql.contains("\"t1\".\"name\" = \"t0\".\"name\""),
9311 "the inner row should be compared against the outer one:\n{}",
9312 out.sql
9313 );
9314 assert!(
9315 !out.sql.contains("\"t1\".\"name\" = \"t1\".\"name\""),
9316 "self-comparison means the correlation was lost:\n{}",
9317 out.sql
9318 );
9319 }
9320
9321 #[test]
9322 fn test_an_assert_on_a_pointer_checks_its_rows() {
9323 let out = compile_and_emit("SELECT Person { name, p := assert_exists(.posts { title }) }");
9327 assert!(
9328 out.sql
9329 .contains(r#"cardinality("_pylon"."assert_exists"("_a"."v"::text[]))"#),
9330 "the assert should read the aggregated rows:\n{}",
9331 out.sql
9332 );
9333 assert_eq!(
9334 out.sql.matches("array_agg(ROW(").count(),
9335 1,
9336 "the pointer's subquery should be evaluated once, not once per use:\n{}",
9337 out.sql
9338 );
9339 }
9340
9341 #[test]
9342 fn test_an_assert_on_a_scalar_stays_an_ordinary_call() {
9343 let out = compile_and_emit("SELECT Person { n := assert_exists(.name) }");
9346 assert!(
9347 !out.sql.contains(r#""_a"."v""#),
9348 "a scalar assert should not be wrapped as a pointer check:\n{}",
9349 out.sql
9350 );
9351 }
9352
9353 #[test]
9354 fn test_a_backlink_tail_walks_past_the_first_step() {
9355 let out = compile_and_emit("SELECT Post FILTER any(.<posts[is Person].posts.title = $t)");
9360 assert!(
9361 out.sql.contains(r#""public"."Person.posts""#),
9362 "the tail should walk the multi-link's junction:\n{}",
9363 out.sql
9364 );
9365 }
9366
9367 #[test]
9368 fn test_an_intersection_narrows_the_rest_of_a_multilink_walk() {
9369 let out = compile_and_emit("SELECT Person FILTER any(.posts[is Post].title = $t)");
9373 assert!(
9374 out.sql.contains(r#""public"."Post""#),
9375 "the walk should continue from the narrowed type:\n{}",
9376 out.sql
9377 );
9378 }
9379
9380 #[test]
9381 fn test_a_union_of_bindings_emits_selects_not_bare_names() {
9382 let out = compile_and_emit_with(
9386 "WITH a := (INSERT Post { title := $t1 }), b := (INSERT Post { title := $t2 }) \
9387 SELECT (INSERT Person { name := $n, posts := (a UNION b) })",
9388 &make_schema(),
9389 );
9390 assert!(
9391 out.sql.contains(r#"(SELECT "_s"."id" FROM "a" AS "_s")"#),
9392 "each union operand should be a select:\n{}",
9393 out.sql
9394 );
9395 }
9396
9397 #[test]
9398 fn test_a_bare_narrowing_reads_as_a_type_check_and_as_an_object() {
9399 for query in [
9403 "SELECT Person FILTER EXISTS [is Person]",
9404 "SELECT Person { p := [is Person] { name } }",
9405 ] {
9406 let ast = parse::parse(query).unwrap_or_else(|e| panic!("{query}: {e}"));
9407 assert!(ir::compile(&ast, &make_schema()).is_ok(), "{query} should compile");
9408 }
9409 }
9410
9411 #[test]
9412 fn test_a_multilink_takes_a_set_wrapped_in_a_select() {
9413 let out = compile_and_emit_with(
9418 "WITH a := (INSERT Post { title := $t1 }), b := (INSERT Post { title := $t2 }) \
9419 SELECT (INSERT Person { name := $n, posts := (SELECT { a, b }) })",
9420 &make_schema(),
9421 );
9422 assert!(
9423 out.sql.contains(r#""public"."Person.posts""#),
9424 "both targets should reach the junction:\n{}",
9425 out.sql
9426 );
9427 }
9428
9429 #[test]
9430 fn test_an_aggregate_over_a_subselected_path() {
9431 let out = compile_and_emit("SELECT array_agg((SELECT Person.name))");
9437 assert!(
9438 out.sql.contains("array_agg") && out.sql.contains(r#""public"."Person""#),
9439 "the aggregate should wrap the walk's column:\n{}",
9440 out.sql
9441 );
9442 }
9443
9444 #[test]
9445 fn test_a_distinct_aggregate_argument_reaches_the_aggregate() {
9446 for query in [
9452 "SELECT array_agg(DISTINCT Person.name)",
9453 "SELECT array_agg((SELECT DISTINCT Person.name))",
9454 ] {
9455 let out = compile_and_emit(query);
9456 assert!(
9457 out.sql.contains("array_agg(DISTINCT "),
9458 "{query} should deduplicate inside the aggregate:\n{}",
9459 out.sql
9460 );
9461 }
9462 }
9463
9464 #[test]
9465 fn test_is_not_negates_the_type_check() {
9466 let positive = compile_and_emit("SELECT Person FILTER Person IS Person");
9470 let negative = compile_and_emit("SELECT Person FILTER Person IS NOT Person");
9471 assert!(
9472 negative.sql.contains("NOT") && !positive.sql.contains("NOT"),
9473 "the negation should reach the SQL:\npositive:\n{}\nnegative:\n{}",
9474 positive.sql,
9475 negative.sql
9476 );
9477 }
9478
9479 #[test]
9480 fn test_a_cast_takes_a_prefix_operator_as_its_operand() {
9481 for query in ["SELECT <bool>EXISTS (SELECT Person LIMIT 1)", "SELECT <int64>-1"] {
9485 let ast = parse::parse(query).unwrap_or_else(|e| panic!("{query} should parse: {e}"));
9486 assert!(ir::compile(&ast, &make_schema()).is_ok(), "{query} should compile");
9487 }
9488 }
9489
9490 #[test]
9491 fn test_a_bare_multilink_reads_as_a_set() {
9492 let out =
9497 compile_and_emit("WITH p := (SELECT DETACHED Post LIMIT 1) SELECT Person { name } FILTER p IN .posts");
9498 assert!(
9499 out.sql.contains(r#""public"."Person.posts""#),
9500 "the membership test should read the link's junction:\n{}",
9501 out.sql
9502 );
9503 }
9504
9505 #[test]
9506 fn test_a_select_reads_its_own_shape_computed_in_its_clauses() {
9507 let out = compile_and_emit("SELECT Person { n := .name } ORDER BY .n ASC");
9511 assert!(
9512 out.sql.contains("ORDER BY"),
9513 "the order by should compile against the declared pointer:\n{}",
9514 out.sql
9515 );
9516 }
9517
9518 #[test]
9519 fn test_a_declared_pointer_can_be_walked_through() {
9520 let out = compile_and_emit_with(
9524 "WITH p := (SELECT Person { boss := .company } LIMIT 1) SELECT p { name, boss_name := .boss.name }",
9525 &make_schema(),
9526 );
9527 assert!(
9528 out.sql.contains(r#""public"."Company""#),
9529 "the walk should reach the declared pointer's own target table:\n{}",
9530 out.sql
9531 );
9532 }
9533
9534 #[test]
9535 fn test_a_replaced_subject_shape_stays_in_scope() {
9536 let out = compile_and_emit("SELECT Person { c := (SELECT .company { n := .name }) { n } }");
9541 assert!(
9542 out.sql.contains(r#"'default::Company'::text"#) && out.sql.contains(r#""t2"."name""#),
9543 "`n` should resolve to the company's own column:\n{}",
9544 out.sql
9545 );
9546 }
9547
9548 #[test]
9549 fn test_a_sibling_shape_pointer_is_not_in_scope() {
9550 let schema = make_schema();
9553 let ast = parse::parse("SELECT Person { n := .name, copy := .n }").unwrap();
9554 assert!(ir::compile(&ast, &schema).is_err());
9555 }
9556
9557 #[test]
9558 fn test_shape_over_a_with_binding_reads_the_binding() {
9559 let out = compile_and_emit("WITH c := (SELECT Company LIMIT 1) SELECT Person { name, employer := c { name } }");
9564 assert!(
9565 out.sql.contains("\"c\""),
9566 "the pointer should read the binding's CTE:\n{}",
9567 out.sql
9568 );
9569 }
9570
9571 #[test]
9572 fn test_shape_over_a_bare_type_name_is_still_rejected() {
9573 let schema = make_schema();
9576 let ast = parse::parse("SELECT Person { name, other := Company { name } }").unwrap();
9577 assert!(ir::compile(&ast, &schema).is_err());
9578 }
9579
9580 #[test]
9581 fn test_nested_shape_reads_a_prefix_bound_by_the_outer_select() {
9582 let out = compile_and_emit("SELECT Person { name, posts: { title, who := Person.name } }");
9587 assert!(
9588 out.sql.contains(r#""t0"."name""#),
9589 "the nested computed should read the outer Person alias:\n{}",
9590 out.sql
9591 );
9592 assert!(
9593 !out.sql.contains(r#"FROM "public"."Person" "t2""#),
9594 "the prefix should not open a second Person source:\n{}",
9595 out.sql
9596 );
9597 }
9598
9599 #[test]
9600 fn test_prefix_naming_a_type_out_of_scope_is_rejected() {
9601 let schema = make_schema();
9604 let ast = parse::parse("SELECT Person { name, who := Company.name }").unwrap();
9605 assert!(ir::compile(&ast, &schema).is_err());
9606 }
9607
9608 #[test]
9609 fn test_free_select_tuple() {
9610 let schema = make_schema();
9611 let ast = parse::parse("SELECT (1, 2)").unwrap();
9612 let ir = ir::compile(&ast, &schema).unwrap();
9613 let out = emit(&ir);
9614 assert!(out.sql.contains("1"));
9615 assert!(out.sql.contains("2"));
9616 assert!(out.sql.contains("AS result"));
9617 assert!(matches!(out.shape.root, crate::query::ShapeNode::Tuple { .. }));
9618 }
9619
9620 #[test]
9621 fn test_free_select_scalar_literal() {
9622 let schema = make_schema();
9623 let ast = parse::parse("SELECT 'hello'").unwrap();
9624 let ir = ir::compile(&ast, &schema).unwrap();
9625 let out = emit(&ir);
9626 assert!(out.sql.contains("SELECT 'hello' AS v"));
9627 assert!(out.sql.contains("ROW(v) AS result"));
9628 assert!(matches!(out.shape.root, crate::query::ShapeNode::Scalar { .. }));
9629 }
9630
9631 #[test]
9632 fn test_float_literal_casts_to_float8() {
9633 let out = compile_and_emit("SELECT 1.0");
9640 assert!(
9641 out.sql.contains("(1.0::float8)"),
9642 "expected explicit float8 cast, got:\n{}",
9643 out.sql
9644 );
9645 }
9646
9647 #[test]
9648 fn test_free_select_array_literal() {
9649 let schema = make_schema();
9650 let ast = parse::parse("SELECT [1, 2, 3]").unwrap();
9651 let ir = ir::compile(&ast, &schema).unwrap();
9652 let out = emit(&ir);
9653 assert!(out.sql.contains("SELECT ARRAY[1, 2, 3] AS v"), "{}", out.sql);
9654 assert!(out.sql.contains("SELECT v AS result, v FROM"), "{}", out.sql);
9655 assert!(matches!(out.shape.root, crate::query::ShapeNode::RawScalar));
9656 }
9657
9658 #[test]
9659 fn test_select_scalars() {
9660 let out = compile_and_emit("SELECT Person { name, age }");
9661 assert!(out.sql.contains("'default::Person'::text"));
9662 assert!(out.sql.contains("\"name\"::text"));
9663 assert!(out.sql.contains("\"age\"::int8"));
9664 assert!(out.sql.contains("FROM \"public\".\"Person\""));
9665 assert!(out.sql.contains(") AS result"));
9666 }
9667
9668 #[test]
9669 fn test_select_filter_param() {
9670 let out = compile_and_emit("SELECT Person { name } FILTER .name = $name");
9671 assert!(out.sql.contains("WHERE"));
9672 assert!(out.sql.contains("$1"));
9673 }
9674
9675 #[test]
9676 fn test_filter_in_set_literal_compiles_to_any_array() {
9677 let out = compile_and_emit("SELECT Person { name } FILTER .name IN {'Carol'}");
9678 assert!(out.sql.contains("= ANY(ARRAY['Carol'])"), "got:\n{}", out.sql);
9679 }
9680
9681 #[test]
9682 fn test_filter_in_multi_element_set_literal_compiles_to_any_array() {
9683 let out = compile_and_emit("SELECT Person { name } FILTER .name IN {'Carol', 'Bob'}");
9684 assert!(out.sql.contains("= ANY(ARRAY['Carol', 'Bob'])"), "got:\n{}", out.sql);
9685 }
9686
9687 #[test]
9694 fn test_filter_in_array_unpack_passes_the_array_straight_to_any() {
9695 let out = compile_and_emit("SELECT Person { name } FILTER .name IN std::array_unpack(<array<str>>$names)");
9696 assert!(
9697 !out.sql.contains("ANY(unnest("),
9698 "unnest inside ANY is invalid in WHERE:\n{}",
9699 out.sql
9700 );
9701 assert!(out.sql.contains("= ANY("), "got:\n{}", out.sql);
9702 }
9703
9704 #[test]
9705 fn test_filter_not_in_array_unpack_passes_the_array_straight_to_all() {
9706 let out = compile_and_emit("SELECT Person { name } FILTER .name NOT IN std::array_unpack(<array<str>>$names)");
9707 assert!(
9708 !out.sql.contains("ALL(unnest("),
9709 "unnest inside ALL is invalid in WHERE:\n{}",
9710 out.sql
9711 );
9712 assert!(out.sql.contains("<> ALL("), "got:\n{}", out.sql);
9713 }
9714
9715 #[test]
9720 fn test_all_over_unpacked_array_aggregates_the_array_not_the_multilink() {
9721 let out = compile_and_emit(
9722 "SELECT Person { name } FILTER all(std::array_unpack(<array<str>>$titles) IN .posts.title)",
9723 );
9724 assert!(out.sql.contains("bool_and("), "got:\n{}", out.sql);
9725 assert!(out.sql.contains("_unnested"), "got:\n{}", out.sql);
9726 }
9727
9728 #[test]
9732 fn test_any_over_a_multilink_against_an_unpacked_array_still_folds_into_any() {
9733 let out = compile_and_emit(
9734 "SELECT Person { name } FILTER any(.posts.title IN std::array_unpack(<array<str>>$titles))",
9735 );
9736 assert!(!out.sql.contains("unnest("), "got:\n{}", out.sql);
9737 assert!(out.sql.contains("= ANY("), "got:\n{}", out.sql);
9738 }
9739
9740 #[test]
9741 fn test_any_over_unpacked_array_aggregates_the_array_not_the_multilink() {
9742 let out = compile_and_emit(
9743 "SELECT Person { name } FILTER any(std::array_unpack(<array<str>>$titles) IN .posts.title)",
9744 );
9745 assert!(out.sql.contains("bool_or("), "got:\n{}", out.sql);
9746 assert!(out.sql.contains("_unnested"), "got:\n{}", out.sql);
9747 }
9748
9749 fn schema_with_array_property() -> SchemaDescriptor {
9751 let mut schema = make_schema();
9752 schema.types[0].properties.push(crate::schema::PropertyDescriptor {
9753 name: "perms".into(),
9754 pg_type: "text[]".into(),
9755 nullable: true,
9756 default_sql: None,
9757 default_pyql: None,
9758 description: None,
9759 check_constraints: vec![],
9760 is_exclusive: false,
9761 is_pk: false,
9762 is_readonly: false,
9763 rewrites: vec![],
9764 tuple_members: None,
9765 column_type: None,
9766 });
9767 schema
9768 }
9769
9770 #[test]
9776 fn test_aggregate_over_an_unpacked_walk_unnests_in_a_row_source() {
9777 let out = compile_and_emit_with(
9778 "WITH teams := (SELECT Person { perms } FILTER .age > 1) SELECT std::array_agg(std::array_unpack(teams.perms))",
9779 &schema_with_array_property(),
9780 );
9781 assert!(!out.sql.contains("array_agg(unnest("), "got:\n{}", out.sql);
9782 assert!(
9783 out.sql.contains(
9784 r#"coalesce(array_agg("_s"."v"), '{}') FROM unnest(ARRAY(SELECT unnest("t1"."perms") FROM "teams" AS "t1"#
9785 ),
9786 "got:\n{}",
9787 out.sql
9788 );
9789 }
9790
9791 #[test]
9792 fn test_aggregate_over_an_unpacked_type_rooted_path_unnests_in_a_row_source() {
9793 let out = compile_and_emit_with(
9794 "SELECT std::count(std::array_unpack(Person.perms))",
9795 &schema_with_array_property(),
9796 );
9797 assert!(!out.sql.contains("count(unnest("), "got:\n{}", out.sql);
9798 assert!(
9799 out.sql
9800 .contains(r#"FROM unnest(ARRAY(SELECT unnest("t0"."perms") FROM "public"."Person" AS "t0"#),
9801 "got:\n{}",
9802 out.sql
9803 );
9804 }
9805
9806 #[test]
9809 fn test_aggregate_over_an_unpacked_parameter_unnests_the_array_itself() {
9810 let out = compile_and_emit("SELECT std::array_agg(std::array_unpack(<array<str>>$names))");
9811 assert!(!out.sql.contains("array_agg(unnest("), "got:\n{}", out.sql);
9812 assert!(
9813 out.sql.contains(r#"FROM unnest(($1)::text[]) AS "_s"("v")"#),
9814 "got:\n{}",
9815 out.sql
9816 );
9817 }
9818
9819 #[test]
9820 fn test_aggregate_over_an_unpacked_property_unnests_that_row_s_array() {
9821 let out = compile_and_emit_with(
9822 "SELECT Person { n := std::array_agg(std::array_unpack(.perms)) }",
9823 &schema_with_array_property(),
9824 );
9825 assert!(!out.sql.contains("array_agg(unnest("), "got:\n{}", out.sql);
9826 assert!(
9827 out.sql.contains(r#"FROM unnest("t0"."perms") AS "_s"("v")"#),
9828 "got:\n{}",
9829 out.sql
9830 );
9831 }
9832
9833 #[test]
9836 fn test_array_agg_over_nothing_is_an_empty_array_not_null() {
9837 for query in [
9838 "SELECT std::array_agg(Person.name)",
9839 "SELECT std::array_agg((SELECT Person).name)",
9840 ] {
9841 let out = compile_and_emit(query);
9842 assert!(
9843 out.sql.contains("coalesce(") && out.sql.contains("'{}'"),
9844 "{query} got:\n{}",
9845 out.sql
9846 );
9847 }
9848 }
9849
9850 #[test]
9854 fn test_an_array_agg_binding_stays_an_array_through_its_coalesce() {
9855 let out = compile_and_emit(
9856 "WITH ids := std::array_agg(Person.id) SELECT Person { name } FILTER std::contains(ids, .id)",
9857 );
9858 assert!(
9859 out.sql.contains("@> ARRAY["),
9860 "expected the array overload, got:\n{}",
9861 out.sql
9862 );
9863 assert!(
9864 !out.sql.contains("strpos("),
9865 "the string overload must not win, got:\n{}",
9866 out.sql
9867 );
9868 }
9869
9870 #[test]
9873 fn test_aggregate_over_a_plain_walk_keeps_its_row_source() {
9874 let out = compile_and_emit("SELECT std::array_agg(Person.name)");
9875 assert!(
9876 out.sql.contains(r#"array_agg("t0"."name")"#) && out.sql.contains(r#"FROM "public"."Person""#),
9877 "got:\n{}",
9878 out.sql
9879 );
9880 }
9881
9882 #[test]
9885 fn test_the_cte_order_check_catches_a_forward_reference() {
9886 let bad = "WITH \"b\" AS (\n SELECT * FROM \"a\"\n),\n\"a\" AS (\n SELECT 1\n)\nSELECT 1";
9887 assert!(
9888 super::forward_cte_reference(bad).is_some(),
9889 "expected a forward reference"
9890 );
9891 let good = "WITH \"a\" AS (\n SELECT 1\n),\n\"b\" AS (\n SELECT * FROM \"a\"\n)\nSELECT 1";
9892 assert_eq!(super::forward_cte_reference(good), None, "ordered CTEs must pass");
9893 }
9894
9895 #[test]
9899 fn test_a_cast_unpacked_array_still_folds_into_any() {
9900 let out = compile_and_emit("SELECT Person { name } FILTER .name IN <str>std::array_unpack(<array<str>>$names)");
9901 assert!(
9902 !out.sql.contains("unnest("),
9903 "the unpack must fold into ANY, got:\n{}",
9904 out.sql
9905 );
9906 assert!(out.sql.contains("= ANY("), "got:\n{}", out.sql);
9907 }
9908
9909 #[test]
9912 fn test_array_unpack_outside_in_still_unnests() {
9913 let out = compile_and_emit("SELECT std::array_unpack(<array<str>>$names)");
9914 assert!(out.sql.contains("unnest("), "got:\n{}", out.sql);
9915 }
9916
9917 #[test]
9918 fn test_filter_not_in_set_literal_compiles_to_all_array() {
9919 let out = compile_and_emit("SELECT Person { name } FILTER .name NOT IN {'Carol'}");
9920 assert!(out.sql.contains("<> ALL(ARRAY['Carol'])"), "got:\n{}", out.sql);
9921 }
9922
9923 #[test]
9924 fn test_deep_splat_fetches_single_link_target_properties_not_just_id() {
9925 let out = compile_and_emit("SELECT Person { ** }");
9930 assert!(
9931 out.sql.contains("\"name\""),
9932 "expected Company.name pulled in via .company's ** expansion, got:\n{}",
9933 out.sql
9934 );
9935 }
9936
9937 #[test]
9938 fn test_deep_splat_fetches_multilink_target_properties_not_just_id() {
9939 let out = compile_and_emit("SELECT Person { ** }");
9940 assert!(
9941 out.sql.contains("\"title\""),
9942 "expected Post.title pulled in via .posts' ** expansion, got:\n{}",
9943 out.sql
9944 );
9945 }
9946
9947 #[test]
9948 fn test_schema_type_cast_select() {
9949 let out = compile_and_emit("SELECT <default::Person><uuid>'019ef1bb-0d42-7a9f-8f6b-b38d028a49ba'");
9950 assert!(out.sql.contains("FROM \"public\".\"Person\""));
9951 assert!(out.sql.contains("WHERE"));
9952 assert!(out.sql.contains("'019ef1bb-0d42-7a9f-8f6b-b38d028a49ba'"));
9953 }
9954
9955 #[test]
9956 fn test_select_single_link() {
9957 let out = compile_and_emit("SELECT Person { name, company { name } }");
9958 assert!(out.sql.contains("'default::Company'::text"));
9959 assert!(out.sql.contains("FROM \"public\".\"Company\""));
9960 assert!(out.sql.contains("\"company_id\" = "));
9962 }
9963
9964 #[test]
9965 fn test_select_multi_link() {
9966 let out = compile_and_emit("SELECT Person { name, posts { title } }");
9967 assert!(out.sql.contains("array_agg(ROW("));
9968 assert!(out.sql.contains("ARRAY[]::record[]"));
9969 assert!(out.sql.contains("'default::Post'::text"));
9970 assert!(out.sql.contains("\"Person.posts\""));
9971 }
9972
9973 fn compile_err(query: &str) -> String {
9974 let schema = make_schema();
9975 let ast = parse::parse(query).expect("parse failed");
9976 match ir::compile(&ast, &schema) {
9977 Ok(_) => panic!("expected a compile error"),
9978 Err(e) => format!("{e}"),
9979 }
9980 }
9981
9982 #[test]
9983 fn test_multi_link_limit_cuts_rows_before_aggregating() {
9984 let out = compile_and_emit("SELECT Person { posts: { title } filter .title = 'x' limit 1 }");
9985 assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
9988 assert!(out.sql.contains("AS \"__lim\""), "{}", out.sql);
9989 let agg = out.sql.find("array_agg").unwrap();
9990 let limit = out.sql.find("LIMIT 1").unwrap();
9991 assert!(agg < limit, "{}", out.sql);
9992 assert!(out.sql.contains("\"t1\".\"title\"::text AS \"c1\""), "{}", out.sql);
9993 }
9994
9995 #[test]
9996 fn test_multi_link_order_by_with_limit_orders_the_aggregate_too() {
9997 let out = compile_and_emit("SELECT Person { posts: { title } order by .title desc offset 1 limit 2 }");
9998 assert!(out.sql.contains("\"t1\".\"title\" AS \"s0\""), "{}", out.sql);
10001 assert!(
10002 out.sql.contains("ORDER BY \"__lim\".\"s0\" DESC NULLS LAST"),
10003 "{}",
10004 out.sql
10005 );
10006 assert!(out.sql.contains("OFFSET 1"), "{}", out.sql);
10007 assert!(out.sql.contains("LIMIT 2"), "{}", out.sql);
10008 }
10009
10010 #[test]
10011 fn test_multi_link_without_limit_keeps_the_flat_aggregate() {
10012 let out = compile_and_emit("SELECT Person { posts: { title } filter .title = 'x' }");
10013 assert!(!out.sql.contains("__lim"), "{}", out.sql);
10014 assert!(out.sql.contains("array_agg(ROW("), "{}", out.sql);
10015 }
10016
10017 #[test]
10018 fn test_sub_select_as_computed_pointer_is_a_link_pointer() {
10019 let out = compile_and_emit("SELECT Person { recent := (select .posts filter .title = 'x' limit 1) { title } }");
10020 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
10021 panic!()
10022 };
10023 let ShapeNode::Object {
10026 name,
10027 pointers: elem_pointers,
10028 ..
10029 } = &pointers[2]
10030 else {
10031 panic!("{:?}", pointers[2])
10032 };
10033 assert_eq!(name, "recent");
10034 assert!(matches!(&elem_pointers[1], ShapeNode::Scalar { name, .. } if name == "title"));
10035 assert!(out.sql.contains("\"jt\".source = \"t0\".id"), "{}", out.sql);
10036 assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
10037 }
10038
10039 #[test]
10040 fn test_sub_select_shape_inside_the_parens_is_equivalent() {
10041 let outer = compile_and_emit("SELECT Person { recent := (select .posts limit 1) { title } }");
10042 let inner = compile_and_emit("SELECT Person { recent := (select .posts { title } limit 1) }");
10043 assert_eq!(outer.sql, inner.sql);
10044 }
10045
10046 #[test]
10047 fn test_sub_select_field_access_is_a_correlated_scalar_subquery() {
10048 let out = compile_and_emit("SELECT Person { t := (select .posts filter .title = 'x' limit 1).title }");
10049 assert!(out.sql.contains("(\"t1\".\"id\" = \"t0\".\"id\")"), "{}", out.sql);
10052 assert!(out.sql.contains("\"t2\".\"title\""), "{}", out.sql);
10053 assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
10054 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
10055 panic!()
10056 };
10057 assert!(matches!(&pointers[2], ShapeNode::Scalar { name, .. } if name == "t"));
10058 }
10059
10060 #[test]
10061 fn test_sub_select_field_access_over_a_single_link() {
10062 let out = compile_and_emit("SELECT Person { c := (select .company).name }");
10063 assert!(out.sql.contains("\"t1\".\"company_id\" = \"t2\".\"id\""), "{}", out.sql);
10064 assert!(out.sql.contains("(\"t1\".\"id\" = \"t0\".\"id\")"), "{}", out.sql);
10065 }
10066
10067 #[test]
10068 fn test_sub_select_field_access_in_a_filter() {
10069 let out = compile_and_emit("SELECT Person { name } filter (select .posts limit 1).title = 'x'");
10070 assert!(out.sql.contains("WHERE ((SELECT \"t2\".\"title\""), "{}", out.sql);
10071 assert!(out.sql.contains("LIMIT 1) = 'x')"), "{}", out.sql);
10072 }
10073
10074 #[test]
10075 fn test_sub_select_over_an_absolute_path_is_not_correlated() {
10076 let out = compile_and_emit("SELECT Person { n := (select Company.name limit 1) }");
10077 assert!(out.sql.contains("FROM \"public\".\"Company\""), "{}", out.sql);
10078 assert!(!out.sql.contains("= \"t0\".\"id\""), "{}", out.sql);
10079 }
10080
10081 #[test]
10082 fn test_backlink_sub_select_carries_its_modifiers() {
10083 let out = compile_and_emit("SELECT Post { authors := (select .<posts[is Person] { name } limit 2) }");
10084 assert!(out.sql.contains("LIMIT 2"), "{}", out.sql);
10085 assert!(out.sql.contains("AS \"__lim\""), "{}", out.sql);
10086 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
10087 panic!()
10088 };
10089 assert!(matches!(&pointers[1], ShapeNode::Array { name, .. } if name == "authors"));
10090 }
10091
10092 #[test]
10093 fn test_with_bound_scalar_is_typed_for_overload_resolution() {
10094 let out = compile_and_emit("WITH xs := (select Person.name) SELECT contains(xs, 'a')");
10097 assert!(
10098 out.sql.contains("strpos((SELECT \"v\" FROM \"xs\"), 'a')"),
10099 "{}",
10100 out.sql
10101 );
10102 }
10103
10104 fn make_schema_with_computed_links() -> SchemaDescriptor {
10107 use crate::schema::ComputedDescriptor;
10108 let mut schema = make_schema();
10109 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
10110 person.computed = vec![
10111 ComputedDescriptor {
10112 name: "published".into(),
10113 expression: "(select .posts filter .title != '')".into(),
10114 return_type: None,
10115 link_target: None,
10116 link_multi: false,
10117 },
10118 ComputedDescriptor {
10119 name: "plain".into(),
10120 expression: ".posts".into(),
10121 return_type: None,
10122 link_target: None,
10123 link_multi: false,
10124 },
10125 ComputedDescriptor {
10126 name: "featured".into(),
10127 expression: "(select .published filter .title != 'draft')".into(),
10128 return_type: None,
10129 link_target: None,
10130 link_multi: false,
10131 },
10132 ComputedDescriptor {
10133 name: "looper".into(),
10134 expression: "(select .looper)".into(),
10135 return_type: None,
10136 link_target: None,
10137 link_multi: false,
10138 },
10139 ComputedDescriptor {
10140 name: "capped".into(),
10141 expression: "(select .posts limit 1)".into(),
10142 return_type: None,
10143 link_target: None,
10144 link_multi: false,
10145 },
10146 ];
10147 schema
10148 }
10149
10150 fn make_schema_with_object_fn_computed() -> SchemaDescriptor {
10153 use crate::schema::{ComputedDescriptor, FunctionDescriptor, FunctionParamDescriptor};
10154 let mut schema = make_schema();
10155 schema.functions = vec![FunctionDescriptor {
10156 name: "latest".into(),
10157 module: "default".into(),
10158 params: vec![FunctionParamDescriptor {
10159 name: "owner".into(),
10160 pg_type: "uuid".into(),
10161 }],
10162 return_pg_type: "default::Post".into(),
10163 return_is_object: true,
10164 return_is_set: true,
10165 return_is_polymorphic: false,
10166 volatility: "stable".into(),
10167 body: String::new(),
10168 }];
10169 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
10170 person.computed = vec![
10171 ComputedDescriptor {
10172 name: "recent".into(),
10173 expression: "latest(.id)".into(),
10174 return_type: None,
10175 link_target: None,
10176 link_multi: false,
10177 },
10178 ComputedDescriptor {
10179 name: "vetted".into(),
10180 expression: "(select latest(.id) filter .title != '')".into(),
10181 return_type: None,
10182 link_target: None,
10183 link_multi: false,
10184 },
10185 ];
10186 schema
10187 }
10188
10189 #[test]
10190 fn test_path_traverses_through_a_function_backed_computed() {
10191 let schema = make_schema_with_object_fn_computed();
10195 let out = compile_and_emit_with("SELECT Person { t := .recent.title }", &schema);
10196 assert!(
10197 out.sql
10198 .contains("CROSS JOIN LATERAL \"public\".\"latest\"(\"t1\".\"id\") AS \"t2\""),
10199 "{}",
10200 out.sql
10201 );
10202 assert!(out.sql.contains("ARRAY(SELECT \"t2\".\"title\""), "{}", out.sql);
10204 }
10205
10206 #[test]
10207 fn test_sub_select_over_a_function_backed_computed() {
10208 let schema = make_schema_with_object_fn_computed();
10211 let out = compile_and_emit_with(
10212 "SELECT Person { t := (select .recent filter .title = 'x' limit 1).title }",
10213 &schema,
10214 );
10215 assert!(
10216 out.sql.contains("CROSS JOIN LATERAL \"public\".\"latest\""),
10217 "{}",
10218 out.sql
10219 );
10220 assert!(out.sql.contains("\"t2\".\"title\" = 'x'"), "{}", out.sql);
10221 assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
10222 assert!(!out.sql.contains("ARRAY("), "{}", out.sql);
10223 }
10224
10225 #[test]
10226 fn test_function_backed_computed_carries_its_own_filter() {
10227 let schema = make_schema_with_object_fn_computed();
10228 let out = compile_and_emit_with(
10229 "SELECT Person { t := (select .vetted filter .title = 'x' limit 1).title }",
10230 &schema,
10231 );
10232 assert!(
10233 out.sql
10234 .contains("(\"t2\".\"title\" = 'x') AND (\"t2\".\"title\" <> '')"),
10235 "{}",
10236 out.sql
10237 );
10238 }
10239
10240 #[test]
10241 fn test_function_backed_computed_checks_its_argument_count() {
10242 use crate::schema::ComputedDescriptor;
10243 let mut schema = make_schema_with_object_fn_computed();
10244 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
10245 person.computed = vec![ComputedDescriptor {
10246 name: "recent".into(),
10247 expression: "latest()".into(),
10248 return_type: None,
10249 link_target: None,
10250 link_multi: false,
10251 }];
10252 let ast = parse::parse("SELECT Person { t := .recent.title }").unwrap();
10253 let err = match ir::compile(&ast, &schema) {
10254 Ok(_) => panic!("expected a compile error"),
10255 Err(e) => format!("{e}"),
10256 };
10257 assert!(err.contains("expects 1 argument(s), got 0"), "{err}");
10258 }
10259
10260 #[test]
10261 fn test_path_traverses_through_a_computed_pointer() {
10262 let schema = make_schema_with_computed_links();
10265 let out = compile_and_emit_with("SELECT Person { t := .published.title }", &schema);
10266 assert!(out.sql.contains("\"public\".\"Person.posts\""), "{}", out.sql);
10267 assert!(
10268 out.sql
10269 .contains("(\"t1\".\"id\" = \"t0\".\"id\") AND (\"t2\".\"title\" <> '')"),
10270 "{}",
10271 out.sql
10272 );
10273 }
10274
10275 #[test]
10276 fn test_sub_select_over_a_computed_pointer_projects_a_property() {
10277 let schema = make_schema_with_computed_links();
10280 let out = compile_and_emit_with(
10281 "SELECT Person { t := (select .published filter .title = 'a' limit 1).title }",
10282 &schema,
10283 );
10284 assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
10285 assert!(out.sql.contains("\"t2\".\"title\" = 'a'"), "{}", out.sql);
10287 assert!(out.sql.contains("\"t2\".\"title\" <> ''"), "{}", out.sql);
10288 }
10289
10290 #[test]
10291 fn test_chained_computed_pointers_each_contribute_their_filter() {
10292 let schema = make_schema_with_computed_links();
10293 let out = compile_and_emit_with("SELECT Person { t := .featured.title }", &schema);
10294 assert_eq!(out.sql.matches("\"public\".\"Person.posts\"").count(), 1, "{}", out.sql);
10295 assert!(out.sql.contains("\"t2\".\"title\" <> 'draft'"), "{}", out.sql);
10296 assert!(out.sql.contains("\"t2\".\"title\" <> ''"), "{}", out.sql);
10297 }
10298
10299 #[test]
10300 fn test_computed_pointer_naming_a_bare_link_is_traversable() {
10301 let schema = make_schema_with_computed_links();
10302 let out = compile_and_emit_with("SELECT Person { t := .plain.title }", &schema);
10303 assert!(out.sql.contains("ARRAY(SELECT \"t2\".\"title\""), "{}", out.sql);
10304 }
10305
10306 #[test]
10307 fn test_a_cycle_of_computed_pointers_is_reported_not_hung_on() {
10308 let schema = make_schema_with_computed_links();
10309 let ast = parse::parse("SELECT Person { t := .looper.title }").unwrap();
10310 let err = match ir::compile(&ast, &schema) {
10311 Ok(_) => panic!("expected a compile error"),
10312 Err(e) => format!("{e}"),
10313 };
10314 assert!(err.contains("expands into itself"), "{err}");
10315 }
10316
10317 #[test]
10318 fn test_a_cycle_of_computed_pointers_read_as_a_value_is_reported_not_hung_on() {
10319 let schema = make_schema_with_computed_links();
10322 for query in ["SELECT Person { looper }", "SELECT Person { * }"] {
10323 let ast = parse::parse(query).unwrap();
10324 let err = match ir::compile(&ast, &schema) {
10325 Ok(_) => panic!("expected a compile error for {query}"),
10326 Err(e) => format!("{e}"),
10327 };
10328 assert!(err.contains("expands into itself"), "{query}: {err}");
10329 }
10330 }
10331
10332 #[test]
10333 fn test_traversing_through_a_limited_computed_uses_a_lateral() {
10334 let schema = make_schema_with_computed_links();
10338 let out = compile_and_emit_with("SELECT Person { t := .capped.title }", &schema);
10339 assert!(out.sql.contains("JOIN LATERAL ("), "{}", out.sql);
10340 assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
10341 assert!(!out.sql.contains("ARRAY(SELECT"), "{}", out.sql);
10342 }
10343
10344 #[test]
10345 fn test_backlink_from_a_type_whose_interface_the_link_targets() {
10346 use crate::schema::{LinkDescriptor, TypeDescriptor};
10347 let mut schema = make_interface_schema();
10348 let individual = schema.types.iter().find(|t| t.name == "Individual").unwrap().clone();
10349 schema.types.push(TypeDescriptor {
10350 name: "Note".into(),
10351 module: "default".into(),
10352 table: "Note".into(),
10353 abstract_: false,
10354 materialized: true,
10355 description: None,
10356 parents: vec![],
10357 interfaces: vec![],
10358 bases: vec![],
10359 properties: individual.properties[..1].to_vec(),
10360 links: vec![LinkDescriptor {
10362 name: "owner".into(),
10363 target: "default::Account".into(),
10364 nullable: true,
10365 through: None,
10366 description: None,
10367 default_pyql: None,
10368 is_exclusive: false,
10369 is_readonly: false,
10370 rewrites: vec![],
10371 on_delete: vec![],
10372 }],
10373 multilinks: vec![],
10374 computed: vec![],
10375 constraints: vec![],
10376 indexes: vec![],
10377 partition: None,
10378 vector_indexes: vec![],
10379 search_indexes: vec![],
10380 triggers: vec![],
10381 junction: false,
10382 signals: vec![],
10383 });
10384 let out = compile_and_emit_with("SELECT Individual { notes := .<owner[is Note] }", &schema);
10387 assert!(out.sql.contains("\"public\".\"Note\""), "{}", out.sql);
10388 assert!(out.sql.contains("\"owner_id\" = \"t0\".id"), "{}", out.sql);
10389 }
10390
10391 #[test]
10392 fn test_with_block_in_expression_position_hoists_its_bindings() {
10393 let out = compile_and_emit("SELECT Person { t := (with c := (select Company.name limit 1) select c) }");
10396 assert!(out.sql.starts_with("WITH\n\"c\" AS ("), "{}", out.sql);
10397 assert!(out.sql.contains("(SELECT \"v\" FROM \"c\")"), "{}", out.sql);
10398 }
10399
10400 #[test]
10401 fn test_hoisted_binding_is_usable_in_the_expression_around_it() {
10402 let out =
10403 compile_and_emit("SELECT Person { t := (with c := (select Company.name limit 1) select .name ++ c) }");
10404 assert!(
10405 out.sql.contains("(\"t0\".\"name\" || (SELECT \"v\" FROM \"c\"))"),
10406 "{}",
10407 out.sql
10408 );
10409 }
10410
10411 #[test]
10412 fn test_hoisted_binding_joins_the_statements_own_with_clause() {
10413 let out = compile_and_emit(
10414 "WITH top := (select Person.name limit 1) \
10415 SELECT Person { t := (with c := (select Company.name limit 1) select c) }",
10416 );
10417 let top = out.sql.find("\"top\" AS (").expect("top-level binding");
10418 let inner = out.sql.find("\"c\" AS (").expect("hoisted binding");
10419 assert!(top < inner, "the statement's own bindings come first:\n{}", out.sql);
10420 }
10421
10422 #[test]
10423 fn test_a_binding_named_after_a_keyword_is_usable() {
10424 let out = compile_and_emit("WITH order := (select Person.name) SELECT Person { name } FILTER .name = order");
10429 assert!(out.sql.contains("(SELECT \"v\" FROM \"order\")"), "{}", out.sql);
10430 }
10431
10432 #[test]
10433 fn test_computed_declared_on_an_interface_is_visible_from_an_implementor() {
10434 use crate::schema::ComputedDescriptor;
10435 let mut schema = make_interface_schema();
10436 let account = schema.types.iter_mut().find(|t| t.name == "Account").unwrap();
10437 account.computed = vec![ComputedDescriptor {
10438 name: "tier".into(),
10439 expression: "'gold'".into(),
10440 return_type: None,
10441 link_target: None,
10442 link_multi: false,
10443 }];
10444 let individual = schema.types.iter_mut().find(|t| t.name == "Individual").unwrap();
10445 individual.computed = vec![];
10446 for q in ["SELECT Individual { tier }", "SELECT Individual { t := .tier }"] {
10449 let out = compile_and_emit_with(q, &schema);
10450 assert!(out.sql.contains("'gold'"), "{q}: {}", out.sql);
10451 }
10452 }
10453
10454 #[test]
10455 fn test_computed_may_carry_trailing_modifiers_without_a_select() {
10456 use crate::schema::ComputedDescriptor;
10457 let mut schema = make_schema();
10458 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
10459 person.computed = vec![ComputedDescriptor {
10460 name: "recent".into(),
10461 expression: ".posts order by .title desc limit 5".into(),
10462 return_type: None,
10463 link_target: None,
10464 link_multi: false,
10465 }];
10466 let out = compile_and_emit_with("SELECT Person { recent { title } }", &schema);
10469 assert!(out.sql.contains("LIMIT 5"), "{}", out.sql);
10470 assert!(out.sql.contains("ORDER BY \"t1\".\"title\" DESC"), "{}", out.sql);
10471 }
10472
10473 #[test]
10474 fn test_computed_may_lead_with_a_bare_select() {
10475 use crate::schema::ComputedDescriptor;
10476 let mut schema = make_schema();
10477 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
10478 person.computed = vec![ComputedDescriptor {
10479 name: "recent".into(),
10480 expression: "select .posts order by .title desc limit 5".into(),
10481 return_type: None,
10482 link_target: None,
10483 link_multi: false,
10484 }];
10485 let out = compile_and_emit_with("SELECT Person { recent { title } }", &schema);
10486 assert!(out.sql.contains("LIMIT 5"), "{}", out.sql);
10487 }
10488
10489 #[test]
10490 fn test_sub_select_with_a_shape_may_project_a_property_off_it() {
10491 let out = compile_and_emit("SELECT Person { t := (select .posts { title } limit 1).title }");
10492 assert!(out.sql.contains("\"t2\".\"title\""), "{}", out.sql);
10493 assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
10494 }
10495
10496 #[test]
10497 fn test_sub_statement_error_names_what_it_got() {
10498 let err = compile_err("SELECT Person { t := (insert Company { name := 'a' }) }");
10499 assert!(err.contains("an insert cannot stand in for a value"), "{err}");
10500 }
10501
10502 #[test]
10503 fn test_with_bound_scalar_path_select_emits_a_real_select() {
10504 let out = compile_and_emit("WITH xs := (select Person.name) SELECT Person { name }");
10507 assert!(
10508 out.sql
10509 .contains("SELECT ROW(v) AS result, v FROM (SELECT \"t0\".\"name\" AS v"),
10510 "{}",
10511 out.sql
10512 );
10513 }
10514
10515 #[test]
10516 fn test_with_bound_scalar_path_select_is_typed_as_its_result() {
10517 let out = compile_and_emit("WITH xs := (select Person.name) SELECT Person { name } FILTER .name IN xs");
10520 assert!(out.sql.contains("ANY((SELECT \"v\" FROM \"xs\"))"), "{}", out.sql);
10521 }
10522
10523 #[test]
10524 fn test_distinct_survives_a_with_binding() {
10525 let out = compile_and_emit("WITH xs := (select distinct Person.name) SELECT Person { name }");
10526 assert!(out.sql.contains("SELECT DISTINCT \"t0\".\"name\" AS v"), "{}", out.sql);
10527 }
10528
10529 #[test]
10530 fn test_for_over_a_derived_set_iterates_every_row() {
10531 let out = compile_and_emit("FOR x IN (select Person.name) UNION (SELECT Person { name } FILTER .name = x)");
10534 assert!(!out.sql.contains("VALUES"), "{}", out.sql);
10535 assert!(out.sql.contains(") AS \"_for_x\""), "{}", out.sql);
10536 assert!(out.sql.contains("CROSS JOIN LATERAL"), "{}", out.sql);
10537 assert!(out.sql.contains("\"t1\".\"name\" = \"_for_x\".\"v\""), "{}", out.sql);
10538 }
10539
10540 #[test]
10541 fn test_for_insert_over_a_derived_set_uses_it_as_the_cte() {
10542 let out = compile_and_emit("FOR x IN (select Person.name) UNION (INSERT Company { name := x })");
10543 assert!(out.sql.starts_with("WITH \"_for_x\" AS ("), "{}", out.sql);
10544 assert!(
10545 out.sql.contains("SELECT \"_for_x\".\"v\" FROM \"_for_x\""),
10546 "{}",
10547 out.sql
10548 );
10549 }
10550
10551 #[test]
10552 fn test_for_over_objects_binds_their_id() {
10553 let out = compile_and_emit("FOR p IN (select Person) UNION (INSERT Company { name := <str>p })");
10554 assert!(out.sql.contains("SELECT \"id\" AS v FROM ("), "{}", out.sql);
10555 }
10556
10557 #[test]
10558 fn test_for_over_a_non_select_statement_is_rejected() {
10559 let ast = parse::parse("FOR x IN (INSERT Company { name := 'a' }) UNION (SELECT Person { name })").unwrap();
10560 let schema = make_schema();
10561 let err = match ir::compile(&ast, &schema) {
10562 Ok(_) => panic!("expected a compile error"),
10563 Err(e) => format!("{e}"),
10564 };
10565 assert!(err.contains("only a select can be iterated"), "{err}");
10566 }
10567
10568 #[test]
10569 fn test_multi_link_path_outside_a_comparison_is_an_array() {
10570 let out = compile_and_emit("SELECT Person { t := .posts.title }");
10573 assert!(out.sql.contains("ARRAY(SELECT \"t2\".\"title\""), "{}", out.sql);
10574 assert!(out.sql.contains("(\"t1\".\"id\" = \"t0\".\"id\")"), "{}", out.sql);
10575 }
10576
10577 #[test]
10578 fn test_multi_link_path_inside_a_comparison_is_still_exists() {
10579 let out = compile_and_emit("SELECT Person { name } filter .posts.title = 'x'");
10580 assert!(out.sql.contains("WHERE EXISTS("), "{}", out.sql);
10581 assert!(!out.sql.contains("ARRAY("), "{}", out.sql);
10582 }
10583
10584 #[test]
10585 fn test_path_traversal_deeper_than_two_steps() {
10586 let schema = make_schema_with_through();
10587 let out = compile_and_emit_with("SELECT Person { t := .friends.friends.name }", &schema);
10588 assert_eq!(out.sql.matches("\"public\".\"PersonFriend\"").count(), 2, "{}", out.sql);
10590 assert!(out.sql.contains("ARRAY(SELECT \"t4\".\"name\""), "{}", out.sql);
10591 }
10592
10593 #[test]
10594 fn test_coalesce_over_backlinked_values_is_a_set_choice() {
10595 let out = compile_and_emit("SELECT Post ORDER BY max(.<posts[is Person].age ?? .<posts[is Person].age)");
10598 assert!(!out.sql.contains("EXISTS"), "{}", out.sql);
10599 assert!(!out.sql.contains("COALESCE("), "{}", out.sql);
10600 assert!(out.sql.contains("CASE WHEN (cardinality("), "{}", out.sql);
10601 assert!(
10602 out.sql
10603 .contains("(SELECT coalesce(max(\"_s\".\"v\"), NULL) FROM unnest("),
10604 "{}",
10605 out.sql
10606 );
10607 }
10608
10609 #[test]
10610 fn test_coalesce_over_multilink_values_is_a_set_choice() {
10611 let out = compile_and_emit("SELECT Person { t := .posts.title ?? .posts.title }");
10612 assert!(!out.sql.contains("EXISTS"), "{}", out.sql);
10613 assert!(out.sql.contains("CASE WHEN (cardinality("), "{}", out.sql);
10614 }
10615
10616 #[test]
10617 fn test_coalesce_of_a_set_with_a_single_value_falls_back_to_it() {
10618 let out = compile_and_emit("SELECT Post { a := .<posts[is Person].age ?? 0 }");
10619 assert!(out.sql.contains("ELSE array_remove(ARRAY[0], NULL) END"), "{}", out.sql);
10620 }
10621
10622 #[test]
10623 fn test_traversal_after_a_backlink() {
10624 let out = compile_and_emit("SELECT Post { t := .<posts[is Person].name }");
10625 assert!(out.sql.contains("ARRAY(SELECT \"t2\".\"name\""), "{}", out.sql);
10626 assert!(out.sql.contains("\"t3\".\"target\" = \"t1\".\"id\""), "{}", out.sql);
10627 }
10628
10629 #[test]
10630 fn test_computed_pointer_on_a_linked_type_is_reachable() {
10631 use crate::schema::ComputedDescriptor;
10632 let mut schema = make_schema();
10633 let company = schema.types.iter_mut().find(|t| t.name == "Company").unwrap();
10634 company.computed = vec![ComputedDescriptor {
10635 name: "shout".into(),
10636 expression: ".name ++ '!'".into(),
10637 return_type: Some("text".into()),
10638 link_target: None,
10639 link_multi: false,
10640 }];
10641 let out = compile_and_emit_with("SELECT Person { t := .company.shout }", &schema);
10645 assert!(out.sql.contains("(\"t2\".\"name\" || '!')"), "{}", out.sql);
10646 }
10647
10648 #[test]
10649 fn test_computed_pointer_behind_a_type_intersection() {
10650 let schema = make_interface_schema();
10651 let out = compile_and_emit_with("SELECT Account { n := [is Individual].full_name }", &schema);
10652 assert!(out.sql.contains("upper(\"t1\".\"first_name\")"), "{}", out.sql);
10653 assert!(out.sql.contains("FROM \"public\".\"Individual\""), "{}", out.sql);
10654 assert!(out.sql.contains("(\"t1\".\"id\" = \"t0\".\"id\")"), "{}", out.sql);
10655 }
10656
10657 #[test]
10658 fn test_computed_pointer_cannot_be_traversed_through() {
10659 use crate::schema::ComputedDescriptor;
10660 let mut schema = make_schema();
10661 let company = schema.types.iter_mut().find(|t| t.name == "Company").unwrap();
10662 company.computed = vec![ComputedDescriptor {
10663 name: "shout".into(),
10664 expression: ".name ++ '!'".into(),
10665 return_type: Some("text".into()),
10666 link_target: None,
10667 link_multi: false,
10668 }];
10669 let ast = parse::parse("SELECT Person { t := .company.shout.nope }").unwrap();
10670 let err = match ir::compile(&ast, &schema) {
10671 Ok(_) => panic!("expected a compile error"),
10672 Err(e) => format!("{e}"),
10673 };
10674 assert!(err.contains("is a computed pointer"), "{err}");
10675 }
10676
10677 #[test]
10678 fn test_sub_select_over_a_multi_link_path_keeps_its_modifiers() {
10679 let out = compile_and_emit("SELECT Person { t := (select .posts.title order by .title desc limit 2) }");
10680 assert!(out.sql.contains("ARRAY(SELECT"), "{}", out.sql);
10681 assert!(out.sql.contains("ORDER BY \"t2\".\"title\" DESC"), "{}", out.sql);
10682 assert!(out.sql.contains("LIMIT 2"), "{}", out.sql);
10683 }
10684
10685 #[test]
10686 fn test_limit_one_over_a_multi_link_path_stays_a_scalar() {
10687 let out = compile_and_emit("SELECT Person { t := (select .posts.title limit 1) }");
10690 assert!(!out.sql.contains("ARRAY("), "{}", out.sql);
10691 assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
10692 }
10693
10694 #[test]
10695 fn test_computed_multilink_can_carry_a_nested_shape() {
10696 let out = compile_and_emit("SELECT Person { p := .posts { title } }");
10697 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
10698 panic!()
10699 };
10700 let ShapeNode::Array { name, element, .. } = &pointers[2] else {
10701 panic!("{:?}", pointers[2])
10702 };
10703 assert_eq!(name, "p");
10704 let ShapeNode::Object {
10705 pointers: elem_pointers,
10706 ..
10707 } = element.as_ref()
10708 else {
10709 panic!()
10710 };
10711 assert!(matches!(&elem_pointers[1], ShapeNode::Scalar { name, .. } if name == "title"));
10712 }
10713
10714 #[test]
10715 fn test_schema_declared_backlink_computed_is_an_object_pointer() {
10716 use crate::schema::ComputedDescriptor;
10717 let mut schema = make_schema();
10718 let post = schema.types.iter_mut().find(|t| t.name == "Post").unwrap();
10719 post.computed = vec![ComputedDescriptor {
10720 name: "authors".into(),
10721 expression: ".<posts[is Person]".into(),
10722 return_type: None,
10723 link_target: None,
10724 link_multi: false,
10725 }];
10726 let out = compile_and_emit_with("SELECT Post { authors { name } }", &schema);
10729 assert!(out.sql.contains("array_agg(ROW("), "{}", out.sql);
10730 assert!(out.sql.contains("\"t1\".\"name\"::text"), "{}", out.sql);
10731 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
10732 panic!()
10733 };
10734 assert!(matches!(&pointers[1], ShapeNode::Array { name, .. } if name == "authors"));
10735 }
10736
10737 #[test]
10738 fn test_schema_declared_multilink_computed_defaults_to_ids_without_a_shape() {
10739 use crate::schema::ComputedDescriptor;
10740 let mut schema = make_schema();
10741 schema.types[0].computed = vec![ComputedDescriptor {
10742 name: "everything".into(),
10743 expression: ".posts".into(),
10744 return_type: None,
10745 link_target: None,
10746 link_multi: false,
10747 }];
10748 let out = compile_and_emit_with("SELECT Person { everything }", &schema);
10749 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
10750 panic!()
10751 };
10752 assert!(matches!(&pointers[2], ShapeNode::Array { name, .. } if name == "everything"));
10753 }
10754
10755 #[test]
10756 fn test_schema_declared_computed_sub_select_is_a_link_pointer() {
10757 use crate::schema::ComputedDescriptor;
10758 let mut schema = make_schema();
10759 schema.types[0].computed = vec![
10760 ComputedDescriptor {
10761 name: "recent".into(),
10762 expression: "(select .posts order by .title desc limit 1)".into(),
10763 return_type: None,
10764 link_target: None,
10765 link_multi: false,
10766 },
10767 ComputedDescriptor {
10768 name: "recent_title".into(),
10769 expression: "(select .posts order by .title desc limit 1).title".into(),
10770 return_type: Some("text".into()),
10771 link_target: None,
10772 link_multi: false,
10773 },
10774 ];
10775 let out = compile_and_emit_with("SELECT Person { recent { title }, recent_title }", &schema);
10776 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
10777 panic!()
10778 };
10779 assert!(matches!(&pointers[2], ShapeNode::Object { name, .. } if name == "recent"));
10782 assert!(matches!(&pointers[3], ShapeNode::Scalar { name, .. } if name == "recent_title"));
10783 assert!(out.sql.contains("\"jt\".source = \"t0\".id"), "{}", out.sql);
10784 assert_eq!(out.sql.matches("LIMIT 1").count(), 2, "{}", out.sql);
10785 crate::validate::validate_schema_types(&schema).expect("schema should validate");
10786 }
10787
10788 #[test]
10793 fn test_a_mutation_read_through_a_path_runs_as_a_hoisted_cte() {
10794 let out = compile_and_emit("SELECT (INSERT Person { name := 'x' }).name");
10795 assert!(
10796 out.sql.contains("\"_nested_dml_0\" AS (") && out.sql.contains("INSERT INTO"),
10797 "the mutation must run as a CTE:\n{}",
10798 out.sql
10799 );
10800 assert!(
10801 out.sql.contains("FROM \"_nested_dml_0\""),
10802 "the projection must read the mutation's own rows:\n{}",
10803 out.sql
10804 );
10805 }
10806
10807 #[test]
10808 fn test_dml_sub_statement_in_expression_position_still_rejected() {
10809 let err = compile_err("SELECT Person { x := (insert Company { name := 'c' }).name }");
10810 assert!(err.contains("an insert cannot stand in for a value"), "{err}");
10811 }
10812
10813 #[test]
10814 fn test_sub_select_with_a_shape_in_expression_position_is_rejected() {
10815 let err = compile_err("SELECT Person { name } filter (select .posts { title }) = 1");
10819 assert!(err.contains("sub-select with a shape"), "{err}");
10820 }
10821
10822 fn make_schema_with_through() -> SchemaDescriptor {
10823 let id_prop = || PropertyDescriptor {
10824 name: "id".into(),
10825 pg_type: "uuid".into(),
10826 nullable: false,
10827 default_sql: Some("gen_random_uuid()".into()),
10828 description: None,
10829 default_pyql: None,
10830 check_constraints: vec![],
10831 is_exclusive: true,
10832 is_pk: true,
10833 is_readonly: true,
10834 rewrites: vec![],
10835 tuple_members: None,
10836 column_type: None,
10837 };
10838 let name_prop = || PropertyDescriptor {
10839 name: "name".into(),
10840 pg_type: "text".into(),
10841 nullable: false,
10842 default_sql: None,
10843 description: None,
10844 check_constraints: vec![],
10845 default_pyql: None,
10846 is_exclusive: false,
10847 is_pk: false,
10848 is_readonly: false,
10849 rewrites: vec![],
10850 tuple_members: None,
10851 column_type: None,
10852 };
10853 SchemaDescriptor {
10854 types: vec![
10855 TypeDescriptor {
10856 name: "Person".into(),
10857 module: "default".into(),
10858 table: "Person".into(),
10859 abstract_: false,
10860 materialized: false,
10861 description: None,
10862 parents: vec![],
10863 interfaces: vec![],
10864 bases: vec![],
10865 properties: vec![id_prop(), name_prop()],
10866 links: vec![],
10867 multilinks: vec![MultiLinkDescriptor {
10868 name: "friends".into(),
10869 target: "default::Person".into(),
10870 through: Some("default::PersonFriend".into()),
10871 nullable: false,
10872 description: None,
10873 default_pyql: None,
10874 on_delete: vec![],
10875 is_exclusive: false,
10876 }],
10877 computed: vec![],
10878 constraints: vec![],
10879 indexes: vec![],
10880 partition: None,
10881 vector_indexes: vec![],
10882 search_indexes: vec![],
10883 triggers: vec![],
10884 junction: false,
10885 signals: vec![],
10886 },
10887 TypeDescriptor {
10888 name: "PersonFriend".into(),
10889 module: "default".into(),
10890 table: "PersonFriend".into(),
10891 abstract_: false,
10892 materialized: false,
10893 description: None,
10894 parents: vec![],
10895 interfaces: vec![],
10896 bases: vec![],
10897 properties: vec![id_prop()],
10898 links: vec![
10899 LinkDescriptor {
10900 name: "person".into(),
10901 target: "default::Person".into(),
10902 nullable: false,
10903 through: None,
10904 description: None,
10905 default_pyql: None,
10906 is_exclusive: false,
10907 is_readonly: false,
10908 rewrites: vec![],
10909 on_delete: vec![],
10910 },
10911 LinkDescriptor {
10912 name: "friend".into(),
10913 target: "default::Person".into(),
10914 nullable: false,
10915 through: None,
10916 description: None,
10917 default_pyql: None,
10918 is_exclusive: false,
10919 is_readonly: false,
10920 rewrites: vec![],
10921 on_delete: vec![],
10922 },
10923 ],
10924 multilinks: vec![],
10925 computed: vec![],
10926 constraints: vec![],
10927 indexes: vec![],
10928 partition: None,
10929 vector_indexes: vec![],
10930 search_indexes: vec![],
10931 triggers: vec![],
10932 junction: false,
10933 signals: vec![],
10934 },
10935 ],
10936 scalars: vec![],
10937 enums: vec![],
10938 named_tuples: vec![],
10939 globals: vec![],
10940 functions: vec![],
10941 aliases: vec![],
10942 channels: vec![],
10943 ..Default::default()
10944 }
10945 }
10946
10947 #[test]
10948 fn test_select_through_multi_link() {
10949 let schema = make_schema_with_through();
10950 let ast = crate::parse::parse("SELECT Person { name, friends { name } }").unwrap();
10951 let ir = crate::ir::compile(&ast, &schema).unwrap();
10952 let out = emit(&ir);
10953 assert!(out.sql.contains("\"public\".\"PersonFriend\""));
10955 assert!(out.sql.contains("\"friend\""));
10957 assert!(out.sql.contains("\"person\""));
10958 assert!(out.sql.contains("array_agg(ROW("));
10960 }
10961
10962 #[test]
10968 fn test_limited_multi_link_keeps_a_nested_multi_link_in_scope() {
10969 let schema = make_schema_with_through();
10972 let ast = crate::parse::parse("SELECT Person { friends: { name, friends { name } } limit 1 }").unwrap();
10973 let ir = crate::ir::compile(&ast, &schema).unwrap();
10974 let out = emit(&ir);
10975 assert_eq!(out.sql.matches("array_agg(ROW(").count(), 2, "{}", out.sql);
10976 let derived = out.sql.find("AS \"__lim\"").unwrap();
10977 let nested = out.sql.rfind("array_agg(ROW(").unwrap();
10978 assert!(
10979 nested < derived,
10980 "nested aggregate must sit inside the derived table:\n{}",
10981 out.sql
10982 );
10983 }
10984
10985 fn make_schema_with_junction_backed_link() -> SchemaDescriptor {
10986 let id_prop = || PropertyDescriptor {
10987 name: "id".into(),
10988 pg_type: "uuid".into(),
10989 nullable: false,
10990 default_sql: Some("gen_random_uuid()".into()),
10991 description: None,
10992 default_pyql: None,
10993 check_constraints: vec![],
10994 is_exclusive: true,
10995 is_pk: true,
10996 is_readonly: true,
10997 rewrites: vec![],
10998 tuple_members: None,
10999 column_type: None,
11000 };
11001 let name_prop = || PropertyDescriptor {
11002 name: "name".into(),
11003 pg_type: "text".into(),
11004 nullable: false,
11005 default_sql: None,
11006 description: None,
11007 check_constraints: vec![],
11008 default_pyql: None,
11009 is_exclusive: false,
11010 is_pk: false,
11011 is_readonly: false,
11012 rewrites: vec![],
11013 tuple_members: None,
11014 column_type: None,
11015 };
11016 SchemaDescriptor {
11017 types: vec![
11018 TypeDescriptor {
11019 name: "Person".into(),
11020 module: "default".into(),
11021 table: "Person".into(),
11022 abstract_: false,
11023 materialized: false,
11024 description: None,
11025 parents: vec![],
11026 interfaces: vec![],
11027 bases: vec![],
11028 properties: vec![id_prop(), name_prop()],
11029 links: vec![LinkDescriptor {
11030 name: "spouse".into(),
11031 target: "default::Org".into(),
11032 nullable: true,
11033 through: Some("default::Marriage".into()),
11034 description: None,
11035 default_pyql: None,
11036 is_exclusive: true,
11037 is_readonly: false,
11038 rewrites: vec![],
11039 on_delete: vec![],
11040 }],
11041 multilinks: vec![],
11042 computed: vec![],
11043 constraints: vec![],
11044 indexes: vec![],
11045 partition: None,
11046 vector_indexes: vec![],
11047 search_indexes: vec![],
11048 triggers: vec![],
11049 junction: false,
11050 signals: vec![],
11051 },
11052 TypeDescriptor {
11053 name: "Org".into(),
11054 module: "default".into(),
11055 table: "Org".into(),
11056 abstract_: false,
11057 materialized: false,
11058 description: None,
11059 parents: vec![],
11060 interfaces: vec![],
11061 bases: vec![],
11062 properties: vec![id_prop(), name_prop()],
11063 links: vec![],
11064 multilinks: vec![],
11065 computed: vec![],
11066 constraints: vec![],
11067 indexes: vec![],
11068 partition: None,
11069 vector_indexes: vec![],
11070 search_indexes: vec![],
11071 triggers: vec![],
11072 junction: false,
11073 signals: vec![],
11074 },
11075 TypeDescriptor {
11076 name: "Marriage".into(),
11077 module: "default".into(),
11078 table: "Person.spouse".into(),
11079 abstract_: false,
11080 materialized: false,
11081 description: None,
11082 parents: vec![],
11083 interfaces: vec![],
11084 bases: vec![],
11085 properties: vec![
11086 id_prop(),
11087 PropertyDescriptor {
11088 name: "since".into(),
11089 pg_type: "int8".into(),
11090 nullable: true,
11091 default_sql: None,
11092 description: None,
11093 check_constraints: vec![],
11094 default_pyql: None,
11095 is_exclusive: false,
11096 is_pk: false,
11097 is_readonly: false,
11098 rewrites: vec![],
11099 tuple_members: None,
11100 column_type: None,
11101 },
11102 ],
11103 links: vec![
11104 LinkDescriptor {
11105 name: "source".into(),
11106 target: "default::Person".into(),
11107 nullable: false,
11108 through: None,
11109 description: None,
11110 default_pyql: None,
11111 is_exclusive: false,
11112 is_readonly: false,
11113 rewrites: vec![],
11114 on_delete: vec![],
11115 },
11116 LinkDescriptor {
11117 name: "target".into(),
11118 target: "default::Org".into(),
11119 nullable: false,
11120 through: None,
11121 description: None,
11122 default_pyql: None,
11123 is_exclusive: false,
11124 is_readonly: false,
11125 rewrites: vec![],
11126 on_delete: vec![],
11127 },
11128 ],
11129 multilinks: vec![],
11130 computed: vec![],
11131 constraints: vec![],
11132 indexes: vec![],
11133 partition: None,
11134 vector_indexes: vec![],
11135 search_indexes: vec![],
11136 triggers: vec![],
11137 junction: true,
11138 signals: vec![],
11139 },
11140 ],
11141 scalars: vec![],
11142 enums: vec![],
11143 named_tuples: vec![],
11144 globals: vec![],
11145 functions: vec![],
11146 aliases: vec![],
11147 channels: vec![],
11148 ..Default::default()
11149 }
11150 }
11151
11152 #[test]
11153 fn test_select_shape_over_junction_backed_single_link() {
11154 let schema = make_schema_with_junction_backed_link();
11155 let ast = crate::parse::parse("SELECT Person { name, spouse { name } }").unwrap();
11156 let ir = crate::ir::compile(&ast, &schema).unwrap();
11157 let out = emit(&ir);
11158 assert!(out.sql.contains("\"public\".\"Person.spouse\""), "got:\n{}", out.sql);
11160 assert!(out.sql.contains("\"jt\".source"), "got:\n{}", out.sql);
11161 assert!(out.sql.contains("\"jt\".target"), "got:\n{}", out.sql);
11162 assert!(!out.sql.contains("array_agg"), "got:\n{}", out.sql);
11164 assert!(!out.sql.contains("spouse_id"), "got:\n{}", out.sql);
11165 }
11166
11167 #[test]
11168 fn test_select_shape_over_junction_backed_single_link_with_link_property() {
11169 let schema = make_schema_with_junction_backed_link();
11175 let ast = crate::parse::parse("SELECT Person { name, spouse { name, @since } }").unwrap();
11176 let ir = crate::ir::compile(&ast, &schema).unwrap();
11177 let out = emit(&ir);
11178 assert!(out.sql.contains("\"jt\".\"since\""), "got:\n{}", out.sql);
11179 assert!(!out.sql.contains("array_agg"), "got:\n{}", out.sql);
11180 }
11181
11182 #[test]
11183 fn test_select_path_over_junction_backed_single_link() {
11184 let schema = make_schema_with_junction_backed_link();
11185 let ast = crate::parse::parse("SELECT Person.spouse { name }").unwrap();
11186 let ir = crate::ir::compile(&ast, &schema).unwrap();
11187 let out = emit(&ir);
11188 assert!(out.sql.contains("\"public\".\"Person.spouse\""), "got:\n{}", out.sql);
11189 assert!(out.sql.contains("\"source\""), "got:\n{}", out.sql);
11190 assert!(out.sql.contains("\"target\""), "got:\n{}", out.sql);
11191 assert!(!out.sql.contains("spouse_id"), "got:\n{}", out.sql);
11192 }
11193
11194 #[test]
11195 fn test_insert_with_junction_backed_single_link() {
11196 let schema = make_schema_with_junction_backed_link();
11197 let out = compile_and_emit_with(
11198 "INSERT Person { name := $name, \
11199 spouse := (SELECT Org FILTER .id = $oid) { @since := <int64>$since } }",
11200 &schema,
11201 );
11202 assert!(out.sql.contains("\"public\".\"Person.spouse\""), "got:\n{}", out.sql);
11203 assert!(out.sql.contains("\"since\""), "got:\n{}", out.sql);
11204 assert!(!out.sql.contains("spouse_id"), "got:\n{}", out.sql);
11205 assert!(out.sql.contains("ON CONFLICT (\"source\") DO UPDATE SET \"target\" = EXCLUDED.\"target\", \"since\" = EXCLUDED.\"since\""), "got:\n{}", out.sql);
11212 }
11213
11214 #[test]
11215 fn test_update_replace_junction_backed_single_link() {
11216 let schema = make_schema_with_junction_backed_link();
11217 let out = compile_and_emit_with(
11218 "UPDATE Person FILTER .id = $id SET { \
11219 spouse := (SELECT Org FILTER .id = $oid) { @since := <int64>$since } }",
11220 &schema,
11221 );
11222 assert!(
11224 out.sql.contains("DELETE FROM \"public\".\"Person.spouse\""),
11225 "got:\n{}",
11226 out.sql
11227 );
11228 assert!(
11229 out.sql.contains("INSERT INTO \"public\".\"Person.spouse\""),
11230 "got:\n{}",
11231 out.sql
11232 );
11233 assert!(out.sql.contains("ON CONFLICT (\"source\") DO UPDATE SET \"target\" = EXCLUDED.\"target\", \"since\" = EXCLUDED.\"since\""), "got:\n{}", out.sql);
11234 assert!(out.sql.contains("NOT IN"), "got:\n{}", out.sql);
11239 }
11240
11241 #[test]
11246 fn test_unless_conflict_else_update_keeps_its_filter() {
11247 let out = compile_and_emit(
11248 "INSERT Person { name := 'a' } UNLESS CONFLICT ON .name \
11249 ELSE (UPDATE Person FILTER .age < 30 SET { name := 'b' })",
11250 );
11251 assert!(
11252 out.sql.contains("DO UPDATE SET") && out.sql.contains("WHERE"),
11253 "the ELSE UPDATE's filter must survive as a DO UPDATE predicate:\n{}",
11254 out.sql
11255 );
11256 assert!(
11257 out.sql.contains("\"Person\".\"age\""),
11258 "the predicate must read the existing row, not `excluded`:\n{}",
11259 out.sql
11260 );
11261 }
11262
11263 #[test]
11265 fn test_unless_conflict_else_update_without_a_filter_has_no_predicate() {
11266 let out = compile_and_emit(
11267 "INSERT Person { name := 'a' } UNLESS CONFLICT ON .name ELSE (UPDATE Person SET { name := 'b' })",
11268 );
11269 let after_set = out.sql.split("DO UPDATE SET").nth(1).unwrap_or_default();
11270 assert!(
11271 !after_set.contains("WHERE"),
11272 "got a predicate we never asked for:\n{}",
11273 out.sql
11274 );
11275 }
11276
11277 #[test]
11278 fn test_junction_backed_single_link_rejected_in_unless_conflict_else() {
11279 let schema = make_schema_with_junction_backed_link();
11286 let ast = parse::parse(
11287 "INSERT Person { name := $name } \
11288 UNLESS CONFLICT ON .name ELSE (UPDATE Person SET { \
11289 spouse := (SELECT Org FILTER .id = $oid) })",
11290 )
11291 .unwrap();
11292 assert!(ir::compile(&ast, &schema).is_err());
11293 }
11294
11295 #[test]
11296 fn test_update_clear_junction_backed_single_link() {
11297 let schema = make_schema_with_junction_backed_link();
11298 let out = compile_and_emit_with("UPDATE Person FILTER .id = $id SET { spouse := {} }", &schema);
11299 assert!(
11300 out.sql.contains("DELETE FROM \"public\".\"Person.spouse\""),
11301 "got:\n{}",
11302 out.sql
11303 );
11304 assert!(
11305 !out.sql.contains("INSERT INTO \"public\".\"Person.spouse\""),
11306 "clearing must not also insert:\n{}",
11307 out.sql
11308 );
11309 }
11310
11311 #[test]
11312 fn test_update_clear_junction_backed_single_link_with_cast_empty_set() {
11313 let schema = make_schema_with_junction_backed_link();
11321 let out = compile_and_emit_with("UPDATE Person FILTER .id = $id SET { spouse := <Org>{} }", &schema);
11322 assert!(
11323 out.sql.contains("DELETE FROM \"public\".\"Person.spouse\""),
11324 "got:\n{}",
11325 out.sql
11326 );
11327 assert!(
11328 !out.sql.contains("INSERT INTO \"public\".\"Person.spouse\""),
11329 "clearing must not also insert:\n{}",
11330 out.sql
11331 );
11332 }
11333
11334 fn make_schema_with_through_and_prop() -> SchemaDescriptor {
11342 let id_prop = || PropertyDescriptor {
11343 name: "id".into(),
11344 pg_type: "uuid".into(),
11345 nullable: false,
11346 default_sql: Some("gen_random_uuid()".into()),
11347 description: None,
11348 default_pyql: None,
11349 check_constraints: vec![],
11350 is_exclusive: true,
11351 is_pk: true,
11352 is_readonly: true,
11353 rewrites: vec![],
11354 tuple_members: None,
11355 column_type: None,
11356 };
11357 let name_prop = || PropertyDescriptor {
11358 name: "name".into(),
11359 pg_type: "text".into(),
11360 nullable: false,
11361 default_sql: None,
11362 description: None,
11363 check_constraints: vec![],
11364 default_pyql: None,
11365 is_exclusive: false,
11366 is_pk: false,
11367 is_readonly: false,
11368 rewrites: vec![],
11369 tuple_members: None,
11370 column_type: None,
11371 };
11372 SchemaDescriptor {
11373 types: vec![
11374 TypeDescriptor {
11375 name: "Product".into(),
11376 module: "default".into(),
11377 table: "Product".into(),
11378 abstract_: false,
11379 materialized: false,
11380 description: None,
11381 parents: vec![],
11382 interfaces: vec![],
11383 bases: vec![],
11384 properties: vec![id_prop(), name_prop()],
11385 links: vec![],
11386 multilinks: vec![MultiLinkDescriptor {
11387 name: "tags".into(),
11388 target: "default::Tag".into(),
11389 through: Some("default::ProductTag".into()),
11390 nullable: false,
11391 description: None,
11392 default_pyql: None,
11393 on_delete: vec![],
11394 is_exclusive: false,
11395 }],
11396 computed: vec![],
11397 constraints: vec![],
11398 indexes: vec![],
11399 partition: None,
11400 vector_indexes: vec![],
11401 search_indexes: vec![],
11402 triggers: vec![],
11403 junction: false,
11404 signals: vec![],
11405 },
11406 TypeDescriptor {
11407 name: "Tag".into(),
11408 module: "default".into(),
11409 table: "Tag".into(),
11410 abstract_: false,
11411 materialized: false,
11412 description: None,
11413 parents: vec![],
11414 interfaces: vec![],
11415 bases: vec![],
11416 properties: vec![id_prop(), name_prop()],
11417 links: vec![],
11418 multilinks: vec![],
11419 computed: vec![],
11420 constraints: vec![],
11421 indexes: vec![],
11422 partition: None,
11423 vector_indexes: vec![],
11424 search_indexes: vec![],
11425 triggers: vec![],
11426 junction: false,
11427 signals: vec![],
11428 },
11429 TypeDescriptor {
11430 name: "ProductTag".into(),
11431 module: "default".into(),
11432 table: "Product.tags".into(),
11433 abstract_: false,
11434 materialized: false,
11435 description: None,
11436 parents: vec![],
11437 interfaces: vec![],
11438 bases: vec![],
11439 properties: vec![
11440 id_prop(),
11441 PropertyDescriptor {
11442 name: "weight".into(),
11443 pg_type: "float8".into(),
11444 nullable: false,
11445 default_sql: None,
11446 default_pyql: None,
11447 description: None,
11448 check_constraints: vec![],
11449 is_exclusive: false,
11450 is_pk: false,
11451 is_readonly: false,
11452 rewrites: vec![],
11453 tuple_members: None,
11454 column_type: None,
11455 },
11456 ],
11457 links: vec![],
11461 multilinks: vec![],
11462 computed: vec![],
11463 constraints: vec![],
11464 indexes: vec![],
11465 partition: None,
11466 vector_indexes: vec![],
11467 search_indexes: vec![],
11468 triggers: vec![],
11469 junction: true,
11470 signals: vec![],
11471 },
11472 ],
11473 scalars: vec![],
11474 enums: vec![],
11475 named_tuples: vec![],
11476 globals: vec![],
11477 functions: vec![],
11478 aliases: vec![],
11479 channels: vec![],
11480 ..Default::default()
11481 }
11482 }
11483
11484 #[test]
11485 fn test_link_property_in_a_links_own_filter() {
11486 let schema = make_schema_with_through_and_prop();
11490 let out = compile_and_emit_with("SELECT Product { tags: { } filter @weight > 0.5 }", &schema);
11491 assert!(out.sql.contains("\"jt\".\"weight\" > (0.5::float8)"), "{}", out.sql);
11492 }
11493
11494 #[test]
11495 fn test_exists_on_a_link_property() {
11496 let schema = make_schema_with_through_and_prop();
11499 let out = compile_and_emit_with("SELECT Product { tags: { } filter exists @weight }", &schema);
11500 assert!(out.sql.contains("\"jt\".\"weight\" IS NOT NULL"), "{}", out.sql);
11501 }
11502
11503 #[test]
11504 fn test_link_property_in_a_computed_link_filter() {
11505 let schema = make_schema_with_through_and_prop();
11506 let out = compile_and_emit_with(
11507 "SELECT Product { best := (select .tags filter @weight > 0.5 limit 1) { @weight } }",
11508 &schema,
11509 );
11510 assert!(out.sql.contains("\"jt\".\"weight\" > (0.5::float8)"), "{}", out.sql);
11511 assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
11512 }
11513
11514 #[test]
11515 fn test_link_property_in_a_projected_sub_select_reads_the_junction() {
11516 let schema = make_schema_with_through_and_prop();
11519 let out = compile_and_emit_with(
11520 "SELECT Product { t := (select .tags filter @weight > 0.5 limit 1).id }",
11521 &schema,
11522 );
11523 assert!(out.sql.contains("\"t3\".\"weight\" > (0.5::float8)"), "{}", out.sql);
11524 }
11525
11526 #[test]
11527 fn test_unknown_link_property_names_the_through_type() {
11528 let schema = make_schema_with_through_and_prop();
11529 let ast = parse::parse("SELECT Product { tags: { } filter @nope > 0.5 }").unwrap();
11530 let err = match ir::compile(&ast, &schema) {
11531 Ok(_) => panic!("expected a compile error"),
11532 Err(e) => format!("{e}"),
11533 };
11534 assert!(err.contains("default::ProductTag") && err.contains("nope"), "{err}");
11535 }
11536
11537 #[test]
11538 fn test_link_property_outside_a_link_says_where_it_belongs() {
11539 let schema = make_schema_with_through_and_prop();
11540 let ast = parse::parse("SELECT Product { t := @weight }").unwrap();
11541 let err = match ir::compile(&ast, &schema) {
11542 Ok(_) => panic!("expected a compile error"),
11543 Err(e) => format!("{e}"),
11544 };
11545 assert!(err.contains("is a link property"), "{err}");
11546 }
11547
11548 #[test]
11549 fn test_multilink_append_with_link_property() {
11550 let schema = make_schema_with_through_and_prop();
11551 let out = compile_and_emit_with(
11552 "UPDATE Product FILTER .id = $id SET { tags += (SELECT Tag FILTER .id = $tid) { @weight := <float64>$w } }",
11553 &schema,
11554 );
11555 assert!(out.sql.contains("\"weight\""), "missing weight column:\n{}", out.sql);
11557 assert!(
11560 out.sql
11561 .contains("ON CONFLICT (\"source\", \"target\") DO UPDATE SET \"weight\" = EXCLUDED.\"weight\""),
11562 "missing upsert conflict clause:\n{}",
11563 out.sql
11564 );
11565 }
11566
11567 #[test]
11568 fn test_multilink_append_union_with_different_link_property_values() {
11569 let schema = make_schema_with_through_and_prop();
11573 let out = compile_and_emit_with(
11574 "UPDATE Product FILTER .id = $id SET { \
11575 tags += (SELECT Tag FILTER .id = $aid) { @weight := <float64>$w1 } \
11576 union (SELECT Tag FILTER .id = $bid) { @weight := <float64>$w2 } \
11577 }",
11578 &schema,
11579 );
11580 assert!(
11581 out.sql.contains("UNION ALL"),
11582 "expected a UNION ALL between the two shaped targets:\n{}",
11583 out.sql
11584 );
11585 assert_eq!(
11588 out.sql.matches("AS \"weight\"").count(),
11589 2,
11590 "each union branch must project its own weight:\n{}",
11591 out.sql
11592 );
11593 }
11594
11595 #[test]
11596 fn test_multilink_append_without_link_property_keeps_do_nothing() {
11597 let schema = make_schema_with_through_and_prop();
11600 let out = compile_and_emit_with(
11601 "UPDATE Product FILTER .id = $id SET { tags += (SELECT Tag FILTER .id = $tid) }",
11602 &schema,
11603 );
11604 assert!(
11605 out.sql.contains("ON CONFLICT DO NOTHING"),
11606 "expected plain DO NOTHING when no link properties are set:\n{}",
11607 out.sql
11608 );
11609 assert!(
11610 !out.sql.contains("\"weight\""),
11611 "unexpected weight column with no link property assignment:\n{}",
11612 out.sql
11613 );
11614 }
11615
11616 #[test]
11617 fn test_multilink_link_property_rejected_on_standard_junction() {
11618 let ast = crate::parse::parse(
11621 "UPDATE Person FILTER .id = $id SET { posts += (SELECT Post FILTER .title = $t) { @weight := <float64>$w } }",
11622 ).unwrap();
11623 match crate::ir::compile(&ast, &make_schema()) {
11624 Ok(_) => panic!("expected a compile error for link property on a Standard junction"),
11625 Err(e) => assert!(
11626 e.to_string().contains("Through"),
11627 "expected a Through[...]-related error, got: {e}"
11628 ),
11629 }
11630 }
11631
11632 #[test]
11633 fn test_multilink_link_property_rejected_on_remove() {
11634 let schema = make_schema_with_through_and_prop();
11635 let ast = crate::parse::parse(
11636 "UPDATE Product FILTER .id = $id SET { tags -= (SELECT Tag FILTER .id = $tid) { @weight := <float64>$w } }",
11637 )
11638 .unwrap();
11639 match crate::ir::compile(&ast, &schema) {
11640 Ok(_) => panic!("expected a compile error for link property on a remove (-=)"),
11641 Err(e) => assert!(
11642 e.to_string().contains("removing"),
11643 "expected a remove-related error, got: {e}"
11644 ),
11645 }
11646 }
11647
11648 #[test]
11649 fn test_insert_with_multilink_assignment() {
11650 let schema = make_schema_with_through_and_prop();
11655 let out = compile_and_emit_with(
11656 "INSERT Product { name := $name, tags := (SELECT Tag FILTER .id = $tid) { @weight := <float64>$w } }",
11657 &schema,
11658 );
11659 assert!(
11661 out.sql.contains("\"_w__ids\" AS (\nINSERT INTO"),
11662 "missing row-insert CTE:\n{}",
11663 out.sql
11664 );
11665 assert!(
11666 out.sql.contains("\"_w__ml_add_0\" AS ("),
11667 "missing junction-append CTE:\n{}",
11668 out.sql
11669 );
11670 assert!(out.sql.contains("\"weight\""), "missing weight column:\n{}", out.sql);
11671 assert!(
11672 out.sql.contains("\"_w\" AS (\n SELECT * FROM \"_w__ids\"\n)"),
11673 "missing _w passthrough:\n{}",
11674 out.sql
11675 );
11676 assert_eq!(
11677 out.sql.matches("WITH\n").count(),
11678 1,
11679 "must be a single flat top-level WITH block:\n{}",
11680 out.sql
11681 );
11682 }
11683
11684 #[test]
11692 fn test_user_with_binding_merges_into_a_statements_own_ctes() {
11693 let schema = make_schema_with_through_and_prop();
11694 for query in [
11695 "with t := (select Tag filter .id = $tid) insert Product { name := $name, tags := t }",
11696 "with t := (select Tag filter .id = $tid) \
11697 update Product filter .name = $name set { tags += t }",
11698 ] {
11699 let out = compile_and_emit_with(query, &schema);
11700 let flat: String = out.sql.split_whitespace().collect::<Vec<_>>().join(" ");
11701 assert!(
11702 !flat.contains(") WITH "),
11703 "a second WITH clause is invalid SQL:\n{}",
11704 out.sql
11705 );
11706 assert_eq!(
11707 out.sql.matches("WITH").count(),
11708 1,
11709 "exactly one WITH keyword expected:\n{}",
11710 out.sql
11711 );
11712 }
11713 }
11714
11715 #[test]
11716 fn test_with_bound_insert_with_multilink_assignment() {
11717 let schema = make_schema_with_through_and_prop();
11721 let out = compile_and_emit_with(
11722 "with insert0 := (insert Product { name := $name, tags := (select Tag filter .id = $tid) }) select insert0",
11723 &schema,
11724 );
11725 assert!(
11726 out.sql.contains("\"insert0__ids\" AS (\nINSERT INTO"),
11727 "missing row-insert CTE:\n{}",
11728 out.sql
11729 );
11730 assert!(
11731 out.sql.contains("\"insert0__ml_add_0\" AS ("),
11732 "missing junction-append CTE:\n{}",
11733 out.sql
11734 );
11735 assert!(
11736 out.sql
11737 .contains("\"insert0\" AS (\n SELECT * FROM \"insert0__ids\"\n)"),
11738 "missing insert0 passthrough:\n{}",
11739 out.sql
11740 );
11741 assert_eq!(
11742 out.sql.matches("WITH\n").count(),
11743 1,
11744 "must be a single flat top-level WITH block:\n{}",
11745 out.sql
11746 );
11747 }
11748
11749 #[test]
11750 fn test_with_block_cte_over_computed_global_merges_into_single_with_clause() {
11751 let mut schema = make_schema();
11760 schema.globals.push(GlobalDescriptor {
11761 name: "current_user_id".into(),
11762 module: "default".into(),
11763 scalar_type: "std::uuid".into(),
11764 required: false,
11765 default_expr: None,
11766 computed_expr: None,
11767 });
11768 schema.globals.push(GlobalDescriptor {
11769 name: "current_user".into(),
11770 module: "default".into(),
11771 scalar_type: "Person".into(),
11772 required: false,
11773 default_expr: None,
11774 computed_expr: Some("select default::Person filter .id = global current_user_id".into()),
11775 });
11776 let out = compile_and_emit_with("with\n user := (select global current_user)\nselect user;", &schema);
11777 assert_eq!(
11778 out.sql.matches("WITH").count(),
11779 1,
11780 "must be a single WITH clause, got:\n{}",
11781 out.sql
11782 );
11783 }
11784
11785 #[test]
11786 fn test_several_mutations_in_a_free_set_each_become_a_cte() {
11787 let schema = make_schema();
11788 let out = compile_and_emit_with(
11789 "select { (update Person filter .name = 'a' set { age := 1 }), \
11790 (update Company filter .name = 'b' set { name := 'c' }) }",
11791 &schema,
11792 );
11793 assert_eq!(
11794 out.sql.matches("UPDATE").count(),
11795 2,
11796 "both mutations have to run, got:\n{}",
11797 out.sql
11798 );
11799 assert!(out.sql.contains("WITH"), "each becomes a CTE, got:\n{}", out.sql);
11800 }
11801
11802 #[test]
11803 fn test_updating_a_binding_touches_only_its_own_rows() {
11804 let schema = make_schema();
11807 let out = compile_and_emit_with(
11808 "with mine := (select Person filter .name = 'a') select { (update mine set { age := 1 }) }",
11809 &schema,
11810 );
11811 assert!(
11812 out.sql.contains("UPDATE") && out.sql.contains("= ANY(ARRAY(SELECT"),
11813 "expected the update narrowed to the binding's rows, got:\n{}",
11814 out.sql
11815 );
11816 }
11817
11818 #[test]
11819 fn test_updating_a_traversal_touches_only_the_rows_it_lands_on() {
11820 let mut schema = make_schema();
11821 let company = schema
11824 .types
11825 .iter_mut()
11826 .find(|t| t.name == "Company")
11827 .expect("test schema has a Company type");
11828 company.properties.push(crate::schema::PropertyDescriptor {
11829 name: "id".into(),
11830 pg_type: "uuid".into(),
11831 nullable: false,
11832 default_sql: Some("uuidv7()".into()),
11833 default_pyql: None,
11834 description: None,
11835 check_constraints: vec![],
11836 is_exclusive: true,
11837 is_pk: true,
11838 is_readonly: true,
11839 rewrites: vec![],
11840 tuple_members: None,
11841 column_type: None,
11842 });
11843 let out = compile_and_emit_with(
11844 "with mine := (select Person filter .name = 'a') select { (update mine.company set { name := 'x' }) }",
11845 &schema,
11846 );
11847 assert!(
11848 out.sql.contains("= ANY(ARRAY(SELECT"),
11849 "expected the update narrowed to the traversal's rows, got:\n{}",
11850 out.sql
11851 );
11852 assert!(
11853 out.sql.contains("\"Company\""),
11854 "expected the table the traversal ends on, got:\n{}",
11855 out.sql
11856 );
11857 }
11858
11859 #[test]
11860 fn test_aggregate_over_a_multi_valued_path_keeps_the_set_flat() {
11861 let schema = make_schema();
11866 let out = compile_and_emit_with(
11867 "with a := (select Person limit 1) select { titles := array_agg(a.posts.title) }",
11868 &schema,
11869 );
11870 assert!(
11871 !out.sql.contains("array_agg(ARRAY("),
11872 "the aggregate must take the set, not the array standing for it, got:\n{}",
11873 out.sql
11874 );
11875 assert!(
11876 out.sql.contains("array_agg(") && out.sql.contains("\"title\""),
11877 "expected the aggregate over the traversal's own column, got:\n{}",
11878 out.sql
11879 );
11880 assert!(
11883 out.sql.contains("ROW(") && !out.sql.contains("jsonb_build_object"),
11884 "expected a composite row, got:\n{}",
11885 out.sql
11886 );
11887 }
11888
11889 #[test]
11890 fn test_with_bound_root_read_by_name_inside_a_nested_select() {
11891 let schema = make_schema();
11896 let out = compile_and_emit_with(
11897 "with
11898 owner := (select Person limit 1)
11899select owner { posts := (select owner.posts.title) };",
11900 &schema,
11901 );
11902 assert!(
11903 out.sql.contains("WITH") && out.sql.contains("\"title\""),
11904 "expected the nested select to traverse the binding, got:\n{}",
11905 out.sql
11906 );
11907 }
11908
11909 #[test]
11910 fn test_path_traversal_into_with_bound_cte_of_object_type() {
11911 let mut schema = make_schema();
11917 schema.globals.push(GlobalDescriptor {
11918 name: "current_user_id".into(),
11919 module: "default".into(),
11920 scalar_type: "std::uuid".into(),
11921 required: false,
11922 default_expr: None,
11923 computed_expr: None,
11924 });
11925 schema.globals.push(GlobalDescriptor {
11926 name: "current_user".into(),
11927 module: "default".into(),
11928 scalar_type: "Person".into(),
11929 required: false,
11930 default_expr: None,
11931 computed_expr: Some("select default::Person filter .id = global current_user_id".into()),
11932 });
11933 let out = compile_and_emit_with(
11934 "with\n user := (select global current_user)\nselect user.name;",
11935 &schema,
11936 );
11937 assert!(
11938 out.sql.contains("FROM \"user\""),
11939 "expected path traversal from the CTE, got:\n{}",
11940 out.sql
11941 );
11942 assert_eq!(
11943 out.sql.matches("WITH").count(),
11944 1,
11945 "must be a single WITH clause, got:\n{}",
11946 out.sql
11947 );
11948 }
11949
11950 #[test]
11951 fn test_with_bound_schema_object_property_access_in_filter_expression() {
11952 let schema = make_schema();
11960 let out = compile_and_emit_with(
11961 "with\n person := (select detached Person filter .id = <uuid>$id)\n\
11962 select Person filter .id not in person.id;",
11963 &schema,
11964 );
11965 assert!(
11966 out.sql.contains("<> ALL((SELECT") && out.sql.contains(".\"id\"") && out.sql.contains("FROM \"person\""),
11967 "got:\n{}",
11968 out.sql
11969 );
11970 }
11971
11972 #[test]
11973 fn test_with_bound_schema_object_scalar_property_access() {
11974 let schema = make_schema();
11978 let out = compile_and_emit_with(
11979 "with\n person := (select detached Person filter .id = <uuid>$id)\n\
11980 select Person filter .name = person.name;",
11981 &schema,
11982 );
11983 assert!(
11984 out.sql.contains("(SELECT") && out.sql.contains(".\"name\"") && out.sql.contains("FROM \"person\""),
11985 "got:\n{}",
11986 out.sql
11987 );
11988 }
11989
11990 #[test]
11991 fn test_with_bound_schema_object_link_traversal_reaches_nested_property() {
11992 let schema = make_schema();
11999 let out = compile_and_emit_with(
12000 "with\n person := (select detached Person filter .id = <uuid>$id)\n\
12001 select Company filter .name = person.company.name;",
12002 &schema,
12003 );
12004 assert!(
12005 out.sql.contains("(SELECT") && out.sql.contains("\"name\"") && out.sql.contains("\"Company\""),
12006 "got:\n{}",
12007 out.sql
12008 );
12009 }
12010
12011 #[test]
12012 fn test_with_bound_schema_object_bare_link_reduces_to_id() {
12013 let schema = make_schema();
12017 let out = compile_and_emit_with(
12018 "with\n person := (select detached Person filter .id = <uuid>$id)\n\
12019 select Person filter .id = person.company;",
12020 &schema,
12021 );
12022 assert!(
12023 out.sql.contains("(SELECT") && out.sql.contains("\"id\""),
12024 "got:\n{}",
12025 out.sql
12026 );
12027 }
12028
12029 #[test]
12030 fn test_with_bound_schema_object_unknown_property_suggests_a_close_match() {
12031 let schema = make_schema();
12032 let ast = parse::parse(
12033 "with\n person := (select detached Person filter .id = <uuid>$id)\n\
12034 select Person filter .name = person.nam;",
12035 )
12036 .unwrap();
12037 match ir::compile(&ast, &schema) {
12038 Err(err) => assert!(format!("{err}").contains("Did you mean 'name'"), "got: {err}"),
12039 Ok(_) => panic!("expected a compile error"),
12040 }
12041 }
12042
12043 #[test]
12044 fn test_with_bound_free_object_passthrough_preserves_all_fields() {
12045 let out = compile_and_emit("with\n test := { test2 := 1.0, test3 := 'str' }\nselect test;");
12051 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
12052 panic!("expected Object shape, got {:?}", out.shape.root)
12053 };
12054 assert_eq!(pointers.len(), 2);
12055 assert!(matches!(&pointers[0], ShapeNode::Scalar { name, .. } if name == "test2"));
12056 assert!(matches!(&pointers[1], ShapeNode::Scalar { name, .. } if name == "test3"));
12057 }
12058
12059 #[test]
12060 fn test_with_bound_free_object_field_access() {
12061 let out = compile_and_emit("with\n test := { test2 := 1.0, test3 := 'str' }\nselect test.test2;");
12066 assert!(out.sql.contains("\"test2\" FROM \"test\""), "got:\n{}", out.sql);
12067 }
12068
12069 #[test]
12070 fn test_with_bound_free_object_nested_field_access_chain() {
12071 let out =
12076 compile_and_emit("with\n test := { test2 := 1.0, test3 := { foo := 'bar' } }\nselect test.test3.foo;");
12077 assert!(out.sql.contains("\"test3\" FROM \"test\""), "got:\n{}", out.sql);
12078 assert!(
12079 out.sql.contains("->'foo'"),
12080 "expected jsonb field extraction, got:\n{}",
12081 out.sql
12082 );
12083 }
12084
12085 #[test]
12086 fn test_with_bound_free_object_nested_field_access_wrong_field_errors() {
12087 let schema = make_schema();
12090 let ast = parse::parse("with\n test := { test2 := 1.0, test3 := { foo := 'bar' } }\nselect test.test3.nope;")
12091 .unwrap();
12092 assert!(ir::compile(&ast, &schema).is_err());
12093 }
12094
12095 #[test]
12096 fn test_nested_free_object_literal_in_computed_shape_element() {
12097 let out = compile_and_emit("select default::Person { id, test := { foo := 'bar' } };");
12103 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
12104 panic!("expected Object shape")
12105 };
12106 let test_node = pointers
12107 .iter()
12108 .find(|p| matches!(p, ShapeNode::NamedTuple { name, .. } if name == "test"))
12109 .unwrap_or_else(|| panic!("expected a NamedTuple shape node for 'test', got {:?}", pointers));
12110 assert!(matches!(
12111 test_node,
12112 ShapeNode::NamedTuple {
12113 is_free_object: true,
12114 ..
12115 }
12116 ));
12117 }
12118
12119 #[test]
12120 fn test_bare_free_cte_reference_in_computed_shape_collapses_to_empty() {
12121 let out = compile_and_emit(
12126 "with\n test := { test2 := 1.0, test3 := 'str' }\n\
12127 select default::Person { id, test := test };",
12128 );
12129 assert!(
12130 out.sql.contains("jsonb_build_object()"),
12131 "expected an empty free object, got:\n{}",
12132 out.sql
12133 );
12134 }
12135
12136 #[test]
12137 fn test_shaped_free_cte_reference_projects_fields() {
12138 let out = compile_and_emit(
12142 "with\n test := { test2 := 1.0, test3 := 'str' }\n\
12143 select default::Person { id, test := test { test2 } };",
12144 );
12145 assert!(out.sql.contains("jsonb_build_object('test2'"), "got:\n{}", out.sql);
12146 assert!(
12147 !out.sql.contains("'test3'"),
12148 "test3 should not be projected, got:\n{}",
12149 out.sql
12150 );
12151 }
12152
12153 #[test]
12154 fn test_insert_multilink_remove_rejected() {
12155 let schema = make_schema_with_through_and_prop();
12156 let ast =
12157 crate::parse::parse("INSERT Product { name := $name, tags -= (SELECT Tag FILTER .id = $tid) }").unwrap();
12158 match crate::ir::compile(&ast, &schema) {
12159 Ok(_) => panic!("expected a compile error for `-=` on a multi-link at insert time"),
12160 Err(e) => assert!(
12161 e.to_string().contains("nothing to remove"),
12162 "expected a 'nothing to remove yet' error, got: {e}"
12163 ),
12164 }
12165 }
12166
12167 #[test]
12168 fn test_multilink_junction_info_disambiguates_self_referencing_through_type() {
12169 let schema = make_schema_with_through();
12176 let out = compile_and_emit_with(
12177 "UPDATE Person FILTER .id = $id SET { friends += (SELECT Person FILTER .id = $fid) }",
12178 &schema,
12179 );
12180 assert!(
12181 out.sql.contains("(\"person\", \"friend\")"),
12182 "expected two distinct FK columns:\n{}",
12183 out.sql
12184 );
12185 assert!(
12186 !out.sql.contains("(\"person\", \"person\")"),
12187 "source/target collapsed to the same column:\n{}",
12188 out.sql
12189 );
12190 }
12191
12192 #[test]
12193 fn test_shape_descriptor_scalars() {
12194 let out = compile_and_emit("SELECT Person { name, age }");
12195 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
12196 panic!()
12197 };
12198 assert_eq!(pointers.len(), 4); assert!(matches!(&pointers[0], ShapeNode::Scalar { name, position: 0 } if name == "__type__"));
12200 assert!(matches!(&pointers[1], ShapeNode::Scalar { name, position: 1 } if name == "id"));
12201 assert!(matches!(&pointers[2], ShapeNode::Scalar { name, position: 2 } if name == "name"));
12202 assert!(matches!(&pointers[3], ShapeNode::Scalar { name, position: 3 } if name == "age"));
12203 }
12204
12205 #[test]
12206 fn test_shape_descriptor_multi_link() {
12207 let out = compile_and_emit("SELECT Person { name, posts { title } }");
12208 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
12209 panic!()
12210 };
12211 assert_eq!(pointers.len(), 4);
12213 let ShapeNode::Array {
12214 name,
12215 position,
12216 element,
12217 } = &pointers[3]
12218 else {
12219 panic!()
12220 };
12221 assert_eq!(name, "posts");
12222 assert_eq!(*position, 3);
12223 let ShapeNode::Object {
12224 pointers: elem_pointers,
12225 ..
12226 } = element.as_ref()
12227 else {
12228 panic!()
12229 };
12230 assert_eq!(elem_pointers.len(), 2);
12233 }
12234
12235 #[test]
12236 fn test_select_order_by_limit() {
12237 let out = compile_and_emit("SELECT Person { name } ORDER BY .name ASC LIMIT 10");
12238 assert!(out.sql.contains("ORDER BY"));
12239 assert!(out.sql.contains("LIMIT 10"));
12240 }
12241
12242 #[test]
12243 fn test_insert_returning() {
12244 let out = compile_and_emit("INSERT Person { name := 'Alice', age := 30 }");
12245 assert!(out.sql.contains("INSERT INTO \"public\".\"Person\""));
12246 assert!(out.sql.contains("RETURNING"));
12247 assert!(out.sql.contains("'default::Person'::text"));
12248 assert!(out.sql.contains(") AS result"));
12249 let ShapeNode::Object {
12251 cardinality, pointers, ..
12252 } = &out.shape.root
12253 else {
12254 panic!()
12255 };
12256 assert_eq!(*cardinality, Cardinality::Required);
12257 assert!(
12259 pointers
12260 .iter()
12261 .any(|f| matches!(f, ShapeNode::Scalar { name, .. } if name == "id"))
12262 );
12263 assert!(
12264 !pointers
12265 .iter()
12266 .any(|f| matches!(f, ShapeNode::Scalar { name, .. } if name == "name"))
12267 );
12268 }
12269
12270 #[test]
12271 fn test_update_returning() {
12272 let out = compile_and_emit("UPDATE Person FILTER .name = $name SET { age := 31 }");
12273 assert!(out.sql.contains("UPDATE \"public\".\"Person\""));
12274 assert!(out.sql.contains("SET"));
12275 assert!(out.sql.contains("RETURNING"));
12277 assert!(out.sql.contains("'default::Person'::text"));
12278 assert!(!out.sql.contains("\"name\"::text"), "bare UPDATE must not return name");
12279 }
12280
12281 #[test]
12282 fn test_update_set_tuple_param_cast_uses_direct_jsonb_cast_not_to_jsonb() {
12283 let out = compile_and_emit("UPDATE Person FILTER .id = $id SET { age := <tuple<x: float64>>$val }");
12290 assert!(
12291 out.sql.contains(")::jsonb"),
12292 "expected a direct ::jsonb cast, got:\n{}",
12293 out.sql
12294 );
12295 assert!(
12296 !out.sql.contains("to_jsonb($"),
12297 "must not pass a bare param straight into to_jsonb(): got:\n{}",
12298 out.sql
12299 );
12300 }
12301
12302 #[test]
12303 fn test_empty_set_cast_to_object_type_clears_optional_link() {
12304 let out = compile_and_emit("UPDATE Person FILTER .id = $id SET { company := <default::Company>{} }");
12313 assert!(out.sql.contains("\"company_id\" = NULL"), "got:\n{}", out.sql);
12314 }
12315
12316 #[test]
12317 fn test_an_aggregate_over_a_set_literal_inside_a_shape() {
12318 let out = compile_and_emit("UPDATE Person FILTER .id = $id SET { age := min({<int64>100, .age}) }");
12324 assert!(out.sql.contains("min(v)"), "got:\n{}", out.sql);
12325 assert!(
12326 out.sql.contains("UNION ALL"),
12327 "both operands must reach the aggregate:\n{}",
12328 out.sql
12329 );
12330 }
12331
12332 #[test]
12333 fn test_an_aggregate_over_a_set_literal_reads_the_subject() {
12334 let out = compile_and_emit("SELECT Person { capped := min({<int64>100, .age}) }");
12337 assert!(out.sql.contains("min(v)"), "got:\n{}", out.sql);
12338 assert!(
12339 out.sql.contains("\"age\""),
12340 "the relative path must reach the column:\n{}",
12341 out.sql
12342 );
12343 }
12344
12345 #[test]
12346 fn test_an_aggregate_over_a_union_inside_a_shape() {
12347 let out = compile_and_emit("SELECT Person { capped := min((<int64>100 union .age)) }");
12350 assert!(out.sql.contains("min(v)"), "got:\n{}", out.sql);
12351 assert!(
12352 out.sql.contains("UNION ALL"),
12353 "both arms must reach the aggregate:\n{}",
12354 out.sql
12355 );
12356 }
12357
12358 #[test]
12359 fn test_an_aggregate_over_a_union_of_three_arms_flattens() {
12360 let out = compile_and_emit("SELECT Person { capped := min((<int64>1 union <int64>2 union .age)) }");
12361 assert_eq!(
12362 out.sql.matches("UNION ALL").count(),
12363 2,
12364 "three arms means two joins:\n{}",
12365 out.sql
12366 );
12367 }
12368
12369 #[test]
12370 fn test_an_aggregate_over_an_empty_set_emits_no_row_source() {
12371 for query in ["SELECT Person { capped := min({}) }", "SELECT min({})"] {
12375 let ast = parse::parse(query).expect("parse failed");
12376 let sql = ir::compile(&ast, &make_schema())
12377 .map(|ir| emit(&ir).sql)
12378 .unwrap_or_default();
12379 assert!(
12380 !sql.contains("FROM () AS"),
12381 "{query} emitted an empty row source:\n{sql}"
12382 );
12383 }
12384 }
12385
12386 #[test]
12387 fn test_delete_returning() {
12388 let out = compile_and_emit("DELETE Person FILTER .id = $id");
12389 assert!(out.sql.contains("DELETE FROM \"public\".\"Person\""));
12390 assert!(out.sql.contains("RETURNING"));
12392 assert!(out.sql.contains("'default::Person'::text"));
12393 assert!(!out.sql.contains("\"name\"::text"), "bare DELETE must not return name");
12394 }
12395
12396 #[test]
12397 fn test_select_over_insert() {
12398 let out = compile_and_emit("SELECT (INSERT Person { name := $name, age := $age }) { id, name }");
12399 assert!(out.sql.contains("WITH\n\"_dml\" AS ("));
12401 assert!(out.sql.contains("INSERT INTO"));
12402 assert!(out.sql.contains("RETURNING *"));
12403 assert!(out.sql.contains("'default::Person'::text"));
12405 assert!(out.sql.contains("\"name\"::text"));
12406 }
12407
12408 #[test]
12409 fn test_select_over_update() {
12410 let out = compile_and_emit("SELECT (UPDATE Person FILTER .id = $id SET { name := $name }) { id, name }");
12411 assert!(out.sql.contains("WITH\n\"_dml\" AS ("));
12412 assert!(out.sql.contains("UPDATE"));
12413 assert!(out.sql.contains("RETURNING \"t1\".*"), "{}", out.sql);
12414 assert!(out.sql.contains("\"name\"::text"));
12415 }
12416
12417 #[test]
12418 fn test_insert_user_specified_id_denied_by_default() {
12419 let schema = make_schema();
12420 let ast = parse::parse("INSERT Person { id := <uuid>$id, name := $name, age := $age }").unwrap();
12421 match ir::compile_with_config(&ast, &schema, &ir::SessionConfig::default()) {
12422 Err(err) => assert!(err.to_string().contains("cannot assign to property 'id'"), "got: {err}"),
12423 Ok(_) => panic!("expected id assignment to be denied by default"),
12424 }
12425 }
12426
12427 #[test]
12428 fn test_insert_user_specified_id_allowed_when_configured() {
12429 let schema = make_schema();
12430 let ast = parse::parse("INSERT Person { id := <uuid>$id, name := $name, age := $age }").unwrap();
12431 let config = ir::SessionConfig {
12432 allow_user_specified_id: true,
12433 };
12434 let ir_out = ir::compile_with_config(&ast, &schema, &config)
12435 .expect("expected id assignment to be allowed with allow_user_specified_id");
12436 let out = emit(&ir_out);
12437 assert!(out.sql.contains("INSERT INTO"));
12438 }
12439
12440 #[test]
12441 fn test_update_user_specified_id_denied_even_when_configured() {
12442 let schema = make_schema();
12443 let ast = parse::parse("UPDATE Person FILTER .name = $name SET { id := <uuid>$id }").unwrap();
12444 let config = ir::SessionConfig {
12445 allow_user_specified_id: true,
12446 };
12447 match ir::compile_with_config(&ast, &schema, &config) {
12448 Err(err) => assert!(err.to_string().contains("cannot assign to property 'id'"), "got: {err}"),
12449 Ok(_) => panic!("expected UPDATE to always deny reassigning id"),
12450 }
12451 }
12452
12453 #[test]
12454 fn test_select_over_update_multilink_only() {
12455 let out = compile_and_emit(
12465 "SELECT (UPDATE Person FILTER .id = $id SET { posts += (SELECT Post FILTER .title = $title) }) { id, name }",
12466 );
12467 assert!(
12468 out.sql.contains("\"_dml__ml_add_0\""),
12469 "missing junction-append CTE:\n{}",
12470 out.sql
12471 );
12472 assert!(out.sql.contains("INSERT INTO"), "missing junction INSERT:\n{}", out.sql);
12473 assert!(
12476 out.sql.contains("\"_dml__ids\" AS (\nSELECT"),
12477 "expected SELECT-based _ids CTE:\n{}",
12478 out.sql
12479 );
12480 assert!(
12481 !out.sql.contains("SET\n\nWHERE") && !out.sql.contains("SET \nWHERE"),
12482 "empty SET clause regression:\n{}",
12483 out.sql
12484 );
12485 assert_eq!(
12488 out.sql.matches("WITH\n").count(),
12489 1,
12490 "junction CTE must not be nested in a second WITH:\n{}",
12491 out.sql
12492 );
12493 assert!(
12495 out.sql.contains("\"_dml\" AS (\n SELECT * FROM \"_dml__ids\"\n)"),
12496 "missing _dml passthrough:\n{}",
12497 out.sql
12498 );
12499 }
12500
12501 #[test]
12502 fn test_select_over_update_scalar_and_multilink() {
12503 let out = compile_and_emit(
12507 "SELECT (UPDATE Person FILTER .id = $id SET { name := $name, posts += (SELECT Post FILTER .title = $title) }) { id, name }",
12508 );
12509 assert!(
12510 out.sql.contains("\"_dml__ml_add_0\""),
12511 "missing junction-append CTE:\n{}",
12512 out.sql
12513 );
12514 assert!(
12515 out.sql.contains("\"_dml__ids\" AS (\nUPDATE"),
12516 "expected UPDATE-based _ids CTE:\n{}",
12517 out.sql
12518 );
12519 assert!(
12520 out.sql.contains("\"name\" = "),
12521 "missing scalar SET assignment:\n{}",
12522 out.sql
12523 );
12524 assert_eq!(
12525 out.sql.matches("WITH\n").count(),
12526 1,
12527 "junction CTE must not be nested in a second WITH:\n{}",
12528 out.sql
12529 );
12530 }
12531
12532 #[test]
12533 fn test_with_bound_insert_and_multilink_update_forward_ref() {
12534 let out = compile_and_emit(
12542 "with insert0 := (insert Post { title := $title }), update0 := (update Person filter .id = $id set { posts += (select insert0) }) select { insert0, update0 }",
12543 );
12544 assert!(
12545 out.sql.contains("\"insert0\" AS (\n INSERT INTO"),
12546 "missing insert0 CTE:\n{}",
12547 out.sql
12548 );
12549 assert!(
12550 out.sql.contains("\"update0__ml_add_0\""),
12551 "missing junction-append CTE for update0:\n{}",
12552 out.sql
12553 );
12554 assert!(
12555 out.sql.contains("\"update0__ids\" AS (\nSELECT"),
12556 "expected SELECT-based update0 ids CTE (no scalar changes):\n{}",
12557 out.sql
12558 );
12559 assert!(
12560 out.sql
12561 .contains("\"update0\" AS (\n SELECT * FROM \"update0__ids\"\n)"),
12562 "missing update0 passthrough:\n{}",
12563 out.sql
12564 );
12565 assert_eq!(
12568 out.sql.matches("WITH\n").count(),
12569 1,
12570 "must be a single flat top-level WITH block:\n{}",
12571 out.sql
12572 );
12573 }
12574
12575 #[test]
12576 fn test_select_over_delete() {
12577 let out = compile_and_emit("SELECT (DELETE Person FILTER .id = $id) { id, name }");
12578 assert!(out.sql.contains("WITH\n\"_dml\" AS ("));
12579 assert!(out.sql.contains("DELETE FROM"));
12580 assert!(out.sql.contains("RETURNING *"));
12581 assert!(out.sql.contains("\"name\"::text"));
12582 }
12583
12584 fn make_schema_with_rewrite() -> SchemaDescriptor {
12585 use crate::schema::RewriteEntry;
12586 let mut schema = make_schema();
12587 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
12589 person.properties.push(PropertyDescriptor {
12590 name: "slug".into(),
12591 pg_type: "text".into(),
12592 nullable: true,
12593 default_sql: None,
12594 default_pyql: None,
12595 description: None,
12596 check_constraints: vec![],
12597 is_exclusive: false,
12598 is_pk: false,
12599 is_readonly: false,
12600 rewrites: vec![
12601 RewriteEntry {
12602 on: 1,
12603 handler: "str_lower(.name)".into(),
12604 }, RewriteEntry {
12606 on: 2,
12607 handler: "str_lower(.name)".into(),
12608 }, ],
12610 tuple_members: None,
12611 column_type: None,
12612 });
12613 schema
12614 }
12615
12616 #[test]
12617 fn a_link_assigned_from_a_select_keeps_its_clauses() {
12618 let out = compile_and_emit(
12619 "INSERT Person { name := 'a', age := 1, company := (SELECT Company ORDER BY .name LIMIT 1) }",
12620 );
12621 assert!(
12622 out.sql.contains("ORDER BY") && out.sql.contains("LIMIT 1"),
12623 "{}",
12624 out.sql
12625 );
12626 }
12627
12628 #[test]
12629 fn a_rewrite_leaves_the_statement_alone() {
12630 let schema = make_schema_with_rewrite();
12632 for query in [
12633 "INSERT Person { name := $name, age := 30 }",
12634 "UPDATE Person FILTER .id = $id SET { name := $name }",
12635 ] {
12636 let out = compile_and_emit_with(query, &schema);
12637 assert!(!out.sql.contains("\"slug\""), "{}", out.sql);
12638 }
12639 }
12640
12641 #[test]
12642 fn a_rewrite_reads_the_row_being_written() {
12643 let schema = make_schema_with_rewrite();
12644 let assignments = crate::ir::compile_rewrite_assignments("default::Person", 1, &schema).unwrap();
12645 let [assignment] = assignments.as_slice() else {
12646 panic!("one rewrite on insert")
12647 };
12648 assert_eq!(assignment.column, "slug");
12649 assert!(assignment.sql.contains("NEW.\"name\""), "{}", assignment.sql);
12650 }
12651
12652 #[test]
12653 fn rewrites_become_before_triggers() {
12654 let ddl = crate::export::export_schema(&make_schema_with_rewrite()).unwrap();
12655 for event in ["BEFORE INSERT", "BEFORE UPDATE"] {
12656 assert!(
12657 ddl.contains(&format!("{event} ON \"public\".\"Person\"")),
12658 "{event}:\n{ddl}"
12659 );
12660 }
12661 assert!(ddl.contains("NEW.\"slug\" := _pylon_rewrites.\"v0\";"), "{ddl}");
12662 }
12663
12664 #[test]
12665 fn a_rewrite_walking_a_link_starts_from_the_new_row() {
12666 use crate::schema::RewriteEntry;
12668 let mut schema = make_schema_with_rewrite();
12669 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
12670 let slug = person.properties.iter_mut().find(|p| p.name == "slug").unwrap();
12671 slug.rewrites = vec![RewriteEntry {
12672 on: 1,
12673 handler: "<str>(.company is Company) ++ .company.name".into(),
12674 }];
12675 let assignments = crate::ir::compile_rewrite_assignments("default::Person", 1, &schema).unwrap();
12676 let sql = &assignments[0].sql;
12677 assert!(!sql.contains("\"public\".\"Person\""), "{sql}");
12678 assert!(
12679 sql.contains("(SELECT (NEW).*)") && sql.contains("NEW.\"company_id\""),
12680 "{sql}"
12681 );
12682 }
12683
12684 #[test]
12685 fn test_unless_conflict_do_nothing() {
12686 let out = compile_and_emit("INSERT Person { name := $name } UNLESS CONFLICT");
12687 assert!(out.sql.contains("ON CONFLICT DO NOTHING"));
12688 }
12689
12690 #[test]
12691 fn test_unless_conflict_on_do_nothing() {
12692 let out = compile_and_emit("INSERT Person { name := $name } UNLESS CONFLICT ON .name");
12693 assert!(out.sql.contains("ON CONFLICT (\"name\") DO NOTHING"));
12694 }
12695
12696 #[test]
12697 fn test_unless_conflict_do_update() {
12698 let out = compile_and_emit(
12699 "INSERT Person { name := $name, age := $age } \
12700 UNLESS CONFLICT ON .name \
12701 ELSE (UPDATE Person SET { age := $age })",
12702 );
12703 assert!(out.sql.contains("ON CONFLICT (\"name\") DO UPDATE SET"));
12704 assert!(out.sql.contains("\"age\" = $2"));
12705 assert!(!out.sql.contains("DO NOTHING"));
12707 }
12708
12709 #[test]
12710 fn test_unless_conflict_else_update_self_reference_is_qualified() {
12711 let out = compile_and_emit(
12719 "INSERT Person { name := $name, age := 0 } \
12720 UNLESS CONFLICT ON .name \
12721 ELSE (UPDATE Person SET { age := .age + 1 })",
12722 );
12723 assert!(
12724 out.sql.contains("\"Person\".\"age\""),
12725 "self-reference must be qualified with the table's own name, got:\n{}",
12726 out.sql
12727 );
12728 assert!(
12729 !out.sql.contains("SET \"age\" = (\"age\""),
12730 "must not emit an unqualified (ambiguous) self-reference, got:\n{}",
12731 out.sql
12732 );
12733 }
12734
12735 #[test]
12736 fn test_unless_conflict_do_update_no_on() {
12737 let out = compile_and_emit(
12738 "INSERT Person { name := $name } \
12739 UNLESS CONFLICT \
12740 ELSE (UPDATE Person SET { age := 0 })",
12741 );
12742 assert!(out.sql.contains("ON CONFLICT DO UPDATE SET"));
12743 assert!(out.sql.contains("\"age\" = 0"));
12744 }
12745
12746 #[test]
12747 fn test_select_over_select() {
12748 let out = compile_and_emit("SELECT (SELECT Person FILTER .age > 18) { name }");
12749 assert!(out.sql.contains("WITH\n\"_dml\" AS ("));
12751 assert!(out.sql.contains("SELECT *"));
12753 assert!(out.sql.contains("FROM \"public\".\"Person\""));
12754 assert!(out.sql.contains("WHERE"));
12756 assert!(out.sql.contains("'default::Person'::text"));
12758 assert!(out.sql.contains("\"name\"::text"));
12759 }
12760
12761 #[test]
12762 fn test_select_over_select_with_outer_filter() {
12763 let out = compile_and_emit("SELECT (SELECT Person FILTER .age > 18) { name } FILTER .name = $name");
12764 assert!(out.sql.contains("WITH\n\"_dml\" AS ("));
12765 assert!(out.sql.contains("SELECT *"));
12766 assert_eq!(out.sql.matches("WHERE").count(), 2);
12768 assert!(out.sql.contains("$1"));
12769 }
12770
12771 #[test]
12772 fn test_insert_link_subquery() {
12773 let out = compile_and_emit("INSERT Person { name := $name, company := (SELECT Company FILTER .name = $co) }");
12774 assert!(out.sql.contains("\"company_id\""));
12776 assert!(out.sql.contains("SELECT"));
12777 assert!(out.sql.contains("\"id\""));
12779 assert!(out.sql.contains("FROM \"public\".\"Company\""));
12780 assert!(out.sql.contains("$2")); }
12783
12784 #[test]
12785 fn test_update_link_subquery() {
12786 let out =
12787 compile_and_emit("UPDATE Person FILTER .id = $id SET { company := (SELECT Company FILTER .name = $co) }");
12788 assert!(out.sql.contains("\"company_id\""));
12789 assert!(out.sql.contains("SELECT"));
12790 assert!(out.sql.contains("FROM \"public\".\"Company\""));
12791 }
12792
12793 #[test]
12794 fn test_insert_link_value_from_nested_insert_hoists_a_with_cte() {
12795 let out = compile_and_emit(
12803 "INSERT Person { name := 'Alice', company := (select (insert Company { name := 'Acme' }) { id }) }",
12804 );
12805 assert!(
12806 out.sql.starts_with("WITH"),
12807 "expected a WITH-hoisted CTE, got:\n{}",
12808 out.sql
12809 );
12810 assert!(
12811 out.sql.contains("INSERT INTO \"public\".\"Company\""),
12812 "expected the nested insert to be its own CTE, got:\n{}",
12813 out.sql,
12814 );
12815 assert!(
12818 out.sql.contains("\"company_id\") SELECT") && out.sql.contains(".\"id\" FROM"),
12819 "expected the outer insert to switch from VALUES to SELECT ... FROM <cte>, got:\n{}",
12820 out.sql,
12821 );
12822 assert!(
12823 !out.sql.contains("FROM \"public\".\"Company\" AS"),
12824 "must not select from the real Company table, got:\n{}",
12825 out.sql
12826 );
12827 }
12828
12829 #[test]
12830 fn test_update_link_value_from_nested_insert_hoists_a_with_cte_and_adds_from_clause() {
12831 let out = compile_and_emit(
12832 "UPDATE Person FILTER .id = $id SET { company := (select (insert Company { name := 'Acme' }) { id }) }",
12833 );
12834 assert!(
12835 out.sql.starts_with("WITH"),
12836 "expected a WITH-hoisted CTE, got:\n{}",
12837 out.sql
12838 );
12839 assert!(
12840 out.sql.contains("INSERT INTO \"public\".\"Company\""),
12841 "expected the nested insert to be its own CTE, got:\n{}",
12842 out.sql,
12843 );
12844 assert!(
12847 out.sql.contains("\nFROM \""),
12848 "expected a FROM clause referencing the nested CTE, got:\n{}",
12849 out.sql
12850 );
12851 assert!(out.sql.contains("SET \"company_id\" = "), "got:\n{}", out.sql);
12852 }
12853
12854 #[test]
12855 fn test_update_link_value_from_nested_insert_combines_with_multilink_mutation() {
12856 let out = compile_and_emit(
12861 "UPDATE Person FILTER .id = $id SET { \
12862 company := (select (insert Company { name := 'Acme' }) { id }), \
12863 posts += (SELECT Post FILTER .title = $t) \
12864 }",
12865 );
12866 assert!(out.sql.starts_with("WITH"), "got:\n{}", out.sql);
12867 assert!(
12868 out.sql.contains("INSERT INTO \"public\".\"Company\""),
12869 "expected the nested insert to be its own CTE, got:\n{}",
12870 out.sql,
12871 );
12872 assert!(
12873 out.sql.contains("\"_ids\" AS (\nUPDATE") && out.sql.contains("\nFROM \""),
12874 "expected the _ids UPDATE to gain a FROM clause referencing the nested CTE, got:\n{}",
12875 out.sql,
12876 );
12877 assert!(
12878 out.sql.contains("\"_ml_add_0\""),
12879 "expected the junction-append CTE to still be present, got:\n{}",
12880 out.sql
12881 );
12882 }
12883
12884 #[test]
12885 fn test_update_link_value_from_nested_insert_combines_with_interface_target() {
12886 fn id_prop() -> PropertyDescriptor {
12892 PropertyDescriptor {
12893 name: "id".into(),
12894 pg_type: "uuid".into(),
12895 nullable: false,
12896 default_sql: Some("uuidv7()".into()),
12897 default_pyql: None,
12898 description: None,
12899 check_constraints: vec![],
12900 is_exclusive: true,
12901 is_pk: true,
12902 is_readonly: true,
12903 rewrites: vec![],
12904 tuple_members: None,
12905 column_type: None,
12906 }
12907 }
12908 fn text_prop(name: &str) -> PropertyDescriptor {
12909 PropertyDescriptor {
12910 name: name.into(),
12911 pg_type: "text".into(),
12912 nullable: false,
12913 default_sql: None,
12914 default_pyql: None,
12915 description: None,
12916 check_constraints: vec![],
12917 is_exclusive: false,
12918 is_pk: false,
12919 is_readonly: false,
12920 rewrites: vec![],
12921 tuple_members: None,
12922 column_type: None,
12923 }
12924 }
12925 fn company_link() -> LinkDescriptor {
12926 LinkDescriptor {
12927 name: "company".into(),
12928 target: "default::Company".into(),
12929 nullable: true,
12930 through: None,
12931 description: None,
12932 default_pyql: None,
12933 is_exclusive: false,
12934 is_readonly: false,
12935 rewrites: vec![],
12936 on_delete: vec![],
12937 }
12938 }
12939 let schema = SchemaDescriptor {
12940 types: vec![
12941 TypeDescriptor {
12942 name: "Company".into(),
12943 module: "default".into(),
12944 table: "Company".into(),
12945 abstract_: false,
12946 materialized: true,
12947 description: None,
12948 parents: vec![],
12949 interfaces: vec![],
12950 bases: vec![],
12951 properties: vec![id_prop(), text_prop("name")],
12952 links: vec![],
12953 multilinks: vec![],
12954 computed: vec![],
12955 constraints: vec![],
12956 indexes: vec![],
12957 partition: None,
12958 vector_indexes: vec![],
12959 search_indexes: vec![],
12960 triggers: vec![],
12961 junction: false,
12962 signals: vec![],
12963 },
12964 TypeDescriptor {
12965 name: "Account".into(),
12966 module: "default".into(),
12967 table: "Account".into(),
12968 abstract_: true,
12969 materialized: true,
12970 description: None,
12971 parents: vec![],
12972 interfaces: vec![],
12973 bases: vec![],
12974 properties: vec![id_prop(), text_prop("email")],
12975 links: vec![company_link()],
12976 multilinks: vec![],
12977 computed: vec![],
12978 constraints: vec![],
12979 indexes: vec![],
12980 partition: None,
12981 vector_indexes: vec![],
12982 search_indexes: vec![],
12983 triggers: vec![],
12984 junction: false,
12985 signals: vec![],
12986 },
12987 TypeDescriptor {
12988 name: "Individual".into(),
12989 module: "default".into(),
12990 table: "Individual".into(),
12991 abstract_: false,
12992 materialized: true,
12993 description: None,
12994 parents: vec![],
12995 interfaces: vec!["default::Account".into()],
12996 bases: vec![],
12997 properties: vec![id_prop(), text_prop("email"), text_prop("first_name")],
12998 links: vec![company_link()],
12999 multilinks: vec![],
13000 computed: vec![],
13001 constraints: vec![],
13002 indexes: vec![],
13003 partition: None,
13004 vector_indexes: vec![],
13005 search_indexes: vec![],
13006 triggers: vec![],
13007 junction: false,
13008 signals: vec![],
13009 },
13010 ],
13011 scalars: vec![],
13012 enums: vec![],
13013 named_tuples: vec![],
13014 globals: vec![],
13015 functions: vec![],
13016 aliases: vec![],
13017 channels: vec![],
13018 ..Default::default()
13019 };
13020 let out = compile_and_emit_with(
13021 "UPDATE Account FILTER .email = $email \
13022 SET { company := (select (insert Company { name := 'Acme' }) { id }) }",
13023 &schema,
13024 );
13025 assert!(out.sql.starts_with("WITH"), "got:\n{}", out.sql);
13026 assert!(
13027 out.sql.contains("INSERT INTO \"public\".\"Company\""),
13028 "expected the nested insert to be its own CTE, got:\n{}",
13029 out.sql,
13030 );
13031 assert!(
13032 out.sql.contains("UPDATE \"public\".\"Individual\"") && out.sql.contains("\nFROM \"_nested_dml_0\""),
13033 "expected the per-implementor UPDATE to gain a FROM clause referencing the nested CTE, got:\n{}",
13034 out.sql,
13035 );
13036 }
13037
13038 #[test]
13039 fn test_a_backlink_narrowed_to_a_supertype() {
13040 let mut schema = interface_link_schema();
13044 schema
13047 .types
13048 .iter_mut()
13049 .find(|t| t.name == "Account")
13050 .expect("the helper declares Account")
13051 .links
13052 .clear();
13053 let out = compile_and_emit_with("SELECT Company { owners := .<company[is Account] { email } }", &schema);
13054 assert!(
13055 out.sql.contains("\"public\".\"Individual\""),
13056 "the backlink must read the type that declares it:\n{}",
13057 out.sql
13058 );
13059 }
13060
13061 #[test]
13062 fn a_path_through_a_backlink_narrowed_to_a_supertype() {
13063 let mut schema = interface_link_schema();
13064 schema
13065 .types
13066 .iter_mut()
13067 .find(|t| t.name == "Account")
13068 .expect("the helper declares Account")
13069 .links
13070 .clear();
13071 let out = compile_and_emit_with(
13072 "SELECT Company { name } FILTER EXISTS (SELECT .<company[is Account])",
13073 &schema,
13074 );
13075 assert!(out.sql.contains("\"public\".\"Individual\""), "{}", out.sql);
13076 }
13077
13078 fn interface_link_schema() -> SchemaDescriptor {
13082 fn id_prop() -> PropertyDescriptor {
13088 PropertyDescriptor {
13089 name: "id".into(),
13090 pg_type: "uuid".into(),
13091 nullable: false,
13092 default_sql: None,
13093 default_pyql: None,
13094 description: None,
13095 check_constraints: vec![],
13096 is_exclusive: true,
13097 is_pk: true,
13098 is_readonly: true,
13099 rewrites: vec![],
13100 tuple_members: None,
13101 column_type: None,
13102 }
13103 }
13104 fn text_prop(name: &str) -> PropertyDescriptor {
13105 PropertyDescriptor {
13106 name: name.into(),
13107 pg_type: "text".into(),
13108 nullable: false,
13109 default_sql: None,
13110 default_pyql: None,
13111 description: None,
13112 check_constraints: vec![],
13113 is_exclusive: false,
13114 is_pk: false,
13115 is_readonly: false,
13116 rewrites: vec![],
13117 tuple_members: None,
13118 column_type: None,
13119 }
13120 }
13121 fn link(name: &str, target: &str) -> LinkDescriptor {
13122 LinkDescriptor {
13123 name: name.into(),
13124 target: target.into(),
13125 nullable: true,
13126 through: None,
13127 description: None,
13128 default_pyql: None,
13129 is_exclusive: false,
13130 is_readonly: false,
13131 rewrites: vec![],
13132 on_delete: vec![],
13133 }
13134 }
13135 fn ty(
13136 name: &str,
13137 abstract_: bool,
13138 interfaces: Vec<String>,
13139 properties: Vec<PropertyDescriptor>,
13140 links: Vec<LinkDescriptor>,
13141 ) -> TypeDescriptor {
13142 TypeDescriptor {
13143 name: name.into(),
13144 module: "default".into(),
13145 table: name.into(),
13146 abstract_,
13147 materialized: true,
13148 description: None,
13149 parents: vec![],
13150 interfaces,
13151 bases: vec![],
13152 properties,
13153 links,
13154 multilinks: vec![],
13155 computed: vec![],
13156 constraints: vec![],
13157 indexes: vec![],
13158 partition: None,
13159 vector_indexes: vec![],
13160 search_indexes: vec![],
13161 triggers: vec![],
13162 junction: false,
13163 signals: vec![],
13164 }
13165 }
13166 SchemaDescriptor {
13167 types: vec![
13168 ty("Company", false, vec![], vec![id_prop(), text_prop("name")], vec![]),
13169 ty(
13170 "Account",
13171 true,
13172 vec![],
13173 vec![id_prop(), text_prop("email")],
13174 vec![link("company", "default::Company")],
13175 ),
13176 ty(
13177 "Individual",
13178 false,
13179 vec!["default::Account".into()],
13180 vec![id_prop(), text_prop("email")],
13181 vec![link("company", "default::Company")],
13182 ),
13183 ty(
13184 "Token",
13185 false,
13186 vec![],
13187 vec![id_prop(), text_prop("value")],
13188 vec![link("account", "default::Account")],
13189 ),
13190 ],
13191 scalars: vec![],
13192 enums: vec![],
13193 named_tuples: vec![],
13194 globals: vec![],
13195 functions: vec![],
13196 aliases: vec![],
13197 channels: vec![],
13198 ..Default::default()
13199 }
13200 }
13201
13202 fn two_hop_interface_schema() -> SchemaDescriptor {
13206 let mut schema = interface_link_schema();
13207 let token = schema
13208 .types
13209 .iter()
13210 .find(|t| t.name == "Token")
13211 .expect("the helper declares Token")
13212 .clone();
13213 let mut connector = token.clone();
13214 connector.name = "Connector".into();
13215 connector.table = "Connector".into();
13216 connector.links[0].name = "provider".into();
13217 let mut installation = token;
13218 installation.name = "Installation".into();
13219 installation.table = "Installation".into();
13220 installation.links[0].name = "connector".into();
13221 installation.links[0].target = "default::Connector".into();
13222 schema.types.push(connector);
13223 schema.types.push(installation);
13224 schema
13225 }
13226
13227 #[test]
13228 fn a_narrowed_two_hop_walk_off_a_pinned_row_is_one_value() {
13229 let out = compile_and_emit_with(
13235 "SELECT { s := ((SELECT Installation FILTER .id = <uuid>$0).connector.provider[is Individual].email) ?? 'y' }",
13236 &two_hop_interface_schema(),
13237 );
13238 assert!(
13239 !out.sql.contains("ARRAY(SELECT"),
13240 "expected a value, not a set:\n{}",
13241 out.sql
13242 );
13243 assert!(
13244 out.sql.contains("COALESCE("),
13245 "the default must stay a scalar one:\n{}",
13246 out.sql
13247 );
13248 }
13249
13250 #[test]
13251 fn a_narrowed_two_hop_walk_off_an_unpinned_row_stays_a_set() {
13252 let out = compile_and_emit_with(
13253 "SELECT { s := (SELECT Installation FILTER .value = 'x').connector.provider[is Individual].email }",
13254 &two_hop_interface_schema(),
13255 );
13256 assert!(
13257 out.sql.contains("ARRAY(SELECT"),
13258 "a walk off many rows is a set:\n{}",
13259 out.sql
13260 );
13261 }
13262
13263 #[test]
13270 fn a_walk_ending_in_a_type_intersection_is_valid_in_a_filter() {
13271 let out = compile_and_emit_with(
13272 "SELECT Token { id } FILTER EXISTS .account[is Individual]",
13273 &interface_link_schema(),
13274 );
13275 assert!(
13276 out.sql.contains("\"public\".\"Individual\""),
13277 "the narrowing must reach the implementor's table:\n{}",
13278 out.sql
13279 );
13280 assert!(
13281 out.sql.contains("IS NOT NULL"),
13282 "exists over the narrowed walk is a nullness test:\n{}",
13283 out.sql
13284 );
13285 }
13286
13287 #[test]
13288 fn a_walk_ending_in_a_type_intersection_is_valid_off_a_binding() {
13289 let out = compile_and_emit_with(
13290 "WITH t := (SELECT Token LIMIT 1) SELECT { e := EXISTS t.account[is Individual] }",
13291 &interface_link_schema(),
13292 );
13293 assert!(
13294 out.sql.contains("\"public\".\"Individual\""),
13295 "the narrowing must reach the implementor's table:\n{}",
13296 out.sql
13297 );
13298 }
13299
13300 #[test]
13301 fn test_reading_a_link_through_an_interface_target() {
13302 let schema = interface_link_schema();
13303 let out = compile_and_emit_with("SELECT Token { account: { email, company: { name } } }", &schema);
13304 assert!(
13305 out.sql
13306 .contains("\"email\", \"company_id\" FROM \"public\".\"Individual\""),
13307 "the fanned-out interface must carry its own link columns:\n{}",
13308 out.sql
13309 );
13310 }
13311
13312 #[test]
13313 fn test_computed_pointer_in_shape_emits_expression() {
13314 let mut schema = make_schema();
13315 schema.types[0].computed.push(crate::schema::ComputedDescriptor {
13316 name: "upper_name".into(),
13317 expression: "str_upper(.name)".into(),
13318 return_type: Some("text".into()),
13319 link_target: None,
13320 link_multi: false,
13321 });
13322 let out = compile_and_emit_with("SELECT Person { upper_name }", &schema);
13323 assert!(
13324 out.sql.to_lowercase().contains("upper"),
13325 "expected upper() in SQL, got:\n{}",
13326 out.sql
13327 );
13328 }
13329
13330 fn make_interface_schema() -> SchemaDescriptor {
13333 fn id_prop() -> PropertyDescriptor {
13334 PropertyDescriptor {
13335 name: "id".into(),
13336 pg_type: "uuid".into(),
13337 nullable: false,
13338 default_sql: Some("uuidv7()".into()),
13339 default_pyql: None,
13340 description: None,
13341 check_constraints: vec![],
13342 is_exclusive: true,
13343 is_pk: true,
13344 is_readonly: true,
13345 rewrites: vec![],
13346 tuple_members: None,
13347 column_type: None,
13348 }
13349 }
13350 SchemaDescriptor {
13351 types: vec![
13352 TypeDescriptor {
13353 name: "Account".into(),
13354 module: "default".into(),
13355 table: "Account".into(),
13356 abstract_: true,
13357 materialized: false,
13358 description: None,
13359 parents: vec![],
13360 interfaces: vec![],
13361 bases: vec![],
13362 properties: vec![
13363 id_prop(),
13364 PropertyDescriptor {
13365 name: "email".into(),
13366 pg_type: "text".into(),
13367 nullable: false,
13368 default_sql: None,
13369 default_pyql: None,
13370 description: None,
13371 check_constraints: vec![],
13372 is_exclusive: false,
13373 is_pk: false,
13374 is_readonly: false,
13375 rewrites: vec![],
13376 tuple_members: None,
13377 column_type: None,
13378 },
13379 ],
13380 links: vec![],
13381 multilinks: vec![],
13382 computed: vec![],
13383 constraints: vec![],
13384 indexes: vec![],
13385 partition: None,
13386 vector_indexes: vec![],
13387 search_indexes: vec![],
13388 triggers: vec![],
13389 junction: false,
13390 signals: vec![],
13391 },
13392 TypeDescriptor {
13393 name: "Individual".into(),
13394 module: "default".into(),
13395 table: "Individual".into(),
13396 abstract_: false,
13397 materialized: true,
13398 description: None,
13399 parents: vec![],
13400 interfaces: vec!["default::Account".into()],
13401 bases: vec![],
13402 properties: vec![
13403 id_prop(),
13404 PropertyDescriptor {
13405 name: "first_name".into(),
13406 pg_type: "text".into(),
13407 nullable: false,
13408 default_sql: None,
13409 default_pyql: None,
13410 description: None,
13411 check_constraints: vec![],
13412 is_exclusive: false,
13413 is_pk: false,
13414 is_readonly: false,
13415 rewrites: vec![],
13416 tuple_members: None,
13417 column_type: None,
13418 },
13419 ],
13420 links: vec![],
13421 multilinks: vec![],
13422 computed: vec![crate::schema::ComputedDescriptor {
13423 name: "full_name".into(),
13424 expression: "str_upper(.first_name)".into(),
13425 return_type: Some("text".into()),
13426 link_target: None,
13427 link_multi: false,
13428 }],
13429 constraints: vec![],
13430 indexes: vec![],
13431 partition: None,
13432 vector_indexes: vec![],
13433 search_indexes: vec![],
13434 triggers: vec![],
13435 junction: false,
13436 signals: vec![],
13437 },
13438 ],
13439 scalars: vec![],
13440 enums: vec![],
13441 named_tuples: vec![],
13442 globals: vec![],
13443 functions: vec![],
13444 aliases: vec![],
13445 channels: vec![],
13446 ..Default::default()
13447 }
13448 }
13449
13450 #[test]
13451 fn test_type_intersection_splat_includes_concrete_computed_pointers() {
13452 let schema = make_interface_schema();
13457 let out = compile_and_emit_with("SELECT Account { *, [is Individual].* }", &schema);
13458 assert!(
13459 out.sql.to_lowercase().contains("upper"),
13460 "expected the concrete type's computed pointer (str_upper(...)) in the shape, got:\n{}",
13461 out.sql
13462 );
13463 assert!(
13464 out.sql.contains("\"first_name\""),
13465 "expected the concrete type's stored property too, got:\n{}",
13466 out.sql
13467 );
13468 }
13469
13470 #[test]
13471 fn test_enum_array_property_casts_to_text_array_and_shapes_as_a_list() {
13472 let mut schema = make_schema();
13479 schema.enums.push(crate::schema::EnumDescriptor {
13480 name: "Gender".into(),
13481 module: "default".into(),
13482 members: vec!["Male".into(), "Female".into()],
13483 });
13484 schema.types[0].properties.push(crate::schema::PropertyDescriptor {
13485 name: "genders".into(),
13486 pg_type: r#""default"."Gender"[]"#.into(),
13487 nullable: true,
13488 default_sql: None,
13489 default_pyql: None,
13490 description: None,
13491 check_constraints: vec![],
13492 is_exclusive: false,
13493 is_pk: false,
13494 is_readonly: false,
13495 rewrites: vec![],
13496 tuple_members: None,
13497 column_type: None,
13498 });
13499
13500 let out = compile_and_emit_with("SELECT Person { genders }", &schema);
13501 assert!(
13502 out.sql.contains(r#""genders"::text[]"#),
13503 "expected a text[] cast, got:\n{}",
13504 out.sql
13505 );
13506
13507 let crate::query::ShapeNode::Object { pointers, .. } = &out.shape.root else {
13508 panic!("expected Object shape, got {:?}", out.shape.root)
13509 };
13510 let genders = pointers
13511 .iter()
13512 .find(|node| matches!(node, crate::query::ShapeNode::Array { name, .. } if name == "genders"))
13513 .unwrap_or_else(|| panic!("expected an Array-shaped pointer, got {pointers:?}"));
13514 let crate::query::ShapeNode::Array { element, .. } = genders else {
13515 unreachable!()
13516 };
13517 assert!(
13518 matches!(
13519 element.as_ref(),
13520 crate::query::ShapeNode::Enum { enum_type, position: 0, .. }
13521 if enum_type == "default::Gender"
13522 ),
13523 "expected the elements to be enum-shaped, got {element:?}",
13524 );
13525 }
13526
13527 #[test]
13528 fn test_scalar_enum_property_still_casts_to_a_single_text() {
13529 let mut schema = make_schema();
13530 schema.enums.push(crate::schema::EnumDescriptor {
13531 name: "Gender".into(),
13532 module: "default".into(),
13533 members: vec!["Male".into(), "Female".into()],
13534 });
13535 schema.types[0].properties.push(crate::schema::PropertyDescriptor {
13536 name: "gender".into(),
13537 pg_type: r#""default"."Gender""#.into(),
13538 nullable: true,
13539 default_sql: None,
13540 default_pyql: None,
13541 description: None,
13542 check_constraints: vec![],
13543 is_exclusive: false,
13544 is_pk: false,
13545 is_readonly: false,
13546 rewrites: vec![],
13547 tuple_members: None,
13548 column_type: None,
13549 });
13550
13551 let out = compile_and_emit_with("SELECT Person { gender }", &schema);
13552 assert!(
13553 out.sql.contains(r#""gender"::text"#) && !out.sql.contains(r#""gender"::text[]"#),
13554 "expected a plain text cast, got:\n{}",
13555 out.sql
13556 );
13557 let crate::query::ShapeNode::Object { pointers, .. } = &out.shape.root else {
13558 panic!("expected Object shape")
13559 };
13560 assert!(
13561 pointers.iter().any(|node| matches!(
13562 node,
13563 crate::query::ShapeNode::Enum { name, enum_type, .. }
13564 if name == "gender" && enum_type == "default::Gender"
13565 )),
13566 "expected an Enum-shaped pointer, got {pointers:?}",
13567 );
13568 }
13569
13570 #[test]
13571 fn test_type_intersection_splat_keeps_enum_pointers_enum_shaped() {
13572 let mut schema = make_interface_schema();
13577 schema.enums.push(crate::schema::EnumDescriptor {
13578 name: "Gender".into(),
13579 module: "default".into(),
13580 members: vec!["Male".into(), "Female".into()],
13581 });
13582 let individual = schema
13585 .types
13586 .iter_mut()
13587 .find(|t| t.name == "Individual")
13588 .expect("the interface schema has an Individual type");
13589 individual.properties.push(crate::schema::PropertyDescriptor {
13590 name: "gender".into(),
13591 pg_type: r#""default"."Gender""#.into(),
13592 nullable: true,
13593 default_sql: None,
13594 default_pyql: None,
13595 description: None,
13596 check_constraints: vec![],
13597 is_exclusive: false,
13598 is_pk: false,
13599 is_readonly: false,
13600 rewrites: vec![],
13601 tuple_members: None,
13602 column_type: None,
13603 });
13604
13605 let out = compile_and_emit_with("SELECT Account { [is Individual].* }", &schema);
13606 let crate::query::ShapeNode::Object { pointers, .. } = &out.shape.root else {
13607 panic!("expected Object shape, got {:?}", out.shape.root)
13608 };
13609 assert!(
13610 pointers.iter().any(|node| matches!(
13611 node,
13612 crate::query::ShapeNode::Enum { name, enum_type, .. }
13613 if name == "gender" && enum_type == "default::Gender"
13614 )),
13615 "expected the enum property to stay enum-shaped through the intersection, got {pointers:?}",
13616 );
13617 }
13618
13619 #[test]
13620 fn test_shallow_splat_leaves_object_valued_computeds_to_the_deep_form() {
13621 let mut schema = make_schema();
13626 schema.types[0].computed.push(crate::schema::ComputedDescriptor {
13627 name: "authors".into(),
13628 expression: ".posts".into(),
13629 return_type: None,
13630 link_target: None,
13631 link_multi: false,
13632 });
13633 schema.types[0].computed.push(crate::schema::ComputedDescriptor {
13634 name: "age_next".into(),
13635 expression: ".age + 1".into(),
13636 return_type: Some("int8".into()),
13637 link_target: None,
13638 link_multi: false,
13639 });
13640
13641 let shallow = compile_and_emit_with("SELECT Person { * }", &schema);
13642 let crate::query::ShapeNode::Object { pointers, .. } = &shallow.shape.root else {
13643 panic!("expected Object shape")
13644 };
13645 let names: Vec<&str> = pointers.iter().map(shape_pointer_name).collect();
13646 assert!(
13647 names.contains(&"age_next"),
13648 "a computed property belongs in `*`: {names:?}"
13649 );
13650 assert!(!names.contains(&"authors"), "a computed link does not: {names:?}");
13651
13652 let deep = compile_and_emit_with("SELECT Person { ** }", &schema);
13653 let crate::query::ShapeNode::Object { pointers, .. } = &deep.shape.root else {
13654 panic!("expected Object shape")
13655 };
13656 let names: Vec<&str> = pointers.iter().map(shape_pointer_name).collect();
13657 assert!(
13658 names.contains(&"authors"),
13659 "`**` includes links, computed ones included: {names:?}"
13660 );
13661 }
13662
13663 fn shape_pointer_name(node: &crate::query::ShapeNode) -> &str {
13664 use crate::query::ShapeNode;
13665 match node {
13666 ShapeNode::Scalar { name, .. }
13667 | ShapeNode::Enum { name, .. }
13668 | ShapeNode::NamedTuple { name, .. }
13669 | ShapeNode::Object { name, .. }
13670 | ShapeNode::Array { name, .. } => name,
13671 _ => "",
13672 }
13673 }
13674
13675 #[test]
13676 fn test_any_over_a_multilink_comparison_does_not_warn() {
13677 let schema = make_schema();
13682 let ast = parse::parse("SELECT Person FILTER any(.posts.title = 'x')").unwrap();
13683 let ir = ir::compile(&ast, &schema).unwrap();
13684 assert!(
13685 !ir.warnings.iter().any(|w| w.contains("FILTER clause")),
13686 "any() states the intent, so nothing should warn: {:?}",
13687 ir.warnings
13688 );
13689 }
13690
13691 #[test]
13692 fn test_bare_multilink_comparison_still_warns() {
13693 let schema = make_schema();
13694 let ast = parse::parse("SELECT Person FILTER .posts.title = 'x'").unwrap();
13695 let ir = ir::compile(&ast, &schema).unwrap();
13696 assert!(
13697 ir.warnings.iter().any(|w| w.contains("FILTER clause")),
13698 "an unwrapped set-valued comparison still warns: {:?}",
13699 ir.warnings
13700 );
13701 }
13702
13703 #[test]
13704 fn test_link_to_an_interface_expands_over_its_implementors() {
13705 let mut schema = make_interface_schema();
13711 let account = schema
13712 .types
13713 .iter_mut()
13714 .find(|t| t.name == "Account")
13715 .expect("the interface schema has an Account type");
13716 account.materialized = true;
13717 let mut organization = schema
13719 .types
13720 .iter()
13721 .find(|t| t.name == "Individual")
13722 .expect("the interface schema has an Individual type")
13723 .clone();
13724 organization.name = "Organization".into();
13725 organization.table = "Organization".into();
13726 organization.computed.clear();
13727 schema.types.push(organization);
13728
13729 let owner = TypeDescriptor {
13730 name: "Note".into(),
13731 module: "default".into(),
13732 table: "Note".into(),
13733 abstract_: false,
13734 materialized: false,
13735 description: None,
13736 parents: vec![],
13737 interfaces: vec![],
13738 bases: vec![],
13739 properties: vec![],
13740 links: vec![LinkDescriptor {
13741 name: "owner".into(),
13742 target: "default::Account".into(),
13743 nullable: true,
13744 description: None,
13745 default_pyql: None,
13746 is_exclusive: false,
13747 is_readonly: false,
13748 rewrites: vec![],
13749 on_delete: vec![],
13750 through: None,
13751 }],
13752 multilinks: vec![],
13753 computed: vec![],
13754 constraints: vec![],
13755 indexes: vec![],
13756 partition: None,
13757 vector_indexes: vec![],
13758 search_indexes: vec![],
13759 triggers: vec![],
13760 junction: false,
13761 signals: vec![],
13762 };
13763 schema.types.push(owner);
13764
13765 let out = compile_and_emit_with("SELECT Note { owner: { id } }", &schema);
13766 assert!(
13767 out.sql.contains("UNION ALL"),
13768 "the link's target should expand over its implementors, got:\n{}",
13769 out.sql
13770 );
13771 assert!(
13772 out.sql.contains("'default::Individual'::text AS \"__type__\""),
13773 "each branch should carry its own discriminator, got:\n{}",
13774 out.sql
13775 );
13776 assert!(
13777 !out.sql.contains("FROM \"default\".\"Account\" AS"),
13778 "the interface's own view should no longer be read directly, got:\n{}",
13779 out.sql
13780 );
13781 }
13782
13783 fn make_two_implementor_schema() -> SchemaDescriptor {
13786 let mut schema = make_interface_schema();
13787 let mut organization = schema
13788 .types
13789 .iter()
13790 .find(|t| t.name == "Individual")
13791 .expect("the interface schema has an Individual type")
13792 .clone();
13793 organization.name = "Organization".into();
13794 organization.table = "Organization".into();
13795 organization.computed.clear();
13796 schema.types.push(organization);
13797 schema
13798 }
13799
13800 fn assert_rows_carry_their_concrete_type(query: &str) {
13803 let out = compile_and_emit_with(query, &make_two_implementor_schema());
13804 assert!(
13805 !out.sql.contains("'default::Account'::text"),
13806 "rows are labelled with the interface instead of their own type:\n{}",
13807 out.sql
13808 );
13809 assert!(
13810 out.sql.contains("'default::Individual'::text AS \"__type__\""),
13811 "the rows should come from the implementors, each tagging its own:\n{}",
13812 out.sql
13813 );
13814 }
13815
13816 #[test]
13817 fn an_aggregated_select_of_an_interface_carries_the_concrete_type() {
13818 assert_rows_carry_their_concrete_type("SELECT (xs := array_agg((SELECT Account { id })))");
13819 }
13820
13821 #[test]
13822 fn an_aggregated_binding_of_an_interface_carries_the_concrete_type() {
13823 assert_rows_carry_their_concrete_type("WITH a := (SELECT Account) SELECT (xs := array_agg((SELECT a { id })))");
13824 }
13825
13826 #[test]
13827 fn a_single_object_read_off_a_binding_carries_the_concrete_type() {
13828 assert_rows_carry_their_concrete_type("WITH a := (SELECT Account) SELECT (x := (SELECT a { id } LIMIT 1))");
13829 }
13830
13831 #[test]
13832 fn grouped_elements_of_an_interface_carry_the_concrete_type() {
13833 assert_rows_carry_their_concrete_type("group Account { id } by .email");
13834 }
13835
13836 fn make_note_schema() -> SchemaDescriptor {
13839 fn link_to(name: &str, target: &str) -> LinkDescriptor {
13840 LinkDescriptor {
13841 name: name.into(),
13842 target: target.into(),
13843 nullable: true,
13844 description: None,
13845 default_pyql: None,
13846 is_exclusive: false,
13847 is_readonly: false,
13848 rewrites: vec![],
13849 on_delete: vec![],
13850 through: None,
13851 }
13852 }
13853 let mut schema = make_two_implementor_schema();
13854 for account in schema
13855 .types
13856 .iter_mut()
13857 .filter(|t| ["Account", "Individual", "Organization"].contains(&t.name.as_str()))
13858 {
13859 account.links.push(link_to("pinned", "default::Note"));
13860 }
13861 schema.types.push(TypeDescriptor {
13862 name: "Note".into(),
13863 module: "default".into(),
13864 table: "Note".into(),
13865 abstract_: false,
13866 materialized: false,
13867 description: None,
13868 parents: vec![],
13869 interfaces: vec![],
13870 bases: vec![],
13871 properties: vec![],
13872 links: vec![link_to("owner", "default::Account")],
13873 multilinks: vec![],
13874 computed: vec![],
13875 constraints: vec![],
13876 indexes: vec![],
13877 partition: None,
13878 vector_indexes: vec![],
13879 search_indexes: vec![],
13880 triggers: vec![],
13881 junction: false,
13882 signals: vec![],
13883 });
13884 schema
13885 }
13886
13887 #[test]
13888 fn a_backlink_to_an_interface_carries_the_concrete_type() {
13889 let out = compile_and_emit_with(
13890 "SELECT Note { pinners := .<pinned[is Account] { id } }",
13891 &make_note_schema(),
13892 );
13893 assert!(
13894 !out.sql.contains("'default::Account'::text"),
13895 "the backlink's rows are labelled with the interface instead of their own type:\n{}",
13896 out.sql
13897 );
13898 }
13899
13900 #[test]
13901 fn a_link_reached_through_a_deep_splat_carries_the_concrete_type() {
13902 let out = compile_and_emit_with("SELECT Note { ** }", &make_note_schema());
13903 assert!(
13904 !out.sql.contains("'default::Account'::text"),
13905 "the splatted link's rows are labelled with the interface instead of their own type:\n{}",
13906 out.sql
13907 );
13908 }
13909
13910 #[test]
13911 fn test_path_traversal_onto_an_interface_carries_the_concrete_type() {
13912 let mut schema = make_interface_schema();
13917 let account = schema
13918 .types
13919 .iter_mut()
13920 .find(|t| t.name == "Account")
13921 .expect("the interface schema has an Account type");
13922 account.materialized = true;
13923 let mut organization = schema
13924 .types
13925 .iter()
13926 .find(|t| t.name == "Individual")
13927 .expect("the interface schema has an Individual type")
13928 .clone();
13929 organization.name = "Organization".into();
13930 organization.table = "Organization".into();
13931 organization.computed.clear();
13932 schema.types.push(organization);
13933 schema.types.push(TypeDescriptor {
13934 name: "Note".into(),
13935 module: "default".into(),
13936 table: "Note".into(),
13937 abstract_: false,
13938 materialized: false,
13939 description: None,
13940 parents: vec![],
13941 interfaces: vec![],
13942 bases: vec![],
13943 properties: vec![],
13944 links: vec![LinkDescriptor {
13945 name: "owner".into(),
13946 target: "default::Account".into(),
13947 nullable: true,
13948 description: None,
13949 default_pyql: None,
13950 is_exclusive: false,
13951 is_readonly: false,
13952 rewrites: vec![],
13953 on_delete: vec![],
13954 through: None,
13955 }],
13956 multilinks: vec![],
13957 computed: vec![],
13958 constraints: vec![],
13959 indexes: vec![],
13960 partition: None,
13961 vector_indexes: vec![],
13962 search_indexes: vec![],
13963 triggers: vec![],
13964 junction: false,
13965 signals: vec![],
13966 });
13967
13968 let out = compile_and_emit_with("SELECT Note.owner { id }", &schema);
13969 assert!(
13970 out.sql.contains("UNION ALL") && out.sql.contains("'default::Individual'::text AS \"__type__\""),
13971 "the traversal's target should fan out over implementors, got:\n{}",
13972 out.sql
13973 );
13974 assert!(
13975 !out.sql.contains("'default::Account'::text,"),
13976 "the row's type should be read off the row, not fixed to the interface, got:\n{}",
13977 out.sql
13978 );
13979 }
13980
13981 #[test]
13982 fn test_computed_multilink_chain_comes_back_as_rows() {
13983 let mut schema = make_schema();
13988 schema.types[0].computed.push(crate::schema::ComputedDescriptor {
13989 name: "coauthors".into(),
13990 expression: ".posts.author".into(),
13991 return_type: None,
13992 link_target: None,
13993 link_multi: false,
13994 });
13995 let post = schema
13996 .types
13997 .iter_mut()
13998 .find(|t| t.name == "Post")
13999 .expect("test schema has a Post type");
14000 post.links.push(LinkDescriptor {
14001 name: "author".into(),
14002 target: "default::Person".into(),
14003 nullable: true,
14004 description: None,
14005 default_pyql: None,
14006 is_exclusive: false,
14007 is_readonly: false,
14008 rewrites: vec![],
14009 on_delete: vec![],
14010 through: None,
14011 });
14012
14013 let out = compile_and_emit_with("SELECT Person { coauthors: { name } }", &schema);
14014 assert!(
14015 out.sql.contains("ARRAY(SELECT"),
14016 "the chain should aggregate into an array, got:\n{}",
14017 out.sql
14018 );
14019 assert!(
14020 out.sql.contains("\"name\""),
14021 "the requested sub-shape should survive, got:\n{}",
14022 out.sql
14023 );
14024
14025 let crate::query::ShapeNode::Object { pointers, .. } = &out.shape.root else {
14026 panic!("expected Object shape, got {:?}", out.shape.root)
14027 };
14028 let coauthors = pointers
14029 .iter()
14030 .find(|node| matches!(node, crate::query::ShapeNode::Array { name, .. } if name == "coauthors"))
14031 .unwrap_or_else(|| panic!("expected an Array-shaped pointer, got {pointers:?}"));
14032 let crate::query::ShapeNode::Array { element, .. } = coauthors else {
14033 unreachable!()
14034 };
14035 assert!(
14036 matches!(
14037 element.as_ref(),
14038 crate::query::ShapeNode::Object { type_name: Some(t), .. } if t == "default::Person"
14039 ),
14040 "the elements should hydrate as objects, got {element:?}",
14041 );
14042 }
14043
14044 #[test]
14045 fn test_multi_sort_with_then_emits_two_order_keys() {
14046 let out = compile_and_emit("SELECT Person { name } ORDER BY .name THEN .age DESC");
14047 assert!(out.sql.contains("ORDER BY"), "expected ORDER BY");
14048 assert!(out.sql.contains("\"name\""));
14050 assert!(out.sql.contains("\"age\""));
14051 assert!(out.sql.contains("DESC"));
14052 }
14053
14054 #[test]
14055 fn test_string_index_emits_str_subscript() {
14056 let out = compile_and_emit("SELECT 'hello'[1]");
14057 assert!(
14058 out.sql.contains("_pylon.str_subscript"),
14059 "expected _pylon.str_subscript() for string index, got:\n{}",
14060 out.sql
14061 );
14062 }
14063
14064 #[test]
14067 fn test_for_over_a_set_returning_call_is_not_a_values_clause() {
14068 let out = compile_and_emit("FOR n IN array_unpack(<array<int64>>$ns) UNION (SELECT n)");
14069 assert!(
14070 !out.sql.contains("VALUES (unnest"),
14071 "a set-returning iterator must not land in VALUES, got:\n{}",
14072 out.sql
14073 );
14074 assert!(
14075 out.sql.contains("SELECT unnest"),
14076 "expected the iterator in a select list, got:\n{}",
14077 out.sql
14078 );
14079 }
14080
14081 #[test]
14084 fn test_for_over_array_unpack_binds_the_element_type() {
14085 let out = compile_and_emit("FOR j IN array_unpack(<array<json>>$rows) UNION (SELECT <str>j['k'])");
14086 assert!(
14087 out.sql.contains("::jsonb AS v"),
14088 "the loop variable should carry the element type, got:\n{}",
14089 out.sql
14090 );
14091 }
14092
14093 #[test]
14096 fn test_a_binding_keeps_its_array_type_through_an_aggregate_and_a_condition() {
14097 for query in [
14098 "WITH ids := ([<uuid>$a] IF true ELSE [<uuid>$b]) SELECT Person FILTER contains(ids, .id)",
14099 "WITH ns := array_agg((SELECT Person.name)) SELECT Person FILTER contains(ns, .name)",
14100 ] {
14101 let out = compile_and_emit(query);
14102 assert!(
14103 out.sql.contains("@> ARRAY["),
14104 "expected the array overload of contains for `{query}`, got:\n{}",
14105 out.sql
14106 );
14107 }
14108 }
14109
14110 #[test]
14114 fn test_an_array_literals_walk_element_is_read_as_one_value() {
14115 let out = compile_and_emit("WITH p := (SELECT Person LIMIT 1) SELECT [<uuid>$a, p.id]");
14116 assert!(
14117 !out.sql.contains("ARRAY(SELECT"),
14118 "the element should be a scalar subquery, not an array of its own:\n{}",
14119 out.sql
14120 );
14121 }
14122
14123 #[test]
14127 fn test_ordering_a_walk_leaves_the_inner_filter_on_its_own_subject() {
14128 let out = compile_and_emit("SELECT (SELECT Person FILTER .name = 'x' LIMIT 1).posts { title } ORDER BY .title");
14129 assert!(
14130 out.sql.contains("WHERE (\"t0\".\"name\" = 'x')"),
14131 "the filter belongs to the Person the walk starts from, got:\n{}",
14132 out.sql
14133 );
14134 assert!(
14135 out.sql.contains("\"title\" ASC"),
14136 "the ordering belongs to the Post the walk lands on, got:\n{}",
14137 out.sql
14138 );
14139 }
14140
14141 #[test]
14145 fn test_a_walks_head_keeps_its_own_limit() {
14146 let out = compile_and_emit("SELECT (SELECT Person FILTER .name = 'x' LIMIT 1).posts { title }");
14147 let (head, walk) = out.sql.split_once(")\nSELECT").expect("expected a bound head");
14148 assert!(
14149 head.contains("LIMIT 1"),
14150 "the head keeps its own row count, got:\n{}",
14151 out.sql
14152 );
14153 assert!(
14154 !walk.contains("LIMIT"),
14155 "the walk keeps every row it reaches, got:\n{}",
14156 out.sql
14157 );
14158 }
14159
14160 #[test]
14164 fn test_for_over_json_array_unpack_binds_json() {
14165 let out = compile_and_emit(
14166 "FOR entry IN json_array_unpack(to_json(<str>$rows)) UNION (SELECT <str>json_get(entry, 'k'))",
14167 );
14168 assert!(
14169 out.sql.contains("::jsonb AS v"),
14170 "the loop variable should carry json, got:\n{}",
14171 out.sql
14172 );
14173 }
14174
14175 #[test]
14178 fn test_casting_json_get_reads_the_value_not_its_json_spelling() {
14179 let out = compile_and_emit("SELECT <uuid><str>json_get(<json>$p, 'id')");
14180 assert!(
14181 out.sql.contains("#>> '{}'"),
14182 "expected the json value to be read out, got:\n{}",
14183 out.sql
14184 );
14185 }
14186
14187 #[test]
14190 fn test_indexing_json_emits_a_jsonb_accessor() {
14191 let out = compile_and_emit("SELECT (<json>$payload)['label']");
14192 assert!(
14193 out.sql.contains("->'label'"),
14194 "expected a jsonb field access, got:\n{}",
14195 out.sql
14196 );
14197 assert!(
14198 !out.sql.contains("str_subscript"),
14199 "json must not be subscripted as a string, got:\n{}",
14200 out.sql
14201 );
14202 }
14203
14204 #[test]
14207 fn test_casting_json_to_a_scalar_unwraps_it() {
14208 let out = compile_and_emit("SELECT <str>to_json(<str>$s)");
14209 assert!(
14210 out.sql.contains("#>> '{}'"),
14211 "expected the json value to be extracted, got:\n{}",
14212 out.sql
14213 );
14214 }
14215
14216 #[test]
14220 fn test_a_binding_that_reads_the_loop_variable_is_keyed_by_iteration() {
14221 let out = compile_and_emit(
14222 "WITH made := (FOR p IN (SELECT Person) UNION ( \
14223 WITH mine := (SELECT p.posts LIMIT 1) \
14224 INSERT Company { name := mine.title } \
14225 )) SELECT count(made)",
14226 );
14227 assert!(
14228 out.sql
14229 .contains("\"mine\" AS (\nSELECT \"_for_p\".\"v\" AS \"_outer__mine\""),
14230 "the binding must be evaluated per iteration, got:\n{}",
14231 out.sql
14232 );
14233 assert!(
14234 out.sql.contains("\"mine\".\"_outer__mine\" = \"_for_p\".\"v\""),
14235 "a read of it must pin to the iteration in scope, got:\n{}",
14236 out.sql
14237 );
14238 }
14239
14240 #[test]
14244 fn test_a_loop_nested_in_a_loop_pairs_its_rows_by_iteration() {
14245 let out = compile_and_emit(
14246 "FOR n IN {'a', 'b'} UNION (INSERT Person { name := n, posts := (FOR t IN {n} UNION (INSERT Post { title := t })) })",
14247 );
14248 assert!(
14249 out.sql.contains("\"_outer\""),
14250 "the nested rows must carry an iteration key, got:\n{}",
14251 out.sql
14252 );
14253 assert!(
14254 !out.sql.contains("CROSS JOIN \"_nested_dml_0\""),
14255 "they must not be cross-joined onto every row, got:\n{}",
14256 out.sql
14257 );
14258 }
14259
14260 #[test]
14263 fn test_for_bodied_insert_writes_its_multi_link() {
14264 let out = compile_and_emit("FOR n IN {'a', 'b'} UNION (INSERT Person { name := n, posts := (SELECT Post) })");
14265 assert!(
14266 out.sql.contains("INSERT INTO \"public\".\"Person.posts\""),
14267 "the junction rows must be written too, got:\n{}",
14268 out.sql
14269 );
14270 }
14271
14272 #[test]
14273 fn test_string_slice_emits_substr() {
14274 let out = compile_and_emit("SELECT 'hello'[1:3]");
14275 assert!(
14276 out.sql.contains("substr"),
14277 "expected substr() for string slice, got:\n{}",
14278 out.sql
14279 );
14280 }
14281
14282 #[test]
14283 fn test_array_index_emits_subscript() {
14284 let out = compile_and_emit("SELECT [1, 2, 3][1]");
14285 assert!(
14286 out.sql.contains("_pylon.array_subscript"),
14287 "expected _pylon.array_subscript() for array index, got:\n{}",
14288 out.sql
14289 );
14290 }
14291
14292 #[test]
14293 fn test_index_into_array_returning_function_emits_array_subscript() {
14294 let out = compile_and_emit("SELECT str_split('a::b', '::')[-1]");
14295 assert!(
14296 out.sql.contains("_pylon.array_subscript(string_to_array"),
14297 "expected _pylon.array_subscript() over str_split, got:\n{}",
14298 out.sql
14299 );
14300 }
14301
14302 #[test]
14303 fn test_array_slice_emits_subscript() {
14304 let out = compile_and_emit("SELECT [1, 2, 3][0:2]");
14305 assert!(
14306 !out.sql.contains("substr"),
14307 "should not use substr for array, got:\n{}",
14308 out.sql
14309 );
14310 assert!(out.sql.contains(")["), "expected array slice syntax, got:\n{}", out.sql);
14311 }
14312
14313 #[test]
14314 fn test_open_ended_string_slice_emits_substr_no_length() {
14315 let out = compile_and_emit("SELECT 'hello'[2:]");
14316 assert!(out.sql.contains("substr"), "expected substr(), got:\n{}", out.sql);
14318 let substr_idx = out.sql.find("substr").unwrap();
14320 let after = &out.sql[substr_idx..];
14321 let commas = after.chars().take_while(|&c| c != ')').filter(|&c| c == ',').count();
14322 assert_eq!(commas, 1, "open-ended slice should use 2-arg substr, got:\n{}", out.sql);
14323 }
14324
14325 #[test]
14326 fn test_group_by_single_key() {
14327 let out = compile_and_emit("group Person { name } by .age");
14328 assert!(out.sql.contains("GROUP BY"), "expected GROUP BY, got:\n{}", out.sql);
14330 assert!(out.sql.contains("\"age\""), "expected age column, got:\n{}", out.sql);
14332 assert!(
14334 out.sql.contains("array_agg(ROW("),
14335 "expected array_agg, got:\n{}",
14336 out.sql
14337 );
14338 assert!(
14340 out.sql.contains("ARRAY['age']"),
14341 "expected grouping array, got:\n{}",
14342 out.sql
14343 );
14344 assert!(matches!(out.shape.root, crate::query::ShapeNode::Group { .. }));
14346 if let crate::query::ShapeNode::Group {
14347 key_nodes,
14348 grouping_position,
14349 elements_position,
14350 ..
14351 } = &out.shape.root
14352 {
14353 assert_eq!(key_nodes.len(), 1);
14354 assert!(matches!(&key_nodes[0], crate::query::ShapeNode::Scalar { name, position: 1 } if name == "age"));
14355 assert_eq!(*grouping_position, 2);
14356 assert_eq!(*elements_position, 3);
14357 }
14358 }
14359
14360 #[test]
14361 fn test_group_using_alias() {
14362 let out = compile_and_emit("group Person using decade := .age // 10 by decade");
14363 assert!(out.sql.contains("GROUP BY"), "expected GROUP BY, got:\n{}", out.sql);
14364 assert!(
14365 out.sql.contains("ARRAY['decade']"),
14366 "expected grouping array, got:\n{}",
14367 out.sql
14368 );
14369 if let crate::query::ShapeNode::Group { key_nodes, .. } = &out.shape.root {
14370 assert_eq!(key_nodes.len(), 1);
14371 assert!(matches!(&key_nodes[0], crate::query::ShapeNode::Scalar { name, .. } if name == "decade"));
14372 }
14373 }
14374
14375 #[test]
14376 fn test_group_orders_elements_within_each_group() {
14377 let out = compile_and_emit("group Person { name } by .age order by .name desc");
14378 assert!(
14379 out.sql.contains(")::record ORDER BY \"t0\".\"name\" DESC NULLS LAST)"),
14380 "{}",
14381 out.sql
14382 );
14383 assert!(!out.sql.contains("row_number()"), "{}", out.sql);
14384 }
14385
14386 #[test]
14387 fn test_group_limit_trims_each_group_not_the_result() {
14388 let out = compile_and_emit("group Person { name } by .age order by .name desc limit 1");
14391 assert!(
14392 out.sql
14393 .contains("row_number() OVER (PARTITION BY \"t0\".\"age\" ORDER BY \"t0\".\"name\" DESC NULLS LAST)"),
14394 "{}",
14395 out.sql
14396 );
14397 assert!(
14398 out.sql.contains("WHERE \"__rk\" > 0 AND \"__rk\" <= 0 + 1"),
14399 "{}",
14400 out.sql
14401 );
14402 assert!(!out.sql.contains("\nLIMIT"), "{}", out.sql);
14403 }
14404
14405 #[test]
14406 fn test_group_by_two_keys_with_a_per_group_limit() {
14407 let out = compile_and_emit(
14408 "group Person { name } by .age, .name filter .age > 18 order by .name desc offset 1 limit 2",
14409 );
14410 assert!(
14411 out.sql.contains("PARTITION BY \"t0\".\"age\", \"t0\".\"name\""),
14412 "{}",
14413 out.sql
14414 );
14415 let where_pos = out.sql.find("WHERE (\"t0\".\"age\" > 18)").expect("filter");
14418 let rank_pos = out.sql.find("WHERE \"__rk\"").expect("rank filter");
14419 assert!(where_pos < rank_pos, "{}", out.sql);
14420 assert!(
14421 out.sql.contains("GROUP BY \"t0\".\"age\", \"t0\".\"name\""),
14422 "{}",
14423 out.sql
14424 );
14425 }
14426
14427 #[test]
14428 fn test_group_filter_without_a_limit_is_a_plain_where() {
14429 let out = compile_and_emit("group Person { name } by .age filter .age > 18");
14430 assert!(out.sql.contains("WHERE (\"t0\".\"age\" > 18)"), "{}", out.sql);
14431 assert!(!out.sql.contains("row_number()"), "{}", out.sql);
14432 }
14433
14434 #[test]
14435 fn test_abs_path_concat_same_type() {
14436 let out = compile_and_emit("SELECT Person.name ++ ' ' ++ Person.name");
14437 assert!(out.sql.contains("\"name\""), "expected name column, got:\n{}", out.sql);
14438 assert!(out.sql.contains("||"), "expected concat operator, got:\n{}", out.sql);
14439 assert!(out.sql.contains("FROM"), "expected FROM clause, got:\n{}", out.sql);
14440 }
14441
14442 #[test]
14443 fn test_abs_path_single_property() {
14444 let out = compile_and_emit("SELECT Person.name");
14445 assert!(out.sql.contains("\"name\""), "expected name column, got:\n{}", out.sql);
14446 assert!(out.sql.contains("FROM"), "expected FROM clause, got:\n{}", out.sql);
14447 }
14448
14449 #[test]
14450 fn test_pgvector_cast_emits_vector_type() {
14451 let out = compile_and_emit("SELECT <pgvector::vector>[1.0, 2.0, 3.0]");
14452 assert!(
14453 out.sql.contains("::vector"),
14454 "expected ::vector cast, got:\n{}",
14455 out.sql
14456 );
14457 assert!(out.sql.contains("ARRAY["), "expected ARRAY literal, got:\n{}", out.sql);
14458 }
14459
14460 #[test]
14461 fn test_pgvector_euclidean_distance_emits_l2_operator() {
14462 let out = compile_and_emit(
14463 "SELECT pgvector::euclidean_distance(<pgvector::vector>[1.0, 2.0], <pgvector::vector>[3.0, 4.0])",
14464 );
14465 assert!(out.sql.contains("<->"), "expected <-> operator, got:\n{}", out.sql);
14466 }
14467
14468 #[test]
14469 fn test_pgvector_cosine_distance_emits_cosine_operator() {
14470 let out = compile_and_emit(
14471 "SELECT pgvector::cosine_distance(<pgvector::vector>[1.0, 2.0], <pgvector::vector>[3.0, 4.0])",
14472 );
14473 assert!(out.sql.contains("<=>"), "expected <=> operator, got:\n{}", out.sql);
14474 }
14475
14476 #[test]
14477 fn test_pgvector_neg_inner_product_emits_ip_operator() {
14478 let out = compile_and_emit(
14479 "SELECT pgvector::neg_inner_product(<pgvector::vector>[1.0, 2.0], <pgvector::vector>[3.0, 4.0])",
14480 );
14481 assert!(out.sql.contains("<#>"), "expected <#> operator, got:\n{}", out.sql);
14482 }
14483
14484 #[test]
14485 fn test_pgvector_inner_product_negates_ip_operator() {
14486 let out = compile_and_emit(
14487 "SELECT pgvector::inner_product(<pgvector::vector>[1.0, 2.0], <pgvector::vector>[3.0, 4.0])",
14488 );
14489 assert!(out.sql.contains("<#>"), "expected <#> operator, got:\n{}", out.sql);
14490 assert!(out.sql.contains("0.0"), "expected negation of <#>, got:\n{}", out.sql);
14491 }
14492
14493 #[test]
14494 fn test_crypto_digest_str_and_bytes_overloads_both_use_pgcrypto_digest() {
14495 let out = compile_and_emit("SELECT crypto::digest('hello', 'sha256')");
14496 assert!(
14497 out.sql.contains("digest("),
14498 "expected pgcrypto's digest(), got:\n{}",
14499 out.sql
14500 );
14501
14502 let out = compile_and_emit("SELECT crypto::digest(std::from_hex('68656c6c6f'), 'sha256')");
14503 assert!(
14504 out.sql.contains("digest("),
14505 "expected pgcrypto's digest(), got:\n{}",
14506 out.sql
14507 );
14508 }
14509
14510 #[test]
14511 fn test_crypto_hmac_str_and_bytes_overloads_both_use_pgcrypto_hmac() {
14512 let out = compile_and_emit("SELECT crypto::hmac('hello', 'key', 'sha256')");
14513 assert!(
14514 out.sql.contains("hmac("),
14515 "expected pgcrypto's hmac(), got:\n{}",
14516 out.sql
14517 );
14518
14519 let out =
14520 compile_and_emit("SELECT crypto::hmac(std::from_hex('68656c6c6f'), std::from_hex('6b6579'), 'sha256')");
14521 assert!(
14522 out.sql.contains("hmac("),
14523 "expected pgcrypto's hmac(), got:\n{}",
14524 out.sql
14525 );
14526 }
14527
14528 #[test]
14529 fn test_crypto_gen_salt_zero_arg_defaults_to_blowfish() {
14530 let out = compile_and_emit("SELECT crypto::gen_salt()");
14531 assert!(
14532 out.sql.contains("gen_salt('bf')"),
14533 "expected default 'bf' salt type, got:\n{}",
14534 out.sql
14535 );
14536 }
14537
14538 #[test]
14539 fn test_crypto_gen_salt_one_arg_passes_type_through() {
14540 let out = compile_and_emit("SELECT crypto::gen_salt('xdes')");
14541 assert!(
14542 out.sql.contains("gen_salt("),
14543 "expected gen_salt() call, got:\n{}",
14544 out.sql
14545 );
14546 }
14547
14548 #[test]
14549 fn test_crypto_gen_salt_iter_count_casts_to_int4() {
14550 let out = compile_and_emit("SELECT crypto::gen_salt('xdes', 5)");
14551 assert!(
14552 out.sql.contains("::int4"),
14553 "expected int8 -> int4 narrowing cast, got:\n{}",
14554 out.sql
14555 );
14556 }
14557
14558 #[test]
14559 fn test_crypto_crypt_uses_pgcrypto_crypt() {
14560 let out = compile_and_emit("SELECT crypto::crypt('hunter2', crypto::gen_salt())");
14561 assert!(
14562 out.sql.contains("crypt("),
14563 "expected pgcrypto's crypt(), got:\n{}",
14564 out.sql
14565 );
14566 }
14567
14568 #[test]
14569 fn test_postgis_cast_emits_geometry_type() {
14570 let out = compile_and_emit("SELECT <postgis::geometry>'POINT(1 2)'");
14571 assert!(
14572 out.sql.contains("::geometry"),
14573 "expected ::geometry cast, got:\n{}",
14574 out.sql
14575 );
14576 }
14577
14578 #[test]
14579 fn test_postgis_x_uses_st_x_builtin() {
14580 let out = compile_and_emit("SELECT postgis::x(<postgis::geometry>'POINT(1 2)')");
14581 assert!(out.sql.contains("st_x("), "expected st_x() call, got:\n{}", out.sql);
14582 }
14583
14584 #[test]
14585 fn test_postgis_area_geometry_and_geography_overloads() {
14586 let out = compile_and_emit("SELECT postgis::area(<postgis::geometry>'POINT(1 2)')");
14587 assert!(
14588 out.sql.contains("st_area("),
14589 "expected st_area() call, got:\n{}",
14590 out.sql
14591 );
14592
14593 let out = compile_and_emit("SELECT postgis::area(<postgis::geography>'POINT(1 2)', true)");
14594 assert!(
14595 out.sql.contains("st_area("),
14596 "expected st_area() call, got:\n{}",
14597 out.sql
14598 );
14599 }
14600
14601 #[test]
14602 fn test_postgis_setsrid_casts_int64_arg_to_int4() {
14603 let out = compile_and_emit("SELECT postgis::setsrid(<postgis::geometry>'POINT(1 2)', 4326)");
14604 assert!(
14605 out.sql.contains("st_setsrid("),
14606 "expected st_setsrid() call, got:\n{}",
14607 out.sql
14608 );
14609 assert!(
14610 out.sql.contains("::int4"),
14611 "expected int8 -> int4 narrowing cast, got:\n{}",
14612 out.sql
14613 );
14614 }
14615
14616 #[test]
14617 fn test_postgis_quantizecoordinates_default_arity_variants_compile() {
14618 let out = compile_and_emit("SELECT postgis::quantizecoordinates(<postgis::geometry>'POINT(1 2)', 5)");
14623 assert!(out.sql.contains("st_quantizecoordinates("), "got:\n{}", out.sql);
14624
14625 let out = compile_and_emit("SELECT postgis::quantizecoordinates(<postgis::geometry>'POINT(1 2)', 5, 5, 5)");
14626 assert!(out.sql.contains("st_quantizecoordinates("), "got:\n{}", out.sql);
14627 }
14628
14629 #[test]
14630 fn test_postgis_op_contains_emits_infix_operator_not_function_call() {
14631 let out = compile_and_emit(
14636 "SELECT postgis::op_contains(<postgis::geometry>'POINT(1 2)', <postgis::geometry>'POINT(3 4)')",
14637 );
14638 assert!(out.sql.contains(" ~ "), "expected infix ~ operator, got:\n{}", out.sql);
14639 assert!(
14640 !out.sql.contains("op_contains("),
14641 "must not call a literal op_contains function, got:\n{}",
14642 out.sql
14643 );
14644 }
14645
14646 #[test]
14647 fn test_postgis_op_overlaps_geometry_and_geography_overloads() {
14648 let out = compile_and_emit(
14649 "SELECT postgis::op_overlaps(<postgis::geometry>'POINT(1 2)', <postgis::geometry>'POINT(3 4)')",
14650 );
14651 assert!(
14652 out.sql.contains(" && "),
14653 "expected infix && operator, got:\n{}",
14654 out.sql
14655 );
14656
14657 let out = compile_and_emit(
14658 "SELECT postgis::op_overlaps(<postgis::geography>'POINT(1 2)', <postgis::geography>'POINT(3 4)')",
14659 );
14660 assert!(
14661 out.sql.contains(" && "),
14662 "expected infix && operator, got:\n{}",
14663 out.sql
14664 );
14665 }
14666
14667 fn make_schema_with_fns() -> SchemaDescriptor {
14670 let mut s = make_schema();
14671 s.functions = vec![
14672 FunctionDescriptor {
14673 name: "mysum".into(),
14674 module: "default".into(),
14675 params: vec![
14676 FunctionParamDescriptor {
14677 name: "a".into(),
14678 pg_type: "int8".into(),
14679 },
14680 FunctionParamDescriptor {
14681 name: "b".into(),
14682 pg_type: "int8".into(),
14683 },
14684 ],
14685 return_pg_type: "int8".into(),
14686 return_is_object: false,
14687 return_is_set: false,
14688 return_is_polymorphic: false,
14689 volatility: "immutable".into(),
14690 body: "a + b".into(),
14691 },
14692 FunctionDescriptor {
14693 name: "adults".into(),
14694 module: "default".into(),
14695 params: vec![],
14696 return_pg_type: "default::Person".into(),
14697 return_is_object: true,
14698 return_is_set: true,
14699 return_is_polymorphic: false,
14700 volatility: "stable".into(),
14701 body: "select Person filter .age > 18".into(),
14702 },
14703 FunctionDescriptor {
14710 name: "mysum".into(),
14711 module: "default".into(),
14712 params: vec![
14713 FunctionParamDescriptor {
14714 name: "a".into(),
14715 pg_type: "int8".into(),
14716 },
14717 FunctionParamDescriptor {
14718 name: "b".into(),
14719 pg_type: "int8".into(),
14720 },
14721 FunctionParamDescriptor {
14722 name: "c".into(),
14723 pg_type: "int8".into(),
14724 },
14725 ],
14726 return_pg_type: "int8".into(),
14727 return_is_object: false,
14728 return_is_set: false,
14729 return_is_polymorphic: false,
14730 volatility: "immutable".into(),
14731 body: "a + b + c".into(),
14732 },
14733 ];
14734 s
14735 }
14736
14737 #[test]
14738 fn test_user_fn_scalar_call() {
14739 let schema = make_schema_with_fns();
14740 let out = compile_and_emit_with("SELECT mysum(1, 2)", &schema);
14741 assert!(out.sql.contains("\"public\".\"mysum\""), "got:\n{}", out.sql);
14742 }
14743
14744 #[test]
14745 fn test_user_fn_overload_resolved_by_argument_count() {
14746 let schema = make_schema_with_fns();
14751 let out = compile_and_emit_with("SELECT mysum(1, 2, 3)", &schema);
14752 assert!(
14753 out.sql
14754 .contains("\"public\".\"mysum\"((1)::int8, (2)::int8, (3)::int8)"),
14755 "got:\n{}",
14756 out.sql,
14757 );
14758
14759 let out = compile_and_emit_with("SELECT mysum(1, 2)", &schema);
14761 assert!(
14762 out.sql.contains("\"public\".\"mysum\"((1)::int8, (2)::int8)"),
14763 "got:\n{}",
14764 out.sql,
14765 );
14766 }
14767
14768 #[test]
14769 fn test_user_fn_object_select_no_shape() {
14770 let schema = make_schema_with_fns();
14771 let out = compile_and_emit_with("SELECT adults()", &schema);
14772 assert!(out.sql.contains("\"public\".\"adults\"()"), "got:\n{}", out.sql);
14773 assert!(out.sql.contains("FROM"), "got:\n{}", out.sql);
14774 }
14775
14776 #[test]
14777 fn test_user_fn_object_select_with_shape() {
14778 let schema = make_schema_with_fns();
14779 let out = compile_and_emit_with("SELECT adults() { name }", &schema);
14780 assert!(out.sql.contains("\"public\".\"adults\"()"), "got:\n{}", out.sql);
14781 assert!(out.sql.contains("\"name\""), "got:\n{}", out.sql);
14782 }
14783
14784 #[test]
14785 fn test_object_fn_projected_to_a_property_inside_an_expression() {
14786 let schema = make_schema_with_fns();
14787 let out = compile_and_emit_with("SELECT Person { n := adults().name }", &schema);
14788 assert!(out.sql.contains("FROM \"public\".\"adults\"() AS"), "got:\n{}", out.sql);
14789 assert!(out.sql.contains("\".\"name\"\n"), "got:\n{}", out.sql);
14790 }
14791
14792 #[test]
14793 fn test_object_fn_sub_select_carries_its_modifiers() {
14794 let schema = make_schema_with_fns();
14795 let out = compile_and_emit_with(
14796 "SELECT Person { n := (select adults() filter .age > 21 limit 1).name }",
14797 &schema,
14798 );
14799 assert!(out.sql.contains("FROM \"public\".\"adults\"() AS"), "got:\n{}", out.sql);
14800 assert!(out.sql.contains("\"age\" > 21"), "got:\n{}", out.sql);
14801 assert!(out.sql.contains("LIMIT 1"), "got:\n{}", out.sql);
14802 }
14803
14804 #[test]
14805 fn test_object_fn_without_a_property_still_explains_the_restriction() {
14806 let schema = make_schema_with_fns();
14807 let ast = parse::parse("SELECT Person { n := adults() + 1 }").unwrap();
14808 let err = match ir::compile(&ast, &schema) {
14809 Ok(_) => panic!("expected a compile error"),
14810 Err(e) => format!("{e}"),
14811 };
14812 assert!(err.contains("returns objects"), "{err}");
14813 }
14814
14815 #[test]
14816 fn test_object_fn_projected_to_an_unknown_property_is_rejected() {
14817 let schema = make_schema_with_fns();
14818 let ast = parse::parse("SELECT Person { n := adults().nope }").unwrap();
14819 let err = match ir::compile(&ast, &schema) {
14820 Ok(_) => panic!("expected a compile error"),
14821 Err(e) => format!("{e}"),
14822 };
14823 assert!(err.contains("nope"), "{err}");
14824 }
14825
14826 #[test]
14827 fn test_user_fn_in_cte_exposes_raw_columns() {
14828 let schema = make_schema_with_fns();
14831 let out = compile_and_emit_with("WITH persons := adults() SELECT persons FILTER .age > 25", &schema);
14832 assert!(
14833 out.sql.contains("SELECT * FROM \"public\".\"adults\"()"),
14834 "CTE source must be SELECT * FROM fn(), got:\n{}",
14835 out.sql,
14836 );
14837 assert!(
14838 out.sql.contains("\"age\""),
14839 "outer filter must reference raw column, got:\n{}",
14840 out.sql
14841 );
14842 }
14843
14844 fn make_schema_with_vector() -> SchemaDescriptor {
14847 use crate::schema::VectorIndexDescriptor;
14848 let mut s = make_schema();
14849 if let Some(td) = s.types.iter_mut().find(|t| t.name == "Person") {
14850 td.vector_indexes.push(VectorIndexDescriptor {
14851 index_name: None,
14852 pointers: vec!["name".into()],
14853 model: "test-embed".into(),
14854 metric: "cosine".into(),
14855 dimensions: 4,
14856 });
14857 }
14858 s
14859 }
14860
14861 #[test]
14862 fn test_vector_search_bare_type_name() {
14863 let schema = make_schema_with_vector();
14864 let out = compile_and_emit_with(
14865 "WITH search := vector::search(Person, <pgvector::vector>[1.0, 2.0, 3.0, 4.0]) \
14866 SELECT search { object { name }, distance }",
14867 &schema,
14868 );
14869 assert!(
14870 out.sql.contains("\"Person\""),
14871 "expected Person table, got:\n{}",
14872 out.sql
14873 );
14874 assert!(out.sql.contains("<=>"), "expected cosine operator, got:\n{}", out.sql);
14875 }
14876
14877 #[test]
14878 fn test_vector_search_qualified_type_name() {
14879 let schema = make_schema_with_vector();
14880 let out = compile_and_emit_with(
14881 "WITH search := vector::search(default::Person, <pgvector::vector>[1.0, 2.0, 3.0, 4.0]) \
14882 SELECT search { object { name }, distance }",
14883 &schema,
14884 );
14885 assert!(
14886 out.sql.contains("\"Person\""),
14887 "expected Person table, got:\n{}",
14888 out.sql
14889 );
14890 assert!(out.sql.contains("<=>"), "expected cosine operator, got:\n{}", out.sql);
14891 }
14892
14893 #[test]
14894 fn test_vector_search_subquery_filter_included_in_where() {
14895 let schema = make_schema_with_vector();
14896 let out = compile_and_emit_with(
14897 "WITH search := vector::search((select Person filter .name = 'Alice'), <pgvector::vector>[1.0, 2.0, 3.0, 4.0]) \
14898 SELECT search { object { name }, distance }",
14899 &schema,
14900 );
14901 assert!(
14902 out.sql.contains("\"Person\""),
14903 "expected Person table, got:\n{}",
14904 out.sql
14905 );
14906 assert!(out.sql.contains("\"name\""), "expected name filter, got:\n{}", out.sql);
14907 assert!(out.sql.contains("Alice"), "expected filter value, got:\n{}", out.sql);
14908 assert!(out.sql.contains("<=>"), "expected cosine operator, got:\n{}", out.sql);
14909 }
14910
14911 #[test]
14912 fn test_vector_search_subquery_filter_combined_with_outer_property_filter() {
14913 let schema = make_schema_with_vector();
14914 let out = compile_and_emit_with(
14915 "WITH search := vector::search((select Person filter .age > 18), <pgvector::vector>[1.0, 2.0, 3.0, 4.0]) \
14916 SELECT search { object { name }, distance }",
14917 &schema,
14918 );
14919 assert!(
14920 out.sql.contains("\"age\""),
14921 "expected age pre-filter, got:\n{}",
14922 out.sql
14923 );
14924 assert!(out.sql.contains("18"), "expected filter value 18, got:\n{}", out.sql);
14925 assert!(out.sql.contains("<=>"), "expected cosine operator, got:\n{}", out.sql);
14926 }
14927
14928 #[test]
14929 fn test_vector_search_text_overload_with_subquery_filter() {
14930 let schema = make_schema_with_vector();
14931 let out = compile_and_emit_with(
14932 "WITH search := vector::search((select Person filter .name = 'Alice'), query := $q) \
14933 SELECT search { object { name }, distance }",
14934 &schema,
14935 );
14936 assert!(
14937 out.sql.contains("\"Person\""),
14938 "expected Person table, got:\n{}",
14939 out.sql
14940 );
14941 assert!(
14942 out.sql.contains("Alice"),
14943 "expected pre-filter value, got:\n{}",
14944 out.sql
14945 );
14946 assert!(
14947 out.sql.contains("float8[]"),
14948 "expected float8[] cast for deferred vec param, got:\n{}",
14949 out.sql
14950 );
14951 assert!(out.sql.contains("<=>"), "expected cosine operator, got:\n{}", out.sql);
14952 }
14953
14954 #[test]
14955 fn test_count_type_ref_compiles_to_agg_over_query() {
14956 let out = compile_and_emit("SELECT count(Person)");
14957 assert!(out.sql.contains("count(*)"), "expected count(*), got:\n{}", out.sql);
14958 assert!(
14959 out.sql.contains("\"Person\""),
14960 "expected Person table, got:\n{}",
14961 out.sql
14962 );
14963 }
14964
14965 #[test]
14966 fn test_count_qualified_type_ref_compiles_to_agg_over_query() {
14967 let out = compile_and_emit("SELECT count(default::Person)");
14968 assert!(out.sql.contains("count(*)"), "expected count(*), got:\n{}", out.sql);
14969 assert!(
14970 out.sql.contains("\"Person\""),
14971 "expected Person table, got:\n{}",
14972 out.sql
14973 );
14974 }
14975
14976 #[test]
14977 fn test_count_subquery_compiles_to_agg_over_query() {
14978 let out = compile_and_emit("SELECT count((select Person))");
14979 assert!(out.sql.contains("count(*)"), "expected count(*), got:\n{}", out.sql);
14980 assert!(
14981 out.sql.contains("\"Person\""),
14982 "expected Person table, got:\n{}",
14983 out.sql
14984 );
14985 }
14986
14987 #[test]
14988 fn test_count_subquery_with_filter() {
14989 let out = compile_and_emit("SELECT count((select Person filter .name = 'Alice'))");
14990 assert!(out.sql.contains("count(*)"), "expected count(*), got:\n{}", out.sql);
14991 assert!(
14992 out.sql.contains("\"name\""),
14993 "expected filter on name, got:\n{}",
14994 out.sql
14995 );
14996 }
14997
14998 #[test]
14999 fn test_positional_param_compiles_to_dollar_n() {
15000 let out = compile_and_emit("SELECT Person FILTER .name = $0");
15001 assert!(out.sql.contains("$1"), "expected $1 placeholder, got:\n{}", out.sql);
15002 }
15003
15004 #[test]
15005 fn test_multiple_positional_params_compile_in_order() {
15006 let out = compile_and_emit("SELECT Person FILTER .name = $0 AND .age > $1");
15007 assert!(out.sql.contains("$1"), "expected $1, got:\n{}", out.sql);
15008 assert!(out.sql.contains("$2"), "expected $2, got:\n{}", out.sql);
15009 }
15010
15011 #[test]
15012 fn test_repeated_positional_param_reuses_slot() {
15013 let out = compile_and_emit("SELECT Person FILTER .name = $0 OR .name = $0");
15014 assert_eq!(
15015 out.sql.matches("$1").count(),
15016 2,
15017 "both uses must reference $1, got:\n{}",
15018 out.sql
15019 );
15020 }
15021
15022 #[test]
15023 fn test_cast_to_nonexistent_type_names_full_type() {
15024 let schema = make_schema();
15025 let ast = parse::parse("SELECT Person FILTER .name = <default::Ghost>$name").unwrap();
15026 match ir::compile(&ast, &schema) {
15027 Ok(_) => panic!("expected compile error for unknown type"),
15028 Err(e) => {
15029 let msg = e.to_string();
15030 assert!(
15031 msg.contains("unknown type 'default::Ghost'"),
15032 "expected full type name in error, got: {msg}",
15033 );
15034 }
15035 }
15036 }
15037
15038 #[test]
15039 fn test_cast_to_nonexistent_unqualified_type_names_type() {
15040 let schema = make_schema();
15041 let ast = parse::parse("SELECT Person FILTER .name = <Ghost>$name").unwrap();
15042 match ir::compile(&ast, &schema) {
15043 Ok(_) => panic!("expected compile error for unknown type"),
15044 Err(e) => {
15045 let msg = e.to_string();
15046 assert!(
15047 msg.contains("unknown type 'Ghost'"),
15048 "expected type name in error, got: {msg}",
15049 );
15050 }
15051 }
15052 }
15053
15054 #[test]
15055 fn test_top_level_cast_to_registered_scalar_uses_its_own_domain() {
15056 use crate::schema::ScalarDescriptor;
15062 let mut schema = make_schema();
15063 schema.scalars.push(ScalarDescriptor {
15064 name: "Email".into(),
15065 module: "default".into(),
15066 base: "Str".into(),
15067 pg_type: "text".into(),
15068 check_constraints: vec!["value ~ '^[^@]+@[^@]+\\.[^@]+$'".into()],
15069 is_sequence: false,
15070 });
15071 let out = compile_and_emit_with("SELECT <default::Email>'test@test.de'", &schema);
15072 assert!(
15073 out.sql.contains("\"public\".\"Email\""),
15074 "expected a cast to the scalar's own domain, got:\n{}",
15075 out.sql
15076 );
15077 assert!(
15078 !out.sql.to_lowercase().contains("\"person\""),
15079 "must not be misrouted to an object-type lookup, got:\n{}",
15080 out.sql
15081 );
15082 }
15083
15084 #[test]
15085 fn test_top_level_cast_to_unknown_type_names_full_type() {
15086 let schema = make_schema();
15091 let ast = parse::parse("SELECT <default::Ghost>$name").unwrap();
15092 match ir::compile(&ast, &schema) {
15093 Ok(_) => panic!("expected compile error for unknown type"),
15094 Err(e) => {
15095 let msg = e.to_string();
15096 assert!(
15097 msg.contains("unknown type 'default::Ghost'"),
15098 "expected full type name in error, got: {msg}",
15099 );
15100 }
15101 }
15102 }
15103
15104 #[test]
15105 fn test_structural_tuple_cast_unnamed_resolves_to_jsonb() {
15106 let out = compile_and_emit("SELECT <tuple<str, bool>>$p");
15107 assert!(out.sql.contains("($1)::jsonb"), "got:\n{}", out.sql);
15108 }
15109
15110 #[test]
15111 fn test_jsonb_to_uuid_cast_extracts_via_text() {
15112 let out = compile_and_emit("SELECT <uuid>(<json>$p)");
15114 assert!(out.sql.contains("#>> '{}'"), "got:\n{}", out.sql);
15115 assert!(out.sql.contains("::uuid"), "got:\n{}", out.sql);
15116 }
15117
15118 #[test]
15119 fn test_jsonb_to_datetime_cast_extracts_via_text() {
15120 let out = compile_and_emit("SELECT <datetime>(<json>$p)");
15121 assert!(out.sql.contains("#>> '{}'"), "got:\n{}", out.sql);
15122 assert!(out.sql.contains("::timestamptz"), "got:\n{}", out.sql);
15123 }
15124
15125 #[test]
15126 fn test_jsonb_to_duration_cast_extracts_via_text() {
15127 let out = compile_and_emit("SELECT <duration>(<json>$p)");
15128 assert!(out.sql.contains("#>> '{}'"), "got:\n{}", out.sql);
15129 assert!(out.sql.contains("::interval"), "got:\n{}", out.sql);
15130 }
15131
15132 #[test]
15133 fn test_jsonb_to_array_cast_unpacks_each_element() {
15134 let out = compile_and_emit("SELECT <array<int64>>(<json>$p)");
15135 assert!(out.sql.contains("jsonb_array_elements("), "got:\n{}", out.sql);
15136 assert!(out.sql.contains("#>> '{}'"), "got:\n{}", out.sql);
15137 assert!(out.sql.contains("::int8"), "got:\n{}", out.sql);
15138 }
15139
15140 #[test]
15141 fn test_non_jsonb_cast_is_unaffected_by_jsonb_extraction() {
15142 let out = compile_and_emit("SELECT <uuid>$p");
15145 assert!(!out.sql.contains("#>>"), "got:\n{}", out.sql);
15146 assert!(out.sql.contains("::uuid"), "got:\n{}", out.sql);
15147 }
15148
15149 #[test]
15150 fn test_datetime_plus_duration_type_checks() {
15151 let out = compile_and_emit("SELECT <datetime>$p + <duration>$d");
15156 assert!(out.sql.contains(" + "), "got:\n{}", out.sql);
15157 }
15158
15159 #[test]
15160 fn test_duration_plus_datetime_type_checks_either_order() {
15161 let out = compile_and_emit("SELECT <duration>$d + <datetime>$p");
15162 assert!(out.sql.contains(" + "), "got:\n{}", out.sql);
15163 }
15164
15165 #[test]
15166 fn test_local_date_plus_duration_type_checks() {
15167 let out = compile_and_emit("SELECT <cal::local_date>$p + <duration>$d");
15168 assert!(out.sql.contains(" + "), "got:\n{}", out.sql);
15169 }
15170
15171 #[test]
15172 fn test_datetime_minus_duration_type_checks() {
15173 let out = compile_and_emit("SELECT <datetime>$p - <duration>$d");
15174 assert!(out.sql.contains(" - "), "got:\n{}", out.sql);
15175 }
15176
15177 #[test]
15178 fn test_local_time_plus_duration_type_checks() {
15179 let out = compile_and_emit("SELECT <cal::local_time>$p + <duration>$d");
15180 assert!(out.sql.contains(" + "), "got:\n{}", out.sql);
15181 }
15182
15183 #[test]
15184 fn test_datetime_plus_duration_only_allowed_for_add_and_sub() {
15185 let schema = make_schema();
15188 let ast = parse::parse("SELECT <datetime>$p = <duration>$d").unwrap();
15189 match ir::compile(&ast, &schema) {
15190 Err(err) => assert!(format!("{err}").contains("cannot be applied"), "got: {err}"),
15191 Ok(_) => panic!("expected a compile error"),
15192 }
15193 }
15194
15195 #[test]
15196 fn test_int_and_float_mix_is_allowed() {
15197 let out = compile_and_emit("SELECT <int16>1 + <float32>2.0");
15201 assert!(out.sql.contains(" + "), "got:\n{}", out.sql);
15202 }
15203
15204 #[test]
15205 fn test_int_and_decimal_mix_is_allowed() {
15206 let out = compile_and_emit("SELECT <int64>1 + <decimal>2.0");
15210 assert!(out.sql.contains(" + "), "got:\n{}", out.sql);
15211 }
15212
15213 #[test]
15214 fn test_float_and_decimal_mix_still_rejected() {
15215 let schema = make_schema();
15219 let ast = parse::parse("SELECT <decimal>$p + <float64>$n").unwrap();
15220 match ir::compile(&ast, &schema) {
15221 Err(err) => assert!(format!("{err}").contains("cannot be applied"), "got: {err}"),
15222 Ok(_) => panic!("expected a compile error"),
15223 }
15224 }
15225
15226 #[test]
15227 fn test_unknown_pointer_suggests_a_close_match() {
15228 let schema = make_schema();
15232 let ast = parse::parse("SELECT Person { nam }").unwrap();
15233 match ir::compile(&ast, &schema) {
15234 Err(err) => assert_eq!(
15235 format!("{err}"),
15236 "object type 'default::Person' has no link or property 'nam'. Did you mean 'name'?"
15237 ),
15238 Ok(_) => panic!("expected a compile error"),
15239 }
15240 }
15241
15242 #[test]
15243 fn test_unrelated_unknown_pointer_gets_no_suggestion() {
15244 let schema = make_schema();
15247 let ast = parse::parse("SELECT Person { xyzxyzxyz }").unwrap();
15248 match ir::compile(&ast, &schema) {
15249 Err(err) => {
15250 let msg = format!("{err}");
15251 assert!(msg.contains("has no link or property 'xyzxyzxyz'"), "got: {msg}");
15252 assert!(!msg.contains("Did you mean"), "got: {msg}");
15253 }
15254 Ok(_) => panic!("expected a compile error"),
15255 }
15256 }
15257
15258 #[test]
15259 fn test_multilink_sourced_backlink_exists_filter_compiles() {
15260 let out = compile_and_emit("SELECT Post filter exists .<posts[is Person]");
15270 assert!(out.sql.contains("EXISTS"), "got:\n{}", out.sql);
15271 }
15272
15273 #[test]
15274 fn test_backlink_as_computed_shape_pointer_compiles() {
15275 let out = compile_and_emit("SELECT Post { title, authors := .<posts[is Person] { name } }");
15288 assert!(out.sql.contains("array_agg(ROW("), "got:\n{}", out.sql);
15289 }
15290
15291 fn schema_with_exclusive_posts() -> SchemaDescriptor {
15292 let mut schema = make_schema();
15293 for td in schema.types.iter_mut().filter(|td| td.name == "Person") {
15294 for ml in td.multilinks.iter_mut().filter(|ml| ml.name == "posts") {
15295 ml.is_exclusive = true;
15296 }
15297 }
15298 schema
15299 }
15300
15301 #[test]
15302 fn test_backlink_through_an_exclusive_multilink_is_single() {
15303 let out = compile_and_emit_with(
15304 "SELECT Post { title, author := .<posts[is Person] { name } }",
15305 &schema_with_exclusive_posts(),
15306 );
15307 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
15308 panic!("expected an object root, got {:?}", out.shape.root)
15309 };
15310 assert!(
15311 matches!(pointers.last(), Some(ShapeNode::Object { name, .. }) if name == "author"),
15312 "got {pointers:?}"
15313 );
15314 }
15315
15316 #[test]
15317 fn test_a_path_through_an_exclusive_backlink_is_single() {
15318 let out = compile_and_emit_with(
15319 "SELECT Post { title, author_name := .<posts[is Person].name }",
15320 &schema_with_exclusive_posts(),
15321 );
15322 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
15323 panic!("expected an object root, got {:?}", out.shape.root)
15324 };
15325 assert!(
15326 matches!(pointers.last(), Some(ShapeNode::Scalar { name, .. }) if name == "author_name"),
15327 "got {pointers:?}"
15328 );
15329 assert!(!out.sql.contains("ARRAY("), "got:\n{}", out.sql);
15330 }
15331
15332 #[test]
15333 fn test_structural_tuple_cast_named_resolves_to_jsonb() {
15334 let out = compile_and_emit("SELECT <tuple<x: float64, y: float64>>$p");
15335 assert!(out.sql.contains("($1)::jsonb"), "got:\n{}", out.sql);
15336 }
15337
15338 #[test]
15339 fn test_structural_tuple_cast_nested_resolves_to_jsonb() {
15340 let out = compile_and_emit("SELECT <tuple<point: tuple<x: float64, y: float64>, label: str>>$p");
15341 assert!(out.sql.contains("($1)::jsonb"), "got:\n{}", out.sql);
15342 }
15343
15344 #[test]
15345 fn test_nominal_named_tuple_cast_resolves_to_jsonb() {
15346 let mut schema = make_schema();
15347 schema.named_tuples.push(NamedTupleDescriptor {
15348 name: "Point".into(),
15349 module: "default".into(),
15350 members: vec![],
15351 });
15352 let out = compile_and_emit_with("SELECT <default::Point>$p", &schema);
15353 assert!(out.sql.contains("($1)::jsonb"), "got:\n{}", out.sql);
15354 }
15355
15356 #[test]
15357 fn test_array_literal_cast_resolves_to_native_pg_array_not_jsonb() {
15358 let out = compile_and_emit("SELECT <array<str>>['foo', 'bar']");
15362 assert!(
15363 out.sql.contains("::text[]") || out.sql.contains("ARRAY["),
15364 "got:\n{}",
15365 out.sql
15366 );
15367 assert!(
15368 !out.sql.contains("jsonb"),
15369 "arrays must not use jsonb, got:\n{}",
15370 out.sql
15371 );
15372 }
15373
15374 #[test]
15375 fn test_array_literal_cast_applies_per_element_cast() {
15376 let out = compile_and_emit("SELECT <array<int64>>['1', '2']");
15380 assert!(out.sql.contains("ARRAY[('1')::int8, ('2')::int8]"), "got:\n{}", out.sql);
15381 }
15382
15383 #[test]
15384 fn test_array_param_cast_uses_direct_suffix_cast() {
15385 let out = compile_and_emit("SELECT <array<int64>>$p");
15386 assert!(out.sql.contains("::int8[]"), "got:\n{}", out.sql);
15387 }
15388
15389 #[test]
15390 fn test_array_of_named_tuple_element_casts_to_jsonb_array() {
15391 let out = compile_and_emit("SELECT <array<tuple<x: float64, y: float64>>>$p");
15395 assert!(out.sql.contains("::jsonb[]"), "got:\n{}", out.sql);
15396 }
15397
15398 #[test]
15399 fn test_contains_on_array_literal_cast_uses_array_overload_not_strpos() {
15400 let out = compile_and_emit("SELECT contains(<array<str>>[1, 2], '2')");
15406 assert!(out.sql.contains("@> ARRAY["), "got:\n{}", out.sql);
15407 assert!(
15408 !out.sql.contains("strpos"),
15409 "must not fall back to the str/str overload, got:\n{}",
15410 out.sql
15411 );
15412 }
15413
15414 #[test]
15415 fn test_nested_array_type_rejected_at_parse_time() {
15416 match parse::parse("SELECT <array<array<str>>>$p") {
15417 Ok(_) => panic!("expected parse error for nested array type"),
15418 Err(e) => assert!(e.to_string().contains("nested arrays are not supported"), "got: {}", e),
15419 }
15420 }
15421
15422 #[test]
15423 fn test_array_cast_in_computed_shape_field_schema_bound_context() {
15424 let out = compile_and_emit("SELECT Person { name, tags := <array<str>>['a', 'b'] }");
15427 assert!(out.sql.contains("ARRAY[('a')::text, ('b')::text]"), "got:\n{}", out.sql);
15428 }
15429
15430 #[test]
15431 fn test_nominal_named_tuple_cast_shape_carries_real_members() {
15432 use crate::schema::{TupleMemberDescriptor, TupleMemberKind};
15433 let mut schema = make_schema();
15434 schema.named_tuples.push(NamedTupleDescriptor {
15435 name: "Point".into(),
15436 module: "default".into(),
15437 members: vec![
15438 TupleMemberDescriptor {
15439 name: Some("x".into()),
15440 kind: TupleMemberKind::Scalar {
15441 pg_type: "float8".into(),
15442 },
15443 },
15444 TupleMemberDescriptor {
15445 name: Some("y".into()),
15446 kind: TupleMemberKind::Scalar {
15447 pg_type: "float8".into(),
15448 },
15449 },
15450 ],
15451 });
15452 let out = compile_and_emit_with("SELECT <default::Point>$p", &schema);
15453 match &out.shape.root {
15454 crate::query::ShapeNode::NamedTuple { type_name, members, .. } => {
15455 assert_eq!(type_name.as_deref(), Some("default::Point"));
15456 let members = members.as_ref().expect("expected resolved members");
15457 assert_eq!(members.len(), 2);
15458 assert_eq!(members[0].key.as_deref(), Some("x"));
15459 assert_eq!(members[1].key.as_deref(), Some("y"));
15460 }
15461 other => panic!("expected ShapeNode::NamedTuple, got {other:?}"),
15462 }
15463 }
15464
15465 #[test]
15466 fn test_structural_tuple_property_read_shape_carries_real_members() {
15467 use crate::schema::{TupleMemberDescriptor, TupleMemberKind};
15468 let schema = SchemaDescriptor {
15469 types: vec![TypeDescriptor {
15470 name: "Person".into(),
15471 module: "default".into(),
15472 table: "Person".into(),
15473 abstract_: false,
15474 materialized: false,
15475 description: None,
15476 parents: vec![],
15477 interfaces: vec![],
15478 bases: vec![],
15479 properties: vec![PropertyDescriptor {
15480 name: "address".into(),
15481 pg_type: "jsonb".into(),
15482 nullable: true,
15483 default_sql: None,
15484 default_pyql: None,
15485 description: None,
15486 check_constraints: vec![],
15487 is_exclusive: false,
15488 is_pk: false,
15489 is_readonly: false,
15490 rewrites: vec![],
15491 tuple_members: Some(vec![
15492 TupleMemberDescriptor {
15493 name: Some("street".into()),
15494 kind: TupleMemberKind::Scalar { pg_type: "text".into() },
15495 },
15496 TupleMemberDescriptor {
15497 name: Some("zip".into()),
15498 kind: TupleMemberKind::Scalar { pg_type: "text".into() },
15499 },
15500 ]),
15501 column_type: None,
15502 }],
15503 links: vec![],
15504 multilinks: vec![],
15505 computed: vec![],
15506 constraints: vec![],
15507 indexes: vec![],
15508 partition: None,
15509 vector_indexes: vec![],
15510 search_indexes: vec![],
15511 triggers: vec![],
15512 junction: false,
15513 signals: vec![],
15514 }],
15515 scalars: vec![],
15516 enums: vec![],
15517 named_tuples: vec![],
15518 globals: vec![],
15519 functions: vec![],
15520 aliases: vec![],
15521 channels: vec![],
15522 ..Default::default()
15523 };
15524 let out = compile_and_emit_with("SELECT Person { address }", &schema);
15525 assert!(out.sql.contains("::jsonb"), "got:\n{}", out.sql);
15526 match &out.shape.root {
15527 crate::query::ShapeNode::Object { pointers, .. } => {
15528 let address = pointers
15529 .iter()
15530 .find(|p| matches!(p, crate::query::ShapeNode::NamedTuple { name, .. } if name == "address"))
15531 .expect("expected address pointer in shape");
15532 match address {
15533 crate::query::ShapeNode::NamedTuple { type_name, members, .. } => {
15534 assert_eq!(*type_name, None);
15535 let members = members.as_ref().expect("expected resolved members");
15536 assert_eq!(members.len(), 2);
15537 assert_eq!(members[0].key.as_deref(), Some("street"));
15538 assert_eq!(members[1].key.as_deref(), Some("zip"));
15539 }
15540 other => panic!("expected NamedTuple, got {other:?}"),
15541 }
15542 }
15543 other => panic!("expected ShapeNode::Object, got {other:?}"),
15544 }
15545 }
15546
15547 #[test]
15548 fn test_bare_path_select_structural_tuple_property_shape_carries_real_members() {
15549 use crate::schema::{TupleMemberDescriptor, TupleMemberKind};
15559 let schema = SchemaDescriptor {
15560 types: vec![TypeDescriptor {
15561 name: "Person".into(),
15562 module: "default".into(),
15563 table: "Person".into(),
15564 abstract_: false,
15565 materialized: false,
15566 description: None,
15567 parents: vec![],
15568 interfaces: vec![],
15569 bases: vec![],
15570 properties: vec![PropertyDescriptor {
15571 name: "address".into(),
15572 pg_type: "jsonb".into(),
15573 nullable: true,
15574 default_sql: None,
15575 default_pyql: None,
15576 description: None,
15577 check_constraints: vec![],
15578 is_exclusive: false,
15579 is_pk: false,
15580 is_readonly: false,
15581 rewrites: vec![],
15582 tuple_members: Some(vec![
15583 TupleMemberDescriptor {
15584 name: Some("street".into()),
15585 kind: TupleMemberKind::Scalar { pg_type: "text".into() },
15586 },
15587 TupleMemberDescriptor {
15588 name: Some("zip".into()),
15589 kind: TupleMemberKind::Scalar { pg_type: "text".into() },
15590 },
15591 ]),
15592 column_type: None,
15593 }],
15594 links: vec![],
15595 multilinks: vec![],
15596 computed: vec![],
15597 constraints: vec![],
15598 indexes: vec![],
15599 partition: None,
15600 vector_indexes: vec![],
15601 search_indexes: vec![],
15602 triggers: vec![],
15603 junction: false,
15604 signals: vec![],
15605 }],
15606 scalars: vec![],
15607 enums: vec![],
15608 named_tuples: vec![],
15609 globals: vec![],
15610 functions: vec![],
15611 aliases: vec![],
15612 channels: vec![],
15613 ..Default::default()
15614 };
15615 let out = compile_and_emit_with("SELECT Person.address", &schema);
15616 match &out.shape.root {
15617 crate::query::ShapeNode::NamedTuple { type_name, members, .. } => {
15618 assert_eq!(*type_name, None);
15619 let members = members.as_ref().expect("expected resolved members");
15620 assert_eq!(members.len(), 2);
15621 assert_eq!(members[0].key.as_deref(), Some("street"));
15622 assert_eq!(members[1].key.as_deref(), Some("zip"));
15623 }
15624 other => panic!("expected ShapeNode::NamedTuple, got {other:?}"),
15625 }
15626 }
15627
15628 #[test]
15629 fn test_path_traversal_into_structural_tuple_property() {
15630 use crate::schema::{TupleMemberDescriptor, TupleMemberKind};
15631 let schema = SchemaDescriptor {
15632 types: vec![TypeDescriptor {
15633 name: "Person".into(),
15634 module: "default".into(),
15635 table: "Person".into(),
15636 abstract_: false,
15637 materialized: false,
15638 description: None,
15639 parents: vec![],
15640 interfaces: vec![],
15641 bases: vec![],
15642 properties: vec![PropertyDescriptor {
15643 name: "address".into(),
15644 pg_type: "jsonb".into(),
15645 nullable: true,
15646 default_sql: None,
15647 default_pyql: None,
15648 description: None,
15649 check_constraints: vec![],
15650 is_exclusive: false,
15651 is_pk: false,
15652 is_readonly: false,
15653 rewrites: vec![],
15654 tuple_members: Some(vec![
15655 TupleMemberDescriptor {
15656 name: Some("street".into()),
15657 kind: TupleMemberKind::Scalar { pg_type: "text".into() },
15658 },
15659 TupleMemberDescriptor {
15660 name: Some("zip".into()),
15661 kind: TupleMemberKind::Scalar { pg_type: "text".into() },
15662 },
15663 ]),
15664 column_type: None,
15665 }],
15666 links: vec![],
15667 multilinks: vec![],
15668 computed: vec![],
15669 constraints: vec![],
15670 indexes: vec![],
15671 partition: None,
15672 vector_indexes: vec![],
15673 search_indexes: vec![],
15674 triggers: vec![],
15675 junction: false,
15676 signals: vec![],
15677 }],
15678 scalars: vec![],
15679 enums: vec![],
15680 named_tuples: vec![],
15681 globals: vec![],
15682 functions: vec![],
15683 aliases: vec![],
15684 channels: vec![],
15685 ..Default::default()
15686 };
15687 let out = compile_and_emit_with("SELECT default::Person.address.street", &schema);
15693 assert!(out.sql.contains("\"address\"->'street'"), "got:\n{}", out.sql);
15694 }
15695
15696 fn make_schema_with_an_array_of_tuples_property() -> SchemaDescriptor {
15700 use crate::schema::{TupleMemberDescriptor, TupleMemberKind};
15701 SchemaDescriptor {
15702 types: vec![TypeDescriptor {
15703 name: "Person".into(),
15704 module: "default".into(),
15705 table: "Person".into(),
15706 abstract_: false,
15707 materialized: false,
15708 description: None,
15709 parents: vec![],
15710 interfaces: vec![],
15711 bases: vec![],
15712 properties: vec![PropertyDescriptor {
15713 name: "addresses".into(),
15714 pg_type: "jsonb[]".into(),
15715 nullable: true,
15716 default_sql: None,
15717 default_pyql: None,
15718 description: None,
15719 check_constraints: vec![],
15720 is_exclusive: false,
15721 is_pk: false,
15722 is_readonly: false,
15723 rewrites: vec![],
15724 tuple_members: Some(vec![
15725 TupleMemberDescriptor {
15726 name: Some("street".into()),
15727 kind: TupleMemberKind::Scalar { pg_type: "text".into() },
15728 },
15729 TupleMemberDescriptor {
15730 name: Some("zip".into()),
15731 kind: TupleMemberKind::Scalar { pg_type: "text".into() },
15732 },
15733 ]),
15734 column_type: None,
15735 }],
15736 links: vec![],
15737 multilinks: vec![],
15738 computed: vec![],
15739 constraints: vec![],
15740 indexes: vec![],
15741 partition: None,
15742 vector_indexes: vec![],
15743 search_indexes: vec![],
15744 triggers: vec![],
15745 junction: false,
15746 signals: vec![],
15747 }],
15748 scalars: vec![],
15749 enums: vec![],
15750 named_tuples: vec![],
15751 globals: vec![],
15752 functions: vec![],
15753 aliases: vec![],
15754 channels: vec![],
15755 ..Default::default()
15756 }
15757 }
15758
15759 #[test]
15760 fn test_array_of_tuples_property_read_shape_is_an_array_of_named_tuples() {
15761 let schema = make_schema_with_an_array_of_tuples_property();
15767 let out = compile_and_emit_with("SELECT Person { addresses }", &schema);
15768 assert!(out.sql.contains("::jsonb[]"), "got:\n{}", out.sql);
15769 let crate::query::ShapeNode::Object { pointers, .. } = &out.shape.root else {
15770 panic!("expected ShapeNode::Object, got {:?}", out.shape.root)
15771 };
15772 let addresses = pointers
15773 .iter()
15774 .find(|p| matches!(p, crate::query::ShapeNode::Array { name, .. } if name == "addresses"))
15775 .expect("expected addresses pointer in shape");
15776 let crate::query::ShapeNode::Array { element, .. } = addresses else {
15777 unreachable!("matched as an array just above")
15778 };
15779 match element.as_ref() {
15780 crate::query::ShapeNode::NamedTuple {
15781 type_name,
15782 members,
15783 position,
15784 ..
15785 } => {
15786 assert_eq!(*type_name, None);
15787 assert_eq!(*position, 0);
15788 let members = members.as_ref().expect("expected resolved members");
15789 assert_eq!(members.len(), 2);
15790 assert_eq!(members[0].key.as_deref(), Some("street"));
15791 assert_eq!(members[1].key.as_deref(), Some("zip"));
15792 }
15793 other => panic!("expected NamedTuple element, got {other:?}"),
15794 }
15795 }
15796
15797 #[test]
15798 fn test_a_path_to_an_optional_property_drops_the_rows_without_one() {
15799 let out = compile_and_emit("SELECT Person.age");
15803 assert!(out.sql.contains(r#"("t0"."age" IS NOT NULL)"#), "got:\n{}", out.sql);
15804 }
15805
15806 #[test]
15807 fn test_a_path_to_a_required_property_needs_no_guard() {
15808 let out = compile_and_emit("SELECT Person.name");
15811 assert!(!out.sql.contains("IS NOT NULL"), "got:\n{}", out.sql);
15812 }
15813
15814 #[test]
15815 fn test_a_path_to_an_optional_property_keeps_its_own_filter_too() {
15816 let out = compile_and_emit("SELECT Person.age FILTER Person.name = $n");
15817 assert!(
15818 out.sql
15819 .contains(r#"WHERE (("t0"."name" = $1) AND ("t0"."age" IS NOT NULL))"#),
15820 "got:\n{}",
15821 out.sql
15822 );
15823 }
15824
15825 #[test]
15826 fn test_a_set_of_array_valued_properties_aggregates_as_records() {
15827 let out = compile_and_emit_with(
15832 "SELECT Company { permissions := .<company[is Person].perms }",
15833 &schema_with_array_property(),
15834 );
15835 assert!(out.sql.contains(r#"ROW("t2"."perms")"#), "got:\n{}", out.sql);
15836 let crate::query::ShapeNode::Object { pointers, .. } = &out.shape.root else {
15837 panic!("expected ShapeNode::Object, got {:?}", out.shape.root)
15838 };
15839 let permissions = pointers
15840 .iter()
15841 .find(|p| matches!(p, crate::query::ShapeNode::Array { name, .. } if name == "permissions"))
15842 .expect("expected the permissions pointer");
15843 let crate::query::ShapeNode::Array { element, .. } = permissions else {
15844 unreachable!("matched as an array just above")
15845 };
15846 match element.as_ref() {
15848 crate::query::ShapeNode::Scalar { position, .. } => assert_eq!(*position, 0),
15849 other => panic!("expected the array at position 0 of its record, got {other:?}"),
15850 }
15851 }
15852
15853 #[test]
15854 fn test_a_set_of_array_of_tuples_properties_keeps_the_member_shape() {
15855 let mut schema = make_schema_with_an_array_of_tuples_property();
15856 schema.types.push(TypeDescriptor {
15858 name: "Company".into(),
15859 module: "default".into(),
15860 table: "Company".into(),
15861 abstract_: false,
15862 materialized: false,
15863 description: None,
15864 parents: vec![],
15865 interfaces: vec![],
15866 bases: vec![],
15867 properties: vec![],
15868 links: vec![],
15869 multilinks: vec![],
15870 computed: vec![],
15871 constraints: vec![],
15872 indexes: vec![],
15873 partition: None,
15874 vector_indexes: vec![],
15875 search_indexes: vec![],
15876 triggers: vec![],
15877 junction: false,
15878 signals: vec![],
15879 });
15880 schema.types[0].links.push(crate::schema::LinkDescriptor {
15881 name: "company".into(),
15882 target: "default::Company".into(),
15883 nullable: true,
15884 description: None,
15885 default_pyql: None,
15886 is_exclusive: false,
15887 is_readonly: false,
15888 rewrites: vec![],
15889 on_delete: vec![],
15890 through: None,
15891 });
15892 let out = compile_and_emit_with(
15893 "SELECT Company { addresses := .<company[is Person].addresses }",
15894 &schema,
15895 );
15896 assert!(out.sql.contains(r#"ROW("t2"."addresses")"#), "got:\n{}", out.sql);
15897 let crate::query::ShapeNode::Object { pointers, .. } = &out.shape.root else {
15898 panic!("expected ShapeNode::Object, got {:?}", out.shape.root)
15899 };
15900 let addresses = pointers
15901 .iter()
15902 .find(|p| matches!(p, crate::query::ShapeNode::Array { name, .. } if name == "addresses"))
15903 .expect("expected the addresses pointer");
15904 let crate::query::ShapeNode::Array { element, .. } = addresses else {
15905 unreachable!("matched as an array just above")
15906 };
15907 match element.as_ref() {
15910 crate::query::ShapeNode::Array { element, position, .. } => {
15911 assert_eq!(*position, 0);
15912 assert!(
15913 matches!(
15914 element.as_ref(),
15915 crate::query::ShapeNode::NamedTuple { members: Some(_), .. }
15916 ),
15917 "expected named-tuple elements, got {element:?}"
15918 );
15919 }
15920 other => panic!("expected an array per element, got {other:?}"),
15921 }
15922 }
15923
15924 #[test]
15925 fn test_an_optional_property_aggregated_into_an_array_drops_its_empties() {
15926 let out = compile_and_emit("SELECT Company { ages := .<company[is Person].age }");
15930 assert!(out.sql.contains(r#"("t2"."age" IS NOT NULL)"#), "got:\n{}", out.sql);
15931 }
15932
15933 #[test]
15934 fn test_bare_path_select_array_of_tuples_property_shape() {
15935 let schema = make_schema_with_an_array_of_tuples_property();
15939 let out = compile_and_emit_with("SELECT Person.addresses", &schema);
15940 assert!(out.sql.contains("ROW("), "got:\n{}", out.sql);
15941 match &out.shape.root {
15942 crate::query::ShapeNode::Array { element, position, .. } => {
15943 assert_eq!(*position, 0);
15944 match element.as_ref() {
15945 crate::query::ShapeNode::NamedTuple { members, .. } => {
15946 let members = members.as_ref().expect("expected resolved members");
15947 assert_eq!(members.len(), 2);
15948 assert_eq!(members[0].key.as_deref(), Some("street"));
15949 }
15950 other => panic!("expected NamedTuple element, got {other:?}"),
15951 }
15952 }
15953 other => panic!("expected ShapeNode::Array, got {other:?}"),
15954 }
15955 }
15956
15957 #[test]
15958 fn test_a_with_bound_path_select_keeps_the_shape_the_path_itself_has() {
15959 let schema = make_schema_with_an_array_of_tuples_property();
15963 let out = compile_and_emit_with("WITH entries := Person.addresses SELECT entries", &schema);
15964 match &out.shape.root {
15965 crate::query::ShapeNode::Array { element, .. } => assert!(
15966 matches!(
15967 element.as_ref(),
15968 crate::query::ShapeNode::NamedTuple { members: Some(_), .. }
15969 ),
15970 "expected a named-tuple element, got {element:?}"
15971 ),
15972 other => panic!("expected ShapeNode::Array, got {other:?}"),
15973 }
15974 }
15975
15976 #[test]
15977 fn test_path_traversal_into_array_of_tuples_property_rejected() {
15978 let schema = make_schema_with_an_array_of_tuples_property();
15981 let ast = parse::parse("SELECT default::Person.addresses.street").unwrap();
15982 match ir::compile(&ast, &schema) {
15983 Ok(_) => panic!("expected error for a field access on an array of tuples"),
15984 Err(e) => assert!(
15985 e.to_string()
15986 .contains("'addresses' is a scalar property, not a link — cannot traverse further"),
15987 "got: {}",
15988 e
15989 ),
15990 }
15991 }
15992
15993 #[test]
15994 fn test_a_tuple_literal_carries_the_members_it_names() {
15995 let out = compile_and_emit("SELECT (amount := 9.99, note := 'x')");
16000 match &out.shape.root {
16001 crate::query::ShapeNode::NamedTuple { members, .. } => {
16002 let members = members.as_ref().expect("a literal names its own members");
16003 let keys: Vec<_> = members.iter().map(|m| m.key.as_deref()).collect();
16004 assert_eq!(keys, vec![Some("amount"), Some("note")]);
16005 }
16006 other => panic!("expected ShapeNode::NamedTuple, got {other:?}"),
16007 }
16008 }
16009
16010 #[test]
16011 fn test_a_tuple_literal_member_that_is_itself_a_tuple_recurses() {
16012 let out = compile_and_emit("SELECT (origin := (x := 1, y := 2), label := 'a')");
16013 let crate::query::ShapeNode::NamedTuple { members, .. } = &out.shape.root else {
16014 panic!("expected ShapeNode::NamedTuple, got {:?}", out.shape.root)
16015 };
16016 let members = members.as_ref().expect("a literal names its own members");
16017 match &members[0].kind {
16018 crate::query::JsonMemberKind::Tuple { members, .. } => {
16019 let keys: Vec<_> = members.iter().map(|m| m.key.as_deref()).collect();
16020 assert_eq!(keys, vec![Some("x"), Some("y")]);
16021 }
16022 other => panic!("expected a nested tuple member, got {other:?}"),
16023 }
16024 }
16025
16026 #[test]
16027 fn test_a_tuple_literal_member_holding_an_enum_keeps_its_type() {
16028 let mut schema = make_schema();
16030 schema.enums.push(crate::schema::EnumDescriptor {
16031 name: "Gender".into(),
16032 module: "default".into(),
16033 members: vec!["Male".into(), "Female".into()],
16034 });
16035 let out = compile_and_emit_with("SELECT (shade := default::Gender.Male, label := 'a')", &schema);
16036 let crate::query::ShapeNode::NamedTuple { members, .. } = &out.shape.root else {
16037 panic!("expected ShapeNode::NamedTuple, got {:?}", out.shape.root)
16038 };
16039 let members = members.as_ref().expect("a literal names its own members");
16040 match &members[0].kind {
16041 crate::query::JsonMemberKind::Enum { enum_type } => assert_eq!(enum_type, "default::Gender"),
16043 other => panic!("expected an enum member, got {other:?}"),
16044 }
16045 }
16046
16047 #[test]
16048 fn test_a_free_object_literal_carries_no_member_plan() {
16049 let out = compile_and_emit("SELECT Person { meta := { foo := 'bar' } }");
16054 let crate::query::ShapeNode::Object { pointers, .. } = &out.shape.root else {
16055 panic!("expected ShapeNode::Object, got {:?}", out.shape.root)
16056 };
16057 let meta = pointers
16058 .iter()
16059 .find(|p| matches!(p, crate::query::ShapeNode::NamedTuple { name, .. } if name == "meta"))
16060 .expect("expected the meta pointer");
16061 match meta {
16062 crate::query::ShapeNode::NamedTuple {
16063 members,
16064 is_free_object,
16065 ..
16066 } => {
16067 assert!(is_free_object);
16068 assert!(members.is_none(), "got {members:?}");
16069 }
16070 other => panic!("expected ShapeNode::NamedTuple, got {other:?}"),
16071 }
16072 }
16073
16074 #[test]
16075 fn test_structural_tuple_cast_shape_carries_real_members() {
16076 let out = compile_and_emit("SELECT <tuple<street: str, zip: str>>$p");
16077 match &out.shape.root {
16078 crate::query::ShapeNode::NamedTuple { type_name, members, .. } => {
16079 assert_eq!(*type_name, None);
16080 let members = members.as_ref().expect("expected resolved members");
16081 assert_eq!(members.len(), 2);
16082 assert_eq!(members[0].key.as_deref(), Some("street"));
16083 assert_eq!(members[1].key.as_deref(), Some("zip"));
16084 }
16085 other => panic!("expected ShapeNode::NamedTuple, got {other:?}"),
16086 }
16087 }
16088
16089 #[test]
16090 fn test_array_of_tuples_cast_shape_is_an_array_of_named_tuples() {
16091 let out = compile_and_emit("SELECT <array<tuple<street: str, zip: str>>>$p");
16095 assert!(out.sql.contains("::jsonb[]"), "got:\n{}", out.sql);
16096 match &out.shape.root {
16097 crate::query::ShapeNode::Array { element, position, .. } => {
16098 assert_eq!(*position, 0);
16099 match element.as_ref() {
16100 crate::query::ShapeNode::NamedTuple { members, position, .. } => {
16101 assert_eq!(*position, 0);
16102 let members = members.as_ref().expect("expected resolved members");
16103 assert_eq!(members.len(), 2);
16104 assert_eq!(members[0].key.as_deref(), Some("street"));
16105 assert_eq!(members[1].key.as_deref(), Some("zip"));
16106 }
16107 other => panic!("expected NamedTuple element, got {other:?}"),
16108 }
16109 }
16110 other => panic!("expected ShapeNode::Array, got {other:?}"),
16111 }
16112 }
16113
16114 #[test]
16115 fn test_tuple_cast_mixed_named_and_unnamed_elements_rejected() {
16116 match parse::parse("SELECT <tuple<x: float64, bool>>$p") {
16117 Ok(_) => panic!("expected parse error for mixed named/unnamed tuple elements"),
16118 Err(e) => assert!(e.to_string().contains("all named or all unnamed"), "got: {}", e),
16119 }
16120 }
16121
16122 #[test]
16123 fn test_is_with_tuple_type_rejected() {
16124 let schema = make_schema();
16125 let ast = parse::parse("SELECT Person FILTER Person is tuple<x: float64, y: float64>").unwrap();
16126 match ir::compile(&ast, &schema) {
16127 Ok(_) => panic!("expected error for IS with a tuple type"),
16128 Err(e) => {
16129 assert!(
16130 e.to_string().contains("cannot use IS with a tuple or array type"),
16131 "got: {}",
16132 e
16133 );
16134 }
16135 }
16136 }
16137
16138 #[test]
16139 fn test_is_with_array_type_rejected() {
16140 let schema = make_schema();
16141 let ast = parse::parse("SELECT Person FILTER Person is array<str>").unwrap();
16142 match ir::compile(&ast, &schema) {
16143 Ok(_) => panic!("expected error for IS with an array type"),
16144 Err(e) => {
16145 assert!(
16146 e.to_string().contains("cannot use IS with a tuple or array type"),
16147 "got: {}",
16148 e
16149 );
16150 }
16151 }
16152 }
16153
16154 #[test]
16155 fn test_tuple_index_on_non_literal_falls_back_to_jsonb_index() {
16156 let out = compile_and_emit("SELECT (<tuple<int64, str>>('1', 3)).1");
16160 assert!(out.sql.contains("->1"), "got:\n{}", out.sql);
16161 }
16162
16163 #[test]
16164 fn test_tuple_index_out_of_bounds_on_cast_target_errors_at_compile_time() {
16165 let ast = parse::parse("SELECT (<tuple<int64, str>>('1', 3)).2").unwrap();
16171 let schema = make_schema();
16172 match ir::compile(&ast, &schema) {
16173 Ok(_) => panic!("expected out-of-bounds tuple index error"),
16174 Err(e) => {
16175 assert!(
16176 e.to_string()
16177 .contains("2 is not a member of tuple<std::int64, std::str>"),
16178 "got: {}",
16179 e
16180 );
16181 }
16182 }
16183 }
16184
16185 #[test]
16186 fn test_positional_tuple_literal_cast_to_tuple_type_compiles() {
16187 let out = compile_and_emit("SELECT <tuple<int64, str>>(1, 'x')");
16191 assert!(
16192 out.sql.contains("jsonb_build_array((1)::int8, ('x')::text)"),
16193 "got:\n{}",
16194 out.sql
16195 );
16196 }
16197
16198 #[test]
16199 fn test_positional_tuple_literal_cast_coerces_mismatched_literal_types() {
16200 let out = compile_and_emit("SELECT <tuple<int64, str>>('1', 3)");
16204 assert!(
16205 out.sql.contains("jsonb_build_array(('1')::int8, (3)::text)"),
16206 "got:\n{}",
16207 out.sql
16208 );
16209 }
16210
16211 #[test]
16212 fn test_nested_tuple_literal_cast_applies_casts_recursively() {
16213 let out = compile_and_emit(
16214 "SELECT <tuple<point: tuple<x: float64, y: float64>, label: str>>(point := ('1', 2), label := 5)",
16215 );
16216 assert!(
16217 out.sql.contains(
16218 "jsonb_build_object('point', jsonb_build_object('x', ('1')::float8, 'y', (2)::float8), 'label', (5)::text)"
16219 ),
16220 "got:\n{}",
16221 out.sql
16222 );
16223 }
16224
16225 #[test]
16226 fn test_positional_tuple_literal_nested_inside_named_tuple_compiles() {
16227 let out = compile_and_emit("SELECT (point := (1, 2), label := 'origin')");
16228 assert!(out.sql.contains("jsonb_build_array(1, 2)"), "got:\n{}", out.sql);
16229 assert!(out.sql.contains("jsonb_build_object("), "got:\n{}", out.sql);
16230 }
16231
16232 #[test]
16233 fn test_positional_tuple_literal_in_schema_bound_shape_field_compiles() {
16234 let out = compile_and_emit("SELECT Person { name, pair := (1, 2) }");
16235 assert!(out.sql.contains("jsonb_build_array(1, 2)"), "got:\n{}", out.sql);
16236 }
16237
16238 fn make_schema_with_search_index(backend: crate::schema::SearchBackend) -> SchemaDescriptor {
16247 use crate::schema::{SearchIndexDescriptor, SearchPointerDescriptor, SearchWeight};
16248 let mut s = make_schema();
16249 if let Some(td) = s.types.iter_mut().find(|t| t.name == "Person") {
16250 td.search_indexes.push(SearchIndexDescriptor {
16251 index_name: None,
16252 backend,
16253 pointers: vec![SearchPointerDescriptor {
16254 name: "name".into(),
16255 weight: SearchWeight::A,
16256 }],
16257 });
16258 }
16259 s
16260 }
16261
16262 #[test]
16263 fn test_insert_enqueues_a_meilisearch_outbox_row() {
16264 let schema = make_schema_with_search_index(crate::schema::SearchBackend::Meilisearch);
16265 let out = compile_and_emit_with("INSERT Person { name := 'Alice', age := 30 }", &schema);
16266 assert!(
16267 out.sql.contains("'Meilisearch'::_pylon.\"IndexKind\""),
16268 "expected a Meilisearch outbox enqueue CTE, got:\n{}",
16269 out.sql,
16270 );
16271 assert!(
16272 out.sql.contains("INSERT INTO _pylon.\"IndexOutbox\""),
16273 "got:\n{}",
16274 out.sql
16275 );
16276 }
16277
16278 #[test]
16279 fn test_insert_enqueues_an_opensearch_outbox_row() {
16280 let schema = make_schema_with_search_index(crate::schema::SearchBackend::OpenSearch);
16281 let out = compile_and_emit_with("INSERT Person { name := 'Alice', age := 30 }", &schema);
16282 assert!(
16283 out.sql.contains("'OpenSearch'::_pylon.\"IndexKind\""),
16284 "expected an OpenSearch outbox enqueue CTE, got:\n{}",
16285 out.sql,
16286 );
16287 }
16288
16289 #[test]
16290 fn test_insert_does_not_enqueue_an_outbox_row_for_a_postgres_backed_search_index() {
16291 let schema = make_schema_with_search_index(crate::schema::SearchBackend::Postgres);
16294 let out = compile_and_emit_with("INSERT Person { name := 'Alice', age := 30 }", &schema);
16295 assert!(
16296 !out.sql.contains("_pylon.\"IndexOutbox\""),
16297 "did not expect an outbox enqueue, got:\n{}",
16298 out.sql
16299 );
16300 }
16301
16302 #[test]
16303 fn test_update_enqueues_a_meilisearch_outbox_row() {
16304 let schema = make_schema_with_search_index(crate::schema::SearchBackend::Meilisearch);
16305 let out = compile_and_emit_with("UPDATE Person FILTER .name = 'Alice' SET { age := 31 }", &schema);
16306 assert!(
16307 out.sql.contains("'Meilisearch'::_pylon.\"IndexKind\""),
16308 "expected a Meilisearch outbox enqueue CTE, got:\n{}",
16309 out.sql,
16310 );
16311 }
16312
16313 #[test]
16314 fn test_delete_enqueues_a_meilisearch_outbox_delete_job() {
16315 let schema = make_schema_with_search_index(crate::schema::SearchBackend::Meilisearch);
16316 let out = compile_and_emit_with("DELETE Person FILTER .name = 'Alice'", &schema);
16317 assert!(
16318 out.sql.contains("'Meilisearch'::_pylon.\"IndexKind\""),
16319 "expected a Meilisearch outbox enqueue CTE, got:\n{}",
16320 out.sql,
16321 );
16322 assert!(
16323 out.sql.contains("'delete'"),
16324 "expected the delete operation literal, got:\n{}",
16325 out.sql
16326 );
16327 }
16328
16329 #[test]
16330 fn test_range_intrinsic_resolves_int_literals_to_int8range() {
16331 let out = compile_and_emit("SELECT std::overlaps(std::range(1, 3), std::range(2, 5))");
16336 assert!(out.sql.contains("int8range(1, 3)"), "got:\n{}", out.sql);
16337 assert!(out.sql.contains("int8range(2, 5)"), "got:\n{}", out.sql);
16338 assert!(
16339 out.sql.contains(" && "),
16340 "expected infix && for overlaps, got:\n{}",
16341 out.sql
16342 );
16343 assert!(
16344 !out.sql.contains("\"std\""),
16345 "must not emit a literal std schema call, got:\n{}",
16346 out.sql
16347 );
16348 }
16349
16350 #[test]
16351 fn test_range_intrinsic_resolves_datetime_to_tstzrange() {
16352 let out =
16353 compile_and_emit("SELECT std::range(<datetime>'2024-01-01T00:00:00Z', <datetime>'2024-06-01T00:00:00Z')");
16354 assert!(out.sql.contains("tstzrange("), "got:\n{}", out.sql);
16355 }
16356
16357 #[test]
16360 fn test_range_intrinsic_named_bounds_become_a_bounds_string() {
16361 let out = compile_and_emit("SELECT std::range(1, 3, inc_lower := true, inc_upper := true)");
16362 assert!(out.sql.contains("int8range(1, 3, '[]')"), "got:\n{}", out.sql);
16363 let out = compile_and_emit("SELECT std::range(1, 3, inc_lower := false)");
16364 assert!(out.sql.contains("int8range(1, 3, '()')"), "got:\n{}", out.sql);
16365 }
16366
16367 #[test]
16369 fn test_range_intrinsic_computes_a_bounds_string_from_an_expression() {
16370 let out = compile_and_emit("SELECT std::range(1, 3, inc_upper := <bool>$closed)");
16371 assert!(out.sql.contains("CASE WHEN"), "got:\n{}", out.sql);
16372 }
16373
16374 #[test]
16376 fn test_range_intrinsic_leaves_a_missing_upper_bound_unbounded() {
16377 let out = compile_and_emit("SELECT std::range(1)");
16378 assert!(out.sql.contains("int8range(1, NULL)"), "got:\n{}", out.sql);
16379 }
16380
16381 #[test]
16384 fn test_a_named_only_parameter_is_reported_as_named_only() {
16385 assert!(
16386 compile_err("SELECT std::range(1, 5, true, false)").contains("takes 'inc_lower' as a named argument only"),
16387 "{}",
16388 compile_err("SELECT std::range(1, 5, true, false)")
16389 );
16390 assert!(
16391 compile_err("SELECT std::json_set(std::to_json('{}'), 'a', std::to_json('1'))")
16392 .contains("requires the named argument 'value'"),
16393 "{}",
16394 compile_err("SELECT std::json_set(std::to_json('{}'), 'a', std::to_json('1'))")
16395 );
16396 assert!(
16398 compile_err("SELECT std::range()").contains("takes 1 or 2 argument(s), got 0"),
16399 "{}",
16400 compile_err("SELECT std::range()")
16401 );
16402 let message = compile_err("SELECT std::json_get(std::to_json('{}'), 'a', 1, 2)");
16404 assert!(message.contains("default := optional<json>"), "{message}");
16405 }
16406
16407 #[test]
16408 fn test_range_intrinsic_builds_the_empty_range() {
16409 let out = compile_and_emit("SELECT std::range(1, 3, empty := true)");
16410 assert!(out.sql.contains("'empty'::int8range"), "got:\n{}", out.sql);
16411 }
16412
16413 #[test]
16414 fn test_multirange_intrinsic_resolves_from_range_element() {
16415 let out = compile_and_emit("SELECT std::multirange([std::range(1, 3), std::range(5, 7)])");
16416 assert!(out.sql.contains("int8multirange(VARIADIC "), "got:\n{}", out.sql);
16417 assert!(
16418 !out.sql.contains("\"std\""),
16419 "must not emit a literal std schema call, got:\n{}",
16420 out.sql
16421 );
16422 }
16423}