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(path_join_target)
450 .find(|target| target.alias == alias)
451}
452
453fn path_join_target(join: &IrPathJoin) -> &IrSource {
454 match join {
455 IrPathJoin::Single { target, .. }
456 | IrPathJoin::Multi { target, .. }
457 | IrPathJoin::BacklinkSingle { target, .. }
458 | IrPathJoin::BacklinkMulti { target, .. }
459 | IrPathJoin::Function { target, .. }
460 | IrPathJoin::Lateral { target, .. } => target,
461 }
462}
463
464fn source_type_disc(src: &IrSource) -> String {
467 match source_fanout(src) {
468 Some(_) => format!("{}.\"__type__\"", qi(&src.alias)),
469 None => type_disc(&src.type_name),
470 }
471}
472
473fn emit_poly_union(implementors: &[IrPolyImplementor], columns: &[String]) -> String {
476 let col_list = columns.iter().map(|c| qi(c)).collect::<Vec<_>>().join(", ");
477 implementors
478 .iter()
479 .map(|imp| {
480 format!(
481 " SELECT {}::text AS \"__type__\", {} FROM {}",
482 sql_str(&imp.type_name),
483 col_list,
484 qn(&imp.module, &imp.table),
485 )
486 })
487 .collect::<Vec<_>>()
488 .join("\n UNION ALL\n")
489}
490
491fn emit_select_stmt(sel: &IrSelect, ctes: &[IrCteDef]) -> SqlOutput {
494 match sel.rows.as_slice() {
495 [IrRowSource::Bound { source, shape }] => emit_bound_select(sel, source, shape),
496 rows if rows.len() > 1 && rows.iter().all(|r| matches!(r, IrRowSource::Bound { .. })) => {
497 emit_bound_union_select(sel, rows)
498 }
499 rows => emit_free_rows(sel, rows, ctes),
500 }
501}
502
503fn bound_union_from_clause(rows: &[IrRowSource], common_columns: &[String]) -> String {
510 let columns = common_columns.iter().map(|c| qi(c)).collect::<Vec<_>>().join(", ");
511 rows.iter()
512 .map(|row| match row {
513 IrRowSource::Bound { source, .. } if !common_columns.is_empty() => {
514 let own_type = match source_fanout(source) {
515 Some(_) => "\"__type__\"".to_string(),
516 None => format!("{} AS \"__type__\"", type_disc(&source.type_name)),
517 };
518 format!(" SELECT {own_type}, {columns} FROM {}", source_ref(source))
519 }
520 IrRowSource::Bound { source, .. } => format!(" SELECT * FROM {}", source_ref(source)),
521 IrRowSource::Free(_) => unreachable!("caller checked every row is bound"),
522 })
523 .collect::<Vec<_>>()
524 .join("\n UNION ALL\n")
525}
526
527fn emit_bound_union_select(sel: &IrSelect, rows: &[IrRowSource]) -> SqlOutput {
528 let IrRowSource::Bound { source, shape } = &rows[0] else {
529 unreachable!("caller checked every row is bound")
530 };
531 let alias = &source.alias;
532 let (pointer_exprs, shape_pointers) = build_shape(shape, alias);
533 let row_type = if sel.poly_columns.is_empty() {
534 type_disc(&source.type_name)
535 } else {
536 format!("{}.\"__type__\"", qi(alias))
537 };
538 let mut parts = vec![row_type];
539 parts.extend(pointer_exprs);
540
541 let mut sql = format!(
542 "SELECT {}(\n {}\n) AS result\nFROM (\n{}\n) AS {}",
543 if sel.distinct { "DISTINCT " } else { "" },
544 parts.join(",\n "),
545 bound_union_from_clause(rows, &sel.poly_columns),
546 qi(alias),
547 );
548 append_filter(&mut sql, &sel.filter);
549 append_order_by(&mut sql, &sel.order_by);
550 append_offset_limit(&mut sql, &sel.offset, &sel.limit);
551
552 SqlOutput {
553 sql,
554 shape: ShapeDescriptor {
555 root: ShapeNode::Object {
556 name: String::new(),
557 type_name: Some(source.type_name.clone()),
558 position: 0,
559 cardinality: Cardinality::Many,
560 pointers: prepend_type(shape_pointers),
561 has_implicit_id: shape_has_implicit_id(shape),
562 },
563 },
564 inference_plan: None,
565 }
566}
567
568fn emit_bound_select(sel: &IrSelect, source: &IrSource, shape: &[IrShapePointer]) -> SqlOutput {
574 let alias = &source.alias;
575 let (pointer_exprs, shape_pointers) = build_shape(shape, alias);
576
577 let type_expr = if sel.polymorphic {
578 format!("{}.\"__type__\"", qi(alias))
579 } else {
580 type_disc(&source.type_name)
581 };
582 let mut parts = vec![type_expr];
583 parts.extend(pointer_exprs);
584 let tuple = parts.join(",\n ");
585
586 let distinct = if sel.distinct { "DISTINCT " } else { "" };
587
588 let from_clause = if let Some(dml) = &sel.dml_source {
590 let mut cte_parts = match dml.as_ref() {
591 IrStmt::Update(upd) if update_has_any_multilink(upd) => emit_update_multilink_ctes(upd, DML_CTE),
592 IrStmt::Insert(ins) if insert_has_any_multilink(ins) => emit_insert_multilink_ctes(ins, DML_CTE),
593 IrStmt::Insert(ins) if !ins.nested_ctes.is_empty() => {
596 let mut parts = emit_user_cte_parts(&ins.nested_ctes);
597 parts.push(format!("\"_dml\" AS (\n{}\n)", emit_dml_as_cte_source(dml)));
598 parts
599 }
600 IrStmt::Update(upd) if !upd.nested_ctes.is_empty() => {
601 let mut parts = emit_user_cte_parts(&upd.nested_ctes);
602 parts.push(format!("\"_dml\" AS (\n{}\n)", emit_dml_as_cte_source(dml)));
603 parts
604 }
605 IrStmt::Update(upd) if !upd.poly_implementors.is_empty() => emit_poly_update_dml_ctes(upd, "_dml"),
606 IrStmt::Delete(del) if !del.poly_implementors.is_empty() => emit_poly_delete_dml_ctes(del, "_dml"),
607 _ => vec![format!("\"_dml\" AS (\n{}\n)", emit_dml_as_cte_source(dml))],
608 };
609 let (enqueue_v, enqueue_s) = match dml.as_ref() {
610 IrStmt::Insert(ins) => (ins.enqueue_vector.as_slice(), ins.enqueue_search.as_slice()),
611 IrStmt::Update(upd) => (upd.enqueue_vector.as_slice(), upd.enqueue_search.as_slice()),
612 _ => (&[][..], &[][..]),
613 };
614 cte_parts.extend(enqueue_ctes(enqueue_v, "_dml"));
615 cte_parts.extend(enqueue_search_ctes(enqueue_s, "_dml", enqueue_v.len()));
616 format!(
617 "WITH\n{}\nSELECT {}(\n {}\n) AS result\nFROM \"_dml\" AS {}",
618 cte_parts.join(",\n"),
619 distinct,
620 tuple,
621 qi(alias)
622 )
623 } else if sel.polymorphic && !source.table.starts_with("@cte:") {
624 let union_sql = emit_poly_union(&sel.poly_implementors, &sel.poly_columns);
626 format!(
627 "SELECT {}(\n {}\n) AS result\nFROM (\n{}\n) AS {}",
628 distinct,
629 tuple,
630 union_sql,
631 qi(alias)
632 )
633 } else {
634 format!(
637 "SELECT {}(\n {}\n) AS result\nFROM {} AS {}",
638 distinct,
639 tuple,
640 source_ref(source),
641 qi(alias)
642 )
643 };
644
645 let mut sql = from_clause;
646 append_filter(&mut sql, &sel.filter);
647 append_order_by(&mut sql, &sel.order_by);
648 append_offset_limit(&mut sql, &sel.offset, &sel.limit);
649 append_lock_clause(&mut sql, &sel.lock);
650
651 let root_pointers = prepend_type(shape_pointers);
652 SqlOutput {
653 sql,
654 shape: ShapeDescriptor {
655 root: ShapeNode::Object {
656 name: String::new(),
657 type_name: Some(source.type_name.clone()),
658 position: 0,
659 cardinality: Cardinality::Many,
660 pointers: root_pointers,
661 has_implicit_id: shape_has_implicit_id(shape),
662 },
663 },
664 inference_plan: None,
665 }
666}
667
668fn nested_cte_from(ctes: &[IrCteDef], reading: &str, lead: &str) -> String {
675 let joined: Vec<String> = ctes
676 .iter()
677 .map(|c| qi(&c.name))
678 .filter(|name| reading.contains(&format!("{name}.")))
679 .collect();
680 if joined.is_empty() {
681 return String::new();
682 }
683 format!("{lead}FROM {}", joined.join(", "))
684}
685
686fn emit_scalar_union(branches: &[IrStmt]) -> String {
689 let operands: Vec<String> = branches
690 .iter()
691 .enumerate()
692 .map(|(i, branch)| {
693 format!(
694 "SELECT \"v\" FROM (\n{}\n) AS \"_u{i}\"",
695 emit_dml_as_cte_source(branch)
696 )
697 })
698 .collect();
699 format!(
700 "SELECT ROW(v) AS result, v FROM (\n{}\n) AS _scalar",
701 operands.join("\nUNION ALL\n")
702 )
703}
704
705fn emit_dml_as_cte_source(stmt: &IrStmt) -> String {
706 match stmt {
707 IrStmt::ScalarUnion(branches) => format!(" {}", emit_scalar_union(branches)),
708 IrStmt::Insert(ins) => {
709 let rewrite_cols: std::collections::HashSet<&str> =
710 ins.rewrites.iter().map(|r| r.column.as_str()).collect();
711 let cols: Vec<String> = ins
712 .assignments
713 .iter()
714 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
715 .map(|(c, _)| format!(" {}", qi(c)))
716 .chain(ins.rewrites.iter().map(|r| format!(" {}", qi(&r.column))))
717 .collect();
718 let vals: Vec<String> = ins
719 .assignments
720 .iter()
721 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
722 .map(|(_, e)| format!(" {}", emit_expr(e)))
723 .chain(ins.rewrites.iter().map(|r| format!(" {}", emit_expr(&r.expr))))
724 .collect();
725 let mut sql = if !ins.nested_ctes.is_empty() || ins.guard.is_some() {
726 format!(" {}", emit_insert_row_sql(ins))
733 } else if cols.is_empty() {
734 format!(" INSERT INTO {} DEFAULT VALUES", target_ref(&ins.target))
736 } else {
737 format!(
738 " INSERT INTO {} (\n{}\n ) VALUES (\n{}\n )",
739 target_ref(&ins.target),
740 cols.join(",\n"),
741 vals.join(",\n"),
742 )
743 };
744 if let Some(conflict) = &ins.unless_conflict {
745 emit_conflict(&mut sql, conflict);
746 }
747 sql.push_str("\n RETURNING *");
748 sql
749 }
750 IrStmt::Update(upd) => {
751 let alias = &upd.target.alias;
760 let sets = update_set_fragments(&upd.assignments, &upd.rewrites, " ");
761 let mut sql = format!(
762 " UPDATE {} AS {}\n SET\n{}",
763 target_ref(&upd.target),
764 qi(alias),
765 sets.join(",\n"),
766 );
767 sql.push_str(&nested_cte_from(&upd.nested_ctes, &sets.join(","), "\n "));
770 append_filter(&mut sql, &upd.filter);
771 sql.push_str(&format!("\n RETURNING {}.*", qi(alias)));
774 sql
775 }
776 IrStmt::Delete(del) => {
777 let alias = &del.target.alias;
778 let mut sql = format!(" DELETE FROM {} AS {}", target_ref(&del.target), qi(alias),);
779 append_filter(&mut sql, &del.filter);
780 sql.push_str("\n RETURNING *");
781 sql
782 }
783 IrStmt::Select(inner) => match inner.rows.as_slice() {
784 [IrRowSource::Bound { source, .. }]
789 if let Some(read) = inner.dml_source.as_deref()
790 && !matches!(read, IrStmt::Insert(_) | IrStmt::Update(_) | IrStmt::Delete(_)) =>
791 {
792 let mut sql = format!(
794 " SELECT {}* FROM (\n{}\n ) AS {}",
795 if inner.distinct { "DISTINCT " } else { "" },
796 emit_dml_as_cte_source(read),
797 qi(&source.alias),
798 );
799 append_filter(&mut sql, &inner.filter);
800 append_order_by(&mut sql, &inner.order_by);
801 append_offset_limit(&mut sql, &inner.offset, &inner.limit);
802 sql
803 }
804 [IrRowSource::Bound { source, .. }] => {
805 let from = if inner.polymorphic && !source.table.starts_with("@cte:") {
808 format!(
809 "(\n{}\n ) AS {}",
810 emit_poly_union(&inner.poly_implementors, &inner.poly_columns),
811 qi(&source.alias),
812 )
813 } else {
814 format!("{} AS {}", source_ref(source), qi(&source.alias))
815 };
816 let mut sql = format!(
817 " SELECT {}* FROM {}",
818 if inner.distinct { "DISTINCT " } else { "" },
819 from
820 );
821 append_filter(&mut sql, &inner.filter);
822 append_order_by(&mut sql, &inner.order_by);
823 append_offset_limit(&mut sql, &inner.offset, &inner.limit);
824 sql
825 }
826 rows if rows.len() > 1 && rows.iter().all(|r| matches!(r, IrRowSource::Bound { .. })) => {
829 let IrRowSource::Bound { source, .. } = &rows[0] else {
830 unreachable!("checked by the guard")
831 };
832 let mut sql = format!(
833 " SELECT {}* FROM (\n{}\n ) AS {}",
834 if inner.distinct { "DISTINCT " } else { "" },
835 bound_union_from_clause(rows, &inner.poly_columns),
836 qi(&source.alias),
837 );
838 append_filter(&mut sql, &inner.filter);
839 append_order_by(&mut sql, &inner.order_by);
840 append_offset_limit(&mut sql, &inner.offset, &inner.limit);
841 sql
842 }
843 _ => emit_select_stmt(inner, &[]).sql,
849 },
850 IrStmt::FunctionSelect(sel) => {
851 let args_sql = sel.fn_args.iter().map(emit_expr).collect::<Vec<_>>().join(", ");
854 let fn_call = format!("{}.{}({})", pg_schema(&sel.fn_module), qi(&sel.fn_name), args_sql);
855 let mut sql = format!(" SELECT * FROM {} AS {}", fn_call, qi(&sel.alias));
856 append_filter(&mut sql, &sel.filter);
857 append_order_by(&mut sql, &sel.order_by);
858 append_offset_limit(&mut sql, &sel.offset, &sel.limit);
859 sql
860 }
861 IrStmt::For(f) => {
862 if matches!(&f.iterator, IrForIterator::Values { exprs, .. } if exprs.is_empty()) {
867 return " SELECT NULL AS \"id\" WHERE FALSE".to_string();
868 }
869 let (values_from, _) = emit_for_iterator(&f.iterator, &format!("_for_{}", f.var_name));
870 let body =
871 prefix_body_ctes(emit_dml_as_cte_source(&f.body), &uncorrelated_body_ctes(f)).replace('\n', "\n ");
872 format!(
873 " SELECT \"_body\".*\n FROM {}\n CROSS JOIN LATERAL (\n {}\n ) AS \"_body\"",
874 values_from.replace('\n', "\n "),
875 body,
876 )
877 }
878 IrStmt::Group(grp) if matches!(grp.output, IrGroupOutput::Elements) => emit_group_elements_source(grp),
879 IrStmt::Group(_) | IrStmt::VectorSearch(_) | IrStmt::FtsSearch(_) => {
880 unreachable!("cannot appear as a CTE source")
881 }
882 IrStmt::PathSelect(ps) => {
883 let distinct = if ps.distinct { "DISTINCT " } else { "" };
889 let projection = match &ps.result {
890 IrPathResult::Scalar(e, _) => format!("{}{} AS v", distinct, emit_expr(e)),
894 IrPathResult::Object { alias, .. } => format!("{}{}.*", distinct, qi(alias)),
895 };
896 let mut sql = format!(
897 "SELECT {}\n FROM {}",
898 projection,
899 emit_path_joins(&ps.root, &ps.joins)
900 );
901 append_filter(&mut sql, &ps.filter);
902 append_order_by(&mut sql, &ps.order_by);
903 append_offset_limit(&mut sql, &ps.offset, &ps.limit);
904 match &ps.result {
905 IrPathResult::Scalar(e, _) => {
906 let row_value = if enum_type_of_expr(e).is_some() { "v::text" } else { "v" };
907 format!(" SELECT ROW({row_value}) AS result, v FROM ({sql}) AS _scalar")
908 }
909 IrPathResult::Object { .. } => format!(" {sql}"),
910 }
911 }
912 }
913}
914
915fn emit_poly_update_dml_ctes(upd: &IrUpdate, name: &str) -> Vec<String> {
950 let alias = &upd.target.alias;
951 let sets = update_set_fragments(&upd.assignments, &upd.rewrites, "");
952 let col_list = upd.poly_columns.iter().map(|c| qi(c)).collect::<Vec<_>>().join(", ");
953
954 let mut cte_parts = vec![];
955 let mut union_parts = vec![];
956 for (i, imp) in upd.poly_implementors.iter().enumerate() {
957 let cte_name = format!("{}__u{}", name, i);
958 let mut upd_sql = format!(
959 "UPDATE {} AS {}\nSET {}",
960 qn(&imp.module, &imp.table),
961 qi(alias),
962 sets.join(", "),
963 );
964 append_filter(&mut upd_sql, &upd.filter);
965 upd_sql.push_str(&format!("\nRETURNING {}", col_list));
966 cte_parts.push(format!("\"{}\" AS (\n{}\n)", cte_name, upd_sql));
967
968 union_parts.push(format!(
969 "SELECT {}::text AS \"__type__\", {} FROM \"{}\"",
970 sql_str(&imp.type_name),
971 col_list,
972 cte_name,
973 ));
974 }
975 cte_parts.push(format!("\"{}\" AS (\n{}\n)", name, union_parts.join("\nUNION ALL\n")));
976 cte_parts
977}
978
979fn concrete_poly_update(upd: &IrUpdate, imp: &IrPolyImplementor) -> IrUpdate {
984 let interface_prefix = format!("{}.", upd.target.table);
985 let own_prefix = format!("{}.", imp.table);
986 let own_junction = |table: &mut String, module: &mut String| {
987 if let Some(link) = table.strip_prefix(&interface_prefix) {
988 *table = format!("{own_prefix}{link}");
989 module.clone_from(&imp.module);
990 }
991 };
992 let mut concrete = upd.clone();
993 concrete.poly_implementors = vec![];
994 concrete.target.table = imp.table.clone();
995 concrete.target.type_name = imp.type_name.clone();
996 concrete.target.poly = None;
997 concrete
998 .multi_link_clears
999 .iter_mut()
1000 .for_each(|c| own_junction(&mut c.junction_table, &mut c.module));
1001 concrete
1002 .multi_link_replaces
1003 .iter_mut()
1004 .chain(concrete.multi_link_appends.iter_mut())
1005 .chain(concrete.multi_link_removals.iter_mut())
1006 .for_each(|m| own_junction(&mut m.junction_table, &mut m.module));
1007 concrete
1008}
1009
1010fn emit_poly_update_multilink_ctes(upd: &IrUpdate, name: &str) -> Vec<String> {
1014 let col_list = upd.poly_columns.iter().map(|c| qi(c)).collect::<Vec<_>>().join(", ");
1015 let mut cte_parts = emit_user_cte_parts(&upd.nested_ctes);
1016 let mut union_parts = vec![];
1017 for (i, imp) in upd.poly_implementors.iter().enumerate() {
1018 let cte_name = format!("{}__u{}", name, i);
1019 cte_parts.extend(emit_update_multilink_cte_parts(
1020 &concrete_poly_update(upd, imp),
1021 &cte_name,
1022 false,
1023 ));
1024 union_parts.push(format!(
1025 "SELECT {}::text AS \"__type__\", {} FROM \"{}\"",
1026 sql_str(&imp.type_name),
1027 col_list,
1028 cte_name,
1029 ));
1030 }
1031 cte_parts.push(format!("\"{}\" AS (\n{}\n)", name, union_parts.join("\nUNION ALL\n")));
1032 cte_parts
1033}
1034
1035fn emit_poly_delete_dml_ctes(del: &IrDelete, name: &str) -> Vec<String> {
1036 let alias = &del.target.alias;
1037 let col_list = del.poly_columns.iter().map(|c| qi(c)).collect::<Vec<_>>().join(", ");
1038
1039 let mut cte_parts = vec![];
1040 let mut union_parts = vec![];
1041 for (i, imp) in del.poly_implementors.iter().enumerate() {
1042 let cte_name = format!("{}__d{}", name, i);
1043 let mut del_sql = format!("DELETE FROM {} AS {}", qn(&imp.module, &imp.table), qi(alias),);
1044 append_filter(&mut del_sql, &del.filter);
1045 del_sql.push_str(&format!("\nRETURNING {}", col_list));
1046 cte_parts.push(format!("\"{}\" AS (\n{}\n)", cte_name, del_sql));
1047
1048 union_parts.push(format!(
1049 "SELECT {}::text AS \"__type__\", {} FROM \"{}\"",
1050 sql_str(&imp.type_name),
1051 col_list,
1052 cte_name,
1053 ));
1054 }
1055 cte_parts.push(format!("\"{}\" AS (\n{}\n)", name, union_parts.join("\nUNION ALL\n")));
1056 cte_parts
1057}
1058
1059fn update_has_any_multilink(upd: &IrUpdate) -> bool {
1062 !upd.multi_link_clears.is_empty()
1063 || !upd.multi_link_replaces.is_empty()
1064 || !upd.multi_link_appends.is_empty()
1065 || !upd.multi_link_removals.is_empty()
1066}
1067
1068fn ml_clear_exclusion(rep: Option<&IrMultiLinkMutation>) -> String {
1086 match rep {
1087 Some(rep) => {
1088 let vals_ref = emit_multilink_values_subquery(&rep.values, &[]);
1089 format!(
1090 " AND {} NOT IN (SELECT \"_v\".\"id\" FROM {} AS \"_v\")",
1091 qi(&rep.target_col),
1092 vals_ref
1093 )
1094 }
1095 None => String::new(),
1096 }
1097}
1098
1099fn emit_update_multilink_ctes(upd: &IrUpdate, name: &str) -> Vec<String> {
1113 emit_update_multilink_cte_parts(upd, name, true)
1114}
1115
1116fn emit_update_multilink_cte_parts(upd: &IrUpdate, name: &str, emit_nested: bool) -> Vec<String> {
1121 let alias = &upd.target.alias;
1122 let has_scalar_changes = !upd.assignments.is_empty() || !upd.rewrites.is_empty();
1123 let ids_name = format!("{}__ids", name);
1124 let mut parts: Vec<String> = if emit_nested {
1126 emit_user_cte_parts(&upd.nested_ctes)
1127 } else {
1128 vec![]
1129 };
1130
1131 if has_scalar_changes {
1132 let sets = update_set_fragments(&upd.assignments, &upd.rewrites, "");
1133 let mut upd_sql = format!(
1134 "UPDATE {} AS {}\nSET {}",
1135 target_ref(&upd.target),
1136 qi(alias),
1137 sets.join(", "),
1138 );
1139 upd_sql.push_str(&nested_cte_from(&upd.nested_ctes, &sets.join(","), "\n"));
1142 append_filter(&mut upd_sql, &upd.filter);
1143 upd_sql.push_str(&format!("\nRETURNING {}.*", qi(alias)));
1146 parts.push(format!("\"{}\" AS (\n{}\n)", ids_name, upd_sql));
1147 } else {
1148 let mut sel = format!(
1149 "SELECT {}.* FROM {} AS {}",
1150 qi(alias),
1151 target_ref(&upd.target),
1152 qi(alias),
1153 );
1154 append_filter(&mut sel, &upd.filter);
1155 parts.push(format!("\"{}\" AS (\n{}\n)", ids_name, sel));
1156 }
1157
1158 for (i, clr) in upd.multi_link_clears.iter().enumerate() {
1159 let exclude = ml_clear_exclusion(upd.multi_link_replaces.get(i));
1160 let del = format!(
1161 "DELETE FROM {} WHERE {} IN (SELECT id FROM \"{}\"){}",
1162 qn(&clr.module, &clr.junction_table),
1163 qi(&clr.source_col),
1164 ids_name,
1165 exclude,
1166 );
1167 parts.push(format!("\"{}__clr_{}\" AS (\n{}\n)", name, i, del));
1168 }
1169 for (i, app) in upd.multi_link_appends.iter().enumerate() {
1170 parts.push(emit_ml_append_cte(
1171 app,
1172 &ids_name,
1173 &upd.target.alias,
1174 &format!("{}__ml_add_{}", name, i),
1175 ));
1176 }
1177 for (i, rem) in upd.multi_link_removals.iter().enumerate() {
1178 parts.push(emit_ml_remove_cte(
1179 rem,
1180 &ids_name,
1181 &upd.target.alias,
1182 &format!("{}__ml_rm_{}", name, i),
1183 ));
1184 }
1185 for (i, rep) in upd.multi_link_replaces.iter().enumerate() {
1186 parts.push(emit_ml_append_cte(
1187 rep,
1188 &ids_name,
1189 &upd.target.alias,
1190 &format!("{}__ml_rep_{}", name, i),
1191 ));
1192 }
1193
1194 parts.push(format!("\"{}\" AS (\n SELECT * FROM \"{}\"\n)", name, ids_name));
1195 parts
1196}
1197
1198fn insert_has_any_multilink(ins: &IrInsert) -> bool {
1199 !ins.multi_link_appends.is_empty()
1200}
1201
1202fn emit_insert_multilink_ctes(ins: &IrInsert, name: &str) -> Vec<String> {
1208 let ids_name = format!("{}__ids", name);
1209 let mut parts: Vec<String> = emit_user_cte_parts(&ins.nested_ctes);
1210
1211 let mut insert_sql = emit_insert_row_sql(ins);
1212 if let Some(conflict) = &ins.unless_conflict {
1213 emit_conflict(&mut insert_sql, conflict);
1214 }
1215 insert_sql.push_str("\nRETURNING *");
1216 parts.push(format!("\"{}\" AS (\n{}\n)", ids_name, insert_sql));
1217
1218 for (i, app) in ins.multi_link_appends.iter().enumerate() {
1219 parts.push(emit_ml_append_cte(
1220 app,
1221 &ids_name,
1222 &ins.target.alias,
1223 &format!("{}__ml_add_{}", name, i),
1224 ));
1225 }
1226
1227 parts.push(format!("\"{}\" AS (\n SELECT * FROM \"{}\"\n)", name, ids_name));
1228 parts
1229}
1230
1231fn append_insert_guard(sql: &mut String, ins: &IrInsert) {
1245 if let Some(guard) = &ins.guard {
1246 sql.push_str(&format!("\nWHERE {}", emit_expr(guard)));
1247 }
1248}
1249
1250fn emit_for_dml_ctes_within(f: &IrFor, name: &str, outer_alias: Option<&str>) -> Vec<String> {
1251 let iter_alias = format!("_for_{}", f.var_name);
1252 let (_, plain_iter_cte) = emit_for_iterator(&f.iterator, &iter_alias);
1253 let correlated = outer_alias;
1254 let iter_cte = match correlated {
1255 Some(outer) => emit_nested_for_iterator(&f.iterator, &iter_alias, outer),
1256 None => plain_iter_cte,
1257 };
1258 let mut parts: Vec<String> = vec![iter_cte];
1261 parts.extend(emit_user_cte_parts(&f.body_ctes));
1262 let ids_name = format!("{}__ids", name);
1263
1264 if correlated.is_some()
1267 && let IrStmt::Insert(ins) = f.body.as_ref()
1268 && ins.multi_link_appends.is_empty()
1269 {
1270 let rows_name = format!("{}__rows", name);
1271 let outer_source = format!("{}.{}", qi(&iter_alias), qi(OUTER_KEY));
1272 parts.push(emit_iteration_rows_cte(ins, &rows_name, &iter_alias, &outer_source));
1273 parts.push(emit_insert_from_rows_cte(ins, &ids_name, &rows_name));
1274 parts.push(format!("\"{}\" AS (\n SELECT * FROM \"{}\"\n)", name, rows_name));
1275 return parts;
1276 }
1277
1278 match f.body.as_ref() {
1279 IrStmt::Insert(ins) => {
1280 let rewrite_cols: std::collections::HashSet<&str> =
1281 ins.rewrites.iter().map(|r| r.column.as_str()).collect();
1282 let cols: Vec<String> = ins
1283 .assignments
1284 .iter()
1285 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
1286 .map(|(c, _)| qi(c))
1287 .chain(ins.rewrites.iter().map(|r| qi(&r.column)))
1288 .collect();
1289 let values: Vec<String> = ins
1290 .assignments
1291 .iter()
1292 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
1293 .map(|(_, e)| emit_expr(e))
1294 .chain(ins.rewrites.iter().map(|r| emit_expr(&r.expr)))
1295 .collect();
1296 let rows_name = format!("{}__rows", name);
1299 let correlated_targets =
1300 correlated_append_indices(&ins.multi_link_appends, &[&f.body_ctes, &ins.nested_ctes]);
1301 if correlated_targets.is_empty() {
1302 let mut sql = format!(
1303 "INSERT INTO {} ({})\nSELECT {} FROM {}",
1304 target_ref(&ins.target),
1305 cols.join(", "),
1306 values.join(", "),
1307 qi(&iter_alias),
1308 );
1309 append_insert_guard(&mut sql, ins);
1310 if let Some(conflict) = &ins.unless_conflict {
1311 emit_conflict(&mut sql, conflict);
1312 }
1313 sql.push_str("\nRETURNING *");
1314 parts.push(format!("\"{}\" AS (\n{}\n)", ids_name, sql));
1315 } else {
1316 let outer_source = match correlated {
1317 Some(_) => format!("{}.{}", qi(&iter_alias), qi(OUTER_KEY)),
1318 None => format!("{}.{}", qi(&iter_alias), qi("v")),
1319 };
1320 parts.push(emit_iteration_rows_cte(ins, &rows_name, &iter_alias, &outer_source));
1321 parts.push(emit_insert_from_rows_cte(ins, &ids_name, &rows_name));
1322 }
1323 for (i, append) in ins.multi_link_appends.iter().enumerate() {
1324 let cte_name = format!("{}__ml_add_{}", name, i);
1325 if correlated_targets.contains(&i) {
1326 let IrMultiLinkValueSource::CteRef(target) = &append.values.source else {
1327 unreachable!("only a CTE-backed value can carry an iteration key")
1328 };
1329 parts.push(emit_correlated_ml_append_cte(append, &rows_name, target, &cte_name));
1330 } else {
1331 parts.push(emit_ml_append_cte(append, &ids_name, &ins.target.alias, &cte_name));
1332 }
1333 }
1334 }
1335 IrStmt::Update(upd) => {
1336 let alias = &upd.target.alias;
1337 if upd.assignments.is_empty() && upd.rewrites.is_empty() && !upd.multi_link_appends.is_empty() {
1338 const ITER_COL: &str = "_iter";
1339 let generated: Vec<Option<&IrInsert>> = upd
1343 .multi_link_appends
1344 .iter()
1345 .map(|a| per_iteration_insert(a, &upd.nested_ctes))
1346 .collect();
1347 let new_cols: String = generated
1348 .iter()
1349 .enumerate()
1350 .filter_map(|(i, ins)| {
1351 ins.map(|ins| {
1352 format!(
1353 ", {} AS {}",
1354 ins.id_default_sql.as_deref().unwrap_or("uuidv7()"),
1355 qi(&format!("_new_{i}"))
1356 )
1357 })
1358 })
1359 .collect();
1360 let mut ids_sql = format!(
1361 "\"{}\" AS (\nSELECT {}.*, {}.\"v\" AS {}{} FROM {} AS {}, {}",
1362 ids_name,
1363 qi(alias),
1364 qi(&iter_alias),
1365 qi(ITER_COL),
1366 new_cols,
1367 target_ref(&upd.target),
1368 qi(alias),
1369 qi(&iter_alias),
1370 );
1371 append_filter(&mut ids_sql, &upd.filter);
1372 ids_sql.push_str("\n)");
1373 parts.push(ids_sql);
1374 for (i, append) in upd.multi_link_appends.iter().enumerate() {
1375 match generated[i] {
1376 Some(ins) => {
1377 parts.push(emit_for_nested_insert_cte(
1378 ins,
1379 &ids_name,
1380 &iter_alias,
1381 &format!("{}__ml_ins_{}", name, i),
1382 &format!("_new_{i}"),
1383 ));
1384 parts.push(emit_for_ml_append_cte(
1385 append,
1386 &ids_name,
1387 &format!("{}__ml_add_{}", name, i),
1388 &format!("_new_{i}"),
1389 ));
1390 }
1391 None => parts.push(emit_for_ml_append_cte(
1392 append,
1393 &ids_name,
1394 &format!("{}__ml_add_{}", name, i),
1395 ITER_COL,
1396 )),
1397 }
1398 }
1399 } else if !upd.poly_implementors.is_empty() {
1400 let sets = update_set_fragments(&upd.assignments, &upd.rewrites, "");
1404 let col_list = upd.poly_columns.iter().map(|c| qi(c)).collect::<Vec<_>>().join(", ");
1405 let ret_list = upd
1408 .poly_columns
1409 .iter()
1410 .map(|c| format!("{}.{}", qi(alias), qi(c)))
1411 .collect::<Vec<_>>()
1412 .join(", ");
1413 let mut union_parts = vec![];
1414 for (i, imp) in upd.poly_implementors.iter().enumerate() {
1415 let cte_name = format!("{}__u{}", ids_name, i);
1416 let mut upd_sql = format!(
1417 "UPDATE {} AS {}\nSET {}\nFROM {}",
1418 qn(&imp.module, &imp.table),
1419 qi(alias),
1420 sets.join(", "),
1421 qi(&iter_alias),
1422 );
1423 append_filter(&mut upd_sql, &upd.filter);
1424 upd_sql.push_str(&format!("\nRETURNING {}", ret_list));
1425 parts.push(format!("\"{}\" AS (\n{}\n)", cte_name, upd_sql));
1426 union_parts.push(format!(
1427 "SELECT {}::text AS \"__type__\", {} FROM \"{}\"",
1428 sql_str(&imp.type_name),
1429 col_list,
1430 cte_name,
1431 ));
1432 }
1433 parts.push(format!(
1434 "\"{}\" AS (\n{}\n)",
1435 ids_name,
1436 union_parts.join("\nUNION ALL\n")
1437 ));
1438 } else {
1439 let sets = update_set_fragments(&upd.assignments, &upd.rewrites, "");
1440 let mut sql = format!(
1441 "UPDATE {} AS {}\nSET {}\nFROM {}",
1442 target_ref(&upd.target),
1443 qi(alias),
1444 sets.join(", "),
1445 qi(&iter_alias),
1446 );
1447 append_filter(&mut sql, &upd.filter);
1448 sql.push_str(&format!("\nRETURNING {}.*", qi(alias)));
1449 parts.push(format!("\"{}\" AS (\n{}\n)", ids_name, sql));
1450 }
1451 }
1452 IrStmt::For(inner) => {
1456 let inner_alias = format!("_for_{}", inner.var_name);
1457 parts.push(emit_nested_for_iterator(&inner.iterator, &inner_alias, &iter_alias));
1458 parts.extend(emit_user_cte_parts(&inner.body_ctes));
1459 let IrStmt::Insert(ins) = inner.body.as_ref() else {
1460 unreachable!("a nested for-loop's own body is an insert: {:?}", inner.body)
1461 };
1462 let rewrite_cols: std::collections::HashSet<&str> =
1463 ins.rewrites.iter().map(|r| r.column.as_str()).collect();
1464 let cols: Vec<String> = ins
1465 .assignments
1466 .iter()
1467 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
1468 .map(|(c, _)| qi(c))
1469 .chain(ins.rewrites.iter().map(|r| qi(&r.column)))
1470 .collect();
1471 let values: Vec<String> = ins
1472 .assignments
1473 .iter()
1474 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
1475 .map(|(_, e)| emit_expr(e))
1476 .chain(ins.rewrites.iter().map(|r| emit_expr(&r.expr)))
1477 .collect();
1478 let mut sql = format!(
1479 "INSERT INTO {} ({})\nSELECT {} FROM {}",
1480 target_ref(&ins.target),
1481 cols.join(", "),
1482 values.join(", "),
1483 nested_for_from(&inner_alias, &iter_alias),
1484 );
1485 append_insert_guard(&mut sql, ins);
1486 if let Some(conflict) = &ins.unless_conflict {
1487 emit_conflict(&mut sql, conflict);
1488 }
1489 sql.push_str("\nRETURNING *");
1490 parts.push(format!("\"{}\" AS (\n{}\n)", ids_name, sql));
1491 for (i, append) in ins.multi_link_appends.iter().enumerate() {
1492 parts.push(emit_ml_append_cte(
1493 append,
1494 &ids_name,
1495 &ins.target.alias,
1496 &format!("{}__ml_add_{}", name, i),
1497 ));
1498 }
1499 }
1500 other => unreachable!("for-loop body is not a mutation: {other:?}"),
1502 }
1503
1504 parts.push(format!("\"{}\" AS (\n SELECT * FROM \"{}\"\n)", name, ids_name));
1505 parts
1506}
1507
1508fn emit_user_cte_parts(ctes: &[IrCteDef]) -> Vec<String> {
1517 let mut parts: Vec<String> = vec![];
1518 for c in ctes {
1519 let nested: &[IrCteDef] = match &c.stmt {
1525 IrStmt::Insert(ins) if insert_has_any_multilink(ins) => &[],
1526 IrStmt::Update(upd) if update_has_any_multilink(upd) => &[],
1527 IrStmt::Insert(ins) => &ins.nested_ctes,
1528 IrStmt::Update(upd) => &upd.nested_ctes,
1529 _ => &[],
1530 };
1531 if !nested.is_empty() {
1532 parts.extend(emit_user_cte_parts(nested));
1533 }
1534 if let IrStmt::Update(upd) = &c.stmt {
1535 if !upd.poly_implementors.is_empty() {
1536 parts.extend(if update_has_any_multilink(upd) {
1537 emit_poly_update_multilink_ctes(upd, &c.name)
1538 } else {
1539 emit_poly_update_dml_ctes(upd, &c.name)
1540 });
1541 continue;
1542 }
1543 if update_has_any_multilink(upd) {
1544 parts.extend(emit_update_multilink_ctes(upd, &c.name));
1545 continue;
1546 }
1547 }
1548 if let IrStmt::Insert(ins) = &c.stmt
1549 && insert_has_any_multilink(ins)
1550 {
1551 parts.extend(emit_insert_multilink_ctes(ins, &c.name));
1552 continue;
1553 }
1554 if let IrStmt::Delete(del) = &c.stmt
1555 && !del.poly_implementors.is_empty()
1556 {
1557 parts.extend(emit_poly_delete_dml_ctes(del, &c.name));
1558 continue;
1559 }
1560 if let IrStmt::For(f) = &c.stmt
1561 && matches!(f.body.as_ref(), IrStmt::Insert(_) | IrStmt::Update(_) | IrStmt::For(_))
1562 {
1563 parts.extend(emit_for_dml_ctes_within(f, &c.name, c.correlated_to.as_deref()));
1564 continue;
1565 }
1566 let body = emit_dml_as_cte_source(&c.stmt);
1567 parts.push(match &c.correlated_to {
1568 Some(iterator) => format!(
1571 "\"{}\" AS (\nSELECT {}.\"v\" AS {}, \"_row\".*\nFROM {}\nCROSS JOIN LATERAL (\n{}\n) AS \"_row\"\n)",
1572 c.name,
1573 qi(iterator),
1574 qi(&binding_outer_key(&c.name)),
1575 qi(iterator),
1576 body,
1577 ),
1578 None => format!("\"{}\" AS (\n{}\n)", c.name, body),
1579 });
1580 }
1581 parts
1582}
1583
1584fn emit_cte_prefix(ctes: &[IrCteDef]) -> String {
1586 format!("WITH\n{}\n", emit_user_cte_parts(ctes).join(",\n"))
1587}
1588
1589pub(crate) fn collect_link_prop_names(vals: &IrMultiLinkValues, names: &mut Vec<String>) {
1594 for (name, _) in &vals.link_props {
1595 if !names.contains(name) {
1596 names.push(name.clone());
1597 }
1598 }
1599 match &vals.source {
1600 IrMultiLinkValueSource::Union(a, b) => {
1601 collect_link_prop_names(a, names);
1602 collect_link_prop_names(b, names);
1603 }
1604 IrMultiLinkValueSource::Asserted { inner, .. } => collect_link_prop_names(inner, names),
1605 _ => {}
1606 }
1607}
1608
1609fn emit_link_prop_cols(vals: &IrMultiLinkValues, prop_names: &[String]) -> String {
1614 prop_names
1615 .iter()
1616 .map(|name| match vals.link_props.iter().find(|(n, _)| n == name) {
1617 Some((_, expr)) => format!(", {} AS {}", emit_expr(expr), qi(name)),
1618 None => format!(", NULL AS {}", qi(name)),
1619 })
1620 .collect()
1621}
1622
1623fn emit_multilink_values_subquery(vals: &IrMultiLinkValues, prop_names: &[String]) -> String {
1629 emit_multilink_values_inner(vals, prop_names, true)
1630}
1631
1632fn emit_multilink_values_inner(vals: &IrMultiLinkValues, prop_names: &[String], bare_cte_ok: bool) -> String {
1637 if let IrMultiLinkValueSource::Union(a, b) = &vals.source {
1638 return format!(
1639 "({}\nUNION ALL\n{})",
1640 emit_multilink_values_inner(a, prop_names, false),
1641 emit_multilink_values_inner(b, prop_names, false),
1642 );
1643 }
1644
1645 if let IrMultiLinkValueSource::Asserted {
1650 fn_name,
1651 inner,
1652 message,
1653 } = &vals.source
1654 {
1655 let inner_sql = emit_multilink_values_inner(inner, &[], false);
1656 let prop_cols = emit_link_prop_cols(vals, prop_names);
1657 return format!(
1658 "(SELECT unnest(\"_pylon\".{}(ARRAY(SELECT \"_v\".\"id\" FROM {} AS \"_v\"){})) AS \"id\"{})",
1659 qi(fn_name),
1660 inner_sql,
1661 assert_message_arg(message),
1662 prop_cols,
1663 );
1664 }
1665
1666 let prop_cols = emit_link_prop_cols(vals, prop_names);
1667
1668 match &vals.source {
1669 IrMultiLinkValueSource::CteRef(name) => {
1670 if prop_cols.is_empty() && bare_cte_ok {
1671 format!("\"{}\"", name)
1673 } else {
1674 format!("(SELECT \"_s\".\"id\"{} FROM \"{}\" AS \"_s\")", prop_cols, name)
1675 }
1676 }
1677 IrMultiLinkValueSource::Select(s) => {
1678 let [IrRowSource::Bound { source, .. }] = s.rows.as_slice() else {
1683 unreachable!("IrMultiLinkValueSource::Select is always schema-bound")
1684 };
1685 let alias = &source.alias;
1686 let mut sql = format!(
1687 "(SELECT {}.\"id\"{} FROM {} AS {}",
1688 qi(alias),
1689 prop_cols,
1690 source_ref(source),
1691 qi(alias)
1692 );
1693 append_filter(&mut sql, &s.filter);
1694 sql.push(')');
1695 sql
1696 }
1697 IrMultiLinkValueSource::PathSelect(ps) => {
1698 let root_alias = &ps.root.alias;
1699 let target_alias = match &ps.result {
1702 IrPathResult::Object { alias, .. } => alias,
1703 IrPathResult::Scalar(..) => root_alias,
1704 };
1705 let mut sql = format!(
1706 "(SELECT {}.\"id\"{} FROM {} AS {}",
1707 qi(target_alias),
1708 prop_cols,
1709 source_ref(&ps.root),
1710 qi(root_alias)
1711 );
1712 for join in &ps.joins {
1713 sql.push_str(&emit_path_join_sql(join));
1714 }
1715 append_filter(&mut sql, &ps.filter);
1716 sql.push(')');
1717 sql
1718 }
1719 IrMultiLinkValueSource::Function(fs) => {
1720 let args = fs.fn_args.iter().map(emit_expr).collect::<Vec<_>>().join(", ");
1721 let mut sql = format!(
1722 "(SELECT {}.\"id\"{} FROM {}.{}({}) AS {}",
1723 qi(&fs.alias),
1724 prop_cols,
1725 pg_schema(&fs.fn_module),
1726 qi(&fs.fn_name),
1727 args,
1728 qi(&fs.alias),
1729 );
1730 append_filter(&mut sql, &fs.filter);
1731 sql.push(')');
1732 sql
1733 }
1734 IrMultiLinkValueSource::Union(..) | IrMultiLinkValueSource::Asserted { .. } => {
1735 unreachable!("handled above")
1736 }
1737 }
1738}
1739
1740fn emit_path_join_sql(join: &IrPathJoin) -> String {
1742 match join {
1743 IrPathJoin::Lateral { inner, target } => {
1744 let projected = match &inner.result {
1745 IrPathResult::Object { alias, .. } => alias.clone(),
1746 IrPathResult::Scalar(..) => unreachable!("a lateral step is only built for an object-valued computed"),
1747 };
1748 let mut body = format!(
1749 "SELECT {}.* FROM {}",
1750 qi(&projected),
1751 emit_path_joins(&inner.root, &inner.joins)
1752 );
1753 append_filter(&mut body, &inner.filter);
1754 append_order_by(&mut body, &inner.order_by);
1755 append_offset_limit(&mut body, &inner.offset, &inner.limit);
1756 format!(" JOIN LATERAL (\n{}\n) AS {} ON TRUE", body, qi(&target.alias))
1757 }
1758 IrPathJoin::Function {
1759 fn_module,
1760 fn_name,
1761 args,
1762 target,
1763 } => {
1764 let args_sql = args.iter().map(emit_expr).collect::<Vec<_>>().join(", ");
1765 format!(
1766 " CROSS JOIN LATERAL {}.{}({}) AS {}",
1767 pg_schema(fn_module),
1768 qi(fn_name),
1769 args_sql,
1770 qi(&target.alias),
1771 )
1772 }
1773 IrPathJoin::Single {
1774 source_alias,
1775 fk_col,
1776 target,
1777 } => {
1778 format!(
1779 " JOIN {} AS {} ON {}.\"id\" = {}.{}",
1780 source_ref(target),
1781 qi(&target.alias),
1782 qi(&target.alias),
1783 qi(source_alias),
1784 qi(fk_col)
1785 )
1786 }
1787 IrPathJoin::Multi {
1788 source_alias,
1789 junction_alias,
1790 join: ml_join,
1791 target,
1792 } => {
1793 let (jt_ref, src_col, tgt_col) = match ml_join {
1794 IrMultiLinkJoin::Standard { junction_table, module } => (
1795 junction_ref(module, junction_table),
1796 "source".to_string(),
1797 "target".to_string(),
1798 ),
1799 IrMultiLinkJoin::Through {
1800 junction_table,
1801 module,
1802 source_col,
1803 target_col,
1804 } => (
1805 junction_ref(module, junction_table),
1806 source_col.clone(),
1807 target_col.clone(),
1808 ),
1809 IrMultiLinkJoin::BacklinkFk { .. } | IrMultiLinkJoin::BacklinkJunction { .. } => {
1814 unreachable!("a forward multi-link path step never uses a backlink join variant")
1815 }
1816 };
1817 format!(
1818 " JOIN {} AS {} ON {}.{} = {}.\"id\" JOIN {} AS {} ON {}.{} = {}.\"id\"",
1819 jt_ref,
1820 qi(junction_alias),
1821 qi(junction_alias),
1822 qi(&src_col),
1823 qi(source_alias),
1824 source_ref(target),
1825 qi(&target.alias),
1826 qi(junction_alias),
1827 qi(&tgt_col),
1828 qi(&target.alias),
1829 )
1830 }
1831 IrPathJoin::BacklinkSingle {
1832 source_alias,
1833 fk_col,
1834 target,
1835 } => {
1836 format!(
1837 " JOIN {} AS {} ON {}.{} = {}.\"id\"",
1838 source_ref(target),
1839 qi(&target.alias),
1840 qi(&target.alias),
1841 qi(fk_col),
1842 qi(source_alias),
1843 )
1844 }
1845 IrPathJoin::BacklinkMulti {
1846 source_alias,
1847 junction_alias,
1848 junction_table,
1849 module,
1850 owner_col,
1851 current_col,
1852 target,
1853 } => {
1854 format!(
1855 " JOIN {} AS {} ON {}.{} = {}.\"id\" JOIN {} AS {} ON {}.\"id\" = {}.{}",
1856 junction_ref(module, junction_table),
1857 qi(junction_alias),
1858 qi(junction_alias),
1859 qi(current_col),
1860 qi(source_alias),
1861 source_ref(target),
1862 qi(&target.alias),
1863 qi(&target.alias),
1864 qi(junction_alias),
1865 qi(owner_col),
1866 )
1867 }
1868 }
1869}
1870
1871pub(crate) fn append_value_is_the_loop_variable(values: &IrMultiLinkValues, var: &str) -> bool {
1883 let IrMultiLinkValueSource::Select(sel) = &values.source else {
1884 return false;
1885 };
1886 let Some(IrExpr::BinOp(cmp)) = sel.filter.as_ref() else {
1887 return false;
1888 };
1889 matches!(
1890 (&cmp.left, &cmp.op, &cmp.right),
1891 (
1892 IrExpr::ColumnRef { column, .. },
1893 crate::parse::ast::BinOpKind::Eq,
1894 IrExpr::ForVar { name, .. },
1895 ) if column == "id" && name == var
1896 ) && values.link_props.is_empty()
1897}
1898
1899pub(crate) fn per_iteration_insert<'c>(mutation: &IrMultiLinkMutation, nested: &'c [IrCteDef]) -> Option<&'c IrInsert> {
1905 let IrMultiLinkValueSource::CteRef(name) = &mutation.values.source else {
1906 return None;
1907 };
1908 if !mutation.values.link_props.is_empty() {
1909 return None;
1910 }
1911 nested.iter().find(|c| &c.name == name).and_then(|c| match &c.stmt {
1912 IrStmt::Insert(ins) if ins.id_default_sql.is_some() && ins.multi_link_appends.is_empty() => Some(ins),
1913 _ => None,
1914 })
1915}
1916
1917fn emit_for_nested_insert_cte(
1922 ins: &IrInsert,
1923 ids_name: &str,
1924 iter_alias: &str,
1925 cte_name: &str,
1926 id_col: &str,
1927) -> String {
1928 let mut cols = vec![qi("id")];
1929 let mut values = vec![format!("\"{}\".{}", ids_name, qi(id_col))];
1930 for (col, expr) in &ins.assignments {
1931 cols.push(qi(col));
1932 values.push(emit_expr(expr));
1933 }
1934 format!(
1935 "\"{}\" AS (\nINSERT INTO {} ({})\nSELECT {} FROM \"{}\", {}\nWHERE \"{}\".{} = {}.\"v\"\nRETURNING {}\n)",
1936 cte_name,
1937 target_ref(&ins.target),
1938 cols.join(", "),
1939 values.join(", "),
1940 ids_name,
1941 qi(iter_alias),
1942 ids_name,
1943 qi("_iter"),
1944 qi(iter_alias),
1945 qi("id"),
1946 )
1947}
1948
1949fn emit_for_ml_append_cte(mutation: &IrMultiLinkMutation, ids_name: &str, cte_name: &str, iter_col: &str) -> String {
1950 format!(
1951 "\"{}\" AS (\nINSERT INTO {} ({}, {})\nSELECT \"{}\".\"id\", \"{}\".{} FROM \"{}\"\nON CONFLICT DO NOTHING\nRETURNING {}, {}\n)",
1952 cte_name,
1953 qn(&mutation.module, &mutation.junction_table),
1954 qi(&mutation.source_col),
1955 qi(&mutation.target_col),
1956 ids_name,
1957 ids_name,
1958 qi(iter_col),
1959 ids_name,
1960 qi(&mutation.source_col),
1961 qi(&mutation.target_col),
1962 )
1963}
1964
1965fn emit_ml_append_cte(mutation: &IrMultiLinkMutation, ids_name: &str, ids_alias: &str, cte_name: &str) -> String {
1968 let mut prop_names = vec![];
1969 collect_link_prop_names(&mutation.values, &mut prop_names);
1970 let vals_ref = emit_multilink_values_subquery(&mutation.values, &prop_names);
1971
1972 let extra_cols: String = prop_names.iter().map(|n| format!(", {}", qi(n))).collect();
1973 let extra_select: String = prop_names.iter().map(|n| format!(", \"_v\".{}", qi(n))).collect();
1974
1975 let conflict_clause = if mutation.single {
1979 let mut sets = vec![format!(
1989 "{} = EXCLUDED.{}",
1990 qi(&mutation.target_col),
1991 qi(&mutation.target_col)
1992 )];
1993 sets.extend(prop_names.iter().map(|n| format!("{} = EXCLUDED.{}", qi(n), qi(n))));
1994 format!(
1995 "ON CONFLICT ({}) DO UPDATE SET {}",
1996 qi(&mutation.source_col),
1997 sets.join(", ")
1998 )
1999 } else if prop_names.is_empty() {
2000 "ON CONFLICT DO NOTHING".to_string()
2001 } else {
2002 let sets: Vec<String> = prop_names
2003 .iter()
2004 .map(|n| format!("{} = EXCLUDED.{}", qi(n), qi(n)))
2005 .collect();
2006 format!(
2007 "ON CONFLICT ({}, {}) DO UPDATE SET {}",
2008 qi(&mutation.source_col),
2009 qi(&mutation.target_col),
2010 sets.join(", "),
2011 )
2012 };
2013
2014 let lateral = if vals_ref.starts_with('(') { "LATERAL " } else { "" };
2017 let ins = format!(
2018 "INSERT INTO {} ({}, {}{})\nSELECT {}.\"id\", \"_v\".\"id\"{} FROM \"{}\" AS {} CROSS JOIN {}{} AS \"_v\"\n{}\nRETURNING {}, {}",
2019 qn(&mutation.module, &mutation.junction_table),
2020 qi(&mutation.source_col),
2021 qi(&mutation.target_col),
2022 extra_cols,
2023 qi(ids_alias),
2024 extra_select,
2025 ids_name,
2026 qi(ids_alias),
2027 lateral,
2028 vals_ref,
2029 conflict_clause,
2030 qi(&mutation.source_col),
2031 qi(&mutation.target_col),
2032 );
2033 format!("\"{}\" AS (\n{}\n)", cte_name, ins)
2034}
2035
2036fn emit_ml_remove_cte(mutation: &IrMultiLinkMutation, ids_name: &str, ids_alias: &str, cte_name: &str) -> String {
2039 let vals_ref = emit_multilink_values_subquery(&mutation.values, &[]);
2042 let del = format!(
2046 "DELETE FROM {} AS \"_jn\"\nUSING \"{}\" AS {}\nWHERE \"_jn\".{} = {}.\"id\"\n AND \"_jn\".{} IN (SELECT \"id\" FROM {})\nRETURNING \"_jn\".{}, \"_jn\".{}",
2047 qn(&mutation.module, &mutation.junction_table),
2048 ids_name,
2049 qi(ids_alias),
2050 qi(&mutation.source_col),
2051 qi(ids_alias),
2052 qi(&mutation.target_col),
2053 vals_ref,
2054 qi(&mutation.source_col),
2055 qi(&mutation.target_col),
2056 );
2057 format!("\"{}\" AS (\n{}\n)", cte_name, del)
2058}
2059
2060use crate::ir::IrConflict;
2063
2064fn emit_conflict(sql: &mut String, conflict: &IrConflict) {
2065 let on_sql = conflict.on.as_ref().map(|e| match e {
2069 IrExpr::Tuple(elements) => format!("({})", elements.iter().map(emit_expr).collect::<Vec<_>>().join(", ")),
2070 other => format!("({})", emit_expr(other)),
2071 });
2072 match (&on_sql, &conflict.do_update) {
2073 (None, None) => sql.push_str(" ON CONFLICT DO NOTHING"),
2074 (Some(on), None) => sql.push_str(&format!(" ON CONFLICT {} DO NOTHING", on)),
2075 (None, Some(updates)) => {
2076 sql.push_str(&format!(" ON CONFLICT DO UPDATE SET {}", do_update_sets(updates)));
2077 }
2078 (Some(on), Some(updates)) => {
2079 sql.push_str(&format!(
2080 " ON CONFLICT {} DO UPDATE SET {}",
2081 on,
2082 do_update_sets(updates),
2083 ));
2084 }
2085 }
2086 if conflict.do_update.is_some()
2087 && let Some(predicate) = &conflict.do_update_where
2088 {
2089 sql.push_str(&format!(" WHERE {}", emit_expr(predicate)));
2090 }
2091}
2092
2093fn do_update_sets(updates: &[(String, IrExpr)]) -> String {
2094 updates
2095 .iter()
2096 .map(|(col, expr)| format!("{} = {}", qi(col), emit_expr(expr)))
2097 .collect::<Vec<_>>()
2098 .join(", ")
2099}
2100
2101fn is_integer_expr(expr: &IrExpr) -> bool {
2106 match expr {
2107 IrExpr::ColumnRef { pg_type, .. } => matches!(
2108 pg_type.as_str(),
2109 "int2" | "int4" | "int8" | "integer" | "bigint" | "smallint"
2110 ),
2111 IrExpr::Literal(crate::ir::IrLiteral::Int(_)) => true,
2112 IrExpr::BinOp(op) => is_integer_expr(&op.left) && is_integer_expr(&op.right),
2113 _ => false,
2114 }
2115}
2116
2117fn is_raw_scalar(expr: &IrExpr) -> bool {
2118 matches!(expr, IrExpr::Array(_))
2119 || matches!(expr, IrExpr::TypeCast(c) if c.pg_type == "jsonb")
2120 || matches!(expr, IrExpr::NamedTuple { .. })
2121 || matches!(expr, IrExpr::Tuple(_))
2122 || matches!(expr, IrExpr::JsonbField { .. })
2123 || matches!(expr, IrExpr::JsonbIndex { .. })
2124}
2125
2126fn emit_free_rows(sel: &IrSelect, rows: &[IrRowSource], ctes: &[IrCteDef]) -> SqlOutput {
2132 use crate::query::ShapeNode;
2133
2134 let items: Vec<&IrFreeExpr> = rows
2135 .iter()
2136 .map(|r| match r {
2137 IrRowSource::Free(item) => item,
2138 IrRowSource::Bound { .. } => unreachable!("mixed Bound/Free rows rejected at compile time"),
2139 })
2140 .collect();
2141
2142 if items.is_empty() {
2143 return SqlOutput {
2144 sql: "SELECT NULL AS result WHERE FALSE".to_string(),
2145 shape: ShapeDescriptor {
2146 root: ShapeNode::Scalar {
2147 name: String::new(),
2148 position: 0,
2149 },
2150 },
2151 inference_plan: None,
2152 };
2153 }
2154
2155 if items.len() == 1
2157 && let IrFreeExpr::AssertSet {
2158 fn_name,
2159 inner,
2160 message,
2161 } = items[0]
2162 {
2163 let array_sql = emit_array_source(inner) + &assert_message_arg(message);
2164 let mut sql = format!(
2165 "SELECT ROW(v) AS result FROM unnest(\"_pylon\".{}({})) AS _assert(v)",
2166 fn_name, array_sql,
2167 );
2168 if sel.distinct {
2169 sql = format!("SELECT DISTINCT * FROM ({}) AS \"_distinct\"", sql);
2170 }
2171 append_order_by(&mut sql, &sel.order_by);
2172 append_offset_limit(&mut sql, &sel.offset, &sel.limit);
2173 return SqlOutput {
2174 sql,
2175 shape: ShapeDescriptor {
2176 root: ShapeNode::Scalar {
2177 name: String::new(),
2178 position: 0,
2179 },
2180 },
2181 inference_plan: None,
2182 };
2183 }
2184
2185 let shape_root = free_item_shape(items.first().unwrap(), ctes);
2186
2187 let branches: Vec<String> = items
2188 .iter()
2189 .map(|item| match item {
2190 IrFreeExpr::Scalar(expr) => {
2191 if matches!(expr, IrExpr::ObjectSubquery(_)) {
2196 return format!("SELECT v AS result, v FROM (SELECT {} AS v) AS _obj", emit_expr(expr));
2197 }
2198 if is_raw_scalar(expr) {
2201 format!("SELECT v AS result, v FROM (SELECT {} AS v) AS _raw", emit_expr(expr))
2204 } else {
2205 let e = emit_expr(expr);
2212 let row_value = if enum_type_of_expr(expr).is_some() {
2213 "v::text"
2214 } else {
2215 "v"
2216 };
2217 let present = if items.len() > 1 { " WHERE v IS NOT NULL" } else { "" };
2221 format!("SELECT ROW({row_value}) AS result, v FROM (SELECT {e} AS v) AS _scalar{present}")
2222 }
2223 }
2224 IrFreeExpr::FreeObject(fields) => {
2225 let inner_cols: Vec<String> = fields
2231 .iter()
2232 .enumerate()
2233 .map(|(i, (_, e))| format!("{} AS \"_f{}\"", emit_expr(e), i))
2234 .collect();
2235 let row_items: Vec<String> = fields
2236 .iter()
2237 .enumerate()
2238 .map(|(i, (_, e))| {
2239 if enum_type_of_expr(e).is_some() {
2240 format!("\"_f{}\"::text", i)
2241 } else {
2242 format!("\"_f{}\"", i)
2243 }
2244 })
2245 .collect();
2246 let named_cols: Vec<String> = fields
2247 .iter()
2248 .enumerate()
2249 .map(|(i, (name, _))| format!("\"_f{}\" AS {}", i, qi(name)))
2250 .collect();
2251 format!(
2252 "SELECT ROW({}) AS result, {} FROM (SELECT {}) AS _obj",
2253 row_items.join(", "),
2254 named_cols.join(", "),
2255 inner_cols.join(", "),
2256 )
2257 }
2258 IrFreeExpr::Tuple(exprs) => {
2259 if exprs.len() == 1 {
2260 format!("SELECT ROW({}) AS result", emit_free_field_expr(&exprs[0]))
2261 } else {
2262 let parts: Vec<String> = exprs.iter().map(emit_free_field_expr).collect();
2263 format!("SELECT ({}) AS result", parts.join(", "))
2264 }
2265 }
2266 IrFreeExpr::NamedTupleRow(fields) => {
2267 let parts: Vec<String> = fields.iter().map(|(_, e)| emit_free_field_expr(e)).collect();
2268 if parts.len() == 1 {
2269 format!("SELECT ROW({}) AS result", parts[0])
2270 } else {
2271 format!("SELECT ({}) AS result", parts.join(", "))
2272 }
2273 }
2274 IrFreeExpr::AssertSet { .. } => unreachable!("AssertSet is handled by early return above"),
2275 IrFreeExpr::CtePassthrough(name) => format!("SELECT \"result\" FROM {}", qi(name)),
2276 })
2277 .collect();
2278
2279 let union_sql = branches.join("\nUNION ALL\n");
2280
2281 let mut sql = if sel.distinct {
2282 format!("SELECT DISTINCT * FROM (\n{}\n) AS \"_distinct\"", union_sql)
2284 } else {
2285 union_sql
2286 };
2287 if let Some(filter) = &sel.filter {
2291 sql = format!(
2292 "SELECT * FROM (\n{}\n) AS \"_filtered\"\nWHERE {}",
2293 sql,
2294 emit_expr(filter)
2295 );
2296 }
2297 append_order_by(&mut sql, &sel.order_by);
2298 append_offset_limit(&mut sql, &sel.offset, &sel.limit);
2299
2300 SqlOutput {
2301 sql,
2302 shape: ShapeDescriptor { root: shape_root },
2303 inference_plan: None,
2304 }
2305}
2306
2307fn enum_type_of_expr(expr: &IrExpr) -> Option<QualifiedPgType> {
2317 match expr {
2318 IrExpr::ColumnRef { pg_type, .. } => QualifiedPgType::of(pg_type),
2319 IrExpr::EnumLiteral { pg_type, .. } => Some(QualifiedPgType {
2320 name: pg_quoted_to_pylon(pg_type),
2321 is_array: false,
2322 }),
2323 _ => None,
2324 }
2325}
2326
2327fn enum_type_of_shape_expr(expr: &IrExpr) -> Option<QualifiedPgType> {
2336 if let Some(qualified) = enum_type_of_expr(expr) {
2337 return Some(qualified);
2338 }
2339 let IrExpr::Subquery(select) = expr else {
2340 return None;
2341 };
2342 let [IrRowSource::Bound { shape, .. }] = select.rows.as_slice() else {
2343 return None;
2344 };
2345 let [IrShapePointer::Scalar(scalar)] = shape.as_slice() else {
2346 return None;
2347 };
2348 QualifiedPgType::of(&scalar.pg_type)
2349}
2350
2351fn emit_free_field_expr(expr: &IrExpr) -> String {
2357 match enum_type_of_expr(expr) {
2358 Some(qualified) => format!("{}{}", emit_expr(expr), qualified.text_cast()),
2359 None => emit_expr(expr),
2360 }
2361}
2362
2363fn free_field_shape_node(name: &str, position: usize, expr: &IrExpr) -> crate::query::ShapeNode {
2366 use crate::query::{Cardinality, ShapeNode};
2367 if let IrExpr::ObjectSubquery(sel) = expr {
2368 let [IrRowSource::Bound { source, shape }] = sel.rows.as_slice() else {
2369 unreachable!("an object subquery is always schema-bound")
2370 };
2371 let (_, nodes) = build_shape(shape, &source.alias);
2372 return ShapeNode::Object {
2373 name: name.to_string(),
2374 type_name: Some(source.type_name.clone()),
2375 position,
2376 cardinality: Cardinality::Optional,
2377 pointers: prepend_type(nodes),
2378 has_implicit_id: shape_has_implicit_id(shape),
2379 };
2380 }
2381 if matches!(expr, IrExpr::ObjectPathSubquery(_) | IrExpr::ObjectPathUnion { .. }) {
2383 return expr_shape_node(name, position, expr);
2384 }
2385 if let IrExpr::ArrayFromSelect(source) = expr
2388 && matches!(
2389 source.as_ref(),
2390 IrArraySource::ObjectSelect(_)
2391 | IrArraySource::ObjectFunction(_)
2392 | IrArraySource::PathSelect(_)
2393 | IrArraySource::Group(_)
2394 )
2395 {
2396 return expr_shape_node(name, position, expr);
2397 }
2398 match enum_type_of_shape_expr(expr) {
2399 Some(qualified) => qualified.shape_node(name.to_string(), position),
2400 None => ShapeNode::Scalar {
2401 name: name.to_string(),
2402 position,
2403 },
2404 }
2405}
2406
2407fn expr_shape_node(name: &str, position: usize, expr: &IrExpr) -> crate::query::ShapeNode {
2414 use crate::query::{Cardinality, ShapeNode};
2415 match expr {
2416 IrExpr::ObjectPathUnion { branches, multi, .. } => {
2417 let first = branches.first().expect("a union has at least one branch");
2418 let IrPathResult::Object {
2419 alias,
2420 type_name,
2421 shape,
2422 } = &first.result
2423 else {
2424 unreachable!("an object path union's branches always land on objects")
2425 };
2426 let (_, pointer_nodes) = build_shape(shape, alias);
2427 let object = ShapeNode::Object {
2428 name: if *multi { String::new() } else { name.to_string() },
2429 type_name: Some(type_name.clone()),
2430 position: if *multi { 0 } else { position },
2431 cardinality: if *multi {
2432 Cardinality::Many
2433 } else {
2434 Cardinality::Optional
2435 },
2436 pointers: prepend_type(pointer_nodes),
2437 has_implicit_id: shape_has_implicit_id(shape),
2438 };
2439 if *multi {
2440 ShapeNode::Array {
2441 name: name.to_string(),
2442 position,
2443 element: Box::new(object),
2444 }
2445 } else {
2446 object
2447 }
2448 }
2449 IrExpr::ObjectPathSubquery(ps) => {
2450 let IrPathResult::Object {
2451 alias,
2452 type_name,
2453 shape,
2454 } = &ps.result
2455 else {
2456 unreachable!("an object path subquery always lands on an object")
2457 };
2458 let (_, pointer_nodes) = build_shape(shape, alias);
2459 ShapeNode::Object {
2460 name: name.to_string(),
2461 type_name: Some(type_name.clone()),
2462 position,
2463 cardinality: Cardinality::Optional,
2464 pointers: prepend_type(pointer_nodes),
2465 has_implicit_id: shape_has_implicit_id(shape),
2466 }
2467 }
2468 IrExpr::TypeCast(c) if c.tuple_shape.is_some() => {
2469 let shape = c.tuple_shape.as_ref().unwrap();
2470 ShapeNode::NamedTuple {
2471 name: name.to_string(),
2472 position,
2473 type_name: shape.type_name.clone(),
2474 members: Some(shape.members.clone()),
2475 is_free_object: false,
2476 }
2477 }
2478 IrExpr::TypeCast(c) if c.pg_type == "jsonb" => ShapeNode::Scalar {
2484 name: name.to_string(),
2485 position,
2486 },
2487 IrExpr::NamedTuple { is_free_object, .. } => ShapeNode::NamedTuple {
2488 name: name.to_string(),
2489 position,
2490 type_name: None,
2491 members: None,
2492 is_free_object: *is_free_object,
2493 },
2494 IrExpr::ArrayFromSelect(source) => match source.as_ref() {
2497 IrArraySource::ObjectFunction(fs) => {
2498 let (_, nodes) = build_shape(&fs.shape, &fs.alias);
2499 ShapeNode::Array {
2500 name: name.to_string(),
2501 position,
2502 element: Box::new(ShapeNode::Object {
2503 name: String::new(),
2504 type_name: Some(fs.type_name.clone()),
2505 position: 0,
2506 cardinality: Cardinality::Many,
2507 pointers: prepend_type(nodes),
2508 has_implicit_id: shape_has_implicit_id(&fs.shape),
2509 }),
2510 }
2511 }
2512 IrArraySource::Group(grp) => ShapeNode::Array {
2513 name: name.to_string(),
2514 position,
2515 element: Box::new(emit_group(grp).shape.root),
2516 },
2517 IrArraySource::ObjectSelect(sel) => {
2518 let [IrRowSource::Bound { source, shape }] = sel.rows.as_slice() else {
2519 unreachable!("IrArraySource::ObjectSelect is always schema-bound")
2520 };
2521 let (_, nodes) = build_shape(shape, &source.alias);
2522 ShapeNode::Array {
2523 name: name.to_string(),
2524 position,
2525 element: Box::new(ShapeNode::Object {
2526 name: String::new(),
2527 type_name: Some(source.type_name.clone()),
2528 position: 0,
2529 cardinality: Cardinality::Many,
2530 pointers: prepend_type(nodes),
2531 has_implicit_id: shape_has_implicit_id(shape),
2532 }),
2533 }
2534 }
2535 IrArraySource::PathSelect(ps) => match &ps.result {
2536 IrPathResult::Object {
2537 alias,
2538 type_name,
2539 shape,
2540 } => {
2541 let (_, pointer_nodes) = build_shape(shape, alias);
2542 ShapeNode::Array {
2543 name: name.to_string(),
2544 position,
2545 element: Box::new(ShapeNode::Object {
2546 name: String::new(),
2547 type_name: Some(type_name.clone()),
2548 position: 0,
2549 cardinality: Cardinality::Many,
2550 pointers: prepend_type(pointer_nodes),
2551 has_implicit_id: shape_has_implicit_id(shape),
2552 }),
2553 }
2554 }
2555 IrPathResult::Scalar(..) => ShapeNode::Scalar {
2556 name: name.to_string(),
2557 position,
2558 },
2559 },
2560 _ => ShapeNode::Scalar {
2561 name: name.to_string(),
2562 position,
2563 },
2564 },
2565 e if is_raw_scalar(e) => ShapeNode::RawScalar,
2566 e => free_field_shape_node(name, position, e),
2567 }
2568}
2569
2570fn free_item_shape(item: &IrFreeExpr, ctes: &[IrCteDef]) -> crate::query::ShapeNode {
2571 use crate::query::{Cardinality, ShapeNode};
2572 match item {
2573 IrFreeExpr::Scalar(e) => match e {
2574 IrExpr::TypeCast(c) if c.pg_type == "jsonb" && c.tuple_shape.is_none() => ShapeNode::JsonScalar,
2578 _ => expr_shape_node("", 0, e),
2579 },
2580 IrFreeExpr::FreeObject(fields) => ShapeNode::Object {
2581 name: String::new(),
2582 type_name: None,
2583 position: 0,
2584 cardinality: Cardinality::Many,
2585 pointers: fields
2586 .iter()
2587 .enumerate()
2588 .map(|(i, (name, e))| free_field_shape_node(name, i, e))
2589 .collect(),
2590 has_implicit_id: false,
2591 },
2592 IrFreeExpr::Tuple(exprs) => ShapeNode::Tuple {
2593 position: 0,
2594 elements: exprs
2595 .iter()
2596 .enumerate()
2597 .map(|(i, e)| free_field_shape_node("", i, e))
2598 .collect(),
2599 names: None,
2600 },
2601 IrFreeExpr::NamedTupleRow(fields) => ShapeNode::Tuple {
2602 position: 0,
2603 elements: fields
2604 .iter()
2605 .enumerate()
2606 .map(|(i, (name, e))| free_field_shape_node(name, i, e))
2607 .collect(),
2608 names: Some(fields.iter().map(|(name, _)| name.clone()).collect()),
2609 },
2610 IrFreeExpr::AssertSet { .. } => ShapeNode::Scalar {
2611 name: String::new(),
2612 position: 0,
2613 },
2614 IrFreeExpr::CtePassthrough(name) => ctes
2620 .iter()
2621 .find(|c| &c.name == name)
2622 .and_then(|c| match &c.stmt {
2623 IrStmt::Select(sel) => match sel.rows.first() {
2624 Some(IrRowSource::Free(inner)) => Some(free_item_shape(inner, ctes)),
2625 _ => None,
2626 },
2627 _ => None,
2628 })
2629 .unwrap_or(ShapeNode::Scalar {
2630 name: String::new(),
2631 position: 0,
2632 }),
2633 }
2634}
2635
2636fn emit_path_joins(root: &IrSource, joins: &[IrPathJoin]) -> String {
2639 let mut parts = vec![format!("{} AS {}", source_ref(root), qi(&root.alias))];
2640 for join in joins {
2641 match join {
2642 IrPathJoin::Single {
2643 source_alias,
2644 fk_col,
2645 target,
2646 } => {
2647 parts.push(format!(
2648 "JOIN {} AS {} ON {}.{} = {}.\"id\"",
2649 source_ref(target),
2650 qi(&target.alias),
2651 qi(source_alias),
2652 qi(fk_col),
2653 qi(&target.alias),
2654 ));
2655 }
2656 IrPathJoin::Multi {
2657 source_alias,
2658 junction_alias,
2659 join,
2660 target,
2661 } => {
2662 match join {
2663 IrMultiLinkJoin::Standard { junction_table, module } => {
2664 parts.push(format!(
2665 "JOIN {} AS {} ON {}.\"source\" = {}.\"id\"",
2666 junction_ref(module, junction_table),
2667 qi(junction_alias),
2668 qi(junction_alias),
2669 qi(source_alias),
2670 ));
2671 parts.push(format!(
2672 "JOIN {} AS {} ON {}.\"id\" = {}.\"target\"",
2673 source_ref(target),
2674 qi(&target.alias),
2675 qi(&target.alias),
2676 qi(junction_alias),
2677 ));
2678 }
2679 IrMultiLinkJoin::Through {
2680 junction_table,
2681 module,
2682 source_col,
2683 target_col,
2684 } => {
2685 parts.push(format!(
2686 "JOIN {} AS {} ON {}.{} = {}.\"id\"",
2687 junction_ref(module, junction_table),
2688 qi(junction_alias),
2689 qi(junction_alias),
2690 qi(source_col),
2691 qi(source_alias),
2692 ));
2693 parts.push(format!(
2694 "JOIN {} AS {} ON {}.\"id\" = {}.{}",
2695 source_ref(target),
2696 qi(&target.alias),
2697 qi(&target.alias),
2698 qi(junction_alias),
2699 qi(target_col),
2700 ));
2701 }
2702 IrMultiLinkJoin::BacklinkFk { .. } | IrMultiLinkJoin::BacklinkJunction { .. } => {
2704 unreachable!("a forward multi-link path step never uses a backlink join variant")
2705 }
2706 }
2707 }
2708 IrPathJoin::BacklinkSingle {
2709 source_alias,
2710 fk_col,
2711 target,
2712 } => {
2713 parts.push(format!(
2714 "JOIN {} AS {} ON {}.{} = {}.\"id\"",
2715 source_ref(target),
2716 qi(&target.alias),
2717 qi(&target.alias),
2718 qi(fk_col),
2719 qi(source_alias),
2720 ));
2721 }
2722 IrPathJoin::Function {
2726 fn_module,
2727 fn_name,
2728 args,
2729 target,
2730 } => {
2731 let args_sql = args.iter().map(emit_expr).collect::<Vec<_>>().join(", ");
2732 parts.push(format!(
2733 "CROSS JOIN LATERAL {}.{}({}) AS {}",
2734 pg_schema(fn_module),
2735 qi(fn_name),
2736 args_sql,
2737 qi(&target.alias),
2738 ));
2739 }
2740 IrPathJoin::Lateral { inner, target } => {
2741 let projected = match &inner.result {
2742 IrPathResult::Object { alias, .. } => alias.clone(),
2743 IrPathResult::Scalar(..) => {
2744 unreachable!("a lateral step is only built for an object-valued computed")
2745 }
2746 };
2747 let mut body = format!(
2748 "SELECT {}.* FROM {}",
2749 qi(&projected),
2750 emit_path_joins(&inner.root, &inner.joins)
2751 );
2752 append_filter(&mut body, &inner.filter);
2753 append_order_by(&mut body, &inner.order_by);
2754 append_offset_limit(&mut body, &inner.offset, &inner.limit);
2755 parts.push(format!("JOIN LATERAL (\n{}\n) AS {} ON TRUE", body, qi(&target.alias)));
2756 }
2757 IrPathJoin::BacklinkMulti {
2758 source_alias,
2759 junction_alias,
2760 junction_table,
2761 module,
2762 owner_col,
2763 current_col,
2764 target,
2765 } => {
2766 parts.push(format!(
2767 "JOIN {} AS {} ON {}.{} = {}.\"id\"",
2768 junction_ref(module, junction_table),
2769 qi(junction_alias),
2770 qi(junction_alias),
2771 qi(current_col),
2772 qi(source_alias),
2773 ));
2774 parts.push(format!(
2775 "JOIN {} AS {} ON {}.\"id\" = {}.{}",
2776 source_ref(target),
2777 qi(&target.alias),
2778 qi(&target.alias),
2779 qi(junction_alias),
2780 qi(owner_col),
2781 ));
2782 }
2783 }
2784 }
2785 parts.join("\n")
2786}
2787
2788fn emit_array_source(src: &IrArraySource) -> String {
2790 match src {
2791 IrArraySource::Select(s) => {
2792 let [IrRowSource::Bound { source, shape }] = s.rows.as_slice() else {
2795 unreachable!("IrArraySource::Select is always schema-bound")
2796 };
2797 let scalar = match shape.first() {
2798 Some(IrShapePointer::Scalar(sf)) => format!("{}.{}", qi(&source.alias), qi(&sf.column)),
2799 _ => format!("{}.\"id\"", qi(&source.alias)),
2800 };
2801 let mut sql = format!("SELECT {} FROM {} AS {}", scalar, source_ref(source), qi(&source.alias));
2802 append_filter(&mut sql, &s.filter);
2803 format!("ARRAY({})", sql)
2804 }
2805 IrArraySource::ObjectFunction(fs) => {
2806 let (exprs, _) = build_shape(&fs.shape, &fs.alias);
2807 let mut parts = vec![sql_str(&fs.type_name) + "::text"];
2808 parts.extend(exprs);
2809 let args_sql = fs.fn_args.iter().map(emit_expr).collect::<Vec<_>>().join(", ");
2810 let mut sql = format!(
2811 "SELECT (\n {}\n) FROM {}.{}({}) AS {}",
2812 parts.join(",\n "),
2813 pg_schema(&fs.fn_module),
2814 qi(&fs.fn_name),
2815 args_sql,
2816 qi(&fs.alias),
2817 );
2818 append_filter(&mut sql, &fs.filter);
2819 append_order_by(&mut sql, &fs.order_by);
2820 append_offset_limit(&mut sql, &fs.offset, &fs.limit);
2821 format!("ARRAY({})", sql)
2822 }
2823 IrArraySource::ObjectSelect(s) => {
2824 let [IrRowSource::Bound { source, shape }] = s.rows.as_slice() else {
2825 unreachable!("IrArraySource::ObjectSelect is always schema-bound")
2826 };
2827 let (exprs, _) = build_shape(shape, &source.alias);
2828 let mut parts = vec![source_type_disc(source)];
2829 parts.extend(exprs);
2830 let mut sql = format!(
2831 "SELECT (\n {}\n) FROM {} AS {}",
2832 parts.join(",\n "),
2833 source_ref(source),
2834 qi(&source.alias)
2835 );
2836 append_filter(&mut sql, &s.filter);
2837 append_order_by(&mut sql, &s.order_by);
2838 append_offset_limit(&mut sql, &s.offset, &s.limit);
2839 format!("ARRAY({})", sql)
2840 }
2841 IrArraySource::Group(grp) => format!("ARRAY({})", emit_group(grp).sql),
2842 IrArraySource::StmtColumn { stmt, column } => format!(
2843 "ARRAY(SELECT {} FROM (\n{}\n) AS \"_rows\")",
2844 qi(column),
2845 emit_dml_as_cte_source(stmt),
2846 ),
2847 IrArraySource::PathSelect(ps) => {
2848 let scalar = match &ps.result {
2849 IrPathResult::Scalar(e, _) => emit_expr(e),
2850 IrPathResult::Object {
2854 alias,
2855 type_name,
2856 shape,
2857 } => {
2858 let (pointer_exprs, _) = build_shape(shape, alias);
2859 let mut parts = vec![result_type_disc(ps, alias, type_name)];
2860 parts.extend(pointer_exprs);
2861 format!("(\n {}\n)", parts.join(",\n "))
2862 }
2863 };
2864 let from_sql = emit_path_joins(&ps.root, &ps.joins);
2865 let mut sql = format!(
2866 "SELECT {}{} FROM {}",
2867 if ps.distinct { "DISTINCT " } else { "" },
2868 scalar,
2869 from_sql
2870 );
2871 append_filter(&mut sql, &ps.filter);
2872 append_order_by(&mut sql, &ps.order_by);
2873 append_offset_limit(&mut sql, &ps.offset, &ps.limit);
2874 format!("ARRAY({})", sql)
2875 }
2876 IrArraySource::RawExpr {
2877 source,
2878 poly_implementors,
2879 poly_columns,
2880 expr,
2881 } => {
2882 let from_sql = if !poly_implementors.is_empty() {
2883 format!(
2884 "(\n{}\n) AS {}",
2885 emit_poly_union(poly_implementors, poly_columns),
2886 qi(&source.alias)
2887 )
2888 } else {
2889 format!("{} AS {}", source_ref(source), qi(&source.alias))
2890 };
2891 format!("ARRAY(SELECT {} FROM {})", emit_expr(expr), from_sql)
2892 }
2893 }
2894}
2895
2896fn emit_key_expr(expr: &IrExpr) -> String {
2899 if let IrExpr::ColumnRef { alias, column, pg_type } = expr
2900 && let Some(qualified) = QualifiedPgType::of(pg_type)
2901 {
2902 let col_ref = if alias.is_empty() {
2903 qi(column)
2904 } else {
2905 format!("{}.{}", qi(alias), qi(column))
2906 };
2907 return format!("{}{}", col_ref, qualified.text_cast());
2908 }
2909 emit_expr(expr)
2910}
2911
2912fn group_rows(grp: &IrGroup) -> (String, Option<String>) {
2916 let alias = &grp.source.alias;
2917 if grp.limit.is_none() && grp.offset.is_none() {
2918 return (
2919 format!("{} AS {}", source_ref(&grp.source), qi(alias)),
2920 grp.filter.as_ref().map(emit_expr),
2921 );
2922 }
2923 let ranked = format!(
2924 "(SELECT {}.*, row_number() OVER (PARTITION BY {}{}) AS \"__rk\"\n FROM {} AS {}{}) AS {}",
2925 qi(alias),
2926 group_by_sql(grp),
2927 group_element_order(grp),
2928 source_ref(&grp.source),
2929 qi(alias),
2930 grp.filter
2931 .as_ref()
2932 .map(|f| format!("\n WHERE {}", emit_expr(f)))
2933 .unwrap_or_default(),
2934 qi(alias),
2935 );
2936 let lower = grp.offset.as_ref().map(emit_expr).unwrap_or_else(|| "0".to_string());
2937 let mut conds = vec![format!("\"__rk\" > {}", lower)];
2938 if let Some(l) = &grp.limit {
2939 conds.push(format!("\"__rk\" <= {} + {}", lower, emit_expr(l)));
2940 }
2941 (ranked, Some(conds.join(" AND ")))
2942}
2943
2944fn group_by_sql(grp: &IrGroup) -> String {
2945 grp.keys
2946 .iter()
2947 .map(|(_, key_expr)| emit_expr(key_expr))
2948 .collect::<Vec<_>>()
2949 .join(", ")
2950}
2951
2952fn group_element_order(grp: &IrGroup) -> String {
2953 if grp.order_by.is_empty() {
2954 return String::new();
2955 }
2956 let s: Vec<_> = grp.order_by.iter().map(emit_sort_clause).collect();
2957 format!(" ORDER BY {}", s.join(", "))
2958}
2959
2960fn emit_group_elements_source(grp: &IrGroup) -> String {
2963 let (from_sql, cond) = group_rows(grp);
2964 let mut sql = format!("SELECT {}.*\n FROM {}", qi(&grp.source.alias), from_sql);
2965 if let Some(cond) = cond {
2966 sql.push_str(&format!("\n WHERE {}", cond));
2967 }
2968 sql
2969}
2970
2971fn emit_group(grp: &IrGroup) -> SqlOutput {
2972 match &grp.output {
2973 IrGroupOutput::Groups => emit_group_rows(grp),
2974 IrGroupOutput::Projection(projection) => emit_group_projection(
2975 grp,
2976 &projection.pointers,
2977 &projection.order_by,
2978 &projection.offset,
2979 &projection.limit,
2980 ),
2981 IrGroupOutput::Elements => emit_group_elements(grp),
2982 }
2983}
2984
2985fn emit_group_projection(
2986 grp: &IrGroup,
2987 pointers: &[IrShapePointer],
2988 order_by: &[IrSort],
2989 offset: &Option<IrExpr>,
2990 limit: &Option<IrExpr>,
2991) -> SqlOutput {
2992 let (exprs, nodes) = build_shape(pointers, &grp.source.alias);
2993 let mut parts = vec!["NULL::text".to_string()];
2994 parts.extend(exprs);
2995 let (from_sql, cond) = group_rows(grp);
2996 let mut sql = format!(
2997 "SELECT (\n {}\n) AS \"result\"\nFROM {}",
2998 parts.join(",\n "),
2999 from_sql
3000 );
3001 if let Some(cond) = cond {
3002 sql.push_str(&format!("\nWHERE {}", cond));
3003 }
3004 sql.push_str(&format!("\nGROUP BY {}", group_by_sql(grp)));
3005 if !order_by.is_empty() {
3006 let sorts: Vec<_> = order_by.iter().map(emit_sort_clause).collect();
3007 sql.push_str(&format!("\nORDER BY {}", sorts.join(", ")));
3008 }
3009 append_offset_limit(&mut sql, offset, limit);
3010 SqlOutput {
3011 sql,
3012 shape: ShapeDescriptor {
3013 root: ShapeNode::Object {
3014 name: String::new(),
3015 type_name: None,
3016 position: 0,
3017 cardinality: Cardinality::Many,
3018 pointers: nodes,
3019 has_implicit_id: false,
3020 },
3021 },
3022 inference_plan: None,
3023 }
3024}
3025
3026fn emit_group_elements(grp: &IrGroup) -> SqlOutput {
3027 let (exprs, nodes) = build_shape(&grp.shape, &grp.source.alias);
3028 let mut parts = vec![source_type_disc(&grp.source)];
3029 parts.extend(exprs);
3030 let (from_sql, cond) = group_rows(grp);
3031 let mut sql = format!(
3032 "SELECT (\n {}\n) AS \"result\"\nFROM {}",
3033 parts.join(",\n "),
3034 from_sql
3035 );
3036 if let Some(cond) = cond {
3037 sql.push_str(&format!("\nWHERE {}", cond));
3038 }
3039 SqlOutput {
3040 sql,
3041 shape: ShapeDescriptor {
3042 root: ShapeNode::Object {
3043 name: String::new(),
3044 type_name: Some(grp.source.type_name.clone()),
3045 position: 0,
3046 cardinality: Cardinality::Many,
3047 pointers: prepend_type(nodes),
3048 has_implicit_id: shape_has_implicit_id(&grp.shape),
3049 },
3050 },
3051 inference_plan: None,
3052 }
3053}
3054
3055fn emit_group_rows(grp: &IrGroup) -> SqlOutput {
3056 let alias = &grp.source.alias;
3057 let (shape_exprs, shape_nodes) = build_shape(&grp.shape, alias);
3058
3059 let mut elem_row_parts = vec![source_type_disc(&grp.source)];
3061 elem_row_parts.extend(shape_exprs);
3062 let elem_row = elem_row_parts.join(",\n ");
3063
3064 let n_keys = grp.keys.len();
3066 let grouping_pos = n_keys + 1;
3067 let elements_pos = n_keys + 2;
3068
3069 let mut key_exprs_sql: Vec<String> = vec![];
3071 let mut key_nodes: Vec<ShapeNode> = vec![];
3072 for (i, (key_name, key_expr)) in grp.keys.iter().enumerate() {
3073 let pos = i + 1;
3074 if let IrExpr::ColumnRef { pg_type, .. } = key_expr
3075 && let Some(qualified) = QualifiedPgType::of(pg_type)
3076 {
3077 key_exprs_sql.push(emit_key_expr(key_expr));
3078 key_nodes.push(qualified.shape_node(key_name.clone(), pos));
3079 continue;
3080 }
3081 key_exprs_sql.push(emit_expr(key_expr));
3082 key_nodes.push(ShapeNode::Scalar {
3083 name: key_name.clone(),
3084 position: pos,
3085 });
3086 }
3087
3088 let mut outer_parts = vec!["NULL::text".to_string()];
3090 outer_parts.extend(key_exprs_sql.clone());
3091 let key_names_sql = grp
3092 .keys
3093 .iter()
3094 .map(|(name, _)| format!("'{}'", name))
3095 .collect::<Vec<_>>()
3096 .join(", ");
3097 outer_parts.push(format!("ARRAY[{}]::text[]", key_names_sql));
3098 outer_parts.push(format!(
3099 "array_agg(ROW(\n {}\n )::record{})",
3100 elem_row,
3101 group_element_order(grp)
3102 ));
3103
3104 let outer_tuple = outer_parts.join(",\n ");
3105 let (from_sql, cond) = group_rows(grp);
3106 let mut sql = format!("SELECT (\n {}\n) AS \"result\"\nFROM {}", outer_tuple, from_sql,);
3107 if let Some(cond) = cond {
3108 sql.push_str(&format!("\nWHERE {}", cond));
3109 }
3110 sql.push_str(&format!("\nGROUP BY {}", group_by_sql(grp)));
3111
3112 let element_node = ShapeNode::Object {
3114 name: String::new(),
3115 type_name: Some(grp.source.type_name.clone()),
3116 position: 0,
3117 cardinality: Cardinality::Many,
3118 pointers: prepend_type(shape_nodes),
3119 has_implicit_id: shape_has_implicit_id(&grp.shape),
3120 };
3121
3122 let root = ShapeNode::Group {
3123 key_nodes,
3124 grouping_position: grouping_pos,
3125 elements_position: elements_pos,
3126 element: Box::new(element_node),
3127 };
3128
3129 SqlOutput {
3130 sql,
3131 shape: ShapeDescriptor { root },
3132 inference_plan: None,
3133 }
3134}
3135
3136fn emit_poly_union_type_only(implementors: &[IrPolyImplementor]) -> String {
3137 implementors
3138 .iter()
3139 .map(|imp| {
3140 format!(
3141 " SELECT {}::text AS \"__type__\" FROM {}",
3142 sql_str(&imp.type_name),
3143 qn(&imp.module, &imp.table),
3144 )
3145 })
3146 .collect::<Vec<_>>()
3147 .join("\n UNION ALL\n")
3148}
3149
3150fn emit_path_select(sel: &IrPathSelect) -> SqlOutput {
3151 let distinct = if sel.distinct { "DISTINCT " } else { "" };
3152 let from_sql = if !sel.poly_implementors.is_empty() {
3153 format!(
3154 "(\n{}\n) AS {}",
3155 emit_poly_union_type_only(&sel.poly_implementors),
3156 qi(&sel.root.alias),
3157 )
3158 } else {
3159 emit_path_joins(&sel.root, &sel.joins)
3160 };
3161
3162 let (result_expr, shape_root) = match &sel.result {
3163 IrPathResult::Scalar(ir_expr, tuple_shape) => {
3164 let is_nt = matches!(ir_expr, IrExpr::NamedTuple { .. })
3166 || matches!(ir_expr, IrExpr::Tuple(_))
3167 || matches!(ir_expr, IrExpr::JsonbField { .. })
3168 || matches!(ir_expr, IrExpr::JsonbIndex { .. })
3169 || matches!(ir_expr, IrExpr::ColumnRef { pg_type, .. } if pg_type.starts_with("__nt__:"))
3170 || tuple_shape.is_some();
3171 if is_nt {
3172 let expr_sql = format!("{} AS result", emit_expr(ir_expr));
3173 let shape = if matches!(
3174 ir_expr,
3175 IrExpr::JsonbField { .. } | IrExpr::JsonbIndex { .. } | IrExpr::Tuple(_)
3176 ) {
3177 ShapeNode::RawScalar
3178 } else if let Some(shape) = tuple_shape {
3179 ShapeNode::NamedTuple {
3185 name: String::new(),
3186 position: 0,
3187 type_name: shape.type_name.clone(),
3188 members: Some(shape.members.clone()),
3189 is_free_object: false,
3190 }
3191 } else {
3192 let type_name = match ir_expr {
3193 IrExpr::ColumnRef { pg_type, .. } => pg_type.strip_prefix("__nt__:").map(|s| s.to_string()),
3194 _ => None,
3195 };
3196 ShapeNode::NamedTuple {
3197 name: String::new(),
3198 position: 0,
3199 type_name,
3200 members: None,
3201 is_free_object: false,
3202 }
3203 };
3204 (expr_sql, shape)
3205 } else {
3206 if let IrExpr::ColumnRef { pg_type, .. } = ir_expr {
3209 if let Some(qualified) = QualifiedPgType::of(pg_type) {
3210 let expr = format!("ROW({}{}) AS result", emit_expr(ir_expr), qualified.text_cast());
3211 let shape = qualified.shape_node(String::new(), 0);
3212 (expr, shape)
3213 } else {
3214 let expr = format!("ROW({}) AS result", emit_expr(ir_expr));
3215 (
3216 expr,
3217 ShapeNode::Scalar {
3218 name: String::new(),
3219 position: 0,
3220 },
3221 )
3222 }
3223 } else {
3224 let expr = format!("ROW({}) AS result", emit_expr(ir_expr));
3225 (
3226 expr,
3227 ShapeNode::Scalar {
3228 name: String::new(),
3229 position: 0,
3230 },
3231 )
3232 }
3233 }
3234 }
3235 IrPathResult::Object {
3236 alias,
3237 type_name,
3238 shape,
3239 } => {
3240 let (pointer_exprs, pointer_nodes) = build_shape(shape, alias);
3241 let mut parts = vec![result_type_disc(sel, alias, type_name)];
3242 parts.extend(pointer_exprs);
3243 let expr = format!("(\n {}\n) AS result", parts.join(",\n "));
3244 let shape_root = ShapeNode::Object {
3245 name: String::new(),
3246 type_name: Some(type_name.clone()),
3247 position: 0,
3248 cardinality: Cardinality::Many,
3249 pointers: prepend_type(pointer_nodes),
3250 has_implicit_id: shape_has_implicit_id(shape),
3251 };
3252 (expr, shape_root)
3253 }
3254 };
3255
3256 let mut sql = format!("SELECT {}{}\nFROM {}", distinct, result_expr, from_sql);
3257 append_filter(&mut sql, &sel.filter);
3258 append_order_by(&mut sql, &sel.order_by);
3259 append_offset_limit(&mut sql, &sel.offset, &sel.limit);
3260
3261 SqlOutput {
3262 sql,
3263 shape: ShapeDescriptor { root: shape_root },
3264 inference_plan: None,
3265 }
3266}
3267
3268fn yields_jsonb(expr: &IrExpr) -> bool {
3275 match expr {
3276 IrExpr::JsonbField { .. } | IrExpr::JsonbIndex { .. } | IrExpr::NamedTuple { .. } => true,
3277 IrExpr::TypeCast(c) => c.pg_type == "jsonb",
3278 IrExpr::ColumnRef { pg_type, .. } | IrExpr::FnParam { pg_type, .. } => pg_type == "jsonb",
3279 IrExpr::CteRef { pg_type, .. } | IrExpr::ForVar { pg_type, .. } => pg_type.as_deref() == Some("jsonb"),
3280 IrExpr::FunctionCall(f) if f.return_pg_type.is_some() => f.return_pg_type.as_deref() == Some("jsonb"),
3284 IrExpr::FunctionCall(f) if f.schema.is_none() => {
3285 let mut overloads = crate::stdlib::registry().iter().filter(|d| d.name == f.name).peekable();
3286 overloads.peek().is_some() && overloads.all(|d| matches!(d.return_type, crate::stdlib::PylonType::Json))
3287 }
3288 _ => false,
3289 }
3290}
3291
3292fn emit_for_iterator(it: &IrForIterator, iter_alias: &str) -> (String, String) {
3293 match it {
3294 IrForIterator::Values { exprs, pg_type } => {
3295 let rows: Vec<String> = exprs
3296 .iter()
3297 .map(|e| format!("({}::{})", emit_expr(e), pg_type))
3298 .collect();
3299 (
3300 format!("(VALUES {}) AS {}(\"v\")", rows.join(", "), qi(iter_alias)),
3301 format!("{}(\"v\") AS (VALUES {})", qi(iter_alias), rows.join(", ")),
3302 )
3303 }
3304 IrForIterator::Query { stmt, scalar } => {
3305 let inner = emit_dml_as_cte_source(stmt);
3306 let body = if *scalar {
3310 inner
3311 } else {
3312 format!(" SELECT \"id\" AS v FROM (\n{}\n ) AS _src", inner)
3313 };
3314 (
3315 format!("(\n{}\n) AS {}", body, qi(iter_alias)),
3316 format!("{} AS (\n{}\n)", qi(iter_alias), body),
3317 )
3318 }
3319 IrForIterator::SetReturning { expr, pg_type } => {
3320 let body = format!(" SELECT {}::{} AS v", emit_expr(expr), pg_type);
3321 (
3322 format!("(\n{}\n) AS {}", body, qi(iter_alias)),
3323 format!("{} AS (\n{}\n)", qi(iter_alias), body),
3324 )
3325 }
3326 }
3327}
3328
3329fn emit_nested_for_iterator(it: &IrForIterator, iter_alias: &str, outer_alias: &str) -> String {
3334 const OUTER: &str = "_outer";
3335 match it {
3336 IrForIterator::Values { exprs, pg_type } => {
3337 let rows: Vec<String> = exprs
3338 .iter()
3339 .map(|e| format!("({}::{})", emit_expr(e), pg_type))
3340 .collect();
3341 format!(
3342 "{} AS (\nSELECT {}.\"v\" AS {}, \"_vals\".\"v\" AS v\nFROM {}, (VALUES {}) AS \"_vals\"(\"v\")\n)",
3343 qi(iter_alias),
3344 qi(outer_alias),
3345 qi(OUTER),
3346 qi(outer_alias),
3347 rows.join(", "),
3348 )
3349 }
3350 IrForIterator::Query { stmt, scalar } => {
3351 let inner = emit_dml_as_cte_source(stmt);
3352 let projected = if *scalar { "\"_src\".\"v\"" } else { "\"_src\".\"id\"" };
3353 format!(
3354 "{} AS (\nSELECT {}.\"v\" AS {}, {} AS v\nFROM {}\nCROSS JOIN LATERAL (\n{}\n) AS \"_src\"\n)",
3355 qi(iter_alias),
3356 qi(outer_alias),
3357 qi(OUTER),
3358 projected,
3359 qi(outer_alias),
3360 inner,
3361 )
3362 }
3363 IrForIterator::SetReturning { expr, pg_type } => format!(
3364 "{} AS (\nSELECT {}.\"v\" AS {}, \"_vals\".\"v\" AS v\nFROM {}, LATERAL (SELECT {}::{} AS v) AS \"_vals\"\n)",
3365 qi(iter_alias),
3366 qi(outer_alias),
3367 qi(OUTER),
3368 qi(outer_alias),
3369 emit_expr(expr),
3370 pg_type,
3371 ),
3372 }
3373}
3374
3375fn nested_for_from(inner_alias: &str, outer_alias: &str) -> String {
3378 format!(
3379 "{} JOIN {} ON {}.\"v\" = {}.\"_outer\"",
3380 qi(inner_alias),
3381 qi(outer_alias),
3382 qi(outer_alias),
3383 qi(inner_alias),
3384 )
3385}
3386
3387fn uncorrelated_body_ctes(f: &IrFor) -> Vec<IrCteDef> {
3401 f.body_ctes
3402 .iter()
3403 .map(|cte| IrCteDef {
3404 correlated_to: None,
3405 ..cte.clone()
3406 })
3407 .collect()
3408}
3409
3410fn prefix_body_ctes(sql: String, body_ctes: &[IrCteDef]) -> String {
3411 if body_ctes.is_empty() {
3412 return sql;
3413 }
3414 merge_into_existing_with(&sql, &emit_user_cte_parts(body_ctes))
3415 .unwrap_or_else(|| format!("{}{}", emit_cte_prefix(body_ctes), sql))
3416}
3417
3418fn emit_for_stmt(f: &IrFor, user_ctes: &[IrCteDef]) -> SqlOutput {
3419 let iter_alias = format!("_for_{}", f.var_name);
3420
3421 if matches!(&f.iterator, IrForIterator::Values { exprs, .. } if exprs.is_empty()) {
3422 let empty = SqlOutput {
3423 sql: "SELECT NULL AS result WHERE FALSE".to_string(),
3424 shape: ShapeDescriptor {
3425 root: ShapeNode::Scalar {
3426 name: String::new(),
3427 position: 0,
3428 },
3429 },
3430 inference_plan: None,
3431 };
3432 return empty;
3433 }
3434
3435 let (values_from, iter_cte) = emit_for_iterator(&f.iterator, &iter_alias);
3436
3437 match f.body.as_ref() {
3438 IrStmt::Insert(ins) => emit_for_insert(ins, &iter_alias, &iter_cte, user_ctes, &f.body_ctes),
3439 IrStmt::Update(upd) => emit_for_update(upd, &iter_alias, &iter_cte, user_ctes, &f.body_ctes),
3440 body => {
3441 let body_out = match body {
3442 IrStmt::Select(sel) => emit_select_stmt(sel, user_ctes),
3443 IrStmt::PathSelect(sel) => emit_path_select(sel),
3444 other => unreachable!("for-loop body should have been rejected at compile time: {other:?}"),
3447 };
3448 let indent_body = prefix_body_ctes(body_out.sql, &uncorrelated_body_ctes(f)).replace('\n', "\n ");
3449 let cte_prefix = if !user_ctes.is_empty() {
3450 emit_cte_prefix(user_ctes)
3451 } else {
3452 String::new()
3453 };
3454 let sql = format!(
3455 "{}SELECT \"_body\".result\nFROM {}\nCROSS JOIN LATERAL (\n {}\n) AS \"_body\"",
3456 cte_prefix, values_from, indent_body,
3457 );
3458 SqlOutput {
3459 sql,
3460 shape: body_out.shape,
3461 inference_plan: None,
3462 }
3463 }
3464 }
3465}
3466
3467fn emit_for_update(
3472 upd: &IrUpdate,
3473 iter_alias: &str,
3474 iter_cte: &str,
3475 user_ctes: &[IrCteDef],
3476 body_ctes: &[IrCteDef],
3477) -> SqlOutput {
3478 let mut cte_parts: Vec<String> = emit_user_cte_parts(user_ctes);
3479 cte_parts.push(iter_cte.to_string());
3482 cte_parts.extend(emit_user_cte_parts(body_ctes));
3483
3484 let alias = &upd.target.alias;
3485 if upd.assignments.is_empty() && upd.rewrites.is_empty() && !upd.multi_link_appends.is_empty() {
3489 const ITER_COL: &str = "_iter";
3490 let ids_name = "_ids";
3491 let generated: Vec<Option<&IrInsert>> = upd
3495 .multi_link_appends
3496 .iter()
3497 .map(|a| per_iteration_insert(a, &upd.nested_ctes))
3498 .collect();
3499 let new_cols: String = generated
3500 .iter()
3501 .enumerate()
3502 .filter_map(|(i, ins)| {
3503 ins.map(|ins| {
3504 format!(
3505 ", {} AS {}",
3506 ins.id_default_sql.as_deref().unwrap_or("uuidv7()"),
3507 qi(&format!("_new_{i}"))
3508 )
3509 })
3510 })
3511 .collect();
3512 let mut ids_sql = format!(
3513 "\"{}\" AS (\nSELECT {}.*, {}.\"v\" AS {}{} FROM {} AS {}, {}",
3514 ids_name,
3515 qi(alias),
3516 qi(iter_alias),
3517 qi(ITER_COL),
3518 new_cols,
3519 target_ref(&upd.target),
3520 qi(alias),
3521 qi(iter_alias),
3522 );
3523 append_filter(&mut ids_sql, &upd.filter);
3524 ids_sql.push_str("\n)");
3525 cte_parts.push(ids_sql);
3526 for (i, append) in upd.multi_link_appends.iter().enumerate() {
3527 match generated[i] {
3528 Some(ins) => {
3529 cte_parts.push(emit_for_nested_insert_cte(
3530 ins,
3531 ids_name,
3532 iter_alias,
3533 &format!("_ml_ins_{i}"),
3534 &format!("_new_{i}"),
3535 ));
3536 cte_parts.push(emit_for_ml_append_cte(
3537 append,
3538 ids_name,
3539 &format!("_ml_add_{i}"),
3540 &format!("_new_{i}"),
3541 ));
3542 }
3543 None => cte_parts.push(emit_for_ml_append_cte(
3544 append,
3545 ids_name,
3546 &format!("_ml_add_{i}"),
3547 ITER_COL,
3548 )),
3549 }
3550 }
3551 let (pointer_exprs, shape_nodes) = build_shape(&upd.returning, ids_name);
3552 let mut parts = vec![type_disc(&upd.target.type_name)];
3553 parts.extend(pointer_exprs);
3554 let sql = format!(
3555 "WITH {}\nSELECT (\n {}\n) AS result\nFROM \"{}\"",
3556 cte_parts.join(",\n"),
3557 parts.join(",\n "),
3558 ids_name,
3559 );
3560 return SqlOutput {
3561 sql,
3562 shape: crate::query::ShapeDescriptor {
3563 root: ShapeNode::Object {
3564 name: String::new(),
3565 type_name: Some(upd.target.type_name.clone()),
3566 position: 0,
3567 cardinality: Cardinality::Many,
3568 pointers: prepend_type(shape_nodes),
3569 has_implicit_id: shape_has_implicit_id(&upd.returning),
3570 },
3571 },
3572 inference_plan: None,
3573 };
3574 }
3575 let sets: Vec<String> = upd
3576 .assignments
3577 .iter()
3578 .map(|(col, expr)| format!("{} = {}", qi(col), emit_expr(expr)))
3579 .chain(
3580 upd.rewrites
3581 .iter()
3582 .map(|rw| format!("{} = {}", qi(&rw.column), emit_expr(&rw.expr))),
3583 )
3584 .collect();
3585 if !upd.poly_implementors.is_empty() {
3586 return emit_for_poly_update(upd, iter_alias, &sets, cte_parts);
3587 }
3588 let mut sql = format!(
3589 "WITH {}\nUPDATE {} AS {}\nSET {}\nFROM {}",
3590 cte_parts.join(",\n"),
3591 target_ref(&upd.target),
3592 qi(alias),
3593 sets.join(", "),
3594 qi(iter_alias),
3595 );
3596 append_filter(&mut sql, &upd.filter);
3597 let mut parts = vec![type_disc(&upd.target.type_name)];
3598 let (pointer_exprs, shape_nodes) = build_shape(&upd.returning, alias);
3599 parts.extend(pointer_exprs);
3600 sql.push_str(&format!("\nRETURNING (\n {}\n) AS result", parts.join(",\n ")));
3601 SqlOutput {
3602 sql,
3603 shape: crate::query::ShapeDescriptor {
3604 root: ShapeNode::Object {
3605 name: String::new(),
3606 type_name: Some(upd.target.type_name.clone()),
3607 position: 0,
3608 cardinality: Cardinality::Many,
3609 pointers: prepend_type(shape_nodes),
3610 has_implicit_id: shape_has_implicit_id(&upd.returning),
3611 },
3612 },
3613 inference_plan: None,
3614 }
3615}
3616
3617const OUTER_KEY: &str = "_outer";
3622
3623fn binding_outer_key(cte_name: &str) -> String {
3628 format!("_outer__{cte_name}")
3629}
3630
3631fn emit_iteration_rows_cte(ins: &IrInsert, cte_name: &str, iter_alias: &str, outer_source: &str) -> String {
3636 let rewrite_cols: std::collections::HashSet<&str> = ins.rewrites.iter().map(|r| r.column.as_str()).collect();
3637 let mut selected = vec![
3638 format!(
3639 "{} AS {}",
3640 ins.id_default_sql.as_deref().unwrap_or("uuidv7()"),
3641 qi("id")
3642 ),
3643 format!("{} AS {}", outer_source, qi(OUTER_KEY)),
3644 ];
3645 selected.extend(
3646 ins.assignments
3647 .iter()
3648 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
3649 .map(|(c, e)| format!("{} AS {}", emit_expr(e), qi(c)))
3650 .chain(
3651 ins.rewrites
3652 .iter()
3653 .map(|r| format!("{} AS {}", emit_expr(&r.expr), qi(&r.column))),
3654 ),
3655 );
3656 let mut sql = format!(
3657 "\"{}\" AS (\nSELECT {}\nFROM {}",
3658 cte_name,
3659 selected.join(", "),
3660 qi(iter_alias),
3661 );
3662 append_insert_guard(&mut sql, ins);
3663 sql.push_str("\n)");
3664 sql
3665}
3666
3667fn emit_insert_from_rows_cte(ins: &IrInsert, cte_name: &str, rows_name: &str) -> String {
3669 let rewrite_cols: std::collections::HashSet<&str> = ins.rewrites.iter().map(|r| r.column.as_str()).collect();
3670 let cols: Vec<String> = std::iter::once(qi("id"))
3671 .chain(
3672 ins.assignments
3673 .iter()
3674 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
3675 .map(|(c, _)| qi(c)),
3676 )
3677 .chain(ins.rewrites.iter().map(|r| qi(&r.column)))
3678 .collect();
3679 let mut sql = format!(
3680 "INSERT INTO {} ({})\nSELECT {} FROM \"{}\"",
3681 target_ref(&ins.target),
3682 cols.join(", "),
3683 cols.join(", "),
3684 rows_name,
3685 );
3686 if let Some(conflict) = &ins.unless_conflict {
3687 emit_conflict(&mut sql, conflict);
3688 }
3689 sql.push_str("\nRETURNING *");
3690 format!("\"{}\" AS (\n{}\n)", cte_name, sql)
3691}
3692
3693fn emit_correlated_ml_append_cte(
3697 mutation: &IrMultiLinkMutation,
3698 rows_name: &str,
3699 targets_name: &str,
3700 cte_name: &str,
3701) -> String {
3702 format!(
3703 "\"{}\" AS (\nINSERT INTO {} ({}, {})\nSELECT \"_src\".{}, \"_tgt\".{} FROM \"{}\" AS \"_src\" JOIN \"{}\" AS \"_tgt\" ON \"_tgt\".{} = \"_src\".{}\nON CONFLICT DO NOTHING\nRETURNING {}, {}\n)",
3704 cte_name,
3705 qn(&mutation.module, &mutation.junction_table),
3706 qi(&mutation.source_col),
3707 qi(&mutation.target_col),
3708 qi("id"),
3709 qi("id"),
3710 rows_name,
3711 targets_name,
3712 qi(OUTER_KEY),
3713 qi(OUTER_KEY),
3714 qi(&mutation.source_col),
3715 qi(&mutation.target_col),
3716 )
3717}
3718
3719fn correlated_append_indices(appends: &[IrMultiLinkMutation], defs: &[&[IrCteDef]]) -> Vec<usize> {
3722 appends
3723 .iter()
3724 .enumerate()
3725 .filter(|(_, append)| match &append.values.source {
3726 IrMultiLinkValueSource::CteRef(target) => defs
3727 .iter()
3728 .flat_map(|group| group.iter())
3729 .any(|c| &c.name == target && c.correlated_to.is_some()),
3730 _ => false,
3731 })
3732 .map(|(i, _)| i)
3733 .collect()
3734}
3735
3736fn emit_for_poly_update(upd: &IrUpdate, iter_alias: &str, sets: &[String], mut cte_parts: Vec<String>) -> SqlOutput {
3740 let alias = &upd.target.alias;
3741 let mut union_parts = vec![];
3742 for (i, imp) in upd.poly_implementors.iter().enumerate() {
3743 let cte_name = format!("_u{}", i);
3744 let mut upd_sql = format!(
3745 "UPDATE {} AS {}\nSET {}\nFROM {}",
3746 qn(&imp.module, &imp.table),
3747 qi(alias),
3748 sets.join(", "),
3749 qi(iter_alias),
3750 );
3751 append_filter(&mut upd_sql, &upd.filter);
3752 upd_sql.push_str(&format!("\nRETURNING {}.\"id\"", qi(alias)));
3753 cte_parts.push(format!("\"{}\" AS (\n{}\n)", cte_name, upd_sql));
3754
3755 let r_alias = format!("_r{}", i);
3756 union_parts.push(format!(
3757 "SELECT ROW({}::text, {}.\"id\") AS result FROM \"{}\" AS {}",
3758 sql_str(&imp.type_name),
3759 qi(&r_alias),
3760 cte_name,
3761 qi(&r_alias),
3762 ));
3763 }
3764 let sql = format!("WITH {}\n{}", cte_parts.join(",\n"), union_parts.join("\nUNION ALL\n"));
3765 let (shape, _) = emit_returning_shape(&upd.target, &upd.returning, true);
3766 SqlOutput {
3767 sql,
3768 shape,
3769 inference_plan: None,
3770 }
3771}
3772
3773fn emit_for_insert(
3774 ins: &IrInsert,
3775 iter_alias: &str,
3776 iter_cte: &str,
3777 user_ctes: &[IrCteDef],
3778 body_ctes: &[IrCteDef],
3779) -> SqlOutput {
3780 let rewrite_cols: std::collections::HashSet<&str> = ins.rewrites.iter().map(|r| r.column.as_str()).collect();
3781
3782 let cols: Vec<String> = ins
3783 .assignments
3784 .iter()
3785 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
3786 .map(|(c, _)| qi(c))
3787 .chain(ins.rewrites.iter().map(|r| qi(&r.column)))
3788 .collect();
3789 let sel_exprs: Vec<String> = ins
3790 .assignments
3791 .iter()
3792 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
3793 .map(|(_, e)| emit_expr(e))
3794 .chain(ins.rewrites.iter().map(|r| emit_expr(&r.expr)))
3795 .collect();
3796
3797 let mut cte_parts: Vec<String> = emit_user_cte_parts(user_ctes);
3798 cte_parts.push(iter_cte.to_string());
3802 cte_parts.extend(emit_user_cte_parts(body_ctes));
3803 cte_parts.extend(emit_user_cte_parts(&ins.nested_ctes));
3804 let correlated_targets = correlated_append_indices(&ins.multi_link_appends, &[body_ctes, &ins.nested_ctes]);
3807
3808 let mut insert_sql = format!(
3809 "INSERT INTO {} ({})\nSELECT {} FROM {}",
3810 target_ref(&ins.target),
3811 cols.join(", "),
3812 sel_exprs.join(", "),
3813 qi(iter_alias),
3814 );
3815 append_insert_guard(&mut insert_sql, ins);
3816 if let Some(conflict) = &ins.unless_conflict {
3817 emit_conflict(&mut insert_sql, conflict);
3818 }
3819 let (shape, returning_sql) = emit_returning_shape(&ins.target, &ins.returning, false);
3820
3821 if ins.multi_link_appends.is_empty() {
3822 let mut sql = format!("WITH {}\n{}", cte_parts.join(",\n"), insert_sql);
3823 if let Some(r) = returning_sql {
3824 sql.push_str(&r);
3825 }
3826 return SqlOutput {
3827 sql,
3828 shape,
3829 inference_plan: None,
3830 };
3831 }
3832
3833 const IDS: &str = "_for_dml__ids";
3836 const ROWS: &str = "_for_dml__rows";
3837 if correlated_targets.is_empty() {
3838 insert_sql.push_str("\nRETURNING *");
3839 cte_parts.push(format!("\"{}\" AS (\n{}\n)", IDS, insert_sql));
3840 } else {
3841 let outer_source = format!("{}.{}", qi(iter_alias), qi("v"));
3842 cte_parts.push(emit_iteration_rows_cte(ins, ROWS, iter_alias, &outer_source));
3843 cte_parts.push(emit_insert_from_rows_cte(ins, IDS, ROWS));
3844 }
3845 for (i, append) in ins.multi_link_appends.iter().enumerate() {
3846 let cte_name = format!("_for_dml__ml_add_{}", i);
3847 if correlated_targets.contains(&i) {
3848 let IrMultiLinkValueSource::CteRef(target) = &append.values.source else {
3849 unreachable!("only a CTE-backed value can carry an iteration key")
3850 };
3851 cte_parts.push(emit_correlated_ml_append_cte(append, ROWS, target, &cte_name));
3852 } else {
3853 cte_parts.push(emit_ml_append_cte(append, IDS, &ins.target.alias, &cte_name));
3854 }
3855 }
3856
3857 let projection = match &returning_sql {
3858 Some(r) => r.trim_start_matches('\n').replacen("RETURNING ", "SELECT ", 1),
3861 None => "SELECT 1".to_string(),
3862 };
3863 SqlOutput {
3864 sql: format!("WITH {}\n{}\nFROM \"{}\"", cte_parts.join(",\n"), projection, IDS),
3865 shape,
3866 inference_plan: None,
3867 }
3868}
3869
3870fn enqueue_cte_sql(eq: &VectorEnqueueInfo, source_cte: &str, cte_name: &str) -> String {
3876 let index_name_sql = match &eq.index_name {
3877 None => "NULL".to_string(),
3878 Some(name) => sql_str(name),
3879 };
3880 format!(
3881 concat!(
3882 "\"{}\" AS (\n",
3883 " INSERT INTO _pylon.\"IndexOutbox\"\n",
3884 " (object_id, type_name, index_kind, index_name)\n",
3885 " SELECT \"id\", {}, 'Vector'::_pylon.\"IndexKind\", {}\n",
3886 " FROM \"{}\"\n",
3887 " ON CONFLICT (object_id, index_kind, index_name)\n",
3888 " DO UPDATE SET status = 'Pending', enqueued_at = now()\n",
3889 ")",
3890 ),
3891 cte_name,
3892 sql_str(&eq.type_name),
3893 index_name_sql,
3894 source_cte,
3895 )
3896}
3897
3898fn enqueue_ctes(enqueue: &[VectorEnqueueInfo], source_cte: &str) -> Vec<String> {
3900 enqueue
3901 .iter()
3902 .enumerate()
3903 .map(|(i, eq)| enqueue_cte_sql(eq, source_cte, &format!("_eq{}", i)))
3904 .collect()
3905}
3906
3907fn search_backend_index_kind(backend: &crate::schema::SearchBackend) -> &'static str {
3911 match backend {
3912 crate::schema::SearchBackend::OpenSearch => "OpenSearch",
3913 crate::schema::SearchBackend::Meilisearch => "Meilisearch",
3914 crate::schema::SearchBackend::Postgres => {
3915 unreachable!("Postgres-backed search indexes are never collected into SearchEnqueueInfo")
3916 }
3917 }
3918}
3919
3920fn enqueue_search_cte_sql(eq: &SearchEnqueueInfo, source_cte: &str, cte_name: &str) -> String {
3922 let index_name_sql = match &eq.index_name {
3923 None => "NULL".to_string(),
3924 Some(name) => sql_str(name),
3925 };
3926 format!(
3927 concat!(
3928 "\"{}\" AS (\n",
3929 " INSERT INTO _pylon.\"IndexOutbox\"\n",
3930 " (object_id, type_name, index_kind, index_name, operation)\n",
3931 " SELECT \"id\", {}, '{}'::_pylon.\"IndexKind\", {}, {}\n",
3932 " FROM \"{}\"\n",
3933 " ON CONFLICT (object_id, index_kind, index_name)\n",
3934 " DO UPDATE SET status = 'Pending', operation = EXCLUDED.operation, enqueued_at = now()\n",
3935 ")",
3936 ),
3937 cte_name,
3938 sql_str(&eq.type_name),
3939 search_backend_index_kind(&eq.backend),
3940 index_name_sql,
3941 sql_str(eq.operation),
3942 source_cte,
3943 )
3944}
3945
3946fn enqueue_search_ctes(enqueue: &[SearchEnqueueInfo], source_cte: &str, offset: usize) -> Vec<String> {
3948 enqueue
3949 .iter()
3950 .enumerate()
3951 .map(|(i, eq)| enqueue_search_cte_sql(eq, source_cte, &format!("_es{}", offset + i)))
3952 .collect()
3953}
3954
3955fn shape_select_from_cte(
3958 target: &IrSource,
3959 returning: &[IrShapePointer],
3960 cte_name: &str,
3961) -> (ShapeDescriptor, Option<String>) {
3962 if returning.is_empty() {
3963 return (
3964 ShapeDescriptor {
3965 root: ShapeNode::Scalar {
3966 name: String::new(),
3967 position: 0,
3968 },
3969 },
3970 None,
3971 );
3972 }
3973 let (pointer_exprs, shape_pointers) = build_shape(returning, "");
3974 let mut parts = vec![type_disc(&target.type_name)];
3975 parts.extend(pointer_exprs);
3976 let tuple = parts.join(",\n ");
3977 let sql = format!("SELECT (\n {}\n) AS result\nFROM {}", tuple, qi(cte_name));
3978 let root_pointers = prepend_type(shape_pointers);
3979 let shape = ShapeDescriptor {
3980 root: ShapeNode::Object {
3981 name: String::new(),
3982 type_name: Some(target.type_name.clone()),
3983 position: 0,
3984 cardinality: Cardinality::Required,
3985 pointers: root_pointers,
3986 has_implicit_id: shape_has_implicit_id(returning),
3987 },
3988 };
3989 (shape, Some(sql))
3990}
3991
3992fn emit_insert_row_sql(ins: &IrInsert) -> String {
4001 let rewrite_cols: std::collections::HashSet<&str> = ins.rewrites.iter().map(|r| r.column.as_str()).collect();
4002 let cols: Vec<String> = ins
4003 .assignments
4004 .iter()
4005 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
4006 .map(|(c, _)| qi(c))
4007 .chain(ins.rewrites.iter().map(|r| qi(&r.column)))
4008 .collect();
4009 let vals: Vec<String> = ins
4010 .assignments
4011 .iter()
4012 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
4013 .map(|(_, e)| emit_expr(e))
4014 .chain(ins.rewrites.iter().map(|r| emit_expr(&r.expr)))
4015 .collect();
4016 if let Some(guard) = &ins.guard {
4019 let from_ctes = nested_cte_from(&ins.nested_ctes, &vals.join(","), " ");
4020 let projection = if cols.is_empty() {
4021 String::new()
4022 } else {
4023 format!(" {}", vals.join(", "))
4024 };
4025 let column_list = if cols.is_empty() {
4026 String::new()
4027 } else {
4028 format!(" ({})", cols.join(", "))
4029 };
4030 return format!(
4031 "INSERT INTO {}{} SELECT{}{} WHERE {}",
4032 target_ref(&ins.target),
4033 column_list,
4034 projection,
4035 from_ctes,
4036 emit_expr(guard),
4037 );
4038 }
4039 if cols.is_empty() && ins.nested_ctes.is_empty() {
4040 format!("INSERT INTO {} DEFAULT VALUES", target_ref(&ins.target))
4043 } else if ins.nested_ctes.is_empty() {
4044 format!(
4045 "INSERT INTO {} ({}) VALUES ({})",
4046 target_ref(&ins.target),
4047 cols.join(", "),
4048 vals.join(", ")
4049 )
4050 } else {
4051 format!(
4052 "INSERT INTO {} ({}) SELECT {}{}",
4053 target_ref(&ins.target),
4054 cols.join(", "),
4055 vals.join(", "),
4056 nested_cte_from(&ins.nested_ctes, &vals.join(","), " "),
4057 )
4058 }
4059}
4060
4061fn emit_insert_stmt(ins: &IrInsert) -> SqlOutput {
4062 if ins.enqueue_vector.is_empty()
4063 && ins.enqueue_search.is_empty()
4064 && !insert_has_any_multilink(ins)
4065 && ins.nested_ctes.is_empty()
4066 {
4067 let mut sql = emit_insert_row_sql(ins);
4068 if let Some(conflict) = &ins.unless_conflict {
4069 emit_conflict(&mut sql, conflict);
4070 }
4071 let (shape, returning_sql) = emit_returning_shape(&ins.target, &ins.returning, false);
4072 if let Some(r) = returning_sql {
4073 sql.push_str(&r);
4074 }
4075 return SqlOutput {
4076 sql,
4077 shape,
4078 inference_plan: None,
4079 };
4080 }
4081
4082 let mut cte_parts = if insert_has_any_multilink(ins) {
4088 emit_insert_multilink_ctes(ins, "_w")
4089 } else {
4090 let mut cte_parts = emit_user_cte_parts(&ins.nested_ctes);
4091 let mut insert_sql = emit_insert_row_sql(ins);
4092 if let Some(conflict) = &ins.unless_conflict {
4093 emit_conflict(&mut insert_sql, conflict);
4094 }
4095 insert_sql.push_str("\nRETURNING \"id\"");
4096 cte_parts.push(format!("\"_w\" AS (\n{}\n)", insert_sql));
4097 cte_parts
4098 };
4099 cte_parts.extend(enqueue_ctes(&ins.enqueue_vector, "_w"));
4100 cte_parts.extend(enqueue_search_ctes(&ins.enqueue_search, "_w", ins.enqueue_vector.len()));
4101
4102 let (shape, select_sql) = shape_select_from_cte(&ins.target, &ins.returning, "_w");
4103 let sql = format!(
4104 "WITH\n{}\n{}",
4105 cte_parts.join(",\n"),
4106 select_sql.unwrap_or_else(|| "SELECT * FROM \"_w\"".to_string()),
4107 );
4108 SqlOutput {
4109 sql,
4110 shape,
4111 inference_plan: None,
4112 }
4113}
4114
4115fn emit_poly_update_stmt(upd: &IrUpdate, user_ctes: &[IrCteDef]) -> SqlOutput {
4118 let alias = &upd.target.alias;
4119 let sets = update_set_fragments(&upd.assignments, &upd.rewrites, "");
4120
4121 let mut cte_parts: Vec<String> = emit_user_cte_parts(user_ctes);
4127 cte_parts.extend(emit_user_cte_parts(&upd.nested_ctes));
4128 let mut union_parts = vec![];
4129 let from_ctes = nested_cte_from(&upd.nested_ctes, &sets.join(","), "\n");
4130
4131 let has_any_multilink = !upd.multi_link_clears.is_empty()
4132 || !upd.multi_link_replaces.is_empty()
4133 || !upd.multi_link_appends.is_empty()
4134 || !upd.multi_link_removals.is_empty();
4135 for (i, imp) in upd.poly_implementors.iter().enumerate() {
4136 let cte_name = format!("_u{}", i);
4137 if has_any_multilink {
4141 cte_parts.extend(emit_update_multilink_cte_parts(
4142 &concrete_poly_update(upd, imp),
4143 &cte_name,
4144 false,
4145 ));
4146 } else {
4147 let mut upd_sql = format!(
4148 "UPDATE {} AS {}\nSET {}{}",
4149 qn(&imp.module, &imp.table),
4150 qi(alias),
4151 sets.join(", "),
4152 from_ctes,
4153 );
4154 append_filter(&mut upd_sql, &upd.filter);
4155 upd_sql.push_str(&format!("\nRETURNING {}.\"id\"", qi(alias)));
4156 cte_parts.push(format!("\"{}\" AS (\n{}\n)", cte_name, upd_sql));
4157 }
4158
4159 let r_alias = format!("_r{}", i);
4160 union_parts.push(format!(
4161 "SELECT ROW({}::text, {}.\"id\") AS result FROM \"{}\" AS {}",
4162 sql_str(&imp.type_name),
4163 qi(&r_alias),
4164 cte_name,
4165 qi(&r_alias),
4166 ));
4167 }
4168
4169 let sql = format!("WITH\n{}\n{}", cte_parts.join(",\n"), union_parts.join("\nUNION ALL\n"),);
4170
4171 let (shape, _) = emit_returning_shape(&upd.target, &upd.returning, true);
4172 SqlOutput {
4173 sql,
4174 shape,
4175 inference_plan: None,
4176 }
4177}
4178
4179fn emit_update_stmt(upd: &IrUpdate, user_ctes: &[IrCteDef]) -> SqlOutput {
4180 if !upd.poly_implementors.is_empty() {
4181 return emit_poly_update_stmt(upd, user_ctes);
4182 }
4183 let alias = &upd.target.alias;
4184 let (shape, returning_sql) = emit_returning_shape(&upd.target, &upd.returning, true);
4185
4186 let has_any_multilink = !upd.multi_link_clears.is_empty()
4187 || !upd.multi_link_replaces.is_empty()
4188 || !upd.multi_link_appends.is_empty()
4189 || !upd.multi_link_removals.is_empty();
4190
4191 if !has_any_multilink && upd.enqueue_vector.is_empty() && upd.enqueue_search.is_empty() {
4192 let sets = update_set_fragments(&upd.assignments, &upd.rewrites, "");
4199 let mut sql = format!(
4200 "UPDATE {} AS {}\nSET {}",
4201 target_ref(&upd.target),
4202 qi(alias),
4203 sets.join(", "),
4204 );
4205 sql.push_str(&nested_cte_from(&upd.nested_ctes, &sets.join(","), "\n"));
4206 append_filter(&mut sql, &upd.filter);
4207 if let Some(r) = returning_sql {
4208 sql.push_str(&r);
4209 }
4210 let combined_ctes: Vec<IrCteDef> = user_ctes
4213 .iter()
4214 .cloned()
4215 .chain(upd.nested_ctes.iter().cloned())
4216 .collect();
4217 if !combined_ctes.is_empty() {
4218 sql = format!("{}{}", emit_cte_prefix(&combined_ctes), sql);
4219 }
4220 return SqlOutput {
4221 sql,
4222 shape,
4223 inference_plan: None,
4224 };
4225 }
4226
4227 if !has_any_multilink && (!upd.enqueue_vector.is_empty() || !upd.enqueue_search.is_empty()) {
4228 let sets = update_set_fragments(&upd.assignments, &upd.rewrites, "");
4230 let mut upd_sql = format!(
4231 " UPDATE {} AS {}\n SET {}",
4232 target_ref(&upd.target),
4233 qi(alias),
4234 sets.join(", "),
4235 );
4236 upd_sql.push_str(&nested_cte_from(&upd.nested_ctes, &sets.join(","), "\n "));
4237 append_filter(&mut upd_sql, &upd.filter);
4238 upd_sql.push_str("\n RETURNING \"id\"");
4239
4240 let mut cte_parts: Vec<String> = emit_user_cte_parts(user_ctes);
4241 cte_parts.extend(emit_user_cte_parts(&upd.nested_ctes));
4242 cte_parts.push(format!("\"_w\" AS (\n{}\n)", upd_sql));
4243 cte_parts.extend(enqueue_ctes(&upd.enqueue_vector, "_w"));
4244 cte_parts.extend(enqueue_search_ctes(&upd.enqueue_search, "_w", upd.enqueue_vector.len()));
4245
4246 let (shape2, select_sql) = shape_select_from_cte(&upd.target, &upd.returning, "_w");
4247 let sql = format!(
4248 "WITH\n{}\n{}",
4249 cte_parts.join(",\n"),
4250 select_sql.unwrap_or_else(|| "SELECT * FROM \"_w\"".to_string()),
4251 );
4252 return SqlOutput {
4253 sql,
4254 shape: shape2,
4255 inference_plan: None,
4256 };
4257 }
4258
4259 let result_expr = if !upd.returning.is_empty() {
4261 let (pointer_exprs, _) = build_shape(&upd.returning, alias);
4262 let mut parts = vec![type_disc(&upd.target.type_name)];
4263 parts.extend(pointer_exprs);
4264 parts.join(",\n ")
4265 } else {
4266 format!("{}.id", qi(alias))
4267 };
4268
4269 let has_scalar_changes = !upd.assignments.is_empty() || !upd.rewrites.is_empty();
4270
4271 let mut cte_parts: Vec<String> = emit_user_cte_parts(user_ctes);
4275 cte_parts.extend(emit_user_cte_parts(&upd.nested_ctes));
4276
4277 if has_scalar_changes {
4279 let sets = update_set_fragments(&upd.assignments, &upd.rewrites, "");
4280 let mut upd_sql = format!(
4281 "UPDATE {} AS {}\nSET {}",
4282 target_ref(&upd.target),
4283 qi(alias),
4284 sets.join(", "),
4285 );
4286 upd_sql.push_str(&nested_cte_from(&upd.nested_ctes, &sets.join(","), "\n"));
4287 append_filter(&mut upd_sql, &upd.filter);
4288 upd_sql.push_str("\nRETURNING *");
4289 cte_parts.push(format!("\"_ids\" AS (\n{}\n)", upd_sql));
4290 } else {
4291 let mut sel = format!(
4292 "SELECT {}.* FROM {} AS {}",
4293 qi(alias),
4294 target_ref(&upd.target),
4295 qi(alias),
4296 );
4297 append_filter(&mut sel, &upd.filter);
4298 cte_parts.push(format!("\"_ids\" AS (\n{}\n)", sel));
4299 }
4300
4301 for (i, clr) in upd.multi_link_clears.iter().enumerate() {
4303 let exclude = ml_clear_exclusion(upd.multi_link_replaces.get(i));
4304 let del = format!(
4305 "DELETE FROM {} WHERE {} IN (SELECT id FROM \"_ids\"){}",
4306 qn(&clr.module, &clr.junction_table),
4307 qi(&clr.source_col),
4308 exclude,
4309 );
4310 cte_parts.push(format!("\"_clr_{}\" AS (\n{}\n)", i, del));
4311 }
4312
4313 for (i, app) in upd.multi_link_appends.iter().enumerate() {
4315 cte_parts.push(emit_ml_append_cte(
4316 app,
4317 "_ids",
4318 &upd.target.alias,
4319 &format!("_ml_add_{}", i),
4320 ));
4321 }
4322
4323 for (i, rem) in upd.multi_link_removals.iter().enumerate() {
4325 cte_parts.push(emit_ml_remove_cte(
4326 rem,
4327 "_ids",
4328 &upd.target.alias,
4329 &format!("_ml_rm_{}", i),
4330 ));
4331 }
4332
4333 for (i, rep) in upd.multi_link_replaces.iter().enumerate() {
4335 cte_parts.push(emit_ml_append_cte(
4336 rep,
4337 "_ids",
4338 &upd.target.alias,
4339 &format!("_ml_rep_{}", i),
4340 ));
4341 }
4342
4343 cte_parts.extend(enqueue_ctes(&upd.enqueue_vector, "_ids"));
4345 cte_parts.extend(enqueue_search_ctes(
4346 &upd.enqueue_search,
4347 "_ids",
4348 upd.enqueue_vector.len(),
4349 ));
4350
4351 let sql = format!(
4352 "WITH\n{}\nSELECT (\n {}\n) AS result\nFROM \"_ids\" AS {}",
4353 cte_parts.join(",\n"),
4354 result_expr,
4355 qi(alias),
4356 );
4357 SqlOutput {
4358 sql,
4359 shape,
4360 inference_plan: None,
4361 }
4362}
4363
4364fn emit_delete_stmt(del: &IrDelete) -> SqlOutput {
4367 if !del.poly_implementors.is_empty() {
4368 return emit_poly_delete_stmt(del);
4369 }
4370 let alias = &del.target.alias;
4371
4372 if del.enqueue_search.is_empty() {
4373 let mut sql = format!("DELETE FROM {} AS {}", target_ref(&del.target), qi(alias),);
4374 append_filter(&mut sql, &del.filter);
4375 let (shape, returning_sql) = emit_returning_shape(&del.target, &del.returning, true);
4376 if let Some(r) = returning_sql {
4377 sql.push_str(&r);
4378 }
4379 return SqlOutput {
4380 sql,
4381 shape,
4382 inference_plan: None,
4383 };
4384 }
4385
4386 let mut del_sql = format!(" DELETE FROM {} AS {}", target_ref(&del.target), qi(alias),);
4388 append_filter(&mut del_sql, &del.filter);
4389 del_sql.push_str("\n RETURNING \"id\"");
4390
4391 let mut cte_parts = vec![format!("\"_del\" AS (\n{}\n)", del_sql)];
4392 cte_parts.extend(enqueue_search_ctes(&del.enqueue_search, "_del", 0));
4393
4394 let (shape, select_sql) = shape_select_from_cte(&del.target, &del.returning, "_del");
4395 let sql = format!(
4396 "WITH\n{}\n{}",
4397 cte_parts.join(",\n"),
4398 select_sql.unwrap_or_else(|| "SELECT * FROM \"_del\"".to_string()),
4399 );
4400 SqlOutput {
4401 sql,
4402 shape,
4403 inference_plan: None,
4404 }
4405}
4406
4407fn emit_poly_delete_stmt(del: &IrDelete) -> SqlOutput {
4408 let alias = &del.target.alias;
4409 let mut cte_parts = vec![];
4410 let mut union_parts = vec![];
4411
4412 for (i, imp) in del.poly_implementors.iter().enumerate() {
4413 let cte_name = format!("_d{}", i);
4414 let mut del_sql = format!("DELETE FROM {} AS {}", qn(&imp.module, &imp.table), qi(alias),);
4415 append_filter(&mut del_sql, &del.filter);
4416 del_sql.push_str(&format!("\nRETURNING {}.\"id\"", qi(alias)));
4417 cte_parts.push(format!("\"{}\" AS (\n{}\n)", cte_name, del_sql));
4418
4419 let r_alias = format!("_r{}", i);
4420 union_parts.push(format!(
4421 "SELECT ROW({}::text, {}.\"id\") AS result FROM \"{}\" AS {}",
4422 sql_str(&imp.type_name),
4423 qi(&r_alias),
4424 cte_name,
4425 qi(&r_alias),
4426 ));
4427 }
4428
4429 let sql = format!("WITH\n{}\n{}", cte_parts.join(",\n"), union_parts.join("\nUNION ALL\n"),);
4430
4431 let (shape, _) = emit_returning_shape(&del.target, &del.returning, true);
4432 SqlOutput {
4433 sql,
4434 shape,
4435 inference_plan: None,
4436 }
4437}
4438
4439fn emit_returning_shape(
4444 target: &IrSource,
4445 returning: &[IrShapePointer],
4446 with_alias: bool,
4447) -> (ShapeDescriptor, Option<String>) {
4448 if returning.is_empty() {
4449 return (
4450 ShapeDescriptor {
4451 root: ShapeNode::Scalar {
4452 name: String::new(),
4453 position: 0,
4454 },
4455 },
4456 None,
4457 );
4458 }
4459
4460 let alias = if with_alias { target.alias.as_str() } else { "" };
4461 let (pointer_exprs, shape_pointers) = build_shape(returning, alias);
4462
4463 let mut parts = vec![type_disc(&target.type_name)];
4464 parts.extend(pointer_exprs);
4465 let tuple = parts.join(",\n ");
4466 let sql = format!("\nRETURNING (\n {}\n) AS result", tuple);
4467
4468 let root_pointers = prepend_type(shape_pointers);
4469 let shape = ShapeDescriptor {
4470 root: ShapeNode::Object {
4471 name: String::new(),
4472 type_name: Some(target.type_name.clone()),
4473 position: 0,
4474 cardinality: Cardinality::Required,
4475 pointers: root_pointers,
4476 has_implicit_id: shape_has_implicit_id(returning),
4477 },
4478 };
4479 (shape, Some(sql))
4480}
4481
4482fn emit_scalar_set(f: &IrScalarSetPointer, pos: usize) -> (String, ShapeNode) {
4485 let from_sql = if !f.poly_implementors.is_empty() {
4486 format!(
4487 "(\n{}\n) AS {}",
4488 emit_poly_union(&f.poly_implementors, &f.poly_columns),
4489 qi(&f.source.alias)
4490 )
4491 } else {
4492 format!("{} AS {}", source_ref(&f.source), qi(&f.source.alias))
4493 };
4494 let sql = format!(
4495 "(SELECT COALESCE(array_agg(ROW({})::record), ARRAY[]::record[]) FROM {})",
4496 emit_expr(&f.bool_expr),
4497 from_sql,
4498 );
4499 let node = ShapeNode::Array {
4500 name: f.alias.clone(),
4501 position: pos,
4502 element: Box::new(ShapeNode::Scalar {
4503 name: String::new(),
4504 position: 0,
4505 }),
4506 };
4507 (sql, node)
4508}
4509
4510fn shape_has_implicit_id(pointers: &[IrShapePointer]) -> bool {
4515 matches!(pointers.first(), Some(IrShapePointer::Scalar(p)) if p.implicit_id)
4516}
4517
4518fn build_shape(pointers: &[IrShapePointer], table_alias: &str) -> (Vec<String>, Vec<ShapeNode>) {
4519 let mut exprs = Vec::new();
4520 let mut nodes = Vec::new();
4521
4522 for (i, pointer) in pointers.iter().enumerate() {
4523 let (sql, node) = emit_shape_pointer(pointer, table_alias, i + 1);
4524 exprs.push(sql);
4525 nodes.push(node);
4526 }
4527
4528 (exprs, nodes)
4529}
4530
4531fn fill_template(template: &str, args: &[String]) -> String {
4537 let mut out = String::with_capacity(template.len());
4538 let mut chars = template.char_indices().peekable();
4539 while let Some((start, c)) = chars.next() {
4540 if c != '$' {
4541 out.push(c);
4542 continue;
4543 }
4544 let mut end = start + 1;
4545 while let Some((i, d)) = chars.peek().copied()
4546 && d.is_ascii_digit()
4547 {
4548 end = i + d.len_utf8();
4549 chars.next();
4550 }
4551 match template[start + 1..end].parse::<usize>() {
4552 Ok(n) if (1..=args.len()).contains(&n) => out.push_str(&args[n - 1]),
4553 _ => out.push_str(&template[start..end]),
4554 }
4555 }
4556 out
4557}
4558
4559fn assert_message_arg(message: &Option<IrExpr>) -> String {
4561 message
4562 .as_ref()
4563 .map(|m| format!(", ({})::text", emit_expr(m)))
4564 .unwrap_or_default()
4565}
4566
4567fn emit_shape_pointer(pointer: &IrShapePointer, table_alias: &str, pos: usize) -> (String, ShapeNode) {
4568 match pointer {
4569 IrShapePointer::Scalar(f) => emit_scalar(f, table_alias, pos),
4570 IrShapePointer::SingleLink(f) => emit_single_link(f, table_alias, pos),
4571 IrShapePointer::MultiLink(f) => emit_multi_link(f, table_alias, pos),
4572 IrShapePointer::Computed(f) => (emit_expr(&f.expr), expr_shape_node(&f.alias, pos, &f.expr)),
4573 IrShapePointer::ScalarSet(f) => emit_scalar_set(f, pos),
4574 IrShapePointer::Asserted(a) => {
4575 let (sql, node) = emit_shape_pointer(&a.inner, table_alias, pos);
4576 let checked_set = match &a.check {
4587 Some(check) if emits_one_object(check) => {
4588 format!("ARRAY{}", emit_shape_pointer(check, table_alias, pos).0)
4589 }
4590 Some(check) => emit_shape_pointer(check, table_alias, pos).0,
4591 None if emits_one_object(&a.inner) => format!("ARRAY{sql}"),
4592 None => "\"_a\".\"v\"".to_string(),
4593 };
4594 let checked = format!(
4595 "(SELECT \"_a\".\"v\" FROM (SELECT {sql} AS \"v\") AS \"_a\"\n WHERE cardinality(\"_pylon\".{}({checked_set}::text[]{})) >= 0)",
4596 qi(&a.fn_name),
4597 assert_message_arg(&a.message),
4598 );
4599 (checked, node)
4600 }
4601 }
4602}
4603
4604fn emits_one_object(pointer: &IrShapePointer) -> bool {
4607 match pointer {
4608 IrShapePointer::SingleLink(_) => true,
4609 IrShapePointer::Computed(c) => matches!(
4610 c.expr,
4611 IrExpr::ObjectPathSubquery(_) | IrExpr::ObjectPathUnion { multi: false, .. }
4612 ),
4613 IrShapePointer::Asserted(a) => emits_one_object(&a.inner),
4614 _ => false,
4615 }
4616}
4617
4618fn pg_quoted_to_pylon(pg_type: &str) -> String {
4621 let inner = pg_type.trim_start_matches('"');
4622 if let Some(idx) = inner.find(r#""."#) {
4623 let module = &inner[..idx];
4624 let type_name = inner[idx + 3..].trim_end_matches('"');
4625 format!("{}::{}", module, type_name)
4626 } else {
4627 pg_type.to_string()
4628 }
4629}
4630
4631struct QualifiedPgType {
4642 name: String,
4644 is_array: bool,
4645}
4646
4647impl QualifiedPgType {
4648 fn of(pg_type: &str) -> Option<Self> {
4649 let (element, is_array) = match pg_type.strip_suffix("[]") {
4650 Some(element) => (element, true),
4651 None => (pg_type, false),
4652 };
4653 element.starts_with('"').then(|| Self {
4654 name: pg_quoted_to_pylon(element),
4655 is_array,
4656 })
4657 }
4658
4659 fn text_cast(&self) -> &'static str {
4661 if self.is_array { "::text[]" } else { "::text" }
4662 }
4663
4664 fn shape_node(&self, name: String, position: usize) -> ShapeNode {
4666 let element = ShapeNode::Enum {
4667 name: name.clone(),
4668 position,
4669 enum_type: self.name.clone(),
4670 };
4671 if !self.is_array {
4672 return element;
4673 }
4674 ShapeNode::Array {
4675 name,
4676 element: Box::new(ShapeNode::Enum {
4679 name: String::new(),
4680 position: 0,
4681 enum_type: self.name.clone(),
4682 }),
4683 position,
4684 }
4685 }
4686}
4687
4688fn emit_scalar(f: &IrScalarPointer, table_alias: &str, pos: usize) -> (String, ShapeNode) {
4689 if let Some(nt_name) = f.pg_type.strip_prefix("__nt__:") {
4690 let sql = if table_alias.is_empty() {
4691 format!("{}::jsonb", qi(&f.column))
4692 } else {
4693 format!("{}.{}::jsonb", qi(table_alias), qi(&f.column))
4694 };
4695 return (
4696 sql,
4697 ShapeNode::NamedTuple {
4698 name: f.alias.clone(),
4699 position: pos,
4700 type_name: Some(nt_name.to_string()),
4701 members: f.tuple_shape.as_ref().map(|s| s.members.clone()),
4702 is_free_object: false,
4703 },
4704 );
4705 }
4706 if let Some(qualified) = QualifiedPgType::of(&f.pg_type) {
4709 let cast = qualified.text_cast();
4710 let sql = if table_alias.is_empty() {
4711 format!("{}{}", qi(&f.column), cast)
4712 } else {
4713 format!("{}.{}{}", qi(table_alias), qi(&f.column), cast)
4714 };
4715 return (sql, qualified.shape_node(f.alias.clone(), pos));
4716 }
4717 if let Some(shape) = &f.tuple_shape {
4721 let sql = if table_alias.is_empty() {
4722 format!("{}::jsonb", qi(&f.column))
4723 } else {
4724 format!("{}.{}::jsonb", qi(table_alias), qi(&f.column))
4725 };
4726 return (
4727 sql,
4728 ShapeNode::NamedTuple {
4729 name: f.alias.clone(),
4730 position: pos,
4731 type_name: shape.type_name.clone(),
4732 members: Some(shape.members.clone()),
4733 is_free_object: false,
4734 },
4735 );
4736 }
4737 let sql = if table_alias.is_empty() {
4738 format!("{}::{}", qi(&f.column), f.pg_type)
4739 } else {
4740 format!("{}.{}::{}", qi(table_alias), qi(&f.column), f.pg_type)
4741 };
4742 (
4743 sql,
4744 ShapeNode::Scalar {
4745 name: f.alias.clone(),
4746 position: pos,
4747 },
4748 )
4749}
4750
4751fn emit_single_link(f: &IrSingleLinkPointer, parent_alias: &str, pos: usize) -> (String, ShapeNode) {
4752 let sub = &f.subquery;
4753 let [IrRowSource::Bound { source, shape }] = sub.rows.as_slice() else {
4754 unreachable!("single-link subquery is always schema-bound")
4755 };
4756 let sub_alias = &source.alias;
4757
4758 let (sub_exprs, mut sub_nodes) = build_shape(shape, sub_alias);
4759 let mut parts = vec![source_type_disc(source)];
4760 parts.extend(sub_exprs);
4761
4762 for lp in &f.link_properties {
4767 parts.push(format!("\"jt\".{}", qi(&lp.name)));
4768 let pos = sub_nodes.len() + 1;
4769 sub_nodes.push(ShapeNode::Scalar {
4770 name: format!("@{}", lp.name),
4771 position: pos,
4772 });
4773 }
4774
4775 let tuple = parts.join(",\n ");
4776
4777 let (from_sql, mut where_parts) = match &f.correlation {
4778 IrSingleLinkCorrelation::Fk { fk_column, target_pk } => {
4779 let from = format!("FROM {} AS {}", source_ref(source), qi(sub_alias));
4780 let cond = format!(
4781 "{}.{} = {}.{}",
4782 qi(parent_alias),
4783 qi(fk_column),
4784 qi(sub_alias),
4785 qi(target_pk),
4786 );
4787 (from, vec![cond])
4788 }
4789 IrSingleLinkCorrelation::Junction { join, target_pk: _ } => match join {
4793 IrMultiLinkJoin::Standard { junction_table, module } => {
4794 let from = format!(
4795 "FROM {} AS \"jt\"\n INNER JOIN {} AS {}\n ON {}.id = \"jt\".target",
4796 junction_ref(module, junction_table),
4797 source_ref(source),
4798 qi(sub_alias),
4799 qi(sub_alias),
4800 );
4801 let cond = format!("\"jt\".source = {}.id", qi(parent_alias));
4802 (from, vec![cond])
4803 }
4804 IrMultiLinkJoin::Through {
4805 junction_table,
4806 module,
4807 source_col,
4808 target_col,
4809 } => {
4810 let from = format!(
4811 "FROM {} AS \"jt\"\n INNER JOIN {} AS {}\n ON {}.id = \"jt\".{}",
4812 junction_ref(module, junction_table),
4813 source_ref(source),
4814 qi(sub_alias),
4815 qi(sub_alias),
4816 qi(target_col),
4817 );
4818 let cond = format!("\"jt\".{} = {}.id", qi(source_col), qi(parent_alias));
4819 (from, vec![cond])
4820 }
4821 IrMultiLinkJoin::BacklinkFk { .. } | IrMultiLinkJoin::BacklinkJunction { .. } => {
4822 unreachable!("a junction-backed single link's own forward join is always Standard or Through")
4823 }
4824 },
4825 };
4826 if let Some(filter) = &sub.filter {
4827 where_parts.push(emit_expr(filter));
4828 }
4829
4830 let mut sql = format!(
4831 "(SELECT (\n {}\n )\n {}\n WHERE {}",
4832 tuple,
4833 from_sql,
4834 where_parts.join(" AND "),
4835 );
4836 if !sub.order_by.is_empty() {
4837 let s: Vec<_> = sub.order_by.iter().map(emit_sort_clause).collect();
4838 sql.push_str(&format!("\n ORDER BY {}", s.join(", ")));
4839 }
4840 sql.push(')');
4841
4842 let node = ShapeNode::Object {
4843 name: f.alias.clone(),
4844 type_name: Some(source.type_name.clone()),
4845 position: pos,
4846 cardinality: Cardinality::Optional,
4847 pointers: prepend_type(sub_nodes),
4848 has_implicit_id: shape_has_implicit_id(shape),
4849 };
4850 (sql, node)
4851}
4852
4853fn emit_multi_link(f: &IrMultiLinkPointer, parent_alias: &str, pos: usize) -> (String, ShapeNode) {
4854 let sub = &f.subquery;
4855 let [IrRowSource::Bound { source, shape }] = sub.rows.as_slice() else {
4856 unreachable!("multi-link subquery is always schema-bound")
4857 };
4858 let sub_alias = &source.alias;
4859
4860 let (sub_exprs, mut sub_nodes) = build_shape(shape, sub_alias);
4861 let mut row_parts = vec![source_type_disc(source)];
4862 row_parts.extend(sub_exprs);
4863
4864 for lp in &f.link_properties {
4868 row_parts.push(format!("\"jt\".{}", qi(&lp.name)));
4869 let pos = sub_nodes.len() + 1;
4872 sub_nodes.push(ShapeNode::Scalar {
4873 name: format!("@{}", lp.name),
4874 position: pos,
4875 });
4876 }
4877
4878 let row = row_parts.join(",\n ");
4879
4880 let order_sql = if !sub.order_by.is_empty() {
4882 let s: Vec<_> = sub.order_by.iter().map(emit_sort_clause).collect();
4883 format!(" ORDER BY {}", s.join(", "))
4884 } else {
4885 String::new()
4886 };
4887
4888 let (from_sql, source_cond) = match &f.join {
4889 IrMultiLinkJoin::Standard { junction_table, module } => {
4890 let from = format!(
4891 "FROM {} AS \"jt\"\n INNER JOIN {} AS {}\n ON {}.id = \"jt\".target",
4892 junction_ref(module, junction_table),
4893 source_ref(source),
4894 qi(sub_alias),
4895 qi(sub_alias),
4896 );
4897 let cond = format!("\"jt\".source = {}.id", qi(parent_alias));
4898 (from, cond)
4899 }
4900 IrMultiLinkJoin::Through {
4901 junction_table,
4902 module,
4903 source_col,
4904 target_col,
4905 } => {
4906 let from = format!(
4907 "FROM {} AS \"jt\"\n INNER JOIN {} AS {}\n ON {}.id = \"jt\".{}",
4908 junction_ref(module, junction_table),
4909 source_ref(source),
4910 qi(sub_alias),
4911 qi(sub_alias),
4912 qi(target_col),
4913 );
4914 let cond = format!("\"jt\".{} = {}.id", qi(source_col), qi(parent_alias));
4915 (from, cond)
4916 }
4917 IrMultiLinkJoin::BacklinkFk { fk_col } => {
4920 let from = format!("FROM {} AS {}", source_ref(source), qi(sub_alias));
4921 let cond = format!("{}.{} = {}.id", qi(sub_alias), qi(fk_col), qi(parent_alias));
4922 (from, cond)
4923 }
4924 IrMultiLinkJoin::BacklinkJunction {
4929 junction_table,
4930 module,
4931 owner_col,
4932 current_col,
4933 } => {
4934 let from = format!(
4935 "FROM {} AS \"jt\"\n INNER JOIN {} AS {}\n ON {}.id = \"jt\".{}",
4936 junction_ref(module, junction_table),
4937 source_ref(source),
4938 qi(sub_alias),
4939 qi(sub_alias),
4940 qi(owner_col),
4941 );
4942 let cond = format!("\"jt\".{} = {}.id", qi(current_col), qi(parent_alias));
4943 (from, cond)
4944 }
4945 };
4946
4947 let mut where_parts = vec![source_cond];
4948 if let Some(filter) = &sub.filter {
4949 where_parts.push(emit_expr(filter));
4950 }
4951
4952 if f.single {
4953 let mut sql = format!(
4954 "(SELECT (\n {}\n )\n {}\n WHERE {}",
4955 row,
4956 from_sql,
4957 where_parts.join(" AND "),
4958 );
4959 append_order_by(&mut sql, &sub.order_by);
4960 append_offset_limit(&mut sql, &sub.offset, &sub.limit);
4961 sql.push(')');
4962 let node = ShapeNode::Object {
4963 name: f.alias.clone(),
4964 type_name: Some(source.type_name.clone()),
4965 position: pos,
4966 cardinality: Cardinality::Optional,
4967 pointers: prepend_type(sub_nodes),
4968 has_implicit_id: shape_has_implicit_id(shape),
4969 };
4970 return (sql, node);
4971 }
4972
4973 if f.single {
4974 let mut sql = format!(
4975 "(SELECT (\n {}\n )\n {}\n WHERE {}",
4976 row,
4977 from_sql,
4978 where_parts.join(" AND "),
4979 );
4980 append_order_by(&mut sql, &sub.order_by);
4981 append_offset_limit(&mut sql, &sub.offset, &sub.limit);
4982 sql.push(')');
4983 let node = ShapeNode::Object {
4984 name: f.alias.clone(),
4985 type_name: Some(source.type_name.clone()),
4986 position: pos,
4987 cardinality: Cardinality::Optional,
4988 pointers: prepend_type(sub_nodes),
4989 has_implicit_id: shape_has_implicit_id(shape),
4990 };
4991 return (sql, node);
4992 }
4993
4994 let sql = if sub.limit.is_some() || sub.offset.is_some() {
4995 let mut cols: Vec<String> = row_parts
5000 .iter()
5001 .enumerate()
5002 .map(|(i, part)| format!("{} AS \"c{}\"", part, i))
5003 .collect();
5004 let agg_row: Vec<String> = (0..row_parts.len()).map(|i| format!("\"__lim\".\"c{}\"", i)).collect();
5005 let mut agg_order: Vec<String> = vec![];
5006 for (i, s) in sub.order_by.iter().enumerate() {
5007 cols.push(format!("{} AS \"s{}\"", emit_expr(&s.expr), i));
5008 agg_order.push(emit_sort_clause(&IrSort {
5009 expr: IrExpr::ColumnRef {
5010 alias: "__lim".to_string(),
5011 column: format!("s{}", i),
5012 pg_type: String::new(),
5013 },
5014 direction: s.direction.clone(),
5015 nulls: s.nulls.clone(),
5016 }));
5017 }
5018 let mut inner = format!(
5019 "SELECT {}\n {}\n WHERE {}",
5020 cols.join(",\n "),
5021 from_sql,
5022 where_parts.join(" AND "),
5023 );
5024 append_order_by(&mut inner, &sub.order_by);
5025 append_offset_limit(&mut inner, &sub.offset, &sub.limit);
5026 let agg_order_sql = if agg_order.is_empty() {
5027 String::new()
5028 } else {
5029 format!(" ORDER BY {}", agg_order.join(", "))
5030 };
5031 format!(
5032 "(SELECT COALESCE(\n array_agg(ROW(\n {}\n )::record{}),\n ARRAY[]::record[]\n )\n FROM ({}) AS \"__lim\")",
5033 agg_row.join(",\n "),
5034 agg_order_sql,
5035 inner,
5036 )
5037 } else {
5038 format!(
5039 "(SELECT COALESCE(\n array_agg(ROW(\n {}\n )::record{}),\n ARRAY[]::record[]\n )\n {}\n WHERE {})",
5040 row,
5041 order_sql,
5042 from_sql,
5043 where_parts.join(" AND "),
5044 )
5045 };
5046
5047 let node = ShapeNode::Array {
5048 name: f.alias.clone(),
5049 position: pos,
5050 element: Box::new(ShapeNode::Object {
5051 name: String::new(),
5052 type_name: Some(source.type_name.clone()),
5053 position: 0,
5054 cardinality: Cardinality::Required,
5055 pointers: prepend_type(sub_nodes),
5056 has_implicit_id: shape_has_implicit_id(shape),
5057 }),
5058 };
5059 (sql, node)
5060}
5061
5062fn prepend_type(nodes: Vec<ShapeNode>) -> Vec<ShapeNode> {
5065 let mut out = vec![ShapeNode::Scalar {
5066 name: "__type__".into(),
5067 position: 0,
5068 }];
5069 out.extend(nodes);
5070 out
5071}
5072
5073fn append_filter(sql: &mut String, filter: &Option<IrExpr>) {
5076 if let Some(f) = filter {
5077 sql.push_str(&format!("\nWHERE {}", emit_expr(f)));
5078 }
5079}
5080
5081fn append_order_by(sql: &mut String, order_by: &[IrSort]) {
5082 if !order_by.is_empty() {
5083 let s: Vec<_> = order_by.iter().map(emit_sort_clause).collect();
5084 sql.push_str(&format!("\nORDER BY {}", s.join(", ")));
5085 }
5086}
5087
5088fn append_offset_limit(sql: &mut String, offset: &Option<IrExpr>, limit: &Option<IrExpr>) {
5089 if let Some(o) = offset {
5090 sql.push_str(&format!("\nOFFSET {}", emit_expr(o)));
5091 }
5092 if let Some(l) = limit {
5093 sql.push_str(&format!("\nLIMIT {}", emit_expr(l)));
5094 }
5095}
5096
5097fn append_lock_clause(sql: &mut String, lock: &Option<IrLockClause>) {
5104 let Some(lock) = lock else { return };
5105 let strength = match lock.strength {
5106 IrLockStrength::Update => "UPDATE",
5107 IrLockStrength::NoKeyUpdate => "NO KEY UPDATE",
5108 IrLockStrength::Share => "SHARE",
5109 IrLockStrength::KeyShare => "KEY SHARE",
5110 };
5111 sql.push_str(&format!("\nFOR {}", strength));
5112 match lock.wait {
5113 IrLockWait::Block => {}
5114 IrLockWait::NoWait => sql.push_str(" NOWAIT"),
5115 IrLockWait::SkipLocked => sql.push_str(" SKIP LOCKED"),
5116 }
5117}
5118
5119fn emit_sort_clause(s: &IrSort) -> String {
5120 let dir = match s.direction {
5121 IrSortDir::Asc => "ASC",
5122 IrSortDir::Desc => "DESC",
5123 };
5124 let nulls = match s.nulls {
5125 IrNulls::First => "NULLS FIRST",
5126 IrNulls::Last => "NULLS LAST",
5127 };
5128 format!("{} {} {}", emit_expr(&s.expr), dir, nulls)
5129}
5130
5131fn set_operand(expr: &IrExpr) -> String {
5136 match expr {
5137 IrExpr::ArrayFromSelect(_) => format!("SELECT unnest({})", emit_expr(expr)),
5138 IrExpr::CteRef { name, scalar, .. } => {
5139 format!("SELECT {} FROM {}", if *scalar { "\"v\"" } else { "\"id\"" }, qi(name))
5140 }
5141 _ => format!("SELECT {}", emit_expr(expr)),
5142 }
5143}
5144
5145pub fn emit_expr_with_fanouts(expr: &IrExpr, fanouts: &HashMap<(String, String), IrPolyFanout>) -> String {
5148 with_subtype_fanouts(fanouts, || emit_expr(expr))
5149}
5150
5151pub fn emit_expr(expr: &IrExpr) -> String {
5152 match expr {
5153 IrExpr::ColumnRef { alias, column, .. } => {
5154 if alias.is_empty() {
5155 qi(column)
5156 } else if alias == "NEW" || alias == "OLD" {
5157 format!("{alias}.{}", qi(column))
5168 } else {
5169 format!("{}.{}", qi(alias), qi(column))
5170 }
5171 }
5172 IrExpr::Param { index } => format!("${}", index + 1),
5173 IrExpr::Literal(lit) => emit_literal(lit),
5174 IrExpr::BinOp(op) => {
5175 let l = emit_expr(&op.left);
5176 let r = emit_expr(&op.right);
5177 match op.op {
5178 BinOpKind::Add => format!("({} + {})", l, r),
5179 BinOpKind::Sub => format!("({} - {})", l, r),
5180 BinOpKind::Mul => format!("({} * {})", l, r),
5181 BinOpKind::Div => format!("({} / {})", l, r),
5182 BinOpKind::FloorDiv => {
5183 if is_integer_expr(&op.left) && is_integer_expr(&op.right) {
5184 format!("({} / {})", l, r)
5185 } else {
5186 format!("floor(({}) / ({}))", l, r)
5187 }
5188 }
5189 BinOpKind::Mod => format!("({} % {})", l, r),
5190 BinOpKind::Pow => format!("power({}, {})", l, r),
5191 BinOpKind::Eq => format!("({} = {})", l, r),
5192 BinOpKind::Ne => format!("({} <> {})", l, r),
5193 BinOpKind::Lt => format!("({} < {})", l, r),
5194 BinOpKind::Le => format!("({} <= {})", l, r),
5195 BinOpKind::Gt => format!("({} > {})", l, r),
5196 BinOpKind::Ge => format!("({} >= {})", l, r),
5197 BinOpKind::And => format!("({} AND {})", l, r),
5198 BinOpKind::Or => format!("({} OR {})", l, r),
5199 BinOpKind::Like => format!("({} LIKE {})", l, r),
5200 BinOpKind::Ilike => format!("({} ILIKE {})", l, r),
5201 BinOpKind::NotLike => format!("({} NOT LIKE {})", l, r),
5202 BinOpKind::NotIlike => format!("({} NOT ILIKE {})", l, r),
5203 BinOpKind::In => format!("({} = ANY({}))", l, unwrap_unnest_for_any(&op.right, &r)),
5209 BinOpKind::NotIn => format!("({} <> ALL({}))", l, unwrap_unnest_for_any(&op.right, &r)),
5210 BinOpKind::Coalesce => format!("COALESCE({}, {})", l, r),
5211 BinOpKind::CoalesceEq => format!("({} IS NOT DISTINCT FROM {})", l, r),
5212 BinOpKind::CoalesceNe => format!("({} IS DISTINCT FROM {})", l, r),
5213 BinOpKind::Concat => format!("({} || {})", l, r),
5214 }
5215 }
5216 IrExpr::UnaryOp(op) => {
5217 let inner = emit_expr(&op.operand);
5218 match op.op {
5219 UnaryOpKind::Not => format!("(NOT {})", inner),
5220 UnaryOpKind::Minus => format!("(-{})", inner),
5221 UnaryOpKind::Exists => format!("EXISTS({})", inner),
5222 UnaryOpKind::Distinct => inner,
5227 }
5228 }
5229 IrExpr::FunctionCall(f) => {
5230 let args: Vec<_> = f.args.iter().map(emit_expr).collect();
5231 if let Some(tmpl) = &f.sql_template {
5232 return fill_template(tmpl, &args);
5233 }
5234 let name = match &f.schema {
5235 Some(s) => format!("{}.{}", pg_schema(s), qi(&f.name)),
5236 None => f.name.clone(),
5237 };
5238 format!("{}({})", name, args.join(", "))
5239 }
5240 IrExpr::TypeCast(c) => {
5241 if c.pg_type == "jsonb" {
5244 match &c.expr {
5245 IrExpr::Param { .. } => format!("({})::jsonb", emit_expr(&c.expr)),
5256 IrExpr::Literal(IrLiteral::Str(_)) => {
5257 format!("to_jsonb({}::text)", emit_expr(&c.expr))
5258 }
5259 _ => format!("to_jsonb({})", emit_expr(&c.expr)),
5260 }
5261 } else if yields_jsonb(&c.expr) {
5262 format!("(({}) #>> '{{}}')::{}", emit_expr(&c.expr), c.pg_type)
5266 } else {
5267 format!("({})::{}", emit_expr(&c.expr), c.pg_type)
5268 }
5269 }
5270 IrExpr::IfElse(ie) => format!(
5271 "CASE WHEN {} THEN {} ELSE {} END",
5272 emit_expr(&ie.condition),
5273 emit_expr(&ie.if_),
5274 emit_expr(&ie.else_),
5275 ),
5276 IrExpr::Array(elems) => {
5277 if elems.is_empty() {
5278 "ARRAY[]::text[]".to_string()
5279 } else {
5280 let parts: Vec<String> = elems.iter().map(emit_expr).collect();
5281 format!("ARRAY[{}]", parts.join(", "))
5282 }
5283 }
5284 IrExpr::Null => "NULL".to_string(),
5285 IrExpr::AggOverSet {
5286 fn_name,
5287 schema: _,
5288 elems,
5289 } => {
5290 let union_all = elems
5291 .iter()
5292 .map(|e| format!("SELECT {}", emit_expr(e)))
5293 .collect::<Vec<_>>()
5294 .join(" UNION ALL ");
5295 format!("(SELECT {}(v) FROM ({}) AS _set(v))", fn_name, union_all)
5296 }
5297 IrExpr::AggOverCte { fn_name, cte, column } => format!(
5298 "(SELECT {}({}) FROM {})",
5299 fn_name,
5300 column.as_deref().map(qi).unwrap_or_else(|| "*".to_string()),
5301 qi(cte),
5302 ),
5303 IrExpr::ExistsOverCte { cte, column } => match column {
5304 Some(column) => format!("EXISTS(SELECT 1 FROM {} WHERE {} IS NOT NULL)", qi(cte), qi(column)),
5305 None => format!("EXISTS(SELECT 1 FROM {})", qi(cte)),
5306 },
5307 IrExpr::AggOverQuery { fn_name, inner } => {
5308 let inner_sql = emit_select_stmt(inner, &[]).sql;
5309 format!("(SELECT {}(*) FROM ({}) _agg)", fn_name, inner_sql)
5310 }
5311 IrExpr::ArrayFromSelect(src) => emit_array_source(src),
5312 IrExpr::ScalarSubquery(sel) => {
5313 format!(
5314 "(SELECT \"v\" FROM (\n{}\n) AS \"_scalar_sub\")",
5315 emit_select_stmt(sel, &[]).sql
5316 )
5317 }
5318
5319 IrExpr::SetOp { op, left, right, mode } => {
5320 let set = format!("({}) {} ({})", set_operand(left), op.sql(), set_operand(right));
5321 match mode {
5322 crate::ir::SetOpMode::Exists => format!("EXISTS({set})"),
5323 crate::ir::SetOpMode::Array => format!("ARRAY({set})"),
5324 crate::ir::SetOpMode::Aggregate(f) => {
5325 format!("(SELECT {f}(\"_s\".\"v\") FROM ({set}) AS \"_s\"(\"v\"))")
5326 }
5327 }
5328 }
5329
5330 IrExpr::CteRef { name, scalar, .. } => {
5331 let col = if *scalar { "v" } else { "id" };
5334 format!("(SELECT \"{}\" FROM \"{}\")", col, name)
5335 }
5336
5337 IrExpr::CteFieldRef { name, field, .. } => {
5338 format!("(SELECT {} FROM {})", qi(field), qi(name))
5339 }
5340
5341 IrExpr::ForVar { name, .. } => format!("\"_for_{}\".\"v\"", name),
5342
5343 IrExpr::EnumLiteral { pg_type, variant } => {
5344 format!("'{}'::{}", variant.replace('\'', "''"), pg_type)
5345 }
5346
5347 IrExpr::GlobalParam { index, pg_type } => {
5348 format!("(${}::{})", index + 1, pg_type)
5349 }
5350
5351 IrExpr::GlobalRef { cte_name } => {
5352 format!("(SELECT \"value\" FROM \"{}\")", cte_name)
5353 }
5354
5355 IrExpr::NamedTuple { fields, .. } => {
5356 let pairs: Vec<String> = fields
5357 .iter()
5358 .flat_map(|(k, v)| [format!("'{}'", k.replace('\'', "''")), emit_expr(v)])
5359 .collect();
5360 format!("jsonb_build_object({})", pairs.join(", "))
5361 }
5362
5363 IrExpr::Tuple(elems) => {
5364 let items: Vec<String> = elems.iter().map(emit_expr).collect();
5365 format!("jsonb_build_array({})", items.join(", "))
5366 }
5367
5368 IrExpr::Subscript { expr, index, is_array } => {
5369 let e = emit_expr(expr);
5370 let i = emit_expr(index);
5371 if *is_array {
5372 format!("_pylon.array_subscript({}, ({})::bigint)", e, i)
5373 } else {
5374 format!("_pylon.str_subscript({}, ({})::bigint)", e, i)
5375 }
5376 }
5377
5378 IrExpr::Slice {
5379 expr,
5380 lower,
5381 upper,
5382 is_array,
5383 } => {
5384 let e = emit_expr(expr);
5385 if *is_array {
5386 let lo = lower
5387 .as_deref()
5388 .map(|x| format!("({}) + 1", emit_expr(x)))
5389 .unwrap_or_else(|| "1".to_string());
5390 let hi = upper.as_deref().map(emit_expr).unwrap_or_default();
5391 if hi.is_empty() {
5392 format!("({})[{}:]", e, lo)
5393 } else {
5394 format!("({})[{}:{}]", e, lo, hi)
5395 }
5396 } else {
5397 let start = lower
5399 .as_deref()
5400 .map(|x| format!("({}) + 1", emit_expr(x)))
5401 .unwrap_or_else(|| "1".to_string());
5402 match upper.as_deref() {
5403 Some(hi_expr) => {
5404 let lo_val = lower.as_deref().map(emit_expr).unwrap_or_else(|| "0".to_string());
5405 format!(
5407 "substr({}, {}, GREATEST(0, ({}) - ({})))",
5408 e,
5409 start,
5410 emit_expr(hi_expr),
5411 lo_val
5412 )
5413 }
5414 None => format!("substr({}, {})", e, start),
5415 }
5416 }
5417 }
5418
5419 IrExpr::JsonbField { expr, field } => {
5420 format!("({}->{})", emit_expr(expr), sql_str(field))
5421 }
5422
5423 IrExpr::JsonbIndex { expr, index } => {
5424 format!("({}->{})", emit_expr(expr), index)
5425 }
5426
5427 IrExpr::FnParam { name, .. } => qi(name),
5428
5429 IrExpr::FnSubquery(fs) => {
5430 let scalar = match fs.shape.as_slice() {
5431 [IrShapePointer::Computed(c)] => emit_expr(&c.expr),
5432 _ => unreachable!("a function scalar subquery always projects exactly one computed pointer"),
5433 };
5434 let args_sql = fs.fn_args.iter().map(emit_expr).collect::<Vec<_>>().join(", ");
5435 let mut sql = format!(
5436 "(SELECT {}{}\nFROM {}.{}({}) AS {}",
5437 if fs.distinct { "DISTINCT " } else { "" },
5438 scalar,
5439 pg_schema(&fs.fn_module),
5440 qi(&fs.fn_name),
5441 args_sql,
5442 qi(&fs.alias),
5443 );
5444 append_filter(&mut sql, &fs.filter);
5445 append_order_by(&mut sql, &fs.order_by);
5446 append_offset_limit(&mut sql, &fs.offset, &fs.limit);
5447 sql.push(')');
5448 sql
5449 }
5450
5451 IrExpr::PathSubquery(ps) => {
5452 let scalar = match &ps.result {
5453 IrPathResult::Scalar(e, _) => emit_expr(e),
5454 IrPathResult::Object { alias, .. } => format!("{}.\"id\"", qi(alias)),
5455 };
5456 let from_sql = emit_path_joins(&ps.root, &ps.joins);
5457 let mut sql = format!(
5458 "(SELECT {}{}\nFROM {}",
5459 if ps.distinct { "DISTINCT " } else { "" },
5460 scalar,
5461 from_sql
5462 );
5463 append_filter(&mut sql, &ps.filter);
5464 append_order_by(&mut sql, &ps.order_by);
5465 append_offset_limit(&mut sql, &ps.offset, &ps.limit);
5466 sql.push(')');
5467 sql
5468 }
5469
5470 IrExpr::ObjectPathUnion { branches, limit, multi } => {
5471 let arms: Vec<String> = branches
5472 .iter()
5473 .map(|ps| {
5474 let IrPathResult::Object {
5475 alias,
5476 type_name,
5477 shape,
5478 } = &ps.result
5479 else {
5480 unreachable!("an object path union's branches always land on objects")
5481 };
5482 let (pointer_exprs, _) = build_shape(shape, alias);
5483 let mut parts = vec![result_type_disc(ps, alias, type_name)];
5484 parts.extend(pointer_exprs);
5485 let mut sql = format!(
5486 "SELECT (\n {}\n) AS \"r\"\nFROM {}",
5487 parts.join(",\n "),
5488 emit_path_joins(&ps.root, &ps.joins),
5489 );
5490 append_filter(&mut sql, &ps.filter);
5491 sql
5492 })
5493 .collect();
5494 let mut sql = format!(
5495 "{}(SELECT \"r\" FROM (\n{}\n) AS \"_u\"",
5496 if *multi { "ARRAY" } else { "" },
5497 arms.join("\nUNION ALL\n")
5498 );
5499 if let Some(limit) = limit {
5500 sql.push_str(&format!("\nLIMIT {}", emit_expr(limit)));
5501 }
5502 sql.push(')');
5503 sql
5504 }
5505
5506 IrExpr::ObjectPathSubquery(ps) => {
5507 let IrPathResult::Object {
5508 alias,
5509 type_name,
5510 shape,
5511 } = &ps.result
5512 else {
5513 unreachable!("an object path subquery always lands on an object")
5514 };
5515 let (pointer_exprs, _) = build_shape(shape, alias);
5516 let mut parts = vec![result_type_disc(ps, alias, type_name)];
5517 parts.extend(pointer_exprs);
5518 let mut sql = format!(
5519 "(SELECT (\n {}\n)\nFROM {}",
5520 parts.join(",\n "),
5521 emit_path_joins(&ps.root, &ps.joins),
5522 );
5523 append_filter(&mut sql, &ps.filter);
5524 append_order_by(&mut sql, &ps.order_by);
5525 append_offset_limit(&mut sql, &ps.offset, &ps.limit);
5526 sql.push(')');
5527 sql
5528 }
5529
5530 IrExpr::ObjectSubquery(sel) => {
5531 let [IrRowSource::Bound { source, shape }] = sel.rows.as_slice() else {
5532 unreachable!("an object subquery is always schema-bound")
5533 };
5534 let alias = &source.alias;
5535 let (sub_exprs, _) = build_shape(shape, alias);
5536 let mut row_parts = vec![source_type_disc(source)];
5537 row_parts.extend(sub_exprs);
5538 let mut sql = format!(
5539 "(SELECT (\n {}\n )\n FROM {} AS {}",
5540 row_parts.join(",\n "),
5541 source_ref(source),
5542 qi(alias),
5543 );
5544 append_filter(&mut sql, &sel.filter);
5545 append_order_by(&mut sql, &sel.order_by);
5546 append_offset_limit(&mut sql, &sel.offset, &sel.limit);
5547 sql.push(')');
5548 sql
5549 }
5550
5551 IrExpr::Subquery(sel) => {
5552 let [IrRowSource::Bound { source, shape }] = sel.rows.as_slice() else {
5553 unreachable!("scalar/exists subquery is always schema-bound")
5554 };
5555 let alias = &source.alias;
5556 let mut sql = if shape.is_empty() {
5557 format!("(SELECT 1\nFROM {} AS {}", source_ref(source), qi(alias))
5559 } else if let Some(c) = shape.iter().find_map(|f| {
5560 if let IrShapePointer::Computed(c) = f {
5561 Some(c)
5562 } else {
5563 None
5564 }
5565 }) {
5566 format!(
5571 "(SELECT {}\nFROM {} AS {}",
5572 emit_expr(&c.expr),
5573 source_ref(source),
5574 qi(alias)
5575 )
5576 } else {
5577 let pk_col = shape
5579 .iter()
5580 .find_map(|f| {
5581 if let IrShapePointer::Scalar(s) = f {
5582 Some(s.column.as_str())
5583 } else {
5584 None
5585 }
5586 })
5587 .unwrap_or("id");
5588 format!(
5589 "(SELECT {}.{}\nFROM {} AS {}",
5590 qi(alias),
5591 qi(pk_col),
5592 source_ref(source),
5593 qi(alias),
5594 )
5595 };
5596 append_filter(&mut sql, &sel.filter);
5597 append_order_by(&mut sql, &sel.order_by);
5598 append_offset_limit(&mut sql, &sel.offset, &sel.limit);
5599 sql.push(')');
5600 sql
5601 }
5602 IrExpr::RawSql(s) => format!("({s})"),
5603 }
5604}
5605
5606fn emit_vector_search(vs: &IrVectorSearch) -> SqlOutput {
5609 let alias = &vs.source.alias;
5610 let dist_sql = format!(
5611 "{}.{} {} {}",
5612 qi(alias),
5613 qi(&vs.vector_col),
5614 vs.distance_op,
5615 emit_expr(&vs.query_expr),
5616 );
5617
5618 let (obj_tuple, object_shape_nodes) = if vs.object_shape.is_empty() {
5623 let type_expr = type_disc(&vs.source.type_name);
5625 let id_expr = format!("{}.\"id\"", qi(alias));
5626 let tuple = format!("{},\n {}", type_expr, id_expr);
5627 let id_node = ShapeNode::Scalar {
5628 name: "id".to_string(),
5629 position: 1,
5630 };
5631 (tuple, vec![id_node])
5632 } else {
5633 let (pointer_exprs, shape_pointers) = build_shape(&vs.object_shape, alias);
5634 let mut parts = vec![type_disc(&vs.source.type_name)];
5635 parts.extend(pointer_exprs);
5636 (parts.join(",\n "), prepend_type(shape_pointers))
5637 };
5638
5639 let outer = format!(
5641 "NULL::text,\n ROW(\n {}\n )::record,\n {}",
5642 obj_tuple, dist_sql,
5643 );
5644 let mut sql = format!(
5645 "SELECT (\n {}\n) AS result\nFROM {} AS {}",
5646 outer,
5647 source_ref(&vs.source),
5648 qi(alias),
5649 );
5650 append_filter(&mut sql, &vs.filter);
5651
5652 if let Some(dir) = &vs.order_by_distance {
5654 let dir_sql = match dir {
5655 IrSortDir::Asc => "ASC",
5656 IrSortDir::Desc => "DESC",
5657 };
5658 sql.push_str(&format!("\nORDER BY {} {}", dist_sql, dir_sql));
5659 }
5660 append_offset_limit(&mut sql, &vs.offset, &vs.limit);
5661
5662 let object_node = ShapeNode::Object {
5663 name: "object".to_string(),
5664 type_name: Some(vs.source.type_name.clone()),
5665 position: 1,
5666 cardinality: Cardinality::Many,
5667 pointers: object_shape_nodes,
5668 has_implicit_id: shape_has_implicit_id(&vs.object_shape),
5669 };
5670 let shape = ShapeDescriptor {
5671 root: ShapeNode::VectorSearch {
5672 object_position: 1,
5673 distance_position: 2,
5674 object_node: Box::new(object_node),
5675 },
5676 };
5677 let inference_plan = vs.inference_model.as_ref().map(|model_name| InferencePlan::Embedding {
5678 model_name: model_name.clone(),
5679 type_name: vs.inference_type_name.clone().unwrap_or_default(),
5680 index_name: vs.inference_index_name.clone().unwrap_or(None),
5681 query_param_name: vs.inference_query_param_name.clone().unwrap_or_default(),
5682 query_literal: vs.inference_query_literal.clone(),
5683 });
5684 SqlOutput {
5685 sql,
5686 shape,
5687 inference_plan,
5688 }
5689}
5690
5691fn emit_fts_search(fs: &IrFtsSearch) -> SqlOutput {
5694 use crate::schema::SearchBackend;
5695 if fs.backend != SearchBackend::Postgres {
5696 return emit_fts_search_deferred(fs);
5697 }
5698
5699 let alias = &fs.source.alias;
5700 let search_col = format!("{}.{}", qi(alias), qi(&fs.search_col));
5701 let query_sql = emit_expr(&fs.query_expr);
5702 let tsquery = format!("{}('english', {})", fs.tsquery_fn, query_sql);
5703 let rank_sql = format!("ts_rank({}, {})", search_col, tsquery);
5704
5705 let (obj_tuple, object_shape_nodes) = if fs.object_shape.is_empty() {
5706 let type_expr = type_disc(&fs.source.type_name);
5707 let id_expr = format!("{}.\"id\"", qi(alias));
5708 let tuple = format!("{},\n {}", type_expr, id_expr);
5709 let id_node = ShapeNode::Scalar {
5710 name: "id".to_string(),
5711 position: 1,
5712 };
5713 (tuple, vec![id_node])
5714 } else {
5715 let (pointer_exprs, shape_pointers) = build_shape(&fs.object_shape, alias);
5716 let mut parts = vec![type_disc(&fs.source.type_name)];
5717 parts.extend(pointer_exprs);
5718 (parts.join(",\n "), prepend_type(shape_pointers))
5719 };
5720
5721 let outer = format!(
5722 "NULL::text,\n ROW(\n {}\n )::record,\n {}",
5723 obj_tuple, rank_sql,
5724 );
5725 let mut sql = format!(
5726 "SELECT (\n {}\n) AS result\nFROM {} AS {}\nWHERE {} @@ {}",
5727 outer,
5728 source_ref(&fs.source),
5729 qi(alias),
5730 search_col,
5731 tsquery,
5732 );
5733 if let Some(f) = &fs.filter {
5734 sql.push_str(&format!(" AND ({})", emit_expr(f)));
5735 }
5736 if let Some(dir) = &fs.order_by_rank {
5737 let dir_sql = match dir {
5738 IrSortDir::Asc => "ASC",
5739 IrSortDir::Desc => "DESC",
5740 };
5741 sql.push_str(&format!("\nORDER BY {} {}", rank_sql, dir_sql));
5742 }
5743 append_offset_limit(&mut sql, &fs.offset, &fs.limit);
5744
5745 let object_node = ShapeNode::Object {
5746 name: "object".to_string(),
5747 type_name: Some(fs.source.type_name.clone()),
5748 position: 1,
5749 cardinality: Cardinality::Many,
5750 pointers: object_shape_nodes,
5751 has_implicit_id: shape_has_implicit_id(&fs.object_shape),
5752 };
5753 let shape = ShapeDescriptor {
5754 root: ShapeNode::FtsSearch {
5755 object_position: 1,
5756 rank_position: 2,
5757 object_node: Box::new(object_node),
5758 },
5759 };
5760 SqlOutput {
5761 sql,
5762 shape,
5763 inference_plan: None,
5764 }
5765}
5766
5767fn emit_fts_search_deferred(fs: &IrFtsSearch) -> SqlOutput {
5768 let alias = &fs.source.alias;
5769 let ids_idx = fs
5770 .deferred_ids_param
5771 .expect("deferred_ids_param must be set for deferred backend");
5772 let scores_idx = fs
5773 .deferred_scores_param
5774 .expect("deferred_scores_param must be set for deferred backend");
5775 let ids_param = format!("${}", ids_idx + 1);
5776 let scores_param = format!("${}", scores_idx + 1);
5777
5778 let (obj_tuple, object_shape_nodes) = if fs.object_shape.is_empty() {
5779 let type_expr = type_disc(&fs.source.type_name);
5780 let id_expr = format!("{}.\"id\"", qi(alias));
5781 let tuple = format!("{},\n {}", type_expr, id_expr);
5782 let id_node = ShapeNode::Scalar {
5783 name: "id".to_string(),
5784 position: 1,
5785 };
5786 (tuple, vec![id_node])
5787 } else {
5788 let (pointer_exprs, shape_pointers) = build_shape(&fs.object_shape, alias);
5789 let mut parts = vec![type_disc(&fs.source.type_name)];
5790 parts.extend(pointer_exprs);
5791 (parts.join(",\n "), prepend_type(shape_pointers))
5792 };
5793
5794 let outer = format!(
5795 "NULL::text,\n ROW(\n {}\n )::record,\n \"_os\".\"score\"",
5796 obj_tuple,
5797 );
5798 let mut sql = format!(
5799 concat!(
5800 "SELECT (\n {}\n) AS result\n",
5801 "FROM {} AS {}\n",
5802 "JOIN UNNEST({}::uuid[], {}::float8[]) AS \"_os\"(\"id\", \"score\")\n",
5803 " ON \"_os\".\"id\" = {}.\"id\"",
5804 ),
5805 outer,
5806 source_ref(&fs.source),
5807 qi(alias),
5808 ids_param,
5809 scores_param,
5810 qi(alias),
5811 );
5812 if let Some(f) = &fs.filter {
5813 sql.push_str(&format!("\nWHERE ({})", emit_expr(f)));
5814 }
5815 if let Some(dir) = &fs.order_by_rank {
5816 let dir_sql = match dir {
5817 IrSortDir::Asc => "ASC",
5818 IrSortDir::Desc => "DESC",
5819 };
5820 sql.push_str(&format!("\nORDER BY \"_os\".\"score\" {}", dir_sql));
5821 }
5822 let size = fs.limit.as_ref().and_then(|lim| {
5824 if let IrExpr::Literal(IrLiteral::Int(n)) = lim {
5825 Some(*n as usize)
5826 } else {
5827 None
5828 }
5829 });
5830
5831 let object_node = ShapeNode::Object {
5832 name: "object".to_string(),
5833 type_name: Some(fs.source.type_name.clone()),
5834 position: 1,
5835 cardinality: Cardinality::Many,
5836 pointers: object_shape_nodes,
5837 has_implicit_id: shape_has_implicit_id(&fs.object_shape),
5838 };
5839 let shape = ShapeDescriptor {
5840 root: ShapeNode::FtsSearch {
5841 object_position: 1,
5842 rank_position: 2,
5843 object_node: Box::new(object_node),
5844 },
5845 };
5846 let backend_str = match fs.backend {
5847 crate::schema::SearchBackend::Meilisearch => "meilisearch",
5848 _ => "opensearch",
5849 };
5850 let inference_plan = Some(InferencePlan::Search {
5851 backend: backend_str.to_string(),
5852 index_name: fs.deferred_index_name.clone().unwrap_or_default(),
5853 query_param_name: fs.deferred_query_param_name.clone().unwrap_or_default(),
5854 query_literal: fs.deferred_query_literal.clone(),
5855 size,
5856 });
5857 SqlOutput {
5858 sql,
5859 shape,
5860 inference_plan,
5861 }
5862}
5863
5864fn emit_function_select(sel: &IrFunctionSelect) -> SqlOutput {
5867 let alias = &sel.alias;
5868 let (pointer_exprs, shape_pointers) = build_shape(&sel.shape, alias);
5869
5870 let type_expr = if sel.polymorphic {
5871 format!("{}.\"__type__\"", qi(alias))
5872 } else {
5873 type_disc(&sel.type_name)
5874 };
5875 let mut parts = vec![type_expr];
5876 parts.extend(pointer_exprs);
5877 let tuple = parts.join(",\n ");
5878 let distinct = if sel.distinct { "DISTINCT " } else { "" };
5879
5880 let args_sql = sel.fn_args.iter().map(emit_expr).collect::<Vec<_>>().join(", ");
5881 let fn_call = format!("{}.{}({})", pg_schema(&sel.fn_module), qi(&sel.fn_name), args_sql);
5882
5883 let from_clause = if sel.polymorphic {
5884 format!("{} AS {}", fn_call, qi(alias))
5887 } else {
5888 format!("{} AS {}", fn_call, qi(alias))
5889 };
5890
5891 let mut sql = format!("SELECT {}(\n {}\n) AS result\nFROM {}", distinct, tuple, from_clause,);
5892 append_filter(&mut sql, &sel.filter);
5893 append_order_by(&mut sql, &sel.order_by);
5894 append_offset_limit(&mut sql, &sel.offset, &sel.limit);
5895
5896 let root_pointers = prepend_type(shape_pointers);
5897 SqlOutput {
5898 sql,
5899 shape: ShapeDescriptor {
5900 root: ShapeNode::Object {
5901 name: String::new(),
5902 type_name: Some(sel.type_name.clone()),
5903 position: 0,
5904 cardinality: Cardinality::Many,
5905 pointers: root_pointers,
5906 has_implicit_id: shape_has_implicit_id(&sel.shape),
5907 },
5908 },
5909 inference_plan: None,
5910 }
5911}
5912
5913pub fn emit_fn_body(ir: &crate::ir::IrOutput) -> String {
5919 with_subtype_fanouts(&ir.subtype_fanouts, || emit_output_fn_body(ir))
5920}
5921
5922fn emit_output_fn_body(ir: &crate::ir::IrOutput) -> String {
5923 let body = match &ir.stmt {
5924 IrStmt::Select(sel) if matches!(sel.rows.as_slice(), [IrRowSource::Free(IrFreeExpr::Scalar(_))]) => {
5925 let IrRowSource::Free(IrFreeExpr::Scalar(e)) = &sel.rows[0] else {
5926 unreachable!()
5927 };
5928 format!("SELECT {}", emit_expr(e))
5929 }
5930 other => emit_dml_as_cte_source(other),
5931 };
5932 if ir.ctes.is_empty() {
5933 body
5934 } else {
5935 let cte_prefix = emit_cte_prefix(&ir.ctes);
5936 format!("{}{}", cte_prefix, body)
5937 }
5938}
5939
5940fn emit_literal(lit: &IrLiteral) -> String {
5941 match lit {
5942 IrLiteral::Str(s) => sql_str(s),
5943 IrLiteral::Int(i) => i.to_string(),
5944 IrLiteral::Float(f) => {
5945 let s = f.to_string();
5954 let s = if s.contains('.') || s.contains('e') {
5955 s
5956 } else {
5957 format!("{}.0", s)
5958 };
5959 format!("({}::float8)", s)
5960 }
5961 IrLiteral::Bool(b) => {
5962 if *b {
5963 "TRUE".into()
5964 } else {
5965 "FALSE".into()
5966 }
5967 }
5968 }
5969}
5970
5971#[cfg(test)]
5974mod tests {
5975 use super::*;
5976 use crate::ir;
5977 use crate::parse;
5978 use crate::schema::{
5979 FunctionDescriptor, FunctionParamDescriptor, GlobalDescriptor, LinkDescriptor, MultiLinkDescriptor,
5980 NamedTupleDescriptor, PropertyDescriptor, SchemaDescriptor, TypeDescriptor,
5981 };
5982
5983 #[test]
5984 fn a_template_argument_keeps_the_parameters_it_carries() {
5985 let filled = fill_template("f($1, $2, $10)", &["($2)::int".to_string(), "b".to_string()]);
5988 assert_eq!(filled, "f(($2)::int, b, $10)");
5989 }
5990
5991 fn make_schema() -> SchemaDescriptor {
5992 SchemaDescriptor {
5993 types: vec![
5994 TypeDescriptor {
5995 name: "Person".into(),
5996 module: "default".into(),
5997 table: "Person".into(),
5998 abstract_: false,
5999 materialized: false,
6000 description: None,
6001 parents: vec![],
6002 interfaces: vec![],
6003 bases: vec![],
6004 properties: vec![
6005 PropertyDescriptor {
6006 name: "id".into(),
6007 pg_type: "uuid".into(),
6008 nullable: false,
6009 default_sql: Some("uuidv7()".into()),
6010 default_pyql: None,
6011 description: None,
6012 check_constraints: vec![],
6013 is_exclusive: true,
6014 is_pk: true,
6015 is_readonly: true,
6016 rewrites: vec![],
6017 tuple_members: None,
6018 column_type: None,
6019 },
6020 PropertyDescriptor {
6021 name: "name".into(),
6022 pg_type: "text".into(),
6023 nullable: false,
6024 default_sql: None,
6025 default_pyql: None,
6026 description: None,
6027 check_constraints: vec![],
6028 is_exclusive: false,
6029 is_pk: false,
6030 is_readonly: false,
6031 rewrites: vec![],
6032 tuple_members: None,
6033 column_type: None,
6034 },
6035 PropertyDescriptor {
6036 name: "age".into(),
6037 pg_type: "int8".into(),
6038 nullable: true,
6039 default_sql: None,
6040 default_pyql: None,
6041 description: None,
6042 check_constraints: vec![],
6043 is_exclusive: false,
6044 is_pk: false,
6045 is_readonly: false,
6046 rewrites: vec![],
6047 tuple_members: None,
6048 column_type: None,
6049 },
6050 ],
6051 links: vec![LinkDescriptor {
6052 name: "company".into(),
6053 target: "default::Company".into(),
6054 nullable: true,
6055 through: None,
6056 description: None,
6057 default_pyql: None,
6058 is_exclusive: false,
6059 is_readonly: false,
6060 rewrites: vec![],
6061 on_delete: vec![],
6062 }],
6063 multilinks: vec![MultiLinkDescriptor {
6064 name: "posts".into(),
6065 target: "default::Post".into(),
6066 through: None,
6067 nullable: false,
6068 description: None,
6069 default_pyql: None,
6070 on_delete: vec![],
6071 is_exclusive: false,
6072 }],
6073 computed: vec![],
6074 constraints: vec![],
6075 indexes: vec![],
6076 partition: None,
6077 vector_indexes: vec![],
6078 search_indexes: vec![],
6079 triggers: vec![],
6080 junction: false,
6081 signals: vec![],
6082 },
6083 TypeDescriptor {
6084 name: "Company".into(),
6085 module: "default".into(),
6086 table: "Company".into(),
6087 abstract_: false,
6088 materialized: false,
6089 description: None,
6090 parents: vec![],
6091 interfaces: vec![],
6092 bases: vec![],
6093 properties: vec![PropertyDescriptor {
6094 name: "name".into(),
6095 pg_type: "text".into(),
6096 nullable: false,
6097 default_sql: None,
6098 default_pyql: None,
6099 description: None,
6100 check_constraints: vec![],
6101 is_exclusive: false,
6102 is_pk: false,
6103 is_readonly: false,
6104 rewrites: vec![],
6105 tuple_members: None,
6106 column_type: None,
6107 }],
6108 links: vec![],
6109 multilinks: vec![],
6110 computed: vec![],
6111 constraints: vec![],
6112 indexes: vec![],
6113 partition: None,
6114 vector_indexes: vec![],
6115 search_indexes: vec![],
6116 triggers: vec![],
6117 junction: false,
6118 signals: vec![],
6119 },
6120 TypeDescriptor {
6121 name: "Post".into(),
6122 module: "default".into(),
6123 table: "Post".into(),
6124 abstract_: false,
6125 materialized: false,
6126 description: None,
6127 parents: vec![],
6128 interfaces: vec![],
6129 bases: vec![],
6130 properties: vec![PropertyDescriptor {
6131 name: "title".into(),
6132 pg_type: "text".into(),
6133 nullable: false,
6134 default_sql: None,
6135 default_pyql: None,
6136 description: None,
6137 check_constraints: vec![],
6138 is_exclusive: false,
6139 is_pk: false,
6140 is_readonly: false,
6141 rewrites: vec![],
6142 tuple_members: None,
6143 column_type: None,
6144 }],
6145 links: vec![],
6146 multilinks: vec![],
6147 computed: vec![],
6148 constraints: vec![],
6149 indexes: vec![],
6150 partition: None,
6151 vector_indexes: vec![],
6152 search_indexes: vec![],
6153 triggers: vec![],
6154 junction: false,
6155 signals: vec![],
6156 },
6157 ],
6158 scalars: vec![],
6159 enums: vec![],
6160 named_tuples: vec![],
6161 globals: vec![],
6162 functions: vec![],
6163 aliases: vec![],
6164 channels: vec![],
6165 ..Default::default()
6166 }
6167 }
6168
6169 fn compile_and_emit(query: &str) -> SqlOutput {
6170 let schema = make_schema();
6171 compile_and_emit_with(query, &schema)
6172 }
6173
6174 fn compile_and_emit_with(query: &str, schema: &SchemaDescriptor) -> SqlOutput {
6175 let ast = parse::parse(query).expect("parse failed");
6176 let ir = ir::compile(&ast, schema).expect("IR compile failed");
6177 emit(&ir)
6178 }
6179
6180 #[test]
6185 fn a_multilink_walk_can_be_tested_against_a_set_literal() {
6186 let out = compile_and_emit("SELECT Person FILTER .posts.title IN {'a', 'b'}");
6187 assert!(out.sql.contains("= ANY(ARRAY['a', 'b'])"), "{}", out.sql);
6188 }
6189
6190 #[test]
6194 fn all_over_a_multilink_is_not_the_same_question_as_any() {
6195 for condition in [
6196 ".posts.title IN {'a', 'b'}",
6197 ".posts IN ps",
6198 ".posts = ps",
6199 ".posts.title = 'x'",
6200 ] {
6201 let sql = |quantifier: &str| {
6202 compile_and_emit(&format!(
6203 "WITH ps := (SELECT Post) SELECT Person FILTER {quantifier}({condition})"
6204 ))
6205 .sql
6206 };
6207 assert_ne!(
6208 sql("all"),
6209 sql("any"),
6210 "all({condition}) must not compile to what any({condition}) does"
6211 );
6212 }
6213 }
6214
6215 #[test]
6219 fn all_over_a_multilink_answers_for_every_element() {
6220 let out = compile_and_emit("SELECT Person FILTER all(.posts.title IN {'a', 'b'})");
6221 assert!(
6222 out.sql.contains("NOT EXISTS(") && out.sql.contains("NOT (\"t2\".\"title\" = ANY("),
6223 "no element fails the test:\n{}",
6224 out.sql
6225 );
6226 let out = compile_and_emit("WITH ps := (SELECT Post) SELECT Person FILTER all(.posts IN ps)");
6227 assert!(
6228 out.sql
6229 .contains("coalesce(bool_and(\"_s\".\"v\"), true) FROM unnest(ARRAY(SELECT ("),
6230 "every answer, reduced:\n{}",
6231 out.sql
6232 );
6233 }
6234
6235 #[test]
6238 fn any_over_a_multilink_keeps_its_exists() {
6239 let out = compile_and_emit("WITH ps := (SELECT Post) SELECT Person FILTER any(.posts IN ps)");
6240 assert!(out.sql.contains("WHERE EXISTS("), "{}", out.sql);
6241 assert!(!out.sql.contains("bool_or"), "{}", out.sql);
6242 }
6243
6244 #[test]
6250 fn comparing_a_multilink_to_a_multi_row_binding_is_membership() {
6251 let out = compile_and_emit("WITH ps := (SELECT Post) SELECT Person FILTER .posts = ps");
6252 assert!(
6253 out.sql.contains("\"target\" = ANY((SELECT \"id\" FROM \"ps\"))"),
6254 "{}",
6255 out.sql
6256 );
6257 let out = compile_and_emit("WITH ps := (SELECT Post) SELECT Person FILTER .posts != ps");
6258 assert!(
6259 out.sql
6260 .contains("NOT (\"t2\".\"target\" = ANY((SELECT \"id\" FROM \"ps\")))"),
6261 "{}",
6262 out.sql
6263 );
6264 }
6265
6266 #[test]
6268 fn comparing_a_multilink_to_a_single_row_binding_stays_an_equality() {
6269 let out = compile_and_emit("WITH p := (SELECT Post LIMIT 1) SELECT Person FILTER .posts = p");
6270 assert!(
6271 out.sql.contains("\"target\" = (SELECT \"id\" FROM \"p\")"),
6272 "{}",
6273 out.sql
6274 );
6275 }
6276
6277 #[test]
6283 fn membership_of_one_set_in_another_answers_per_element() {
6284 let out = compile_and_emit("WITH a := (SELECT Person), b := (SELECT Person) SELECT a IN b");
6285 assert!(
6286 out.sql.contains("FROM \"a\""),
6287 "the left set is the source:\n{}",
6288 out.sql
6289 );
6290 assert!(
6291 out.sql.contains("\"id\" = ANY((SELECT \"id\" FROM \"b\"))"),
6292 "each row tests its own id:\n{}",
6293 out.sql
6294 );
6295 assert!(
6296 !out.sql.contains("(SELECT \"id\" FROM \"a\") = ANY"),
6297 "the left set must not collapse to one value:\n{}",
6298 out.sql
6299 );
6300 }
6301
6302 #[test]
6304 fn membership_of_a_type_in_a_set_answers_per_row() {
6305 let out = compile_and_emit("WITH b := (SELECT Person) SELECT Person NOT IN b");
6306 assert!(
6307 out.sql.contains("FROM \"public\".\"Person\""),
6308 "the type is the source:\n{}",
6309 out.sql
6310 );
6311 assert!(
6312 out.sql.contains("\"id\" <> ALL((SELECT \"id\" FROM \"b\"))"),
6313 "{}",
6314 out.sql
6315 );
6316 }
6317
6318 #[test]
6322 fn quantifying_membership_of_one_set_in_another_aggregates_the_elements() {
6323 for (query, aggregate) in [("SELECT all(a IN b)", "bool_and"), ("SELECT any(a IN b)", "bool_or")] {
6324 let out = compile_and_emit(&format!("WITH a := (SELECT Person), b := (SELECT Person) {query}"));
6325 assert!(
6326 out.sql.contains(&format!("{aggregate}(\"_s\".\"v\")")),
6327 "{query}:\n{}",
6328 out.sql
6329 );
6330 assert!(
6331 out.sql.contains("FROM unnest(ARRAY(SELECT ("),
6332 "one answer per element, then reduced — {query}:\n{}",
6333 out.sql
6334 );
6335 assert!(
6336 !out.sql.contains("(SELECT \"id\" FROM \"a\") = ANY"),
6337 "the left set must not collapse to one value — {query}:\n{}",
6338 out.sql
6339 );
6340 }
6341 }
6342
6343 #[test]
6347 fn membership_of_a_multilink_is_a_boolean_per_element() {
6348 let out = compile_and_emit("WITH ps := (SELECT Post) SELECT Person { t := .posts IN ps }");
6349 assert!(
6350 out.sql
6351 .contains("ARRAY(SELECT (\"t3\".\"id\" = ANY((SELECT \"id\" FROM \"ps\")))"),
6352 "one answer per post:\n{}",
6353 out.sql
6354 );
6355 assert!(
6356 !out.sql.contains("EXISTS("),
6357 "not one answer for the link:\n{}",
6358 out.sql
6359 );
6360 }
6361
6362 #[test]
6365 fn membership_of_a_multilink_in_a_filter_stays_one_answer() {
6366 let out = compile_and_emit("WITH ps := (SELECT Post) SELECT Person FILTER .posts IN ps");
6367 assert!(out.sql.contains("WHERE EXISTS("), "{}", out.sql);
6368 }
6369
6370 #[test]
6374 fn quantifying_membership_of_a_multilink_aggregates_its_elements() {
6375 let out = compile_and_emit("WITH ps := (SELECT Post) SELECT Person { t := all(.posts IN ps) }");
6376 assert!(
6377 out.sql
6378 .contains("coalesce(bool_and(\"_s\".\"v\"), true) FROM unnest(ARRAY(SELECT ("),
6379 "{}",
6380 out.sql
6381 );
6382 let out = compile_and_emit("WITH ps := (SELECT Post) SELECT Person { t := any(.posts IN ps) }");
6383 assert!(
6384 out.sql
6385 .contains("coalesce(bool_or(\"_s\".\"v\"), false) FROM unnest(ARRAY(SELECT ("),
6386 "{}",
6387 out.sql
6388 );
6389 }
6390
6391 #[test]
6394 fn membership_in_a_set_literal_answers_per_element() {
6395 let out = compile_and_emit("SELECT Person { t := .posts.title IN {'a', 'b'} }");
6396 assert!(
6397 out.sql
6398 .contains("ARRAY(SELECT (\"t2\".\"title\" = ANY(ARRAY['a', 'b']))"),
6399 "{}",
6400 out.sql
6401 );
6402 }
6403
6404 #[test]
6412 fn test_a_mutating_loop_as_a_select_source_is_bound_to_a_cte() {
6413 let out =
6414 compile_and_emit("SELECT (FOR p IN (SELECT Person) UNION (INSERT Post { title := p.name })) { title }");
6415 assert!(
6416 !out.sql.contains("LATERAL"),
6417 "the insert must not sit in a LATERAL, got:\n{}",
6418 out.sql
6419 );
6420 assert!(
6421 out.sql
6422 .contains("\"_nested_dml_0__ids\" AS (\nINSERT INTO \"public\".\"Post\""),
6423 "the loop's insert belongs in a top-level CTE, got:\n{}",
6424 out.sql
6425 );
6426 assert!(
6427 out.sql.contains("FROM \"_nested_dml_0\" AS "),
6428 "the select must read the bound loop back, got:\n{}",
6429 out.sql
6430 );
6431 }
6432
6433 #[test]
6437 fn test_a_loop_updating_its_own_variable_compiles_as_a_select_source() {
6438 let out = compile_and_emit("SELECT (FOR p IN (SELECT Person) UNION (UPDATE p SET { age := 1 })) { name }");
6439 assert!(
6440 out.sql
6441 .contains("\"_nested_dml_0__ids\" AS (\nUPDATE \"public\".\"Person\""),
6442 "got:\n{}",
6443 out.sql
6444 );
6445 assert!(!out.sql.contains("LATERAL"), "got:\n{}", out.sql);
6446 }
6447
6448 #[test]
6457 fn test_a_guarded_insert_reaches_a_binding_inside_a_nested_loop() {
6458 let out = compile_and_emit(
6459 "WITH existing := (SELECT Company FILTER .name = 'x'), \
6460 made := (INSERT Person { name := 'a', \
6461 posts := (FOR s IN {'p','q'} UNION ( \
6462 WITH extra := (INSERT Company { name := s }) \
6463 INSERT Post { title := s })) }) \
6464 IF NOT EXISTS existing ELSE {} \
6465 SELECT made",
6466 );
6467 let at = out
6468 .sql
6469 .find("INSERT INTO \"public\".\"Company\"")
6470 .unwrap_or_else(|| panic!("no Company insert in:\n{}", out.sql));
6471 let tail = &out.sql[at..];
6472 let stmt_end = tail.find("RETURNING").unwrap_or(tail.len());
6473 assert!(
6474 tail[..stmt_end].contains("NOT EXISTS(SELECT 1 FROM \"existing\")"),
6475 "a binding inside the loop must carry the condition too, got:\n{}",
6476 out.sql
6477 );
6478 }
6479
6480 #[test]
6485 fn test_a_guarded_insert_carries_its_condition_into_what_it_nests() {
6486 let out = compile_and_emit(
6487 "WITH existing := (SELECT Company FILTER .name = 'x'), \
6488 made := (INSERT Person { name := 'a', company := (INSERT Company { name := 'b' }), \
6489 posts := (FOR s IN {'p','q'} UNION (INSERT Post { title := s })) }) \
6490 IF NOT EXISTS existing ELSE {} \
6491 SELECT made",
6492 );
6493 for table in ["\"Person\"", "\"Company\" (", "\"Post\""] {
6494 let at = out
6495 .sql
6496 .find(&format!("INSERT INTO \"public\".{}", table))
6497 .unwrap_or_else(|| panic!("no insert into {} in:\n{}", table, out.sql));
6498 let tail = &out.sql[at..];
6499 let stmt_end = tail.find("RETURNING").unwrap_or(tail.len());
6500 assert!(
6501 tail[..stmt_end].contains("NOT EXISTS(SELECT 1 FROM \"existing\")"),
6502 "insert into {} writes whatever the condition says, got:\n{}",
6503 table,
6504 out.sql
6505 );
6506 }
6507 }
6508
6509 #[test]
6510 fn test_a_guarded_insert_keeps_its_guard_when_it_nests_another() {
6511 let out = compile_and_emit(
6512 "WITH existing := (SELECT Company FILTER .name = 'x'), \
6513 made := (INSERT Person { name := 'a', company := (INSERT Company { name := 'b' }) }) \
6514 IF NOT EXISTS existing ELSE {} \
6515 SELECT made",
6516 );
6517 let outer = out
6518 .sql
6519 .find("INSERT INTO \"public\".\"Person\"")
6520 .unwrap_or_else(|| panic!("no Person insert in:\n{}", out.sql));
6521 let tail = &out.sql[outer..];
6522 let stmt_end = tail.find("RETURNING").unwrap_or(tail.len());
6523 assert!(
6524 tail[..stmt_end].contains("NOT EXISTS(SELECT 1 FROM \"existing\")"),
6525 "the insert the conditional names must carry the condition, got:\n{}",
6526 out.sql
6527 );
6528 }
6529
6530 #[test]
6531 fn a_nested_read_inside_a_with_opens_no_second_with() {
6532 let out = compile_and_emit_with(
6533 "WITH a := (SELECT Person FILTER .age > 1), b := (SELECT Person LIMIT 1) SELECT (SELECT (a UNION b))",
6534 &make_schema(),
6535 );
6536 assert_eq!(out.sql.matches("WITH").count(), 1, "{}", out.sql);
6537 }
6538
6539 #[test]
6540 fn a_comparison_against_a_multi_row_with_binding_tests_membership() {
6541 let out = compile_and_emit_with(
6545 "WITH companies := (SELECT Company FILTER .name = 'x') SELECT Person FILTER .company = companies",
6546 &make_schema(),
6547 );
6548 assert!(
6549 out.sql.contains("= ANY((SELECT \"id\" FROM \"companies\"))"),
6550 "{}",
6551 out.sql
6552 );
6553
6554 let out = compile_and_emit_with(
6555 "WITH companies := (SELECT Company) SELECT Person FILTER any(.company = companies)",
6556 &make_schema(),
6557 );
6558 assert!(
6559 out.sql.contains("= ANY((SELECT \"id\" FROM \"companies\"))"),
6560 "{}",
6561 out.sql
6562 );
6563
6564 let out = compile_and_emit_with(
6565 "WITH companies := (SELECT Company) SELECT Person FILTER .company != companies",
6566 &make_schema(),
6567 );
6568 assert!(out.sql.contains("NOT (\"t1\".\"company_id\" = ANY("), "{}", out.sql);
6569 }
6570
6571 #[test]
6572 fn a_comparison_against_a_single_row_with_binding_stays_a_scalar_read() {
6573 let out = compile_and_emit_with(
6574 "WITH company := (SELECT Company LIMIT 1) SELECT Person FILTER .company = company",
6575 &make_schema(),
6576 );
6577 assert!(out.sql.contains("= (SELECT \"id\" FROM \"company\")"), "{}", out.sql);
6578 }
6579
6580 #[test]
6581 fn an_aggregate_over_a_backlink_counts_inside_a_subquery() {
6582 let out = compile_and_emit_with("SELECT Post { n := count(.<posts) }", &make_schema());
6583 assert!(
6584 !out.sql.contains("count((SELECT"),
6585 "an aggregate over a scalar subquery:\n{}",
6586 out.sql
6587 );
6588 assert!(out.sql.contains("count("), "{}", out.sql);
6589 }
6590
6591 #[test]
6592 fn an_aggregate_over_a_relative_subselect_counts_inside_a_subquery() {
6593 let out = compile_and_emit_with(
6594 "SELECT Post { n := count((SELECT .<posts FILTER .age > 1)) }",
6595 &make_schema(),
6596 );
6597 assert!(
6598 !out.sql.contains("count((SELECT"),
6599 "an aggregate over a scalar subquery:\n{}",
6600 out.sql
6601 );
6602 assert!(
6603 out.sql.contains(r#"(SELECT count("_s"."v") FROM unnest(ARRAY("#),
6604 "{}",
6605 out.sql
6606 );
6607 }
6608
6609 #[test]
6610 fn any_over_a_link_type_check_tests_each_element() {
6611 let out = compile_and_emit_with(
6612 "SELECT Company { name } FILTER any(.<company.posts IS Post)",
6613 &make_schema(),
6614 );
6615 assert!(
6616 !out.sql.contains("bool_or(ARRAY"),
6617 "an aggregate over outer rows:\n{}",
6618 out.sql
6619 );
6620 assert!(
6621 out.sql
6622 .contains("FROM unnest(ARRAY(SELECT ('default::Post' = 'default::Post')"),
6623 "{}",
6624 out.sql
6625 );
6626 }
6627
6628 #[test]
6629 fn a_link_type_check_reads_the_linked_object() {
6630 let out = compile_and_emit_with("SELECT Person { name } FILTER .company IS Company", &make_schema());
6631 assert!(!out.sql.contains("WHERE FALSE"), "{}", out.sql);
6632 assert!(out.sql.contains("'default::Company'"), "{}", out.sql);
6633 }
6634
6635 fn schema_with_a_subtype() -> SchemaDescriptor {
6637 let mut schema = make_schema();
6638 let mut admin = schema.types[0].clone();
6639 admin.name = "Admin".into();
6640 admin.table = "Admin".into();
6641 admin.bases = vec!["default::Person".into()];
6642 schema.types.push(admin);
6643 schema
6644 }
6645
6646 #[test]
6647 fn a_type_with_subtypes_reads_their_rows_too() {
6648 let out = compile_and_emit_with("SELECT Person { name }", &schema_with_a_subtype());
6649 assert!(
6650 out.sql.contains("'default::Admin'::text AS \"__type__\"") && out.sql.contains("FROM \"public\".\"Admin\""),
6651 "expected the subtype's table in the read:\n{}",
6652 out.sql
6653 );
6654 }
6655
6656 #[test]
6657 fn a_path_from_a_type_with_subtypes_reads_their_rows_too() {
6658 let out = compile_and_emit_with("SELECT Person.name", &schema_with_a_subtype());
6659 assert!(out.sql.contains("FROM \"public\".\"Admin\""), "{}", out.sql);
6660 }
6661
6662 #[test]
6663 fn a_multilink_of_a_type_with_subtypes_reads_their_junctions_too() {
6664 let out = compile_and_emit_with("SELECT Person { posts: { title } }", &schema_with_a_subtype());
6665 assert!(
6666 out.sql.contains(
6667 "(SELECT \"source\", \"target\" FROM \"public\".\"Person.posts\" UNION ALL \
6668 SELECT \"source\", \"target\" FROM \"public\".\"Admin.posts\")"
6669 ),
6670 "expected the junctions unioned:\n{}",
6671 out.sql
6672 );
6673 }
6674
6675 #[test]
6676 fn is_a_supertype_matches_its_subtypes() {
6677 let out = compile_and_emit_with(
6678 "SELECT Person { name } FILTER Person IS Admin",
6679 &schema_with_a_subtype(),
6680 );
6681 assert!(out.sql.contains("\"__type__\" = 'default::Admin'"), "{}", out.sql);
6682 }
6683
6684 #[test]
6685 fn a_write_to_a_type_with_subtypes_reaches_their_tables() {
6686 let out = compile_and_emit_with("UPDATE Person SET { age := 1 }", &schema_with_a_subtype());
6687 assert!(
6688 out.sql.contains("UPDATE \"public\".\"Person\"") && out.sql.contains("UPDATE \"public\".\"Admin\""),
6689 "expected both tables updated:\n{}",
6690 out.sql
6691 );
6692 assert!(
6693 !out.sql.contains("UPDATE (\n"),
6694 "a write never targets the union:\n{}",
6695 out.sql
6696 );
6697 }
6698
6699 #[test]
6700 fn test_comparing_a_multilink_itself_to_an_object() {
6701 let out = compile_and_emit_with(
6705 "WITH p := (SELECT Post LIMIT 1) SELECT Person { id } FILTER any(.posts = p)",
6706 &make_schema(),
6707 );
6708 assert!(
6709 out.sql.contains("EXISTS((SELECT 1\nFROM \"public\".\"Person.posts\""),
6710 "expected a junction EXISTS:\n{}",
6711 out.sql
6712 );
6713 assert!(
6714 out.sql.contains("\"target\" = (SELECT \"id\" FROM \"p\")"),
6715 "expected the junction target compared to the bound object's id:\n{}",
6716 out.sql
6717 );
6718 }
6719
6720 #[test]
6721 fn test_comparing_a_multilink_itself_warns_without_any() {
6722 let ast = parse::parse("WITH p := (SELECT Post LIMIT 1) SELECT Person { id } FILTER .posts = p").unwrap();
6723 let ir = ir::compile(&ast, &make_schema()).unwrap();
6724 assert!(
6725 ir.warnings.iter().any(|w| w.contains("multi-link '.posts'")),
6726 "expected the set-valued FILTER warning, got: {:?}",
6727 ir.warnings
6728 );
6729 }
6730
6731 #[test]
6732 fn test_detached_names_the_type_of_a_nested_select() {
6733 let out = compile_and_emit_with(
6737 "INSERT Person { name := $n, company := (SELECT detached Company FILTER .name = $c LIMIT 1) }",
6738 &make_schema(),
6739 );
6740 assert!(
6741 out.sql.contains("INSERT INTO \"public\".\"Person\""),
6742 "expected the insert to compile:\n{}",
6743 out.sql
6744 );
6745 assert!(
6746 out.sql.contains("FROM \"public\".\"Company\""),
6747 "expected the detached select to read the Company table:\n{}",
6748 out.sql
6749 );
6750 }
6751
6752 #[test]
6753 fn test_select_over_an_update_on_a_with_binding() {
6754 let out = compile_and_emit_with(
6759 "WITH p := (SELECT Person FILTER .id = $a LIMIT 1) SELECT (UPDATE p SET { name := $n }) { id }",
6760 &make_schema(),
6761 );
6762 assert!(
6763 out.sql.contains("UPDATE \"public\".\"Person\""),
6764 "expected the update to resolve to the bound type's table:\n{}",
6765 out.sql
6766 );
6767 }
6768
6769 #[test]
6770 fn test_a_mutation_as_a_free_object_field() {
6771 let out = compile_and_emit_with(
6775 "SELECT { renamed := (UPDATE Person FILTER .id = $a SET { name := $n }), n := 1 }",
6776 &make_schema(),
6777 );
6778 assert!(
6779 out.sql.contains("UPDATE \"public\".\"Person\""),
6780 "expected the mutation to become a data-modifying CTE:\n{}",
6781 out.sql
6782 );
6783 assert!(
6784 out.sql
6785 .contains("(SELECT \"t1\".\"id\"\nFROM \"_nested_dml_0\" AS \"t1\") AS \"_f0\""),
6786 "expected the field to carry the rows the mutation touched:\n{}",
6787 out.sql
6788 );
6789 }
6790
6791 #[test]
6792 fn test_a_sub_selects_filter_stays_on_its_own_subject_when_a_field_chain_projects_off_it() {
6793 let out = compile_and_emit_with("SELECT (SELECT Person FILTER .name = $n).company.name", &make_schema());
6797 assert!(
6798 out.sql.contains("WHERE (\"t0\".\"name\" = $1)"),
6799 "the inner filter belongs to the Person alias:\n{}",
6800 out.sql
6801 );
6802 }
6803
6804 #[test]
6805 fn test_reading_back_a_nested_insert_through_its_own_statement() {
6806 let out = compile_and_emit_with(
6807 "SELECT (INSERT Person { name := $n, posts := (INSERT Post { title := $t }) }) { id, posts: { title } }",
6808 &make_schema(),
6809 );
6810 assert!(out.sql.contains("__ml_add_0"), "got:\n{}", out.sql);
6811 }
6812
6813 #[test]
6814 fn test_reading_back_a_nested_insert_within_its_own_statement() {
6815 let out = compile_and_emit_with(
6819 "SELECT (INSERT Person { name := $n, posts := (INSERT Post { title := $t }) }) \
6820 { id, posts: { title } }",
6821 &make_schema(),
6822 );
6823 assert!(
6824 out.sql.contains("FROM \"_dml__ml_add_0\" AS \"jt\""),
6825 "the junction rows must come from the CTE that wrote them:\n{}",
6826 out.sql
6827 );
6828 assert!(
6829 out.sql.contains("\"_nested_dml_0\""),
6830 "and the targets from the nested insert's own CTE:\n{}",
6831 out.sql
6832 );
6833 }
6834
6835 #[test]
6836 fn test_a_nested_single_link_insert_reads_back_from_its_cte() {
6837 let out = compile_and_emit_with(
6838 "SELECT (INSERT Person { name := $n, company := (INSERT Company { name := $c }) }) \
6839 { id, company: { name } }",
6840 &make_schema(),
6841 );
6842 assert!(
6843 out.sql.contains("FROM \"_nested_dml_0\""),
6844 "the linked row must be read from the CTE that inserted it:\n{}",
6845 out.sql
6846 );
6847 }
6848
6849 #[test]
6850 fn test_a_free_object_field_holding_an_object_keeps_it_an_object() {
6851 let out = compile_and_emit_with(
6855 "WITH c := (SELECT Company LIMIT 1) SELECT { co := c { name }, n := 1 }",
6856 &make_schema(),
6857 );
6858 assert!(
6859 out.sql.contains("'default::Company'::text"),
6860 "the field must carry the object's own row:\n{}",
6861 out.sql
6862 );
6863 }
6864
6865 #[test]
6866 fn test_projecting_an_object_off_a_free_object() {
6867 let out = compile_and_emit_with(
6870 "WITH c := (SELECT Company LIMIT 1) SELECT { co := c { name }, n := 1 }.co",
6871 &make_schema(),
6872 );
6873 assert!(
6874 !out.sql.contains("jsonb_build_object"),
6875 "projecting a pointer must not go through jsonb:\n{}",
6876 out.sql
6877 );
6878 assert!(
6879 out.sql.contains("SELECT v AS result, v FROM"),
6880 "the object row is already the result row:\n{}",
6881 out.sql
6882 );
6883 }
6884
6885 #[test]
6886 fn test_a_shape_on_a_single_valued_object_path() {
6887 let out = compile_and_emit_with("SELECT Person { c := .company { name } }", &make_schema());
6892 assert!(
6893 out.sql.contains("'default::Company'::text"),
6894 "the pointer must carry the object's own row:\n{}",
6895 out.sql
6896 );
6897 assert!(
6898 !out.sql.contains("ARRAY(SELECT"),
6899 "a single-valued walk is one object, not an array of one:\n{}",
6900 out.sql
6901 );
6902 }
6903
6904 #[test]
6905 fn test_a_shapeless_single_link_still_reads_as_its_value() {
6906 let out = compile_and_emit_with("SELECT Person { c := .company }", &make_schema());
6909 assert!(
6910 !out.sql.contains("'default::Company'::text"),
6911 "a bare link reference must not grow an object row:\n{}",
6912 out.sql
6913 );
6914 }
6915
6916 #[test]
6917 fn test_a_tuple_element_holding_a_shaped_object() {
6918 let out = compile_and_emit_with(
6922 "WITH c := (SELECT Company LIMIT 1) SELECT (c { name }, 1)",
6923 &make_schema(),
6924 );
6925 assert!(
6926 out.sql.contains("'default::Company'::text"),
6927 "the element must carry the object's own row:\n{}",
6928 out.sql
6929 );
6930 }
6931
6932 #[test]
6933 fn test_an_object_returning_function_as_a_link_value() {
6934 let mut schema = make_schema();
6939 let company = schema
6941 .types
6942 .iter_mut()
6943 .find(|t| t.name == "Company")
6944 .expect("make_schema declares Company");
6945 company.properties.push(PropertyDescriptor {
6946 name: "id".into(),
6947 pg_type: "uuid".into(),
6948 nullable: false,
6949 default_sql: None,
6950 default_pyql: None,
6951 description: None,
6952 check_constraints: vec![],
6953 is_exclusive: true,
6954 is_pk: true,
6955 is_readonly: true,
6956 rewrites: vec![],
6957 tuple_members: None,
6958 column_type: None,
6959 });
6960 schema.functions.push(crate::schema::FunctionDescriptor {
6961 name: "current_company".into(),
6962 module: "default".into(),
6963 params: vec![],
6964 return_pg_type: "default::Company".into(),
6965 return_is_object: true,
6966 return_is_set: false,
6967 return_is_polymorphic: false,
6968 volatility: "stable".into(),
6969 body: "select Company limit 1".into(),
6970 });
6971 let out = compile_and_emit_with("INSERT Person { name := $n, company := current_company() }", &schema);
6972 assert!(
6973 out.sql.contains("FROM \"public\".\"current_company\"()"),
6974 "the function must supply the foreign key:\n{}",
6975 out.sql
6976 );
6977 }
6978
6979 fn make_schema_with_an_object_returning_default() -> SchemaDescriptor {
6983 let mut schema = make_schema();
6984 let company = schema
6985 .types
6986 .iter_mut()
6987 .find(|t| t.name == "Company")
6988 .expect("make_schema declares Company");
6989 company.properties.push(PropertyDescriptor {
6990 name: "id".into(),
6991 pg_type: "uuid".into(),
6992 nullable: false,
6993 default_sql: None,
6994 default_pyql: None,
6995 description: None,
6996 check_constraints: vec![],
6997 is_exclusive: true,
6998 is_pk: true,
6999 is_readonly: true,
7000 rewrites: vec![],
7001 tuple_members: None,
7002 column_type: None,
7003 });
7004 schema.functions.push(crate::schema::FunctionDescriptor {
7005 name: "current_company".into(),
7006 module: "default".into(),
7007 params: vec![],
7008 return_pg_type: "default::Company".into(),
7009 return_is_object: true,
7010 return_is_set: false,
7011 return_is_polymorphic: false,
7012 volatility: "stable".into(),
7013 body: "select Company limit 1".into(),
7014 });
7015 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
7016 let company_link = person.links.iter_mut().find(|l| l.name == "company").unwrap();
7017 company_link.default_pyql = Some("current_company()".into());
7018 schema
7019 }
7020
7021 #[test]
7022 fn an_object_returning_link_default_is_applied_by_the_insert() {
7023 let schema = make_schema_with_an_object_returning_default();
7027 let out = compile_and_emit_with("INSERT Person { name := $n }", &schema);
7028 assert!(
7029 out.sql.contains("\"company_id\"") && out.sql.contains("current_company"),
7030 "the default must supply the foreign key:\n{}",
7031 out.sql
7032 );
7033 }
7034
7035 #[test]
7036 fn an_explicit_value_wins_over_an_inlined_default() {
7037 let schema = make_schema_with_an_object_returning_default();
7038 let out = compile_and_emit_with("INSERT Person { name := $n, company := {} }", &schema);
7039 assert!(
7040 !out.sql.contains("current_company"),
7041 "the shape named the pointer, so the default does not apply:\n{}",
7042 out.sql
7043 );
7044 }
7045
7046 #[test]
7047 fn an_object_returning_link_default_gets_no_column_default() {
7048 let schema = make_schema_with_an_object_returning_default();
7051 let ddl = crate::export::export_schema(&schema).unwrap();
7052 assert!(
7053 !ddl.contains("\"company_id\" uuid DEFAULT"),
7054 "the column must carry no default:\n{ddl}"
7055 );
7056 crate::validate::validate_schema_types(&schema).expect("an inlined default is not an error");
7057 }
7058
7059 #[test]
7060 fn a_column_expressible_default_stays_in_the_ddl() {
7061 let mut schema = make_schema();
7063 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
7064 let age = person.properties.iter_mut().find(|p| p.name == "age").unwrap();
7065 age.default_pyql = Some("21".into());
7066 assert!(
7067 crate::ir::inlined_pointer_defaults(schema.types.iter().find(|t| t.name == "Person").unwrap(), &schema)
7068 .is_empty()
7069 );
7070 let ddl = crate::export::export_schema(&schema).unwrap();
7071 assert!(ddl.contains("DEFAULT 21"), "{ddl}");
7072 let out = compile_and_emit_with("INSERT Person { name := $n }", &schema);
7073 assert!(
7074 !out.sql.contains("21"),
7075 "the column DEFAULT still applies it:\n{}",
7076 out.sql
7077 );
7078 }
7079
7080 fn make_schema_with_an_inherited_self_naming_computed() -> SchemaDescriptor {
7084 let mut schema = make_schema();
7085 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
7086 person.computed.push(crate::schema::ComputedDescriptor {
7087 name: "own_name".into(),
7088 expression: "default::Person.name".into(),
7089 return_type: Some("text".into()),
7090 link_target: None,
7091 link_multi: false,
7092 });
7093 let mut employee = person.clone();
7094 employee.name = "Employee".into();
7095 employee.table = "Employee".into();
7096 employee.bases = vec!["default::Person".into()];
7097 schema.types.push(employee);
7098 schema
7099 }
7100
7101 #[test]
7102 fn an_inherited_computed_naming_its_declaring_type_still_means_the_row() {
7103 let schema = make_schema_with_an_inherited_self_naming_computed();
7108 let out = compile_and_emit_with("SELECT Employee { own_name }", &schema);
7109 assert!(
7110 !out.sql.contains("\"Person\""),
7111 "the computed must read the Employee row, not Person's table:\n{}",
7112 out.sql
7113 );
7114 crate::validate::validate_schema_types(&schema).expect("an inherited computed is single-valued");
7115 }
7116
7117 #[test]
7118 fn an_inherited_type_check_computed_tests_the_row_it_is_read_on() {
7119 let mut schema = make_schema();
7125 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
7126 person.computed.push(crate::schema::ComputedDescriptor {
7127 name: "employed".into(),
7128 expression: "(default::Person is default::Employee)".into(),
7129 return_type: Some("boolean".into()),
7130 link_target: None,
7131 link_multi: false,
7132 });
7133 let mut employee = person.clone();
7134 employee.name = "Employee".into();
7135 employee.table = "Employee".into();
7136 employee.bases = vec!["default::Person".into()];
7137 schema.types.push(employee);
7138
7139 crate::validate::validate_schema_types(&schema).expect("the check is single-valued on both types");
7140 let out = compile_and_emit_with("SELECT Employee { employed }", &schema);
7141 assert!(
7142 !out.sql.contains("\"Person\""),
7143 "the check must read the Employee row's own type:\n{}",
7144 out.sql
7145 );
7146 }
7147
7148 fn make_interface_schema_with_an_outsider() -> SchemaDescriptor {
7155 let mut schema = make_interface_schema();
7156 let individual = schema
7157 .types
7158 .iter()
7159 .find(|t| t.name == "Individual")
7160 .expect("Individual is in the interface schema")
7161 .clone();
7162 let mut company = individual.clone();
7163 company.name = "Company".into();
7164 company.table = "Company".into();
7165 company.interfaces = vec![];
7166 company.computed = vec![];
7167 schema.types.push(company);
7168 let mut organisation = individual;
7169 organisation.name = "Organisation".into();
7170 organisation.table = "Organisation".into();
7171 organisation.computed = vec![];
7172 schema.types.push(organisation);
7173 schema
7174 }
7175
7176 #[test]
7177 fn a_type_check_naming_another_type_reads_as_one_value_in_a_filter() {
7178 let out = compile_and_emit(
7186 "SELECT Person { name } FILTER (.name = 'a' if default::Company is default::Post else FALSE)",
7187 );
7188 assert!(
7189 !out.sql.contains("ARRAY(SELECT"),
7190 "an answer that is the same for every row has nothing to iterate:\n{}",
7191 out.sql
7192 );
7193 assert!(
7194 out.sql.contains("CASE WHEN FALSE"),
7195 "a Company is never a Post:\n{}",
7196 out.sql
7197 );
7198 }
7199
7200 #[test]
7201 fn a_polymorphic_type_is_never_one_from_a_hierarchy_it_shares_nothing_with() {
7202 let schema = make_interface_schema_with_an_outsider();
7209 let out = compile_and_emit_with(
7210 "SELECT Company { id } FILTER (.first_name = 'a' if default::Account is default::Company else FALSE)",
7211 &schema,
7212 );
7213 assert!(
7214 !out.sql.contains("assert_single") && !out.sql.contains("ARRAY(SELECT"),
7215 "nothing to ask per row when the hierarchies are disjoint:\n{}",
7216 out.sql
7217 );
7218 assert!(
7219 out.sql.contains("CASE WHEN FALSE"),
7220 "an Account is never a Company:\n{}",
7221 out.sql
7222 );
7223 }
7224
7225 #[test]
7226 fn a_check_covering_every_implementor_holds_without_reading_a_row() {
7227 let mut schema = make_interface_schema_with_an_outsider();
7231 schema.types.retain(|t| t.name != "Organisation");
7232 let out = compile_and_emit_with(
7233 "SELECT Company { id } FILTER (default::Account is default::Individual)",
7234 &schema,
7235 );
7236 assert!(
7237 !out.sql.contains("assert_single"),
7238 "every Account is an Individual when it is the only implementor:\n{}",
7239 out.sql
7240 );
7241 assert!(out.sql.contains("WHERE TRUE"), "{}", out.sql);
7242 }
7243
7244 #[test]
7245 fn a_type_check_over_a_polymorphic_outsider_collapses_to_one_value() {
7246 let schema = make_interface_schema_with_an_outsider();
7251 let out = compile_and_emit_with(
7252 "SELECT Company { id } FILTER (default::Account is default::Individual)",
7253 &schema,
7254 );
7255 assert!(
7256 out.sql.contains("\"_pylon\".\"assert_single\"(ARRAY(SELECT"),
7257 "one answer per Account row, read as a single value:\n{}",
7258 out.sql
7259 );
7260 assert!(
7261 out.sql
7262 .contains("is default::Individual'' is asked once for every default::Account object"),
7263 "the error has to name the check, not assert_single:\n{}",
7264 out.sql
7265 );
7266 }
7267
7268 #[test]
7269 fn a_type_check_naming_another_type_keeps_its_whole_set_in_a_shape() {
7270 let schema = make_interface_schema_with_an_outsider();
7273 let out = compile_and_emit_with(
7274 "SELECT Company { id, each := default::Account is default::Individual }",
7275 &schema,
7276 );
7277 assert!(
7278 out.sql.contains("array_agg") && !out.sql.contains("assert_single"),
7279 "a shape keeps every answer:\n{}",
7280 out.sql
7281 );
7282 }
7283
7284 #[test]
7285 fn a_shape_writing_the_computed_out_itself_does_not_get_the_anchor() {
7286 let schema = make_schema_with_an_inherited_self_naming_computed();
7294 let ast = parse::parse("SELECT Employee { own_name := default::Person.name }").expect("parse failed");
7295 let Err(error) = ir::compile(&ast, &schema) else {
7296 panic!("an absolute path has nothing to anchor to here")
7297 };
7298 assert!(
7299 format!("{error:?}").contains("absolute paths are not valid"),
7300 "{error:?}"
7301 );
7302 }
7303
7304 #[test]
7305 fn a_default_that_compiles_nowhere_is_still_an_error() {
7306 let mut schema = make_schema();
7309 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
7310 let age = person.properties.iter_mut().find(|p| p.name == "age").unwrap();
7311 age.default_pyql = Some("sequence_next(INTROSPECT default::AgeSeq)".into());
7312 let errs = crate::validate::validate_schema_types(&schema).unwrap_err();
7313 let (_, msg, _) = errs[0].class_name_message_position();
7314 assert!(msg.contains("default"), "{msg}");
7315 }
7316
7317 #[test]
7318 fn test_comparing_a_link_with_an_object_returning_function() {
7319 let mut schema = make_schema();
7323 let company = schema
7325 .types
7326 .iter_mut()
7327 .find(|t| t.name == "Company")
7328 .expect("make_schema declares Company");
7329 company.properties.push(PropertyDescriptor {
7330 name: "id".into(),
7331 pg_type: "uuid".into(),
7332 nullable: false,
7333 default_sql: None,
7334 default_pyql: None,
7335 description: None,
7336 check_constraints: vec![],
7337 is_exclusive: true,
7338 is_pk: true,
7339 is_readonly: true,
7340 rewrites: vec![],
7341 tuple_members: None,
7342 column_type: None,
7343 });
7344 schema.functions.push(crate::schema::FunctionDescriptor {
7345 name: "current_company".into(),
7346 module: "default".into(),
7347 params: vec![],
7348 return_pg_type: "default::Company".into(),
7349 return_is_object: true,
7350 return_is_set: false,
7351 return_is_polymorphic: false,
7352 volatility: "stable".into(),
7353 body: "select Company limit 1".into(),
7354 });
7355 let out = compile_and_emit_with("SELECT Person FILTER .company = current_company()", &schema);
7356 assert!(
7357 out.sql.contains("\"company_id\"") && out.sql.contains("current_company"),
7358 "the link's key against the function's row:\n{}",
7359 out.sql
7360 );
7361 }
7362
7363 #[test]
7364 fn test_choosing_between_two_object_sets() {
7365 let out = compile_and_emit_with(
7369 "WITH a := (SELECT Person FILTER .name = $n), b := (SELECT Person) \
7370 SELECT (a IF EXISTS a ELSE b) { name }",
7371 &make_schema(),
7372 );
7373 assert!(
7374 out.sql.contains("UNION ALL"),
7375 "expected one branch per side:\n{}",
7376 out.sql
7377 );
7378 }
7379
7380 #[test]
7381 fn test_a_union_operand_written_inline() {
7382 let out = compile_and_emit_with(
7386 "SELECT ((SELECT Person FILTER .name = $n) UNION (SELECT Person)) { name }",
7387 &make_schema(),
7388 );
7389 assert!(
7390 out.sql.starts_with("WITH"),
7391 "expected the operands hoisted:\n{}",
7392 out.sql
7393 );
7394 assert!(out.sql.contains("UNION ALL"), "{}", out.sql);
7395 }
7396
7397 #[test]
7398 fn test_a_for_loop_variable_over_objects() {
7399 let out = compile_and_emit_with("FOR p IN (SELECT Person) UNION (SELECT p.name)", &make_schema());
7403 assert!(
7404 out.sql.contains("FROM \"public\".\"Person\"") && out.sql.contains("\"_for_p\".\"v\""),
7405 "the body must read the row the variable names:\n{}",
7406 out.sql
7407 );
7408 }
7409
7410 #[test]
7411 fn test_a_shape_on_a_for_loop_variable() {
7412 let out = compile_and_emit_with("FOR p IN (SELECT Person) UNION (SELECT p { name })", &make_schema());
7413 assert!(
7414 out.sql.contains("'default::Person'::text"),
7415 "a shape on the variable yields its object:\n{}",
7416 out.sql
7417 );
7418 }
7419
7420 #[test]
7421 fn test_a_for_loop_variable_over_values_is_still_a_scalar() {
7422 let out = compile_and_emit_with("FOR n IN {1, 2} UNION (SELECT n)", &make_schema());
7423 assert!(out.sql.contains("VALUES"), "{}", out.sql);
7424 }
7425
7426 #[test]
7427 fn test_exists_over_a_sub_select_on_a_relative_path() {
7428 let out = compile_and_emit_with(
7433 "SELECT Person { id } FILTER EXISTS ((SELECT .posts FILTER .title = $t))",
7434 &make_schema(),
7435 );
7436 assert!(out.sql.contains("EXISTS("), "{}", out.sql);
7437 assert!(
7438 out.sql.contains("\"Person.posts\""),
7439 "the junction join must survive into the EXISTS:\n{}",
7440 out.sql
7441 );
7442 assert!(
7443 out.sql.contains("\"t0\".\"id\""),
7444 "and it must correlate to the enclosing row:\n{}",
7445 out.sql
7446 );
7447 }
7448
7449 #[test]
7450 fn test_counting_the_rows_a_mutation_touched() {
7451 let out = compile_and_emit_with("SELECT count((DELETE Person FILTER .name = $n))", &make_schema());
7455 assert!(out.sql.starts_with("WITH"), "{}", out.sql);
7456 assert!(out.sql.contains("DELETE FROM \"public\".\"Person\""), "{}", out.sql);
7457 assert!(out.sql.contains("count(*)"), "{}", out.sql);
7458 }
7459
7460 #[test]
7461 fn test_a_conflict_target_naming_several_columns() {
7462 let out = compile_and_emit_with(
7466 "INSERT Person { name := $n } UNLESS CONFLICT ON (.name, .id) ELSE (UPDATE Person SET { name := $n })",
7467 &make_schema(),
7468 );
7469 assert!(
7470 out.sql.contains("ON CONFLICT (\"name\", \"id\")"),
7471 "expected a column list:\n{}",
7472 out.sql
7473 );
7474 }
7475
7476 #[test]
7477 fn test_a_for_loop_over_a_path() {
7478 let out = compile_and_emit_with(
7482 "WITH p := (SELECT Person LIMIT 1) FOR post IN p.posts UNION (SELECT post.title)",
7483 &make_schema(),
7484 );
7485 assert!(out.sql.contains("CROSS JOIN LATERAL"), "{}", out.sql);
7486 }
7487
7488 #[test]
7489 fn test_a_relative_sub_select_keeps_its_enclosing_object() {
7490 let out = compile_and_emit_with(
7495 "SELECT Person { ps := assert_exists((SELECT .posts { title })) }",
7496 &make_schema(),
7497 );
7498 assert!(out.sql.contains("assert_exists"), "{}", out.sql);
7499 assert!(
7500 out.sql.contains("\"Person.posts\""),
7501 "the walk must reach the junction:\n{}",
7502 out.sql
7503 );
7504 }
7505
7506 #[test]
7507 fn test_a_relative_sub_select_as_a_multi_link_value() {
7508 let out = compile_and_emit_with(
7509 "UPDATE Person FILTER .name = $n SET { posts := (SELECT .posts FILTER .title = $t) }",
7510 &make_schema(),
7511 );
7512 assert!(
7513 out.sql.contains("\"Person.posts\""),
7514 "the value must read the row's own posts:\n{}",
7515 out.sql
7516 );
7517 }
7518
7519 #[test]
7520 fn test_a_shape_written_after_a_sub_select() {
7521 let out = compile_and_emit_with(
7525 "SELECT Person { ps := (SELECT Post FILTER .title = $t) { title } }",
7526 &make_schema(),
7527 );
7528 assert!(
7529 out.sql.contains("'default::Post'::text"),
7530 "the pointer must carry the object rows:\n{}",
7531 out.sql
7532 );
7533 }
7534
7535 #[test]
7536 fn test_a_shape_after_a_sub_select_with_a_with_block() {
7537 let out = compile_and_emit_with(
7538 "SELECT Person { ps := (WITH t := $t SELECT Post FILTER .title = t) { title } }",
7539 &make_schema(),
7540 );
7541 assert!(out.sql.contains("'default::Post'::text"), "{}", out.sql);
7542 }
7543
7544 #[test]
7545 fn test_a_computed_declared_as_an_object_returning_call() {
7546 let mut schema = make_schema();
7551 schema.functions.push(crate::schema::FunctionDescriptor {
7552 name: "recent".into(),
7553 module: "default".into(),
7554 params: vec![],
7555 return_pg_type: "default::Post".into(),
7556 return_is_object: true,
7557 return_is_set: true,
7558 return_is_polymorphic: false,
7559 volatility: "stable".into(),
7560 body: "select Post".into(),
7561 });
7562 schema.types[0].computed.push(crate::schema::ComputedDescriptor {
7563 name: "recent".into(),
7564 expression: "default::recent()".into(),
7565 return_type: None,
7566 link_target: None,
7567 link_multi: false,
7568 });
7569 let out = compile_and_emit_with("SELECT Person { recent }", &schema);
7570 assert!(
7571 out.sql.contains("ARRAY(SELECT") && out.sql.contains("'default::Post'::text"),
7572 "the pointer must carry the function's rows:\n{}",
7573 out.sql
7574 );
7575 }
7576
7577 #[test]
7578 fn test_one_branch_of_an_object_if_else_is_empty() {
7579 let out = compile_and_emit_with(
7583 "WITH p := (SELECT Person LIMIT 1) SELECT p { name } IF EXISTS p ELSE {}",
7584 &make_schema(),
7585 );
7586 assert!(!out.sql.contains("UNION ALL"), "nothing to union with:\n{}", out.sql);
7587 assert!(out.sql.contains("'default::Person'::text"), "{}", out.sql);
7588 }
7589
7590 #[test]
7591 fn test_a_conditional_insert_is_guarded_by_its_condition() {
7592 let out = compile_and_emit_with("SELECT (INSERT Person { name := $n }) IF FALSE ELSE {}", &make_schema());
7597 assert!(
7598 out.sql.contains("SELECT") && out.sql.contains("WHERE"),
7599 "a guarded insert selects its row rather than listing values:\n{}",
7600 out.sql
7601 );
7602 assert!(!out.sql.contains("VALUES"), "VALUES always yields a row:\n{}", out.sql);
7603 }
7604
7605 #[test]
7606 fn test_a_conditional_update_carries_its_condition() {
7607 let out = compile_and_emit(
7612 "SELECT (UPDATE Person FILTER .name = $n SET { name := $m }) IF EXISTS (SELECT Company) ELSE {}",
7613 );
7614 let update = out.sql.find("UPDATE").expect("an update is emitted");
7615 let where_clause = out.sql[update..].find("WHERE").expect("the update is filtered");
7616 assert!(
7617 out.sql[update + where_clause..].contains("EXISTS"),
7618 "the condition narrows the rows the update touches:\n{}",
7619 out.sql
7620 );
7621 }
7622
7623 #[test]
7624 fn test_a_conditional_delete_carries_its_condition() {
7625 let out = compile_and_emit("SELECT (DELETE Person FILTER .name = $n) IF FALSE ELSE {}");
7629 let delete = out
7630 .sql
7631 .split("DELETE FROM")
7632 .nth(1)
7633 .expect("the delete should still be emitted");
7634 let where_clause = delete.split("WHERE").nth(1).expect("the delete should be filtered");
7635 assert!(
7636 where_clause.contains("FALSE") || where_clause.contains("false"),
7637 "the condition must narrow the delete itself:\n{}",
7638 out.sql
7639 );
7640 }
7641
7642 #[test]
7643 fn test_a_pointer_a_binding_declared_in_its_own_shape() {
7644 let out = compile_and_emit_with(
7648 "WITH p := (SELECT Person { co := .company { name } } LIMIT 1) SELECT p { name, co: { name } }",
7649 &make_schema(),
7650 );
7651 assert!(
7652 out.sql.contains("'default::Company'::text"),
7653 "the declared pointer must be inlined:\n{}",
7654 out.sql
7655 );
7656 }
7657
7658 #[test]
7659 fn test_a_binding_declared_pointer_read_from_a_tuple_element() {
7660 let out = compile_and_emit_with(
7661 "WITH p := (SELECT Person { co := .company { name } } LIMIT 1) SELECT (p { co: { name } }, 1)",
7662 &make_schema(),
7663 );
7664 assert!(out.sql.contains("'default::Company'::text"), "{}", out.sql);
7665 }
7666
7667 #[test]
7668 fn test_a_for_loop_variable_shaped_inside_a_tuple() {
7669 let out = compile_and_emit_with(
7673 "WITH p := (SELECT Person LIMIT 1) FOR q IN p.posts UNION (SELECT (a := q { title }, b := 1))",
7674 &make_schema(),
7675 );
7676 assert!(
7677 out.sql.contains("\"_for_q\".\"v\""),
7678 "the element must be narrowed to the loop's row:\n{}",
7679 out.sql
7680 );
7681 }
7682
7683 #[test]
7684 fn test_choosing_between_two_walks() {
7685 let out = compile_and_emit_with(
7688 "WITH p := (SELECT Person LIMIT 1), q := (SELECT p.posts LIMIT 1) IF TRUE ELSE (SELECT p.posts) \
7689 SELECT q { title }",
7690 &make_schema(),
7691 );
7692 assert!(out.sql.contains("UNION ALL"), "one branch per side:\n{}", out.sql);
7693 }
7694
7695 #[test]
7696 fn test_a_bare_coalesce_of_correlated_walks_reaching_many_objects() {
7697 let out = compile_and_emit_with(
7700 "SELECT Post { owners := .<posts[is Person] ?? .<posts[is Person] }",
7701 &make_schema(),
7702 );
7703 assert!(
7704 !out.sql.contains("COALESCE("),
7705 "not a value-by-value choice:\n{}",
7706 out.sql
7707 );
7708 assert!(
7709 out.sql.contains("ARRAY(SELECT \"r\" FROM ("),
7710 "the arms are aggregated:\n{}",
7711 out.sql
7712 );
7713 }
7714
7715 #[test]
7716 fn test_a_union_of_correlated_walks_reaching_many() {
7717 let out = compile_and_emit_with(
7722 "SELECT Post { owners := (.<posts[is Person] UNION .<posts[is Person]) { name } }",
7723 &make_schema(),
7724 );
7725 assert!(
7726 out.sql.contains("ARRAY(SELECT \"r\" FROM ("),
7727 "the arms are aggregated:\n{}",
7728 out.sql
7729 );
7730 }
7731
7732 #[test]
7733 fn test_a_coalesce_of_correlated_walks_reaching_many() {
7734 let out = compile_and_emit_with(
7737 "SELECT Post { owners := (.<posts[is Person] ?? .<posts[is Person]) { name } }",
7738 &make_schema(),
7739 );
7740 assert!(
7741 out.sql.contains("ARRAY(SELECT \"r\" FROM ("),
7742 "the arms are aggregated:\n{}",
7743 out.sql
7744 );
7745 }
7746
7747 #[test]
7748 fn test_a_union_of_correlated_walks() {
7749 let out = compile_and_emit_with(
7753 "SELECT Post { owners := (SELECT (.<posts[is Person] UNION .<posts[is Person]) { name } LIMIT 1) }",
7754 &make_schema(),
7755 );
7756 assert!(out.sql.contains("UNION ALL"), "one arm per operand:\n{}", out.sql);
7757 assert!(
7758 out.sql.contains("AS \"_u\""),
7759 "the arms are read as one set:\n{}",
7760 out.sql
7761 );
7762 }
7763
7764 #[test]
7765 fn test_a_for_loop_whose_body_updates() {
7766 let out = compile_and_emit_with(
7770 "FOR p IN (SELECT Person) UNION (UPDATE Person FILTER .id = p.id SET { name := $n })",
7771 &make_schema(),
7772 );
7773 assert!(out.sql.contains("UPDATE \"public\".\"Person\""), "{}", out.sql);
7774 assert!(
7775 out.sql.contains("FROM \"_for_p\""),
7776 "driven from the iteration:\n{}",
7777 out.sql
7778 );
7779 assert!(
7780 !out.sql.contains("LATERAL"),
7781 "DML cannot sit in a LATERAL:\n{}",
7782 out.sql
7783 );
7784 }
7785
7786 #[test]
7787 fn test_a_for_loop_body_appends_the_row_it_is_iterating() {
7788 let out = compile_and_emit(
7789 "WITH t := (SELECT Post) FOR q IN t UNION (UPDATE Person FILTER .id = $i SET { posts += q })",
7790 );
7791 assert!(
7794 out.sql.contains("\"_iter\"") && out.sql.contains("FROM \"_ids\""),
7795 "junction rows driven from the iteration:\n{}",
7796 out.sql
7797 );
7798 assert!(
7799 !out.sql.contains("LATERAL"),
7800 "DML cannot sit in a LATERAL:\n{}",
7801 out.sql
7802 );
7803 }
7804
7805 #[test]
7806 fn test_select_on_a_for_loop_variable_reads_only_its_row() {
7807 let out = compile_and_emit("FOR p IN (SELECT Person) UNION (SELECT (SELECT p) { name })");
7808 assert!(
7809 out.sql.contains("= \"_for_p\".\"v\""),
7810 "narrowed to the row the variable holds:\n{}",
7811 out.sql
7812 );
7813 }
7814
7815 #[test]
7816 fn test_a_walk_off_a_for_loop_variable_reads_the_binding() {
7817 let out = compile_and_emit("WITH ps := (SELECT Post) FOR q IN ps UNION (SELECT Person FILTER .name = q.title)");
7820 assert!(
7821 out.sql.contains("FROM \"ps\""),
7822 "the walk reads the binding:\n{}",
7823 out.sql
7824 );
7825 }
7826
7827 #[test]
7828 fn test_a_mutating_for_loop_binding_becomes_sibling_ctes() {
7829 let out = compile_and_emit(
7830 "WITH made := (FOR n IN {'a', 'b'} UNION (INSERT Post { title := n })) SELECT count(made)",
7831 );
7832 assert!(
7833 out.sql.contains("\"made__ids\" AS (\nINSERT INTO"),
7834 "the insert is its own top-level CTE:\n{}",
7835 out.sql
7836 );
7837 assert!(
7838 !out.sql.contains("LATERAL"),
7839 "DML cannot sit in a LATERAL:\n{}",
7840 out.sql
7841 );
7842 }
7843
7844 #[test]
7845 fn test_count_of_a_binding_counts_its_rows() {
7846 let out = compile_and_emit("WITH people := (SELECT Person) SELECT count(people)");
7849 assert!(out.sql.contains("(SELECT count(*) FROM \"people\")"), "{}", out.sql);
7850 }
7851
7852 #[test]
7853 fn test_dividing_integers_yields_a_float() {
7854 for (query, expected) in [
7855 ("SELECT 100 / 133", "((100)::float8 / 133)"),
7856 ("SELECT Person { a := .age / 2 }", "::float8 / 2)"),
7857 ("SELECT Person.age / 2", "::float8 / 2)"),
7858 (
7859 "WITH total := max({133}), current := 38 - 1 SELECT 100 / total * current",
7860 "(((100)::float8 / (SELECT \"v\" FROM \"total\")) * (SELECT \"v\" FROM \"current\"))",
7861 ),
7862 (
7863 "WITH p := (SELECT Person LIMIT 1), index := p.age - 1, total := max(Person.age) SELECT 100 / total * index",
7864 "((100)::float8 / (SELECT \"v\" FROM \"total\"))",
7865 ),
7866 ] {
7867 let out = compile_and_emit(query);
7868 assert!(out.sql.contains(expected), "{query}:\n{}", out.sql);
7869 }
7870 }
7871
7872 #[test]
7873 fn test_dividing_anything_but_two_integers_is_left_to_postgres() {
7874 for query in [
7875 "SELECT 100 // 133",
7876 "SELECT 100 / 1.5",
7877 "SELECT <decimal>100 / <decimal>3",
7878 "SELECT 100 / <decimal>3",
7879 ] {
7880 let out = compile_and_emit(query);
7881 assert!(!out.sql.contains("::float8 /"), "{query}:\n{}", out.sql);
7882 }
7883 }
7884
7885 #[test]
7886 fn test_a_free_object_under_a_condition_stays_an_object() {
7887 for query in [
7888 "SELECT { a := 1 } IF true ELSE {}",
7889 "SELECT {} IF true ELSE { a := 1 }",
7890 "SELECT { a := 1 } IF true ELSE {} LIMIT 1",
7891 ] {
7892 let out = compile_and_emit(query);
7893 assert!(
7894 matches!(&out.shape.root, ShapeNode::Object { type_name: None, pointers, .. } if pointers.len() == 1),
7895 "{query}: {:?}\n{}",
7896 out.shape.root,
7897 out.sql
7898 );
7899 }
7900 }
7901
7902 #[test]
7903 fn test_exists_of_a_binding_asks_for_any_row() {
7904 let out = compile_and_emit("WITH people := (SELECT Person) SELECT exists people");
7905 assert!(out.sql.contains("EXISTS(SELECT 1 FROM \"people\")"), "{}", out.sql);
7906
7907 let out = compile_and_emit("WITH names := {'a', 'b'} SELECT exists names");
7908 assert!(
7909 out.sql
7910 .contains("EXISTS(SELECT 1 FROM \"names\" WHERE \"v\" IS NOT NULL)"),
7911 "{}",
7912 out.sql
7913 );
7914 }
7915
7916 #[test]
7917 fn test_a_conditional_insert_beside_a_read_branch_unions_both() {
7918 let out = compile_and_emit(
7919 "WITH existing := (SELECT Person FILTER .name = $n LIMIT 1) \
7920 SELECT (existing IF EXISTS existing ELSE (INSERT Person { name := $n })) { name }",
7921 );
7922 assert!(out.sql.contains("UNION ALL"), "{}", out.sql);
7923 assert!(
7924 out.sql.contains("INSERT INTO") && out.sql.contains("WHERE (NOT EXISTS(SELECT 1 FROM \"existing\"))"),
7925 "the insert carries the condition itself:\n{}",
7926 out.sql
7927 );
7928 }
7929
7930 fn make_schema_with_a_plain_abstract() -> SchemaDescriptor {
7935 let mut schema = make_schema();
7936 let mut archivable = schema
7937 .types
7938 .iter()
7939 .find(|t| t.name == "Person")
7940 .expect("Person is in the test schema")
7941 .clone();
7942 archivable.name = "Archivable".into();
7943 archivable.table = "Archivable".into();
7944 archivable.abstract_ = true;
7945 archivable.materialized = false;
7946 archivable.links = vec![];
7947 archivable.multilinks = vec![];
7948 schema.types.push(archivable);
7949 let person = schema
7950 .types
7951 .iter_mut()
7952 .find(|t| t.name == "Person")
7953 .expect("just found");
7954 person.parents.push("default::Archivable".into());
7955 schema
7956 }
7957
7958 #[test]
7959 fn test_an_intersection_with_a_plain_abstract_fans_out() {
7960 let schema = make_schema_with_a_plain_abstract();
7961 let out = compile_and_emit_with("SELECT Post { who := [is default::Archivable].name }", &schema);
7962 assert!(
7963 !out.sql.contains("\"Archivable\""),
7964 "a plain abstract backs no relation:\n{}",
7965 out.sql
7966 );
7967 assert!(
7968 out.sql.contains("\"public\".\"Person\""),
7969 "read from the type that carries the column:\n{}",
7970 out.sql
7971 );
7972 }
7973
7974 fn make_schema_with_a_linking_plain_abstract() -> SchemaDescriptor {
7978 let mut schema = make_schema_with_a_plain_abstract();
7979 let person_links = schema
7980 .types
7981 .iter()
7982 .find(|t| t.name == "Person")
7983 .expect("Person is in the test schema")
7984 .links
7985 .clone();
7986 let archivable = schema
7987 .types
7988 .iter_mut()
7989 .find(|t| t.name == "Archivable")
7990 .expect("just added");
7991 archivable.links = person_links;
7992 schema
7993 }
7994
7995 #[test]
7996 fn test_a_plain_abstract_in_a_subquery_fans_out() {
7997 let schema = make_schema_with_a_plain_abstract();
7998 let out = compile_and_emit_with(
7999 "SELECT Person FILTER .id IN (SELECT default::Archivable FILTER .age > 1).id",
8000 &schema,
8001 );
8002 assert!(
8003 !out.sql.contains("\"Archivable\""),
8004 "a plain abstract backs no relation:\n{}",
8005 out.sql
8006 );
8007 }
8008
8009 #[test]
8010 fn test_a_backlink_narrowed_to_a_plain_abstract_fans_out() {
8011 let schema = make_schema_with_a_linking_plain_abstract();
8012 let out = compile_and_emit_with(
8013 "SELECT Company FILTER .<company[is default::Archivable].name = 'a'",
8014 &schema,
8015 );
8016 assert!(
8017 !out.sql.contains("\"Archivable\""),
8018 "a plain abstract backs no relation:\n{}",
8019 out.sql
8020 );
8021 }
8022
8023 fn make_interface_schema_with_a_plain_abstract() -> SchemaDescriptor {
8027 let mut schema = make_interface_schema();
8028 let mut archivable = schema
8029 .types
8030 .iter()
8031 .find(|t| t.name == "Individual")
8032 .expect("Individual is in the interface schema")
8033 .clone();
8034 archivable.name = "Archivable".into();
8035 archivable.table = "Archivable".into();
8036 archivable.abstract_ = true;
8037 archivable.materialized = false;
8038 archivable.interfaces = vec![];
8039 archivable.links = vec![];
8040 archivable.multilinks = vec![];
8041 archivable.computed = vec![];
8042 schema.types.push(archivable);
8043 for t in schema.types.iter_mut() {
8044 match t.name.as_str() {
8045 "Account" => t.materialized = true,
8047 "Individual" => t.parents.push("default::Archivable".into()),
8048 _ => {}
8049 }
8050 }
8051 schema
8052 }
8053
8054 #[test]
8055 fn test_a_splat_over_a_plain_abstract_fans_out() {
8056 let schema = make_interface_schema_with_a_plain_abstract();
8057 let out = compile_and_emit_with("SELECT Account { id, [is default::Archivable].* }", &schema);
8058 assert!(
8059 !out.sql.contains("\"Archivable\""),
8060 "a plain abstract backs no relation:\n{}",
8061 out.sql
8062 );
8063 assert!(
8064 out.sql.contains("first_name"),
8065 "the splat still reaches the mixin's own pointers:\n{}",
8066 out.sql
8067 );
8068 }
8069
8070 #[test]
8071 fn test_an_aggregate_over_a_relative_walk_runs_inside_the_subquery() {
8072 let out = compile_and_emit("SELECT Person { latest := max(.posts.title) }");
8075 assert!(out.sql.contains("(SELECT max("), "{}", out.sql);
8076 assert!(!out.sql.contains("max(ARRAY("), "{}", out.sql);
8077 }
8078
8079 fn make_schema_with_a_computed_backlink() -> SchemaDescriptor {
8082 use crate::schema::ComputedDescriptor;
8083 let mut schema = make_schema();
8084 let company = schema
8085 .types
8086 .iter_mut()
8087 .find(|t| t.name == "Company")
8088 .expect("Company is in the test schema");
8089 company.computed = vec![ComputedDescriptor {
8090 name: "staff".into(),
8091 expression: ".<company[is default::Person]".into(),
8092 return_type: None,
8093 link_target: None,
8094 link_multi: false,
8095 }];
8096 schema
8097 }
8098
8099 #[test]
8100 fn test_a_computed_backlink_reads_the_objects_it_lands_on() {
8101 let schema = make_schema_with_a_computed_backlink();
8102 let out = compile_and_emit_with("SELECT Company { staff: { name } }", &schema);
8103 assert!(
8104 out.sql.contains("\"public\".\"Person\""),
8105 "the walk reaches Person:\n{}",
8106 out.sql
8107 );
8108 }
8109
8110 #[test]
8111 fn test_a_computed_backlinks_modifiers_belong_to_what_it_lands_on() {
8112 let schema = make_schema_with_a_computed_backlink();
8115 let out = compile_and_emit_with("SELECT Company.staff { name } FILTER .name = 'ada'", &schema);
8116 assert!(out.sql.contains("'ada'"), "{}", out.sql);
8117 assert!(
8118 out.sql.contains("\"public\".\"Person\""),
8119 "filtered on the Person the walk lands on:\n{}",
8120 out.sql
8121 );
8122 }
8123
8124 #[test]
8125 fn test_an_unreserved_keyword_names_a_free_shape_field() {
8126 let out = compile_and_emit("SELECT { last := 2, first := 1, order := 3 }");
8129 assert!(out.sql.contains("\"last\""), "{}", out.sql);
8130 assert!(out.sql.contains("\"first\""), "{}", out.sql);
8131 assert!(out.sql.contains("\"order\""), "{}", out.sql);
8132 }
8133
8134 #[test]
8135 fn test_a_shaped_sub_select_over_a_type_is_an_object() {
8136 let out = compile_and_emit("SELECT Person { latest := (SELECT Post { title } LIMIT 1) }");
8139 assert!(out.sql.contains("\"title\""), "{}", out.sql);
8140 }
8141
8142 #[test]
8143 fn test_a_walk_inside_a_path_selects_shape_finds_its_row() {
8144 let out = compile_and_emit("SELECT Person.posts { mine := (SELECT Post FILTER .title = .title LIMIT 1) }");
8147 assert!(out.sql.contains("\"public\".\"Post\""), "{}", out.sql);
8148 }
8149
8150 #[test]
8151 fn test_a_walk_off_a_binding_in_a_free_select() {
8152 let out = compile_and_emit("WITH p := (SELECT Person) SELECT { n := p.company.name }");
8155 assert!(out.sql.contains("\"p\""), "{}", out.sql);
8156 assert!(out.sql.contains("\"public\".\"Company\""), "{}", out.sql);
8157 }
8158
8159 #[test]
8160 fn test_a_free_selects_filter_can_root_at_a_binding() {
8161 let out = compile_and_emit("WITH p := (SELECT Person) SELECT { n := 1 } FILTER p.company.name = 'Acme'");
8162 assert!(out.sql.contains("'Acme'"), "{}", out.sql);
8163 }
8164
8165 #[test]
8166 fn test_a_trailing_shape_on_a_field_access_select() {
8167 let out = compile_and_emit("SELECT (SELECT Person).company { name }");
8168 assert!(out.sql.contains("\"name\""), "{}", out.sql);
8169 assert!(out.sql.contains("\"public\".\"Company\""), "{}", out.sql);
8170 }
8171
8172 #[test]
8173 fn test_a_union_of_walks_names_each_branch() {
8174 let out = compile_and_emit(
8177 "WITH p := (SELECT Person FILTER .name = $a), q := (SELECT Person FILTER .name = $b), \
8178 t := (SELECT p.posts UNION q.posts) SELECT t { title }",
8179 );
8180 assert!(out.sql.contains("UNION ALL"), "{}", out.sql);
8181 assert!(out.sql.contains("\"title\""), "{}", out.sql);
8182 }
8183
8184 #[test]
8185 fn test_sibling_for_loops_each_get_their_own_iterator() {
8186 let out = compile_and_emit(
8189 "WITH a := (SELECT Post), b := (SELECT Post), \
8190 x := (FOR p IN a UNION (INSERT Person { name := p.title, age := 1 })), \
8191 y := (FOR p IN b UNION (INSERT Person { name := p.title, age := 2 })) \
8192 SELECT x",
8193 );
8194 assert!(out.sql.contains("\"_for_p\" AS"), "{}", out.sql);
8195 assert!(out.sql.contains("\"_for_p__1\" AS"), "{}", out.sql);
8196 }
8197
8198 #[test]
8199 fn test_a_nested_for_carries_the_outer_loops_key() {
8200 let out = compile_and_emit(
8203 "WITH made := (FOR p IN (SELECT Person) UNION ( \
8204 FOR q IN p.posts UNION (INSERT Company { name := q.title }) \
8205 )) SELECT count(made)",
8206 );
8207 assert!(
8208 out.sql.contains("\"_outer\""),
8209 "the inner iterator carries the bond:\n{}",
8210 out.sql
8211 );
8212 assert!(
8213 out.sql.contains("JOIN \"_for_p\" ON"),
8214 "the insert reads both loop variables:\n{}",
8215 out.sql
8216 );
8217 assert!(
8218 !out.sql.contains("LATERAL (\nINSERT"),
8219 "DML cannot sit in a LATERAL:\n{}",
8220 out.sql
8221 );
8222 }
8223
8224 #[test]
8225 fn test_a_link_value_can_be_a_walk_off_a_binding() {
8226 let mut schema = make_schema();
8229 let company = schema
8230 .types
8231 .iter_mut()
8232 .find(|t| t.name == "Company")
8233 .expect("make_schema declares Company");
8234 company.properties.push(PropertyDescriptor {
8235 name: "id".into(),
8236 pg_type: "uuid".into(),
8237 nullable: false,
8238 default_sql: None,
8239 default_pyql: None,
8240 description: None,
8241 check_constraints: vec![],
8242 is_exclusive: true,
8243 is_pk: true,
8244 is_readonly: true,
8245 rewrites: vec![],
8246 tuple_members: None,
8247 column_type: None,
8248 });
8249 let out = compile_and_emit_with(
8250 "WITH others := (SELECT Person) \
8251 SELECT (INSERT Person { name := $n, age := 1, company := (SELECT others.company LIMIT 1) })",
8252 &schema,
8253 );
8254 assert!(out.sql.contains("\"company_id\""), "{}", out.sql);
8255 }
8256
8257 #[test]
8258 fn test_a_shape_over_a_coalesce_of_walks() {
8259 let schema = make_schema_with_computed_links();
8262 let out = compile_and_emit_with("SELECT Person { x := (.published ?? .plain) { title } }", &schema);
8263 assert!(out.sql.contains("UNION ALL"), "{}", out.sql);
8264 assert!(
8265 out.sql.contains("NOT EXISTS"),
8266 "the later branch only stands in when the first is empty:\n{}",
8267 out.sql
8268 );
8269 }
8270
8271 #[test]
8272 fn test_an_aggregate_over_a_walk_onto_objects_counts_its_rows() {
8273 let out = compile_and_emit("WITH c := (SELECT Company) SELECT c { n := count(.<company[is default::Person]) }");
8277 assert!(out.sql.contains("count("), "{}", out.sql);
8278 assert!(out.sql.contains("\"public\".\"Person\""), "{}", out.sql);
8279 }
8280
8281 #[test]
8282 fn an_aggregate_over_a_computed_set_counts_inside_a_subquery() {
8283 let schema = make_schema_with_computed_links();
8289 let out = compile_and_emit_with("SELECT Person { n := count(.plain) }", &schema);
8290 assert!(
8291 !out.sql.contains("count((SELECT"),
8292 "the aggregate belongs inside the walk's subquery, not around it:\n{}",
8293 out.sql
8294 );
8295 assert!(
8296 out.sql.contains("(SELECT count(") && out.sql.contains("\"public\".\"Post\""),
8297 "it counts the rows the computed's own path lands on:\n{}",
8298 out.sql
8299 );
8300 }
8301
8302 #[test]
8305 fn an_aggregate_over_a_stored_multi_link_still_counts_its_junction_rows() {
8306 let schema = make_schema_with_computed_links();
8307 let out = compile_and_emit_with("SELECT Person { n := count(.posts) }", &schema);
8308 assert!(out.sql.contains("\"public\".\"Person.posts\""), "{}", out.sql);
8309 assert!(
8310 !out.sql.contains("JOIN \"public\".\"Post\""),
8311 "counting junction rows needs no join onto the targets:\n{}",
8312 out.sql
8313 );
8314 }
8315
8316 #[test]
8317 fn test_a_select_can_name_its_own_result() {
8318 let out = compile_and_emit("SELECT oldest := max(Person.age)");
8321 assert!(out.sql.contains("max("), "{}", out.sql);
8322 let filtered = compile_and_emit("SELECT n := Person.age FILTER n > 18");
8323 assert!(
8324 filtered.sql.contains("18"),
8325 "the alias resolves to the result:\n{}",
8326 filtered.sql
8327 );
8328 }
8329
8330 #[test]
8331 fn test_exists_sees_a_computed_pointer() {
8332 let schema = make_schema_with_computed_links();
8336 let out = compile_and_emit_with("SELECT Person FILTER EXISTS .published", &schema);
8337 assert!(out.sql.contains("EXISTS"), "{}", out.sql);
8338 assert!(
8339 out.sql.contains("\"public\".\"Post\""),
8340 "the computed's own path:\n{}",
8341 out.sql
8342 );
8343 }
8344
8345 #[test]
8346 fn test_walking_through_a_computed_field_access() {
8347 use crate::schema::ComputedDescriptor;
8348 let mut schema = make_schema_with_computed_links();
8351 let person = schema.types.iter_mut().find(|t| t.name == "Person").expect("Person");
8352 person.computed.push(ComputedDescriptor {
8353 name: "latest_title".into(),
8354 expression: "(select .posts order by .title desc limit 1).title".into(),
8355 return_type: None,
8356 link_target: None,
8357 link_multi: false,
8358 });
8359 let out = compile_and_emit_with("SELECT Person { x := .latest_title }", &schema);
8360 assert!(out.sql.contains("\"title\""), "{}", out.sql);
8361 assert!(
8362 out.sql.contains("LIMIT"),
8363 "the computed's own limit survives:\n{}",
8364 out.sql
8365 );
8366 }
8367
8368 #[test]
8369 fn test_a_computed_field_access_onto_an_object_takes_a_shape() {
8370 use crate::schema::ComputedDescriptor;
8371 let mut schema = make_schema_with_computed_links();
8374 let post = schema.types.iter_mut().find(|t| t.name == "Post").expect("Post");
8375 post.links.push(LinkDescriptor {
8376 name: "author".into(),
8377 target: "default::Person".into(),
8378 nullable: true,
8379 through: None,
8380 description: None,
8381 default_pyql: None,
8382 is_exclusive: false,
8383 is_readonly: false,
8384 rewrites: vec![],
8385 on_delete: vec![],
8386 });
8387 let person = schema.types.iter_mut().find(|t| t.name == "Person").expect("Person");
8388 person.computed.retain(|c| c.name != "looper");
8389 person.computed.push(ComputedDescriptor {
8390 name: "latest_author".into(),
8391 expression: "((select .posts order by .title desc limit 1)).author".into(),
8392 return_type: None,
8393 link_target: None,
8394 link_multi: false,
8395 });
8396 let out = compile_and_emit_with("SELECT Person { latest_author: { name } }", &schema);
8397 assert!(out.sql.contains("\"name\""), "{}", out.sql);
8398 assert!(
8399 out.sql.contains("\"title\" DESC"),
8400 "the order stays on the posts:\n{}",
8401 out.sql
8402 );
8403 assert!(
8404 out.sql.contains("LIMIT"),
8405 "the computed's own limit survives:\n{}",
8406 out.sql
8407 );
8408 let splat = compile_and_emit_with("SELECT Person { * }", &schema);
8409 assert!(
8410 !splat.sql.contains("latest_author"),
8411 "`*` leaves links out:\n{}",
8412 splat.sql
8413 );
8414 }
8415
8416 #[test]
8417 fn test_a_narrowed_pointer_can_carry_a_shape() {
8418 let out = compile_and_emit("SELECT Company { x := 1, [is default::Person].posts: { title } }");
8422 assert!(out.sql.contains("\"title\""), "{}", out.sql);
8423 assert!(out.sql.contains("\"public\".\"Post\""), "{}", out.sql);
8424 }
8425
8426 #[test]
8427 fn test_select_type_name_as_a_path_step() {
8428 let out = compile_and_emit("SELECT Person.__type__");
8429 assert!(out.sql.contains("ROW('default::Person')"), "{}", out.sql);
8430 assert!(out.sql.contains("\"Person\" AS \"t0\""), "{}", out.sql);
8431 }
8432
8433 #[test]
8434 fn test_aggregate_over_a_with_bound_object_set() {
8435 let out = compile_and_emit("WITH people := (SELECT Person) SELECT max(people.age)");
8436 assert!(out.sql.contains("\"people\" AS ("));
8437 assert!(out.sql.contains("max("));
8438 assert!(!out.sql.contains("FROM \"default\".\"Person\" AS \"t1\""));
8440 }
8441
8442 #[test]
8443 fn test_select_union_of_two_object_bindings() {
8444 let out = compile_and_emit("WITH a := (SELECT Person LIMIT 1), b := (SELECT Person) SELECT (a UNION b)");
8445 assert!(out.sql.contains("SELECT * FROM \"a\""));
8446 assert!(out.sql.contains("UNION ALL"));
8447 assert!(out.sql.contains("SELECT * FROM \"b\""));
8448 }
8449
8450 #[test]
8451 fn test_select_union_of_different_object_types_is_rejected() {
8452 let schema = make_schema();
8453 let ast = parse::parse("WITH a := (SELECT Person), b := (SELECT Company) SELECT (a UNION b)").unwrap();
8454 let Err(err) = ir::compile(&ast, &schema) else {
8455 panic!("union of unrelated types must be rejected")
8456 };
8457 assert!(format!("{err}").contains("UNION"));
8458 }
8459
8460 #[test]
8461 fn test_for_union_as_a_function_body() {
8462 let schema = make_schema();
8463 let descriptor = FunctionDescriptor {
8464 name: "recent".into(),
8465 module: "default".into(),
8466 params: vec![],
8467 return_pg_type: "default::Person".into(),
8468 return_is_object: true,
8469 return_is_set: true,
8470 return_is_polymorphic: false,
8471 volatility: "stable".into(),
8472 body: "for n in {1, 2} union (select Person filter .age = n)".into(),
8473 };
8474 let ir = ir::compile_fn_body(&descriptor, &schema).expect("function body must compile");
8475 let body = emit_fn_body(&ir);
8476 assert!(body.contains("VALUES"), "{body}");
8477 assert!(body.contains("CROSS JOIN LATERAL"), "{body}");
8478 assert!(body.contains("\"_body\".*"), "{body}");
8479 }
8480
8481 #[test]
8482 fn test_with_binding_in_a_computed_reads_the_enclosing_object() {
8483 let out = compile_and_emit("SELECT Person { n := (WITH own := .name SELECT own) }");
8484 assert!(out.sql.contains("\"name\""), "{}", out.sql);
8485 }
8486
8487 #[test]
8488 fn test_subject_path_update_names_its_own_alias() {
8489 let mut schema = make_schema();
8490 let company = schema.types.iter_mut().find(|t| t.name == "Company").unwrap();
8491 company.properties.insert(
8492 0,
8493 PropertyDescriptor {
8494 name: "id".into(),
8495 pg_type: "uuid".into(),
8496 nullable: false,
8497 default_sql: Some("uuidv7()".into()),
8498 default_pyql: None,
8499 description: None,
8500 check_constraints: vec![],
8501 is_exclusive: true,
8502 is_pk: true,
8503 is_readonly: true,
8504 rewrites: vec![],
8505 tuple_members: None,
8506 column_type: None,
8507 },
8508 );
8509 let out = compile_and_emit_with("SELECT (UPDATE Person.company SET { name := 'x' }) { name }", &schema);
8513 assert!(out.sql.contains("\"t1\".\"id\" = ANY("), "{}", out.sql);
8514 }
8515
8516 #[test]
8517 fn test_an_update_past_a_from_returns_only_its_own_columns() {
8518 let out = compile_and_emit(
8522 "SELECT { (UPDATE Person FILTER .name = 'a' SET { company := (INSERT Company { name := 'c' }) }) }",
8523 );
8524 assert!(out.sql.contains("FROM \"_nested_dml_0\""), "{}", out.sql);
8525 assert!(
8526 out.sql.contains("RETURNING \"t0\".*"),
8527 "the update names its own target:\n{}",
8528 out.sql
8529 );
8530 }
8531
8532 #[test]
8533 fn test_nested_insert_in_a_mutation_written_as_a_free_set() {
8534 let out = compile_and_emit(
8535 "SELECT { (UPDATE Person FILTER .name = 'a' SET { company := (INSERT Company { name := 'c' }) }) }",
8536 );
8537 assert!(
8538 out.sql.contains("INSERT INTO \"public\".\"Company\""),
8539 "the hoisted insert is defined, not just referenced:\n{}",
8540 out.sql
8541 );
8542 }
8543
8544 #[test]
8545 fn test_nested_insert_as_a_link_value_in_an_update() {
8546 let out = compile_and_emit(
8550 "SELECT (UPDATE Person FILTER .name = 'a' SET { company := (INSERT Company { name := 'c' }) }) { name }",
8551 );
8552 assert!(out.sql.contains("INSERT INTO \"public\".\"Company\""), "{}", out.sql);
8553 assert!(out.sql.contains("_nested_dml_0"), "{}", out.sql);
8554 assert!(
8555 out.sql.contains("FROM \"_nested_dml_0\""),
8556 "the update reads the hoisted CTE:\n{}",
8557 out.sql
8558 );
8559 }
8560
8561 #[test]
8562 fn test_nested_insert_as_a_link_value() {
8563 let out = compile_and_emit(
8564 "SELECT (INSERT Person { name := 'a', company := (INSERT Company { name := 'c' }) }) { name }",
8565 );
8566 assert!(out.sql.contains("_nested_dml_0"), "{}", out.sql);
8567 assert!(out.sql.contains("INSERT INTO \"public\".\"Company\""), "{}", out.sql);
8568 }
8569
8570 #[test]
8571 fn test_nested_insert_in_a_one_element_set_is_that_insert() {
8572 let out = compile_and_emit(
8573 "SELECT (INSERT Person { name := 'a', company := { (INSERT Company { name := 'c' }) } }) { name }",
8574 );
8575 assert!(out.sql.contains("INSERT INTO \"public\".\"Company\""), "{}", out.sql);
8576 }
8577
8578 #[test]
8579 fn test_insert_with_no_assignments_uses_default_values() {
8580 let out = compile_and_emit("SELECT (INSERT Person { name := 'a', company := (INSERT Company {}) }) { name }");
8581 assert!(out.sql.contains("DEFAULT VALUES"), "{}", out.sql);
8582 }
8583
8584 #[test]
8585 fn test_backlink_narrowed_to_an_interface_its_implementor_declares() {
8586 use crate::schema::{LinkDescriptor, TypeDescriptor};
8589 let mut schema = make_interface_schema();
8590 let individual = schema.types.iter().find(|t| t.name == "Individual").unwrap().clone();
8591 schema.types.push(TypeDescriptor {
8592 name: "Note".into(),
8593 module: "default".into(),
8594 table: "Note".into(),
8595 abstract_: false,
8596 materialized: true,
8597 description: None,
8598 parents: vec![],
8599 interfaces: vec![],
8600 bases: vec![],
8601 properties: individual.properties[..1].to_vec(),
8602 links: vec![],
8603 multilinks: vec![],
8604 computed: vec![],
8605 constraints: vec![],
8606 indexes: vec![],
8607 partition: None,
8608 vector_indexes: vec![],
8609 search_indexes: vec![],
8610 triggers: vec![],
8611 junction: false,
8612 signals: vec![],
8613 });
8614 schema
8615 .types
8616 .iter_mut()
8617 .find(|t| t.name == "Individual")
8618 .unwrap()
8619 .links
8620 .push(LinkDescriptor {
8621 name: "note".into(),
8622 target: "default::Note".into(),
8623 nullable: true,
8624 description: None,
8625 default_pyql: None,
8626 is_exclusive: false,
8627 is_readonly: false,
8628 rewrites: vec![],
8629 on_delete: vec![],
8630 through: None,
8631 });
8632 let out = compile_and_emit_with("SELECT Note FILTER EXISTS .<note[is Account]", &schema);
8633 assert!(out.sql.contains("\"Individual\""), "{}", out.sql);
8634 }
8635
8636 #[test]
8637 fn test_subject_reads_the_row_a_constraint_checks() {
8638 let schema = make_schema();
8639 let sql = ir::compile_constraint_expr("__subject__.age > 18", "default::Person", &schema)
8640 .expect("__subject__ names the row under check");
8641 assert!(sql.contains("\"age\""), "{sql}");
8642 }
8643
8644 #[test]
8645 fn test_free_select_filter_gates_the_result_and_warns() {
8646 let schema = make_schema();
8647 let ast = parse::parse("SELECT count(Person) FILTER (Person.age > 18)").unwrap();
8648 let ir = ir::compile(&ast, &schema).unwrap();
8649 let out = emit(&ir);
8650 assert!(
8651 out.sql.contains("EXISTS("),
8652 "the condition gates the count:\n{}",
8653 out.sql
8654 );
8655 assert!(
8656 ir.warnings.iter().any(|w| w.contains("FILTER clause")),
8657 "a set-valued filter warns: {:?}",
8658 ir.warnings
8659 );
8660 }
8661
8662 #[test]
8663 fn test_free_sub_select_with_modifiers_reads_inline() {
8664 let out = compile_and_emit(
8667 "SELECT Person { n := (WITH mine := .name SELECT count(Post) FILTER (Post.title = mine)) }",
8668 );
8669 assert!(out.sql.contains("_scalar_sub"), "{}", out.sql);
8670 assert!(!out.sql.contains("WITH"), "nothing is hoisted:\n{}", out.sql);
8671 }
8672
8673 #[test]
8674 fn test_correlated_with_binding_is_not_hoisted() {
8675 let out = compile_and_emit("SELECT Person { n := (WITH own := .name SELECT own) }");
8676 assert!(
8677 !out.sql.contains("\"own\" AS ("),
8678 "the binding is inlined:\n{}",
8679 out.sql
8680 );
8681 assert!(out.sql.contains("\"name\""), "{}", out.sql);
8682 }
8683
8684 #[test]
8685 fn test_array_literal_binding_is_readable_and_typed() {
8686 let out = compile_and_emit("WITH order := ['a', 'b'] SELECT std::find(order, 'b')");
8687 assert!(out.sql.contains("array_position"), "its type is known:\n{}", out.sql);
8688 assert!(
8689 out.sql.contains("AS v"),
8690 "the binding exposes a value column:\n{}",
8691 out.sql
8692 );
8693 }
8694
8695 #[test]
8696 fn test_any_over_a_single_boolean_is_that_boolean() {
8697 let out = compile_and_emit("SELECT Person FILTER std::any(.age > 18)");
8698 assert!(!out.sql.contains("bool_or"), "no aggregate in WHERE:\n{}", out.sql);
8699 assert!(out.sql.contains("> 18"), "{}", out.sql);
8700 }
8701
8702 #[test]
8703 fn test_comparing_a_value_to_a_set_is_membership() {
8704 let schema = make_schema_with_computed_links();
8705 let out = compile_and_emit_with("SELECT Person FILTER .name = Person.posts.title", &schema);
8706 assert!(out.sql.contains("= ANY("), "{}", out.sql);
8707 }
8708
8709 #[test]
8710 fn test_for_over_a_with_binding_iterates_every_row() {
8711 let out = compile_and_emit(
8712 "WITH names := (SELECT Person.name) FOR n IN names UNION (SELECT Person FILTER .name = n)",
8713 );
8714 assert!(out.sql.contains("FROM \"names\""), "{}", out.sql);
8715 assert!(!out.sql.contains("VALUES"), "the whole set is iterated:\n{}", out.sql);
8716 }
8717
8718 #[test]
8719 fn test_for_body_bindings_stay_inside_the_body() {
8720 let out = compile_and_emit(
8723 "WITH names := (SELECT Person.name) \
8724 FOR n IN names UNION (WITH matching := (SELECT Person FILTER .name = n) SELECT matching)",
8725 );
8726 let lateral = out.sql.find("CROSS JOIN LATERAL").expect("a lateral body");
8727 let binding = out.sql.find("\"matching\" AS (").expect("the body's binding");
8728 assert!(binding > lateral, "the binding sits inside the body:\n{}", out.sql);
8729 }
8730
8731 #[test]
8732 fn test_array_valued_binding_picks_the_array_overload() {
8733 let out = compile_and_emit("WITH l := <array<std::str>>$0 SELECT std::find(l, 'a')");
8736 assert!(out.sql.contains("array_position"), "{}", out.sql);
8737 assert!(!out.sql.contains("strpos"), "{}", out.sql);
8738 }
8739
8740 #[test]
8741 fn test_aggregate_over_a_path_in_a_filter_is_a_subquery() {
8742 let out = compile_and_emit("SELECT Person FILTER .age = std::max(Person.age)");
8743 assert!(
8744 out.sql.contains("(SELECT max("),
8745 "an aggregate in WHERE needs its own query:\n{}",
8746 out.sql
8747 );
8748 }
8749
8750 #[test]
8751 fn test_distinct_over_a_single_value_is_that_value() {
8752 let out = compile_and_emit("SELECT Person { n := (DISTINCT .name) }");
8753 assert!(out.sql.contains("\"name\""), "{}", out.sql);
8754 assert!(!out.sql.contains("DISTINCT \""), "{}", out.sql);
8755 }
8756
8757 #[test]
8758 fn test_narrowing_an_interface_joins_the_implementor_table() {
8759 let mut schema = make_interface_schema();
8762 let account = schema.types.iter_mut().find(|t| t.name == "Account").unwrap();
8763 account.materialized = true;
8764 let out = compile_and_emit_with("SELECT Account[is Individual].first_name", &schema);
8765 assert!(
8766 out.sql.contains("JOIN \"public\".\"Individual\""),
8767 "the implementor's own table is joined:\n{}",
8768 out.sql
8769 );
8770 assert!(out.sql.contains("\"first_name\""), "{}", out.sql);
8771 }
8772
8773 #[test]
8774 fn test_sub_select_shape_declares_names_its_filter_and_projection_read() {
8775 let out = compile_and_emit("SELECT Person { n := (SELECT .posts { t := .title } FILTER .t = 'x' LIMIT 1).t }");
8776 assert!(out.sql.contains("'x'"), "{}", out.sql);
8777 assert!(out.sql.contains("\"title\""), "{}", out.sql);
8778 }
8779
8780 #[test]
8781 fn test_assert_over_an_object_set_returns_rows() {
8782 let out = compile_and_emit("SELECT std::assert_distinct((SELECT Person))");
8783 assert!(
8784 out.sql.contains("\"assert_distinct\"(ARRAY(SELECT \"id\""),
8785 "{}",
8786 out.sql
8787 );
8788 assert!(out.sql.contains("= ANY("), "{}", out.sql);
8789 assert!(out.sql.contains("\"Person\" AS \"t1\""), "{}", out.sql);
8790 assert!(!out.sql.contains("unnest("), "{}", out.sql);
8791 }
8792
8793 #[test]
8794 fn test_assert_over_a_for_union_reads_its_rows() {
8795 let out =
8796 compile_and_emit("SELECT std::assert_distinct((FOR n IN {1, 2} UNION (SELECT Person FILTER .age = n)))");
8797 assert!(out.sql.contains("CROSS JOIN LATERAL"), "{}", out.sql);
8798 assert!(out.sql.contains("= ANY("), "{}", out.sql);
8799 }
8800
8801 #[test]
8802 fn test_declared_computed_with_binding_reads_its_own_object() {
8803 use crate::schema::ComputedDescriptor;
8806 let schema = make_schema();
8807 let cd = ComputedDescriptor {
8808 name: "shouted".into(),
8809 expression: "(with own := .name select std::str_upper(own))".into(),
8810 return_type: None,
8811 link_target: None,
8812 link_multi: false,
8813 };
8814 let ir = ir::compile_computed_in_type(&cd, "default::Person", &schema)
8815 .expect("a computed may read the object it is declared on");
8816 assert!(ir.is_some());
8817 }
8818
8819 #[test]
8820 fn test_sub_select_modifiers_scope_to_its_own_subject() {
8821 let mut schema = make_schema();
8824 let post = schema
8825 .types
8826 .iter_mut()
8827 .find(|t| t.name == "Post")
8828 .expect("Post is in the test schema");
8829 post.links.push(LinkDescriptor {
8830 name: "author".into(),
8831 target: "default::Person".into(),
8832 nullable: true,
8833 description: None,
8834 default_pyql: None,
8835 is_exclusive: false,
8836 is_readonly: false,
8837 rewrites: vec![],
8838 on_delete: vec![],
8839 through: None,
8840 });
8841 let out = compile_and_emit_with(
8842 "SELECT Person { a := (SELECT .posts FILTER .title = 'x' LIMIT 1).author.name }",
8843 &schema,
8844 );
8845 assert!(out.sql.contains("'x'"), "{}", out.sql);
8846 }
8847
8848 #[test]
8849 fn test_free_select_set_literal() {
8850 let schema = make_schema();
8851 let ast = parse::parse("SELECT {1, 2, 3}").unwrap();
8852 let ir = ir::compile(&ast, &schema).unwrap();
8853 let out = emit(&ir);
8854 assert_eq!(out.sql.matches("UNION ALL").count(), 2);
8856 assert!(out.sql.contains("1 AS v"));
8857 assert!(out.sql.contains("2 AS v"));
8858 assert!(out.sql.contains("3 AS v"));
8859 assert!(out.sql.contains("ROW(v) AS result"));
8860 assert!(matches!(out.shape.root, crate::query::ShapeNode::Scalar { .. }));
8861 }
8862
8863 #[test]
8864 fn test_free_select_free_object() {
8865 let schema = make_schema();
8866 let ast = parse::parse("SELECT { foo := 'bar', n := 42 }").unwrap();
8867 let ir = ir::compile(&ast, &schema).unwrap();
8868 let out = emit(&ir);
8869 assert!(out.sql.contains("'bar'"));
8870 assert!(out.sql.contains("42"));
8871 assert!(out.sql.contains("AS result"));
8872 let crate::query::ShapeNode::Object {
8874 pointers, type_name, ..
8875 } = &out.shape.root
8876 else {
8877 panic!("expected Object shape")
8878 };
8879 assert!(type_name.is_none());
8880 assert_eq!(pointers.len(), 2);
8881 assert!(matches!(&pointers[0], crate::query::ShapeNode::Scalar { name, position: 0 } if name == "foo"));
8882 assert!(matches!(&pointers[1], crate::query::ShapeNode::Scalar { name, position: 1 } if name == "n"));
8883 }
8884
8885 #[test]
8886 fn test_free_select_object_with_enum_field_casts_to_text_and_tags_shape() {
8887 let mut schema = make_schema();
8895 schema.enums.push(crate::schema::EnumDescriptor {
8896 name: "Gender".into(),
8897 module: "default".into(),
8898 members: vec!["Male".into(), "Female".into()],
8899 });
8900 let out = compile_and_emit_with("select { gender := default::Gender.Male }", &schema);
8901 assert!(
8908 out.sql.contains("'Male'::\"public\".\"Gender\""),
8909 "expected the enum literal, got:\n{}",
8910 out.sql
8911 );
8912 assert!(
8913 out.sql.contains("ROW(\"_f0\"::text) AS result"),
8914 "expected the ROW composite to cast the enum field to text, got:\n{}",
8915 out.sql
8916 );
8917 let crate::query::ShapeNode::Object { pointers, .. } = &out.shape.root else {
8918 panic!("expected Object shape")
8919 };
8920 assert_eq!(pointers.len(), 1);
8921 assert!(
8922 matches!(
8923 &pointers[0],
8924 crate::query::ShapeNode::Enum { name, position: 0, enum_type }
8925 if name == "gender" && enum_type == "public::Gender"
8926 ),
8927 "expected Enum-tagged shape, got: {:?}",
8928 pointers[0]
8929 );
8930 }
8931
8932 #[test]
8933 fn test_free_select_bare_enum_literal_casts_to_text_inside_row() {
8934 let mut schema = make_schema();
8938 schema.enums.push(crate::schema::EnumDescriptor {
8939 name: "Gender".into(),
8940 module: "default".into(),
8941 members: vec!["Male".into(), "Female".into()],
8942 });
8943 let out = compile_and_emit_with("select default::Gender.Male", &schema);
8944 assert!(
8945 out.sql.contains("ROW(v::text) AS result"),
8946 "expected ROW(v::text), got:\n{}",
8947 out.sql
8948 );
8949 assert!(
8950 matches!(
8951 &out.shape.root,
8952 crate::query::ShapeNode::Enum { enum_type, .. } if enum_type == "public::Gender"
8953 ),
8954 "expected Enum-tagged shape, got: {:?}",
8955 out.shape.root
8956 );
8957 }
8958
8959 #[test]
8962 fn test_for_update_defaults_to_blocking() {
8963 let out = compile_and_emit("SELECT Person FOR UPDATE");
8964 assert!(out.sql.trim_end().ends_with("FOR UPDATE"), "got:\n{}", out.sql);
8965 }
8966
8967 #[test]
8968 fn test_for_update_skip_locked() {
8969 let out = compile_and_emit("SELECT Person FOR UPDATE SKIP LOCKED");
8970 assert!(
8971 out.sql.trim_end().ends_with("FOR UPDATE SKIP LOCKED"),
8972 "got:\n{}",
8973 out.sql
8974 );
8975 }
8976
8977 #[test]
8978 fn test_for_update_nowait() {
8979 let out = compile_and_emit("SELECT Person FOR UPDATE NOWAIT");
8980 assert!(out.sql.trim_end().ends_with("FOR UPDATE NOWAIT"), "got:\n{}", out.sql);
8981 }
8982
8983 #[test]
8984 fn test_for_no_key_update_skip_locked() {
8985 let out = compile_and_emit("SELECT Person FOR NO KEY UPDATE SKIP LOCKED");
8986 assert!(
8987 out.sql.trim_end().ends_with("FOR NO KEY UPDATE SKIP LOCKED"),
8988 "got:\n{}",
8989 out.sql
8990 );
8991 }
8992
8993 #[test]
8994 fn test_for_share() {
8995 let out = compile_and_emit("SELECT Person FOR SHARE");
8996 assert!(out.sql.trim_end().ends_with("FOR SHARE"), "got:\n{}", out.sql);
8997 }
8998
8999 #[test]
9000 fn test_for_key_share_nowait() {
9001 let out = compile_and_emit("SELECT Person FOR KEY SHARE NOWAIT");
9002 assert!(
9003 out.sql.trim_end().ends_with("FOR KEY SHARE NOWAIT"),
9004 "got:\n{}",
9005 out.sql
9006 );
9007 }
9008
9009 #[test]
9010 fn test_for_update_comes_after_order_by_limit_offset_in_emitted_sql() {
9011 let out = compile_and_emit("SELECT Person { name } ORDER BY .name OFFSET 1 LIMIT 5 FOR UPDATE SKIP LOCKED");
9014 let order_pos = out.sql.find("ORDER BY").unwrap();
9015 let offset_pos = out.sql.find("OFFSET").unwrap();
9016 let limit_pos = out.sql.find("LIMIT").unwrap();
9017 let for_pos = out.sql.find("FOR UPDATE").unwrap();
9018 assert!(
9019 order_pos < offset_pos && offset_pos < limit_pos && limit_pos < for_pos,
9020 "got:\n{}",
9021 out.sql
9022 );
9023 }
9024
9025 #[test]
9026 fn test_for_update_combined_with_distinct_is_rejected() {
9027 let ast = parse::parse("SELECT DISTINCT Person FOR UPDATE").expect("parse failed");
9028 let err = ir::compile(&ast, &make_schema())
9029 .err()
9030 .expect("expected a compile error");
9031 assert!(err.to_string().contains("DISTINCT"), "unexpected: {err}");
9032 }
9033
9034 #[test]
9035 fn test_for_update_combined_with_select_over_insert_is_rejected() {
9036 let ast = parse::parse("SELECT (INSERT Person { name := 'Alice' }) { name } FOR UPDATE").expect("parse failed");
9037 let err = ir::compile(&ast, &make_schema())
9038 .err()
9039 .expect("expected a compile error");
9040 assert!(err.to_string().contains("INSERT"), "unexpected: {err}");
9041 }
9042
9043 #[test]
9044 fn test_for_update_on_an_interface_type_is_rejected() {
9045 fn id_prop() -> PropertyDescriptor {
9046 PropertyDescriptor {
9047 name: "id".into(),
9048 pg_type: "uuid".into(),
9049 nullable: false,
9050 default_sql: Some("uuidv7()".into()),
9051 default_pyql: None,
9052 description: None,
9053 check_constraints: vec![],
9054 is_exclusive: true,
9055 is_pk: true,
9056 is_readonly: true,
9057 rewrites: vec![],
9058 tuple_members: None,
9059 column_type: None,
9060 }
9061 }
9062 let schema = SchemaDescriptor {
9063 types: vec![
9064 TypeDescriptor {
9065 name: "Account".into(),
9066 module: "default".into(),
9067 table: "Account".into(),
9068 abstract_: true,
9069 materialized: true,
9070 description: None,
9071 parents: vec![],
9072 interfaces: vec![],
9073 bases: vec![],
9074 properties: vec![id_prop()],
9075 links: vec![],
9076 multilinks: vec![],
9077 computed: vec![],
9078 constraints: vec![],
9079 indexes: vec![],
9080 partition: None,
9081 vector_indexes: vec![],
9082 search_indexes: vec![],
9083 triggers: vec![],
9084 junction: false,
9085 signals: vec![],
9086 },
9087 TypeDescriptor {
9088 name: "Individual".into(),
9089 module: "default".into(),
9090 table: "Individual".into(),
9091 abstract_: false,
9092 materialized: true,
9093 description: None,
9094 parents: vec![],
9095 interfaces: vec!["default::Account".into()],
9096 bases: vec![],
9097 properties: vec![id_prop()],
9098 links: vec![],
9099 multilinks: vec![],
9100 computed: vec![],
9101 constraints: vec![],
9102 indexes: vec![],
9103 partition: None,
9104 vector_indexes: vec![],
9105 search_indexes: vec![],
9106 triggers: vec![],
9107 junction: false,
9108 signals: vec![],
9109 },
9110 ],
9111 scalars: vec![],
9112 enums: vec![],
9113 named_tuples: vec![],
9114 globals: vec![],
9115 functions: vec![],
9116 aliases: vec![],
9117 channels: vec![],
9118 ..Default::default()
9119 };
9120 let ast = parse::parse("SELECT Account FOR UPDATE").expect("parse failed");
9121 let err = ir::compile(&ast, &schema).err().expect("expected a compile error");
9122 assert!(err.to_string().contains("interface"), "unexpected: {err}");
9123 }
9124
9125 #[test]
9126 fn test_schema_select_distinct_emits_distinct_keyword() {
9127 let out = compile_and_emit("SELECT DISTINCT Person { name }");
9132 assert!(out.sql.contains("DISTINCT"), "expected DISTINCT in SQL:\n{}", out.sql);
9133 }
9134
9135 #[test]
9136 fn test_schema_select_detached_compiles_as_ordinary_select() {
9137 let out = compile_and_emit("SELECT DETACHED Person { name }");
9141 assert!(
9142 out.sql.contains("\"name\""),
9143 "expected name column in SQL:\n{}",
9144 out.sql
9145 );
9146 }
9147
9148 #[test]
9149 fn test_detached_subquery_correlates_against_the_outer_row() {
9150 let out = compile_and_emit(
9157 "SELECT Person { name } FILTER NOT EXISTS (\
9158 SELECT DETACHED Person FILTER .name = Person.name AND .age > Person.age)",
9159 );
9160 assert!(
9161 out.sql.contains("\"t1\".\"name\" = \"t0\".\"name\""),
9162 "the inner row should be compared against the outer one:\n{}",
9163 out.sql
9164 );
9165 assert!(
9166 !out.sql.contains("\"t1\".\"name\" = \"t1\".\"name\""),
9167 "self-comparison means the correlation was lost:\n{}",
9168 out.sql
9169 );
9170 }
9171
9172 #[test]
9173 fn test_an_assert_on_a_pointer_checks_its_rows() {
9174 let out = compile_and_emit("SELECT Person { name, p := assert_exists(.posts { title }) }");
9178 assert!(
9179 out.sql
9180 .contains(r#"cardinality("_pylon"."assert_exists"("_a"."v"::text[]))"#),
9181 "the assert should read the aggregated rows:\n{}",
9182 out.sql
9183 );
9184 assert_eq!(
9185 out.sql.matches("array_agg(ROW(").count(),
9186 1,
9187 "the pointer's subquery should be evaluated once, not once per use:\n{}",
9188 out.sql
9189 );
9190 }
9191
9192 #[test]
9193 fn test_an_assert_on_a_scalar_stays_an_ordinary_call() {
9194 let out = compile_and_emit("SELECT Person { n := assert_exists(.name) }");
9197 assert!(
9198 !out.sql.contains(r#""_a"."v""#),
9199 "a scalar assert should not be wrapped as a pointer check:\n{}",
9200 out.sql
9201 );
9202 }
9203
9204 #[test]
9205 fn test_a_backlink_tail_walks_past_the_first_step() {
9206 let out = compile_and_emit("SELECT Post FILTER any(.<posts[is Person].posts.title = $t)");
9211 assert!(
9212 out.sql.contains(r#""public"."Person.posts""#),
9213 "the tail should walk the multi-link's junction:\n{}",
9214 out.sql
9215 );
9216 }
9217
9218 #[test]
9219 fn test_an_intersection_narrows_the_rest_of_a_multilink_walk() {
9220 let out = compile_and_emit("SELECT Person FILTER any(.posts[is Post].title = $t)");
9224 assert!(
9225 out.sql.contains(r#""public"."Post""#),
9226 "the walk should continue from the narrowed type:\n{}",
9227 out.sql
9228 );
9229 }
9230
9231 #[test]
9232 fn test_a_union_of_bindings_emits_selects_not_bare_names() {
9233 let out = compile_and_emit_with(
9237 "WITH a := (INSERT Post { title := $t1 }), b := (INSERT Post { title := $t2 }) \
9238 SELECT (INSERT Person { name := $n, posts := (a UNION b) })",
9239 &make_schema(),
9240 );
9241 assert!(
9242 out.sql.contains(r#"(SELECT "_s"."id" FROM "a" AS "_s")"#),
9243 "each union operand should be a select:\n{}",
9244 out.sql
9245 );
9246 }
9247
9248 #[test]
9249 fn test_a_bare_narrowing_reads_as_a_type_check_and_as_an_object() {
9250 for query in [
9254 "SELECT Person FILTER EXISTS [is Person]",
9255 "SELECT Person { p := [is Person] { name } }",
9256 ] {
9257 let ast = parse::parse(query).unwrap_or_else(|e| panic!("{query}: {e}"));
9258 assert!(ir::compile(&ast, &make_schema()).is_ok(), "{query} should compile");
9259 }
9260 }
9261
9262 #[test]
9263 fn test_a_multilink_takes_a_set_wrapped_in_a_select() {
9264 let out = compile_and_emit_with(
9269 "WITH a := (INSERT Post { title := $t1 }), b := (INSERT Post { title := $t2 }) \
9270 SELECT (INSERT Person { name := $n, posts := (SELECT { a, b }) })",
9271 &make_schema(),
9272 );
9273 assert!(
9274 out.sql.contains(r#""public"."Person.posts""#),
9275 "both targets should reach the junction:\n{}",
9276 out.sql
9277 );
9278 }
9279
9280 #[test]
9281 fn test_an_aggregate_over_a_subselected_path() {
9282 let out = compile_and_emit("SELECT array_agg((SELECT Person.name))");
9288 assert!(
9289 out.sql.contains("array_agg") && out.sql.contains(r#""public"."Person""#),
9290 "the aggregate should wrap the walk's column:\n{}",
9291 out.sql
9292 );
9293 }
9294
9295 #[test]
9296 fn test_a_distinct_aggregate_argument_reaches_the_aggregate() {
9297 for query in [
9303 "SELECT array_agg(DISTINCT Person.name)",
9304 "SELECT array_agg((SELECT DISTINCT Person.name))",
9305 ] {
9306 let out = compile_and_emit(query);
9307 assert!(
9308 out.sql.contains("array_agg(DISTINCT "),
9309 "{query} should deduplicate inside the aggregate:\n{}",
9310 out.sql
9311 );
9312 }
9313 }
9314
9315 #[test]
9316 fn test_is_not_negates_the_type_check() {
9317 let positive = compile_and_emit("SELECT Person FILTER Person IS Person");
9321 let negative = compile_and_emit("SELECT Person FILTER Person IS NOT Person");
9322 assert!(
9323 negative.sql.contains("NOT") && !positive.sql.contains("NOT"),
9324 "the negation should reach the SQL:\npositive:\n{}\nnegative:\n{}",
9325 positive.sql,
9326 negative.sql
9327 );
9328 }
9329
9330 #[test]
9331 fn test_a_cast_takes_a_prefix_operator_as_its_operand() {
9332 for query in ["SELECT <bool>EXISTS (SELECT Person LIMIT 1)", "SELECT <int64>-1"] {
9336 let ast = parse::parse(query).unwrap_or_else(|e| panic!("{query} should parse: {e}"));
9337 assert!(ir::compile(&ast, &make_schema()).is_ok(), "{query} should compile");
9338 }
9339 }
9340
9341 #[test]
9342 fn test_a_bare_multilink_reads_as_a_set() {
9343 let out =
9348 compile_and_emit("WITH p := (SELECT DETACHED Post LIMIT 1) SELECT Person { name } FILTER p IN .posts");
9349 assert!(
9350 out.sql.contains(r#""public"."Person.posts""#),
9351 "the membership test should read the link's junction:\n{}",
9352 out.sql
9353 );
9354 }
9355
9356 #[test]
9357 fn test_a_select_reads_its_own_shape_computed_in_its_clauses() {
9358 let out = compile_and_emit("SELECT Person { n := .name } ORDER BY .n ASC");
9362 assert!(
9363 out.sql.contains("ORDER BY"),
9364 "the order by should compile against the declared pointer:\n{}",
9365 out.sql
9366 );
9367 }
9368
9369 #[test]
9370 fn test_a_declared_pointer_can_be_walked_through() {
9371 let out = compile_and_emit_with(
9375 "WITH p := (SELECT Person { boss := .company } LIMIT 1) SELECT p { name, boss_name := .boss.name }",
9376 &make_schema(),
9377 );
9378 assert!(
9379 out.sql.contains(r#""public"."Company""#),
9380 "the walk should reach the declared pointer's own target table:\n{}",
9381 out.sql
9382 );
9383 }
9384
9385 #[test]
9386 fn test_a_replaced_subject_shape_stays_in_scope() {
9387 let out = compile_and_emit("SELECT Person { c := (SELECT .company { n := .name }) { n } }");
9392 assert!(
9393 out.sql.contains(r#"'default::Company'::text"#) && out.sql.contains(r#""t2"."name""#),
9394 "`n` should resolve to the company's own column:\n{}",
9395 out.sql
9396 );
9397 }
9398
9399 #[test]
9400 fn test_a_sibling_shape_pointer_is_not_in_scope() {
9401 let schema = make_schema();
9404 let ast = parse::parse("SELECT Person { n := .name, copy := .n }").unwrap();
9405 assert!(ir::compile(&ast, &schema).is_err());
9406 }
9407
9408 #[test]
9409 fn test_shape_over_a_with_binding_reads_the_binding() {
9410 let out = compile_and_emit("WITH c := (SELECT Company LIMIT 1) SELECT Person { name, employer := c { name } }");
9415 assert!(
9416 out.sql.contains("\"c\""),
9417 "the pointer should read the binding's CTE:\n{}",
9418 out.sql
9419 );
9420 }
9421
9422 #[test]
9423 fn test_shape_over_a_bare_type_name_is_still_rejected() {
9424 let schema = make_schema();
9427 let ast = parse::parse("SELECT Person { name, other := Company { name } }").unwrap();
9428 assert!(ir::compile(&ast, &schema).is_err());
9429 }
9430
9431 #[test]
9432 fn test_nested_shape_reads_a_prefix_bound_by_the_outer_select() {
9433 let out = compile_and_emit("SELECT Person { name, posts: { title, who := Person.name } }");
9438 assert!(
9439 out.sql.contains(r#""t0"."name""#),
9440 "the nested computed should read the outer Person alias:\n{}",
9441 out.sql
9442 );
9443 assert!(
9444 !out.sql.contains(r#"FROM "public"."Person" "t2""#),
9445 "the prefix should not open a second Person source:\n{}",
9446 out.sql
9447 );
9448 }
9449
9450 #[test]
9451 fn test_prefix_naming_a_type_out_of_scope_is_rejected() {
9452 let schema = make_schema();
9455 let ast = parse::parse("SELECT Person { name, who := Company.name }").unwrap();
9456 assert!(ir::compile(&ast, &schema).is_err());
9457 }
9458
9459 #[test]
9460 fn test_free_select_tuple() {
9461 let schema = make_schema();
9462 let ast = parse::parse("SELECT (1, 2)").unwrap();
9463 let ir = ir::compile(&ast, &schema).unwrap();
9464 let out = emit(&ir);
9465 assert!(out.sql.contains("1"));
9466 assert!(out.sql.contains("2"));
9467 assert!(out.sql.contains("AS result"));
9468 assert!(matches!(out.shape.root, crate::query::ShapeNode::Tuple { .. }));
9469 }
9470
9471 #[test]
9472 fn test_free_select_scalar_literal() {
9473 let schema = make_schema();
9474 let ast = parse::parse("SELECT 'hello'").unwrap();
9475 let ir = ir::compile(&ast, &schema).unwrap();
9476 let out = emit(&ir);
9477 assert!(out.sql.contains("SELECT 'hello' AS v"));
9478 assert!(out.sql.contains("ROW(v) AS result"));
9479 assert!(matches!(out.shape.root, crate::query::ShapeNode::Scalar { .. }));
9480 }
9481
9482 #[test]
9483 fn test_float_literal_casts_to_float8() {
9484 let out = compile_and_emit("SELECT 1.0");
9491 assert!(
9492 out.sql.contains("(1.0::float8)"),
9493 "expected explicit float8 cast, got:\n{}",
9494 out.sql
9495 );
9496 }
9497
9498 #[test]
9499 fn test_free_select_array_literal() {
9500 let schema = make_schema();
9501 let ast = parse::parse("SELECT [1, 2, 3]").unwrap();
9502 let ir = ir::compile(&ast, &schema).unwrap();
9503 let out = emit(&ir);
9504 assert!(out.sql.contains("SELECT ARRAY[1, 2, 3] AS v"), "{}", out.sql);
9505 assert!(out.sql.contains("SELECT v AS result, v FROM"), "{}", out.sql);
9506 assert!(matches!(out.shape.root, crate::query::ShapeNode::RawScalar));
9507 }
9508
9509 #[test]
9510 fn test_select_scalars() {
9511 let out = compile_and_emit("SELECT Person { name, age }");
9512 assert!(out.sql.contains("'default::Person'::text"));
9513 assert!(out.sql.contains("\"name\"::text"));
9514 assert!(out.sql.contains("\"age\"::int8"));
9515 assert!(out.sql.contains("FROM \"public\".\"Person\""));
9516 assert!(out.sql.contains(") AS result"));
9517 }
9518
9519 #[test]
9520 fn test_select_filter_param() {
9521 let out = compile_and_emit("SELECT Person { name } FILTER .name = $name");
9522 assert!(out.sql.contains("WHERE"));
9523 assert!(out.sql.contains("$1"));
9524 }
9525
9526 #[test]
9527 fn test_filter_in_set_literal_compiles_to_any_array() {
9528 let out = compile_and_emit("SELECT Person { name } FILTER .name IN {'Carol'}");
9529 assert!(out.sql.contains("= ANY(ARRAY['Carol'])"), "got:\n{}", out.sql);
9530 }
9531
9532 #[test]
9533 fn test_filter_in_multi_element_set_literal_compiles_to_any_array() {
9534 let out = compile_and_emit("SELECT Person { name } FILTER .name IN {'Carol', 'Bob'}");
9535 assert!(out.sql.contains("= ANY(ARRAY['Carol', 'Bob'])"), "got:\n{}", out.sql);
9536 }
9537
9538 #[test]
9545 fn test_filter_in_array_unpack_passes_the_array_straight_to_any() {
9546 let out = compile_and_emit("SELECT Person { name } FILTER .name IN std::array_unpack(<array<str>>$names)");
9547 assert!(
9548 !out.sql.contains("ANY(unnest("),
9549 "unnest inside ANY is invalid in WHERE:\n{}",
9550 out.sql
9551 );
9552 assert!(out.sql.contains("= ANY("), "got:\n{}", out.sql);
9553 }
9554
9555 #[test]
9556 fn test_filter_not_in_array_unpack_passes_the_array_straight_to_all() {
9557 let out = compile_and_emit("SELECT Person { name } FILTER .name NOT IN std::array_unpack(<array<str>>$names)");
9558 assert!(
9559 !out.sql.contains("ALL(unnest("),
9560 "unnest inside ALL is invalid in WHERE:\n{}",
9561 out.sql
9562 );
9563 assert!(out.sql.contains("<> ALL("), "got:\n{}", out.sql);
9564 }
9565
9566 #[test]
9571 fn test_all_over_unpacked_array_aggregates_the_array_not_the_multilink() {
9572 let out = compile_and_emit(
9573 "SELECT Person { name } FILTER all(std::array_unpack(<array<str>>$titles) IN .posts.title)",
9574 );
9575 assert!(out.sql.contains("bool_and("), "got:\n{}", out.sql);
9576 assert!(out.sql.contains("_unnested"), "got:\n{}", out.sql);
9577 }
9578
9579 #[test]
9583 fn test_any_over_a_multilink_against_an_unpacked_array_still_folds_into_any() {
9584 let out = compile_and_emit(
9585 "SELECT Person { name } FILTER any(.posts.title IN std::array_unpack(<array<str>>$titles))",
9586 );
9587 assert!(!out.sql.contains("unnest("), "got:\n{}", out.sql);
9588 assert!(out.sql.contains("= ANY("), "got:\n{}", out.sql);
9589 }
9590
9591 #[test]
9592 fn test_any_over_unpacked_array_aggregates_the_array_not_the_multilink() {
9593 let out = compile_and_emit(
9594 "SELECT Person { name } FILTER any(std::array_unpack(<array<str>>$titles) IN .posts.title)",
9595 );
9596 assert!(out.sql.contains("bool_or("), "got:\n{}", out.sql);
9597 assert!(out.sql.contains("_unnested"), "got:\n{}", out.sql);
9598 }
9599
9600 fn schema_with_array_property() -> SchemaDescriptor {
9602 let mut schema = make_schema();
9603 schema.types[0].properties.push(crate::schema::PropertyDescriptor {
9604 name: "perms".into(),
9605 pg_type: "text[]".into(),
9606 nullable: true,
9607 default_sql: None,
9608 default_pyql: None,
9609 description: None,
9610 check_constraints: vec![],
9611 is_exclusive: false,
9612 is_pk: false,
9613 is_readonly: false,
9614 rewrites: vec![],
9615 tuple_members: None,
9616 column_type: None,
9617 });
9618 schema
9619 }
9620
9621 #[test]
9627 fn test_aggregate_over_an_unpacked_walk_unnests_in_a_row_source() {
9628 let out = compile_and_emit_with(
9629 "WITH teams := (SELECT Person { perms } FILTER .age > 1) SELECT std::array_agg(std::array_unpack(teams.perms))",
9630 &schema_with_array_property(),
9631 );
9632 assert!(!out.sql.contains("array_agg(unnest("), "got:\n{}", out.sql);
9633 assert!(
9634 out.sql.contains(
9635 r#"coalesce(array_agg("_s"."v"), '{}') FROM unnest(ARRAY(SELECT unnest("t1"."perms") FROM "teams" AS "t1"))"#
9636 ),
9637 "got:\n{}",
9638 out.sql
9639 );
9640 }
9641
9642 #[test]
9643 fn test_aggregate_over_an_unpacked_type_rooted_path_unnests_in_a_row_source() {
9644 let out = compile_and_emit_with(
9645 "SELECT std::count(std::array_unpack(Person.perms))",
9646 &schema_with_array_property(),
9647 );
9648 assert!(!out.sql.contains("count(unnest("), "got:\n{}", out.sql);
9649 assert!(
9650 out.sql
9651 .contains(r#"FROM unnest(ARRAY(SELECT unnest("t0"."perms") FROM "public"."Person" AS "t0"))"#),
9652 "got:\n{}",
9653 out.sql
9654 );
9655 }
9656
9657 #[test]
9660 fn test_aggregate_over_an_unpacked_parameter_unnests_the_array_itself() {
9661 let out = compile_and_emit("SELECT std::array_agg(std::array_unpack(<array<str>>$names))");
9662 assert!(!out.sql.contains("array_agg(unnest("), "got:\n{}", out.sql);
9663 assert!(
9664 out.sql.contains(r#"FROM unnest(($1)::text[]) AS "_s"("v")"#),
9665 "got:\n{}",
9666 out.sql
9667 );
9668 }
9669
9670 #[test]
9671 fn test_aggregate_over_an_unpacked_property_unnests_that_row_s_array() {
9672 let out = compile_and_emit_with(
9673 "SELECT Person { n := std::array_agg(std::array_unpack(.perms)) }",
9674 &schema_with_array_property(),
9675 );
9676 assert!(!out.sql.contains("array_agg(unnest("), "got:\n{}", out.sql);
9677 assert!(
9678 out.sql.contains(r#"FROM unnest("t0"."perms") AS "_s"("v")"#),
9679 "got:\n{}",
9680 out.sql
9681 );
9682 }
9683
9684 #[test]
9687 fn test_array_agg_over_nothing_is_an_empty_array_not_null() {
9688 for query in [
9689 "SELECT std::array_agg(Person.name)",
9690 "SELECT std::array_agg((SELECT Person).name)",
9691 ] {
9692 let out = compile_and_emit(query);
9693 assert!(
9694 out.sql.contains("coalesce(") && out.sql.contains("'{}'"),
9695 "{query} got:\n{}",
9696 out.sql
9697 );
9698 }
9699 }
9700
9701 #[test]
9705 fn test_an_array_agg_binding_stays_an_array_through_its_coalesce() {
9706 let out = compile_and_emit(
9707 "WITH ids := std::array_agg(Person.id) SELECT Person { name } FILTER std::contains(ids, .id)",
9708 );
9709 assert!(
9710 out.sql.contains("@> ARRAY["),
9711 "expected the array overload, got:\n{}",
9712 out.sql
9713 );
9714 assert!(
9715 !out.sql.contains("strpos("),
9716 "the string overload must not win, got:\n{}",
9717 out.sql
9718 );
9719 }
9720
9721 #[test]
9724 fn test_aggregate_over_a_plain_walk_keeps_its_row_source() {
9725 let out = compile_and_emit("SELECT std::array_agg(Person.name)");
9726 assert!(
9727 out.sql.contains(r#"array_agg("t0"."name")"#) && out.sql.contains(r#"FROM "public"."Person""#),
9728 "got:\n{}",
9729 out.sql
9730 );
9731 }
9732
9733 #[test]
9736 fn test_the_cte_order_check_catches_a_forward_reference() {
9737 let bad = "WITH \"b\" AS (\n SELECT * FROM \"a\"\n),\n\"a\" AS (\n SELECT 1\n)\nSELECT 1";
9738 assert!(
9739 super::forward_cte_reference(bad).is_some(),
9740 "expected a forward reference"
9741 );
9742 let good = "WITH \"a\" AS (\n SELECT 1\n),\n\"b\" AS (\n SELECT * FROM \"a\"\n)\nSELECT 1";
9743 assert_eq!(super::forward_cte_reference(good), None, "ordered CTEs must pass");
9744 }
9745
9746 #[test]
9750 fn test_a_cast_unpacked_array_still_folds_into_any() {
9751 let out = compile_and_emit("SELECT Person { name } FILTER .name IN <str>std::array_unpack(<array<str>>$names)");
9752 assert!(
9753 !out.sql.contains("unnest("),
9754 "the unpack must fold into ANY, got:\n{}",
9755 out.sql
9756 );
9757 assert!(out.sql.contains("= ANY("), "got:\n{}", out.sql);
9758 }
9759
9760 #[test]
9763 fn test_array_unpack_outside_in_still_unnests() {
9764 let out = compile_and_emit("SELECT std::array_unpack(<array<str>>$names)");
9765 assert!(out.sql.contains("unnest("), "got:\n{}", out.sql);
9766 }
9767
9768 #[test]
9769 fn test_filter_not_in_set_literal_compiles_to_all_array() {
9770 let out = compile_and_emit("SELECT Person { name } FILTER .name NOT IN {'Carol'}");
9771 assert!(out.sql.contains("<> ALL(ARRAY['Carol'])"), "got:\n{}", out.sql);
9772 }
9773
9774 #[test]
9775 fn test_deep_splat_fetches_single_link_target_properties_not_just_id() {
9776 let out = compile_and_emit("SELECT Person { ** }");
9781 assert!(
9782 out.sql.contains("\"name\""),
9783 "expected Company.name pulled in via .company's ** expansion, got:\n{}",
9784 out.sql
9785 );
9786 }
9787
9788 #[test]
9789 fn test_deep_splat_fetches_multilink_target_properties_not_just_id() {
9790 let out = compile_and_emit("SELECT Person { ** }");
9791 assert!(
9792 out.sql.contains("\"title\""),
9793 "expected Post.title pulled in via .posts' ** expansion, got:\n{}",
9794 out.sql
9795 );
9796 }
9797
9798 #[test]
9799 fn test_schema_type_cast_select() {
9800 let out = compile_and_emit("SELECT <default::Person><uuid>'019ef1bb-0d42-7a9f-8f6b-b38d028a49ba'");
9801 assert!(out.sql.contains("FROM \"public\".\"Person\""));
9802 assert!(out.sql.contains("WHERE"));
9803 assert!(out.sql.contains("'019ef1bb-0d42-7a9f-8f6b-b38d028a49ba'"));
9804 }
9805
9806 #[test]
9807 fn test_select_single_link() {
9808 let out = compile_and_emit("SELECT Person { name, company { name } }");
9809 assert!(out.sql.contains("'default::Company'::text"));
9810 assert!(out.sql.contains("FROM \"public\".\"Company\""));
9811 assert!(out.sql.contains("\"company_id\" = "));
9813 }
9814
9815 #[test]
9816 fn test_select_multi_link() {
9817 let out = compile_and_emit("SELECT Person { name, posts { title } }");
9818 assert!(out.sql.contains("array_agg(ROW("));
9819 assert!(out.sql.contains("ARRAY[]::record[]"));
9820 assert!(out.sql.contains("'default::Post'::text"));
9821 assert!(out.sql.contains("\"Person.posts\""));
9822 }
9823
9824 fn compile_err(query: &str) -> String {
9825 let schema = make_schema();
9826 let ast = parse::parse(query).expect("parse failed");
9827 match ir::compile(&ast, &schema) {
9828 Ok(_) => panic!("expected a compile error"),
9829 Err(e) => format!("{e}"),
9830 }
9831 }
9832
9833 #[test]
9834 fn test_multi_link_limit_cuts_rows_before_aggregating() {
9835 let out = compile_and_emit("SELECT Person { posts: { title } filter .title = 'x' limit 1 }");
9836 assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
9839 assert!(out.sql.contains("AS \"__lim\""), "{}", out.sql);
9840 let agg = out.sql.find("array_agg").unwrap();
9841 let limit = out.sql.find("LIMIT 1").unwrap();
9842 assert!(agg < limit, "{}", out.sql);
9843 assert!(out.sql.contains("\"t1\".\"title\"::text AS \"c1\""), "{}", out.sql);
9844 }
9845
9846 #[test]
9847 fn test_multi_link_order_by_with_limit_orders_the_aggregate_too() {
9848 let out = compile_and_emit("SELECT Person { posts: { title } order by .title desc offset 1 limit 2 }");
9849 assert!(out.sql.contains("\"t1\".\"title\" AS \"s0\""), "{}", out.sql);
9852 assert!(
9853 out.sql.contains("ORDER BY \"__lim\".\"s0\" DESC NULLS LAST"),
9854 "{}",
9855 out.sql
9856 );
9857 assert!(out.sql.contains("OFFSET 1"), "{}", out.sql);
9858 assert!(out.sql.contains("LIMIT 2"), "{}", out.sql);
9859 }
9860
9861 #[test]
9862 fn test_multi_link_without_limit_keeps_the_flat_aggregate() {
9863 let out = compile_and_emit("SELECT Person { posts: { title } filter .title = 'x' }");
9864 assert!(!out.sql.contains("__lim"), "{}", out.sql);
9865 assert!(out.sql.contains("array_agg(ROW("), "{}", out.sql);
9866 }
9867
9868 #[test]
9869 fn test_sub_select_as_computed_pointer_is_a_link_pointer() {
9870 let out = compile_and_emit("SELECT Person { recent := (select .posts filter .title = 'x' limit 1) { title } }");
9871 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
9872 panic!()
9873 };
9874 let ShapeNode::Object {
9877 name,
9878 pointers: elem_pointers,
9879 ..
9880 } = &pointers[2]
9881 else {
9882 panic!("{:?}", pointers[2])
9883 };
9884 assert_eq!(name, "recent");
9885 assert!(matches!(&elem_pointers[1], ShapeNode::Scalar { name, .. } if name == "title"));
9886 assert!(out.sql.contains("\"jt\".source = \"t0\".id"), "{}", out.sql);
9887 assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
9888 }
9889
9890 #[test]
9891 fn test_sub_select_shape_inside_the_parens_is_equivalent() {
9892 let outer = compile_and_emit("SELECT Person { recent := (select .posts limit 1) { title } }");
9893 let inner = compile_and_emit("SELECT Person { recent := (select .posts { title } limit 1) }");
9894 assert_eq!(outer.sql, inner.sql);
9895 }
9896
9897 #[test]
9898 fn test_sub_select_field_access_is_a_correlated_scalar_subquery() {
9899 let out = compile_and_emit("SELECT Person { t := (select .posts filter .title = 'x' limit 1).title }");
9900 assert!(out.sql.contains("(\"t1\".\"id\" = \"t0\".\"id\")"), "{}", out.sql);
9903 assert!(out.sql.contains("\"t2\".\"title\""), "{}", out.sql);
9904 assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
9905 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
9906 panic!()
9907 };
9908 assert!(matches!(&pointers[2], ShapeNode::Scalar { name, .. } if name == "t"));
9909 }
9910
9911 #[test]
9912 fn test_sub_select_field_access_over_a_single_link() {
9913 let out = compile_and_emit("SELECT Person { c := (select .company).name }");
9914 assert!(out.sql.contains("\"t1\".\"company_id\" = \"t2\".\"id\""), "{}", out.sql);
9915 assert!(out.sql.contains("(\"t1\".\"id\" = \"t0\".\"id\")"), "{}", out.sql);
9916 }
9917
9918 #[test]
9919 fn test_sub_select_field_access_in_a_filter() {
9920 let out = compile_and_emit("SELECT Person { name } filter (select .posts limit 1).title = 'x'");
9921 assert!(out.sql.contains("WHERE ((SELECT \"t2\".\"title\""), "{}", out.sql);
9922 assert!(out.sql.contains("LIMIT 1) = 'x')"), "{}", out.sql);
9923 }
9924
9925 #[test]
9926 fn test_sub_select_over_an_absolute_path_is_not_correlated() {
9927 let out = compile_and_emit("SELECT Person { n := (select Company.name limit 1) }");
9928 assert!(out.sql.contains("FROM \"public\".\"Company\""), "{}", out.sql);
9929 assert!(!out.sql.contains("= \"t0\".\"id\""), "{}", out.sql);
9930 }
9931
9932 #[test]
9933 fn test_backlink_sub_select_carries_its_modifiers() {
9934 let out = compile_and_emit("SELECT Post { authors := (select .<posts[is Person] { name } limit 2) }");
9935 assert!(out.sql.contains("LIMIT 2"), "{}", out.sql);
9936 assert!(out.sql.contains("AS \"__lim\""), "{}", out.sql);
9937 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
9938 panic!()
9939 };
9940 assert!(matches!(&pointers[1], ShapeNode::Array { name, .. } if name == "authors"));
9941 }
9942
9943 #[test]
9944 fn test_with_bound_scalar_is_typed_for_overload_resolution() {
9945 let out = compile_and_emit("WITH xs := (select Person.name) SELECT contains(xs, 'a')");
9948 assert!(
9949 out.sql.contains("strpos((SELECT \"v\" FROM \"xs\"), 'a')"),
9950 "{}",
9951 out.sql
9952 );
9953 }
9954
9955 fn make_schema_with_computed_links() -> SchemaDescriptor {
9958 use crate::schema::ComputedDescriptor;
9959 let mut schema = make_schema();
9960 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
9961 person.computed = vec![
9962 ComputedDescriptor {
9963 name: "published".into(),
9964 expression: "(select .posts filter .title != '')".into(),
9965 return_type: None,
9966 link_target: None,
9967 link_multi: false,
9968 },
9969 ComputedDescriptor {
9970 name: "plain".into(),
9971 expression: ".posts".into(),
9972 return_type: None,
9973 link_target: None,
9974 link_multi: false,
9975 },
9976 ComputedDescriptor {
9977 name: "featured".into(),
9978 expression: "(select .published filter .title != 'draft')".into(),
9979 return_type: None,
9980 link_target: None,
9981 link_multi: false,
9982 },
9983 ComputedDescriptor {
9984 name: "looper".into(),
9985 expression: "(select .looper)".into(),
9986 return_type: None,
9987 link_target: None,
9988 link_multi: false,
9989 },
9990 ComputedDescriptor {
9991 name: "capped".into(),
9992 expression: "(select .posts limit 1)".into(),
9993 return_type: None,
9994 link_target: None,
9995 link_multi: false,
9996 },
9997 ];
9998 schema
9999 }
10000
10001 fn make_schema_with_object_fn_computed() -> SchemaDescriptor {
10004 use crate::schema::{ComputedDescriptor, FunctionDescriptor, FunctionParamDescriptor};
10005 let mut schema = make_schema();
10006 schema.functions = vec![FunctionDescriptor {
10007 name: "latest".into(),
10008 module: "default".into(),
10009 params: vec![FunctionParamDescriptor {
10010 name: "owner".into(),
10011 pg_type: "uuid".into(),
10012 }],
10013 return_pg_type: "default::Post".into(),
10014 return_is_object: true,
10015 return_is_set: true,
10016 return_is_polymorphic: false,
10017 volatility: "stable".into(),
10018 body: String::new(),
10019 }];
10020 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
10021 person.computed = vec![
10022 ComputedDescriptor {
10023 name: "recent".into(),
10024 expression: "latest(.id)".into(),
10025 return_type: None,
10026 link_target: None,
10027 link_multi: false,
10028 },
10029 ComputedDescriptor {
10030 name: "vetted".into(),
10031 expression: "(select latest(.id) filter .title != '')".into(),
10032 return_type: None,
10033 link_target: None,
10034 link_multi: false,
10035 },
10036 ];
10037 schema
10038 }
10039
10040 #[test]
10041 fn test_path_traverses_through_a_function_backed_computed() {
10042 let schema = make_schema_with_object_fn_computed();
10046 let out = compile_and_emit_with("SELECT Person { t := .recent.title }", &schema);
10047 assert!(
10048 out.sql
10049 .contains("CROSS JOIN LATERAL \"public\".\"latest\"(\"t1\".\"id\") AS \"t2\""),
10050 "{}",
10051 out.sql
10052 );
10053 assert!(out.sql.contains("ARRAY(SELECT \"t2\".\"title\""), "{}", out.sql);
10055 }
10056
10057 #[test]
10058 fn test_sub_select_over_a_function_backed_computed() {
10059 let schema = make_schema_with_object_fn_computed();
10062 let out = compile_and_emit_with(
10063 "SELECT Person { t := (select .recent filter .title = 'x' limit 1).title }",
10064 &schema,
10065 );
10066 assert!(
10067 out.sql.contains("CROSS JOIN LATERAL \"public\".\"latest\""),
10068 "{}",
10069 out.sql
10070 );
10071 assert!(out.sql.contains("\"t2\".\"title\" = 'x'"), "{}", out.sql);
10072 assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
10073 assert!(!out.sql.contains("ARRAY("), "{}", out.sql);
10074 }
10075
10076 #[test]
10077 fn test_function_backed_computed_carries_its_own_filter() {
10078 let schema = make_schema_with_object_fn_computed();
10079 let out = compile_and_emit_with(
10080 "SELECT Person { t := (select .vetted filter .title = 'x' limit 1).title }",
10081 &schema,
10082 );
10083 assert!(
10084 out.sql
10085 .contains("(\"t2\".\"title\" = 'x') AND (\"t2\".\"title\" <> '')"),
10086 "{}",
10087 out.sql
10088 );
10089 }
10090
10091 #[test]
10092 fn test_function_backed_computed_checks_its_argument_count() {
10093 use crate::schema::ComputedDescriptor;
10094 let mut schema = make_schema_with_object_fn_computed();
10095 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
10096 person.computed = vec![ComputedDescriptor {
10097 name: "recent".into(),
10098 expression: "latest()".into(),
10099 return_type: None,
10100 link_target: None,
10101 link_multi: false,
10102 }];
10103 let ast = parse::parse("SELECT Person { t := .recent.title }").unwrap();
10104 let err = match ir::compile(&ast, &schema) {
10105 Ok(_) => panic!("expected a compile error"),
10106 Err(e) => format!("{e}"),
10107 };
10108 assert!(err.contains("expects 1 argument(s), got 0"), "{err}");
10109 }
10110
10111 #[test]
10112 fn test_path_traverses_through_a_computed_pointer() {
10113 let schema = make_schema_with_computed_links();
10116 let out = compile_and_emit_with("SELECT Person { t := .published.title }", &schema);
10117 assert!(out.sql.contains("\"public\".\"Person.posts\""), "{}", out.sql);
10118 assert!(
10119 out.sql
10120 .contains("(\"t1\".\"id\" = \"t0\".\"id\") AND (\"t2\".\"title\" <> '')"),
10121 "{}",
10122 out.sql
10123 );
10124 }
10125
10126 #[test]
10127 fn test_sub_select_over_a_computed_pointer_projects_a_property() {
10128 let schema = make_schema_with_computed_links();
10131 let out = compile_and_emit_with(
10132 "SELECT Person { t := (select .published filter .title = 'a' limit 1).title }",
10133 &schema,
10134 );
10135 assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
10136 assert!(out.sql.contains("\"t2\".\"title\" = 'a'"), "{}", out.sql);
10138 assert!(out.sql.contains("\"t2\".\"title\" <> ''"), "{}", out.sql);
10139 }
10140
10141 #[test]
10142 fn test_chained_computed_pointers_each_contribute_their_filter() {
10143 let schema = make_schema_with_computed_links();
10144 let out = compile_and_emit_with("SELECT Person { t := .featured.title }", &schema);
10145 assert_eq!(out.sql.matches("\"public\".\"Person.posts\"").count(), 1, "{}", out.sql);
10146 assert!(out.sql.contains("\"t2\".\"title\" <> 'draft'"), "{}", out.sql);
10147 assert!(out.sql.contains("\"t2\".\"title\" <> ''"), "{}", out.sql);
10148 }
10149
10150 #[test]
10151 fn test_computed_pointer_naming_a_bare_link_is_traversable() {
10152 let schema = make_schema_with_computed_links();
10153 let out = compile_and_emit_with("SELECT Person { t := .plain.title }", &schema);
10154 assert!(out.sql.contains("ARRAY(SELECT \"t2\".\"title\""), "{}", out.sql);
10155 }
10156
10157 #[test]
10158 fn test_a_cycle_of_computed_pointers_is_reported_not_hung_on() {
10159 let schema = make_schema_with_computed_links();
10160 let ast = parse::parse("SELECT Person { t := .looper.title }").unwrap();
10161 let err = match ir::compile(&ast, &schema) {
10162 Ok(_) => panic!("expected a compile error"),
10163 Err(e) => format!("{e}"),
10164 };
10165 assert!(err.contains("expands into itself"), "{err}");
10166 }
10167
10168 #[test]
10169 fn test_a_cycle_of_computed_pointers_read_as_a_value_is_reported_not_hung_on() {
10170 let schema = make_schema_with_computed_links();
10173 for query in ["SELECT Person { looper }", "SELECT Person { * }"] {
10174 let ast = parse::parse(query).unwrap();
10175 let err = match ir::compile(&ast, &schema) {
10176 Ok(_) => panic!("expected a compile error for {query}"),
10177 Err(e) => format!("{e}"),
10178 };
10179 assert!(err.contains("expands into itself"), "{query}: {err}");
10180 }
10181 }
10182
10183 #[test]
10184 fn test_traversing_through_a_limited_computed_uses_a_lateral() {
10185 let schema = make_schema_with_computed_links();
10189 let out = compile_and_emit_with("SELECT Person { t := .capped.title }", &schema);
10190 assert!(out.sql.contains("JOIN LATERAL ("), "{}", out.sql);
10191 assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
10192 assert!(!out.sql.contains("ARRAY(SELECT"), "{}", out.sql);
10193 }
10194
10195 #[test]
10196 fn test_backlink_from_a_type_whose_interface_the_link_targets() {
10197 use crate::schema::{LinkDescriptor, TypeDescriptor};
10198 let mut schema = make_interface_schema();
10199 let individual = schema.types.iter().find(|t| t.name == "Individual").unwrap().clone();
10200 schema.types.push(TypeDescriptor {
10201 name: "Note".into(),
10202 module: "default".into(),
10203 table: "Note".into(),
10204 abstract_: false,
10205 materialized: true,
10206 description: None,
10207 parents: vec![],
10208 interfaces: vec![],
10209 bases: vec![],
10210 properties: individual.properties[..1].to_vec(),
10211 links: vec![LinkDescriptor {
10213 name: "owner".into(),
10214 target: "default::Account".into(),
10215 nullable: true,
10216 through: None,
10217 description: None,
10218 default_pyql: None,
10219 is_exclusive: false,
10220 is_readonly: false,
10221 rewrites: vec![],
10222 on_delete: vec![],
10223 }],
10224 multilinks: vec![],
10225 computed: vec![],
10226 constraints: vec![],
10227 indexes: vec![],
10228 partition: None,
10229 vector_indexes: vec![],
10230 search_indexes: vec![],
10231 triggers: vec![],
10232 junction: false,
10233 signals: vec![],
10234 });
10235 let out = compile_and_emit_with("SELECT Individual { notes := .<owner[is Note] }", &schema);
10238 assert!(out.sql.contains("\"public\".\"Note\""), "{}", out.sql);
10239 assert!(out.sql.contains("\"owner_id\" = \"t0\".id"), "{}", out.sql);
10240 }
10241
10242 #[test]
10243 fn test_with_block_in_expression_position_hoists_its_bindings() {
10244 let out = compile_and_emit("SELECT Person { t := (with c := (select Company.name limit 1) select c) }");
10247 assert!(out.sql.starts_with("WITH\n\"c\" AS ("), "{}", out.sql);
10248 assert!(out.sql.contains("(SELECT \"v\" FROM \"c\")"), "{}", out.sql);
10249 }
10250
10251 #[test]
10252 fn test_hoisted_binding_is_usable_in_the_expression_around_it() {
10253 let out =
10254 compile_and_emit("SELECT Person { t := (with c := (select Company.name limit 1) select .name ++ c) }");
10255 assert!(
10256 out.sql.contains("(\"t0\".\"name\" || (SELECT \"v\" FROM \"c\"))"),
10257 "{}",
10258 out.sql
10259 );
10260 }
10261
10262 #[test]
10263 fn test_hoisted_binding_joins_the_statements_own_with_clause() {
10264 let out = compile_and_emit(
10265 "WITH top := (select Person.name limit 1) \
10266 SELECT Person { t := (with c := (select Company.name limit 1) select c) }",
10267 );
10268 let top = out.sql.find("\"top\" AS (").expect("top-level binding");
10269 let inner = out.sql.find("\"c\" AS (").expect("hoisted binding");
10270 assert!(top < inner, "the statement's own bindings come first:\n{}", out.sql);
10271 }
10272
10273 #[test]
10274 fn test_a_binding_named_after_a_keyword_is_usable() {
10275 let out = compile_and_emit("WITH order := (select Person.name) SELECT Person { name } FILTER .name = order");
10280 assert!(out.sql.contains("(SELECT \"v\" FROM \"order\")"), "{}", out.sql);
10281 }
10282
10283 #[test]
10284 fn test_computed_declared_on_an_interface_is_visible_from_an_implementor() {
10285 use crate::schema::ComputedDescriptor;
10286 let mut schema = make_interface_schema();
10287 let account = schema.types.iter_mut().find(|t| t.name == "Account").unwrap();
10288 account.computed = vec![ComputedDescriptor {
10289 name: "tier".into(),
10290 expression: "'gold'".into(),
10291 return_type: None,
10292 link_target: None,
10293 link_multi: false,
10294 }];
10295 let individual = schema.types.iter_mut().find(|t| t.name == "Individual").unwrap();
10296 individual.computed = vec![];
10297 for q in ["SELECT Individual { tier }", "SELECT Individual { t := .tier }"] {
10300 let out = compile_and_emit_with(q, &schema);
10301 assert!(out.sql.contains("'gold'"), "{q}: {}", out.sql);
10302 }
10303 }
10304
10305 #[test]
10306 fn test_computed_may_carry_trailing_modifiers_without_a_select() {
10307 use crate::schema::ComputedDescriptor;
10308 let mut schema = make_schema();
10309 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
10310 person.computed = vec![ComputedDescriptor {
10311 name: "recent".into(),
10312 expression: ".posts order by .title desc limit 5".into(),
10313 return_type: None,
10314 link_target: None,
10315 link_multi: false,
10316 }];
10317 let out = compile_and_emit_with("SELECT Person { recent { title } }", &schema);
10320 assert!(out.sql.contains("LIMIT 5"), "{}", out.sql);
10321 assert!(out.sql.contains("ORDER BY \"t1\".\"title\" DESC"), "{}", out.sql);
10322 }
10323
10324 #[test]
10325 fn test_computed_may_lead_with_a_bare_select() {
10326 use crate::schema::ComputedDescriptor;
10327 let mut schema = make_schema();
10328 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
10329 person.computed = vec![ComputedDescriptor {
10330 name: "recent".into(),
10331 expression: "select .posts order by .title desc limit 5".into(),
10332 return_type: None,
10333 link_target: None,
10334 link_multi: false,
10335 }];
10336 let out = compile_and_emit_with("SELECT Person { recent { title } }", &schema);
10337 assert!(out.sql.contains("LIMIT 5"), "{}", out.sql);
10338 }
10339
10340 #[test]
10341 fn test_sub_select_with_a_shape_may_project_a_property_off_it() {
10342 let out = compile_and_emit("SELECT Person { t := (select .posts { title } limit 1).title }");
10343 assert!(out.sql.contains("\"t2\".\"title\""), "{}", out.sql);
10344 assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
10345 }
10346
10347 #[test]
10348 fn test_sub_statement_error_names_what_it_got() {
10349 let err = compile_err("SELECT Person { t := (insert Company { name := 'a' }) }");
10350 assert!(err.contains("an insert cannot stand in for a value"), "{err}");
10351 }
10352
10353 #[test]
10354 fn test_with_bound_scalar_path_select_emits_a_real_select() {
10355 let out = compile_and_emit("WITH xs := (select Person.name) SELECT Person { name }");
10358 assert!(
10359 out.sql
10360 .contains("SELECT ROW(v) AS result, v FROM (SELECT \"t0\".\"name\" AS v"),
10361 "{}",
10362 out.sql
10363 );
10364 }
10365
10366 #[test]
10367 fn test_with_bound_scalar_path_select_is_typed_as_its_result() {
10368 let out = compile_and_emit("WITH xs := (select Person.name) SELECT Person { name } FILTER .name IN xs");
10371 assert!(out.sql.contains("ANY((SELECT \"v\" FROM \"xs\"))"), "{}", out.sql);
10372 }
10373
10374 #[test]
10375 fn test_distinct_survives_a_with_binding() {
10376 let out = compile_and_emit("WITH xs := (select distinct Person.name) SELECT Person { name }");
10377 assert!(out.sql.contains("SELECT DISTINCT \"t0\".\"name\" AS v"), "{}", out.sql);
10378 }
10379
10380 #[test]
10381 fn test_for_over_a_derived_set_iterates_every_row() {
10382 let out = compile_and_emit("FOR x IN (select Person.name) UNION (SELECT Person { name } FILTER .name = x)");
10385 assert!(!out.sql.contains("VALUES"), "{}", out.sql);
10386 assert!(out.sql.contains(") AS \"_for_x\""), "{}", out.sql);
10387 assert!(out.sql.contains("CROSS JOIN LATERAL"), "{}", out.sql);
10388 assert!(out.sql.contains("\"t1\".\"name\" = \"_for_x\".\"v\""), "{}", out.sql);
10389 }
10390
10391 #[test]
10392 fn test_for_insert_over_a_derived_set_uses_it_as_the_cte() {
10393 let out = compile_and_emit("FOR x IN (select Person.name) UNION (INSERT Company { name := x })");
10394 assert!(out.sql.starts_with("WITH \"_for_x\" AS ("), "{}", out.sql);
10395 assert!(
10396 out.sql.contains("SELECT \"_for_x\".\"v\" FROM \"_for_x\""),
10397 "{}",
10398 out.sql
10399 );
10400 }
10401
10402 #[test]
10403 fn test_for_over_objects_binds_their_id() {
10404 let out = compile_and_emit("FOR p IN (select Person) UNION (INSERT Company { name := <str>p })");
10405 assert!(out.sql.contains("SELECT \"id\" AS v FROM ("), "{}", out.sql);
10406 }
10407
10408 #[test]
10409 fn test_for_over_a_non_select_statement_is_rejected() {
10410 let ast = parse::parse("FOR x IN (INSERT Company { name := 'a' }) UNION (SELECT Person { name })").unwrap();
10411 let schema = make_schema();
10412 let err = match ir::compile(&ast, &schema) {
10413 Ok(_) => panic!("expected a compile error"),
10414 Err(e) => format!("{e}"),
10415 };
10416 assert!(err.contains("only a select can be iterated"), "{err}");
10417 }
10418
10419 #[test]
10420 fn test_multi_link_path_outside_a_comparison_is_an_array() {
10421 let out = compile_and_emit("SELECT Person { t := .posts.title }");
10424 assert!(out.sql.contains("ARRAY(SELECT \"t2\".\"title\""), "{}", out.sql);
10425 assert!(out.sql.contains("(\"t1\".\"id\" = \"t0\".\"id\")"), "{}", out.sql);
10426 }
10427
10428 #[test]
10429 fn test_multi_link_path_inside_a_comparison_is_still_exists() {
10430 let out = compile_and_emit("SELECT Person { name } filter .posts.title = 'x'");
10431 assert!(out.sql.contains("WHERE EXISTS("), "{}", out.sql);
10432 assert!(!out.sql.contains("ARRAY("), "{}", out.sql);
10433 }
10434
10435 #[test]
10436 fn test_path_traversal_deeper_than_two_steps() {
10437 let schema = make_schema_with_through();
10438 let out = compile_and_emit_with("SELECT Person { t := .friends.friends.name }", &schema);
10439 assert_eq!(out.sql.matches("\"public\".\"PersonFriend\"").count(), 2, "{}", out.sql);
10441 assert!(out.sql.contains("ARRAY(SELECT \"t4\".\"name\""), "{}", out.sql);
10442 }
10443
10444 #[test]
10445 fn test_coalesce_over_backlinked_values_is_a_set_choice() {
10446 let out = compile_and_emit("SELECT Post ORDER BY max(.<posts[is Person].age ?? .<posts[is Person].age)");
10449 assert!(!out.sql.contains("EXISTS"), "{}", out.sql);
10450 assert!(!out.sql.contains("COALESCE("), "{}", out.sql);
10451 assert!(out.sql.contains("CASE WHEN (cardinality("), "{}", out.sql);
10452 assert!(
10453 out.sql
10454 .contains("(SELECT coalesce(max(\"_s\".\"v\"), NULL) FROM unnest("),
10455 "{}",
10456 out.sql
10457 );
10458 }
10459
10460 #[test]
10461 fn test_coalesce_over_multilink_values_is_a_set_choice() {
10462 let out = compile_and_emit("SELECT Person { t := .posts.title ?? .posts.title }");
10463 assert!(!out.sql.contains("EXISTS"), "{}", out.sql);
10464 assert!(out.sql.contains("CASE WHEN (cardinality("), "{}", out.sql);
10465 }
10466
10467 #[test]
10468 fn test_coalesce_of_a_set_with_a_single_value_falls_back_to_it() {
10469 let out = compile_and_emit("SELECT Post { a := .<posts[is Person].age ?? 0 }");
10470 assert!(out.sql.contains("ELSE array_remove(ARRAY[0], NULL) END"), "{}", out.sql);
10471 }
10472
10473 #[test]
10474 fn test_traversal_after_a_backlink() {
10475 let out = compile_and_emit("SELECT Post { t := .<posts[is Person].name }");
10476 assert!(out.sql.contains("ARRAY(SELECT \"t2\".\"name\""), "{}", out.sql);
10477 assert!(out.sql.contains("\"t3\".\"target\" = \"t1\".\"id\""), "{}", out.sql);
10478 }
10479
10480 #[test]
10481 fn test_computed_pointer_on_a_linked_type_is_reachable() {
10482 use crate::schema::ComputedDescriptor;
10483 let mut schema = make_schema();
10484 let company = schema.types.iter_mut().find(|t| t.name == "Company").unwrap();
10485 company.computed = vec![ComputedDescriptor {
10486 name: "shout".into(),
10487 expression: ".name ++ '!'".into(),
10488 return_type: Some("text".into()),
10489 link_target: None,
10490 link_multi: false,
10491 }];
10492 let out = compile_and_emit_with("SELECT Person { t := .company.shout }", &schema);
10496 assert!(out.sql.contains("(\"t2\".\"name\" || '!')"), "{}", out.sql);
10497 }
10498
10499 #[test]
10500 fn test_computed_pointer_behind_a_type_intersection() {
10501 let schema = make_interface_schema();
10502 let out = compile_and_emit_with("SELECT Account { n := [is Individual].full_name }", &schema);
10503 assert!(out.sql.contains("upper(\"t1\".\"first_name\")"), "{}", out.sql);
10504 assert!(out.sql.contains("FROM \"public\".\"Individual\""), "{}", out.sql);
10505 assert!(out.sql.contains("(\"t1\".\"id\" = \"t0\".\"id\")"), "{}", out.sql);
10506 }
10507
10508 #[test]
10509 fn test_computed_pointer_cannot_be_traversed_through() {
10510 use crate::schema::ComputedDescriptor;
10511 let mut schema = make_schema();
10512 let company = schema.types.iter_mut().find(|t| t.name == "Company").unwrap();
10513 company.computed = vec![ComputedDescriptor {
10514 name: "shout".into(),
10515 expression: ".name ++ '!'".into(),
10516 return_type: Some("text".into()),
10517 link_target: None,
10518 link_multi: false,
10519 }];
10520 let ast = parse::parse("SELECT Person { t := .company.shout.nope }").unwrap();
10521 let err = match ir::compile(&ast, &schema) {
10522 Ok(_) => panic!("expected a compile error"),
10523 Err(e) => format!("{e}"),
10524 };
10525 assert!(err.contains("is a computed pointer"), "{err}");
10526 }
10527
10528 #[test]
10529 fn test_sub_select_over_a_multi_link_path_keeps_its_modifiers() {
10530 let out = compile_and_emit("SELECT Person { t := (select .posts.title order by .title desc limit 2) }");
10531 assert!(out.sql.contains("ARRAY(SELECT"), "{}", out.sql);
10532 assert!(out.sql.contains("ORDER BY \"t2\".\"title\" DESC"), "{}", out.sql);
10533 assert!(out.sql.contains("LIMIT 2"), "{}", out.sql);
10534 }
10535
10536 #[test]
10537 fn test_limit_one_over_a_multi_link_path_stays_a_scalar() {
10538 let out = compile_and_emit("SELECT Person { t := (select .posts.title limit 1) }");
10541 assert!(!out.sql.contains("ARRAY("), "{}", out.sql);
10542 assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
10543 }
10544
10545 #[test]
10546 fn test_computed_multilink_can_carry_a_nested_shape() {
10547 let out = compile_and_emit("SELECT Person { p := .posts { title } }");
10548 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
10549 panic!()
10550 };
10551 let ShapeNode::Array { name, element, .. } = &pointers[2] else {
10552 panic!("{:?}", pointers[2])
10553 };
10554 assert_eq!(name, "p");
10555 let ShapeNode::Object {
10556 pointers: elem_pointers,
10557 ..
10558 } = element.as_ref()
10559 else {
10560 panic!()
10561 };
10562 assert!(matches!(&elem_pointers[1], ShapeNode::Scalar { name, .. } if name == "title"));
10563 }
10564
10565 #[test]
10566 fn test_schema_declared_backlink_computed_is_an_object_pointer() {
10567 use crate::schema::ComputedDescriptor;
10568 let mut schema = make_schema();
10569 let post = schema.types.iter_mut().find(|t| t.name == "Post").unwrap();
10570 post.computed = vec![ComputedDescriptor {
10571 name: "authors".into(),
10572 expression: ".<posts[is Person]".into(),
10573 return_type: None,
10574 link_target: None,
10575 link_multi: false,
10576 }];
10577 let out = compile_and_emit_with("SELECT Post { authors { name } }", &schema);
10580 assert!(out.sql.contains("array_agg(ROW("), "{}", out.sql);
10581 assert!(out.sql.contains("\"t1\".\"name\"::text"), "{}", out.sql);
10582 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
10583 panic!()
10584 };
10585 assert!(matches!(&pointers[1], ShapeNode::Array { name, .. } if name == "authors"));
10586 }
10587
10588 #[test]
10589 fn test_schema_declared_multilink_computed_defaults_to_ids_without_a_shape() {
10590 use crate::schema::ComputedDescriptor;
10591 let mut schema = make_schema();
10592 schema.types[0].computed = vec![ComputedDescriptor {
10593 name: "everything".into(),
10594 expression: ".posts".into(),
10595 return_type: None,
10596 link_target: None,
10597 link_multi: false,
10598 }];
10599 let out = compile_and_emit_with("SELECT Person { everything }", &schema);
10600 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
10601 panic!()
10602 };
10603 assert!(matches!(&pointers[2], ShapeNode::Array { name, .. } if name == "everything"));
10604 }
10605
10606 #[test]
10607 fn test_schema_declared_computed_sub_select_is_a_link_pointer() {
10608 use crate::schema::ComputedDescriptor;
10609 let mut schema = make_schema();
10610 schema.types[0].computed = vec![
10611 ComputedDescriptor {
10612 name: "recent".into(),
10613 expression: "(select .posts order by .title desc limit 1)".into(),
10614 return_type: None,
10615 link_target: None,
10616 link_multi: false,
10617 },
10618 ComputedDescriptor {
10619 name: "recent_title".into(),
10620 expression: "(select .posts order by .title desc limit 1).title".into(),
10621 return_type: Some("text".into()),
10622 link_target: None,
10623 link_multi: false,
10624 },
10625 ];
10626 let out = compile_and_emit_with("SELECT Person { recent { title }, recent_title }", &schema);
10627 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
10628 panic!()
10629 };
10630 assert!(matches!(&pointers[2], ShapeNode::Object { name, .. } if name == "recent"));
10633 assert!(matches!(&pointers[3], ShapeNode::Scalar { name, .. } if name == "recent_title"));
10634 assert!(out.sql.contains("\"jt\".source = \"t0\".id"), "{}", out.sql);
10635 assert_eq!(out.sql.matches("LIMIT 1").count(), 2, "{}", out.sql);
10636 crate::validate::validate_schema_types(&schema).expect("schema should validate");
10637 }
10638
10639 #[test]
10644 fn test_a_mutation_read_through_a_path_runs_as_a_hoisted_cte() {
10645 let out = compile_and_emit("SELECT (INSERT Person { name := 'x' }).name");
10646 assert!(
10647 out.sql.contains("\"_nested_dml_0\" AS (") && out.sql.contains("INSERT INTO"),
10648 "the mutation must run as a CTE:\n{}",
10649 out.sql
10650 );
10651 assert!(
10652 out.sql.contains("FROM \"_nested_dml_0\""),
10653 "the projection must read the mutation's own rows:\n{}",
10654 out.sql
10655 );
10656 }
10657
10658 #[test]
10659 fn test_dml_sub_statement_in_expression_position_still_rejected() {
10660 let err = compile_err("SELECT Person { x := (insert Company { name := 'c' }).name }");
10661 assert!(err.contains("an insert cannot stand in for a value"), "{err}");
10662 }
10663
10664 #[test]
10665 fn test_sub_select_with_a_shape_in_expression_position_is_rejected() {
10666 let err = compile_err("SELECT Person { name } filter (select .posts { title }) = 1");
10670 assert!(err.contains("sub-select with a shape"), "{err}");
10671 }
10672
10673 fn make_schema_with_through() -> SchemaDescriptor {
10674 let id_prop = || PropertyDescriptor {
10675 name: "id".into(),
10676 pg_type: "uuid".into(),
10677 nullable: false,
10678 default_sql: Some("gen_random_uuid()".into()),
10679 description: None,
10680 default_pyql: None,
10681 check_constraints: vec![],
10682 is_exclusive: true,
10683 is_pk: true,
10684 is_readonly: true,
10685 rewrites: vec![],
10686 tuple_members: None,
10687 column_type: None,
10688 };
10689 let name_prop = || PropertyDescriptor {
10690 name: "name".into(),
10691 pg_type: "text".into(),
10692 nullable: false,
10693 default_sql: None,
10694 description: None,
10695 check_constraints: vec![],
10696 default_pyql: None,
10697 is_exclusive: false,
10698 is_pk: false,
10699 is_readonly: false,
10700 rewrites: vec![],
10701 tuple_members: None,
10702 column_type: None,
10703 };
10704 SchemaDescriptor {
10705 types: vec![
10706 TypeDescriptor {
10707 name: "Person".into(),
10708 module: "default".into(),
10709 table: "Person".into(),
10710 abstract_: false,
10711 materialized: false,
10712 description: None,
10713 parents: vec![],
10714 interfaces: vec![],
10715 bases: vec![],
10716 properties: vec![id_prop(), name_prop()],
10717 links: vec![],
10718 multilinks: vec![MultiLinkDescriptor {
10719 name: "friends".into(),
10720 target: "default::Person".into(),
10721 through: Some("default::PersonFriend".into()),
10722 nullable: false,
10723 description: None,
10724 default_pyql: None,
10725 on_delete: vec![],
10726 is_exclusive: false,
10727 }],
10728 computed: vec![],
10729 constraints: vec![],
10730 indexes: vec![],
10731 partition: None,
10732 vector_indexes: vec![],
10733 search_indexes: vec![],
10734 triggers: vec![],
10735 junction: false,
10736 signals: vec![],
10737 },
10738 TypeDescriptor {
10739 name: "PersonFriend".into(),
10740 module: "default".into(),
10741 table: "PersonFriend".into(),
10742 abstract_: false,
10743 materialized: false,
10744 description: None,
10745 parents: vec![],
10746 interfaces: vec![],
10747 bases: vec![],
10748 properties: vec![id_prop()],
10749 links: vec![
10750 LinkDescriptor {
10751 name: "person".into(),
10752 target: "default::Person".into(),
10753 nullable: false,
10754 through: None,
10755 description: None,
10756 default_pyql: None,
10757 is_exclusive: false,
10758 is_readonly: false,
10759 rewrites: vec![],
10760 on_delete: vec![],
10761 },
10762 LinkDescriptor {
10763 name: "friend".into(),
10764 target: "default::Person".into(),
10765 nullable: false,
10766 through: None,
10767 description: None,
10768 default_pyql: None,
10769 is_exclusive: false,
10770 is_readonly: false,
10771 rewrites: vec![],
10772 on_delete: vec![],
10773 },
10774 ],
10775 multilinks: vec![],
10776 computed: vec![],
10777 constraints: vec![],
10778 indexes: vec![],
10779 partition: None,
10780 vector_indexes: vec![],
10781 search_indexes: vec![],
10782 triggers: vec![],
10783 junction: false,
10784 signals: vec![],
10785 },
10786 ],
10787 scalars: vec![],
10788 enums: vec![],
10789 named_tuples: vec![],
10790 globals: vec![],
10791 functions: vec![],
10792 aliases: vec![],
10793 channels: vec![],
10794 ..Default::default()
10795 }
10796 }
10797
10798 #[test]
10799 fn test_select_through_multi_link() {
10800 let schema = make_schema_with_through();
10801 let ast = crate::parse::parse("SELECT Person { name, friends { name } }").unwrap();
10802 let ir = crate::ir::compile(&ast, &schema).unwrap();
10803 let out = emit(&ir);
10804 assert!(out.sql.contains("\"public\".\"PersonFriend\""));
10806 assert!(out.sql.contains("\"friend\""));
10808 assert!(out.sql.contains("\"person\""));
10809 assert!(out.sql.contains("array_agg(ROW("));
10811 }
10812
10813 #[test]
10819 fn test_limited_multi_link_keeps_a_nested_multi_link_in_scope() {
10820 let schema = make_schema_with_through();
10823 let ast = crate::parse::parse("SELECT Person { friends: { name, friends { name } } limit 1 }").unwrap();
10824 let ir = crate::ir::compile(&ast, &schema).unwrap();
10825 let out = emit(&ir);
10826 assert_eq!(out.sql.matches("array_agg(ROW(").count(), 2, "{}", out.sql);
10827 let derived = out.sql.find("AS \"__lim\"").unwrap();
10828 let nested = out.sql.rfind("array_agg(ROW(").unwrap();
10829 assert!(
10830 nested < derived,
10831 "nested aggregate must sit inside the derived table:\n{}",
10832 out.sql
10833 );
10834 }
10835
10836 fn make_schema_with_junction_backed_link() -> SchemaDescriptor {
10837 let id_prop = || PropertyDescriptor {
10838 name: "id".into(),
10839 pg_type: "uuid".into(),
10840 nullable: false,
10841 default_sql: Some("gen_random_uuid()".into()),
10842 description: None,
10843 default_pyql: None,
10844 check_constraints: vec![],
10845 is_exclusive: true,
10846 is_pk: true,
10847 is_readonly: true,
10848 rewrites: vec![],
10849 tuple_members: None,
10850 column_type: None,
10851 };
10852 let name_prop = || PropertyDescriptor {
10853 name: "name".into(),
10854 pg_type: "text".into(),
10855 nullable: false,
10856 default_sql: None,
10857 description: None,
10858 check_constraints: vec![],
10859 default_pyql: None,
10860 is_exclusive: false,
10861 is_pk: false,
10862 is_readonly: false,
10863 rewrites: vec![],
10864 tuple_members: None,
10865 column_type: None,
10866 };
10867 SchemaDescriptor {
10868 types: vec![
10869 TypeDescriptor {
10870 name: "Person".into(),
10871 module: "default".into(),
10872 table: "Person".into(),
10873 abstract_: false,
10874 materialized: false,
10875 description: None,
10876 parents: vec![],
10877 interfaces: vec![],
10878 bases: vec![],
10879 properties: vec![id_prop(), name_prop()],
10880 links: vec![LinkDescriptor {
10881 name: "spouse".into(),
10882 target: "default::Org".into(),
10883 nullable: true,
10884 through: Some("default::Marriage".into()),
10885 description: None,
10886 default_pyql: None,
10887 is_exclusive: true,
10888 is_readonly: false,
10889 rewrites: vec![],
10890 on_delete: vec![],
10891 }],
10892 multilinks: vec![],
10893 computed: vec![],
10894 constraints: vec![],
10895 indexes: vec![],
10896 partition: None,
10897 vector_indexes: vec![],
10898 search_indexes: vec![],
10899 triggers: vec![],
10900 junction: false,
10901 signals: vec![],
10902 },
10903 TypeDescriptor {
10904 name: "Org".into(),
10905 module: "default".into(),
10906 table: "Org".into(),
10907 abstract_: false,
10908 materialized: false,
10909 description: None,
10910 parents: vec![],
10911 interfaces: vec![],
10912 bases: vec![],
10913 properties: vec![id_prop(), name_prop()],
10914 links: vec![],
10915 multilinks: vec![],
10916 computed: vec![],
10917 constraints: vec![],
10918 indexes: vec![],
10919 partition: None,
10920 vector_indexes: vec![],
10921 search_indexes: vec![],
10922 triggers: vec![],
10923 junction: false,
10924 signals: vec![],
10925 },
10926 TypeDescriptor {
10927 name: "Marriage".into(),
10928 module: "default".into(),
10929 table: "Person.spouse".into(),
10930 abstract_: false,
10931 materialized: false,
10932 description: None,
10933 parents: vec![],
10934 interfaces: vec![],
10935 bases: vec![],
10936 properties: vec![
10937 id_prop(),
10938 PropertyDescriptor {
10939 name: "since".into(),
10940 pg_type: "int8".into(),
10941 nullable: true,
10942 default_sql: None,
10943 description: None,
10944 check_constraints: vec![],
10945 default_pyql: None,
10946 is_exclusive: false,
10947 is_pk: false,
10948 is_readonly: false,
10949 rewrites: vec![],
10950 tuple_members: None,
10951 column_type: None,
10952 },
10953 ],
10954 links: vec![
10955 LinkDescriptor {
10956 name: "source".into(),
10957 target: "default::Person".into(),
10958 nullable: false,
10959 through: None,
10960 description: None,
10961 default_pyql: None,
10962 is_exclusive: false,
10963 is_readonly: false,
10964 rewrites: vec![],
10965 on_delete: vec![],
10966 },
10967 LinkDescriptor {
10968 name: "target".into(),
10969 target: "default::Org".into(),
10970 nullable: false,
10971 through: None,
10972 description: None,
10973 default_pyql: None,
10974 is_exclusive: false,
10975 is_readonly: false,
10976 rewrites: vec![],
10977 on_delete: vec![],
10978 },
10979 ],
10980 multilinks: vec![],
10981 computed: vec![],
10982 constraints: vec![],
10983 indexes: vec![],
10984 partition: None,
10985 vector_indexes: vec![],
10986 search_indexes: vec![],
10987 triggers: vec![],
10988 junction: true,
10989 signals: vec![],
10990 },
10991 ],
10992 scalars: vec![],
10993 enums: vec![],
10994 named_tuples: vec![],
10995 globals: vec![],
10996 functions: vec![],
10997 aliases: vec![],
10998 channels: vec![],
10999 ..Default::default()
11000 }
11001 }
11002
11003 #[test]
11004 fn test_select_shape_over_junction_backed_single_link() {
11005 let schema = make_schema_with_junction_backed_link();
11006 let ast = crate::parse::parse("SELECT Person { name, spouse { name } }").unwrap();
11007 let ir = crate::ir::compile(&ast, &schema).unwrap();
11008 let out = emit(&ir);
11009 assert!(out.sql.contains("\"public\".\"Person.spouse\""), "got:\n{}", out.sql);
11011 assert!(out.sql.contains("\"jt\".source"), "got:\n{}", out.sql);
11012 assert!(out.sql.contains("\"jt\".target"), "got:\n{}", out.sql);
11013 assert!(!out.sql.contains("array_agg"), "got:\n{}", out.sql);
11015 assert!(!out.sql.contains("spouse_id"), "got:\n{}", out.sql);
11016 }
11017
11018 #[test]
11019 fn test_select_shape_over_junction_backed_single_link_with_link_property() {
11020 let schema = make_schema_with_junction_backed_link();
11026 let ast = crate::parse::parse("SELECT Person { name, spouse { name, @since } }").unwrap();
11027 let ir = crate::ir::compile(&ast, &schema).unwrap();
11028 let out = emit(&ir);
11029 assert!(out.sql.contains("\"jt\".\"since\""), "got:\n{}", out.sql);
11030 assert!(!out.sql.contains("array_agg"), "got:\n{}", out.sql);
11031 }
11032
11033 #[test]
11034 fn test_select_path_over_junction_backed_single_link() {
11035 let schema = make_schema_with_junction_backed_link();
11036 let ast = crate::parse::parse("SELECT Person.spouse { name }").unwrap();
11037 let ir = crate::ir::compile(&ast, &schema).unwrap();
11038 let out = emit(&ir);
11039 assert!(out.sql.contains("\"public\".\"Person.spouse\""), "got:\n{}", out.sql);
11040 assert!(out.sql.contains("\"source\""), "got:\n{}", out.sql);
11041 assert!(out.sql.contains("\"target\""), "got:\n{}", out.sql);
11042 assert!(!out.sql.contains("spouse_id"), "got:\n{}", out.sql);
11043 }
11044
11045 #[test]
11046 fn test_insert_with_junction_backed_single_link() {
11047 let schema = make_schema_with_junction_backed_link();
11048 let out = compile_and_emit_with(
11049 "INSERT Person { name := $name, \
11050 spouse := (SELECT Org FILTER .id = $oid) { @since := <int64>$since } }",
11051 &schema,
11052 );
11053 assert!(out.sql.contains("\"public\".\"Person.spouse\""), "got:\n{}", out.sql);
11054 assert!(out.sql.contains("\"since\""), "got:\n{}", out.sql);
11055 assert!(!out.sql.contains("spouse_id"), "got:\n{}", out.sql);
11056 assert!(out.sql.contains("ON CONFLICT (\"source\") DO UPDATE SET \"target\" = EXCLUDED.\"target\", \"since\" = EXCLUDED.\"since\""), "got:\n{}", out.sql);
11063 }
11064
11065 #[test]
11066 fn test_update_replace_junction_backed_single_link() {
11067 let schema = make_schema_with_junction_backed_link();
11068 let out = compile_and_emit_with(
11069 "UPDATE Person FILTER .id = $id SET { \
11070 spouse := (SELECT Org FILTER .id = $oid) { @since := <int64>$since } }",
11071 &schema,
11072 );
11073 assert!(
11075 out.sql.contains("DELETE FROM \"public\".\"Person.spouse\""),
11076 "got:\n{}",
11077 out.sql
11078 );
11079 assert!(
11080 out.sql.contains("INSERT INTO \"public\".\"Person.spouse\""),
11081 "got:\n{}",
11082 out.sql
11083 );
11084 assert!(out.sql.contains("ON CONFLICT (\"source\") DO UPDATE SET \"target\" = EXCLUDED.\"target\", \"since\" = EXCLUDED.\"since\""), "got:\n{}", out.sql);
11085 assert!(out.sql.contains("NOT IN"), "got:\n{}", out.sql);
11090 }
11091
11092 #[test]
11097 fn test_unless_conflict_else_update_keeps_its_filter() {
11098 let out = compile_and_emit(
11099 "INSERT Person { name := 'a' } UNLESS CONFLICT ON .name \
11100 ELSE (UPDATE Person FILTER .age < 30 SET { name := 'b' })",
11101 );
11102 assert!(
11103 out.sql.contains("DO UPDATE SET") && out.sql.contains("WHERE"),
11104 "the ELSE UPDATE's filter must survive as a DO UPDATE predicate:\n{}",
11105 out.sql
11106 );
11107 assert!(
11108 out.sql.contains("\"Person\".\"age\""),
11109 "the predicate must read the existing row, not `excluded`:\n{}",
11110 out.sql
11111 );
11112 }
11113
11114 #[test]
11116 fn test_unless_conflict_else_update_without_a_filter_has_no_predicate() {
11117 let out = compile_and_emit(
11118 "INSERT Person { name := 'a' } UNLESS CONFLICT ON .name ELSE (UPDATE Person SET { name := 'b' })",
11119 );
11120 let after_set = out.sql.split("DO UPDATE SET").nth(1).unwrap_or_default();
11121 assert!(
11122 !after_set.contains("WHERE"),
11123 "got a predicate we never asked for:\n{}",
11124 out.sql
11125 );
11126 }
11127
11128 #[test]
11129 fn test_junction_backed_single_link_rejected_in_unless_conflict_else() {
11130 let schema = make_schema_with_junction_backed_link();
11137 let ast = parse::parse(
11138 "INSERT Person { name := $name } \
11139 UNLESS CONFLICT ON .name ELSE (UPDATE Person SET { \
11140 spouse := (SELECT Org FILTER .id = $oid) })",
11141 )
11142 .unwrap();
11143 assert!(ir::compile(&ast, &schema).is_err());
11144 }
11145
11146 #[test]
11147 fn test_update_clear_junction_backed_single_link() {
11148 let schema = make_schema_with_junction_backed_link();
11149 let out = compile_and_emit_with("UPDATE Person FILTER .id = $id SET { spouse := {} }", &schema);
11150 assert!(
11151 out.sql.contains("DELETE FROM \"public\".\"Person.spouse\""),
11152 "got:\n{}",
11153 out.sql
11154 );
11155 assert!(
11156 !out.sql.contains("INSERT INTO \"public\".\"Person.spouse\""),
11157 "clearing must not also insert:\n{}",
11158 out.sql
11159 );
11160 }
11161
11162 #[test]
11163 fn test_update_clear_junction_backed_single_link_with_cast_empty_set() {
11164 let schema = make_schema_with_junction_backed_link();
11172 let out = compile_and_emit_with("UPDATE Person FILTER .id = $id SET { spouse := <Org>{} }", &schema);
11173 assert!(
11174 out.sql.contains("DELETE FROM \"public\".\"Person.spouse\""),
11175 "got:\n{}",
11176 out.sql
11177 );
11178 assert!(
11179 !out.sql.contains("INSERT INTO \"public\".\"Person.spouse\""),
11180 "clearing must not also insert:\n{}",
11181 out.sql
11182 );
11183 }
11184
11185 fn make_schema_with_through_and_prop() -> SchemaDescriptor {
11193 let id_prop = || PropertyDescriptor {
11194 name: "id".into(),
11195 pg_type: "uuid".into(),
11196 nullable: false,
11197 default_sql: Some("gen_random_uuid()".into()),
11198 description: None,
11199 default_pyql: None,
11200 check_constraints: vec![],
11201 is_exclusive: true,
11202 is_pk: true,
11203 is_readonly: true,
11204 rewrites: vec![],
11205 tuple_members: None,
11206 column_type: None,
11207 };
11208 let name_prop = || PropertyDescriptor {
11209 name: "name".into(),
11210 pg_type: "text".into(),
11211 nullable: false,
11212 default_sql: None,
11213 description: None,
11214 check_constraints: vec![],
11215 default_pyql: None,
11216 is_exclusive: false,
11217 is_pk: false,
11218 is_readonly: false,
11219 rewrites: vec![],
11220 tuple_members: None,
11221 column_type: None,
11222 };
11223 SchemaDescriptor {
11224 types: vec![
11225 TypeDescriptor {
11226 name: "Product".into(),
11227 module: "default".into(),
11228 table: "Product".into(),
11229 abstract_: false,
11230 materialized: false,
11231 description: None,
11232 parents: vec![],
11233 interfaces: vec![],
11234 bases: vec![],
11235 properties: vec![id_prop(), name_prop()],
11236 links: vec![],
11237 multilinks: vec![MultiLinkDescriptor {
11238 name: "tags".into(),
11239 target: "default::Tag".into(),
11240 through: Some("default::ProductTag".into()),
11241 nullable: false,
11242 description: None,
11243 default_pyql: None,
11244 on_delete: vec![],
11245 is_exclusive: false,
11246 }],
11247 computed: vec![],
11248 constraints: vec![],
11249 indexes: vec![],
11250 partition: None,
11251 vector_indexes: vec![],
11252 search_indexes: vec![],
11253 triggers: vec![],
11254 junction: false,
11255 signals: vec![],
11256 },
11257 TypeDescriptor {
11258 name: "Tag".into(),
11259 module: "default".into(),
11260 table: "Tag".into(),
11261 abstract_: false,
11262 materialized: false,
11263 description: None,
11264 parents: vec![],
11265 interfaces: vec![],
11266 bases: vec![],
11267 properties: vec![id_prop(), name_prop()],
11268 links: vec![],
11269 multilinks: vec![],
11270 computed: vec![],
11271 constraints: vec![],
11272 indexes: vec![],
11273 partition: None,
11274 vector_indexes: vec![],
11275 search_indexes: vec![],
11276 triggers: vec![],
11277 junction: false,
11278 signals: vec![],
11279 },
11280 TypeDescriptor {
11281 name: "ProductTag".into(),
11282 module: "default".into(),
11283 table: "Product.tags".into(),
11284 abstract_: false,
11285 materialized: false,
11286 description: None,
11287 parents: vec![],
11288 interfaces: vec![],
11289 bases: vec![],
11290 properties: vec![
11291 id_prop(),
11292 PropertyDescriptor {
11293 name: "weight".into(),
11294 pg_type: "float8".into(),
11295 nullable: false,
11296 default_sql: None,
11297 default_pyql: None,
11298 description: None,
11299 check_constraints: vec![],
11300 is_exclusive: false,
11301 is_pk: false,
11302 is_readonly: false,
11303 rewrites: vec![],
11304 tuple_members: None,
11305 column_type: None,
11306 },
11307 ],
11308 links: vec![],
11312 multilinks: vec![],
11313 computed: vec![],
11314 constraints: vec![],
11315 indexes: vec![],
11316 partition: None,
11317 vector_indexes: vec![],
11318 search_indexes: vec![],
11319 triggers: vec![],
11320 junction: true,
11321 signals: vec![],
11322 },
11323 ],
11324 scalars: vec![],
11325 enums: vec![],
11326 named_tuples: vec![],
11327 globals: vec![],
11328 functions: vec![],
11329 aliases: vec![],
11330 channels: vec![],
11331 ..Default::default()
11332 }
11333 }
11334
11335 #[test]
11336 fn test_link_property_in_a_links_own_filter() {
11337 let schema = make_schema_with_through_and_prop();
11341 let out = compile_and_emit_with("SELECT Product { tags: { } filter @weight > 0.5 }", &schema);
11342 assert!(out.sql.contains("\"jt\".\"weight\" > (0.5::float8)"), "{}", out.sql);
11343 }
11344
11345 #[test]
11346 fn test_exists_on_a_link_property() {
11347 let schema = make_schema_with_through_and_prop();
11350 let out = compile_and_emit_with("SELECT Product { tags: { } filter exists @weight }", &schema);
11351 assert!(out.sql.contains("\"jt\".\"weight\" IS NOT NULL"), "{}", out.sql);
11352 }
11353
11354 #[test]
11355 fn test_link_property_in_a_computed_link_filter() {
11356 let schema = make_schema_with_through_and_prop();
11357 let out = compile_and_emit_with(
11358 "SELECT Product { best := (select .tags filter @weight > 0.5 limit 1) { @weight } }",
11359 &schema,
11360 );
11361 assert!(out.sql.contains("\"jt\".\"weight\" > (0.5::float8)"), "{}", out.sql);
11362 assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
11363 }
11364
11365 #[test]
11366 fn test_link_property_in_a_projected_sub_select_reads_the_junction() {
11367 let schema = make_schema_with_through_and_prop();
11370 let out = compile_and_emit_with(
11371 "SELECT Product { t := (select .tags filter @weight > 0.5 limit 1).id }",
11372 &schema,
11373 );
11374 assert!(out.sql.contains("\"t3\".\"weight\" > (0.5::float8)"), "{}", out.sql);
11375 }
11376
11377 #[test]
11378 fn test_unknown_link_property_names_the_through_type() {
11379 let schema = make_schema_with_through_and_prop();
11380 let ast = parse::parse("SELECT Product { tags: { } filter @nope > 0.5 }").unwrap();
11381 let err = match ir::compile(&ast, &schema) {
11382 Ok(_) => panic!("expected a compile error"),
11383 Err(e) => format!("{e}"),
11384 };
11385 assert!(err.contains("default::ProductTag") && err.contains("nope"), "{err}");
11386 }
11387
11388 #[test]
11389 fn test_link_property_outside_a_link_says_where_it_belongs() {
11390 let schema = make_schema_with_through_and_prop();
11391 let ast = parse::parse("SELECT Product { t := @weight }").unwrap();
11392 let err = match ir::compile(&ast, &schema) {
11393 Ok(_) => panic!("expected a compile error"),
11394 Err(e) => format!("{e}"),
11395 };
11396 assert!(err.contains("is a link property"), "{err}");
11397 }
11398
11399 #[test]
11400 fn test_multilink_append_with_link_property() {
11401 let schema = make_schema_with_through_and_prop();
11402 let out = compile_and_emit_with(
11403 "UPDATE Product FILTER .id = $id SET { tags += (SELECT Tag FILTER .id = $tid) { @weight := <float64>$w } }",
11404 &schema,
11405 );
11406 assert!(out.sql.contains("\"weight\""), "missing weight column:\n{}", out.sql);
11408 assert!(
11411 out.sql
11412 .contains("ON CONFLICT (\"source\", \"target\") DO UPDATE SET \"weight\" = EXCLUDED.\"weight\""),
11413 "missing upsert conflict clause:\n{}",
11414 out.sql
11415 );
11416 }
11417
11418 #[test]
11419 fn test_multilink_append_union_with_different_link_property_values() {
11420 let schema = make_schema_with_through_and_prop();
11424 let out = compile_and_emit_with(
11425 "UPDATE Product FILTER .id = $id SET { \
11426 tags += (SELECT Tag FILTER .id = $aid) { @weight := <float64>$w1 } \
11427 union (SELECT Tag FILTER .id = $bid) { @weight := <float64>$w2 } \
11428 }",
11429 &schema,
11430 );
11431 assert!(
11432 out.sql.contains("UNION ALL"),
11433 "expected a UNION ALL between the two shaped targets:\n{}",
11434 out.sql
11435 );
11436 assert_eq!(
11439 out.sql.matches("AS \"weight\"").count(),
11440 2,
11441 "each union branch must project its own weight:\n{}",
11442 out.sql
11443 );
11444 }
11445
11446 #[test]
11447 fn test_multilink_append_without_link_property_keeps_do_nothing() {
11448 let schema = make_schema_with_through_and_prop();
11451 let out = compile_and_emit_with(
11452 "UPDATE Product FILTER .id = $id SET { tags += (SELECT Tag FILTER .id = $tid) }",
11453 &schema,
11454 );
11455 assert!(
11456 out.sql.contains("ON CONFLICT DO NOTHING"),
11457 "expected plain DO NOTHING when no link properties are set:\n{}",
11458 out.sql
11459 );
11460 assert!(
11461 !out.sql.contains("\"weight\""),
11462 "unexpected weight column with no link property assignment:\n{}",
11463 out.sql
11464 );
11465 }
11466
11467 #[test]
11468 fn test_multilink_link_property_rejected_on_standard_junction() {
11469 let ast = crate::parse::parse(
11472 "UPDATE Person FILTER .id = $id SET { posts += (SELECT Post FILTER .title = $t) { @weight := <float64>$w } }",
11473 ).unwrap();
11474 match crate::ir::compile(&ast, &make_schema()) {
11475 Ok(_) => panic!("expected a compile error for link property on a Standard junction"),
11476 Err(e) => assert!(
11477 e.to_string().contains("Through"),
11478 "expected a Through[...]-related error, got: {e}"
11479 ),
11480 }
11481 }
11482
11483 #[test]
11484 fn test_multilink_link_property_rejected_on_remove() {
11485 let schema = make_schema_with_through_and_prop();
11486 let ast = crate::parse::parse(
11487 "UPDATE Product FILTER .id = $id SET { tags -= (SELECT Tag FILTER .id = $tid) { @weight := <float64>$w } }",
11488 )
11489 .unwrap();
11490 match crate::ir::compile(&ast, &schema) {
11491 Ok(_) => panic!("expected a compile error for link property on a remove (-=)"),
11492 Err(e) => assert!(
11493 e.to_string().contains("removing"),
11494 "expected a remove-related error, got: {e}"
11495 ),
11496 }
11497 }
11498
11499 #[test]
11500 fn test_insert_with_multilink_assignment() {
11501 let schema = make_schema_with_through_and_prop();
11506 let out = compile_and_emit_with(
11507 "INSERT Product { name := $name, tags := (SELECT Tag FILTER .id = $tid) { @weight := <float64>$w } }",
11508 &schema,
11509 );
11510 assert!(
11512 out.sql.contains("\"_w__ids\" AS (\nINSERT INTO"),
11513 "missing row-insert CTE:\n{}",
11514 out.sql
11515 );
11516 assert!(
11517 out.sql.contains("\"_w__ml_add_0\" AS ("),
11518 "missing junction-append CTE:\n{}",
11519 out.sql
11520 );
11521 assert!(out.sql.contains("\"weight\""), "missing weight column:\n{}", out.sql);
11522 assert!(
11523 out.sql.contains("\"_w\" AS (\n SELECT * FROM \"_w__ids\"\n)"),
11524 "missing _w passthrough:\n{}",
11525 out.sql
11526 );
11527 assert_eq!(
11528 out.sql.matches("WITH\n").count(),
11529 1,
11530 "must be a single flat top-level WITH block:\n{}",
11531 out.sql
11532 );
11533 }
11534
11535 #[test]
11543 fn test_user_with_binding_merges_into_a_statements_own_ctes() {
11544 let schema = make_schema_with_through_and_prop();
11545 for query in [
11546 "with t := (select Tag filter .id = $tid) insert Product { name := $name, tags := t }",
11547 "with t := (select Tag filter .id = $tid) \
11548 update Product filter .name = $name set { tags += t }",
11549 ] {
11550 let out = compile_and_emit_with(query, &schema);
11551 let flat: String = out.sql.split_whitespace().collect::<Vec<_>>().join(" ");
11552 assert!(
11553 !flat.contains(") WITH "),
11554 "a second WITH clause is invalid SQL:\n{}",
11555 out.sql
11556 );
11557 assert_eq!(
11558 out.sql.matches("WITH").count(),
11559 1,
11560 "exactly one WITH keyword expected:\n{}",
11561 out.sql
11562 );
11563 }
11564 }
11565
11566 #[test]
11567 fn test_with_bound_insert_with_multilink_assignment() {
11568 let schema = make_schema_with_through_and_prop();
11572 let out = compile_and_emit_with(
11573 "with insert0 := (insert Product { name := $name, tags := (select Tag filter .id = $tid) }) select insert0",
11574 &schema,
11575 );
11576 assert!(
11577 out.sql.contains("\"insert0__ids\" AS (\nINSERT INTO"),
11578 "missing row-insert CTE:\n{}",
11579 out.sql
11580 );
11581 assert!(
11582 out.sql.contains("\"insert0__ml_add_0\" AS ("),
11583 "missing junction-append CTE:\n{}",
11584 out.sql
11585 );
11586 assert!(
11587 out.sql
11588 .contains("\"insert0\" AS (\n SELECT * FROM \"insert0__ids\"\n)"),
11589 "missing insert0 passthrough:\n{}",
11590 out.sql
11591 );
11592 assert_eq!(
11593 out.sql.matches("WITH\n").count(),
11594 1,
11595 "must be a single flat top-level WITH block:\n{}",
11596 out.sql
11597 );
11598 }
11599
11600 #[test]
11601 fn test_with_block_cte_over_computed_global_merges_into_single_with_clause() {
11602 let mut schema = make_schema();
11611 schema.globals.push(GlobalDescriptor {
11612 name: "current_user_id".into(),
11613 module: "default".into(),
11614 scalar_type: "std::uuid".into(),
11615 required: false,
11616 default_expr: None,
11617 computed_expr: None,
11618 });
11619 schema.globals.push(GlobalDescriptor {
11620 name: "current_user".into(),
11621 module: "default".into(),
11622 scalar_type: "Person".into(),
11623 required: false,
11624 default_expr: None,
11625 computed_expr: Some("select default::Person filter .id = global current_user_id".into()),
11626 });
11627 let out = compile_and_emit_with("with\n user := (select global current_user)\nselect user;", &schema);
11628 assert_eq!(
11629 out.sql.matches("WITH").count(),
11630 1,
11631 "must be a single WITH clause, got:\n{}",
11632 out.sql
11633 );
11634 }
11635
11636 #[test]
11637 fn test_several_mutations_in_a_free_set_each_become_a_cte() {
11638 let schema = make_schema();
11639 let out = compile_and_emit_with(
11640 "select { (update Person filter .name = 'a' set { age := 1 }), \
11641 (update Company filter .name = 'b' set { name := 'c' }) }",
11642 &schema,
11643 );
11644 assert_eq!(
11645 out.sql.matches("UPDATE").count(),
11646 2,
11647 "both mutations have to run, got:\n{}",
11648 out.sql
11649 );
11650 assert!(out.sql.contains("WITH"), "each becomes a CTE, got:\n{}", out.sql);
11651 }
11652
11653 #[test]
11654 fn test_updating_a_binding_touches_only_its_own_rows() {
11655 let schema = make_schema();
11658 let out = compile_and_emit_with(
11659 "with mine := (select Person filter .name = 'a') select { (update mine set { age := 1 }) }",
11660 &schema,
11661 );
11662 assert!(
11663 out.sql.contains("UPDATE") && out.sql.contains("= ANY(ARRAY(SELECT"),
11664 "expected the update narrowed to the binding's rows, got:\n{}",
11665 out.sql
11666 );
11667 }
11668
11669 #[test]
11670 fn test_updating_a_traversal_touches_only_the_rows_it_lands_on() {
11671 let mut schema = make_schema();
11672 let company = schema
11675 .types
11676 .iter_mut()
11677 .find(|t| t.name == "Company")
11678 .expect("test schema has a Company type");
11679 company.properties.push(crate::schema::PropertyDescriptor {
11680 name: "id".into(),
11681 pg_type: "uuid".into(),
11682 nullable: false,
11683 default_sql: Some("uuidv7()".into()),
11684 default_pyql: None,
11685 description: None,
11686 check_constraints: vec![],
11687 is_exclusive: true,
11688 is_pk: true,
11689 is_readonly: true,
11690 rewrites: vec![],
11691 tuple_members: None,
11692 column_type: None,
11693 });
11694 let out = compile_and_emit_with(
11695 "with mine := (select Person filter .name = 'a') select { (update mine.company set { name := 'x' }) }",
11696 &schema,
11697 );
11698 assert!(
11699 out.sql.contains("= ANY(ARRAY(SELECT"),
11700 "expected the update narrowed to the traversal's rows, got:\n{}",
11701 out.sql
11702 );
11703 assert!(
11704 out.sql.contains("\"Company\""),
11705 "expected the table the traversal ends on, got:\n{}",
11706 out.sql
11707 );
11708 }
11709
11710 #[test]
11711 fn test_aggregate_over_a_multi_valued_path_keeps_the_set_flat() {
11712 let schema = make_schema();
11717 let out = compile_and_emit_with(
11718 "with a := (select Person limit 1) select { titles := array_agg(a.posts.title) }",
11719 &schema,
11720 );
11721 assert!(
11722 !out.sql.contains("array_agg(ARRAY("),
11723 "the aggregate must take the set, not the array standing for it, got:\n{}",
11724 out.sql
11725 );
11726 assert!(
11727 out.sql.contains("array_agg(") && out.sql.contains("\"title\""),
11728 "expected the aggregate over the traversal's own column, got:\n{}",
11729 out.sql
11730 );
11731 assert!(
11734 out.sql.contains("ROW(") && !out.sql.contains("jsonb_build_object"),
11735 "expected a composite row, got:\n{}",
11736 out.sql
11737 );
11738 }
11739
11740 #[test]
11741 fn test_with_bound_root_read_by_name_inside_a_nested_select() {
11742 let schema = make_schema();
11747 let out = compile_and_emit_with(
11748 "with
11749 owner := (select Person limit 1)
11750select owner { posts := (select owner.posts.title) };",
11751 &schema,
11752 );
11753 assert!(
11754 out.sql.contains("WITH") && out.sql.contains("\"title\""),
11755 "expected the nested select to traverse the binding, got:\n{}",
11756 out.sql
11757 );
11758 }
11759
11760 #[test]
11761 fn test_path_traversal_into_with_bound_cte_of_object_type() {
11762 let mut schema = make_schema();
11768 schema.globals.push(GlobalDescriptor {
11769 name: "current_user_id".into(),
11770 module: "default".into(),
11771 scalar_type: "std::uuid".into(),
11772 required: false,
11773 default_expr: None,
11774 computed_expr: None,
11775 });
11776 schema.globals.push(GlobalDescriptor {
11777 name: "current_user".into(),
11778 module: "default".into(),
11779 scalar_type: "Person".into(),
11780 required: false,
11781 default_expr: None,
11782 computed_expr: Some("select default::Person filter .id = global current_user_id".into()),
11783 });
11784 let out = compile_and_emit_with(
11785 "with\n user := (select global current_user)\nselect user.name;",
11786 &schema,
11787 );
11788 assert!(
11789 out.sql.contains("FROM \"user\""),
11790 "expected path traversal from the CTE, got:\n{}",
11791 out.sql
11792 );
11793 assert_eq!(
11794 out.sql.matches("WITH").count(),
11795 1,
11796 "must be a single WITH clause, got:\n{}",
11797 out.sql
11798 );
11799 }
11800
11801 #[test]
11802 fn test_with_bound_schema_object_property_access_in_filter_expression() {
11803 let schema = make_schema();
11811 let out = compile_and_emit_with(
11812 "with\n person := (select detached Person filter .id = <uuid>$id)\n\
11813 select Person filter .id not in person.id;",
11814 &schema,
11815 );
11816 assert!(
11817 out.sql.contains("<> ALL((SELECT") && out.sql.contains(".\"id\"") && out.sql.contains("FROM \"person\""),
11818 "got:\n{}",
11819 out.sql
11820 );
11821 }
11822
11823 #[test]
11824 fn test_with_bound_schema_object_scalar_property_access() {
11825 let schema = make_schema();
11829 let out = compile_and_emit_with(
11830 "with\n person := (select detached Person filter .id = <uuid>$id)\n\
11831 select Person filter .name = person.name;",
11832 &schema,
11833 );
11834 assert!(
11835 out.sql.contains("(SELECT") && out.sql.contains(".\"name\"") && out.sql.contains("FROM \"person\""),
11836 "got:\n{}",
11837 out.sql
11838 );
11839 }
11840
11841 #[test]
11842 fn test_with_bound_schema_object_link_traversal_reaches_nested_property() {
11843 let schema = make_schema();
11850 let out = compile_and_emit_with(
11851 "with\n person := (select detached Person filter .id = <uuid>$id)\n\
11852 select Company filter .name = person.company.name;",
11853 &schema,
11854 );
11855 assert!(
11856 out.sql.contains("(SELECT") && out.sql.contains("\"name\"") && out.sql.contains("\"Company\""),
11857 "got:\n{}",
11858 out.sql
11859 );
11860 }
11861
11862 #[test]
11863 fn test_with_bound_schema_object_bare_link_reduces_to_id() {
11864 let schema = make_schema();
11868 let out = compile_and_emit_with(
11869 "with\n person := (select detached Person filter .id = <uuid>$id)\n\
11870 select Person filter .id = person.company;",
11871 &schema,
11872 );
11873 assert!(
11874 out.sql.contains("(SELECT") && out.sql.contains("\"id\""),
11875 "got:\n{}",
11876 out.sql
11877 );
11878 }
11879
11880 #[test]
11881 fn test_with_bound_schema_object_unknown_property_suggests_a_close_match() {
11882 let schema = make_schema();
11883 let ast = parse::parse(
11884 "with\n person := (select detached Person filter .id = <uuid>$id)\n\
11885 select Person filter .name = person.nam;",
11886 )
11887 .unwrap();
11888 match ir::compile(&ast, &schema) {
11889 Err(err) => assert!(format!("{err}").contains("Did you mean 'name'"), "got: {err}"),
11890 Ok(_) => panic!("expected a compile error"),
11891 }
11892 }
11893
11894 #[test]
11895 fn test_with_bound_free_object_passthrough_preserves_all_fields() {
11896 let out = compile_and_emit("with\n test := { test2 := 1.0, test3 := 'str' }\nselect test;");
11902 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
11903 panic!("expected Object shape, got {:?}", out.shape.root)
11904 };
11905 assert_eq!(pointers.len(), 2);
11906 assert!(matches!(&pointers[0], ShapeNode::Scalar { name, .. } if name == "test2"));
11907 assert!(matches!(&pointers[1], ShapeNode::Scalar { name, .. } if name == "test3"));
11908 }
11909
11910 #[test]
11911 fn test_with_bound_free_object_field_access() {
11912 let out = compile_and_emit("with\n test := { test2 := 1.0, test3 := 'str' }\nselect test.test2;");
11917 assert!(out.sql.contains("\"test2\" FROM \"test\""), "got:\n{}", out.sql);
11918 }
11919
11920 #[test]
11921 fn test_with_bound_free_object_nested_field_access_chain() {
11922 let out =
11927 compile_and_emit("with\n test := { test2 := 1.0, test3 := { foo := 'bar' } }\nselect test.test3.foo;");
11928 assert!(out.sql.contains("\"test3\" FROM \"test\""), "got:\n{}", out.sql);
11929 assert!(
11930 out.sql.contains("->'foo'"),
11931 "expected jsonb field extraction, got:\n{}",
11932 out.sql
11933 );
11934 }
11935
11936 #[test]
11937 fn test_with_bound_free_object_nested_field_access_wrong_field_errors() {
11938 let schema = make_schema();
11941 let ast = parse::parse("with\n test := { test2 := 1.0, test3 := { foo := 'bar' } }\nselect test.test3.nope;")
11942 .unwrap();
11943 assert!(ir::compile(&ast, &schema).is_err());
11944 }
11945
11946 #[test]
11947 fn test_nested_free_object_literal_in_computed_shape_element() {
11948 let out = compile_and_emit("select default::Person { id, test := { foo := 'bar' } };");
11954 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
11955 panic!("expected Object shape")
11956 };
11957 let test_node = pointers
11958 .iter()
11959 .find(|p| matches!(p, ShapeNode::NamedTuple { name, .. } if name == "test"))
11960 .unwrap_or_else(|| panic!("expected a NamedTuple shape node for 'test', got {:?}", pointers));
11961 assert!(matches!(
11962 test_node,
11963 ShapeNode::NamedTuple {
11964 is_free_object: true,
11965 ..
11966 }
11967 ));
11968 }
11969
11970 #[test]
11971 fn test_bare_free_cte_reference_in_computed_shape_collapses_to_empty() {
11972 let out = compile_and_emit(
11977 "with\n test := { test2 := 1.0, test3 := 'str' }\n\
11978 select default::Person { id, test := test };",
11979 );
11980 assert!(
11981 out.sql.contains("jsonb_build_object()"),
11982 "expected an empty free object, got:\n{}",
11983 out.sql
11984 );
11985 }
11986
11987 #[test]
11988 fn test_shaped_free_cte_reference_projects_fields() {
11989 let out = compile_and_emit(
11993 "with\n test := { test2 := 1.0, test3 := 'str' }\n\
11994 select default::Person { id, test := test { test2 } };",
11995 );
11996 assert!(out.sql.contains("jsonb_build_object('test2'"), "got:\n{}", out.sql);
11997 assert!(
11998 !out.sql.contains("'test3'"),
11999 "test3 should not be projected, got:\n{}",
12000 out.sql
12001 );
12002 }
12003
12004 #[test]
12005 fn test_insert_multilink_remove_rejected() {
12006 let schema = make_schema_with_through_and_prop();
12007 let ast =
12008 crate::parse::parse("INSERT Product { name := $name, tags -= (SELECT Tag FILTER .id = $tid) }").unwrap();
12009 match crate::ir::compile(&ast, &schema) {
12010 Ok(_) => panic!("expected a compile error for `-=` on a multi-link at insert time"),
12011 Err(e) => assert!(
12012 e.to_string().contains("nothing to remove"),
12013 "expected a 'nothing to remove yet' error, got: {e}"
12014 ),
12015 }
12016 }
12017
12018 #[test]
12019 fn test_multilink_junction_info_disambiguates_self_referencing_through_type() {
12020 let schema = make_schema_with_through();
12027 let out = compile_and_emit_with(
12028 "UPDATE Person FILTER .id = $id SET { friends += (SELECT Person FILTER .id = $fid) }",
12029 &schema,
12030 );
12031 assert!(
12032 out.sql.contains("(\"person\", \"friend\")"),
12033 "expected two distinct FK columns:\n{}",
12034 out.sql
12035 );
12036 assert!(
12037 !out.sql.contains("(\"person\", \"person\")"),
12038 "source/target collapsed to the same column:\n{}",
12039 out.sql
12040 );
12041 }
12042
12043 #[test]
12044 fn test_shape_descriptor_scalars() {
12045 let out = compile_and_emit("SELECT Person { name, age }");
12046 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
12047 panic!()
12048 };
12049 assert_eq!(pointers.len(), 4); assert!(matches!(&pointers[0], ShapeNode::Scalar { name, position: 0 } if name == "__type__"));
12051 assert!(matches!(&pointers[1], ShapeNode::Scalar { name, position: 1 } if name == "id"));
12052 assert!(matches!(&pointers[2], ShapeNode::Scalar { name, position: 2 } if name == "name"));
12053 assert!(matches!(&pointers[3], ShapeNode::Scalar { name, position: 3 } if name == "age"));
12054 }
12055
12056 #[test]
12057 fn test_shape_descriptor_multi_link() {
12058 let out = compile_and_emit("SELECT Person { name, posts { title } }");
12059 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
12060 panic!()
12061 };
12062 assert_eq!(pointers.len(), 4);
12064 let ShapeNode::Array {
12065 name,
12066 position,
12067 element,
12068 } = &pointers[3]
12069 else {
12070 panic!()
12071 };
12072 assert_eq!(name, "posts");
12073 assert_eq!(*position, 3);
12074 let ShapeNode::Object {
12075 pointers: elem_pointers,
12076 ..
12077 } = element.as_ref()
12078 else {
12079 panic!()
12080 };
12081 assert_eq!(elem_pointers.len(), 2);
12084 }
12085
12086 #[test]
12087 fn test_select_order_by_limit() {
12088 let out = compile_and_emit("SELECT Person { name } ORDER BY .name ASC LIMIT 10");
12089 assert!(out.sql.contains("ORDER BY"));
12090 assert!(out.sql.contains("LIMIT 10"));
12091 }
12092
12093 #[test]
12094 fn test_insert_returning() {
12095 let out = compile_and_emit("INSERT Person { name := 'Alice', age := 30 }");
12096 assert!(out.sql.contains("INSERT INTO \"public\".\"Person\""));
12097 assert!(out.sql.contains("RETURNING"));
12098 assert!(out.sql.contains("'default::Person'::text"));
12099 assert!(out.sql.contains(") AS result"));
12100 let ShapeNode::Object {
12102 cardinality, pointers, ..
12103 } = &out.shape.root
12104 else {
12105 panic!()
12106 };
12107 assert_eq!(*cardinality, Cardinality::Required);
12108 assert!(
12110 pointers
12111 .iter()
12112 .any(|f| matches!(f, ShapeNode::Scalar { name, .. } if name == "id"))
12113 );
12114 assert!(
12115 !pointers
12116 .iter()
12117 .any(|f| matches!(f, ShapeNode::Scalar { name, .. } if name == "name"))
12118 );
12119 }
12120
12121 #[test]
12122 fn test_update_returning() {
12123 let out = compile_and_emit("UPDATE Person FILTER .name = $name SET { age := 31 }");
12124 assert!(out.sql.contains("UPDATE \"public\".\"Person\""));
12125 assert!(out.sql.contains("SET"));
12126 assert!(out.sql.contains("RETURNING"));
12128 assert!(out.sql.contains("'default::Person'::text"));
12129 assert!(!out.sql.contains("\"name\"::text"), "bare UPDATE must not return name");
12130 }
12131
12132 #[test]
12133 fn test_update_set_tuple_param_cast_uses_direct_jsonb_cast_not_to_jsonb() {
12134 let out = compile_and_emit("UPDATE Person FILTER .id = $id SET { age := <tuple<x: float64>>$val }");
12141 assert!(
12142 out.sql.contains(")::jsonb"),
12143 "expected a direct ::jsonb cast, got:\n{}",
12144 out.sql
12145 );
12146 assert!(
12147 !out.sql.contains("to_jsonb($"),
12148 "must not pass a bare param straight into to_jsonb(): got:\n{}",
12149 out.sql
12150 );
12151 }
12152
12153 #[test]
12154 fn test_empty_set_cast_to_object_type_clears_optional_link() {
12155 let out = compile_and_emit("UPDATE Person FILTER .id = $id SET { company := <default::Company>{} }");
12164 assert!(out.sql.contains("\"company_id\" = NULL"), "got:\n{}", out.sql);
12165 }
12166
12167 #[test]
12168 fn test_an_aggregate_over_a_set_literal_inside_a_shape() {
12169 let out = compile_and_emit("UPDATE Person FILTER .id = $id SET { age := min({<int64>100, .age}) }");
12175 assert!(out.sql.contains("min(v)"), "got:\n{}", out.sql);
12176 assert!(
12177 out.sql.contains("UNION ALL"),
12178 "both operands must reach the aggregate:\n{}",
12179 out.sql
12180 );
12181 }
12182
12183 #[test]
12184 fn test_an_aggregate_over_a_set_literal_reads_the_subject() {
12185 let out = compile_and_emit("SELECT Person { capped := min({<int64>100, .age}) }");
12188 assert!(out.sql.contains("min(v)"), "got:\n{}", out.sql);
12189 assert!(
12190 out.sql.contains("\"age\""),
12191 "the relative path must reach the column:\n{}",
12192 out.sql
12193 );
12194 }
12195
12196 #[test]
12197 fn test_an_aggregate_over_a_union_inside_a_shape() {
12198 let out = compile_and_emit("SELECT Person { capped := min((<int64>100 union .age)) }");
12201 assert!(out.sql.contains("min(v)"), "got:\n{}", out.sql);
12202 assert!(
12203 out.sql.contains("UNION ALL"),
12204 "both arms must reach the aggregate:\n{}",
12205 out.sql
12206 );
12207 }
12208
12209 #[test]
12210 fn test_an_aggregate_over_a_union_of_three_arms_flattens() {
12211 let out = compile_and_emit("SELECT Person { capped := min((<int64>1 union <int64>2 union .age)) }");
12212 assert_eq!(
12213 out.sql.matches("UNION ALL").count(),
12214 2,
12215 "three arms means two joins:\n{}",
12216 out.sql
12217 );
12218 }
12219
12220 #[test]
12221 fn test_an_aggregate_over_an_empty_set_emits_no_row_source() {
12222 for query in ["SELECT Person { capped := min({}) }", "SELECT min({})"] {
12226 let ast = parse::parse(query).expect("parse failed");
12227 let sql = ir::compile(&ast, &make_schema())
12228 .map(|ir| emit(&ir).sql)
12229 .unwrap_or_default();
12230 assert!(
12231 !sql.contains("FROM () AS"),
12232 "{query} emitted an empty row source:\n{sql}"
12233 );
12234 }
12235 }
12236
12237 #[test]
12238 fn test_delete_returning() {
12239 let out = compile_and_emit("DELETE Person FILTER .id = $id");
12240 assert!(out.sql.contains("DELETE FROM \"public\".\"Person\""));
12241 assert!(out.sql.contains("RETURNING"));
12243 assert!(out.sql.contains("'default::Person'::text"));
12244 assert!(!out.sql.contains("\"name\"::text"), "bare DELETE must not return name");
12245 }
12246
12247 #[test]
12248 fn test_select_over_insert() {
12249 let out = compile_and_emit("SELECT (INSERT Person { name := $name, age := $age }) { id, name }");
12250 assert!(out.sql.contains("WITH\n\"_dml\" AS ("));
12252 assert!(out.sql.contains("INSERT INTO"));
12253 assert!(out.sql.contains("RETURNING *"));
12254 assert!(out.sql.contains("'default::Person'::text"));
12256 assert!(out.sql.contains("\"name\"::text"));
12257 }
12258
12259 #[test]
12260 fn test_select_over_update() {
12261 let out = compile_and_emit("SELECT (UPDATE Person FILTER .id = $id SET { name := $name }) { id, name }");
12262 assert!(out.sql.contains("WITH\n\"_dml\" AS ("));
12263 assert!(out.sql.contains("UPDATE"));
12264 assert!(out.sql.contains("RETURNING \"t1\".*"), "{}", out.sql);
12265 assert!(out.sql.contains("\"name\"::text"));
12266 }
12267
12268 #[test]
12269 fn test_insert_user_specified_id_denied_by_default() {
12270 let schema = make_schema();
12271 let ast = parse::parse("INSERT Person { id := <uuid>$id, name := $name, age := $age }").unwrap();
12272 match ir::compile_with_config(&ast, &schema, &ir::SessionConfig::default()) {
12273 Err(err) => assert!(err.to_string().contains("cannot assign to property 'id'"), "got: {err}"),
12274 Ok(_) => panic!("expected id assignment to be denied by default"),
12275 }
12276 }
12277
12278 #[test]
12279 fn test_insert_user_specified_id_allowed_when_configured() {
12280 let schema = make_schema();
12281 let ast = parse::parse("INSERT Person { id := <uuid>$id, name := $name, age := $age }").unwrap();
12282 let config = ir::SessionConfig {
12283 allow_user_specified_id: true,
12284 };
12285 let ir_out = ir::compile_with_config(&ast, &schema, &config)
12286 .expect("expected id assignment to be allowed with allow_user_specified_id");
12287 let out = emit(&ir_out);
12288 assert!(out.sql.contains("INSERT INTO"));
12289 }
12290
12291 #[test]
12292 fn test_update_user_specified_id_denied_even_when_configured() {
12293 let schema = make_schema();
12294 let ast = parse::parse("UPDATE Person FILTER .name = $name SET { id := <uuid>$id }").unwrap();
12295 let config = ir::SessionConfig {
12296 allow_user_specified_id: true,
12297 };
12298 match ir::compile_with_config(&ast, &schema, &config) {
12299 Err(err) => assert!(err.to_string().contains("cannot assign to property 'id'"), "got: {err}"),
12300 Ok(_) => panic!("expected UPDATE to always deny reassigning id"),
12301 }
12302 }
12303
12304 #[test]
12305 fn test_select_over_update_multilink_only() {
12306 let out = compile_and_emit(
12316 "SELECT (UPDATE Person FILTER .id = $id SET { posts += (SELECT Post FILTER .title = $title) }) { id, name }",
12317 );
12318 assert!(
12319 out.sql.contains("\"_dml__ml_add_0\""),
12320 "missing junction-append CTE:\n{}",
12321 out.sql
12322 );
12323 assert!(out.sql.contains("INSERT INTO"), "missing junction INSERT:\n{}", out.sql);
12324 assert!(
12327 out.sql.contains("\"_dml__ids\" AS (\nSELECT"),
12328 "expected SELECT-based _ids CTE:\n{}",
12329 out.sql
12330 );
12331 assert!(
12332 !out.sql.contains("SET\n\nWHERE") && !out.sql.contains("SET \nWHERE"),
12333 "empty SET clause regression:\n{}",
12334 out.sql
12335 );
12336 assert_eq!(
12339 out.sql.matches("WITH\n").count(),
12340 1,
12341 "junction CTE must not be nested in a second WITH:\n{}",
12342 out.sql
12343 );
12344 assert!(
12346 out.sql.contains("\"_dml\" AS (\n SELECT * FROM \"_dml__ids\"\n)"),
12347 "missing _dml passthrough:\n{}",
12348 out.sql
12349 );
12350 }
12351
12352 #[test]
12353 fn test_select_over_update_scalar_and_multilink() {
12354 let out = compile_and_emit(
12358 "SELECT (UPDATE Person FILTER .id = $id SET { name := $name, posts += (SELECT Post FILTER .title = $title) }) { id, name }",
12359 );
12360 assert!(
12361 out.sql.contains("\"_dml__ml_add_0\""),
12362 "missing junction-append CTE:\n{}",
12363 out.sql
12364 );
12365 assert!(
12366 out.sql.contains("\"_dml__ids\" AS (\nUPDATE"),
12367 "expected UPDATE-based _ids CTE:\n{}",
12368 out.sql
12369 );
12370 assert!(
12371 out.sql.contains("\"name\" = "),
12372 "missing scalar SET assignment:\n{}",
12373 out.sql
12374 );
12375 assert_eq!(
12376 out.sql.matches("WITH\n").count(),
12377 1,
12378 "junction CTE must not be nested in a second WITH:\n{}",
12379 out.sql
12380 );
12381 }
12382
12383 #[test]
12384 fn test_with_bound_insert_and_multilink_update_forward_ref() {
12385 let out = compile_and_emit(
12393 "with insert0 := (insert Post { title := $title }), update0 := (update Person filter .id = $id set { posts += (select insert0) }) select { insert0, update0 }",
12394 );
12395 assert!(
12396 out.sql.contains("\"insert0\" AS (\n INSERT INTO"),
12397 "missing insert0 CTE:\n{}",
12398 out.sql
12399 );
12400 assert!(
12401 out.sql.contains("\"update0__ml_add_0\""),
12402 "missing junction-append CTE for update0:\n{}",
12403 out.sql
12404 );
12405 assert!(
12406 out.sql.contains("\"update0__ids\" AS (\nSELECT"),
12407 "expected SELECT-based update0 ids CTE (no scalar changes):\n{}",
12408 out.sql
12409 );
12410 assert!(
12411 out.sql
12412 .contains("\"update0\" AS (\n SELECT * FROM \"update0__ids\"\n)"),
12413 "missing update0 passthrough:\n{}",
12414 out.sql
12415 );
12416 assert_eq!(
12419 out.sql.matches("WITH\n").count(),
12420 1,
12421 "must be a single flat top-level WITH block:\n{}",
12422 out.sql
12423 );
12424 }
12425
12426 #[test]
12427 fn test_select_over_delete() {
12428 let out = compile_and_emit("SELECT (DELETE Person FILTER .id = $id) { id, name }");
12429 assert!(out.sql.contains("WITH\n\"_dml\" AS ("));
12430 assert!(out.sql.contains("DELETE FROM"));
12431 assert!(out.sql.contains("RETURNING *"));
12432 assert!(out.sql.contains("\"name\"::text"));
12433 }
12434
12435 fn make_schema_with_rewrite() -> SchemaDescriptor {
12436 use crate::schema::RewriteEntry;
12437 let mut schema = make_schema();
12438 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
12440 person.properties.push(PropertyDescriptor {
12441 name: "slug".into(),
12442 pg_type: "text".into(),
12443 nullable: true,
12444 default_sql: None,
12445 default_pyql: None,
12446 description: None,
12447 check_constraints: vec![],
12448 is_exclusive: false,
12449 is_pk: false,
12450 is_readonly: false,
12451 rewrites: vec![
12452 RewriteEntry {
12453 on: 1,
12454 handler: "str_lower(.name)".into(),
12455 }, RewriteEntry {
12457 on: 2,
12458 handler: "str_lower(.name)".into(),
12459 }, ],
12461 tuple_members: None,
12462 column_type: None,
12463 });
12464 schema
12465 }
12466
12467 #[test]
12468 fn a_link_assigned_from_a_select_keeps_its_clauses() {
12469 let out = compile_and_emit(
12470 "INSERT Person { name := 'a', age := 1, company := (SELECT Company ORDER BY .name LIMIT 1) }",
12471 );
12472 assert!(
12473 out.sql.contains("ORDER BY") && out.sql.contains("LIMIT 1"),
12474 "{}",
12475 out.sql
12476 );
12477 }
12478
12479 #[test]
12480 fn a_rewrite_leaves_the_statement_alone() {
12481 let schema = make_schema_with_rewrite();
12483 for query in [
12484 "INSERT Person { name := $name, age := 30 }",
12485 "UPDATE Person FILTER .id = $id SET { name := $name }",
12486 ] {
12487 let out = compile_and_emit_with(query, &schema);
12488 assert!(!out.sql.contains("\"slug\""), "{}", out.sql);
12489 }
12490 }
12491
12492 #[test]
12493 fn a_rewrite_reads_the_row_being_written() {
12494 let schema = make_schema_with_rewrite();
12495 let assignments = crate::ir::compile_rewrite_assignments("default::Person", 1, &schema).unwrap();
12496 let [assignment] = assignments.as_slice() else {
12497 panic!("one rewrite on insert")
12498 };
12499 assert_eq!(assignment.column, "slug");
12500 assert!(assignment.sql.contains("NEW.\"name\""), "{}", assignment.sql);
12501 }
12502
12503 #[test]
12504 fn rewrites_become_before_triggers() {
12505 let ddl = crate::export::export_schema(&make_schema_with_rewrite()).unwrap();
12506 for event in ["BEFORE INSERT", "BEFORE UPDATE"] {
12507 assert!(
12508 ddl.contains(&format!("{event} ON \"public\".\"Person\"")),
12509 "{event}:\n{ddl}"
12510 );
12511 }
12512 assert!(ddl.contains("NEW.\"slug\" := _pylon_rewrites.\"v0\";"), "{ddl}");
12513 }
12514
12515 #[test]
12516 fn a_rewrite_walking_a_link_starts_from_the_new_row() {
12517 use crate::schema::RewriteEntry;
12519 let mut schema = make_schema_with_rewrite();
12520 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
12521 let slug = person.properties.iter_mut().find(|p| p.name == "slug").unwrap();
12522 slug.rewrites = vec![RewriteEntry {
12523 on: 1,
12524 handler: "<str>(.company is Company) ++ .company.name".into(),
12525 }];
12526 let assignments = crate::ir::compile_rewrite_assignments("default::Person", 1, &schema).unwrap();
12527 let sql = &assignments[0].sql;
12528 assert!(!sql.contains("\"public\".\"Person\""), "{sql}");
12529 assert!(
12530 sql.contains("(SELECT (NEW).*)") && sql.contains("NEW.\"company_id\""),
12531 "{sql}"
12532 );
12533 }
12534
12535 #[test]
12536 fn test_unless_conflict_do_nothing() {
12537 let out = compile_and_emit("INSERT Person { name := $name } UNLESS CONFLICT");
12538 assert!(out.sql.contains("ON CONFLICT DO NOTHING"));
12539 }
12540
12541 #[test]
12542 fn test_unless_conflict_on_do_nothing() {
12543 let out = compile_and_emit("INSERT Person { name := $name } UNLESS CONFLICT ON .name");
12544 assert!(out.sql.contains("ON CONFLICT (\"name\") DO NOTHING"));
12545 }
12546
12547 #[test]
12548 fn test_unless_conflict_do_update() {
12549 let out = compile_and_emit(
12550 "INSERT Person { name := $name, age := $age } \
12551 UNLESS CONFLICT ON .name \
12552 ELSE (UPDATE Person SET { age := $age })",
12553 );
12554 assert!(out.sql.contains("ON CONFLICT (\"name\") DO UPDATE SET"));
12555 assert!(out.sql.contains("\"age\" = $2"));
12556 assert!(!out.sql.contains("DO NOTHING"));
12558 }
12559
12560 #[test]
12561 fn test_unless_conflict_else_update_self_reference_is_qualified() {
12562 let out = compile_and_emit(
12570 "INSERT Person { name := $name, age := 0 } \
12571 UNLESS CONFLICT ON .name \
12572 ELSE (UPDATE Person SET { age := .age + 1 })",
12573 );
12574 assert!(
12575 out.sql.contains("\"Person\".\"age\""),
12576 "self-reference must be qualified with the table's own name, got:\n{}",
12577 out.sql
12578 );
12579 assert!(
12580 !out.sql.contains("SET \"age\" = (\"age\""),
12581 "must not emit an unqualified (ambiguous) self-reference, got:\n{}",
12582 out.sql
12583 );
12584 }
12585
12586 #[test]
12587 fn test_unless_conflict_do_update_no_on() {
12588 let out = compile_and_emit(
12589 "INSERT Person { name := $name } \
12590 UNLESS CONFLICT \
12591 ELSE (UPDATE Person SET { age := 0 })",
12592 );
12593 assert!(out.sql.contains("ON CONFLICT DO UPDATE SET"));
12594 assert!(out.sql.contains("\"age\" = 0"));
12595 }
12596
12597 #[test]
12598 fn test_select_over_select() {
12599 let out = compile_and_emit("SELECT (SELECT Person FILTER .age > 18) { name }");
12600 assert!(out.sql.contains("WITH\n\"_dml\" AS ("));
12602 assert!(out.sql.contains("SELECT *"));
12604 assert!(out.sql.contains("FROM \"public\".\"Person\""));
12605 assert!(out.sql.contains("WHERE"));
12607 assert!(out.sql.contains("'default::Person'::text"));
12609 assert!(out.sql.contains("\"name\"::text"));
12610 }
12611
12612 #[test]
12613 fn test_select_over_select_with_outer_filter() {
12614 let out = compile_and_emit("SELECT (SELECT Person FILTER .age > 18) { name } FILTER .name = $name");
12615 assert!(out.sql.contains("WITH\n\"_dml\" AS ("));
12616 assert!(out.sql.contains("SELECT *"));
12617 assert_eq!(out.sql.matches("WHERE").count(), 2);
12619 assert!(out.sql.contains("$1"));
12620 }
12621
12622 #[test]
12623 fn test_insert_link_subquery() {
12624 let out = compile_and_emit("INSERT Person { name := $name, company := (SELECT Company FILTER .name = $co) }");
12625 assert!(out.sql.contains("\"company_id\""));
12627 assert!(out.sql.contains("SELECT"));
12628 assert!(out.sql.contains("\"id\""));
12630 assert!(out.sql.contains("FROM \"public\".\"Company\""));
12631 assert!(out.sql.contains("$2")); }
12634
12635 #[test]
12636 fn test_update_link_subquery() {
12637 let out =
12638 compile_and_emit("UPDATE Person FILTER .id = $id SET { company := (SELECT Company FILTER .name = $co) }");
12639 assert!(out.sql.contains("\"company_id\""));
12640 assert!(out.sql.contains("SELECT"));
12641 assert!(out.sql.contains("FROM \"public\".\"Company\""));
12642 }
12643
12644 #[test]
12645 fn test_insert_link_value_from_nested_insert_hoists_a_with_cte() {
12646 let out = compile_and_emit(
12654 "INSERT Person { name := 'Alice', company := (select (insert Company { name := 'Acme' }) { id }) }",
12655 );
12656 assert!(
12657 out.sql.starts_with("WITH"),
12658 "expected a WITH-hoisted CTE, got:\n{}",
12659 out.sql
12660 );
12661 assert!(
12662 out.sql.contains("INSERT INTO \"public\".\"Company\""),
12663 "expected the nested insert to be its own CTE, got:\n{}",
12664 out.sql,
12665 );
12666 assert!(
12669 out.sql.contains("\"company_id\") SELECT") && out.sql.contains(".\"id\" FROM"),
12670 "expected the outer insert to switch from VALUES to SELECT ... FROM <cte>, got:\n{}",
12671 out.sql,
12672 );
12673 assert!(
12674 !out.sql.contains("FROM \"public\".\"Company\" AS"),
12675 "must not select from the real Company table, got:\n{}",
12676 out.sql
12677 );
12678 }
12679
12680 #[test]
12681 fn test_update_link_value_from_nested_insert_hoists_a_with_cte_and_adds_from_clause() {
12682 let out = compile_and_emit(
12683 "UPDATE Person FILTER .id = $id SET { company := (select (insert Company { name := 'Acme' }) { id }) }",
12684 );
12685 assert!(
12686 out.sql.starts_with("WITH"),
12687 "expected a WITH-hoisted CTE, got:\n{}",
12688 out.sql
12689 );
12690 assert!(
12691 out.sql.contains("INSERT INTO \"public\".\"Company\""),
12692 "expected the nested insert to be its own CTE, got:\n{}",
12693 out.sql,
12694 );
12695 assert!(
12698 out.sql.contains("\nFROM \""),
12699 "expected a FROM clause referencing the nested CTE, got:\n{}",
12700 out.sql
12701 );
12702 assert!(out.sql.contains("SET \"company_id\" = "), "got:\n{}", out.sql);
12703 }
12704
12705 #[test]
12706 fn test_update_link_value_from_nested_insert_combines_with_multilink_mutation() {
12707 let out = compile_and_emit(
12712 "UPDATE Person FILTER .id = $id SET { \
12713 company := (select (insert Company { name := 'Acme' }) { id }), \
12714 posts += (SELECT Post FILTER .title = $t) \
12715 }",
12716 );
12717 assert!(out.sql.starts_with("WITH"), "got:\n{}", out.sql);
12718 assert!(
12719 out.sql.contains("INSERT INTO \"public\".\"Company\""),
12720 "expected the nested insert to be its own CTE, got:\n{}",
12721 out.sql,
12722 );
12723 assert!(
12724 out.sql.contains("\"_ids\" AS (\nUPDATE") && out.sql.contains("\nFROM \""),
12725 "expected the _ids UPDATE to gain a FROM clause referencing the nested CTE, got:\n{}",
12726 out.sql,
12727 );
12728 assert!(
12729 out.sql.contains("\"_ml_add_0\""),
12730 "expected the junction-append CTE to still be present, got:\n{}",
12731 out.sql
12732 );
12733 }
12734
12735 #[test]
12736 fn test_update_link_value_from_nested_insert_combines_with_interface_target() {
12737 fn id_prop() -> PropertyDescriptor {
12743 PropertyDescriptor {
12744 name: "id".into(),
12745 pg_type: "uuid".into(),
12746 nullable: false,
12747 default_sql: Some("uuidv7()".into()),
12748 default_pyql: None,
12749 description: None,
12750 check_constraints: vec![],
12751 is_exclusive: true,
12752 is_pk: true,
12753 is_readonly: true,
12754 rewrites: vec![],
12755 tuple_members: None,
12756 column_type: None,
12757 }
12758 }
12759 fn text_prop(name: &str) -> PropertyDescriptor {
12760 PropertyDescriptor {
12761 name: name.into(),
12762 pg_type: "text".into(),
12763 nullable: false,
12764 default_sql: None,
12765 default_pyql: None,
12766 description: None,
12767 check_constraints: vec![],
12768 is_exclusive: false,
12769 is_pk: false,
12770 is_readonly: false,
12771 rewrites: vec![],
12772 tuple_members: None,
12773 column_type: None,
12774 }
12775 }
12776 fn company_link() -> LinkDescriptor {
12777 LinkDescriptor {
12778 name: "company".into(),
12779 target: "default::Company".into(),
12780 nullable: true,
12781 through: None,
12782 description: None,
12783 default_pyql: None,
12784 is_exclusive: false,
12785 is_readonly: false,
12786 rewrites: vec![],
12787 on_delete: vec![],
12788 }
12789 }
12790 let schema = SchemaDescriptor {
12791 types: vec![
12792 TypeDescriptor {
12793 name: "Company".into(),
12794 module: "default".into(),
12795 table: "Company".into(),
12796 abstract_: false,
12797 materialized: true,
12798 description: None,
12799 parents: vec![],
12800 interfaces: vec![],
12801 bases: vec![],
12802 properties: vec![id_prop(), text_prop("name")],
12803 links: vec![],
12804 multilinks: vec![],
12805 computed: vec![],
12806 constraints: vec![],
12807 indexes: vec![],
12808 partition: None,
12809 vector_indexes: vec![],
12810 search_indexes: vec![],
12811 triggers: vec![],
12812 junction: false,
12813 signals: vec![],
12814 },
12815 TypeDescriptor {
12816 name: "Account".into(),
12817 module: "default".into(),
12818 table: "Account".into(),
12819 abstract_: true,
12820 materialized: true,
12821 description: None,
12822 parents: vec![],
12823 interfaces: vec![],
12824 bases: vec![],
12825 properties: vec![id_prop(), text_prop("email")],
12826 links: vec![company_link()],
12827 multilinks: vec![],
12828 computed: vec![],
12829 constraints: vec![],
12830 indexes: vec![],
12831 partition: None,
12832 vector_indexes: vec![],
12833 search_indexes: vec![],
12834 triggers: vec![],
12835 junction: false,
12836 signals: vec![],
12837 },
12838 TypeDescriptor {
12839 name: "Individual".into(),
12840 module: "default".into(),
12841 table: "Individual".into(),
12842 abstract_: false,
12843 materialized: true,
12844 description: None,
12845 parents: vec![],
12846 interfaces: vec!["default::Account".into()],
12847 bases: vec![],
12848 properties: vec![id_prop(), text_prop("email"), text_prop("first_name")],
12849 links: vec![company_link()],
12850 multilinks: vec![],
12851 computed: vec![],
12852 constraints: vec![],
12853 indexes: vec![],
12854 partition: None,
12855 vector_indexes: vec![],
12856 search_indexes: vec![],
12857 triggers: vec![],
12858 junction: false,
12859 signals: vec![],
12860 },
12861 ],
12862 scalars: vec![],
12863 enums: vec![],
12864 named_tuples: vec![],
12865 globals: vec![],
12866 functions: vec![],
12867 aliases: vec![],
12868 channels: vec![],
12869 ..Default::default()
12870 };
12871 let out = compile_and_emit_with(
12872 "UPDATE Account FILTER .email = $email \
12873 SET { company := (select (insert Company { name := 'Acme' }) { id }) }",
12874 &schema,
12875 );
12876 assert!(out.sql.starts_with("WITH"), "got:\n{}", out.sql);
12877 assert!(
12878 out.sql.contains("INSERT INTO \"public\".\"Company\""),
12879 "expected the nested insert to be its own CTE, got:\n{}",
12880 out.sql,
12881 );
12882 assert!(
12883 out.sql.contains("UPDATE \"public\".\"Individual\"") && out.sql.contains("\nFROM \"_nested_dml_0\""),
12884 "expected the per-implementor UPDATE to gain a FROM clause referencing the nested CTE, got:\n{}",
12885 out.sql,
12886 );
12887 }
12888
12889 #[test]
12890 fn test_a_backlink_narrowed_to_a_supertype() {
12891 let mut schema = interface_link_schema();
12895 schema
12898 .types
12899 .iter_mut()
12900 .find(|t| t.name == "Account")
12901 .expect("the helper declares Account")
12902 .links
12903 .clear();
12904 let out = compile_and_emit_with("SELECT Company { owners := .<company[is Account] { email } }", &schema);
12905 assert!(
12906 out.sql.contains("\"public\".\"Individual\""),
12907 "the backlink must read the type that declares it:\n{}",
12908 out.sql
12909 );
12910 }
12911
12912 #[test]
12913 fn a_path_through_a_backlink_narrowed_to_a_supertype() {
12914 let mut schema = interface_link_schema();
12915 schema
12916 .types
12917 .iter_mut()
12918 .find(|t| t.name == "Account")
12919 .expect("the helper declares Account")
12920 .links
12921 .clear();
12922 let out = compile_and_emit_with(
12923 "SELECT Company { name } FILTER EXISTS (SELECT .<company[is Account])",
12924 &schema,
12925 );
12926 assert!(out.sql.contains("\"public\".\"Individual\""), "{}", out.sql);
12927 }
12928
12929 fn interface_link_schema() -> SchemaDescriptor {
12933 fn id_prop() -> PropertyDescriptor {
12939 PropertyDescriptor {
12940 name: "id".into(),
12941 pg_type: "uuid".into(),
12942 nullable: false,
12943 default_sql: None,
12944 default_pyql: None,
12945 description: None,
12946 check_constraints: vec![],
12947 is_exclusive: true,
12948 is_pk: true,
12949 is_readonly: true,
12950 rewrites: vec![],
12951 tuple_members: None,
12952 column_type: None,
12953 }
12954 }
12955 fn text_prop(name: &str) -> PropertyDescriptor {
12956 PropertyDescriptor {
12957 name: name.into(),
12958 pg_type: "text".into(),
12959 nullable: false,
12960 default_sql: None,
12961 default_pyql: None,
12962 description: None,
12963 check_constraints: vec![],
12964 is_exclusive: false,
12965 is_pk: false,
12966 is_readonly: false,
12967 rewrites: vec![],
12968 tuple_members: None,
12969 column_type: None,
12970 }
12971 }
12972 fn link(name: &str, target: &str) -> LinkDescriptor {
12973 LinkDescriptor {
12974 name: name.into(),
12975 target: target.into(),
12976 nullable: true,
12977 through: None,
12978 description: None,
12979 default_pyql: None,
12980 is_exclusive: false,
12981 is_readonly: false,
12982 rewrites: vec![],
12983 on_delete: vec![],
12984 }
12985 }
12986 fn ty(
12987 name: &str,
12988 abstract_: bool,
12989 interfaces: Vec<String>,
12990 properties: Vec<PropertyDescriptor>,
12991 links: Vec<LinkDescriptor>,
12992 ) -> TypeDescriptor {
12993 TypeDescriptor {
12994 name: name.into(),
12995 module: "default".into(),
12996 table: name.into(),
12997 abstract_,
12998 materialized: true,
12999 description: None,
13000 parents: vec![],
13001 interfaces,
13002 bases: vec![],
13003 properties,
13004 links,
13005 multilinks: vec![],
13006 computed: vec![],
13007 constraints: vec![],
13008 indexes: vec![],
13009 partition: None,
13010 vector_indexes: vec![],
13011 search_indexes: vec![],
13012 triggers: vec![],
13013 junction: false,
13014 signals: vec![],
13015 }
13016 }
13017 SchemaDescriptor {
13018 types: vec![
13019 ty("Company", false, vec![], vec![id_prop(), text_prop("name")], vec![]),
13020 ty(
13021 "Account",
13022 true,
13023 vec![],
13024 vec![id_prop(), text_prop("email")],
13025 vec![link("company", "default::Company")],
13026 ),
13027 ty(
13028 "Individual",
13029 false,
13030 vec!["default::Account".into()],
13031 vec![id_prop(), text_prop("email")],
13032 vec![link("company", "default::Company")],
13033 ),
13034 ty(
13035 "Token",
13036 false,
13037 vec![],
13038 vec![id_prop(), text_prop("value")],
13039 vec![link("account", "default::Account")],
13040 ),
13041 ],
13042 scalars: vec![],
13043 enums: vec![],
13044 named_tuples: vec![],
13045 globals: vec![],
13046 functions: vec![],
13047 aliases: vec![],
13048 channels: vec![],
13049 ..Default::default()
13050 }
13051 }
13052
13053 fn two_hop_interface_schema() -> SchemaDescriptor {
13057 let mut schema = interface_link_schema();
13058 let token = schema
13059 .types
13060 .iter()
13061 .find(|t| t.name == "Token")
13062 .expect("the helper declares Token")
13063 .clone();
13064 let mut connector = token.clone();
13065 connector.name = "Connector".into();
13066 connector.table = "Connector".into();
13067 connector.links[0].name = "provider".into();
13068 let mut installation = token;
13069 installation.name = "Installation".into();
13070 installation.table = "Installation".into();
13071 installation.links[0].name = "connector".into();
13072 installation.links[0].target = "default::Connector".into();
13073 schema.types.push(connector);
13074 schema.types.push(installation);
13075 schema
13076 }
13077
13078 #[test]
13079 fn a_narrowed_two_hop_walk_off_a_pinned_row_is_one_value() {
13080 let out = compile_and_emit_with(
13086 "SELECT { s := ((SELECT Installation FILTER .id = <uuid>$0).connector.provider[is Individual].email) ?? 'y' }",
13087 &two_hop_interface_schema(),
13088 );
13089 assert!(
13090 !out.sql.contains("ARRAY(SELECT"),
13091 "expected a value, not a set:\n{}",
13092 out.sql
13093 );
13094 assert!(
13095 out.sql.contains("COALESCE("),
13096 "the default must stay a scalar one:\n{}",
13097 out.sql
13098 );
13099 }
13100
13101 #[test]
13102 fn a_narrowed_two_hop_walk_off_an_unpinned_row_stays_a_set() {
13103 let out = compile_and_emit_with(
13104 "SELECT { s := (SELECT Installation FILTER .value = 'x').connector.provider[is Individual].email }",
13105 &two_hop_interface_schema(),
13106 );
13107 assert!(
13108 out.sql.contains("ARRAY(SELECT"),
13109 "a walk off many rows is a set:\n{}",
13110 out.sql
13111 );
13112 }
13113
13114 #[test]
13121 fn a_walk_ending_in_a_type_intersection_is_valid_in_a_filter() {
13122 let out = compile_and_emit_with(
13123 "SELECT Token { id } FILTER EXISTS .account[is Individual]",
13124 &interface_link_schema(),
13125 );
13126 assert!(
13127 out.sql.contains("\"public\".\"Individual\""),
13128 "the narrowing must reach the implementor's table:\n{}",
13129 out.sql
13130 );
13131 assert!(
13132 out.sql.contains("IS NOT NULL"),
13133 "exists over the narrowed walk is a nullness test:\n{}",
13134 out.sql
13135 );
13136 }
13137
13138 #[test]
13139 fn a_walk_ending_in_a_type_intersection_is_valid_off_a_binding() {
13140 let out = compile_and_emit_with(
13141 "WITH t := (SELECT Token LIMIT 1) SELECT { e := EXISTS t.account[is Individual] }",
13142 &interface_link_schema(),
13143 );
13144 assert!(
13145 out.sql.contains("\"public\".\"Individual\""),
13146 "the narrowing must reach the implementor's table:\n{}",
13147 out.sql
13148 );
13149 }
13150
13151 #[test]
13152 fn test_reading_a_link_through_an_interface_target() {
13153 let schema = interface_link_schema();
13154 let out = compile_and_emit_with("SELECT Token { account: { email, company: { name } } }", &schema);
13155 assert!(
13156 out.sql
13157 .contains("\"email\", \"company_id\" FROM \"public\".\"Individual\""),
13158 "the fanned-out interface must carry its own link columns:\n{}",
13159 out.sql
13160 );
13161 }
13162
13163 #[test]
13164 fn test_computed_pointer_in_shape_emits_expression() {
13165 let mut schema = make_schema();
13166 schema.types[0].computed.push(crate::schema::ComputedDescriptor {
13167 name: "upper_name".into(),
13168 expression: "str_upper(.name)".into(),
13169 return_type: Some("text".into()),
13170 link_target: None,
13171 link_multi: false,
13172 });
13173 let out = compile_and_emit_with("SELECT Person { upper_name }", &schema);
13174 assert!(
13175 out.sql.to_lowercase().contains("upper"),
13176 "expected upper() in SQL, got:\n{}",
13177 out.sql
13178 );
13179 }
13180
13181 fn make_interface_schema() -> SchemaDescriptor {
13184 fn id_prop() -> PropertyDescriptor {
13185 PropertyDescriptor {
13186 name: "id".into(),
13187 pg_type: "uuid".into(),
13188 nullable: false,
13189 default_sql: Some("uuidv7()".into()),
13190 default_pyql: None,
13191 description: None,
13192 check_constraints: vec![],
13193 is_exclusive: true,
13194 is_pk: true,
13195 is_readonly: true,
13196 rewrites: vec![],
13197 tuple_members: None,
13198 column_type: None,
13199 }
13200 }
13201 SchemaDescriptor {
13202 types: vec![
13203 TypeDescriptor {
13204 name: "Account".into(),
13205 module: "default".into(),
13206 table: "Account".into(),
13207 abstract_: true,
13208 materialized: false,
13209 description: None,
13210 parents: vec![],
13211 interfaces: vec![],
13212 bases: vec![],
13213 properties: vec![
13214 id_prop(),
13215 PropertyDescriptor {
13216 name: "email".into(),
13217 pg_type: "text".into(),
13218 nullable: false,
13219 default_sql: None,
13220 default_pyql: None,
13221 description: None,
13222 check_constraints: vec![],
13223 is_exclusive: false,
13224 is_pk: false,
13225 is_readonly: false,
13226 rewrites: vec![],
13227 tuple_members: None,
13228 column_type: None,
13229 },
13230 ],
13231 links: vec![],
13232 multilinks: vec![],
13233 computed: vec![],
13234 constraints: vec![],
13235 indexes: vec![],
13236 partition: None,
13237 vector_indexes: vec![],
13238 search_indexes: vec![],
13239 triggers: vec![],
13240 junction: false,
13241 signals: vec![],
13242 },
13243 TypeDescriptor {
13244 name: "Individual".into(),
13245 module: "default".into(),
13246 table: "Individual".into(),
13247 abstract_: false,
13248 materialized: true,
13249 description: None,
13250 parents: vec![],
13251 interfaces: vec!["default::Account".into()],
13252 bases: vec![],
13253 properties: vec![
13254 id_prop(),
13255 PropertyDescriptor {
13256 name: "first_name".into(),
13257 pg_type: "text".into(),
13258 nullable: false,
13259 default_sql: None,
13260 default_pyql: None,
13261 description: None,
13262 check_constraints: vec![],
13263 is_exclusive: false,
13264 is_pk: false,
13265 is_readonly: false,
13266 rewrites: vec![],
13267 tuple_members: None,
13268 column_type: None,
13269 },
13270 ],
13271 links: vec![],
13272 multilinks: vec![],
13273 computed: vec![crate::schema::ComputedDescriptor {
13274 name: "full_name".into(),
13275 expression: "str_upper(.first_name)".into(),
13276 return_type: Some("text".into()),
13277 link_target: None,
13278 link_multi: false,
13279 }],
13280 constraints: vec![],
13281 indexes: vec![],
13282 partition: None,
13283 vector_indexes: vec![],
13284 search_indexes: vec![],
13285 triggers: vec![],
13286 junction: false,
13287 signals: vec![],
13288 },
13289 ],
13290 scalars: vec![],
13291 enums: vec![],
13292 named_tuples: vec![],
13293 globals: vec![],
13294 functions: vec![],
13295 aliases: vec![],
13296 channels: vec![],
13297 ..Default::default()
13298 }
13299 }
13300
13301 #[test]
13302 fn test_type_intersection_splat_includes_concrete_computed_pointers() {
13303 let schema = make_interface_schema();
13308 let out = compile_and_emit_with("SELECT Account { *, [is Individual].* }", &schema);
13309 assert!(
13310 out.sql.to_lowercase().contains("upper"),
13311 "expected the concrete type's computed pointer (str_upper(...)) in the shape, got:\n{}",
13312 out.sql
13313 );
13314 assert!(
13315 out.sql.contains("\"first_name\""),
13316 "expected the concrete type's stored property too, got:\n{}",
13317 out.sql
13318 );
13319 }
13320
13321 #[test]
13322 fn test_enum_array_property_casts_to_text_array_and_shapes_as_a_list() {
13323 let mut schema = make_schema();
13330 schema.enums.push(crate::schema::EnumDescriptor {
13331 name: "Gender".into(),
13332 module: "default".into(),
13333 members: vec!["Male".into(), "Female".into()],
13334 });
13335 schema.types[0].properties.push(crate::schema::PropertyDescriptor {
13336 name: "genders".into(),
13337 pg_type: r#""default"."Gender"[]"#.into(),
13338 nullable: true,
13339 default_sql: None,
13340 default_pyql: None,
13341 description: None,
13342 check_constraints: vec![],
13343 is_exclusive: false,
13344 is_pk: false,
13345 is_readonly: false,
13346 rewrites: vec![],
13347 tuple_members: None,
13348 column_type: None,
13349 });
13350
13351 let out = compile_and_emit_with("SELECT Person { genders }", &schema);
13352 assert!(
13353 out.sql.contains(r#""genders"::text[]"#),
13354 "expected a text[] cast, got:\n{}",
13355 out.sql
13356 );
13357
13358 let crate::query::ShapeNode::Object { pointers, .. } = &out.shape.root else {
13359 panic!("expected Object shape, got {:?}", out.shape.root)
13360 };
13361 let genders = pointers
13362 .iter()
13363 .find(|node| matches!(node, crate::query::ShapeNode::Array { name, .. } if name == "genders"))
13364 .unwrap_or_else(|| panic!("expected an Array-shaped pointer, got {pointers:?}"));
13365 let crate::query::ShapeNode::Array { element, .. } = genders else {
13366 unreachable!()
13367 };
13368 assert!(
13369 matches!(
13370 element.as_ref(),
13371 crate::query::ShapeNode::Enum { enum_type, position: 0, .. }
13372 if enum_type == "default::Gender"
13373 ),
13374 "expected the elements to be enum-shaped, got {element:?}",
13375 );
13376 }
13377
13378 #[test]
13379 fn test_scalar_enum_property_still_casts_to_a_single_text() {
13380 let mut schema = make_schema();
13381 schema.enums.push(crate::schema::EnumDescriptor {
13382 name: "Gender".into(),
13383 module: "default".into(),
13384 members: vec!["Male".into(), "Female".into()],
13385 });
13386 schema.types[0].properties.push(crate::schema::PropertyDescriptor {
13387 name: "gender".into(),
13388 pg_type: r#""default"."Gender""#.into(),
13389 nullable: true,
13390 default_sql: None,
13391 default_pyql: None,
13392 description: None,
13393 check_constraints: vec![],
13394 is_exclusive: false,
13395 is_pk: false,
13396 is_readonly: false,
13397 rewrites: vec![],
13398 tuple_members: None,
13399 column_type: None,
13400 });
13401
13402 let out = compile_and_emit_with("SELECT Person { gender }", &schema);
13403 assert!(
13404 out.sql.contains(r#""gender"::text"#) && !out.sql.contains(r#""gender"::text[]"#),
13405 "expected a plain text cast, got:\n{}",
13406 out.sql
13407 );
13408 let crate::query::ShapeNode::Object { pointers, .. } = &out.shape.root else {
13409 panic!("expected Object shape")
13410 };
13411 assert!(
13412 pointers.iter().any(|node| matches!(
13413 node,
13414 crate::query::ShapeNode::Enum { name, enum_type, .. }
13415 if name == "gender" && enum_type == "default::Gender"
13416 )),
13417 "expected an Enum-shaped pointer, got {pointers:?}",
13418 );
13419 }
13420
13421 #[test]
13422 fn test_type_intersection_splat_keeps_enum_pointers_enum_shaped() {
13423 let mut schema = make_interface_schema();
13428 schema.enums.push(crate::schema::EnumDescriptor {
13429 name: "Gender".into(),
13430 module: "default".into(),
13431 members: vec!["Male".into(), "Female".into()],
13432 });
13433 let individual = schema
13436 .types
13437 .iter_mut()
13438 .find(|t| t.name == "Individual")
13439 .expect("the interface schema has an Individual type");
13440 individual.properties.push(crate::schema::PropertyDescriptor {
13441 name: "gender".into(),
13442 pg_type: r#""default"."Gender""#.into(),
13443 nullable: true,
13444 default_sql: None,
13445 default_pyql: None,
13446 description: None,
13447 check_constraints: vec![],
13448 is_exclusive: false,
13449 is_pk: false,
13450 is_readonly: false,
13451 rewrites: vec![],
13452 tuple_members: None,
13453 column_type: None,
13454 });
13455
13456 let out = compile_and_emit_with("SELECT Account { [is Individual].* }", &schema);
13457 let crate::query::ShapeNode::Object { pointers, .. } = &out.shape.root else {
13458 panic!("expected Object shape, got {:?}", out.shape.root)
13459 };
13460 assert!(
13461 pointers.iter().any(|node| matches!(
13462 node,
13463 crate::query::ShapeNode::Enum { name, enum_type, .. }
13464 if name == "gender" && enum_type == "default::Gender"
13465 )),
13466 "expected the enum property to stay enum-shaped through the intersection, got {pointers:?}",
13467 );
13468 }
13469
13470 #[test]
13471 fn test_shallow_splat_leaves_object_valued_computeds_to_the_deep_form() {
13472 let mut schema = make_schema();
13477 schema.types[0].computed.push(crate::schema::ComputedDescriptor {
13478 name: "authors".into(),
13479 expression: ".posts".into(),
13480 return_type: None,
13481 link_target: None,
13482 link_multi: false,
13483 });
13484 schema.types[0].computed.push(crate::schema::ComputedDescriptor {
13485 name: "age_next".into(),
13486 expression: ".age + 1".into(),
13487 return_type: Some("int8".into()),
13488 link_target: None,
13489 link_multi: false,
13490 });
13491
13492 let shallow = compile_and_emit_with("SELECT Person { * }", &schema);
13493 let crate::query::ShapeNode::Object { pointers, .. } = &shallow.shape.root else {
13494 panic!("expected Object shape")
13495 };
13496 let names: Vec<&str> = pointers.iter().map(shape_pointer_name).collect();
13497 assert!(
13498 names.contains(&"age_next"),
13499 "a computed property belongs in `*`: {names:?}"
13500 );
13501 assert!(!names.contains(&"authors"), "a computed link does not: {names:?}");
13502
13503 let deep = compile_and_emit_with("SELECT Person { ** }", &schema);
13504 let crate::query::ShapeNode::Object { pointers, .. } = &deep.shape.root else {
13505 panic!("expected Object shape")
13506 };
13507 let names: Vec<&str> = pointers.iter().map(shape_pointer_name).collect();
13508 assert!(
13509 names.contains(&"authors"),
13510 "`**` includes links, computed ones included: {names:?}"
13511 );
13512 }
13513
13514 fn shape_pointer_name(node: &crate::query::ShapeNode) -> &str {
13515 use crate::query::ShapeNode;
13516 match node {
13517 ShapeNode::Scalar { name, .. }
13518 | ShapeNode::Enum { name, .. }
13519 | ShapeNode::NamedTuple { name, .. }
13520 | ShapeNode::Object { name, .. }
13521 | ShapeNode::Array { name, .. } => name,
13522 _ => "",
13523 }
13524 }
13525
13526 #[test]
13527 fn test_any_over_a_multilink_comparison_does_not_warn() {
13528 let schema = make_schema();
13533 let ast = parse::parse("SELECT Person FILTER any(.posts.title = 'x')").unwrap();
13534 let ir = ir::compile(&ast, &schema).unwrap();
13535 assert!(
13536 !ir.warnings.iter().any(|w| w.contains("FILTER clause")),
13537 "any() states the intent, so nothing should warn: {:?}",
13538 ir.warnings
13539 );
13540 }
13541
13542 #[test]
13543 fn test_bare_multilink_comparison_still_warns() {
13544 let schema = make_schema();
13545 let ast = parse::parse("SELECT Person FILTER .posts.title = 'x'").unwrap();
13546 let ir = ir::compile(&ast, &schema).unwrap();
13547 assert!(
13548 ir.warnings.iter().any(|w| w.contains("FILTER clause")),
13549 "an unwrapped set-valued comparison still warns: {:?}",
13550 ir.warnings
13551 );
13552 }
13553
13554 #[test]
13555 fn test_link_to_an_interface_expands_over_its_implementors() {
13556 let mut schema = make_interface_schema();
13562 let account = schema
13563 .types
13564 .iter_mut()
13565 .find(|t| t.name == "Account")
13566 .expect("the interface schema has an Account type");
13567 account.materialized = true;
13568 let mut organization = schema
13570 .types
13571 .iter()
13572 .find(|t| t.name == "Individual")
13573 .expect("the interface schema has an Individual type")
13574 .clone();
13575 organization.name = "Organization".into();
13576 organization.table = "Organization".into();
13577 organization.computed.clear();
13578 schema.types.push(organization);
13579
13580 let owner = TypeDescriptor {
13581 name: "Note".into(),
13582 module: "default".into(),
13583 table: "Note".into(),
13584 abstract_: false,
13585 materialized: false,
13586 description: None,
13587 parents: vec![],
13588 interfaces: vec![],
13589 bases: vec![],
13590 properties: vec![],
13591 links: vec![LinkDescriptor {
13592 name: "owner".into(),
13593 target: "default::Account".into(),
13594 nullable: true,
13595 description: None,
13596 default_pyql: None,
13597 is_exclusive: false,
13598 is_readonly: false,
13599 rewrites: vec![],
13600 on_delete: vec![],
13601 through: None,
13602 }],
13603 multilinks: vec![],
13604 computed: vec![],
13605 constraints: vec![],
13606 indexes: vec![],
13607 partition: None,
13608 vector_indexes: vec![],
13609 search_indexes: vec![],
13610 triggers: vec![],
13611 junction: false,
13612 signals: vec![],
13613 };
13614 schema.types.push(owner);
13615
13616 let out = compile_and_emit_with("SELECT Note { owner: { id } }", &schema);
13617 assert!(
13618 out.sql.contains("UNION ALL"),
13619 "the link's target should expand over its implementors, got:\n{}",
13620 out.sql
13621 );
13622 assert!(
13623 out.sql.contains("'default::Individual'::text AS \"__type__\""),
13624 "each branch should carry its own discriminator, got:\n{}",
13625 out.sql
13626 );
13627 assert!(
13628 !out.sql.contains("FROM \"default\".\"Account\" AS"),
13629 "the interface's own view should no longer be read directly, got:\n{}",
13630 out.sql
13631 );
13632 }
13633
13634 fn make_two_implementor_schema() -> SchemaDescriptor {
13637 let mut schema = make_interface_schema();
13638 let mut organization = schema
13639 .types
13640 .iter()
13641 .find(|t| t.name == "Individual")
13642 .expect("the interface schema has an Individual type")
13643 .clone();
13644 organization.name = "Organization".into();
13645 organization.table = "Organization".into();
13646 organization.computed.clear();
13647 schema.types.push(organization);
13648 schema
13649 }
13650
13651 fn assert_rows_carry_their_concrete_type(query: &str) {
13654 let out = compile_and_emit_with(query, &make_two_implementor_schema());
13655 assert!(
13656 !out.sql.contains("'default::Account'::text"),
13657 "rows are labelled with the interface instead of their own type:\n{}",
13658 out.sql
13659 );
13660 assert!(
13661 out.sql.contains("'default::Individual'::text AS \"__type__\""),
13662 "the rows should come from the implementors, each tagging its own:\n{}",
13663 out.sql
13664 );
13665 }
13666
13667 #[test]
13668 fn an_aggregated_select_of_an_interface_carries_the_concrete_type() {
13669 assert_rows_carry_their_concrete_type("SELECT (xs := array_agg((SELECT Account { id })))");
13670 }
13671
13672 #[test]
13673 fn an_aggregated_binding_of_an_interface_carries_the_concrete_type() {
13674 assert_rows_carry_their_concrete_type("WITH a := (SELECT Account) SELECT (xs := array_agg((SELECT a { id })))");
13675 }
13676
13677 #[test]
13678 fn a_single_object_read_off_a_binding_carries_the_concrete_type() {
13679 assert_rows_carry_their_concrete_type("WITH a := (SELECT Account) SELECT (x := (SELECT a { id } LIMIT 1))");
13680 }
13681
13682 #[test]
13683 fn grouped_elements_of_an_interface_carry_the_concrete_type() {
13684 assert_rows_carry_their_concrete_type("group Account { id } by .email");
13685 }
13686
13687 fn make_note_schema() -> SchemaDescriptor {
13690 fn link_to(name: &str, target: &str) -> LinkDescriptor {
13691 LinkDescriptor {
13692 name: name.into(),
13693 target: target.into(),
13694 nullable: true,
13695 description: None,
13696 default_pyql: None,
13697 is_exclusive: false,
13698 is_readonly: false,
13699 rewrites: vec![],
13700 on_delete: vec![],
13701 through: None,
13702 }
13703 }
13704 let mut schema = make_two_implementor_schema();
13705 for account in schema
13706 .types
13707 .iter_mut()
13708 .filter(|t| ["Account", "Individual", "Organization"].contains(&t.name.as_str()))
13709 {
13710 account.links.push(link_to("pinned", "default::Note"));
13711 }
13712 schema.types.push(TypeDescriptor {
13713 name: "Note".into(),
13714 module: "default".into(),
13715 table: "Note".into(),
13716 abstract_: false,
13717 materialized: false,
13718 description: None,
13719 parents: vec![],
13720 interfaces: vec![],
13721 bases: vec![],
13722 properties: vec![],
13723 links: vec![link_to("owner", "default::Account")],
13724 multilinks: vec![],
13725 computed: vec![],
13726 constraints: vec![],
13727 indexes: vec![],
13728 partition: None,
13729 vector_indexes: vec![],
13730 search_indexes: vec![],
13731 triggers: vec![],
13732 junction: false,
13733 signals: vec![],
13734 });
13735 schema
13736 }
13737
13738 #[test]
13739 fn a_backlink_to_an_interface_carries_the_concrete_type() {
13740 let out = compile_and_emit_with(
13741 "SELECT Note { pinners := .<pinned[is Account] { id } }",
13742 &make_note_schema(),
13743 );
13744 assert!(
13745 !out.sql.contains("'default::Account'::text"),
13746 "the backlink's rows are labelled with the interface instead of their own type:\n{}",
13747 out.sql
13748 );
13749 }
13750
13751 #[test]
13752 fn a_link_reached_through_a_deep_splat_carries_the_concrete_type() {
13753 let out = compile_and_emit_with("SELECT Note { ** }", &make_note_schema());
13754 assert!(
13755 !out.sql.contains("'default::Account'::text"),
13756 "the splatted link's rows are labelled with the interface instead of their own type:\n{}",
13757 out.sql
13758 );
13759 }
13760
13761 #[test]
13762 fn test_path_traversal_onto_an_interface_carries_the_concrete_type() {
13763 let mut schema = make_interface_schema();
13768 let account = schema
13769 .types
13770 .iter_mut()
13771 .find(|t| t.name == "Account")
13772 .expect("the interface schema has an Account type");
13773 account.materialized = true;
13774 let mut organization = schema
13775 .types
13776 .iter()
13777 .find(|t| t.name == "Individual")
13778 .expect("the interface schema has an Individual type")
13779 .clone();
13780 organization.name = "Organization".into();
13781 organization.table = "Organization".into();
13782 organization.computed.clear();
13783 schema.types.push(organization);
13784 schema.types.push(TypeDescriptor {
13785 name: "Note".into(),
13786 module: "default".into(),
13787 table: "Note".into(),
13788 abstract_: false,
13789 materialized: false,
13790 description: None,
13791 parents: vec![],
13792 interfaces: vec![],
13793 bases: vec![],
13794 properties: vec![],
13795 links: vec![LinkDescriptor {
13796 name: "owner".into(),
13797 target: "default::Account".into(),
13798 nullable: true,
13799 description: None,
13800 default_pyql: None,
13801 is_exclusive: false,
13802 is_readonly: false,
13803 rewrites: vec![],
13804 on_delete: vec![],
13805 through: None,
13806 }],
13807 multilinks: vec![],
13808 computed: vec![],
13809 constraints: vec![],
13810 indexes: vec![],
13811 partition: None,
13812 vector_indexes: vec![],
13813 search_indexes: vec![],
13814 triggers: vec![],
13815 junction: false,
13816 signals: vec![],
13817 });
13818
13819 let out = compile_and_emit_with("SELECT Note.owner { id }", &schema);
13820 assert!(
13821 out.sql.contains("UNION ALL") && out.sql.contains("'default::Individual'::text AS \"__type__\""),
13822 "the traversal's target should fan out over implementors, got:\n{}",
13823 out.sql
13824 );
13825 assert!(
13826 !out.sql.contains("'default::Account'::text,"),
13827 "the row's type should be read off the row, not fixed to the interface, got:\n{}",
13828 out.sql
13829 );
13830 }
13831
13832 #[test]
13833 fn test_computed_multilink_chain_comes_back_as_rows() {
13834 let mut schema = make_schema();
13839 schema.types[0].computed.push(crate::schema::ComputedDescriptor {
13840 name: "coauthors".into(),
13841 expression: ".posts.author".into(),
13842 return_type: None,
13843 link_target: None,
13844 link_multi: false,
13845 });
13846 let post = schema
13847 .types
13848 .iter_mut()
13849 .find(|t| t.name == "Post")
13850 .expect("test schema has a Post type");
13851 post.links.push(LinkDescriptor {
13852 name: "author".into(),
13853 target: "default::Person".into(),
13854 nullable: true,
13855 description: None,
13856 default_pyql: None,
13857 is_exclusive: false,
13858 is_readonly: false,
13859 rewrites: vec![],
13860 on_delete: vec![],
13861 through: None,
13862 });
13863
13864 let out = compile_and_emit_with("SELECT Person { coauthors: { name } }", &schema);
13865 assert!(
13866 out.sql.contains("ARRAY(SELECT"),
13867 "the chain should aggregate into an array, got:\n{}",
13868 out.sql
13869 );
13870 assert!(
13871 out.sql.contains("\"name\""),
13872 "the requested sub-shape should survive, got:\n{}",
13873 out.sql
13874 );
13875
13876 let crate::query::ShapeNode::Object { pointers, .. } = &out.shape.root else {
13877 panic!("expected Object shape, got {:?}", out.shape.root)
13878 };
13879 let coauthors = pointers
13880 .iter()
13881 .find(|node| matches!(node, crate::query::ShapeNode::Array { name, .. } if name == "coauthors"))
13882 .unwrap_or_else(|| panic!("expected an Array-shaped pointer, got {pointers:?}"));
13883 let crate::query::ShapeNode::Array { element, .. } = coauthors else {
13884 unreachable!()
13885 };
13886 assert!(
13887 matches!(
13888 element.as_ref(),
13889 crate::query::ShapeNode::Object { type_name: Some(t), .. } if t == "default::Person"
13890 ),
13891 "the elements should hydrate as objects, got {element:?}",
13892 );
13893 }
13894
13895 #[test]
13896 fn test_multi_sort_with_then_emits_two_order_keys() {
13897 let out = compile_and_emit("SELECT Person { name } ORDER BY .name THEN .age DESC");
13898 assert!(out.sql.contains("ORDER BY"), "expected ORDER BY");
13899 assert!(out.sql.contains("\"name\""));
13901 assert!(out.sql.contains("\"age\""));
13902 assert!(out.sql.contains("DESC"));
13903 }
13904
13905 #[test]
13906 fn test_string_index_emits_str_subscript() {
13907 let out = compile_and_emit("SELECT 'hello'[1]");
13908 assert!(
13909 out.sql.contains("_pylon.str_subscript"),
13910 "expected _pylon.str_subscript() for string index, got:\n{}",
13911 out.sql
13912 );
13913 }
13914
13915 #[test]
13918 fn test_for_over_a_set_returning_call_is_not_a_values_clause() {
13919 let out = compile_and_emit("FOR n IN array_unpack(<array<int64>>$ns) UNION (SELECT n)");
13920 assert!(
13921 !out.sql.contains("VALUES (unnest"),
13922 "a set-returning iterator must not land in VALUES, got:\n{}",
13923 out.sql
13924 );
13925 assert!(
13926 out.sql.contains("SELECT unnest"),
13927 "expected the iterator in a select list, got:\n{}",
13928 out.sql
13929 );
13930 }
13931
13932 #[test]
13935 fn test_for_over_array_unpack_binds_the_element_type() {
13936 let out = compile_and_emit("FOR j IN array_unpack(<array<json>>$rows) UNION (SELECT <str>j['k'])");
13937 assert!(
13938 out.sql.contains("::jsonb AS v"),
13939 "the loop variable should carry the element type, got:\n{}",
13940 out.sql
13941 );
13942 }
13943
13944 #[test]
13947 fn test_a_binding_keeps_its_array_type_through_an_aggregate_and_a_condition() {
13948 for query in [
13949 "WITH ids := ([<uuid>$a] IF true ELSE [<uuid>$b]) SELECT Person FILTER contains(ids, .id)",
13950 "WITH ns := array_agg((SELECT Person.name)) SELECT Person FILTER contains(ns, .name)",
13951 ] {
13952 let out = compile_and_emit(query);
13953 assert!(
13954 out.sql.contains("@> ARRAY["),
13955 "expected the array overload of contains for `{query}`, got:\n{}",
13956 out.sql
13957 );
13958 }
13959 }
13960
13961 #[test]
13965 fn test_an_array_literals_walk_element_is_read_as_one_value() {
13966 let out = compile_and_emit("WITH p := (SELECT Person LIMIT 1) SELECT [<uuid>$a, p.id]");
13967 assert!(
13968 !out.sql.contains("ARRAY(SELECT"),
13969 "the element should be a scalar subquery, not an array of its own:\n{}",
13970 out.sql
13971 );
13972 }
13973
13974 #[test]
13978 fn test_ordering_a_walk_leaves_the_inner_filter_on_its_own_subject() {
13979 let out = compile_and_emit("SELECT (SELECT Person FILTER .name = 'x' LIMIT 1).posts { title } ORDER BY .title");
13980 assert!(
13981 out.sql.contains("WHERE (\"t0\".\"name\" = 'x')"),
13982 "the filter belongs to the Person the walk starts from, got:\n{}",
13983 out.sql
13984 );
13985 assert!(
13986 out.sql.contains("\"title\" ASC"),
13987 "the ordering belongs to the Post the walk lands on, got:\n{}",
13988 out.sql
13989 );
13990 }
13991
13992 #[test]
13996 fn test_a_walks_head_keeps_its_own_limit() {
13997 let out = compile_and_emit("SELECT (SELECT Person FILTER .name = 'x' LIMIT 1).posts { title }");
13998 let (head, walk) = out.sql.split_once(")\nSELECT").expect("expected a bound head");
13999 assert!(
14000 head.contains("LIMIT 1"),
14001 "the head keeps its own row count, got:\n{}",
14002 out.sql
14003 );
14004 assert!(
14005 !walk.contains("LIMIT"),
14006 "the walk keeps every row it reaches, got:\n{}",
14007 out.sql
14008 );
14009 }
14010
14011 #[test]
14015 fn test_for_over_json_array_unpack_binds_json() {
14016 let out = compile_and_emit(
14017 "FOR entry IN json_array_unpack(to_json(<str>$rows)) UNION (SELECT <str>json_get(entry, 'k'))",
14018 );
14019 assert!(
14020 out.sql.contains("::jsonb AS v"),
14021 "the loop variable should carry json, got:\n{}",
14022 out.sql
14023 );
14024 }
14025
14026 #[test]
14029 fn test_casting_json_get_reads_the_value_not_its_json_spelling() {
14030 let out = compile_and_emit("SELECT <uuid><str>json_get(<json>$p, 'id')");
14031 assert!(
14032 out.sql.contains("#>> '{}'"),
14033 "expected the json value to be read out, got:\n{}",
14034 out.sql
14035 );
14036 }
14037
14038 #[test]
14041 fn test_indexing_json_emits_a_jsonb_accessor() {
14042 let out = compile_and_emit("SELECT (<json>$payload)['label']");
14043 assert!(
14044 out.sql.contains("->'label'"),
14045 "expected a jsonb field access, got:\n{}",
14046 out.sql
14047 );
14048 assert!(
14049 !out.sql.contains("str_subscript"),
14050 "json must not be subscripted as a string, got:\n{}",
14051 out.sql
14052 );
14053 }
14054
14055 #[test]
14058 fn test_casting_json_to_a_scalar_unwraps_it() {
14059 let out = compile_and_emit("SELECT <str>to_json(<str>$s)");
14060 assert!(
14061 out.sql.contains("#>> '{}'"),
14062 "expected the json value to be extracted, got:\n{}",
14063 out.sql
14064 );
14065 }
14066
14067 #[test]
14071 fn test_a_binding_that_reads_the_loop_variable_is_keyed_by_iteration() {
14072 let out = compile_and_emit(
14073 "WITH made := (FOR p IN (SELECT Person) UNION ( \
14074 WITH mine := (SELECT p.posts LIMIT 1) \
14075 INSERT Company { name := mine.title } \
14076 )) SELECT count(made)",
14077 );
14078 assert!(
14079 out.sql
14080 .contains("\"mine\" AS (\nSELECT \"_for_p\".\"v\" AS \"_outer__mine\""),
14081 "the binding must be evaluated per iteration, got:\n{}",
14082 out.sql
14083 );
14084 assert!(
14085 out.sql.contains("\"mine\".\"_outer__mine\" = \"_for_p\".\"v\""),
14086 "a read of it must pin to the iteration in scope, got:\n{}",
14087 out.sql
14088 );
14089 }
14090
14091 #[test]
14095 fn test_a_loop_nested_in_a_loop_pairs_its_rows_by_iteration() {
14096 let out = compile_and_emit(
14097 "FOR n IN {'a', 'b'} UNION (INSERT Person { name := n, posts := (FOR t IN {n} UNION (INSERT Post { title := t })) })",
14098 );
14099 assert!(
14100 out.sql.contains("\"_outer\""),
14101 "the nested rows must carry an iteration key, got:\n{}",
14102 out.sql
14103 );
14104 assert!(
14105 !out.sql.contains("CROSS JOIN \"_nested_dml_0\""),
14106 "they must not be cross-joined onto every row, got:\n{}",
14107 out.sql
14108 );
14109 }
14110
14111 #[test]
14114 fn test_for_bodied_insert_writes_its_multi_link() {
14115 let out = compile_and_emit("FOR n IN {'a', 'b'} UNION (INSERT Person { name := n, posts := (SELECT Post) })");
14116 assert!(
14117 out.sql.contains("INSERT INTO \"public\".\"Person.posts\""),
14118 "the junction rows must be written too, got:\n{}",
14119 out.sql
14120 );
14121 }
14122
14123 #[test]
14124 fn test_string_slice_emits_substr() {
14125 let out = compile_and_emit("SELECT 'hello'[1:3]");
14126 assert!(
14127 out.sql.contains("substr"),
14128 "expected substr() for string slice, got:\n{}",
14129 out.sql
14130 );
14131 }
14132
14133 #[test]
14134 fn test_array_index_emits_subscript() {
14135 let out = compile_and_emit("SELECT [1, 2, 3][1]");
14136 assert!(
14137 out.sql.contains("_pylon.array_subscript"),
14138 "expected _pylon.array_subscript() for array index, got:\n{}",
14139 out.sql
14140 );
14141 }
14142
14143 #[test]
14144 fn test_index_into_array_returning_function_emits_array_subscript() {
14145 let out = compile_and_emit("SELECT str_split('a::b', '::')[-1]");
14146 assert!(
14147 out.sql.contains("_pylon.array_subscript(string_to_array"),
14148 "expected _pylon.array_subscript() over str_split, got:\n{}",
14149 out.sql
14150 );
14151 }
14152
14153 #[test]
14154 fn test_array_slice_emits_subscript() {
14155 let out = compile_and_emit("SELECT [1, 2, 3][0:2]");
14156 assert!(
14157 !out.sql.contains("substr"),
14158 "should not use substr for array, got:\n{}",
14159 out.sql
14160 );
14161 assert!(out.sql.contains(")["), "expected array slice syntax, got:\n{}", out.sql);
14162 }
14163
14164 #[test]
14165 fn test_open_ended_string_slice_emits_substr_no_length() {
14166 let out = compile_and_emit("SELECT 'hello'[2:]");
14167 assert!(out.sql.contains("substr"), "expected substr(), got:\n{}", out.sql);
14169 let substr_idx = out.sql.find("substr").unwrap();
14171 let after = &out.sql[substr_idx..];
14172 let commas = after.chars().take_while(|&c| c != ')').filter(|&c| c == ',').count();
14173 assert_eq!(commas, 1, "open-ended slice should use 2-arg substr, got:\n{}", out.sql);
14174 }
14175
14176 #[test]
14177 fn test_group_by_single_key() {
14178 let out = compile_and_emit("group Person { name } by .age");
14179 assert!(out.sql.contains("GROUP BY"), "expected GROUP BY, got:\n{}", out.sql);
14181 assert!(out.sql.contains("\"age\""), "expected age column, got:\n{}", out.sql);
14183 assert!(
14185 out.sql.contains("array_agg(ROW("),
14186 "expected array_agg, got:\n{}",
14187 out.sql
14188 );
14189 assert!(
14191 out.sql.contains("ARRAY['age']"),
14192 "expected grouping array, got:\n{}",
14193 out.sql
14194 );
14195 assert!(matches!(out.shape.root, crate::query::ShapeNode::Group { .. }));
14197 if let crate::query::ShapeNode::Group {
14198 key_nodes,
14199 grouping_position,
14200 elements_position,
14201 ..
14202 } = &out.shape.root
14203 {
14204 assert_eq!(key_nodes.len(), 1);
14205 assert!(matches!(&key_nodes[0], crate::query::ShapeNode::Scalar { name, position: 1 } if name == "age"));
14206 assert_eq!(*grouping_position, 2);
14207 assert_eq!(*elements_position, 3);
14208 }
14209 }
14210
14211 #[test]
14212 fn test_group_using_alias() {
14213 let out = compile_and_emit("group Person using decade := .age // 10 by decade");
14214 assert!(out.sql.contains("GROUP BY"), "expected GROUP BY, got:\n{}", out.sql);
14215 assert!(
14216 out.sql.contains("ARRAY['decade']"),
14217 "expected grouping array, got:\n{}",
14218 out.sql
14219 );
14220 if let crate::query::ShapeNode::Group { key_nodes, .. } = &out.shape.root {
14221 assert_eq!(key_nodes.len(), 1);
14222 assert!(matches!(&key_nodes[0], crate::query::ShapeNode::Scalar { name, .. } if name == "decade"));
14223 }
14224 }
14225
14226 #[test]
14227 fn test_group_orders_elements_within_each_group() {
14228 let out = compile_and_emit("group Person { name } by .age order by .name desc");
14229 assert!(
14230 out.sql.contains(")::record ORDER BY \"t0\".\"name\" DESC NULLS LAST)"),
14231 "{}",
14232 out.sql
14233 );
14234 assert!(!out.sql.contains("row_number()"), "{}", out.sql);
14235 }
14236
14237 #[test]
14238 fn test_group_limit_trims_each_group_not_the_result() {
14239 let out = compile_and_emit("group Person { name } by .age order by .name desc limit 1");
14242 assert!(
14243 out.sql
14244 .contains("row_number() OVER (PARTITION BY \"t0\".\"age\" ORDER BY \"t0\".\"name\" DESC NULLS LAST)"),
14245 "{}",
14246 out.sql
14247 );
14248 assert!(
14249 out.sql.contains("WHERE \"__rk\" > 0 AND \"__rk\" <= 0 + 1"),
14250 "{}",
14251 out.sql
14252 );
14253 assert!(!out.sql.contains("\nLIMIT"), "{}", out.sql);
14254 }
14255
14256 #[test]
14257 fn test_group_by_two_keys_with_a_per_group_limit() {
14258 let out = compile_and_emit(
14259 "group Person { name } by .age, .name filter .age > 18 order by .name desc offset 1 limit 2",
14260 );
14261 assert!(
14262 out.sql.contains("PARTITION BY \"t0\".\"age\", \"t0\".\"name\""),
14263 "{}",
14264 out.sql
14265 );
14266 let where_pos = out.sql.find("WHERE (\"t0\".\"age\" > 18)").expect("filter");
14269 let rank_pos = out.sql.find("WHERE \"__rk\"").expect("rank filter");
14270 assert!(where_pos < rank_pos, "{}", out.sql);
14271 assert!(
14272 out.sql.contains("GROUP BY \"t0\".\"age\", \"t0\".\"name\""),
14273 "{}",
14274 out.sql
14275 );
14276 }
14277
14278 #[test]
14279 fn test_group_filter_without_a_limit_is_a_plain_where() {
14280 let out = compile_and_emit("group Person { name } by .age filter .age > 18");
14281 assert!(out.sql.contains("WHERE (\"t0\".\"age\" > 18)"), "{}", out.sql);
14282 assert!(!out.sql.contains("row_number()"), "{}", out.sql);
14283 }
14284
14285 #[test]
14286 fn test_abs_path_concat_same_type() {
14287 let out = compile_and_emit("SELECT Person.name ++ ' ' ++ Person.name");
14288 assert!(out.sql.contains("\"name\""), "expected name column, got:\n{}", out.sql);
14289 assert!(out.sql.contains("||"), "expected concat operator, got:\n{}", out.sql);
14290 assert!(out.sql.contains("FROM"), "expected FROM clause, got:\n{}", out.sql);
14291 }
14292
14293 #[test]
14294 fn test_abs_path_single_property() {
14295 let out = compile_and_emit("SELECT Person.name");
14296 assert!(out.sql.contains("\"name\""), "expected name column, got:\n{}", out.sql);
14297 assert!(out.sql.contains("FROM"), "expected FROM clause, got:\n{}", out.sql);
14298 }
14299
14300 #[test]
14301 fn test_pgvector_cast_emits_vector_type() {
14302 let out = compile_and_emit("SELECT <pgvector::vector>[1.0, 2.0, 3.0]");
14303 assert!(
14304 out.sql.contains("::vector"),
14305 "expected ::vector cast, got:\n{}",
14306 out.sql
14307 );
14308 assert!(out.sql.contains("ARRAY["), "expected ARRAY literal, got:\n{}", out.sql);
14309 }
14310
14311 #[test]
14312 fn test_pgvector_euclidean_distance_emits_l2_operator() {
14313 let out = compile_and_emit(
14314 "SELECT pgvector::euclidean_distance(<pgvector::vector>[1.0, 2.0], <pgvector::vector>[3.0, 4.0])",
14315 );
14316 assert!(out.sql.contains("<->"), "expected <-> operator, got:\n{}", out.sql);
14317 }
14318
14319 #[test]
14320 fn test_pgvector_cosine_distance_emits_cosine_operator() {
14321 let out = compile_and_emit(
14322 "SELECT pgvector::cosine_distance(<pgvector::vector>[1.0, 2.0], <pgvector::vector>[3.0, 4.0])",
14323 );
14324 assert!(out.sql.contains("<=>"), "expected <=> operator, got:\n{}", out.sql);
14325 }
14326
14327 #[test]
14328 fn test_pgvector_neg_inner_product_emits_ip_operator() {
14329 let out = compile_and_emit(
14330 "SELECT pgvector::neg_inner_product(<pgvector::vector>[1.0, 2.0], <pgvector::vector>[3.0, 4.0])",
14331 );
14332 assert!(out.sql.contains("<#>"), "expected <#> operator, got:\n{}", out.sql);
14333 }
14334
14335 #[test]
14336 fn test_pgvector_inner_product_negates_ip_operator() {
14337 let out = compile_and_emit(
14338 "SELECT pgvector::inner_product(<pgvector::vector>[1.0, 2.0], <pgvector::vector>[3.0, 4.0])",
14339 );
14340 assert!(out.sql.contains("<#>"), "expected <#> operator, got:\n{}", out.sql);
14341 assert!(out.sql.contains("0.0"), "expected negation of <#>, got:\n{}", out.sql);
14342 }
14343
14344 #[test]
14345 fn test_crypto_digest_str_and_bytes_overloads_both_use_pgcrypto_digest() {
14346 let out = compile_and_emit("SELECT crypto::digest('hello', 'sha256')");
14347 assert!(
14348 out.sql.contains("digest("),
14349 "expected pgcrypto's digest(), got:\n{}",
14350 out.sql
14351 );
14352
14353 let out = compile_and_emit("SELECT crypto::digest(std::from_hex('68656c6c6f'), 'sha256')");
14354 assert!(
14355 out.sql.contains("digest("),
14356 "expected pgcrypto's digest(), got:\n{}",
14357 out.sql
14358 );
14359 }
14360
14361 #[test]
14362 fn test_crypto_hmac_str_and_bytes_overloads_both_use_pgcrypto_hmac() {
14363 let out = compile_and_emit("SELECT crypto::hmac('hello', 'key', 'sha256')");
14364 assert!(
14365 out.sql.contains("hmac("),
14366 "expected pgcrypto's hmac(), got:\n{}",
14367 out.sql
14368 );
14369
14370 let out =
14371 compile_and_emit("SELECT crypto::hmac(std::from_hex('68656c6c6f'), std::from_hex('6b6579'), 'sha256')");
14372 assert!(
14373 out.sql.contains("hmac("),
14374 "expected pgcrypto's hmac(), got:\n{}",
14375 out.sql
14376 );
14377 }
14378
14379 #[test]
14380 fn test_crypto_gen_salt_zero_arg_defaults_to_blowfish() {
14381 let out = compile_and_emit("SELECT crypto::gen_salt()");
14382 assert!(
14383 out.sql.contains("gen_salt('bf')"),
14384 "expected default 'bf' salt type, got:\n{}",
14385 out.sql
14386 );
14387 }
14388
14389 #[test]
14390 fn test_crypto_gen_salt_one_arg_passes_type_through() {
14391 let out = compile_and_emit("SELECT crypto::gen_salt('xdes')");
14392 assert!(
14393 out.sql.contains("gen_salt("),
14394 "expected gen_salt() call, got:\n{}",
14395 out.sql
14396 );
14397 }
14398
14399 #[test]
14400 fn test_crypto_gen_salt_iter_count_casts_to_int4() {
14401 let out = compile_and_emit("SELECT crypto::gen_salt('xdes', 5)");
14402 assert!(
14403 out.sql.contains("::int4"),
14404 "expected int8 -> int4 narrowing cast, got:\n{}",
14405 out.sql
14406 );
14407 }
14408
14409 #[test]
14410 fn test_crypto_crypt_uses_pgcrypto_crypt() {
14411 let out = compile_and_emit("SELECT crypto::crypt('hunter2', crypto::gen_salt())");
14412 assert!(
14413 out.sql.contains("crypt("),
14414 "expected pgcrypto's crypt(), got:\n{}",
14415 out.sql
14416 );
14417 }
14418
14419 #[test]
14420 fn test_postgis_cast_emits_geometry_type() {
14421 let out = compile_and_emit("SELECT <postgis::geometry>'POINT(1 2)'");
14422 assert!(
14423 out.sql.contains("::geometry"),
14424 "expected ::geometry cast, got:\n{}",
14425 out.sql
14426 );
14427 }
14428
14429 #[test]
14430 fn test_postgis_x_uses_st_x_builtin() {
14431 let out = compile_and_emit("SELECT postgis::x(<postgis::geometry>'POINT(1 2)')");
14432 assert!(out.sql.contains("st_x("), "expected st_x() call, got:\n{}", out.sql);
14433 }
14434
14435 #[test]
14436 fn test_postgis_area_geometry_and_geography_overloads() {
14437 let out = compile_and_emit("SELECT postgis::area(<postgis::geometry>'POINT(1 2)')");
14438 assert!(
14439 out.sql.contains("st_area("),
14440 "expected st_area() call, got:\n{}",
14441 out.sql
14442 );
14443
14444 let out = compile_and_emit("SELECT postgis::area(<postgis::geography>'POINT(1 2)', true)");
14445 assert!(
14446 out.sql.contains("st_area("),
14447 "expected st_area() call, got:\n{}",
14448 out.sql
14449 );
14450 }
14451
14452 #[test]
14453 fn test_postgis_setsrid_casts_int64_arg_to_int4() {
14454 let out = compile_and_emit("SELECT postgis::setsrid(<postgis::geometry>'POINT(1 2)', 4326)");
14455 assert!(
14456 out.sql.contains("st_setsrid("),
14457 "expected st_setsrid() call, got:\n{}",
14458 out.sql
14459 );
14460 assert!(
14461 out.sql.contains("::int4"),
14462 "expected int8 -> int4 narrowing cast, got:\n{}",
14463 out.sql
14464 );
14465 }
14466
14467 #[test]
14468 fn test_postgis_quantizecoordinates_default_arity_variants_compile() {
14469 let out = compile_and_emit("SELECT postgis::quantizecoordinates(<postgis::geometry>'POINT(1 2)', 5)");
14474 assert!(out.sql.contains("st_quantizecoordinates("), "got:\n{}", out.sql);
14475
14476 let out = compile_and_emit("SELECT postgis::quantizecoordinates(<postgis::geometry>'POINT(1 2)', 5, 5, 5)");
14477 assert!(out.sql.contains("st_quantizecoordinates("), "got:\n{}", out.sql);
14478 }
14479
14480 #[test]
14481 fn test_postgis_op_contains_emits_infix_operator_not_function_call() {
14482 let out = compile_and_emit(
14487 "SELECT postgis::op_contains(<postgis::geometry>'POINT(1 2)', <postgis::geometry>'POINT(3 4)')",
14488 );
14489 assert!(out.sql.contains(" ~ "), "expected infix ~ operator, got:\n{}", out.sql);
14490 assert!(
14491 !out.sql.contains("op_contains("),
14492 "must not call a literal op_contains function, got:\n{}",
14493 out.sql
14494 );
14495 }
14496
14497 #[test]
14498 fn test_postgis_op_overlaps_geometry_and_geography_overloads() {
14499 let out = compile_and_emit(
14500 "SELECT postgis::op_overlaps(<postgis::geometry>'POINT(1 2)', <postgis::geometry>'POINT(3 4)')",
14501 );
14502 assert!(
14503 out.sql.contains(" && "),
14504 "expected infix && operator, got:\n{}",
14505 out.sql
14506 );
14507
14508 let out = compile_and_emit(
14509 "SELECT postgis::op_overlaps(<postgis::geography>'POINT(1 2)', <postgis::geography>'POINT(3 4)')",
14510 );
14511 assert!(
14512 out.sql.contains(" && "),
14513 "expected infix && operator, got:\n{}",
14514 out.sql
14515 );
14516 }
14517
14518 fn make_schema_with_fns() -> SchemaDescriptor {
14521 let mut s = make_schema();
14522 s.functions = vec![
14523 FunctionDescriptor {
14524 name: "mysum".into(),
14525 module: "default".into(),
14526 params: vec![
14527 FunctionParamDescriptor {
14528 name: "a".into(),
14529 pg_type: "int8".into(),
14530 },
14531 FunctionParamDescriptor {
14532 name: "b".into(),
14533 pg_type: "int8".into(),
14534 },
14535 ],
14536 return_pg_type: "int8".into(),
14537 return_is_object: false,
14538 return_is_set: false,
14539 return_is_polymorphic: false,
14540 volatility: "immutable".into(),
14541 body: "a + b".into(),
14542 },
14543 FunctionDescriptor {
14544 name: "adults".into(),
14545 module: "default".into(),
14546 params: vec![],
14547 return_pg_type: "default::Person".into(),
14548 return_is_object: true,
14549 return_is_set: true,
14550 return_is_polymorphic: false,
14551 volatility: "stable".into(),
14552 body: "select Person filter .age > 18".into(),
14553 },
14554 FunctionDescriptor {
14561 name: "mysum".into(),
14562 module: "default".into(),
14563 params: vec![
14564 FunctionParamDescriptor {
14565 name: "a".into(),
14566 pg_type: "int8".into(),
14567 },
14568 FunctionParamDescriptor {
14569 name: "b".into(),
14570 pg_type: "int8".into(),
14571 },
14572 FunctionParamDescriptor {
14573 name: "c".into(),
14574 pg_type: "int8".into(),
14575 },
14576 ],
14577 return_pg_type: "int8".into(),
14578 return_is_object: false,
14579 return_is_set: false,
14580 return_is_polymorphic: false,
14581 volatility: "immutable".into(),
14582 body: "a + b + c".into(),
14583 },
14584 ];
14585 s
14586 }
14587
14588 #[test]
14589 fn test_user_fn_scalar_call() {
14590 let schema = make_schema_with_fns();
14591 let out = compile_and_emit_with("SELECT mysum(1, 2)", &schema);
14592 assert!(out.sql.contains("\"public\".\"mysum\""), "got:\n{}", out.sql);
14593 }
14594
14595 #[test]
14596 fn test_user_fn_overload_resolved_by_argument_count() {
14597 let schema = make_schema_with_fns();
14602 let out = compile_and_emit_with("SELECT mysum(1, 2, 3)", &schema);
14603 assert!(
14604 out.sql
14605 .contains("\"public\".\"mysum\"((1)::int8, (2)::int8, (3)::int8)"),
14606 "got:\n{}",
14607 out.sql,
14608 );
14609
14610 let out = compile_and_emit_with("SELECT mysum(1, 2)", &schema);
14612 assert!(
14613 out.sql.contains("\"public\".\"mysum\"((1)::int8, (2)::int8)"),
14614 "got:\n{}",
14615 out.sql,
14616 );
14617 }
14618
14619 #[test]
14620 fn test_user_fn_object_select_no_shape() {
14621 let schema = make_schema_with_fns();
14622 let out = compile_and_emit_with("SELECT adults()", &schema);
14623 assert!(out.sql.contains("\"public\".\"adults\"()"), "got:\n{}", out.sql);
14624 assert!(out.sql.contains("FROM"), "got:\n{}", out.sql);
14625 }
14626
14627 #[test]
14628 fn test_user_fn_object_select_with_shape() {
14629 let schema = make_schema_with_fns();
14630 let out = compile_and_emit_with("SELECT adults() { name }", &schema);
14631 assert!(out.sql.contains("\"public\".\"adults\"()"), "got:\n{}", out.sql);
14632 assert!(out.sql.contains("\"name\""), "got:\n{}", out.sql);
14633 }
14634
14635 #[test]
14636 fn test_object_fn_projected_to_a_property_inside_an_expression() {
14637 let schema = make_schema_with_fns();
14638 let out = compile_and_emit_with("SELECT Person { n := adults().name }", &schema);
14639 assert!(out.sql.contains("FROM \"public\".\"adults\"() AS"), "got:\n{}", out.sql);
14640 assert!(out.sql.contains("\".\"name\"\n"), "got:\n{}", out.sql);
14641 }
14642
14643 #[test]
14644 fn test_object_fn_sub_select_carries_its_modifiers() {
14645 let schema = make_schema_with_fns();
14646 let out = compile_and_emit_with(
14647 "SELECT Person { n := (select adults() filter .age > 21 limit 1).name }",
14648 &schema,
14649 );
14650 assert!(out.sql.contains("FROM \"public\".\"adults\"() AS"), "got:\n{}", out.sql);
14651 assert!(out.sql.contains("\"age\" > 21"), "got:\n{}", out.sql);
14652 assert!(out.sql.contains("LIMIT 1"), "got:\n{}", out.sql);
14653 }
14654
14655 #[test]
14656 fn test_object_fn_without_a_property_still_explains_the_restriction() {
14657 let schema = make_schema_with_fns();
14658 let ast = parse::parse("SELECT Person { n := adults() + 1 }").unwrap();
14659 let err = match ir::compile(&ast, &schema) {
14660 Ok(_) => panic!("expected a compile error"),
14661 Err(e) => format!("{e}"),
14662 };
14663 assert!(err.contains("returns objects"), "{err}");
14664 }
14665
14666 #[test]
14667 fn test_object_fn_projected_to_an_unknown_property_is_rejected() {
14668 let schema = make_schema_with_fns();
14669 let ast = parse::parse("SELECT Person { n := adults().nope }").unwrap();
14670 let err = match ir::compile(&ast, &schema) {
14671 Ok(_) => panic!("expected a compile error"),
14672 Err(e) => format!("{e}"),
14673 };
14674 assert!(err.contains("nope"), "{err}");
14675 }
14676
14677 #[test]
14678 fn test_user_fn_in_cte_exposes_raw_columns() {
14679 let schema = make_schema_with_fns();
14682 let out = compile_and_emit_with("WITH persons := adults() SELECT persons FILTER .age > 25", &schema);
14683 assert!(
14684 out.sql.contains("SELECT * FROM \"public\".\"adults\"()"),
14685 "CTE source must be SELECT * FROM fn(), got:\n{}",
14686 out.sql,
14687 );
14688 assert!(
14689 out.sql.contains("\"age\""),
14690 "outer filter must reference raw column, got:\n{}",
14691 out.sql
14692 );
14693 }
14694
14695 fn make_schema_with_vector() -> SchemaDescriptor {
14698 use crate::schema::VectorIndexDescriptor;
14699 let mut s = make_schema();
14700 if let Some(td) = s.types.iter_mut().find(|t| t.name == "Person") {
14701 td.vector_indexes.push(VectorIndexDescriptor {
14702 index_name: None,
14703 pointers: vec!["name".into()],
14704 model: "test-embed".into(),
14705 metric: "cosine".into(),
14706 dimensions: 4,
14707 });
14708 }
14709 s
14710 }
14711
14712 #[test]
14713 fn test_vector_search_bare_type_name() {
14714 let schema = make_schema_with_vector();
14715 let out = compile_and_emit_with(
14716 "WITH search := vector::search(Person, <pgvector::vector>[1.0, 2.0, 3.0, 4.0]) \
14717 SELECT search { object { name }, distance }",
14718 &schema,
14719 );
14720 assert!(
14721 out.sql.contains("\"Person\""),
14722 "expected Person table, got:\n{}",
14723 out.sql
14724 );
14725 assert!(out.sql.contains("<=>"), "expected cosine operator, got:\n{}", out.sql);
14726 }
14727
14728 #[test]
14729 fn test_vector_search_qualified_type_name() {
14730 let schema = make_schema_with_vector();
14731 let out = compile_and_emit_with(
14732 "WITH search := vector::search(default::Person, <pgvector::vector>[1.0, 2.0, 3.0, 4.0]) \
14733 SELECT search { object { name }, distance }",
14734 &schema,
14735 );
14736 assert!(
14737 out.sql.contains("\"Person\""),
14738 "expected Person table, got:\n{}",
14739 out.sql
14740 );
14741 assert!(out.sql.contains("<=>"), "expected cosine operator, got:\n{}", out.sql);
14742 }
14743
14744 #[test]
14745 fn test_vector_search_subquery_filter_included_in_where() {
14746 let schema = make_schema_with_vector();
14747 let out = compile_and_emit_with(
14748 "WITH search := vector::search((select Person filter .name = 'Alice'), <pgvector::vector>[1.0, 2.0, 3.0, 4.0]) \
14749 SELECT search { object { name }, distance }",
14750 &schema,
14751 );
14752 assert!(
14753 out.sql.contains("\"Person\""),
14754 "expected Person table, got:\n{}",
14755 out.sql
14756 );
14757 assert!(out.sql.contains("\"name\""), "expected name filter, got:\n{}", out.sql);
14758 assert!(out.sql.contains("Alice"), "expected filter value, got:\n{}", out.sql);
14759 assert!(out.sql.contains("<=>"), "expected cosine operator, got:\n{}", out.sql);
14760 }
14761
14762 #[test]
14763 fn test_vector_search_subquery_filter_combined_with_outer_property_filter() {
14764 let schema = make_schema_with_vector();
14765 let out = compile_and_emit_with(
14766 "WITH search := vector::search((select Person filter .age > 18), <pgvector::vector>[1.0, 2.0, 3.0, 4.0]) \
14767 SELECT search { object { name }, distance }",
14768 &schema,
14769 );
14770 assert!(
14771 out.sql.contains("\"age\""),
14772 "expected age pre-filter, got:\n{}",
14773 out.sql
14774 );
14775 assert!(out.sql.contains("18"), "expected filter value 18, got:\n{}", out.sql);
14776 assert!(out.sql.contains("<=>"), "expected cosine operator, got:\n{}", out.sql);
14777 }
14778
14779 #[test]
14780 fn test_vector_search_text_overload_with_subquery_filter() {
14781 let schema = make_schema_with_vector();
14782 let out = compile_and_emit_with(
14783 "WITH search := vector::search((select Person filter .name = 'Alice'), query := $q) \
14784 SELECT search { object { name }, distance }",
14785 &schema,
14786 );
14787 assert!(
14788 out.sql.contains("\"Person\""),
14789 "expected Person table, got:\n{}",
14790 out.sql
14791 );
14792 assert!(
14793 out.sql.contains("Alice"),
14794 "expected pre-filter value, got:\n{}",
14795 out.sql
14796 );
14797 assert!(
14798 out.sql.contains("float8[]"),
14799 "expected float8[] cast for deferred vec param, got:\n{}",
14800 out.sql
14801 );
14802 assert!(out.sql.contains("<=>"), "expected cosine operator, got:\n{}", out.sql);
14803 }
14804
14805 #[test]
14806 fn test_count_type_ref_compiles_to_agg_over_query() {
14807 let out = compile_and_emit("SELECT count(Person)");
14808 assert!(out.sql.contains("count(*)"), "expected count(*), got:\n{}", out.sql);
14809 assert!(
14810 out.sql.contains("\"Person\""),
14811 "expected Person table, got:\n{}",
14812 out.sql
14813 );
14814 }
14815
14816 #[test]
14817 fn test_count_qualified_type_ref_compiles_to_agg_over_query() {
14818 let out = compile_and_emit("SELECT count(default::Person)");
14819 assert!(out.sql.contains("count(*)"), "expected count(*), got:\n{}", out.sql);
14820 assert!(
14821 out.sql.contains("\"Person\""),
14822 "expected Person table, got:\n{}",
14823 out.sql
14824 );
14825 }
14826
14827 #[test]
14828 fn test_count_subquery_compiles_to_agg_over_query() {
14829 let out = compile_and_emit("SELECT count((select Person))");
14830 assert!(out.sql.contains("count(*)"), "expected count(*), got:\n{}", out.sql);
14831 assert!(
14832 out.sql.contains("\"Person\""),
14833 "expected Person table, got:\n{}",
14834 out.sql
14835 );
14836 }
14837
14838 #[test]
14839 fn test_count_subquery_with_filter() {
14840 let out = compile_and_emit("SELECT count((select Person filter .name = 'Alice'))");
14841 assert!(out.sql.contains("count(*)"), "expected count(*), got:\n{}", out.sql);
14842 assert!(
14843 out.sql.contains("\"name\""),
14844 "expected filter on name, got:\n{}",
14845 out.sql
14846 );
14847 }
14848
14849 #[test]
14850 fn test_positional_param_compiles_to_dollar_n() {
14851 let out = compile_and_emit("SELECT Person FILTER .name = $0");
14852 assert!(out.sql.contains("$1"), "expected $1 placeholder, got:\n{}", out.sql);
14853 }
14854
14855 #[test]
14856 fn test_multiple_positional_params_compile_in_order() {
14857 let out = compile_and_emit("SELECT Person FILTER .name = $0 AND .age > $1");
14858 assert!(out.sql.contains("$1"), "expected $1, got:\n{}", out.sql);
14859 assert!(out.sql.contains("$2"), "expected $2, got:\n{}", out.sql);
14860 }
14861
14862 #[test]
14863 fn test_repeated_positional_param_reuses_slot() {
14864 let out = compile_and_emit("SELECT Person FILTER .name = $0 OR .name = $0");
14865 assert_eq!(
14866 out.sql.matches("$1").count(),
14867 2,
14868 "both uses must reference $1, got:\n{}",
14869 out.sql
14870 );
14871 }
14872
14873 #[test]
14874 fn test_cast_to_nonexistent_type_names_full_type() {
14875 let schema = make_schema();
14876 let ast = parse::parse("SELECT Person FILTER .name = <default::Ghost>$name").unwrap();
14877 match ir::compile(&ast, &schema) {
14878 Ok(_) => panic!("expected compile error for unknown type"),
14879 Err(e) => {
14880 let msg = e.to_string();
14881 assert!(
14882 msg.contains("unknown type 'default::Ghost'"),
14883 "expected full type name in error, got: {msg}",
14884 );
14885 }
14886 }
14887 }
14888
14889 #[test]
14890 fn test_cast_to_nonexistent_unqualified_type_names_type() {
14891 let schema = make_schema();
14892 let ast = parse::parse("SELECT Person FILTER .name = <Ghost>$name").unwrap();
14893 match ir::compile(&ast, &schema) {
14894 Ok(_) => panic!("expected compile error for unknown type"),
14895 Err(e) => {
14896 let msg = e.to_string();
14897 assert!(
14898 msg.contains("unknown type 'Ghost'"),
14899 "expected type name in error, got: {msg}",
14900 );
14901 }
14902 }
14903 }
14904
14905 #[test]
14906 fn test_top_level_cast_to_registered_scalar_uses_its_own_domain() {
14907 use crate::schema::ScalarDescriptor;
14913 let mut schema = make_schema();
14914 schema.scalars.push(ScalarDescriptor {
14915 name: "Email".into(),
14916 module: "default".into(),
14917 base: "Str".into(),
14918 pg_type: "text".into(),
14919 check_constraints: vec!["value ~ '^[^@]+@[^@]+\\.[^@]+$'".into()],
14920 is_sequence: false,
14921 });
14922 let out = compile_and_emit_with("SELECT <default::Email>'test@test.de'", &schema);
14923 assert!(
14924 out.sql.contains("\"public\".\"Email\""),
14925 "expected a cast to the scalar's own domain, got:\n{}",
14926 out.sql
14927 );
14928 assert!(
14929 !out.sql.to_lowercase().contains("\"person\""),
14930 "must not be misrouted to an object-type lookup, got:\n{}",
14931 out.sql
14932 );
14933 }
14934
14935 #[test]
14936 fn test_top_level_cast_to_unknown_type_names_full_type() {
14937 let schema = make_schema();
14942 let ast = parse::parse("SELECT <default::Ghost>$name").unwrap();
14943 match ir::compile(&ast, &schema) {
14944 Ok(_) => panic!("expected compile error for unknown type"),
14945 Err(e) => {
14946 let msg = e.to_string();
14947 assert!(
14948 msg.contains("unknown type 'default::Ghost'"),
14949 "expected full type name in error, got: {msg}",
14950 );
14951 }
14952 }
14953 }
14954
14955 #[test]
14956 fn test_structural_tuple_cast_unnamed_resolves_to_jsonb() {
14957 let out = compile_and_emit("SELECT <tuple<str, bool>>$p");
14958 assert!(out.sql.contains("($1)::jsonb"), "got:\n{}", out.sql);
14959 }
14960
14961 #[test]
14962 fn test_jsonb_to_uuid_cast_extracts_via_text() {
14963 let out = compile_and_emit("SELECT <uuid>(<json>$p)");
14965 assert!(out.sql.contains("#>> '{}'"), "got:\n{}", out.sql);
14966 assert!(out.sql.contains("::uuid"), "got:\n{}", out.sql);
14967 }
14968
14969 #[test]
14970 fn test_jsonb_to_datetime_cast_extracts_via_text() {
14971 let out = compile_and_emit("SELECT <datetime>(<json>$p)");
14972 assert!(out.sql.contains("#>> '{}'"), "got:\n{}", out.sql);
14973 assert!(out.sql.contains("::timestamptz"), "got:\n{}", out.sql);
14974 }
14975
14976 #[test]
14977 fn test_jsonb_to_duration_cast_extracts_via_text() {
14978 let out = compile_and_emit("SELECT <duration>(<json>$p)");
14979 assert!(out.sql.contains("#>> '{}'"), "got:\n{}", out.sql);
14980 assert!(out.sql.contains("::interval"), "got:\n{}", out.sql);
14981 }
14982
14983 #[test]
14984 fn test_jsonb_to_array_cast_unpacks_each_element() {
14985 let out = compile_and_emit("SELECT <array<int64>>(<json>$p)");
14986 assert!(out.sql.contains("jsonb_array_elements("), "got:\n{}", out.sql);
14987 assert!(out.sql.contains("#>> '{}'"), "got:\n{}", out.sql);
14988 assert!(out.sql.contains("::int8"), "got:\n{}", out.sql);
14989 }
14990
14991 #[test]
14992 fn test_non_jsonb_cast_is_unaffected_by_jsonb_extraction() {
14993 let out = compile_and_emit("SELECT <uuid>$p");
14996 assert!(!out.sql.contains("#>>"), "got:\n{}", out.sql);
14997 assert!(out.sql.contains("::uuid"), "got:\n{}", out.sql);
14998 }
14999
15000 #[test]
15001 fn test_datetime_plus_duration_type_checks() {
15002 let out = compile_and_emit("SELECT <datetime>$p + <duration>$d");
15007 assert!(out.sql.contains(" + "), "got:\n{}", out.sql);
15008 }
15009
15010 #[test]
15011 fn test_duration_plus_datetime_type_checks_either_order() {
15012 let out = compile_and_emit("SELECT <duration>$d + <datetime>$p");
15013 assert!(out.sql.contains(" + "), "got:\n{}", out.sql);
15014 }
15015
15016 #[test]
15017 fn test_local_date_plus_duration_type_checks() {
15018 let out = compile_and_emit("SELECT <cal::local_date>$p + <duration>$d");
15019 assert!(out.sql.contains(" + "), "got:\n{}", out.sql);
15020 }
15021
15022 #[test]
15023 fn test_datetime_minus_duration_type_checks() {
15024 let out = compile_and_emit("SELECT <datetime>$p - <duration>$d");
15025 assert!(out.sql.contains(" - "), "got:\n{}", out.sql);
15026 }
15027
15028 #[test]
15029 fn test_local_time_plus_duration_type_checks() {
15030 let out = compile_and_emit("SELECT <cal::local_time>$p + <duration>$d");
15031 assert!(out.sql.contains(" + "), "got:\n{}", out.sql);
15032 }
15033
15034 #[test]
15035 fn test_datetime_plus_duration_only_allowed_for_add_and_sub() {
15036 let schema = make_schema();
15039 let ast = parse::parse("SELECT <datetime>$p = <duration>$d").unwrap();
15040 match ir::compile(&ast, &schema) {
15041 Err(err) => assert!(format!("{err}").contains("cannot be applied"), "got: {err}"),
15042 Ok(_) => panic!("expected a compile error"),
15043 }
15044 }
15045
15046 #[test]
15047 fn test_int_and_float_mix_is_allowed() {
15048 let out = compile_and_emit("SELECT <int16>1 + <float32>2.0");
15052 assert!(out.sql.contains(" + "), "got:\n{}", out.sql);
15053 }
15054
15055 #[test]
15056 fn test_int_and_decimal_mix_is_allowed() {
15057 let out = compile_and_emit("SELECT <int64>1 + <decimal>2.0");
15061 assert!(out.sql.contains(" + "), "got:\n{}", out.sql);
15062 }
15063
15064 #[test]
15065 fn test_float_and_decimal_mix_still_rejected() {
15066 let schema = make_schema();
15070 let ast = parse::parse("SELECT <decimal>$p + <float64>$n").unwrap();
15071 match ir::compile(&ast, &schema) {
15072 Err(err) => assert!(format!("{err}").contains("cannot be applied"), "got: {err}"),
15073 Ok(_) => panic!("expected a compile error"),
15074 }
15075 }
15076
15077 #[test]
15078 fn test_unknown_pointer_suggests_a_close_match() {
15079 let schema = make_schema();
15083 let ast = parse::parse("SELECT Person { nam }").unwrap();
15084 match ir::compile(&ast, &schema) {
15085 Err(err) => assert_eq!(
15086 format!("{err}"),
15087 "object type 'default::Person' has no link or property 'nam'. Did you mean 'name'?"
15088 ),
15089 Ok(_) => panic!("expected a compile error"),
15090 }
15091 }
15092
15093 #[test]
15094 fn test_unrelated_unknown_pointer_gets_no_suggestion() {
15095 let schema = make_schema();
15098 let ast = parse::parse("SELECT Person { xyzxyzxyz }").unwrap();
15099 match ir::compile(&ast, &schema) {
15100 Err(err) => {
15101 let msg = format!("{err}");
15102 assert!(msg.contains("has no link or property 'xyzxyzxyz'"), "got: {msg}");
15103 assert!(!msg.contains("Did you mean"), "got: {msg}");
15104 }
15105 Ok(_) => panic!("expected a compile error"),
15106 }
15107 }
15108
15109 #[test]
15110 fn test_multilink_sourced_backlink_exists_filter_compiles() {
15111 let out = compile_and_emit("SELECT Post filter exists .<posts[is Person]");
15121 assert!(out.sql.contains("EXISTS"), "got:\n{}", out.sql);
15122 }
15123
15124 #[test]
15125 fn test_backlink_as_computed_shape_pointer_compiles() {
15126 let out = compile_and_emit("SELECT Post { title, authors := .<posts[is Person] { name } }");
15139 assert!(out.sql.contains("array_agg(ROW("), "got:\n{}", out.sql);
15140 }
15141
15142 fn schema_with_exclusive_posts() -> SchemaDescriptor {
15143 let mut schema = make_schema();
15144 for td in schema.types.iter_mut().filter(|td| td.name == "Person") {
15145 for ml in td.multilinks.iter_mut().filter(|ml| ml.name == "posts") {
15146 ml.is_exclusive = true;
15147 }
15148 }
15149 schema
15150 }
15151
15152 #[test]
15153 fn test_backlink_through_an_exclusive_multilink_is_single() {
15154 let out = compile_and_emit_with(
15155 "SELECT Post { title, author := .<posts[is Person] { name } }",
15156 &schema_with_exclusive_posts(),
15157 );
15158 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
15159 panic!("expected an object root, got {:?}", out.shape.root)
15160 };
15161 assert!(
15162 matches!(pointers.last(), Some(ShapeNode::Object { name, .. }) if name == "author"),
15163 "got {pointers:?}"
15164 );
15165 }
15166
15167 #[test]
15168 fn test_a_path_through_an_exclusive_backlink_is_single() {
15169 let out = compile_and_emit_with(
15170 "SELECT Post { title, author_name := .<posts[is Person].name }",
15171 &schema_with_exclusive_posts(),
15172 );
15173 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
15174 panic!("expected an object root, got {:?}", out.shape.root)
15175 };
15176 assert!(
15177 matches!(pointers.last(), Some(ShapeNode::Scalar { name, .. }) if name == "author_name"),
15178 "got {pointers:?}"
15179 );
15180 assert!(!out.sql.contains("ARRAY("), "got:\n{}", out.sql);
15181 }
15182
15183 #[test]
15184 fn test_structural_tuple_cast_named_resolves_to_jsonb() {
15185 let out = compile_and_emit("SELECT <tuple<x: float64, y: float64>>$p");
15186 assert!(out.sql.contains("($1)::jsonb"), "got:\n{}", out.sql);
15187 }
15188
15189 #[test]
15190 fn test_structural_tuple_cast_nested_resolves_to_jsonb() {
15191 let out = compile_and_emit("SELECT <tuple<point: tuple<x: float64, y: float64>, label: str>>$p");
15192 assert!(out.sql.contains("($1)::jsonb"), "got:\n{}", out.sql);
15193 }
15194
15195 #[test]
15196 fn test_nominal_named_tuple_cast_resolves_to_jsonb() {
15197 let mut schema = make_schema();
15198 schema.named_tuples.push(NamedTupleDescriptor {
15199 name: "Point".into(),
15200 module: "default".into(),
15201 members: vec![],
15202 });
15203 let out = compile_and_emit_with("SELECT <default::Point>$p", &schema);
15204 assert!(out.sql.contains("($1)::jsonb"), "got:\n{}", out.sql);
15205 }
15206
15207 #[test]
15208 fn test_array_literal_cast_resolves_to_native_pg_array_not_jsonb() {
15209 let out = compile_and_emit("SELECT <array<str>>['foo', 'bar']");
15213 assert!(
15214 out.sql.contains("::text[]") || out.sql.contains("ARRAY["),
15215 "got:\n{}",
15216 out.sql
15217 );
15218 assert!(
15219 !out.sql.contains("jsonb"),
15220 "arrays must not use jsonb, got:\n{}",
15221 out.sql
15222 );
15223 }
15224
15225 #[test]
15226 fn test_array_literal_cast_applies_per_element_cast() {
15227 let out = compile_and_emit("SELECT <array<int64>>['1', '2']");
15231 assert!(out.sql.contains("ARRAY[('1')::int8, ('2')::int8]"), "got:\n{}", out.sql);
15232 }
15233
15234 #[test]
15235 fn test_array_param_cast_uses_direct_suffix_cast() {
15236 let out = compile_and_emit("SELECT <array<int64>>$p");
15237 assert!(out.sql.contains("::int8[]"), "got:\n{}", out.sql);
15238 }
15239
15240 #[test]
15241 fn test_array_of_named_tuple_element_casts_to_jsonb_array() {
15242 let out = compile_and_emit("SELECT <array<tuple<x: float64, y: float64>>>$p");
15246 assert!(out.sql.contains("::jsonb[]"), "got:\n{}", out.sql);
15247 }
15248
15249 #[test]
15250 fn test_contains_on_array_literal_cast_uses_array_overload_not_strpos() {
15251 let out = compile_and_emit("SELECT contains(<array<str>>[1, 2], '2')");
15257 assert!(out.sql.contains("@> ARRAY["), "got:\n{}", out.sql);
15258 assert!(
15259 !out.sql.contains("strpos"),
15260 "must not fall back to the str/str overload, got:\n{}",
15261 out.sql
15262 );
15263 }
15264
15265 #[test]
15266 fn test_nested_array_type_rejected_at_parse_time() {
15267 match parse::parse("SELECT <array<array<str>>>$p") {
15268 Ok(_) => panic!("expected parse error for nested array type"),
15269 Err(e) => assert!(e.to_string().contains("nested arrays are not supported"), "got: {}", e),
15270 }
15271 }
15272
15273 #[test]
15274 fn test_array_cast_in_computed_shape_field_schema_bound_context() {
15275 let out = compile_and_emit("SELECT Person { name, tags := <array<str>>['a', 'b'] }");
15278 assert!(out.sql.contains("ARRAY[('a')::text, ('b')::text]"), "got:\n{}", out.sql);
15279 }
15280
15281 #[test]
15282 fn test_nominal_named_tuple_cast_shape_carries_real_members() {
15283 use crate::schema::{TupleMemberDescriptor, TupleMemberKind};
15284 let mut schema = make_schema();
15285 schema.named_tuples.push(NamedTupleDescriptor {
15286 name: "Point".into(),
15287 module: "default".into(),
15288 members: vec![
15289 TupleMemberDescriptor {
15290 name: Some("x".into()),
15291 kind: TupleMemberKind::Scalar {
15292 pg_type: "float8".into(),
15293 },
15294 },
15295 TupleMemberDescriptor {
15296 name: Some("y".into()),
15297 kind: TupleMemberKind::Scalar {
15298 pg_type: "float8".into(),
15299 },
15300 },
15301 ],
15302 });
15303 let out = compile_and_emit_with("SELECT <default::Point>$p", &schema);
15304 match &out.shape.root {
15305 crate::query::ShapeNode::NamedTuple { type_name, members, .. } => {
15306 assert_eq!(type_name.as_deref(), Some("default::Point"));
15307 let members = members.as_ref().expect("expected resolved members");
15308 assert_eq!(members.len(), 2);
15309 assert_eq!(members[0].key.as_deref(), Some("x"));
15310 assert_eq!(members[1].key.as_deref(), Some("y"));
15311 }
15312 other => panic!("expected ShapeNode::NamedTuple, got {other:?}"),
15313 }
15314 }
15315
15316 #[test]
15317 fn test_structural_tuple_property_read_shape_carries_real_members() {
15318 use crate::schema::{TupleMemberDescriptor, TupleMemberKind};
15319 let schema = SchemaDescriptor {
15320 types: vec![TypeDescriptor {
15321 name: "Person".into(),
15322 module: "default".into(),
15323 table: "Person".into(),
15324 abstract_: false,
15325 materialized: false,
15326 description: None,
15327 parents: vec![],
15328 interfaces: vec![],
15329 bases: vec![],
15330 properties: vec![PropertyDescriptor {
15331 name: "address".into(),
15332 pg_type: "jsonb".into(),
15333 nullable: true,
15334 default_sql: None,
15335 default_pyql: None,
15336 description: None,
15337 check_constraints: vec![],
15338 is_exclusive: false,
15339 is_pk: false,
15340 is_readonly: false,
15341 rewrites: vec![],
15342 tuple_members: Some(vec![
15343 TupleMemberDescriptor {
15344 name: Some("street".into()),
15345 kind: TupleMemberKind::Scalar { pg_type: "text".into() },
15346 },
15347 TupleMemberDescriptor {
15348 name: Some("zip".into()),
15349 kind: TupleMemberKind::Scalar { pg_type: "text".into() },
15350 },
15351 ]),
15352 column_type: None,
15353 }],
15354 links: vec![],
15355 multilinks: vec![],
15356 computed: vec![],
15357 constraints: vec![],
15358 indexes: vec![],
15359 partition: None,
15360 vector_indexes: vec![],
15361 search_indexes: vec![],
15362 triggers: vec![],
15363 junction: false,
15364 signals: vec![],
15365 }],
15366 scalars: vec![],
15367 enums: vec![],
15368 named_tuples: vec![],
15369 globals: vec![],
15370 functions: vec![],
15371 aliases: vec![],
15372 channels: vec![],
15373 ..Default::default()
15374 };
15375 let out = compile_and_emit_with("SELECT Person { address }", &schema);
15376 assert!(out.sql.contains("::jsonb"), "got:\n{}", out.sql);
15377 match &out.shape.root {
15378 crate::query::ShapeNode::Object { pointers, .. } => {
15379 let address = pointers
15380 .iter()
15381 .find(|p| matches!(p, crate::query::ShapeNode::NamedTuple { name, .. } if name == "address"))
15382 .expect("expected address pointer in shape");
15383 match address {
15384 crate::query::ShapeNode::NamedTuple { type_name, members, .. } => {
15385 assert_eq!(*type_name, None);
15386 let members = members.as_ref().expect("expected resolved members");
15387 assert_eq!(members.len(), 2);
15388 assert_eq!(members[0].key.as_deref(), Some("street"));
15389 assert_eq!(members[1].key.as_deref(), Some("zip"));
15390 }
15391 other => panic!("expected NamedTuple, got {other:?}"),
15392 }
15393 }
15394 other => panic!("expected ShapeNode::Object, got {other:?}"),
15395 }
15396 }
15397
15398 #[test]
15399 fn test_bare_path_select_structural_tuple_property_shape_carries_real_members() {
15400 use crate::schema::{TupleMemberDescriptor, TupleMemberKind};
15410 let schema = SchemaDescriptor {
15411 types: vec![TypeDescriptor {
15412 name: "Person".into(),
15413 module: "default".into(),
15414 table: "Person".into(),
15415 abstract_: false,
15416 materialized: false,
15417 description: None,
15418 parents: vec![],
15419 interfaces: vec![],
15420 bases: vec![],
15421 properties: vec![PropertyDescriptor {
15422 name: "address".into(),
15423 pg_type: "jsonb".into(),
15424 nullable: true,
15425 default_sql: None,
15426 default_pyql: None,
15427 description: None,
15428 check_constraints: vec![],
15429 is_exclusive: false,
15430 is_pk: false,
15431 is_readonly: false,
15432 rewrites: vec![],
15433 tuple_members: Some(vec![
15434 TupleMemberDescriptor {
15435 name: Some("street".into()),
15436 kind: TupleMemberKind::Scalar { pg_type: "text".into() },
15437 },
15438 TupleMemberDescriptor {
15439 name: Some("zip".into()),
15440 kind: TupleMemberKind::Scalar { pg_type: "text".into() },
15441 },
15442 ]),
15443 column_type: None,
15444 }],
15445 links: vec![],
15446 multilinks: vec![],
15447 computed: vec![],
15448 constraints: vec![],
15449 indexes: vec![],
15450 partition: None,
15451 vector_indexes: vec![],
15452 search_indexes: vec![],
15453 triggers: vec![],
15454 junction: false,
15455 signals: vec![],
15456 }],
15457 scalars: vec![],
15458 enums: vec![],
15459 named_tuples: vec![],
15460 globals: vec![],
15461 functions: vec![],
15462 aliases: vec![],
15463 channels: vec![],
15464 ..Default::default()
15465 };
15466 let out = compile_and_emit_with("SELECT Person.address", &schema);
15467 match &out.shape.root {
15468 crate::query::ShapeNode::NamedTuple { type_name, members, .. } => {
15469 assert_eq!(*type_name, None);
15470 let members = members.as_ref().expect("expected resolved members");
15471 assert_eq!(members.len(), 2);
15472 assert_eq!(members[0].key.as_deref(), Some("street"));
15473 assert_eq!(members[1].key.as_deref(), Some("zip"));
15474 }
15475 other => panic!("expected ShapeNode::NamedTuple, got {other:?}"),
15476 }
15477 }
15478
15479 #[test]
15480 fn test_path_traversal_into_structural_tuple_property() {
15481 use crate::schema::{TupleMemberDescriptor, TupleMemberKind};
15482 let schema = SchemaDescriptor {
15483 types: vec![TypeDescriptor {
15484 name: "Person".into(),
15485 module: "default".into(),
15486 table: "Person".into(),
15487 abstract_: false,
15488 materialized: false,
15489 description: None,
15490 parents: vec![],
15491 interfaces: vec![],
15492 bases: vec![],
15493 properties: vec![PropertyDescriptor {
15494 name: "address".into(),
15495 pg_type: "jsonb".into(),
15496 nullable: true,
15497 default_sql: None,
15498 default_pyql: None,
15499 description: None,
15500 check_constraints: vec![],
15501 is_exclusive: false,
15502 is_pk: false,
15503 is_readonly: false,
15504 rewrites: vec![],
15505 tuple_members: Some(vec![
15506 TupleMemberDescriptor {
15507 name: Some("street".into()),
15508 kind: TupleMemberKind::Scalar { pg_type: "text".into() },
15509 },
15510 TupleMemberDescriptor {
15511 name: Some("zip".into()),
15512 kind: TupleMemberKind::Scalar { pg_type: "text".into() },
15513 },
15514 ]),
15515 column_type: None,
15516 }],
15517 links: vec![],
15518 multilinks: vec![],
15519 computed: vec![],
15520 constraints: vec![],
15521 indexes: vec![],
15522 partition: None,
15523 vector_indexes: vec![],
15524 search_indexes: vec![],
15525 triggers: vec![],
15526 junction: false,
15527 signals: vec![],
15528 }],
15529 scalars: vec![],
15530 enums: vec![],
15531 named_tuples: vec![],
15532 globals: vec![],
15533 functions: vec![],
15534 aliases: vec![],
15535 channels: vec![],
15536 ..Default::default()
15537 };
15538 let out = compile_and_emit_with("SELECT default::Person.address.street", &schema);
15544 assert!(out.sql.contains("\"address\"->'street'"), "got:\n{}", out.sql);
15545 }
15546
15547 #[test]
15548 fn test_structural_tuple_cast_shape_carries_real_members() {
15549 let out = compile_and_emit("SELECT <tuple<street: str, zip: str>>$p");
15550 match &out.shape.root {
15551 crate::query::ShapeNode::NamedTuple { type_name, members, .. } => {
15552 assert_eq!(*type_name, None);
15553 let members = members.as_ref().expect("expected resolved members");
15554 assert_eq!(members.len(), 2);
15555 assert_eq!(members[0].key.as_deref(), Some("street"));
15556 assert_eq!(members[1].key.as_deref(), Some("zip"));
15557 }
15558 other => panic!("expected ShapeNode::NamedTuple, got {other:?}"),
15559 }
15560 }
15561
15562 #[test]
15563 fn test_tuple_cast_mixed_named_and_unnamed_elements_rejected() {
15564 match parse::parse("SELECT <tuple<x: float64, bool>>$p") {
15565 Ok(_) => panic!("expected parse error for mixed named/unnamed tuple elements"),
15566 Err(e) => assert!(e.to_string().contains("all named or all unnamed"), "got: {}", e),
15567 }
15568 }
15569
15570 #[test]
15571 fn test_is_with_tuple_type_rejected() {
15572 let schema = make_schema();
15573 let ast = parse::parse("SELECT Person FILTER Person is tuple<x: float64, y: float64>").unwrap();
15574 match ir::compile(&ast, &schema) {
15575 Ok(_) => panic!("expected error for IS with a tuple type"),
15576 Err(e) => {
15577 assert!(
15578 e.to_string().contains("cannot use IS with a tuple or array type"),
15579 "got: {}",
15580 e
15581 );
15582 }
15583 }
15584 }
15585
15586 #[test]
15587 fn test_is_with_array_type_rejected() {
15588 let schema = make_schema();
15589 let ast = parse::parse("SELECT Person FILTER Person is array<str>").unwrap();
15590 match ir::compile(&ast, &schema) {
15591 Ok(_) => panic!("expected error for IS with an array type"),
15592 Err(e) => {
15593 assert!(
15594 e.to_string().contains("cannot use IS with a tuple or array type"),
15595 "got: {}",
15596 e
15597 );
15598 }
15599 }
15600 }
15601
15602 #[test]
15603 fn test_tuple_index_on_non_literal_falls_back_to_jsonb_index() {
15604 let out = compile_and_emit("SELECT (<tuple<int64, str>>('1', 3)).1");
15608 assert!(out.sql.contains("->1"), "got:\n{}", out.sql);
15609 }
15610
15611 #[test]
15612 fn test_tuple_index_out_of_bounds_on_cast_target_errors_at_compile_time() {
15613 let ast = parse::parse("SELECT (<tuple<int64, str>>('1', 3)).2").unwrap();
15619 let schema = make_schema();
15620 match ir::compile(&ast, &schema) {
15621 Ok(_) => panic!("expected out-of-bounds tuple index error"),
15622 Err(e) => {
15623 assert!(
15624 e.to_string()
15625 .contains("2 is not a member of tuple<std::int64, std::str>"),
15626 "got: {}",
15627 e
15628 );
15629 }
15630 }
15631 }
15632
15633 #[test]
15634 fn test_positional_tuple_literal_cast_to_tuple_type_compiles() {
15635 let out = compile_and_emit("SELECT <tuple<int64, str>>(1, 'x')");
15639 assert!(
15640 out.sql.contains("jsonb_build_array((1)::int8, ('x')::text)"),
15641 "got:\n{}",
15642 out.sql
15643 );
15644 }
15645
15646 #[test]
15647 fn test_positional_tuple_literal_cast_coerces_mismatched_literal_types() {
15648 let out = compile_and_emit("SELECT <tuple<int64, str>>('1', 3)");
15652 assert!(
15653 out.sql.contains("jsonb_build_array(('1')::int8, (3)::text)"),
15654 "got:\n{}",
15655 out.sql
15656 );
15657 }
15658
15659 #[test]
15660 fn test_nested_tuple_literal_cast_applies_casts_recursively() {
15661 let out = compile_and_emit(
15662 "SELECT <tuple<point: tuple<x: float64, y: float64>, label: str>>(point := ('1', 2), label := 5)",
15663 );
15664 assert!(
15665 out.sql.contains(
15666 "jsonb_build_object('point', jsonb_build_object('x', ('1')::float8, 'y', (2)::float8), 'label', (5)::text)"
15667 ),
15668 "got:\n{}",
15669 out.sql
15670 );
15671 }
15672
15673 #[test]
15674 fn test_positional_tuple_literal_nested_inside_named_tuple_compiles() {
15675 let out = compile_and_emit("SELECT (point := (1, 2), label := 'origin')");
15676 assert!(out.sql.contains("jsonb_build_array(1, 2)"), "got:\n{}", out.sql);
15677 assert!(out.sql.contains("jsonb_build_object("), "got:\n{}", out.sql);
15678 }
15679
15680 #[test]
15681 fn test_positional_tuple_literal_in_schema_bound_shape_field_compiles() {
15682 let out = compile_and_emit("SELECT Person { name, pair := (1, 2) }");
15683 assert!(out.sql.contains("jsonb_build_array(1, 2)"), "got:\n{}", out.sql);
15684 }
15685
15686 fn make_schema_with_search_index(backend: crate::schema::SearchBackend) -> SchemaDescriptor {
15695 use crate::schema::{SearchIndexDescriptor, SearchPointerDescriptor, SearchWeight};
15696 let mut s = make_schema();
15697 if let Some(td) = s.types.iter_mut().find(|t| t.name == "Person") {
15698 td.search_indexes.push(SearchIndexDescriptor {
15699 index_name: None,
15700 backend,
15701 pointers: vec![SearchPointerDescriptor {
15702 name: "name".into(),
15703 weight: SearchWeight::A,
15704 }],
15705 });
15706 }
15707 s
15708 }
15709
15710 #[test]
15711 fn test_insert_enqueues_a_meilisearch_outbox_row() {
15712 let schema = make_schema_with_search_index(crate::schema::SearchBackend::Meilisearch);
15713 let out = compile_and_emit_with("INSERT Person { name := 'Alice', age := 30 }", &schema);
15714 assert!(
15715 out.sql.contains("'Meilisearch'::_pylon.\"IndexKind\""),
15716 "expected a Meilisearch outbox enqueue CTE, got:\n{}",
15717 out.sql,
15718 );
15719 assert!(
15720 out.sql.contains("INSERT INTO _pylon.\"IndexOutbox\""),
15721 "got:\n{}",
15722 out.sql
15723 );
15724 }
15725
15726 #[test]
15727 fn test_insert_enqueues_an_opensearch_outbox_row() {
15728 let schema = make_schema_with_search_index(crate::schema::SearchBackend::OpenSearch);
15729 let out = compile_and_emit_with("INSERT Person { name := 'Alice', age := 30 }", &schema);
15730 assert!(
15731 out.sql.contains("'OpenSearch'::_pylon.\"IndexKind\""),
15732 "expected an OpenSearch outbox enqueue CTE, got:\n{}",
15733 out.sql,
15734 );
15735 }
15736
15737 #[test]
15738 fn test_insert_does_not_enqueue_an_outbox_row_for_a_postgres_backed_search_index() {
15739 let schema = make_schema_with_search_index(crate::schema::SearchBackend::Postgres);
15742 let out = compile_and_emit_with("INSERT Person { name := 'Alice', age := 30 }", &schema);
15743 assert!(
15744 !out.sql.contains("_pylon.\"IndexOutbox\""),
15745 "did not expect an outbox enqueue, got:\n{}",
15746 out.sql
15747 );
15748 }
15749
15750 #[test]
15751 fn test_update_enqueues_a_meilisearch_outbox_row() {
15752 let schema = make_schema_with_search_index(crate::schema::SearchBackend::Meilisearch);
15753 let out = compile_and_emit_with("UPDATE Person FILTER .name = 'Alice' SET { age := 31 }", &schema);
15754 assert!(
15755 out.sql.contains("'Meilisearch'::_pylon.\"IndexKind\""),
15756 "expected a Meilisearch outbox enqueue CTE, got:\n{}",
15757 out.sql,
15758 );
15759 }
15760
15761 #[test]
15762 fn test_delete_enqueues_a_meilisearch_outbox_delete_job() {
15763 let schema = make_schema_with_search_index(crate::schema::SearchBackend::Meilisearch);
15764 let out = compile_and_emit_with("DELETE Person FILTER .name = 'Alice'", &schema);
15765 assert!(
15766 out.sql.contains("'Meilisearch'::_pylon.\"IndexKind\""),
15767 "expected a Meilisearch outbox enqueue CTE, got:\n{}",
15768 out.sql,
15769 );
15770 assert!(
15771 out.sql.contains("'delete'"),
15772 "expected the delete operation literal, got:\n{}",
15773 out.sql
15774 );
15775 }
15776
15777 #[test]
15778 fn test_range_intrinsic_resolves_int_literals_to_int8range() {
15779 let out = compile_and_emit("SELECT std::overlaps(std::range(1, 3), std::range(2, 5))");
15784 assert!(out.sql.contains("int8range(1, 3)"), "got:\n{}", out.sql);
15785 assert!(out.sql.contains("int8range(2, 5)"), "got:\n{}", out.sql);
15786 assert!(
15787 out.sql.contains(" && "),
15788 "expected infix && for overlaps, got:\n{}",
15789 out.sql
15790 );
15791 assert!(
15792 !out.sql.contains("\"std\""),
15793 "must not emit a literal std schema call, got:\n{}",
15794 out.sql
15795 );
15796 }
15797
15798 #[test]
15799 fn test_range_intrinsic_resolves_datetime_to_tstzrange() {
15800 let out =
15801 compile_and_emit("SELECT std::range(<datetime>'2024-01-01T00:00:00Z', <datetime>'2024-06-01T00:00:00Z')");
15802 assert!(out.sql.contains("tstzrange("), "got:\n{}", out.sql);
15803 }
15804
15805 #[test]
15808 fn test_range_intrinsic_named_bounds_become_a_bounds_string() {
15809 let out = compile_and_emit("SELECT std::range(1, 3, inc_lower := true, inc_upper := true)");
15810 assert!(out.sql.contains("int8range(1, 3, '[]')"), "got:\n{}", out.sql);
15811 let out = compile_and_emit("SELECT std::range(1, 3, inc_lower := false)");
15812 assert!(out.sql.contains("int8range(1, 3, '()')"), "got:\n{}", out.sql);
15813 }
15814
15815 #[test]
15817 fn test_range_intrinsic_computes_a_bounds_string_from_an_expression() {
15818 let out = compile_and_emit("SELECT std::range(1, 3, inc_upper := <bool>$closed)");
15819 assert!(out.sql.contains("CASE WHEN"), "got:\n{}", out.sql);
15820 }
15821
15822 #[test]
15824 fn test_range_intrinsic_leaves_a_missing_upper_bound_unbounded() {
15825 let out = compile_and_emit("SELECT std::range(1)");
15826 assert!(out.sql.contains("int8range(1, NULL)"), "got:\n{}", out.sql);
15827 }
15828
15829 #[test]
15832 fn test_a_named_only_parameter_is_reported_as_named_only() {
15833 assert!(
15834 compile_err("SELECT std::range(1, 5, true, false)").contains("takes 'inc_lower' as a named argument only"),
15835 "{}",
15836 compile_err("SELECT std::range(1, 5, true, false)")
15837 );
15838 assert!(
15839 compile_err("SELECT std::json_set(std::to_json('{}'), 'a', std::to_json('1'))")
15840 .contains("requires the named argument 'value'"),
15841 "{}",
15842 compile_err("SELECT std::json_set(std::to_json('{}'), 'a', std::to_json('1'))")
15843 );
15844 assert!(
15846 compile_err("SELECT std::range()").contains("takes 1 or 2 argument(s), got 0"),
15847 "{}",
15848 compile_err("SELECT std::range()")
15849 );
15850 let message = compile_err("SELECT std::json_get(std::to_json('{}'), 'a', 1, 2)");
15852 assert!(message.contains("default := optional<json>"), "{message}");
15853 }
15854
15855 #[test]
15856 fn test_range_intrinsic_builds_the_empty_range() {
15857 let out = compile_and_emit("SELECT std::range(1, 3, empty := true)");
15858 assert!(out.sql.contains("'empty'::int8range"), "got:\n{}", out.sql);
15859 }
15860
15861 #[test]
15862 fn test_multirange_intrinsic_resolves_from_range_element() {
15863 let out = compile_and_emit("SELECT std::multirange([std::range(1, 3), std::range(5, 7)])");
15864 assert!(out.sql.contains("int8multirange(VARIADIC "), "got:\n{}", out.sql);
15865 assert!(
15866 !out.sql.contains("\"std\""),
15867 "must not emit a literal std schema call, got:\n{}",
15868 out.sql
15869 );
15870 }
15871}