1use crate::ir::{
21 IrArraySource, IrCteDef, IrDelete, IrExpr, IrFor, IrForIterator, IrFreeExpr, IrFtsSearch, IrFunctionSelect,
22 IrGlobalCte, IrGroup, IrGroupOutput, IrInsert, IrLiteral, IrLockClause, IrLockStrength, IrLockWait,
23 IrMultiLinkJoin, IrMultiLinkMutation, IrMultiLinkPointer, IrMultiLinkValueSource, IrMultiLinkValues, IrNulls,
24 IrOutput, IrPathJoin, IrPathResult, IrPathSelect, IrPolyFanout, IrPolyImplementor, IrRewrite, IrRowSource,
25 IrScalarPointer, IrScalarSetPointer, IrSelect, IrShapePointer, IrSingleLinkCorrelation, IrSingleLinkPointer,
26 IrSort, IrSortDir, IrSource, IrStmt, IrUpdate, IrVectorSearch, SearchEnqueueInfo, VectorEnqueueInfo,
27};
28use crate::parse::ast::{BinOpKind, UnaryOpKind};
29use crate::query::{Cardinality, InferencePlan, ShapeDescriptor, ShapeNode};
30use std::collections::HashMap;
31
32pub struct SqlOutput {
33 pub sql: String,
34 pub shape: ShapeDescriptor,
35 pub inference_plan: Option<InferencePlan>,
36}
37
38fn update_set_fragments(assignments: &[(String, IrExpr)], rewrites: &[IrRewrite], indent: &str) -> Vec<String> {
49 let rewrite_cols: std::collections::HashSet<&str> = rewrites.iter().map(|r| r.column.as_str()).collect();
50 assignments
51 .iter()
52 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
53 .map(|(col, expr)| format!("{indent}{} = {}", qi(col), emit_expr(expr)))
54 .chain(
55 rewrites
56 .iter()
57 .map(|rw| format!("{indent}{} = {}", qi(&rw.column), emit_expr(&rw.expr))),
58 )
59 .collect()
60}
61
62fn emit_for_global_cte(stmt: &IrStmt) -> String {
64 match stmt {
65 IrStmt::Select(sel) => match sel.rows.as_slice() {
66 [IrRowSource::Bound { source, .. }] => {
67 let alias = &source.alias;
68 let mut sql = format!(
69 "SELECT {}.\"id\" AS \"value\"\nFROM {} AS {}",
70 qi(alias),
71 source_ref(source),
72 qi(alias)
73 );
74 append_filter(&mut sql, &sel.filter);
75 sql
76 }
77 rows => match rows.first() {
78 Some(IrRowSource::Free(IrFreeExpr::Scalar(e))) => format!("SELECT {} AS \"value\"", emit_expr(e)),
79 _ => "SELECT NULL AS \"value\"".to_string(),
80 },
81 },
82 IrStmt::PathSelect(sel) => {
83 let from_sql = emit_path_joins(&sel.root, &sel.joins);
84 let scalar_expr = match &sel.result {
85 IrPathResult::Scalar(e, _) => emit_expr(e),
86 IrPathResult::Object { alias, .. } => format!("{}.\"id\"", qi(alias)),
87 };
88 let mut sql = format!("SELECT {} AS \"value\"\nFROM {}", scalar_expr, from_sql);
89 append_filter(&mut sql, &sel.filter);
90 append_order_by(&mut sql, &sel.order_by);
91 append_offset_limit(&mut sql, &sel.offset, &sel.limit);
92 sql
93 }
94 _ => "SELECT NULL AS \"value\"".to_string(),
95 }
96}
97
98fn emit_global_cte_parts(global_ctes: &[IrGlobalCte]) -> Vec<String> {
99 global_ctes
100 .iter()
101 .map(|g| match g {
102 IrGlobalCte::Session(s) => format!(
103 "\"{}\" AS (SELECT ${}::{} AS \"value\")",
104 s.cte_name,
105 s.param_index + 1,
106 s.pg_type
107 ),
108 IrGlobalCte::Computed(c) => {
109 let body = emit_for_global_cte(&c.stmt);
110 format!("\"{}\" AS (\n{}\n)", c.cte_name, body)
111 }
112 })
113 .collect()
114}
115
116pub fn emit(ir: &IrOutput) -> SqlOutput {
117 let out = with_subtype_fanouts(&ir.subtype_fanouts, || {
118 with_correlated_ctes(&ir.ctes, || emit_output(ir))
119 });
120 #[cfg(debug_assertions)]
121 if let Some(problem) = forward_cte_reference(&out.sql) {
122 panic!("{problem}\n{}", out.sql);
123 }
124 out
125}
126
127#[cfg(debug_assertions)]
133fn forward_cte_reference(sql: &str) -> Option<String> {
134 let definitions: Vec<(String, usize)> = sql
135 .match_indices("\" AS (")
136 .filter_map(|(end, _)| {
137 let head = &sql[..end];
138 let start = head.rfind('"')?;
139 Some((head[start + 1..].to_string(), start))
140 })
141 .collect();
142 for (name, defined_at) in &definitions {
143 let quoted = qi(name);
146 for prefix in ["FROM ", "JOIN ", "CROSS JOIN "] {
147 let needle = format!("{prefix}{quoted}");
148 if let Some(used_at) = sql.find(&needle)
149 && used_at < *defined_at
150 {
151 return Some(format!(
152 "CTE {quoted} is read at byte {used_at} but not defined until {defined_at}"
153 ));
154 }
155 }
156 }
157 None
158}
159
160fn emit_output(ir: &IrOutput) -> SqlOutput {
161 let mut out = match &ir.stmt {
162 IrStmt::Update(upd) => emit_update_stmt(upd, &ir.ctes),
163 IrStmt::For(f) => emit_for_stmt(f, &ir.ctes),
164 stmt => {
165 let mut o = match stmt {
166 IrStmt::Select(sel) => emit_select_stmt(sel, &ir.ctes),
167 IrStmt::PathSelect(sel) => emit_path_select(sel),
168 IrStmt::Insert(ins) => emit_insert_stmt(ins),
169 IrStmt::Delete(del) => emit_delete_stmt(del),
170 IrStmt::Group(grp) => emit_group(grp),
171 IrStmt::FunctionSelect(sel) => emit_function_select(sel),
172 IrStmt::VectorSearch(vs) => emit_vector_search(vs),
173 IrStmt::FtsSearch(fs) => emit_fts_search(fs),
174 IrStmt::ScalarUnion(branches) => SqlOutput {
175 sql: emit_scalar_union(branches),
176 shape: ShapeDescriptor {
177 root: ShapeNode::Scalar {
178 name: String::new(),
179 position: 0,
180 },
181 },
182 inference_plan: None,
183 },
184 IrStmt::Update(_) | IrStmt::For(_) => unreachable!(),
185 };
186 if !ir.ctes.is_empty() {
187 o.sql = merge_into_existing_with(&o.sql, &emit_user_cte_parts(&ir.ctes))
191 .unwrap_or_else(|| format!("{}{}", emit_cte_prefix(&ir.ctes), o.sql));
192 }
193 o
194 }
195 };
196
197 if !ir.global_ctes.is_empty() {
198 let global_parts = emit_global_cte_parts(&ir.global_ctes);
199 out.sql = merge_into_existing_with(&out.sql, &global_parts)
200 .unwrap_or_else(|| format!("WITH {}\n{}", global_parts.join(",\n "), out.sql));
201 }
202
203 out
204}
205
206fn unwrap_unnest_for_any(right: &IrExpr, rendered: &str) -> String {
239 if let Some(array) = unnest_argument(right) {
240 return array;
241 }
242 if let IrExpr::TypeCast(cast) = right
245 && let Some(array) = unnest_argument(&cast.expr)
246 {
247 return format!("({})::{}[]", array, cast.pg_type);
248 }
249 rendered.to_string()
250}
251
252fn unnest_argument(expr: &IrExpr) -> Option<String> {
254 let IrExpr::FunctionCall(call) = expr else {
255 return None;
256 };
257 (call.schema.is_none() && call.name == "unnest" && call.sql_template.is_none() && call.args.len() == 1)
258 .then(|| emit_expr(&call.args[0]))
259}
260
261fn merge_into_existing_with(sql: &str, parts: &[String]) -> Option<String> {
262 if parts.is_empty() || !(sql.starts_with("WITH\n") || sql.starts_with("WITH ")) {
263 return None;
264 }
265 Some(format!("WITH {},\n {}", parts.join(",\n "), &sql[5..]))
266}
267
268fn qi(s: &str) -> String {
271 format!("\"{}\"", s.replace('"', "\"\""))
272}
273
274fn pg_schema(module: &str) -> String {
275 if module == "default" {
276 "\"public\"".into()
277 } else {
278 qi(module)
279 }
280}
281
282pub fn pg_schema_str(module: &str) -> String {
283 pg_schema(module)
284}
285
286fn qn(module: &str, name: &str) -> String {
287 format!("{}.{}", pg_schema(module), qi(name))
288}
289
290fn sql_str(s: &str) -> String {
291 format!("'{}'", s.replace('\'', "''"))
292}
293
294fn type_disc(type_name: &str) -> String {
296 format!("{}::text", sql_str(type_name))
297}
298
299fn module_of(type_name: &str) -> &str {
301 type_name.split("::").next().unwrap_or("public")
302}
303
304fn source_ref(src: &IrSource) -> String {
305 if let Some(cte_name) = src.table.strip_prefix("@cte:") {
307 return match correlated_cte_iterator(cte_name) {
311 Some(iterator) => format!(
312 "(SELECT * FROM {} WHERE {}.{} = {}.\"v\")",
313 qi(cte_name),
314 qi(cte_name),
315 qi(OUTER_KEY),
316 qi(&iterator),
317 ),
318 None => qi(cte_name),
319 };
320 }
321 if let Some(row) = src.table.strip_prefix("@row:") {
323 return format!("(SELECT ({row}).*)");
324 }
325 match source_fanout(src) {
328 Some(fanout) => format!("(\n{}\n)", emit_poly_union(&fanout.implementors, &fanout.columns)),
329 None => junction_ref(module_of(&src.type_name), &src.table),
330 }
331}
332
333fn target_ref(src: &IrSource) -> String {
336 match src.table.strip_prefix("@cte:") {
337 Some(cte_name) => qi(cte_name),
338 None => qn(module_of(&src.type_name), &src.table),
339 }
340}
341
342thread_local! {
343 static SUBTYPE_FANOUTS: std::cell::RefCell<HashMap<(String, String), IrPolyFanout>> =
345 std::cell::RefCell::new(HashMap::new());
346
347 static CORRELATED_CTES: std::cell::RefCell<HashMap<String, String>> =
350 std::cell::RefCell::new(HashMap::new());
351}
352
353fn correlated_cte_iterator(cte_name: &str) -> Option<String> {
356 CORRELATED_CTES.with(|cell| cell.borrow().get(cte_name).cloned())
357}
358
359fn with_correlated_ctes<T>(ctes: &[IrCteDef], emit: impl FnOnce() -> T) -> T {
362 fn collect(ctes: &[IrCteDef], into: &mut HashMap<String, String>) {
363 for c in ctes {
364 if let Some(iterator) = &c.correlated_to {
365 into.insert(c.name.clone(), iterator.clone());
366 }
367 match &c.stmt {
368 IrStmt::For(f) => collect(&f.body_ctes, into),
369 IrStmt::Insert(ins) => collect(&ins.nested_ctes, into),
370 IrStmt::Update(upd) => collect(&upd.nested_ctes, into),
371 _ => {}
372 }
373 }
374 }
375 let mut collected = HashMap::new();
376 collect(ctes, &mut collected);
377 let previous = CORRELATED_CTES.with(|cell| cell.replace(collected));
378 let result = emit();
379 CORRELATED_CTES.with(|cell| cell.replace(previous));
380 result
381}
382
383fn with_subtype_fanouts<T>(fanouts: &HashMap<(String, String), IrPolyFanout>, emit: impl FnOnce() -> T) -> T {
386 let previous = SUBTYPE_FANOUTS.with(|cell| cell.replace(fanouts.clone()));
387 let result = emit();
388 SUBTYPE_FANOUTS.with(|cell| cell.replace(previous));
389 result
390}
391
392fn source_fanout(src: &IrSource) -> Option<IrPolyFanout> {
395 if let Some(fanout) = &src.poly {
396 return Some(fanout.clone());
397 }
398 if src.table.starts_with('@') {
399 return None;
400 }
401 let key = (module_of(&src.type_name).to_string(), src.table.clone());
402 SUBTYPE_FANOUTS.with(|cell| cell.borrow().get(&key).cloned())
403}
404
405pub const DML_CTE: &str = "_dml";
415
416fn junction_ref(module: &str, junction_table: &str) -> String {
417 if let Some(cte_name) = junction_table.strip_prefix("@cte:") {
418 return qi(cte_name);
419 }
420 let Some((tables, columns)) = crate::ir::parse_inherited_junction(junction_table) else {
421 return qn(module, junction_table);
422 };
423 let columns = columns.iter().map(|c| qi(c)).collect::<Vec<_>>().join(", ");
424 let branches = tables
425 .iter()
426 .map(|(module, table)| format!("SELECT {columns} FROM {}", qn(module, table)))
427 .collect::<Vec<_>>()
428 .join(" UNION ALL ");
429 format!("({branches})")
430}
431
432fn result_type_disc(path: &IrPathSelect, alias: &str, type_name: &str) -> String {
437 match terminal_source(path, alias) {
438 Some(source) => source_type_disc(source),
439 None => type_disc(type_name),
440 }
441}
442
443fn terminal_source<'a>(path: &'a IrPathSelect, alias: &str) -> Option<&'a IrSource> {
444 if path.root.alias == alias {
445 return Some(&path.root);
446 }
447 path.joins
448 .iter()
449 .map(path_join_target)
450 .find(|target| target.alias == alias)
451}
452
453fn path_join_target(join: &IrPathJoin) -> &IrSource {
454 match join {
455 IrPathJoin::Single { target, .. }
456 | IrPathJoin::Multi { target, .. }
457 | IrPathJoin::BacklinkSingle { target, .. }
458 | IrPathJoin::BacklinkMulti { target, .. }
459 | IrPathJoin::Function { target, .. }
460 | IrPathJoin::Lateral { target, .. } => target,
461 }
462}
463
464fn source_type_disc(src: &IrSource) -> String {
467 match source_fanout(src) {
468 Some(_) => format!("{}.\"__type__\"", qi(&src.alias)),
469 None => type_disc(&src.type_name),
470 }
471}
472
473fn emit_poly_union(implementors: &[IrPolyImplementor], columns: &[String]) -> String {
476 let col_list = columns.iter().map(|c| qi(c)).collect::<Vec<_>>().join(", ");
477 implementors
478 .iter()
479 .map(|imp| {
480 format!(
481 " SELECT {}::text AS \"__type__\", {} FROM {}",
482 sql_str(&imp.type_name),
483 col_list,
484 qn(&imp.module, &imp.table),
485 )
486 })
487 .collect::<Vec<_>>()
488 .join("\n UNION ALL\n")
489}
490
491fn emit_select_stmt(sel: &IrSelect, ctes: &[IrCteDef]) -> SqlOutput {
494 match sel.rows.as_slice() {
495 [IrRowSource::Bound { source, shape }] => emit_bound_select(sel, source, shape),
496 rows if rows.len() > 1 && rows.iter().all(|r| matches!(r, IrRowSource::Bound { .. })) => {
497 emit_bound_union_select(sel, rows)
498 }
499 rows => emit_free_rows(sel, rows, ctes),
500 }
501}
502
503fn bound_union_from_clause(rows: &[IrRowSource], common_columns: &[String]) -> String {
510 let columns = common_columns.iter().map(|c| qi(c)).collect::<Vec<_>>().join(", ");
511 rows.iter()
512 .map(|row| match row {
513 IrRowSource::Bound { source, .. } if !common_columns.is_empty() => {
514 let own_type = match source_fanout(source) {
515 Some(_) => "\"__type__\"".to_string(),
516 None => format!("{} AS \"__type__\"", type_disc(&source.type_name)),
517 };
518 format!(" SELECT {own_type}, {columns} FROM {}", source_ref(source))
519 }
520 IrRowSource::Bound { source, .. } => format!(" SELECT * FROM {}", source_ref(source)),
521 IrRowSource::Free(_) => unreachable!("caller checked every row is bound"),
522 })
523 .collect::<Vec<_>>()
524 .join("\n UNION ALL\n")
525}
526
527fn emit_bound_union_select(sel: &IrSelect, rows: &[IrRowSource]) -> SqlOutput {
528 let IrRowSource::Bound { source, shape } = &rows[0] else {
529 unreachable!("caller checked every row is bound")
530 };
531 let alias = &source.alias;
532 let (pointer_exprs, shape_pointers) = build_shape(shape, alias);
533 let row_type = if sel.poly_columns.is_empty() {
534 type_disc(&source.type_name)
535 } else {
536 format!("{}.\"__type__\"", qi(alias))
537 };
538 let mut parts = vec![row_type];
539 parts.extend(pointer_exprs);
540
541 let mut sql = format!(
542 "SELECT {}(\n {}\n) AS result\nFROM (\n{}\n) AS {}",
543 if sel.distinct { "DISTINCT " } else { "" },
544 parts.join(",\n "),
545 bound_union_from_clause(rows, &sel.poly_columns),
546 qi(alias),
547 );
548 append_filter(&mut sql, &sel.filter);
549 append_order_by(&mut sql, &sel.order_by);
550 append_offset_limit(&mut sql, &sel.offset, &sel.limit);
551
552 SqlOutput {
553 sql,
554 shape: ShapeDescriptor {
555 root: ShapeNode::Object {
556 name: String::new(),
557 type_name: Some(source.type_name.clone()),
558 position: 0,
559 cardinality: Cardinality::Many,
560 pointers: prepend_type(shape_pointers),
561 has_implicit_id: shape_has_implicit_id(shape),
562 },
563 },
564 inference_plan: None,
565 }
566}
567
568fn emit_bound_select(sel: &IrSelect, source: &IrSource, shape: &[IrShapePointer]) -> SqlOutput {
574 let alias = &source.alias;
575 let (pointer_exprs, shape_pointers) = build_shape(shape, alias);
576
577 let type_expr = if sel.polymorphic {
578 format!("{}.\"__type__\"", qi(alias))
579 } else {
580 type_disc(&source.type_name)
581 };
582 let mut parts = vec![type_expr];
583 parts.extend(pointer_exprs);
584 let tuple = parts.join(",\n ");
585
586 let distinct = if sel.distinct { "DISTINCT " } else { "" };
587
588 let from_clause = if let Some(dml) = &sel.dml_source {
590 let mut cte_parts = match dml.as_ref() {
591 IrStmt::Update(upd) if update_has_any_multilink(upd) => emit_update_multilink_ctes(upd, DML_CTE),
592 IrStmt::Insert(ins) if insert_has_any_multilink(ins) => emit_insert_multilink_ctes(ins, DML_CTE),
593 IrStmt::Insert(ins) if !ins.nested_ctes.is_empty() => {
596 let mut parts = emit_user_cte_parts(&ins.nested_ctes);
597 parts.push(format!("\"_dml\" AS (\n{}\n)", emit_dml_as_cte_source(dml)));
598 parts
599 }
600 IrStmt::Update(upd) if !upd.nested_ctes.is_empty() => {
601 let mut parts = emit_user_cte_parts(&upd.nested_ctes);
602 parts.push(format!("\"_dml\" AS (\n{}\n)", emit_dml_as_cte_source(dml)));
603 parts
604 }
605 IrStmt::Update(upd) if !upd.poly_implementors.is_empty() => emit_poly_update_dml_ctes(upd, "_dml"),
606 IrStmt::Delete(del) if !del.poly_implementors.is_empty() => emit_poly_delete_dml_ctes(del, "_dml"),
607 _ => vec![format!("\"_dml\" AS (\n{}\n)", emit_dml_as_cte_source(dml))],
608 };
609 let (enqueue_v, enqueue_s) = match dml.as_ref() {
610 IrStmt::Insert(ins) => (ins.enqueue_vector.as_slice(), ins.enqueue_search.as_slice()),
611 IrStmt::Update(upd) => (upd.enqueue_vector.as_slice(), upd.enqueue_search.as_slice()),
612 _ => (&[][..], &[][..]),
613 };
614 cte_parts.extend(enqueue_ctes(enqueue_v, "_dml"));
615 cte_parts.extend(enqueue_search_ctes(enqueue_s, "_dml", enqueue_v.len()));
616 format!(
617 "WITH\n{}\nSELECT {}(\n {}\n) AS result\nFROM \"_dml\" AS {}",
618 cte_parts.join(",\n"),
619 distinct,
620 tuple,
621 qi(alias)
622 )
623 } else if sel.polymorphic && !source.table.starts_with("@cte:") {
624 let union_sql = emit_poly_union(&sel.poly_implementors, &sel.poly_columns);
626 format!(
627 "SELECT {}(\n {}\n) AS result\nFROM (\n{}\n) AS {}",
628 distinct,
629 tuple,
630 union_sql,
631 qi(alias)
632 )
633 } else {
634 format!(
637 "SELECT {}(\n {}\n) AS result\nFROM {} AS {}",
638 distinct,
639 tuple,
640 source_ref(source),
641 qi(alias)
642 )
643 };
644
645 let mut sql = from_clause;
646 append_filter(&mut sql, &sel.filter);
647 append_order_by(&mut sql, &sel.order_by);
648 append_offset_limit(&mut sql, &sel.offset, &sel.limit);
649 append_lock_clause(&mut sql, &sel.lock);
650
651 let root_pointers = prepend_type(shape_pointers);
652 SqlOutput {
653 sql,
654 shape: ShapeDescriptor {
655 root: ShapeNode::Object {
656 name: String::new(),
657 type_name: Some(source.type_name.clone()),
658 position: 0,
659 cardinality: Cardinality::Many,
660 pointers: root_pointers,
661 has_implicit_id: shape_has_implicit_id(shape),
662 },
663 },
664 inference_plan: None,
665 }
666}
667
668fn nested_cte_from(ctes: &[IrCteDef], reading: &str, lead: &str) -> String {
675 let joined: Vec<String> = ctes
676 .iter()
677 .map(|c| qi(&c.name))
678 .filter(|name| reading.contains(&format!("{name}.")))
679 .collect();
680 if joined.is_empty() {
681 return String::new();
682 }
683 format!("{lead}FROM {}", joined.join(", "))
684}
685
686fn emit_scalar_union(branches: &[IrStmt]) -> String {
689 let operands: Vec<String> = branches
690 .iter()
691 .enumerate()
692 .map(|(i, branch)| {
693 format!(
694 "SELECT \"v\" FROM (\n{}\n) AS \"_u{i}\"",
695 emit_dml_as_cte_source(branch)
696 )
697 })
698 .collect();
699 format!(
700 "SELECT ROW(v) AS result, v FROM (\n{}\n) AS _scalar",
701 operands.join("\nUNION ALL\n")
702 )
703}
704
705fn emit_dml_as_cte_source(stmt: &IrStmt) -> String {
706 match stmt {
707 IrStmt::ScalarUnion(branches) => format!(" {}", emit_scalar_union(branches)),
708 IrStmt::Insert(ins) => {
709 let rewrite_cols: std::collections::HashSet<&str> =
710 ins.rewrites.iter().map(|r| r.column.as_str()).collect();
711 let cols: Vec<String> = ins
712 .assignments
713 .iter()
714 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
715 .map(|(c, _)| format!(" {}", qi(c)))
716 .chain(ins.rewrites.iter().map(|r| format!(" {}", qi(&r.column))))
717 .collect();
718 let vals: Vec<String> = ins
719 .assignments
720 .iter()
721 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
722 .map(|(_, e)| format!(" {}", emit_expr(e)))
723 .chain(ins.rewrites.iter().map(|r| format!(" {}", emit_expr(&r.expr))))
724 .collect();
725 let mut sql = if !ins.nested_ctes.is_empty() || ins.guard.is_some() {
726 format!(" {}", emit_insert_row_sql(ins))
733 } else if cols.is_empty() {
734 format!(" INSERT INTO {} DEFAULT VALUES", target_ref(&ins.target))
736 } else {
737 format!(
738 " INSERT INTO {} (\n{}\n ) VALUES (\n{}\n )",
739 target_ref(&ins.target),
740 cols.join(",\n"),
741 vals.join(",\n"),
742 )
743 };
744 if let Some(conflict) = &ins.unless_conflict {
745 emit_conflict(&mut sql, conflict);
746 }
747 sql.push_str("\n RETURNING *");
748 sql
749 }
750 IrStmt::Update(upd) => {
751 let alias = &upd.target.alias;
760 let sets = update_set_fragments(&upd.assignments, &upd.rewrites, " ");
761 let mut sql = format!(
762 " UPDATE {} AS {}\n SET\n{}",
763 target_ref(&upd.target),
764 qi(alias),
765 sets.join(",\n"),
766 );
767 sql.push_str(&nested_cte_from(&upd.nested_ctes, &sets.join(","), "\n "));
770 append_filter(&mut sql, &upd.filter);
771 sql.push_str(&format!("\n RETURNING {}.*", qi(alias)));
774 sql
775 }
776 IrStmt::Delete(del) => {
777 let alias = &del.target.alias;
778 let mut sql = format!(" DELETE FROM {} AS {}", target_ref(&del.target), qi(alias),);
779 append_filter(&mut sql, &del.filter);
780 sql.push_str("\n RETURNING *");
781 sql
782 }
783 IrStmt::Select(inner) => match inner.rows.as_slice() {
784 [IrRowSource::Bound { source, .. }]
789 if let Some(read) = inner.dml_source.as_deref()
790 && !matches!(read, IrStmt::Insert(_) | IrStmt::Update(_) | IrStmt::Delete(_)) =>
791 {
792 let mut sql = format!(
794 " SELECT {}* FROM (\n{}\n ) AS {}",
795 if inner.distinct { "DISTINCT " } else { "" },
796 emit_dml_as_cte_source(read),
797 qi(&source.alias),
798 );
799 append_filter(&mut sql, &inner.filter);
800 append_order_by(&mut sql, &inner.order_by);
801 append_offset_limit(&mut sql, &inner.offset, &inner.limit);
802 sql
803 }
804 [IrRowSource::Bound { source, .. }] => {
805 let from = if inner.polymorphic && !source.table.starts_with("@cte:") {
808 format!(
809 "(\n{}\n ) AS {}",
810 emit_poly_union(&inner.poly_implementors, &inner.poly_columns),
811 qi(&source.alias),
812 )
813 } else {
814 format!("{} AS {}", source_ref(source), qi(&source.alias))
815 };
816 let mut sql = format!(
817 " SELECT {}* FROM {}",
818 if inner.distinct { "DISTINCT " } else { "" },
819 from
820 );
821 append_filter(&mut sql, &inner.filter);
822 append_order_by(&mut sql, &inner.order_by);
823 append_offset_limit(&mut sql, &inner.offset, &inner.limit);
824 sql
825 }
826 rows if rows.len() > 1 && rows.iter().all(|r| matches!(r, IrRowSource::Bound { .. })) => {
829 let IrRowSource::Bound { source, .. } = &rows[0] else {
830 unreachable!("checked by the guard")
831 };
832 let mut sql = format!(
833 " SELECT {}* FROM (\n{}\n ) AS {}",
834 if inner.distinct { "DISTINCT " } else { "" },
835 bound_union_from_clause(rows, &inner.poly_columns),
836 qi(&source.alias),
837 );
838 append_filter(&mut sql, &inner.filter);
839 append_order_by(&mut sql, &inner.order_by);
840 append_offset_limit(&mut sql, &inner.offset, &inner.limit);
841 sql
842 }
843 _ => emit_select_stmt(inner, &[]).sql,
849 },
850 IrStmt::FunctionSelect(sel) => {
851 let args_sql = sel.fn_args.iter().map(emit_expr).collect::<Vec<_>>().join(", ");
854 let fn_call = format!("{}.{}({})", pg_schema(&sel.fn_module), qi(&sel.fn_name), args_sql);
855 let mut sql = format!(" SELECT * FROM {} AS {}", fn_call, qi(&sel.alias));
856 append_filter(&mut sql, &sel.filter);
857 append_order_by(&mut sql, &sel.order_by);
858 append_offset_limit(&mut sql, &sel.offset, &sel.limit);
859 sql
860 }
861 IrStmt::For(f) => {
862 if matches!(&f.iterator, IrForIterator::Values { exprs, .. } if exprs.is_empty()) {
867 return " SELECT NULL AS \"id\" WHERE FALSE".to_string();
868 }
869 let (values_from, _) = emit_for_iterator(&f.iterator, &format!("_for_{}", f.var_name));
870 let body =
871 prefix_body_ctes(emit_dml_as_cte_source(&f.body), &uncorrelated_body_ctes(f)).replace('\n', "\n ");
872 format!(
873 " SELECT \"_body\".*\n FROM {}\n CROSS JOIN LATERAL (\n {}\n ) AS \"_body\"",
874 values_from.replace('\n', "\n "),
875 body,
876 )
877 }
878 IrStmt::Group(grp) if matches!(grp.output, IrGroupOutput::Elements) => emit_group_elements_source(grp),
879 IrStmt::Group(_) | IrStmt::VectorSearch(_) | IrStmt::FtsSearch(_) => {
880 unreachable!("cannot appear as a CTE source")
881 }
882 IrStmt::PathSelect(ps) => {
883 let distinct = if ps.distinct { "DISTINCT " } else { "" };
889 let projection = match &ps.result {
890 IrPathResult::Scalar(e, _) => format!("{}{} AS v", distinct, emit_expr(e)),
894 IrPathResult::Object { alias, .. } => format!("{}{}.*", distinct, qi(alias)),
895 };
896 let mut sql = format!(
897 "SELECT {}\n FROM {}",
898 projection,
899 emit_path_joins(&ps.root, &ps.joins)
900 );
901 append_filter(&mut sql, &ps.filter);
902 append_order_by(&mut sql, &ps.order_by);
903 append_offset_limit(&mut sql, &ps.offset, &ps.limit);
904 match &ps.result {
905 IrPathResult::Scalar(e, _) => {
906 let row_value = if enum_type_of_expr(e).is_some() { "v::text" } else { "v" };
907 format!(" SELECT ROW({row_value}) AS result, v FROM ({sql}) AS _scalar")
908 }
909 IrPathResult::Object { .. } => format!(" {sql}"),
910 }
911 }
912 }
913}
914
915fn emit_poly_update_dml_ctes(upd: &IrUpdate, name: &str) -> Vec<String> {
950 let alias = &upd.target.alias;
951 let sets = update_set_fragments(&upd.assignments, &upd.rewrites, "");
952 let col_list = upd.poly_columns.iter().map(|c| qi(c)).collect::<Vec<_>>().join(", ");
953
954 let mut cte_parts = vec![];
955 let mut union_parts = vec![];
956 for (i, imp) in upd.poly_implementors.iter().enumerate() {
957 let cte_name = format!("{}__u{}", name, i);
958 let mut upd_sql = format!(
959 "UPDATE {} AS {}\nSET {}",
960 qn(&imp.module, &imp.table),
961 qi(alias),
962 sets.join(", "),
963 );
964 append_filter(&mut upd_sql, &upd.filter);
965 upd_sql.push_str(&format!("\nRETURNING {}", col_list));
966 cte_parts.push(format!("\"{}\" AS (\n{}\n)", cte_name, upd_sql));
967
968 union_parts.push(format!(
969 "SELECT {}::text AS \"__type__\", {} FROM \"{}\"",
970 sql_str(&imp.type_name),
971 col_list,
972 cte_name,
973 ));
974 }
975 cte_parts.push(format!("\"{}\" AS (\n{}\n)", name, union_parts.join("\nUNION ALL\n")));
976 cte_parts
977}
978
979fn concrete_poly_update(upd: &IrUpdate, imp: &IrPolyImplementor) -> IrUpdate {
984 let interface_prefix = format!("{}.", upd.target.table);
985 let own_prefix = format!("{}.", imp.table);
986 let own_junction = |table: &mut String, module: &mut String| {
987 if let Some(link) = table.strip_prefix(&interface_prefix) {
988 *table = format!("{own_prefix}{link}");
989 module.clone_from(&imp.module);
990 }
991 };
992 let mut concrete = upd.clone();
993 concrete.poly_implementors = vec![];
994 concrete.target.table = imp.table.clone();
995 concrete.target.type_name = imp.type_name.clone();
996 concrete.target.poly = None;
997 concrete
998 .multi_link_clears
999 .iter_mut()
1000 .for_each(|c| own_junction(&mut c.junction_table, &mut c.module));
1001 concrete
1002 .multi_link_replaces
1003 .iter_mut()
1004 .chain(concrete.multi_link_appends.iter_mut())
1005 .chain(concrete.multi_link_removals.iter_mut())
1006 .for_each(|m| own_junction(&mut m.junction_table, &mut m.module));
1007 concrete
1008}
1009
1010fn emit_poly_update_multilink_ctes(upd: &IrUpdate, name: &str) -> Vec<String> {
1014 let col_list = upd.poly_columns.iter().map(|c| qi(c)).collect::<Vec<_>>().join(", ");
1015 let mut cte_parts = emit_user_cte_parts(&upd.nested_ctes);
1016 let mut union_parts = vec![];
1017 for (i, imp) in upd.poly_implementors.iter().enumerate() {
1018 let cte_name = format!("{}__u{}", name, i);
1019 cte_parts.extend(emit_update_multilink_cte_parts(
1020 &concrete_poly_update(upd, imp),
1021 &cte_name,
1022 false,
1023 ));
1024 union_parts.push(format!(
1025 "SELECT {}::text AS \"__type__\", {} FROM \"{}\"",
1026 sql_str(&imp.type_name),
1027 col_list,
1028 cte_name,
1029 ));
1030 }
1031 cte_parts.push(format!("\"{}\" AS (\n{}\n)", name, union_parts.join("\nUNION ALL\n")));
1032 cte_parts
1033}
1034
1035fn emit_poly_delete_dml_ctes(del: &IrDelete, name: &str) -> Vec<String> {
1036 let alias = &del.target.alias;
1037 let col_list = del.poly_columns.iter().map(|c| qi(c)).collect::<Vec<_>>().join(", ");
1038
1039 let mut cte_parts = vec![];
1040 let mut union_parts = vec![];
1041 for (i, imp) in del.poly_implementors.iter().enumerate() {
1042 let cte_name = format!("{}__d{}", name, i);
1043 let mut del_sql = format!("DELETE FROM {} AS {}", qn(&imp.module, &imp.table), qi(alias),);
1044 append_filter(&mut del_sql, &del.filter);
1045 del_sql.push_str(&format!("\nRETURNING {}", col_list));
1046 cte_parts.push(format!("\"{}\" AS (\n{}\n)", cte_name, del_sql));
1047
1048 union_parts.push(format!(
1049 "SELECT {}::text AS \"__type__\", {} FROM \"{}\"",
1050 sql_str(&imp.type_name),
1051 col_list,
1052 cte_name,
1053 ));
1054 }
1055 cte_parts.push(format!("\"{}\" AS (\n{}\n)", name, union_parts.join("\nUNION ALL\n")));
1056 cte_parts
1057}
1058
1059fn update_has_any_multilink(upd: &IrUpdate) -> bool {
1062 !upd.multi_link_clears.is_empty()
1063 || !upd.multi_link_replaces.is_empty()
1064 || !upd.multi_link_appends.is_empty()
1065 || !upd.multi_link_removals.is_empty()
1066}
1067
1068fn ml_clear_exclusion(rep: Option<&IrMultiLinkMutation>) -> String {
1086 match rep {
1087 Some(rep) => {
1088 let vals_ref = emit_multilink_values_subquery(&rep.values, &[]);
1089 format!(
1090 " AND {} NOT IN (SELECT \"_v\".\"id\" FROM {} AS \"_v\")",
1091 qi(&rep.target_col),
1092 vals_ref
1093 )
1094 }
1095 None => String::new(),
1096 }
1097}
1098
1099fn emit_update_multilink_ctes(upd: &IrUpdate, name: &str) -> Vec<String> {
1113 emit_update_multilink_cte_parts(upd, name, true)
1114}
1115
1116fn emit_update_multilink_cte_parts(upd: &IrUpdate, name: &str, emit_nested: bool) -> Vec<String> {
1121 let alias = &upd.target.alias;
1122 let has_scalar_changes = !upd.assignments.is_empty() || !upd.rewrites.is_empty();
1123 let ids_name = format!("{}__ids", name);
1124 let mut parts: Vec<String> = if emit_nested {
1126 emit_user_cte_parts(&upd.nested_ctes)
1127 } else {
1128 vec![]
1129 };
1130
1131 if has_scalar_changes {
1132 let sets = update_set_fragments(&upd.assignments, &upd.rewrites, "");
1133 let mut upd_sql = format!(
1134 "UPDATE {} AS {}\nSET {}",
1135 target_ref(&upd.target),
1136 qi(alias),
1137 sets.join(", "),
1138 );
1139 upd_sql.push_str(&nested_cte_from(&upd.nested_ctes, &sets.join(","), "\n"));
1142 append_filter(&mut upd_sql, &upd.filter);
1143 upd_sql.push_str(&format!("\nRETURNING {}.*", qi(alias)));
1146 parts.push(format!("\"{}\" AS (\n{}\n)", ids_name, upd_sql));
1147 } else {
1148 let mut sel = format!(
1149 "SELECT {}.* FROM {} AS {}",
1150 qi(alias),
1151 target_ref(&upd.target),
1152 qi(alias),
1153 );
1154 append_filter(&mut sel, &upd.filter);
1155 parts.push(format!("\"{}\" AS (\n{}\n)", ids_name, sel));
1156 }
1157
1158 for (i, clr) in upd.multi_link_clears.iter().enumerate() {
1159 let exclude = ml_clear_exclusion(upd.multi_link_replaces.get(i));
1160 let del = format!(
1161 "DELETE FROM {} WHERE {} IN (SELECT id FROM \"{}\"){}",
1162 qn(&clr.module, &clr.junction_table),
1163 qi(&clr.source_col),
1164 ids_name,
1165 exclude,
1166 );
1167 parts.push(format!("\"{}__clr_{}\" AS (\n{}\n)", name, i, del));
1168 }
1169 for (i, app) in upd.multi_link_appends.iter().enumerate() {
1170 parts.push(emit_ml_append_cte(
1171 app,
1172 &ids_name,
1173 &upd.target.alias,
1174 &format!("{}__ml_add_{}", name, i),
1175 ));
1176 }
1177 for (i, rem) in upd.multi_link_removals.iter().enumerate() {
1178 parts.push(emit_ml_remove_cte(
1179 rem,
1180 &ids_name,
1181 &upd.target.alias,
1182 &format!("{}__ml_rm_{}", name, i),
1183 ));
1184 }
1185 for (i, rep) in upd.multi_link_replaces.iter().enumerate() {
1186 parts.push(emit_ml_append_cte(
1187 rep,
1188 &ids_name,
1189 &upd.target.alias,
1190 &format!("{}__ml_rep_{}", name, i),
1191 ));
1192 }
1193
1194 parts.push(format!("\"{}\" AS (\n SELECT * FROM \"{}\"\n)", name, ids_name));
1195 parts
1196}
1197
1198fn insert_has_any_multilink(ins: &IrInsert) -> bool {
1199 !ins.multi_link_appends.is_empty()
1200}
1201
1202fn emit_insert_multilink_ctes(ins: &IrInsert, name: &str) -> Vec<String> {
1208 let ids_name = format!("{}__ids", name);
1209 let mut parts: Vec<String> = emit_user_cte_parts(&ins.nested_ctes);
1210
1211 let mut insert_sql = emit_insert_row_sql(ins);
1212 if let Some(conflict) = &ins.unless_conflict {
1213 emit_conflict(&mut insert_sql, conflict);
1214 }
1215 insert_sql.push_str("\nRETURNING *");
1216 parts.push(format!("\"{}\" AS (\n{}\n)", ids_name, insert_sql));
1217
1218 for (i, app) in ins.multi_link_appends.iter().enumerate() {
1219 parts.push(emit_ml_append_cte(
1220 app,
1221 &ids_name,
1222 &ins.target.alias,
1223 &format!("{}__ml_add_{}", name, i),
1224 ));
1225 }
1226
1227 parts.push(format!("\"{}\" AS (\n SELECT * FROM \"{}\"\n)", name, ids_name));
1228 parts
1229}
1230
1231fn emit_for_dml_ctes_within(f: &IrFor, name: &str, outer_alias: Option<&str>) -> Vec<String> {
1242 let iter_alias = format!("_for_{}", f.var_name);
1243 let (_, plain_iter_cte) = emit_for_iterator(&f.iterator, &iter_alias);
1244 let correlated = outer_alias;
1245 let iter_cte = match correlated {
1246 Some(outer) => emit_nested_for_iterator(&f.iterator, &iter_alias, outer),
1247 None => plain_iter_cte,
1248 };
1249 let mut parts: Vec<String> = vec![iter_cte];
1252 parts.extend(emit_user_cte_parts(&f.body_ctes));
1253 let ids_name = format!("{}__ids", name);
1254
1255 if correlated.is_some()
1258 && let IrStmt::Insert(ins) = f.body.as_ref()
1259 && ins.multi_link_appends.is_empty()
1260 {
1261 let rows_name = format!("{}__rows", name);
1262 let outer_source = format!("{}.{}", qi(&iter_alias), qi(OUTER_KEY));
1263 parts.push(emit_iteration_rows_cte(ins, &rows_name, &iter_alias, &outer_source));
1264 parts.push(emit_insert_from_rows_cte(ins, &ids_name, &rows_name));
1265 parts.push(format!("\"{}\" AS (\n SELECT * FROM \"{}\"\n)", name, rows_name));
1266 return parts;
1267 }
1268
1269 match f.body.as_ref() {
1270 IrStmt::Insert(ins) => {
1271 let rewrite_cols: std::collections::HashSet<&str> =
1272 ins.rewrites.iter().map(|r| r.column.as_str()).collect();
1273 let cols: Vec<String> = ins
1274 .assignments
1275 .iter()
1276 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
1277 .map(|(c, _)| qi(c))
1278 .chain(ins.rewrites.iter().map(|r| qi(&r.column)))
1279 .collect();
1280 let values: Vec<String> = ins
1281 .assignments
1282 .iter()
1283 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
1284 .map(|(_, e)| emit_expr(e))
1285 .chain(ins.rewrites.iter().map(|r| emit_expr(&r.expr)))
1286 .collect();
1287 let rows_name = format!("{}__rows", name);
1290 let correlated_targets =
1291 correlated_append_indices(&ins.multi_link_appends, &[&f.body_ctes, &ins.nested_ctes]);
1292 if correlated_targets.is_empty() {
1293 let mut sql = format!(
1294 "INSERT INTO {} ({})\nSELECT {} FROM {}",
1295 target_ref(&ins.target),
1296 cols.join(", "),
1297 values.join(", "),
1298 qi(&iter_alias),
1299 );
1300 if let Some(conflict) = &ins.unless_conflict {
1301 emit_conflict(&mut sql, conflict);
1302 }
1303 sql.push_str("\nRETURNING *");
1304 parts.push(format!("\"{}\" AS (\n{}\n)", ids_name, sql));
1305 } else {
1306 let outer_source = match correlated {
1307 Some(_) => format!("{}.{}", qi(&iter_alias), qi(OUTER_KEY)),
1308 None => format!("{}.{}", qi(&iter_alias), qi("v")),
1309 };
1310 parts.push(emit_iteration_rows_cte(ins, &rows_name, &iter_alias, &outer_source));
1311 parts.push(emit_insert_from_rows_cte(ins, &ids_name, &rows_name));
1312 }
1313 for (i, append) in ins.multi_link_appends.iter().enumerate() {
1314 let cte_name = format!("{}__ml_add_{}", name, i);
1315 if correlated_targets.contains(&i) {
1316 let IrMultiLinkValueSource::CteRef(target) = &append.values.source else {
1317 unreachable!("only a CTE-backed value can carry an iteration key")
1318 };
1319 parts.push(emit_correlated_ml_append_cte(append, &rows_name, target, &cte_name));
1320 } else {
1321 parts.push(emit_ml_append_cte(append, &ids_name, &ins.target.alias, &cte_name));
1322 }
1323 }
1324 }
1325 IrStmt::Update(upd) => {
1326 let alias = &upd.target.alias;
1327 if upd.assignments.is_empty() && upd.rewrites.is_empty() && !upd.multi_link_appends.is_empty() {
1328 const ITER_COL: &str = "_iter";
1329 let generated: Vec<Option<&IrInsert>> = upd
1333 .multi_link_appends
1334 .iter()
1335 .map(|a| per_iteration_insert(a, &upd.nested_ctes))
1336 .collect();
1337 let new_cols: String = generated
1338 .iter()
1339 .enumerate()
1340 .filter_map(|(i, ins)| {
1341 ins.map(|ins| {
1342 format!(
1343 ", {} AS {}",
1344 ins.id_default_sql.as_deref().unwrap_or("uuidv7()"),
1345 qi(&format!("_new_{i}"))
1346 )
1347 })
1348 })
1349 .collect();
1350 let mut ids_sql = format!(
1351 "\"{}\" AS (\nSELECT {}.*, {}.\"v\" AS {}{} FROM {} AS {}, {}",
1352 ids_name,
1353 qi(alias),
1354 qi(&iter_alias),
1355 qi(ITER_COL),
1356 new_cols,
1357 target_ref(&upd.target),
1358 qi(alias),
1359 qi(&iter_alias),
1360 );
1361 append_filter(&mut ids_sql, &upd.filter);
1362 ids_sql.push_str("\n)");
1363 parts.push(ids_sql);
1364 for (i, append) in upd.multi_link_appends.iter().enumerate() {
1365 match generated[i] {
1366 Some(ins) => {
1367 parts.push(emit_for_nested_insert_cte(
1368 ins,
1369 &ids_name,
1370 &iter_alias,
1371 &format!("{}__ml_ins_{}", name, i),
1372 &format!("_new_{i}"),
1373 ));
1374 parts.push(emit_for_ml_append_cte(
1375 append,
1376 &ids_name,
1377 &format!("{}__ml_add_{}", name, i),
1378 &format!("_new_{i}"),
1379 ));
1380 }
1381 None => parts.push(emit_for_ml_append_cte(
1382 append,
1383 &ids_name,
1384 &format!("{}__ml_add_{}", name, i),
1385 ITER_COL,
1386 )),
1387 }
1388 }
1389 } else {
1390 let sets = update_set_fragments(&upd.assignments, &upd.rewrites, "");
1391 let mut sql = format!(
1392 "UPDATE {} AS {}\nSET {}\nFROM {}",
1393 target_ref(&upd.target),
1394 qi(alias),
1395 sets.join(", "),
1396 qi(&iter_alias),
1397 );
1398 append_filter(&mut sql, &upd.filter);
1399 sql.push_str(&format!("\nRETURNING {}.*", qi(alias)));
1400 parts.push(format!("\"{}\" AS (\n{}\n)", ids_name, sql));
1401 }
1402 }
1403 IrStmt::For(inner) => {
1407 let inner_alias = format!("_for_{}", inner.var_name);
1408 parts.push(emit_nested_for_iterator(&inner.iterator, &inner_alias, &iter_alias));
1409 parts.extend(emit_user_cte_parts(&inner.body_ctes));
1410 let IrStmt::Insert(ins) = inner.body.as_ref() else {
1411 unreachable!("a nested for-loop's own body is an insert: {:?}", inner.body)
1412 };
1413 let rewrite_cols: std::collections::HashSet<&str> =
1414 ins.rewrites.iter().map(|r| r.column.as_str()).collect();
1415 let cols: Vec<String> = ins
1416 .assignments
1417 .iter()
1418 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
1419 .map(|(c, _)| qi(c))
1420 .chain(ins.rewrites.iter().map(|r| qi(&r.column)))
1421 .collect();
1422 let values: Vec<String> = ins
1423 .assignments
1424 .iter()
1425 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
1426 .map(|(_, e)| emit_expr(e))
1427 .chain(ins.rewrites.iter().map(|r| emit_expr(&r.expr)))
1428 .collect();
1429 let mut sql = format!(
1430 "INSERT INTO {} ({})\nSELECT {} FROM {}",
1431 target_ref(&ins.target),
1432 cols.join(", "),
1433 values.join(", "),
1434 nested_for_from(&inner_alias, &iter_alias),
1435 );
1436 if let Some(conflict) = &ins.unless_conflict {
1437 emit_conflict(&mut sql, conflict);
1438 }
1439 sql.push_str("\nRETURNING *");
1440 parts.push(format!("\"{}\" AS (\n{}\n)", ids_name, sql));
1441 for (i, append) in ins.multi_link_appends.iter().enumerate() {
1442 parts.push(emit_ml_append_cte(
1443 append,
1444 &ids_name,
1445 &ins.target.alias,
1446 &format!("{}__ml_add_{}", name, i),
1447 ));
1448 }
1449 }
1450 other => unreachable!("for-loop body is not a mutation: {other:?}"),
1452 }
1453
1454 parts.push(format!("\"{}\" AS (\n SELECT * FROM \"{}\"\n)", name, ids_name));
1455 parts
1456}
1457
1458fn emit_user_cte_parts(ctes: &[IrCteDef]) -> Vec<String> {
1467 let mut parts: Vec<String> = vec![];
1468 for c in ctes {
1469 let nested: &[IrCteDef] = match &c.stmt {
1475 IrStmt::Insert(ins) if insert_has_any_multilink(ins) => &[],
1476 IrStmt::Update(upd) if update_has_any_multilink(upd) => &[],
1477 IrStmt::Insert(ins) => &ins.nested_ctes,
1478 IrStmt::Update(upd) => &upd.nested_ctes,
1479 _ => &[],
1480 };
1481 if !nested.is_empty() {
1482 parts.extend(emit_user_cte_parts(nested));
1483 }
1484 if let IrStmt::Update(upd) = &c.stmt {
1485 if !upd.poly_implementors.is_empty() {
1486 parts.extend(if update_has_any_multilink(upd) {
1487 emit_poly_update_multilink_ctes(upd, &c.name)
1488 } else {
1489 emit_poly_update_dml_ctes(upd, &c.name)
1490 });
1491 continue;
1492 }
1493 if update_has_any_multilink(upd) {
1494 parts.extend(emit_update_multilink_ctes(upd, &c.name));
1495 continue;
1496 }
1497 }
1498 if let IrStmt::Insert(ins) = &c.stmt
1499 && insert_has_any_multilink(ins)
1500 {
1501 parts.extend(emit_insert_multilink_ctes(ins, &c.name));
1502 continue;
1503 }
1504 if let IrStmt::Delete(del) = &c.stmt
1505 && !del.poly_implementors.is_empty()
1506 {
1507 parts.extend(emit_poly_delete_dml_ctes(del, &c.name));
1508 continue;
1509 }
1510 if let IrStmt::For(f) = &c.stmt
1511 && matches!(f.body.as_ref(), IrStmt::Insert(_) | IrStmt::Update(_) | IrStmt::For(_))
1512 {
1513 parts.extend(emit_for_dml_ctes_within(f, &c.name, c.correlated_to.as_deref()));
1514 continue;
1515 }
1516 let body = emit_dml_as_cte_source(&c.stmt);
1517 parts.push(match &c.correlated_to {
1518 Some(iterator) => format!(
1521 "\"{}\" AS (\nSELECT {}.\"v\" AS {}, \"_row\".*\nFROM {}\nCROSS JOIN LATERAL (\n{}\n) AS \"_row\"\n)",
1522 c.name,
1523 qi(iterator),
1524 qi(OUTER_KEY),
1525 qi(iterator),
1526 body,
1527 ),
1528 None => format!("\"{}\" AS (\n{}\n)", c.name, body),
1529 });
1530 }
1531 parts
1532}
1533
1534fn emit_cte_prefix(ctes: &[IrCteDef]) -> String {
1536 format!("WITH\n{}\n", emit_user_cte_parts(ctes).join(",\n"))
1537}
1538
1539pub(crate) fn collect_link_prop_names(vals: &IrMultiLinkValues, names: &mut Vec<String>) {
1544 for (name, _) in &vals.link_props {
1545 if !names.contains(name) {
1546 names.push(name.clone());
1547 }
1548 }
1549 match &vals.source {
1550 IrMultiLinkValueSource::Union(a, b) => {
1551 collect_link_prop_names(a, names);
1552 collect_link_prop_names(b, names);
1553 }
1554 IrMultiLinkValueSource::Asserted { inner, .. } => collect_link_prop_names(inner, names),
1555 _ => {}
1556 }
1557}
1558
1559fn emit_link_prop_cols(vals: &IrMultiLinkValues, prop_names: &[String]) -> String {
1564 prop_names
1565 .iter()
1566 .map(|name| match vals.link_props.iter().find(|(n, _)| n == name) {
1567 Some((_, expr)) => format!(", {} AS {}", emit_expr(expr), qi(name)),
1568 None => format!(", NULL AS {}", qi(name)),
1569 })
1570 .collect()
1571}
1572
1573fn emit_multilink_values_subquery(vals: &IrMultiLinkValues, prop_names: &[String]) -> String {
1579 emit_multilink_values_inner(vals, prop_names, true)
1580}
1581
1582fn emit_multilink_values_inner(vals: &IrMultiLinkValues, prop_names: &[String], bare_cte_ok: bool) -> String {
1587 if let IrMultiLinkValueSource::Union(a, b) = &vals.source {
1588 return format!(
1589 "({}\nUNION ALL\n{})",
1590 emit_multilink_values_inner(a, prop_names, false),
1591 emit_multilink_values_inner(b, prop_names, false),
1592 );
1593 }
1594
1595 if let IrMultiLinkValueSource::Asserted {
1600 fn_name,
1601 inner,
1602 message,
1603 } = &vals.source
1604 {
1605 let inner_sql = emit_multilink_values_inner(inner, &[], false);
1606 let prop_cols = emit_link_prop_cols(vals, prop_names);
1607 return format!(
1608 "(SELECT unnest(\"_pylon\".{}(ARRAY(SELECT \"_v\".\"id\" FROM {} AS \"_v\"){})) AS \"id\"{})",
1609 qi(fn_name),
1610 inner_sql,
1611 assert_message_arg(message),
1612 prop_cols,
1613 );
1614 }
1615
1616 let prop_cols = emit_link_prop_cols(vals, prop_names);
1617
1618 match &vals.source {
1619 IrMultiLinkValueSource::CteRef(name) => {
1620 if prop_cols.is_empty() && bare_cte_ok {
1621 format!("\"{}\"", name)
1623 } else {
1624 format!("(SELECT \"_s\".\"id\"{} FROM \"{}\" AS \"_s\")", prop_cols, name)
1625 }
1626 }
1627 IrMultiLinkValueSource::Select(s) => {
1628 let [IrRowSource::Bound { source, .. }] = s.rows.as_slice() else {
1633 unreachable!("IrMultiLinkValueSource::Select is always schema-bound")
1634 };
1635 let alias = &source.alias;
1636 let mut sql = format!(
1637 "(SELECT {}.\"id\"{} FROM {} AS {}",
1638 qi(alias),
1639 prop_cols,
1640 source_ref(source),
1641 qi(alias)
1642 );
1643 append_filter(&mut sql, &s.filter);
1644 sql.push(')');
1645 sql
1646 }
1647 IrMultiLinkValueSource::PathSelect(ps) => {
1648 let root_alias = &ps.root.alias;
1649 let target_alias = match &ps.result {
1652 IrPathResult::Object { alias, .. } => alias,
1653 IrPathResult::Scalar(..) => root_alias,
1654 };
1655 let mut sql = format!(
1656 "(SELECT {}.\"id\"{} FROM {} AS {}",
1657 qi(target_alias),
1658 prop_cols,
1659 source_ref(&ps.root),
1660 qi(root_alias)
1661 );
1662 for join in &ps.joins {
1663 sql.push_str(&emit_path_join_sql(join));
1664 }
1665 append_filter(&mut sql, &ps.filter);
1666 sql.push(')');
1667 sql
1668 }
1669 IrMultiLinkValueSource::Function(fs) => {
1670 let args = fs.fn_args.iter().map(emit_expr).collect::<Vec<_>>().join(", ");
1671 let mut sql = format!(
1672 "(SELECT {}.\"id\"{} FROM {}.{}({}) AS {}",
1673 qi(&fs.alias),
1674 prop_cols,
1675 pg_schema(&fs.fn_module),
1676 qi(&fs.fn_name),
1677 args,
1678 qi(&fs.alias),
1679 );
1680 append_filter(&mut sql, &fs.filter);
1681 sql.push(')');
1682 sql
1683 }
1684 IrMultiLinkValueSource::Union(..) | IrMultiLinkValueSource::Asserted { .. } => {
1685 unreachable!("handled above")
1686 }
1687 }
1688}
1689
1690fn emit_path_join_sql(join: &IrPathJoin) -> String {
1692 match join {
1693 IrPathJoin::Lateral { inner, target } => {
1694 let projected = match &inner.result {
1695 IrPathResult::Object { alias, .. } => alias.clone(),
1696 IrPathResult::Scalar(..) => unreachable!("a lateral step is only built for an object-valued computed"),
1697 };
1698 let mut body = format!(
1699 "SELECT {}.* FROM {}",
1700 qi(&projected),
1701 emit_path_joins(&inner.root, &inner.joins)
1702 );
1703 append_filter(&mut body, &inner.filter);
1704 append_order_by(&mut body, &inner.order_by);
1705 append_offset_limit(&mut body, &inner.offset, &inner.limit);
1706 format!(" JOIN LATERAL (\n{}\n) AS {} ON TRUE", body, qi(&target.alias))
1707 }
1708 IrPathJoin::Function {
1709 fn_module,
1710 fn_name,
1711 args,
1712 target,
1713 } => {
1714 let args_sql = args.iter().map(emit_expr).collect::<Vec<_>>().join(", ");
1715 format!(
1716 " CROSS JOIN LATERAL {}.{}({}) AS {}",
1717 pg_schema(fn_module),
1718 qi(fn_name),
1719 args_sql,
1720 qi(&target.alias),
1721 )
1722 }
1723 IrPathJoin::Single {
1724 source_alias,
1725 fk_col,
1726 target,
1727 } => {
1728 format!(
1729 " JOIN {} AS {} ON {}.\"id\" = {}.{}",
1730 source_ref(target),
1731 qi(&target.alias),
1732 qi(&target.alias),
1733 qi(source_alias),
1734 qi(fk_col)
1735 )
1736 }
1737 IrPathJoin::Multi {
1738 source_alias,
1739 junction_alias,
1740 join: ml_join,
1741 target,
1742 } => {
1743 let (jt_ref, src_col, tgt_col) = match ml_join {
1744 IrMultiLinkJoin::Standard { junction_table, module } => (
1745 junction_ref(module, junction_table),
1746 "source".to_string(),
1747 "target".to_string(),
1748 ),
1749 IrMultiLinkJoin::Through {
1750 junction_table,
1751 module,
1752 source_col,
1753 target_col,
1754 } => (
1755 junction_ref(module, junction_table),
1756 source_col.clone(),
1757 target_col.clone(),
1758 ),
1759 IrMultiLinkJoin::BacklinkFk { .. } | IrMultiLinkJoin::BacklinkJunction { .. } => {
1764 unreachable!("a forward multi-link path step never uses a backlink join variant")
1765 }
1766 };
1767 format!(
1768 " JOIN {} AS {} ON {}.{} = {}.\"id\" JOIN {} AS {} ON {}.{} = {}.\"id\"",
1769 jt_ref,
1770 qi(junction_alias),
1771 qi(junction_alias),
1772 qi(&src_col),
1773 qi(source_alias),
1774 source_ref(target),
1775 qi(&target.alias),
1776 qi(junction_alias),
1777 qi(&tgt_col),
1778 qi(&target.alias),
1779 )
1780 }
1781 IrPathJoin::BacklinkSingle {
1782 source_alias,
1783 fk_col,
1784 target,
1785 } => {
1786 format!(
1787 " JOIN {} AS {} ON {}.{} = {}.\"id\"",
1788 source_ref(target),
1789 qi(&target.alias),
1790 qi(&target.alias),
1791 qi(fk_col),
1792 qi(source_alias),
1793 )
1794 }
1795 IrPathJoin::BacklinkMulti {
1796 source_alias,
1797 junction_alias,
1798 junction_table,
1799 module,
1800 owner_col,
1801 current_col,
1802 target,
1803 } => {
1804 format!(
1805 " JOIN {} AS {} ON {}.{} = {}.\"id\" JOIN {} AS {} ON {}.\"id\" = {}.{}",
1806 junction_ref(module, junction_table),
1807 qi(junction_alias),
1808 qi(junction_alias),
1809 qi(current_col),
1810 qi(source_alias),
1811 source_ref(target),
1812 qi(&target.alias),
1813 qi(&target.alias),
1814 qi(junction_alias),
1815 qi(owner_col),
1816 )
1817 }
1818 }
1819}
1820
1821pub(crate) fn append_value_is_the_loop_variable(values: &IrMultiLinkValues, var: &str) -> bool {
1833 let IrMultiLinkValueSource::Select(sel) = &values.source else {
1834 return false;
1835 };
1836 let Some(IrExpr::BinOp(cmp)) = sel.filter.as_ref() else {
1837 return false;
1838 };
1839 matches!(
1840 (&cmp.left, &cmp.op, &cmp.right),
1841 (
1842 IrExpr::ColumnRef { column, .. },
1843 crate::parse::ast::BinOpKind::Eq,
1844 IrExpr::ForVar { name, .. },
1845 ) if column == "id" && name == var
1846 ) && values.link_props.is_empty()
1847}
1848
1849pub(crate) fn per_iteration_insert<'c>(mutation: &IrMultiLinkMutation, nested: &'c [IrCteDef]) -> Option<&'c IrInsert> {
1855 let IrMultiLinkValueSource::CteRef(name) = &mutation.values.source else {
1856 return None;
1857 };
1858 if !mutation.values.link_props.is_empty() {
1859 return None;
1860 }
1861 nested.iter().find(|c| &c.name == name).and_then(|c| match &c.stmt {
1862 IrStmt::Insert(ins) if ins.id_default_sql.is_some() && ins.multi_link_appends.is_empty() => Some(ins),
1863 _ => None,
1864 })
1865}
1866
1867fn emit_for_nested_insert_cte(
1872 ins: &IrInsert,
1873 ids_name: &str,
1874 iter_alias: &str,
1875 cte_name: &str,
1876 id_col: &str,
1877) -> String {
1878 let mut cols = vec![qi("id")];
1879 let mut values = vec![format!("\"{}\".{}", ids_name, qi(id_col))];
1880 for (col, expr) in &ins.assignments {
1881 cols.push(qi(col));
1882 values.push(emit_expr(expr));
1883 }
1884 format!(
1885 "\"{}\" AS (\nINSERT INTO {} ({})\nSELECT {} FROM \"{}\", {}\nWHERE \"{}\".{} = {}.\"v\"\nRETURNING {}\n)",
1886 cte_name,
1887 target_ref(&ins.target),
1888 cols.join(", "),
1889 values.join(", "),
1890 ids_name,
1891 qi(iter_alias),
1892 ids_name,
1893 qi("_iter"),
1894 qi(iter_alias),
1895 qi("id"),
1896 )
1897}
1898
1899fn emit_for_ml_append_cte(mutation: &IrMultiLinkMutation, ids_name: &str, cte_name: &str, iter_col: &str) -> String {
1900 format!(
1901 "\"{}\" AS (\nINSERT INTO {} ({}, {})\nSELECT \"{}\".\"id\", \"{}\".{} FROM \"{}\"\nON CONFLICT DO NOTHING\nRETURNING {}, {}\n)",
1902 cte_name,
1903 qn(&mutation.module, &mutation.junction_table),
1904 qi(&mutation.source_col),
1905 qi(&mutation.target_col),
1906 ids_name,
1907 ids_name,
1908 qi(iter_col),
1909 ids_name,
1910 qi(&mutation.source_col),
1911 qi(&mutation.target_col),
1912 )
1913}
1914
1915fn emit_ml_append_cte(mutation: &IrMultiLinkMutation, ids_name: &str, ids_alias: &str, cte_name: &str) -> String {
1918 let mut prop_names = vec![];
1919 collect_link_prop_names(&mutation.values, &mut prop_names);
1920 let vals_ref = emit_multilink_values_subquery(&mutation.values, &prop_names);
1921
1922 let extra_cols: String = prop_names.iter().map(|n| format!(", {}", qi(n))).collect();
1923 let extra_select: String = prop_names.iter().map(|n| format!(", \"_v\".{}", qi(n))).collect();
1924
1925 let conflict_clause = if mutation.single {
1929 let mut sets = vec![format!(
1939 "{} = EXCLUDED.{}",
1940 qi(&mutation.target_col),
1941 qi(&mutation.target_col)
1942 )];
1943 sets.extend(prop_names.iter().map(|n| format!("{} = EXCLUDED.{}", qi(n), qi(n))));
1944 format!(
1945 "ON CONFLICT ({}) DO UPDATE SET {}",
1946 qi(&mutation.source_col),
1947 sets.join(", ")
1948 )
1949 } else if prop_names.is_empty() {
1950 "ON CONFLICT DO NOTHING".to_string()
1951 } else {
1952 let sets: Vec<String> = prop_names
1953 .iter()
1954 .map(|n| format!("{} = EXCLUDED.{}", qi(n), qi(n)))
1955 .collect();
1956 format!(
1957 "ON CONFLICT ({}, {}) DO UPDATE SET {}",
1958 qi(&mutation.source_col),
1959 qi(&mutation.target_col),
1960 sets.join(", "),
1961 )
1962 };
1963
1964 let lateral = if vals_ref.starts_with('(') { "LATERAL " } else { "" };
1967 let ins = format!(
1968 "INSERT INTO {} ({}, {}{})\nSELECT {}.\"id\", \"_v\".\"id\"{} FROM \"{}\" AS {} CROSS JOIN {}{} AS \"_v\"\n{}\nRETURNING {}, {}",
1969 qn(&mutation.module, &mutation.junction_table),
1970 qi(&mutation.source_col),
1971 qi(&mutation.target_col),
1972 extra_cols,
1973 qi(ids_alias),
1974 extra_select,
1975 ids_name,
1976 qi(ids_alias),
1977 lateral,
1978 vals_ref,
1979 conflict_clause,
1980 qi(&mutation.source_col),
1981 qi(&mutation.target_col),
1982 );
1983 format!("\"{}\" AS (\n{}\n)", cte_name, ins)
1984}
1985
1986fn emit_ml_remove_cte(mutation: &IrMultiLinkMutation, ids_name: &str, ids_alias: &str, cte_name: &str) -> String {
1989 let vals_ref = emit_multilink_values_subquery(&mutation.values, &[]);
1992 let del = format!(
1996 "DELETE FROM {} AS \"_jn\"\nUSING \"{}\" AS {}\nWHERE \"_jn\".{} = {}.\"id\"\n AND \"_jn\".{} IN (SELECT \"id\" FROM {})\nRETURNING \"_jn\".{}, \"_jn\".{}",
1997 qn(&mutation.module, &mutation.junction_table),
1998 ids_name,
1999 qi(ids_alias),
2000 qi(&mutation.source_col),
2001 qi(ids_alias),
2002 qi(&mutation.target_col),
2003 vals_ref,
2004 qi(&mutation.source_col),
2005 qi(&mutation.target_col),
2006 );
2007 format!("\"{}\" AS (\n{}\n)", cte_name, del)
2008}
2009
2010use crate::ir::IrConflict;
2013
2014fn emit_conflict(sql: &mut String, conflict: &IrConflict) {
2015 let on_sql = conflict.on.as_ref().map(|e| match e {
2019 IrExpr::Tuple(elements) => format!("({})", elements.iter().map(emit_expr).collect::<Vec<_>>().join(", ")),
2020 other => format!("({})", emit_expr(other)),
2021 });
2022 match (&on_sql, &conflict.do_update) {
2023 (None, None) => sql.push_str(" ON CONFLICT DO NOTHING"),
2024 (Some(on), None) => sql.push_str(&format!(" ON CONFLICT {} DO NOTHING", on)),
2025 (None, Some(updates)) => {
2026 sql.push_str(&format!(" ON CONFLICT DO UPDATE SET {}", do_update_sets(updates)));
2027 }
2028 (Some(on), Some(updates)) => {
2029 sql.push_str(&format!(
2030 " ON CONFLICT {} DO UPDATE SET {}",
2031 on,
2032 do_update_sets(updates),
2033 ));
2034 }
2035 }
2036 if conflict.do_update.is_some()
2037 && let Some(predicate) = &conflict.do_update_where
2038 {
2039 sql.push_str(&format!(" WHERE {}", emit_expr(predicate)));
2040 }
2041}
2042
2043fn do_update_sets(updates: &[(String, IrExpr)]) -> String {
2044 updates
2045 .iter()
2046 .map(|(col, expr)| format!("{} = {}", qi(col), emit_expr(expr)))
2047 .collect::<Vec<_>>()
2048 .join(", ")
2049}
2050
2051fn is_integer_expr(expr: &IrExpr) -> bool {
2056 match expr {
2057 IrExpr::ColumnRef { pg_type, .. } => matches!(
2058 pg_type.as_str(),
2059 "int2" | "int4" | "int8" | "integer" | "bigint" | "smallint"
2060 ),
2061 IrExpr::Literal(crate::ir::IrLiteral::Int(_)) => true,
2062 IrExpr::BinOp(op) => is_integer_expr(&op.left) && is_integer_expr(&op.right),
2063 _ => false,
2064 }
2065}
2066
2067fn is_raw_scalar(expr: &IrExpr) -> bool {
2068 matches!(expr, IrExpr::Array(_))
2069 || matches!(expr, IrExpr::TypeCast(c) if c.pg_type == "jsonb")
2070 || matches!(expr, IrExpr::NamedTuple { .. })
2071 || matches!(expr, IrExpr::Tuple(_))
2072 || matches!(expr, IrExpr::JsonbField { .. })
2073 || matches!(expr, IrExpr::JsonbIndex { .. })
2074}
2075
2076fn emit_free_rows(sel: &IrSelect, rows: &[IrRowSource], ctes: &[IrCteDef]) -> SqlOutput {
2082 use crate::query::ShapeNode;
2083
2084 let items: Vec<&IrFreeExpr> = rows
2085 .iter()
2086 .map(|r| match r {
2087 IrRowSource::Free(item) => item,
2088 IrRowSource::Bound { .. } => unreachable!("mixed Bound/Free rows rejected at compile time"),
2089 })
2090 .collect();
2091
2092 if items.is_empty() {
2093 return SqlOutput {
2094 sql: "SELECT NULL AS result WHERE FALSE".to_string(),
2095 shape: ShapeDescriptor {
2096 root: ShapeNode::Scalar {
2097 name: String::new(),
2098 position: 0,
2099 },
2100 },
2101 inference_plan: None,
2102 };
2103 }
2104
2105 if items.len() == 1
2107 && let IrFreeExpr::AssertSet {
2108 fn_name,
2109 inner,
2110 message,
2111 } = items[0]
2112 {
2113 let array_sql = emit_array_source(inner) + &assert_message_arg(message);
2114 let mut sql = format!(
2115 "SELECT ROW(v) AS result FROM unnest(\"_pylon\".{}({})) AS _assert(v)",
2116 fn_name, array_sql,
2117 );
2118 if sel.distinct {
2119 sql = format!("SELECT DISTINCT * FROM ({}) AS \"_distinct\"", sql);
2120 }
2121 append_order_by(&mut sql, &sel.order_by);
2122 append_offset_limit(&mut sql, &sel.offset, &sel.limit);
2123 return SqlOutput {
2124 sql,
2125 shape: ShapeDescriptor {
2126 root: ShapeNode::Scalar {
2127 name: String::new(),
2128 position: 0,
2129 },
2130 },
2131 inference_plan: None,
2132 };
2133 }
2134
2135 let shape_root = free_item_shape(items.first().unwrap(), ctes);
2136
2137 let branches: Vec<String> = items
2138 .iter()
2139 .map(|item| match item {
2140 IrFreeExpr::Scalar(expr) => {
2141 if matches!(expr, IrExpr::ObjectSubquery(_)) {
2146 return format!("SELECT v AS result, v FROM (SELECT {} AS v) AS _obj", emit_expr(expr));
2147 }
2148 if is_raw_scalar(expr) {
2151 format!("SELECT v AS result, v FROM (SELECT {} AS v) AS _raw", emit_expr(expr))
2154 } else {
2155 let e = emit_expr(expr);
2162 let row_value = if enum_type_of_expr(expr).is_some() {
2163 "v::text"
2164 } else {
2165 "v"
2166 };
2167 let present = if items.len() > 1 { " WHERE v IS NOT NULL" } else { "" };
2171 format!("SELECT ROW({row_value}) AS result, v FROM (SELECT {e} AS v) AS _scalar{present}")
2172 }
2173 }
2174 IrFreeExpr::FreeObject(fields) => {
2175 let inner_cols: Vec<String> = fields
2181 .iter()
2182 .enumerate()
2183 .map(|(i, (_, e))| format!("{} AS \"_f{}\"", emit_expr(e), i))
2184 .collect();
2185 let row_items: Vec<String> = fields
2186 .iter()
2187 .enumerate()
2188 .map(|(i, (_, e))| {
2189 if enum_type_of_expr(e).is_some() {
2190 format!("\"_f{}\"::text", i)
2191 } else {
2192 format!("\"_f{}\"", i)
2193 }
2194 })
2195 .collect();
2196 let named_cols: Vec<String> = fields
2197 .iter()
2198 .enumerate()
2199 .map(|(i, (name, _))| format!("\"_f{}\" AS {}", i, qi(name)))
2200 .collect();
2201 format!(
2202 "SELECT ROW({}) AS result, {} FROM (SELECT {}) AS _obj",
2203 row_items.join(", "),
2204 named_cols.join(", "),
2205 inner_cols.join(", "),
2206 )
2207 }
2208 IrFreeExpr::Tuple(exprs) => {
2209 if exprs.len() == 1 {
2210 format!("SELECT ROW({}) AS result", emit_free_field_expr(&exprs[0]))
2211 } else {
2212 let parts: Vec<String> = exprs.iter().map(emit_free_field_expr).collect();
2213 format!("SELECT ({}) AS result", parts.join(", "))
2214 }
2215 }
2216 IrFreeExpr::NamedTupleRow(fields) => {
2217 let parts: Vec<String> = fields.iter().map(|(_, e)| emit_free_field_expr(e)).collect();
2218 if parts.len() == 1 {
2219 format!("SELECT ROW({}) AS result", parts[0])
2220 } else {
2221 format!("SELECT ({}) AS result", parts.join(", "))
2222 }
2223 }
2224 IrFreeExpr::AssertSet { .. } => unreachable!("AssertSet is handled by early return above"),
2225 IrFreeExpr::CtePassthrough(name) => format!("SELECT \"result\" FROM {}", qi(name)),
2226 })
2227 .collect();
2228
2229 let union_sql = branches.join("\nUNION ALL\n");
2230
2231 let mut sql = if sel.distinct {
2232 format!("SELECT DISTINCT * FROM (\n{}\n) AS \"_distinct\"", union_sql)
2234 } else {
2235 union_sql
2236 };
2237 if let Some(filter) = &sel.filter {
2241 sql = format!(
2242 "SELECT * FROM (\n{}\n) AS \"_filtered\"\nWHERE {}",
2243 sql,
2244 emit_expr(filter)
2245 );
2246 }
2247 append_order_by(&mut sql, &sel.order_by);
2248 append_offset_limit(&mut sql, &sel.offset, &sel.limit);
2249
2250 SqlOutput {
2251 sql,
2252 shape: ShapeDescriptor { root: shape_root },
2253 inference_plan: None,
2254 }
2255}
2256
2257fn enum_type_of_expr(expr: &IrExpr) -> Option<QualifiedPgType> {
2267 match expr {
2268 IrExpr::ColumnRef { pg_type, .. } => QualifiedPgType::of(pg_type),
2269 IrExpr::EnumLiteral { pg_type, .. } => Some(QualifiedPgType {
2270 name: pg_quoted_to_pylon(pg_type),
2271 is_array: false,
2272 }),
2273 _ => None,
2274 }
2275}
2276
2277fn enum_type_of_shape_expr(expr: &IrExpr) -> Option<QualifiedPgType> {
2286 if let Some(qualified) = enum_type_of_expr(expr) {
2287 return Some(qualified);
2288 }
2289 let IrExpr::Subquery(select) = expr else {
2290 return None;
2291 };
2292 let [IrRowSource::Bound { shape, .. }] = select.rows.as_slice() else {
2293 return None;
2294 };
2295 let [IrShapePointer::Scalar(scalar)] = shape.as_slice() else {
2296 return None;
2297 };
2298 QualifiedPgType::of(&scalar.pg_type)
2299}
2300
2301fn emit_free_field_expr(expr: &IrExpr) -> String {
2307 match enum_type_of_expr(expr) {
2308 Some(qualified) => format!("{}{}", emit_expr(expr), qualified.text_cast()),
2309 None => emit_expr(expr),
2310 }
2311}
2312
2313fn free_field_shape_node(name: &str, position: usize, expr: &IrExpr) -> crate::query::ShapeNode {
2316 use crate::query::{Cardinality, ShapeNode};
2317 if let IrExpr::ObjectSubquery(sel) = expr {
2318 let [IrRowSource::Bound { source, shape }] = sel.rows.as_slice() else {
2319 unreachable!("an object subquery is always schema-bound")
2320 };
2321 let (_, nodes) = build_shape(shape, &source.alias);
2322 return ShapeNode::Object {
2323 name: name.to_string(),
2324 type_name: Some(source.type_name.clone()),
2325 position,
2326 cardinality: Cardinality::Optional,
2327 pointers: prepend_type(nodes),
2328 has_implicit_id: shape_has_implicit_id(shape),
2329 };
2330 }
2331 if matches!(expr, IrExpr::ObjectPathSubquery(_) | IrExpr::ObjectPathUnion { .. }) {
2333 return expr_shape_node(name, position, expr);
2334 }
2335 if let IrExpr::ArrayFromSelect(source) = expr
2338 && matches!(
2339 source.as_ref(),
2340 IrArraySource::ObjectSelect(_)
2341 | IrArraySource::ObjectFunction(_)
2342 | IrArraySource::PathSelect(_)
2343 | IrArraySource::Group(_)
2344 )
2345 {
2346 return expr_shape_node(name, position, expr);
2347 }
2348 match enum_type_of_shape_expr(expr) {
2349 Some(qualified) => qualified.shape_node(name.to_string(), position),
2350 None => ShapeNode::Scalar {
2351 name: name.to_string(),
2352 position,
2353 },
2354 }
2355}
2356
2357fn expr_shape_node(name: &str, position: usize, expr: &IrExpr) -> crate::query::ShapeNode {
2364 use crate::query::{Cardinality, ShapeNode};
2365 match expr {
2366 IrExpr::ObjectPathUnion { branches, multi, .. } => {
2367 let first = branches.first().expect("a union has at least one branch");
2368 let IrPathResult::Object {
2369 alias,
2370 type_name,
2371 shape,
2372 } = &first.result
2373 else {
2374 unreachable!("an object path union's branches always land on objects")
2375 };
2376 let (_, pointer_nodes) = build_shape(shape, alias);
2377 let object = ShapeNode::Object {
2378 name: if *multi { String::new() } else { name.to_string() },
2379 type_name: Some(type_name.clone()),
2380 position: if *multi { 0 } else { position },
2381 cardinality: if *multi {
2382 Cardinality::Many
2383 } else {
2384 Cardinality::Optional
2385 },
2386 pointers: prepend_type(pointer_nodes),
2387 has_implicit_id: shape_has_implicit_id(shape),
2388 };
2389 if *multi {
2390 ShapeNode::Array {
2391 name: name.to_string(),
2392 position,
2393 element: Box::new(object),
2394 }
2395 } else {
2396 object
2397 }
2398 }
2399 IrExpr::ObjectPathSubquery(ps) => {
2400 let IrPathResult::Object {
2401 alias,
2402 type_name,
2403 shape,
2404 } = &ps.result
2405 else {
2406 unreachable!("an object path subquery always lands on an object")
2407 };
2408 let (_, pointer_nodes) = build_shape(shape, alias);
2409 ShapeNode::Object {
2410 name: name.to_string(),
2411 type_name: Some(type_name.clone()),
2412 position,
2413 cardinality: Cardinality::Optional,
2414 pointers: prepend_type(pointer_nodes),
2415 has_implicit_id: shape_has_implicit_id(shape),
2416 }
2417 }
2418 IrExpr::TypeCast(c) if c.tuple_shape.is_some() => {
2419 let shape = c.tuple_shape.as_ref().unwrap();
2420 ShapeNode::NamedTuple {
2421 name: name.to_string(),
2422 position,
2423 type_name: shape.type_name.clone(),
2424 members: Some(shape.members.clone()),
2425 is_free_object: false,
2426 }
2427 }
2428 IrExpr::TypeCast(c) if c.pg_type == "jsonb" => ShapeNode::Scalar {
2434 name: name.to_string(),
2435 position,
2436 },
2437 IrExpr::NamedTuple { is_free_object, .. } => ShapeNode::NamedTuple {
2438 name: name.to_string(),
2439 position,
2440 type_name: None,
2441 members: None,
2442 is_free_object: *is_free_object,
2443 },
2444 IrExpr::ArrayFromSelect(source) => match source.as_ref() {
2447 IrArraySource::ObjectFunction(fs) => {
2448 let (_, nodes) = build_shape(&fs.shape, &fs.alias);
2449 ShapeNode::Array {
2450 name: name.to_string(),
2451 position,
2452 element: Box::new(ShapeNode::Object {
2453 name: String::new(),
2454 type_name: Some(fs.type_name.clone()),
2455 position: 0,
2456 cardinality: Cardinality::Many,
2457 pointers: prepend_type(nodes),
2458 has_implicit_id: shape_has_implicit_id(&fs.shape),
2459 }),
2460 }
2461 }
2462 IrArraySource::Group(grp) => ShapeNode::Array {
2463 name: name.to_string(),
2464 position,
2465 element: Box::new(emit_group(grp).shape.root),
2466 },
2467 IrArraySource::ObjectSelect(sel) => {
2468 let [IrRowSource::Bound { source, shape }] = sel.rows.as_slice() else {
2469 unreachable!("IrArraySource::ObjectSelect is always schema-bound")
2470 };
2471 let (_, nodes) = build_shape(shape, &source.alias);
2472 ShapeNode::Array {
2473 name: name.to_string(),
2474 position,
2475 element: Box::new(ShapeNode::Object {
2476 name: String::new(),
2477 type_name: Some(source.type_name.clone()),
2478 position: 0,
2479 cardinality: Cardinality::Many,
2480 pointers: prepend_type(nodes),
2481 has_implicit_id: shape_has_implicit_id(shape),
2482 }),
2483 }
2484 }
2485 IrArraySource::PathSelect(ps) => match &ps.result {
2486 IrPathResult::Object {
2487 alias,
2488 type_name,
2489 shape,
2490 } => {
2491 let (_, pointer_nodes) = build_shape(shape, alias);
2492 ShapeNode::Array {
2493 name: name.to_string(),
2494 position,
2495 element: Box::new(ShapeNode::Object {
2496 name: String::new(),
2497 type_name: Some(type_name.clone()),
2498 position: 0,
2499 cardinality: Cardinality::Many,
2500 pointers: prepend_type(pointer_nodes),
2501 has_implicit_id: shape_has_implicit_id(shape),
2502 }),
2503 }
2504 }
2505 IrPathResult::Scalar(..) => ShapeNode::Scalar {
2506 name: name.to_string(),
2507 position,
2508 },
2509 },
2510 _ => ShapeNode::Scalar {
2511 name: name.to_string(),
2512 position,
2513 },
2514 },
2515 e if is_raw_scalar(e) => ShapeNode::RawScalar,
2516 e => free_field_shape_node(name, position, e),
2517 }
2518}
2519
2520fn free_item_shape(item: &IrFreeExpr, ctes: &[IrCteDef]) -> crate::query::ShapeNode {
2521 use crate::query::{Cardinality, ShapeNode};
2522 match item {
2523 IrFreeExpr::Scalar(e) => match e {
2524 IrExpr::TypeCast(c) if c.pg_type == "jsonb" && c.tuple_shape.is_none() => ShapeNode::JsonScalar,
2528 _ => expr_shape_node("", 0, e),
2529 },
2530 IrFreeExpr::FreeObject(fields) => ShapeNode::Object {
2531 name: String::new(),
2532 type_name: None,
2533 position: 0,
2534 cardinality: Cardinality::Many,
2535 pointers: fields
2536 .iter()
2537 .enumerate()
2538 .map(|(i, (name, e))| free_field_shape_node(name, i, e))
2539 .collect(),
2540 has_implicit_id: false,
2541 },
2542 IrFreeExpr::Tuple(exprs) => ShapeNode::Tuple {
2543 position: 0,
2544 elements: exprs
2545 .iter()
2546 .enumerate()
2547 .map(|(i, e)| free_field_shape_node("", i, e))
2548 .collect(),
2549 names: None,
2550 },
2551 IrFreeExpr::NamedTupleRow(fields) => ShapeNode::Tuple {
2552 position: 0,
2553 elements: fields
2554 .iter()
2555 .enumerate()
2556 .map(|(i, (name, e))| free_field_shape_node(name, i, e))
2557 .collect(),
2558 names: Some(fields.iter().map(|(name, _)| name.clone()).collect()),
2559 },
2560 IrFreeExpr::AssertSet { .. } => ShapeNode::Scalar {
2561 name: String::new(),
2562 position: 0,
2563 },
2564 IrFreeExpr::CtePassthrough(name) => ctes
2570 .iter()
2571 .find(|c| &c.name == name)
2572 .and_then(|c| match &c.stmt {
2573 IrStmt::Select(sel) => match sel.rows.first() {
2574 Some(IrRowSource::Free(inner)) => Some(free_item_shape(inner, ctes)),
2575 _ => None,
2576 },
2577 _ => None,
2578 })
2579 .unwrap_or(ShapeNode::Scalar {
2580 name: String::new(),
2581 position: 0,
2582 }),
2583 }
2584}
2585
2586fn emit_path_joins(root: &IrSource, joins: &[IrPathJoin]) -> String {
2589 let mut parts = vec![format!("{} AS {}", source_ref(root), qi(&root.alias))];
2590 for join in joins {
2591 match join {
2592 IrPathJoin::Single {
2593 source_alias,
2594 fk_col,
2595 target,
2596 } => {
2597 parts.push(format!(
2598 "JOIN {} AS {} ON {}.{} = {}.\"id\"",
2599 source_ref(target),
2600 qi(&target.alias),
2601 qi(source_alias),
2602 qi(fk_col),
2603 qi(&target.alias),
2604 ));
2605 }
2606 IrPathJoin::Multi {
2607 source_alias,
2608 junction_alias,
2609 join,
2610 target,
2611 } => {
2612 match join {
2613 IrMultiLinkJoin::Standard { junction_table, module } => {
2614 parts.push(format!(
2615 "JOIN {} AS {} ON {}.\"source\" = {}.\"id\"",
2616 junction_ref(module, junction_table),
2617 qi(junction_alias),
2618 qi(junction_alias),
2619 qi(source_alias),
2620 ));
2621 parts.push(format!(
2622 "JOIN {} AS {} ON {}.\"id\" = {}.\"target\"",
2623 source_ref(target),
2624 qi(&target.alias),
2625 qi(&target.alias),
2626 qi(junction_alias),
2627 ));
2628 }
2629 IrMultiLinkJoin::Through {
2630 junction_table,
2631 module,
2632 source_col,
2633 target_col,
2634 } => {
2635 parts.push(format!(
2636 "JOIN {} AS {} ON {}.{} = {}.\"id\"",
2637 junction_ref(module, junction_table),
2638 qi(junction_alias),
2639 qi(junction_alias),
2640 qi(source_col),
2641 qi(source_alias),
2642 ));
2643 parts.push(format!(
2644 "JOIN {} AS {} ON {}.\"id\" = {}.{}",
2645 source_ref(target),
2646 qi(&target.alias),
2647 qi(&target.alias),
2648 qi(junction_alias),
2649 qi(target_col),
2650 ));
2651 }
2652 IrMultiLinkJoin::BacklinkFk { .. } | IrMultiLinkJoin::BacklinkJunction { .. } => {
2654 unreachable!("a forward multi-link path step never uses a backlink join variant")
2655 }
2656 }
2657 }
2658 IrPathJoin::BacklinkSingle {
2659 source_alias,
2660 fk_col,
2661 target,
2662 } => {
2663 parts.push(format!(
2664 "JOIN {} AS {} ON {}.{} = {}.\"id\"",
2665 source_ref(target),
2666 qi(&target.alias),
2667 qi(&target.alias),
2668 qi(fk_col),
2669 qi(source_alias),
2670 ));
2671 }
2672 IrPathJoin::Function {
2676 fn_module,
2677 fn_name,
2678 args,
2679 target,
2680 } => {
2681 let args_sql = args.iter().map(emit_expr).collect::<Vec<_>>().join(", ");
2682 parts.push(format!(
2683 "CROSS JOIN LATERAL {}.{}({}) AS {}",
2684 pg_schema(fn_module),
2685 qi(fn_name),
2686 args_sql,
2687 qi(&target.alias),
2688 ));
2689 }
2690 IrPathJoin::Lateral { inner, target } => {
2691 let projected = match &inner.result {
2692 IrPathResult::Object { alias, .. } => alias.clone(),
2693 IrPathResult::Scalar(..) => {
2694 unreachable!("a lateral step is only built for an object-valued computed")
2695 }
2696 };
2697 let mut body = format!(
2698 "SELECT {}.* FROM {}",
2699 qi(&projected),
2700 emit_path_joins(&inner.root, &inner.joins)
2701 );
2702 append_filter(&mut body, &inner.filter);
2703 append_order_by(&mut body, &inner.order_by);
2704 append_offset_limit(&mut body, &inner.offset, &inner.limit);
2705 parts.push(format!("JOIN LATERAL (\n{}\n) AS {} ON TRUE", body, qi(&target.alias)));
2706 }
2707 IrPathJoin::BacklinkMulti {
2708 source_alias,
2709 junction_alias,
2710 junction_table,
2711 module,
2712 owner_col,
2713 current_col,
2714 target,
2715 } => {
2716 parts.push(format!(
2717 "JOIN {} AS {} ON {}.{} = {}.\"id\"",
2718 junction_ref(module, junction_table),
2719 qi(junction_alias),
2720 qi(junction_alias),
2721 qi(current_col),
2722 qi(source_alias),
2723 ));
2724 parts.push(format!(
2725 "JOIN {} AS {} ON {}.\"id\" = {}.{}",
2726 source_ref(target),
2727 qi(&target.alias),
2728 qi(&target.alias),
2729 qi(junction_alias),
2730 qi(owner_col),
2731 ));
2732 }
2733 }
2734 }
2735 parts.join("\n")
2736}
2737
2738fn emit_array_source(src: &IrArraySource) -> String {
2740 match src {
2741 IrArraySource::Select(s) => {
2742 let [IrRowSource::Bound { source, shape }] = s.rows.as_slice() else {
2745 unreachable!("IrArraySource::Select is always schema-bound")
2746 };
2747 let scalar = match shape.first() {
2748 Some(IrShapePointer::Scalar(sf)) => format!("{}.{}", qi(&source.alias), qi(&sf.column)),
2749 _ => format!("{}.\"id\"", qi(&source.alias)),
2750 };
2751 let mut sql = format!("SELECT {} FROM {} AS {}", scalar, source_ref(source), qi(&source.alias));
2752 append_filter(&mut sql, &s.filter);
2753 format!("ARRAY({})", sql)
2754 }
2755 IrArraySource::ObjectFunction(fs) => {
2756 let (exprs, _) = build_shape(&fs.shape, &fs.alias);
2757 let mut parts = vec![sql_str(&fs.type_name) + "::text"];
2758 parts.extend(exprs);
2759 let args_sql = fs.fn_args.iter().map(emit_expr).collect::<Vec<_>>().join(", ");
2760 let mut sql = format!(
2761 "SELECT (\n {}\n) FROM {}.{}({}) AS {}",
2762 parts.join(",\n "),
2763 pg_schema(&fs.fn_module),
2764 qi(&fs.fn_name),
2765 args_sql,
2766 qi(&fs.alias),
2767 );
2768 append_filter(&mut sql, &fs.filter);
2769 append_order_by(&mut sql, &fs.order_by);
2770 append_offset_limit(&mut sql, &fs.offset, &fs.limit);
2771 format!("ARRAY({})", sql)
2772 }
2773 IrArraySource::ObjectSelect(s) => {
2774 let [IrRowSource::Bound { source, shape }] = s.rows.as_slice() else {
2775 unreachable!("IrArraySource::ObjectSelect is always schema-bound")
2776 };
2777 let (exprs, _) = build_shape(shape, &source.alias);
2778 let mut parts = vec![source_type_disc(source)];
2779 parts.extend(exprs);
2780 let mut sql = format!(
2781 "SELECT (\n {}\n) FROM {} AS {}",
2782 parts.join(",\n "),
2783 source_ref(source),
2784 qi(&source.alias)
2785 );
2786 append_filter(&mut sql, &s.filter);
2787 append_order_by(&mut sql, &s.order_by);
2788 append_offset_limit(&mut sql, &s.offset, &s.limit);
2789 format!("ARRAY({})", sql)
2790 }
2791 IrArraySource::Group(grp) => format!("ARRAY({})", emit_group(grp).sql),
2792 IrArraySource::StmtColumn { stmt, column } => format!(
2793 "ARRAY(SELECT {} FROM (\n{}\n) AS \"_rows\")",
2794 qi(column),
2795 emit_dml_as_cte_source(stmt),
2796 ),
2797 IrArraySource::PathSelect(ps) => {
2798 let scalar = match &ps.result {
2799 IrPathResult::Scalar(e, _) => emit_expr(e),
2800 IrPathResult::Object {
2804 alias,
2805 type_name,
2806 shape,
2807 } => {
2808 let (pointer_exprs, _) = build_shape(shape, alias);
2809 let mut parts = vec![result_type_disc(ps, alias, type_name)];
2810 parts.extend(pointer_exprs);
2811 format!("(\n {}\n)", parts.join(",\n "))
2812 }
2813 };
2814 let from_sql = emit_path_joins(&ps.root, &ps.joins);
2815 let mut sql = format!(
2816 "SELECT {}{} FROM {}",
2817 if ps.distinct { "DISTINCT " } else { "" },
2818 scalar,
2819 from_sql
2820 );
2821 append_filter(&mut sql, &ps.filter);
2822 append_order_by(&mut sql, &ps.order_by);
2823 append_offset_limit(&mut sql, &ps.offset, &ps.limit);
2824 format!("ARRAY({})", sql)
2825 }
2826 IrArraySource::RawExpr {
2827 source,
2828 poly_implementors,
2829 poly_columns,
2830 expr,
2831 } => {
2832 let from_sql = if !poly_implementors.is_empty() {
2833 format!(
2834 "(\n{}\n) AS {}",
2835 emit_poly_union(poly_implementors, poly_columns),
2836 qi(&source.alias)
2837 )
2838 } else {
2839 format!("{} AS {}", source_ref(source), qi(&source.alias))
2840 };
2841 format!("ARRAY(SELECT {} FROM {})", emit_expr(expr), from_sql)
2842 }
2843 }
2844}
2845
2846fn emit_key_expr(expr: &IrExpr) -> String {
2849 if let IrExpr::ColumnRef { alias, column, pg_type } = expr
2850 && let Some(qualified) = QualifiedPgType::of(pg_type)
2851 {
2852 let col_ref = if alias.is_empty() {
2853 qi(column)
2854 } else {
2855 format!("{}.{}", qi(alias), qi(column))
2856 };
2857 return format!("{}{}", col_ref, qualified.text_cast());
2858 }
2859 emit_expr(expr)
2860}
2861
2862fn group_rows(grp: &IrGroup) -> (String, Option<String>) {
2866 let alias = &grp.source.alias;
2867 if grp.limit.is_none() && grp.offset.is_none() {
2868 return (
2869 format!("{} AS {}", source_ref(&grp.source), qi(alias)),
2870 grp.filter.as_ref().map(emit_expr),
2871 );
2872 }
2873 let ranked = format!(
2874 "(SELECT {}.*, row_number() OVER (PARTITION BY {}{}) AS \"__rk\"\n FROM {} AS {}{}) AS {}",
2875 qi(alias),
2876 group_by_sql(grp),
2877 group_element_order(grp),
2878 source_ref(&grp.source),
2879 qi(alias),
2880 grp.filter
2881 .as_ref()
2882 .map(|f| format!("\n WHERE {}", emit_expr(f)))
2883 .unwrap_or_default(),
2884 qi(alias),
2885 );
2886 let lower = grp.offset.as_ref().map(emit_expr).unwrap_or_else(|| "0".to_string());
2887 let mut conds = vec![format!("\"__rk\" > {}", lower)];
2888 if let Some(l) = &grp.limit {
2889 conds.push(format!("\"__rk\" <= {} + {}", lower, emit_expr(l)));
2890 }
2891 (ranked, Some(conds.join(" AND ")))
2892}
2893
2894fn group_by_sql(grp: &IrGroup) -> String {
2895 grp.keys
2896 .iter()
2897 .map(|(_, key_expr)| emit_expr(key_expr))
2898 .collect::<Vec<_>>()
2899 .join(", ")
2900}
2901
2902fn group_element_order(grp: &IrGroup) -> String {
2903 if grp.order_by.is_empty() {
2904 return String::new();
2905 }
2906 let s: Vec<_> = grp.order_by.iter().map(emit_sort_clause).collect();
2907 format!(" ORDER BY {}", s.join(", "))
2908}
2909
2910fn emit_group_elements_source(grp: &IrGroup) -> String {
2913 let (from_sql, cond) = group_rows(grp);
2914 let mut sql = format!("SELECT {}.*\n FROM {}", qi(&grp.source.alias), from_sql);
2915 if let Some(cond) = cond {
2916 sql.push_str(&format!("\n WHERE {}", cond));
2917 }
2918 sql
2919}
2920
2921fn emit_group(grp: &IrGroup) -> SqlOutput {
2922 match &grp.output {
2923 IrGroupOutput::Groups => emit_group_rows(grp),
2924 IrGroupOutput::Projection(projection) => emit_group_projection(
2925 grp,
2926 &projection.pointers,
2927 &projection.order_by,
2928 &projection.offset,
2929 &projection.limit,
2930 ),
2931 IrGroupOutput::Elements => emit_group_elements(grp),
2932 }
2933}
2934
2935fn emit_group_projection(
2936 grp: &IrGroup,
2937 pointers: &[IrShapePointer],
2938 order_by: &[IrSort],
2939 offset: &Option<IrExpr>,
2940 limit: &Option<IrExpr>,
2941) -> SqlOutput {
2942 let (exprs, nodes) = build_shape(pointers, &grp.source.alias);
2943 let mut parts = vec!["NULL::text".to_string()];
2944 parts.extend(exprs);
2945 let (from_sql, cond) = group_rows(grp);
2946 let mut sql = format!(
2947 "SELECT (\n {}\n) AS \"result\"\nFROM {}",
2948 parts.join(",\n "),
2949 from_sql
2950 );
2951 if let Some(cond) = cond {
2952 sql.push_str(&format!("\nWHERE {}", cond));
2953 }
2954 sql.push_str(&format!("\nGROUP BY {}", group_by_sql(grp)));
2955 if !order_by.is_empty() {
2956 let sorts: Vec<_> = order_by.iter().map(emit_sort_clause).collect();
2957 sql.push_str(&format!("\nORDER BY {}", sorts.join(", ")));
2958 }
2959 append_offset_limit(&mut sql, offset, limit);
2960 SqlOutput {
2961 sql,
2962 shape: ShapeDescriptor {
2963 root: ShapeNode::Object {
2964 name: String::new(),
2965 type_name: None,
2966 position: 0,
2967 cardinality: Cardinality::Many,
2968 pointers: nodes,
2969 has_implicit_id: false,
2970 },
2971 },
2972 inference_plan: None,
2973 }
2974}
2975
2976fn emit_group_elements(grp: &IrGroup) -> SqlOutput {
2977 let (exprs, nodes) = build_shape(&grp.shape, &grp.source.alias);
2978 let mut parts = vec![source_type_disc(&grp.source)];
2979 parts.extend(exprs);
2980 let (from_sql, cond) = group_rows(grp);
2981 let mut sql = format!(
2982 "SELECT (\n {}\n) AS \"result\"\nFROM {}",
2983 parts.join(",\n "),
2984 from_sql
2985 );
2986 if let Some(cond) = cond {
2987 sql.push_str(&format!("\nWHERE {}", cond));
2988 }
2989 SqlOutput {
2990 sql,
2991 shape: ShapeDescriptor {
2992 root: ShapeNode::Object {
2993 name: String::new(),
2994 type_name: Some(grp.source.type_name.clone()),
2995 position: 0,
2996 cardinality: Cardinality::Many,
2997 pointers: prepend_type(nodes),
2998 has_implicit_id: shape_has_implicit_id(&grp.shape),
2999 },
3000 },
3001 inference_plan: None,
3002 }
3003}
3004
3005fn emit_group_rows(grp: &IrGroup) -> SqlOutput {
3006 let alias = &grp.source.alias;
3007 let (shape_exprs, shape_nodes) = build_shape(&grp.shape, alias);
3008
3009 let mut elem_row_parts = vec![source_type_disc(&grp.source)];
3011 elem_row_parts.extend(shape_exprs);
3012 let elem_row = elem_row_parts.join(",\n ");
3013
3014 let n_keys = grp.keys.len();
3016 let grouping_pos = n_keys + 1;
3017 let elements_pos = n_keys + 2;
3018
3019 let mut key_exprs_sql: Vec<String> = vec![];
3021 let mut key_nodes: Vec<ShapeNode> = vec![];
3022 for (i, (key_name, key_expr)) in grp.keys.iter().enumerate() {
3023 let pos = i + 1;
3024 if let IrExpr::ColumnRef { pg_type, .. } = key_expr
3025 && let Some(qualified) = QualifiedPgType::of(pg_type)
3026 {
3027 key_exprs_sql.push(emit_key_expr(key_expr));
3028 key_nodes.push(qualified.shape_node(key_name.clone(), pos));
3029 continue;
3030 }
3031 key_exprs_sql.push(emit_expr(key_expr));
3032 key_nodes.push(ShapeNode::Scalar {
3033 name: key_name.clone(),
3034 position: pos,
3035 });
3036 }
3037
3038 let mut outer_parts = vec!["NULL::text".to_string()];
3040 outer_parts.extend(key_exprs_sql.clone());
3041 let key_names_sql = grp
3042 .keys
3043 .iter()
3044 .map(|(name, _)| format!("'{}'", name))
3045 .collect::<Vec<_>>()
3046 .join(", ");
3047 outer_parts.push(format!("ARRAY[{}]::text[]", key_names_sql));
3048 outer_parts.push(format!(
3049 "array_agg(ROW(\n {}\n )::record{})",
3050 elem_row,
3051 group_element_order(grp)
3052 ));
3053
3054 let outer_tuple = outer_parts.join(",\n ");
3055 let (from_sql, cond) = group_rows(grp);
3056 let mut sql = format!("SELECT (\n {}\n) AS \"result\"\nFROM {}", outer_tuple, from_sql,);
3057 if let Some(cond) = cond {
3058 sql.push_str(&format!("\nWHERE {}", cond));
3059 }
3060 sql.push_str(&format!("\nGROUP BY {}", group_by_sql(grp)));
3061
3062 let element_node = ShapeNode::Object {
3064 name: String::new(),
3065 type_name: Some(grp.source.type_name.clone()),
3066 position: 0,
3067 cardinality: Cardinality::Many,
3068 pointers: prepend_type(shape_nodes),
3069 has_implicit_id: shape_has_implicit_id(&grp.shape),
3070 };
3071
3072 let root = ShapeNode::Group {
3073 key_nodes,
3074 grouping_position: grouping_pos,
3075 elements_position: elements_pos,
3076 element: Box::new(element_node),
3077 };
3078
3079 SqlOutput {
3080 sql,
3081 shape: ShapeDescriptor { root },
3082 inference_plan: None,
3083 }
3084}
3085
3086fn emit_poly_union_type_only(implementors: &[IrPolyImplementor]) -> String {
3087 implementors
3088 .iter()
3089 .map(|imp| {
3090 format!(
3091 " SELECT {}::text AS \"__type__\" FROM {}",
3092 sql_str(&imp.type_name),
3093 qn(&imp.module, &imp.table),
3094 )
3095 })
3096 .collect::<Vec<_>>()
3097 .join("\n UNION ALL\n")
3098}
3099
3100fn emit_path_select(sel: &IrPathSelect) -> SqlOutput {
3101 let distinct = if sel.distinct { "DISTINCT " } else { "" };
3102 let from_sql = if !sel.poly_implementors.is_empty() {
3103 format!(
3104 "(\n{}\n) AS {}",
3105 emit_poly_union_type_only(&sel.poly_implementors),
3106 qi(&sel.root.alias),
3107 )
3108 } else {
3109 emit_path_joins(&sel.root, &sel.joins)
3110 };
3111
3112 let (result_expr, shape_root) = match &sel.result {
3113 IrPathResult::Scalar(ir_expr, tuple_shape) => {
3114 let is_nt = matches!(ir_expr, IrExpr::NamedTuple { .. })
3116 || matches!(ir_expr, IrExpr::Tuple(_))
3117 || matches!(ir_expr, IrExpr::JsonbField { .. })
3118 || matches!(ir_expr, IrExpr::JsonbIndex { .. })
3119 || matches!(ir_expr, IrExpr::ColumnRef { pg_type, .. } if pg_type.starts_with("__nt__:"))
3120 || tuple_shape.is_some();
3121 if is_nt {
3122 let expr_sql = format!("{} AS result", emit_expr(ir_expr));
3123 let shape = if matches!(
3124 ir_expr,
3125 IrExpr::JsonbField { .. } | IrExpr::JsonbIndex { .. } | IrExpr::Tuple(_)
3126 ) {
3127 ShapeNode::RawScalar
3128 } else if let Some(shape) = tuple_shape {
3129 ShapeNode::NamedTuple {
3135 name: String::new(),
3136 position: 0,
3137 type_name: shape.type_name.clone(),
3138 members: Some(shape.members.clone()),
3139 is_free_object: false,
3140 }
3141 } else {
3142 let type_name = match ir_expr {
3143 IrExpr::ColumnRef { pg_type, .. } => pg_type.strip_prefix("__nt__:").map(|s| s.to_string()),
3144 _ => None,
3145 };
3146 ShapeNode::NamedTuple {
3147 name: String::new(),
3148 position: 0,
3149 type_name,
3150 members: None,
3151 is_free_object: false,
3152 }
3153 };
3154 (expr_sql, shape)
3155 } else {
3156 if let IrExpr::ColumnRef { pg_type, .. } = ir_expr {
3159 if let Some(qualified) = QualifiedPgType::of(pg_type) {
3160 let expr = format!("ROW({}{}) AS result", emit_expr(ir_expr), qualified.text_cast());
3161 let shape = qualified.shape_node(String::new(), 0);
3162 (expr, shape)
3163 } else {
3164 let expr = format!("ROW({}) AS result", emit_expr(ir_expr));
3165 (
3166 expr,
3167 ShapeNode::Scalar {
3168 name: String::new(),
3169 position: 0,
3170 },
3171 )
3172 }
3173 } else {
3174 let expr = format!("ROW({}) AS result", emit_expr(ir_expr));
3175 (
3176 expr,
3177 ShapeNode::Scalar {
3178 name: String::new(),
3179 position: 0,
3180 },
3181 )
3182 }
3183 }
3184 }
3185 IrPathResult::Object {
3186 alias,
3187 type_name,
3188 shape,
3189 } => {
3190 let (pointer_exprs, pointer_nodes) = build_shape(shape, alias);
3191 let mut parts = vec![result_type_disc(sel, alias, type_name)];
3192 parts.extend(pointer_exprs);
3193 let expr = format!("(\n {}\n) AS result", parts.join(",\n "));
3194 let shape_root = ShapeNode::Object {
3195 name: String::new(),
3196 type_name: Some(type_name.clone()),
3197 position: 0,
3198 cardinality: Cardinality::Many,
3199 pointers: prepend_type(pointer_nodes),
3200 has_implicit_id: shape_has_implicit_id(shape),
3201 };
3202 (expr, shape_root)
3203 }
3204 };
3205
3206 let mut sql = format!("SELECT {}{}\nFROM {}", distinct, result_expr, from_sql);
3207 append_filter(&mut sql, &sel.filter);
3208 append_order_by(&mut sql, &sel.order_by);
3209 append_offset_limit(&mut sql, &sel.offset, &sel.limit);
3210
3211 SqlOutput {
3212 sql,
3213 shape: ShapeDescriptor { root: shape_root },
3214 inference_plan: None,
3215 }
3216}
3217
3218fn yields_jsonb(expr: &IrExpr) -> bool {
3225 match expr {
3226 IrExpr::JsonbField { .. } | IrExpr::JsonbIndex { .. } | IrExpr::NamedTuple { .. } => true,
3227 IrExpr::TypeCast(c) => c.pg_type == "jsonb",
3228 IrExpr::ColumnRef { pg_type, .. } | IrExpr::FnParam { pg_type, .. } => pg_type == "jsonb",
3229 IrExpr::CteRef { pg_type, .. } | IrExpr::ForVar { pg_type, .. } => pg_type.as_deref() == Some("jsonb"),
3230 IrExpr::FunctionCall(f) if f.return_pg_type.is_some() => f.return_pg_type.as_deref() == Some("jsonb"),
3234 IrExpr::FunctionCall(f) if f.schema.is_none() => {
3235 let mut overloads = crate::stdlib::registry().iter().filter(|d| d.name == f.name).peekable();
3236 overloads.peek().is_some() && overloads.all(|d| matches!(d.return_type, crate::stdlib::PylonType::Json))
3237 }
3238 _ => false,
3239 }
3240}
3241
3242fn emit_for_iterator(it: &IrForIterator, iter_alias: &str) -> (String, String) {
3243 match it {
3244 IrForIterator::Values { exprs, pg_type } => {
3245 let rows: Vec<String> = exprs
3246 .iter()
3247 .map(|e| format!("({}::{})", emit_expr(e), pg_type))
3248 .collect();
3249 (
3250 format!("(VALUES {}) AS {}(\"v\")", rows.join(", "), qi(iter_alias)),
3251 format!("{}(\"v\") AS (VALUES {})", qi(iter_alias), rows.join(", ")),
3252 )
3253 }
3254 IrForIterator::Query { stmt, scalar } => {
3255 let inner = emit_dml_as_cte_source(stmt);
3256 let body = if *scalar {
3260 inner
3261 } else {
3262 format!(" SELECT \"id\" AS v FROM (\n{}\n ) AS _src", inner)
3263 };
3264 (
3265 format!("(\n{}\n) AS {}", body, qi(iter_alias)),
3266 format!("{} AS (\n{}\n)", qi(iter_alias), body),
3267 )
3268 }
3269 IrForIterator::SetReturning { expr, pg_type } => {
3270 let body = format!(" SELECT {}::{} AS v", emit_expr(expr), pg_type);
3271 (
3272 format!("(\n{}\n) AS {}", body, qi(iter_alias)),
3273 format!("{} AS (\n{}\n)", qi(iter_alias), body),
3274 )
3275 }
3276 }
3277}
3278
3279fn emit_nested_for_iterator(it: &IrForIterator, iter_alias: &str, outer_alias: &str) -> String {
3284 const OUTER: &str = "_outer";
3285 match it {
3286 IrForIterator::Values { exprs, pg_type } => {
3287 let rows: Vec<String> = exprs
3288 .iter()
3289 .map(|e| format!("({}::{})", emit_expr(e), pg_type))
3290 .collect();
3291 format!(
3292 "{} AS (\nSELECT {}.\"v\" AS {}, \"_vals\".\"v\" AS v\nFROM {}, (VALUES {}) AS \"_vals\"(\"v\")\n)",
3293 qi(iter_alias),
3294 qi(outer_alias),
3295 qi(OUTER),
3296 qi(outer_alias),
3297 rows.join(", "),
3298 )
3299 }
3300 IrForIterator::Query { stmt, scalar } => {
3301 let inner = emit_dml_as_cte_source(stmt);
3302 let projected = if *scalar { "\"_src\".\"v\"" } else { "\"_src\".\"id\"" };
3303 format!(
3304 "{} AS (\nSELECT {}.\"v\" AS {}, {} AS v\nFROM {}\nCROSS JOIN LATERAL (\n{}\n) AS \"_src\"\n)",
3305 qi(iter_alias),
3306 qi(outer_alias),
3307 qi(OUTER),
3308 projected,
3309 qi(outer_alias),
3310 inner,
3311 )
3312 }
3313 IrForIterator::SetReturning { expr, pg_type } => format!(
3314 "{} AS (\nSELECT {}.\"v\" AS {}, \"_vals\".\"v\" AS v\nFROM {}, LATERAL (SELECT {}::{} AS v) AS \"_vals\"\n)",
3315 qi(iter_alias),
3316 qi(outer_alias),
3317 qi(OUTER),
3318 qi(outer_alias),
3319 emit_expr(expr),
3320 pg_type,
3321 ),
3322 }
3323}
3324
3325fn nested_for_from(inner_alias: &str, outer_alias: &str) -> String {
3328 format!(
3329 "{} JOIN {} ON {}.\"v\" = {}.\"_outer\"",
3330 qi(inner_alias),
3331 qi(outer_alias),
3332 qi(outer_alias),
3333 qi(inner_alias),
3334 )
3335}
3336
3337fn uncorrelated_body_ctes(f: &IrFor) -> Vec<IrCteDef> {
3351 f.body_ctes
3352 .iter()
3353 .map(|cte| IrCteDef {
3354 correlated_to: None,
3355 ..cte.clone()
3356 })
3357 .collect()
3358}
3359
3360fn prefix_body_ctes(sql: String, body_ctes: &[IrCteDef]) -> String {
3361 if body_ctes.is_empty() {
3362 return sql;
3363 }
3364 merge_into_existing_with(&sql, &emit_user_cte_parts(body_ctes))
3365 .unwrap_or_else(|| format!("{}{}", emit_cte_prefix(body_ctes), sql))
3366}
3367
3368fn emit_for_stmt(f: &IrFor, user_ctes: &[IrCteDef]) -> SqlOutput {
3369 let iter_alias = format!("_for_{}", f.var_name);
3370
3371 if matches!(&f.iterator, IrForIterator::Values { exprs, .. } if exprs.is_empty()) {
3372 let empty = SqlOutput {
3373 sql: "SELECT NULL AS result WHERE FALSE".to_string(),
3374 shape: ShapeDescriptor {
3375 root: ShapeNode::Scalar {
3376 name: String::new(),
3377 position: 0,
3378 },
3379 },
3380 inference_plan: None,
3381 };
3382 return empty;
3383 }
3384
3385 let (values_from, iter_cte) = emit_for_iterator(&f.iterator, &iter_alias);
3386
3387 match f.body.as_ref() {
3388 IrStmt::Insert(ins) => emit_for_insert(ins, &iter_alias, &iter_cte, user_ctes, &f.body_ctes),
3389 IrStmt::Update(upd) => emit_for_update(upd, &iter_alias, &iter_cte, user_ctes, &f.body_ctes),
3390 body => {
3391 let body_out = match body {
3392 IrStmt::Select(sel) => emit_select_stmt(sel, user_ctes),
3393 IrStmt::PathSelect(sel) => emit_path_select(sel),
3394 other => unreachable!("for-loop body should have been rejected at compile time: {other:?}"),
3397 };
3398 let indent_body = prefix_body_ctes(body_out.sql, &uncorrelated_body_ctes(f)).replace('\n', "\n ");
3399 let cte_prefix = if !user_ctes.is_empty() {
3400 emit_cte_prefix(user_ctes)
3401 } else {
3402 String::new()
3403 };
3404 let sql = format!(
3405 "{}SELECT \"_body\".result\nFROM {}\nCROSS JOIN LATERAL (\n {}\n) AS \"_body\"",
3406 cte_prefix, values_from, indent_body,
3407 );
3408 SqlOutput {
3409 sql,
3410 shape: body_out.shape,
3411 inference_plan: None,
3412 }
3413 }
3414 }
3415}
3416
3417fn emit_for_update(
3422 upd: &IrUpdate,
3423 iter_alias: &str,
3424 iter_cte: &str,
3425 user_ctes: &[IrCteDef],
3426 body_ctes: &[IrCteDef],
3427) -> SqlOutput {
3428 let mut cte_parts: Vec<String> = emit_user_cte_parts(user_ctes);
3429 cte_parts.extend(emit_user_cte_parts(body_ctes));
3430 cte_parts.push(iter_cte.to_string());
3431
3432 let alias = &upd.target.alias;
3433 if upd.assignments.is_empty() && upd.rewrites.is_empty() && !upd.multi_link_appends.is_empty() {
3437 const ITER_COL: &str = "_iter";
3438 let ids_name = "_ids";
3439 let generated: Vec<Option<&IrInsert>> = upd
3443 .multi_link_appends
3444 .iter()
3445 .map(|a| per_iteration_insert(a, &upd.nested_ctes))
3446 .collect();
3447 let new_cols: String = generated
3448 .iter()
3449 .enumerate()
3450 .filter_map(|(i, ins)| {
3451 ins.map(|ins| {
3452 format!(
3453 ", {} AS {}",
3454 ins.id_default_sql.as_deref().unwrap_or("uuidv7()"),
3455 qi(&format!("_new_{i}"))
3456 )
3457 })
3458 })
3459 .collect();
3460 let mut ids_sql = format!(
3461 "\"{}\" AS (\nSELECT {}.*, {}.\"v\" AS {}{} FROM {} AS {}, {}",
3462 ids_name,
3463 qi(alias),
3464 qi(iter_alias),
3465 qi(ITER_COL),
3466 new_cols,
3467 target_ref(&upd.target),
3468 qi(alias),
3469 qi(iter_alias),
3470 );
3471 append_filter(&mut ids_sql, &upd.filter);
3472 ids_sql.push_str("\n)");
3473 cte_parts.push(ids_sql);
3474 for (i, append) in upd.multi_link_appends.iter().enumerate() {
3475 match generated[i] {
3476 Some(ins) => {
3477 cte_parts.push(emit_for_nested_insert_cte(
3478 ins,
3479 ids_name,
3480 iter_alias,
3481 &format!("_ml_ins_{i}"),
3482 &format!("_new_{i}"),
3483 ));
3484 cte_parts.push(emit_for_ml_append_cte(
3485 append,
3486 ids_name,
3487 &format!("_ml_add_{i}"),
3488 &format!("_new_{i}"),
3489 ));
3490 }
3491 None => cte_parts.push(emit_for_ml_append_cte(
3492 append,
3493 ids_name,
3494 &format!("_ml_add_{i}"),
3495 ITER_COL,
3496 )),
3497 }
3498 }
3499 let (pointer_exprs, shape_nodes) = build_shape(&upd.returning, ids_name);
3500 let mut parts = vec![type_disc(&upd.target.type_name)];
3501 parts.extend(pointer_exprs);
3502 let sql = format!(
3503 "WITH {}\nSELECT (\n {}\n) AS result\nFROM \"{}\"",
3504 cte_parts.join(",\n"),
3505 parts.join(",\n "),
3506 ids_name,
3507 );
3508 return SqlOutput {
3509 sql,
3510 shape: crate::query::ShapeDescriptor {
3511 root: ShapeNode::Object {
3512 name: String::new(),
3513 type_name: Some(upd.target.type_name.clone()),
3514 position: 0,
3515 cardinality: Cardinality::Many,
3516 pointers: prepend_type(shape_nodes),
3517 has_implicit_id: shape_has_implicit_id(&upd.returning),
3518 },
3519 },
3520 inference_plan: None,
3521 };
3522 }
3523 let sets: Vec<String> = upd
3524 .assignments
3525 .iter()
3526 .map(|(col, expr)| format!("{} = {}", qi(col), emit_expr(expr)))
3527 .chain(
3528 upd.rewrites
3529 .iter()
3530 .map(|rw| format!("{} = {}", qi(&rw.column), emit_expr(&rw.expr))),
3531 )
3532 .collect();
3533 let mut sql = format!(
3534 "WITH {}\nUPDATE {} AS {}\nSET {}\nFROM {}",
3535 cte_parts.join(",\n"),
3536 target_ref(&upd.target),
3537 qi(alias),
3538 sets.join(", "),
3539 qi(iter_alias),
3540 );
3541 append_filter(&mut sql, &upd.filter);
3542 let mut parts = vec![type_disc(&upd.target.type_name)];
3543 let (pointer_exprs, shape_nodes) = build_shape(&upd.returning, alias);
3544 parts.extend(pointer_exprs);
3545 sql.push_str(&format!("\nRETURNING (\n {}\n) AS result", parts.join(",\n ")));
3546 SqlOutput {
3547 sql,
3548 shape: crate::query::ShapeDescriptor {
3549 root: ShapeNode::Object {
3550 name: String::new(),
3551 type_name: Some(upd.target.type_name.clone()),
3552 position: 0,
3553 cardinality: Cardinality::Many,
3554 pointers: prepend_type(shape_nodes),
3555 has_implicit_id: shape_has_implicit_id(&upd.returning),
3556 },
3557 },
3558 inference_plan: None,
3559 }
3560}
3561
3562const OUTER_KEY: &str = "_outer";
3567
3568fn emit_iteration_rows_cte(ins: &IrInsert, cte_name: &str, iter_alias: &str, outer_source: &str) -> String {
3573 let rewrite_cols: std::collections::HashSet<&str> = ins.rewrites.iter().map(|r| r.column.as_str()).collect();
3574 let mut selected = vec![
3575 format!(
3576 "{} AS {}",
3577 ins.id_default_sql.as_deref().unwrap_or("uuidv7()"),
3578 qi("id")
3579 ),
3580 format!("{} AS {}", outer_source, qi(OUTER_KEY)),
3581 ];
3582 selected.extend(
3583 ins.assignments
3584 .iter()
3585 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
3586 .map(|(c, e)| format!("{} AS {}", emit_expr(e), qi(c)))
3587 .chain(
3588 ins.rewrites
3589 .iter()
3590 .map(|r| format!("{} AS {}", emit_expr(&r.expr), qi(&r.column))),
3591 ),
3592 );
3593 format!(
3594 "\"{}\" AS (\nSELECT {}\nFROM {}\n)",
3595 cte_name,
3596 selected.join(", "),
3597 qi(iter_alias),
3598 )
3599}
3600
3601fn emit_insert_from_rows_cte(ins: &IrInsert, cte_name: &str, rows_name: &str) -> String {
3603 let rewrite_cols: std::collections::HashSet<&str> = ins.rewrites.iter().map(|r| r.column.as_str()).collect();
3604 let cols: Vec<String> = std::iter::once(qi("id"))
3605 .chain(
3606 ins.assignments
3607 .iter()
3608 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
3609 .map(|(c, _)| qi(c)),
3610 )
3611 .chain(ins.rewrites.iter().map(|r| qi(&r.column)))
3612 .collect();
3613 let mut sql = format!(
3614 "INSERT INTO {} ({})\nSELECT {} FROM \"{}\"",
3615 target_ref(&ins.target),
3616 cols.join(", "),
3617 cols.join(", "),
3618 rows_name,
3619 );
3620 if let Some(conflict) = &ins.unless_conflict {
3621 emit_conflict(&mut sql, conflict);
3622 }
3623 sql.push_str("\nRETURNING *");
3624 format!("\"{}\" AS (\n{}\n)", cte_name, sql)
3625}
3626
3627fn emit_correlated_ml_append_cte(
3631 mutation: &IrMultiLinkMutation,
3632 rows_name: &str,
3633 targets_name: &str,
3634 cte_name: &str,
3635) -> String {
3636 format!(
3637 "\"{}\" AS (\nINSERT INTO {} ({}, {})\nSELECT \"_src\".{}, \"_tgt\".{} FROM \"{}\" AS \"_src\" JOIN \"{}\" AS \"_tgt\" ON \"_tgt\".{} = \"_src\".{}\nON CONFLICT DO NOTHING\nRETURNING {}, {}\n)",
3638 cte_name,
3639 qn(&mutation.module, &mutation.junction_table),
3640 qi(&mutation.source_col),
3641 qi(&mutation.target_col),
3642 qi("id"),
3643 qi("id"),
3644 rows_name,
3645 targets_name,
3646 qi(OUTER_KEY),
3647 qi(OUTER_KEY),
3648 qi(&mutation.source_col),
3649 qi(&mutation.target_col),
3650 )
3651}
3652
3653fn correlated_append_indices(appends: &[IrMultiLinkMutation], defs: &[&[IrCteDef]]) -> Vec<usize> {
3656 appends
3657 .iter()
3658 .enumerate()
3659 .filter(|(_, append)| match &append.values.source {
3660 IrMultiLinkValueSource::CteRef(target) => defs
3661 .iter()
3662 .flat_map(|group| group.iter())
3663 .any(|c| &c.name == target && c.correlated_to.is_some()),
3664 _ => false,
3665 })
3666 .map(|(i, _)| i)
3667 .collect()
3668}
3669
3670fn emit_for_insert(
3671 ins: &IrInsert,
3672 iter_alias: &str,
3673 iter_cte: &str,
3674 user_ctes: &[IrCteDef],
3675 body_ctes: &[IrCteDef],
3676) -> SqlOutput {
3677 let rewrite_cols: std::collections::HashSet<&str> = ins.rewrites.iter().map(|r| r.column.as_str()).collect();
3678
3679 let cols: Vec<String> = ins
3680 .assignments
3681 .iter()
3682 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
3683 .map(|(c, _)| qi(c))
3684 .chain(ins.rewrites.iter().map(|r| qi(&r.column)))
3685 .collect();
3686 let sel_exprs: Vec<String> = ins
3687 .assignments
3688 .iter()
3689 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
3690 .map(|(_, e)| emit_expr(e))
3691 .chain(ins.rewrites.iter().map(|r| emit_expr(&r.expr)))
3692 .collect();
3693
3694 let mut cte_parts: Vec<String> = emit_user_cte_parts(user_ctes);
3695 cte_parts.push(iter_cte.to_string());
3699 cte_parts.extend(emit_user_cte_parts(body_ctes));
3700 cte_parts.extend(emit_user_cte_parts(&ins.nested_ctes));
3701 let correlated_targets = correlated_append_indices(&ins.multi_link_appends, &[body_ctes, &ins.nested_ctes]);
3704
3705 let mut insert_sql = format!(
3706 "INSERT INTO {} ({})\nSELECT {} FROM {}",
3707 target_ref(&ins.target),
3708 cols.join(", "),
3709 sel_exprs.join(", "),
3710 qi(iter_alias),
3711 );
3712 if let Some(conflict) = &ins.unless_conflict {
3713 emit_conflict(&mut insert_sql, conflict);
3714 }
3715 let (shape, returning_sql) = emit_returning_shape(&ins.target, &ins.returning, false);
3716
3717 if ins.multi_link_appends.is_empty() {
3718 let mut sql = format!("WITH {}\n{}", cte_parts.join(",\n"), insert_sql);
3719 if let Some(r) = returning_sql {
3720 sql.push_str(&r);
3721 }
3722 return SqlOutput {
3723 sql,
3724 shape,
3725 inference_plan: None,
3726 };
3727 }
3728
3729 const IDS: &str = "_for_dml__ids";
3732 const ROWS: &str = "_for_dml__rows";
3733 if correlated_targets.is_empty() {
3734 insert_sql.push_str("\nRETURNING *");
3735 cte_parts.push(format!("\"{}\" AS (\n{}\n)", IDS, insert_sql));
3736 } else {
3737 let outer_source = format!("{}.{}", qi(iter_alias), qi("v"));
3738 cte_parts.push(emit_iteration_rows_cte(ins, ROWS, iter_alias, &outer_source));
3739 cte_parts.push(emit_insert_from_rows_cte(ins, IDS, ROWS));
3740 }
3741 for (i, append) in ins.multi_link_appends.iter().enumerate() {
3742 let cte_name = format!("_for_dml__ml_add_{}", i);
3743 if correlated_targets.contains(&i) {
3744 let IrMultiLinkValueSource::CteRef(target) = &append.values.source else {
3745 unreachable!("only a CTE-backed value can carry an iteration key")
3746 };
3747 cte_parts.push(emit_correlated_ml_append_cte(append, ROWS, target, &cte_name));
3748 } else {
3749 cte_parts.push(emit_ml_append_cte(append, IDS, &ins.target.alias, &cte_name));
3750 }
3751 }
3752
3753 let projection = match &returning_sql {
3754 Some(r) => r.trim_start_matches('\n').replacen("RETURNING ", "SELECT ", 1),
3757 None => "SELECT 1".to_string(),
3758 };
3759 SqlOutput {
3760 sql: format!("WITH {}\n{}\nFROM \"{}\"", cte_parts.join(",\n"), projection, IDS),
3761 shape,
3762 inference_plan: None,
3763 }
3764}
3765
3766fn enqueue_cte_sql(eq: &VectorEnqueueInfo, source_cte: &str, cte_name: &str) -> String {
3772 let index_name_sql = match &eq.index_name {
3773 None => "NULL".to_string(),
3774 Some(name) => sql_str(name),
3775 };
3776 format!(
3777 concat!(
3778 "\"{}\" AS (\n",
3779 " INSERT INTO _pylon.\"IndexOutbox\"\n",
3780 " (object_id, type_name, index_kind, index_name)\n",
3781 " SELECT \"id\", {}, 'Vector'::_pylon.\"IndexKind\", {}\n",
3782 " FROM \"{}\"\n",
3783 " ON CONFLICT (object_id, index_kind, index_name)\n",
3784 " DO UPDATE SET status = 'Pending', enqueued_at = now()\n",
3785 ")",
3786 ),
3787 cte_name,
3788 sql_str(&eq.type_name),
3789 index_name_sql,
3790 source_cte,
3791 )
3792}
3793
3794fn enqueue_ctes(enqueue: &[VectorEnqueueInfo], source_cte: &str) -> Vec<String> {
3796 enqueue
3797 .iter()
3798 .enumerate()
3799 .map(|(i, eq)| enqueue_cte_sql(eq, source_cte, &format!("_eq{}", i)))
3800 .collect()
3801}
3802
3803fn search_backend_index_kind(backend: &crate::schema::SearchBackend) -> &'static str {
3807 match backend {
3808 crate::schema::SearchBackend::OpenSearch => "OpenSearch",
3809 crate::schema::SearchBackend::Meilisearch => "Meilisearch",
3810 crate::schema::SearchBackend::Postgres => {
3811 unreachable!("Postgres-backed search indexes are never collected into SearchEnqueueInfo")
3812 }
3813 }
3814}
3815
3816fn enqueue_search_cte_sql(eq: &SearchEnqueueInfo, source_cte: &str, cte_name: &str) -> String {
3818 let index_name_sql = match &eq.index_name {
3819 None => "NULL".to_string(),
3820 Some(name) => sql_str(name),
3821 };
3822 format!(
3823 concat!(
3824 "\"{}\" AS (\n",
3825 " INSERT INTO _pylon.\"IndexOutbox\"\n",
3826 " (object_id, type_name, index_kind, index_name, operation)\n",
3827 " SELECT \"id\", {}, '{}'::_pylon.\"IndexKind\", {}, {}\n",
3828 " FROM \"{}\"\n",
3829 " ON CONFLICT (object_id, index_kind, index_name)\n",
3830 " DO UPDATE SET status = 'Pending', operation = EXCLUDED.operation, enqueued_at = now()\n",
3831 ")",
3832 ),
3833 cte_name,
3834 sql_str(&eq.type_name),
3835 search_backend_index_kind(&eq.backend),
3836 index_name_sql,
3837 sql_str(eq.operation),
3838 source_cte,
3839 )
3840}
3841
3842fn enqueue_search_ctes(enqueue: &[SearchEnqueueInfo], source_cte: &str, offset: usize) -> Vec<String> {
3844 enqueue
3845 .iter()
3846 .enumerate()
3847 .map(|(i, eq)| enqueue_search_cte_sql(eq, source_cte, &format!("_es{}", offset + i)))
3848 .collect()
3849}
3850
3851fn shape_select_from_cte(
3854 target: &IrSource,
3855 returning: &[IrShapePointer],
3856 cte_name: &str,
3857) -> (ShapeDescriptor, Option<String>) {
3858 if returning.is_empty() {
3859 return (
3860 ShapeDescriptor {
3861 root: ShapeNode::Scalar {
3862 name: String::new(),
3863 position: 0,
3864 },
3865 },
3866 None,
3867 );
3868 }
3869 let (pointer_exprs, shape_pointers) = build_shape(returning, "");
3870 let mut parts = vec![type_disc(&target.type_name)];
3871 parts.extend(pointer_exprs);
3872 let tuple = parts.join(",\n ");
3873 let sql = format!("SELECT (\n {}\n) AS result\nFROM {}", tuple, qi(cte_name));
3874 let root_pointers = prepend_type(shape_pointers);
3875 let shape = ShapeDescriptor {
3876 root: ShapeNode::Object {
3877 name: String::new(),
3878 type_name: Some(target.type_name.clone()),
3879 position: 0,
3880 cardinality: Cardinality::Required,
3881 pointers: root_pointers,
3882 has_implicit_id: shape_has_implicit_id(returning),
3883 },
3884 };
3885 (shape, Some(sql))
3886}
3887
3888fn emit_insert_row_sql(ins: &IrInsert) -> String {
3897 let rewrite_cols: std::collections::HashSet<&str> = ins.rewrites.iter().map(|r| r.column.as_str()).collect();
3898 let cols: Vec<String> = ins
3899 .assignments
3900 .iter()
3901 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
3902 .map(|(c, _)| qi(c))
3903 .chain(ins.rewrites.iter().map(|r| qi(&r.column)))
3904 .collect();
3905 let vals: Vec<String> = ins
3906 .assignments
3907 .iter()
3908 .filter(|(c, _)| !rewrite_cols.contains(c.as_str()))
3909 .map(|(_, e)| emit_expr(e))
3910 .chain(ins.rewrites.iter().map(|r| emit_expr(&r.expr)))
3911 .collect();
3912 if let Some(guard) = &ins.guard {
3915 let from_ctes = nested_cte_from(&ins.nested_ctes, &vals.join(","), " ");
3916 let projection = if cols.is_empty() {
3917 String::new()
3918 } else {
3919 format!(" {}", vals.join(", "))
3920 };
3921 let column_list = if cols.is_empty() {
3922 String::new()
3923 } else {
3924 format!(" ({})", cols.join(", "))
3925 };
3926 return format!(
3927 "INSERT INTO {}{} SELECT{}{} WHERE {}",
3928 target_ref(&ins.target),
3929 column_list,
3930 projection,
3931 from_ctes,
3932 emit_expr(guard),
3933 );
3934 }
3935 if cols.is_empty() && ins.nested_ctes.is_empty() {
3936 format!("INSERT INTO {} DEFAULT VALUES", target_ref(&ins.target))
3939 } else if ins.nested_ctes.is_empty() {
3940 format!(
3941 "INSERT INTO {} ({}) VALUES ({})",
3942 target_ref(&ins.target),
3943 cols.join(", "),
3944 vals.join(", ")
3945 )
3946 } else {
3947 format!(
3948 "INSERT INTO {} ({}) SELECT {}{}",
3949 target_ref(&ins.target),
3950 cols.join(", "),
3951 vals.join(", "),
3952 nested_cte_from(&ins.nested_ctes, &vals.join(","), " "),
3953 )
3954 }
3955}
3956
3957fn emit_insert_stmt(ins: &IrInsert) -> SqlOutput {
3958 if ins.enqueue_vector.is_empty()
3959 && ins.enqueue_search.is_empty()
3960 && !insert_has_any_multilink(ins)
3961 && ins.nested_ctes.is_empty()
3962 {
3963 let mut sql = emit_insert_row_sql(ins);
3964 if let Some(conflict) = &ins.unless_conflict {
3965 emit_conflict(&mut sql, conflict);
3966 }
3967 let (shape, returning_sql) = emit_returning_shape(&ins.target, &ins.returning, false);
3968 if let Some(r) = returning_sql {
3969 sql.push_str(&r);
3970 }
3971 return SqlOutput {
3972 sql,
3973 shape,
3974 inference_plan: None,
3975 };
3976 }
3977
3978 let mut cte_parts = if insert_has_any_multilink(ins) {
3984 emit_insert_multilink_ctes(ins, "_w")
3985 } else {
3986 let mut cte_parts = emit_user_cte_parts(&ins.nested_ctes);
3987 let mut insert_sql = emit_insert_row_sql(ins);
3988 if let Some(conflict) = &ins.unless_conflict {
3989 emit_conflict(&mut insert_sql, conflict);
3990 }
3991 insert_sql.push_str("\nRETURNING \"id\"");
3992 cte_parts.push(format!("\"_w\" AS (\n{}\n)", insert_sql));
3993 cte_parts
3994 };
3995 cte_parts.extend(enqueue_ctes(&ins.enqueue_vector, "_w"));
3996 cte_parts.extend(enqueue_search_ctes(&ins.enqueue_search, "_w", ins.enqueue_vector.len()));
3997
3998 let (shape, select_sql) = shape_select_from_cte(&ins.target, &ins.returning, "_w");
3999 let sql = format!(
4000 "WITH\n{}\n{}",
4001 cte_parts.join(",\n"),
4002 select_sql.unwrap_or_else(|| "SELECT * FROM \"_w\"".to_string()),
4003 );
4004 SqlOutput {
4005 sql,
4006 shape,
4007 inference_plan: None,
4008 }
4009}
4010
4011fn emit_poly_update_stmt(upd: &IrUpdate, user_ctes: &[IrCteDef]) -> SqlOutput {
4014 let alias = &upd.target.alias;
4015 let sets = update_set_fragments(&upd.assignments, &upd.rewrites, "");
4016
4017 let mut cte_parts: Vec<String> = emit_user_cte_parts(user_ctes);
4023 cte_parts.extend(emit_user_cte_parts(&upd.nested_ctes));
4024 let mut union_parts = vec![];
4025 let from_ctes = nested_cte_from(&upd.nested_ctes, &sets.join(","), "\n");
4026
4027 let has_any_multilink = !upd.multi_link_clears.is_empty()
4028 || !upd.multi_link_replaces.is_empty()
4029 || !upd.multi_link_appends.is_empty()
4030 || !upd.multi_link_removals.is_empty();
4031 for (i, imp) in upd.poly_implementors.iter().enumerate() {
4032 let cte_name = format!("_u{}", i);
4033 if has_any_multilink {
4037 cte_parts.extend(emit_update_multilink_cte_parts(
4038 &concrete_poly_update(upd, imp),
4039 &cte_name,
4040 false,
4041 ));
4042 } else {
4043 let mut upd_sql = format!(
4044 "UPDATE {} AS {}\nSET {}{}",
4045 qn(&imp.module, &imp.table),
4046 qi(alias),
4047 sets.join(", "),
4048 from_ctes,
4049 );
4050 append_filter(&mut upd_sql, &upd.filter);
4051 upd_sql.push_str(&format!("\nRETURNING {}.\"id\"", qi(alias)));
4052 cte_parts.push(format!("\"{}\" AS (\n{}\n)", cte_name, upd_sql));
4053 }
4054
4055 let r_alias = format!("_r{}", i);
4056 union_parts.push(format!(
4057 "SELECT ROW({}::text, {}.\"id\") AS result FROM \"{}\" AS {}",
4058 sql_str(&imp.type_name),
4059 qi(&r_alias),
4060 cte_name,
4061 qi(&r_alias),
4062 ));
4063 }
4064
4065 let sql = format!("WITH\n{}\n{}", cte_parts.join(",\n"), union_parts.join("\nUNION ALL\n"),);
4066
4067 let (shape, _) = emit_returning_shape(&upd.target, &upd.returning, true);
4068 SqlOutput {
4069 sql,
4070 shape,
4071 inference_plan: None,
4072 }
4073}
4074
4075fn emit_update_stmt(upd: &IrUpdate, user_ctes: &[IrCteDef]) -> SqlOutput {
4076 if !upd.poly_implementors.is_empty() {
4077 return emit_poly_update_stmt(upd, user_ctes);
4078 }
4079 let alias = &upd.target.alias;
4080 let (shape, returning_sql) = emit_returning_shape(&upd.target, &upd.returning, true);
4081
4082 let has_any_multilink = !upd.multi_link_clears.is_empty()
4083 || !upd.multi_link_replaces.is_empty()
4084 || !upd.multi_link_appends.is_empty()
4085 || !upd.multi_link_removals.is_empty();
4086
4087 if !has_any_multilink && upd.enqueue_vector.is_empty() && upd.enqueue_search.is_empty() {
4088 let sets = update_set_fragments(&upd.assignments, &upd.rewrites, "");
4095 let mut sql = format!(
4096 "UPDATE {} AS {}\nSET {}",
4097 target_ref(&upd.target),
4098 qi(alias),
4099 sets.join(", "),
4100 );
4101 sql.push_str(&nested_cte_from(&upd.nested_ctes, &sets.join(","), "\n"));
4102 append_filter(&mut sql, &upd.filter);
4103 if let Some(r) = returning_sql {
4104 sql.push_str(&r);
4105 }
4106 let combined_ctes: Vec<IrCteDef> = user_ctes
4109 .iter()
4110 .cloned()
4111 .chain(upd.nested_ctes.iter().cloned())
4112 .collect();
4113 if !combined_ctes.is_empty() {
4114 sql = format!("{}{}", emit_cte_prefix(&combined_ctes), sql);
4115 }
4116 return SqlOutput {
4117 sql,
4118 shape,
4119 inference_plan: None,
4120 };
4121 }
4122
4123 if !has_any_multilink && (!upd.enqueue_vector.is_empty() || !upd.enqueue_search.is_empty()) {
4124 let sets = update_set_fragments(&upd.assignments, &upd.rewrites, "");
4126 let mut upd_sql = format!(
4127 " UPDATE {} AS {}\n SET {}",
4128 target_ref(&upd.target),
4129 qi(alias),
4130 sets.join(", "),
4131 );
4132 upd_sql.push_str(&nested_cte_from(&upd.nested_ctes, &sets.join(","), "\n "));
4133 append_filter(&mut upd_sql, &upd.filter);
4134 upd_sql.push_str("\n RETURNING \"id\"");
4135
4136 let mut cte_parts: Vec<String> = emit_user_cte_parts(user_ctes);
4137 cte_parts.extend(emit_user_cte_parts(&upd.nested_ctes));
4138 cte_parts.push(format!("\"_w\" AS (\n{}\n)", upd_sql));
4139 cte_parts.extend(enqueue_ctes(&upd.enqueue_vector, "_w"));
4140 cte_parts.extend(enqueue_search_ctes(&upd.enqueue_search, "_w", upd.enqueue_vector.len()));
4141
4142 let (shape2, select_sql) = shape_select_from_cte(&upd.target, &upd.returning, "_w");
4143 let sql = format!(
4144 "WITH\n{}\n{}",
4145 cte_parts.join(",\n"),
4146 select_sql.unwrap_or_else(|| "SELECT * FROM \"_w\"".to_string()),
4147 );
4148 return SqlOutput {
4149 sql,
4150 shape: shape2,
4151 inference_plan: None,
4152 };
4153 }
4154
4155 let result_expr = if !upd.returning.is_empty() {
4157 let (pointer_exprs, _) = build_shape(&upd.returning, alias);
4158 let mut parts = vec![type_disc(&upd.target.type_name)];
4159 parts.extend(pointer_exprs);
4160 parts.join(",\n ")
4161 } else {
4162 format!("{}.id", qi(alias))
4163 };
4164
4165 let has_scalar_changes = !upd.assignments.is_empty() || !upd.rewrites.is_empty();
4166
4167 let mut cte_parts: Vec<String> = emit_user_cte_parts(user_ctes);
4171 cte_parts.extend(emit_user_cte_parts(&upd.nested_ctes));
4172
4173 if has_scalar_changes {
4175 let sets = update_set_fragments(&upd.assignments, &upd.rewrites, "");
4176 let mut upd_sql = format!(
4177 "UPDATE {} AS {}\nSET {}",
4178 target_ref(&upd.target),
4179 qi(alias),
4180 sets.join(", "),
4181 );
4182 upd_sql.push_str(&nested_cte_from(&upd.nested_ctes, &sets.join(","), "\n"));
4183 append_filter(&mut upd_sql, &upd.filter);
4184 upd_sql.push_str("\nRETURNING *");
4185 cte_parts.push(format!("\"_ids\" AS (\n{}\n)", upd_sql));
4186 } else {
4187 let mut sel = format!(
4188 "SELECT {}.* FROM {} AS {}",
4189 qi(alias),
4190 target_ref(&upd.target),
4191 qi(alias),
4192 );
4193 append_filter(&mut sel, &upd.filter);
4194 cte_parts.push(format!("\"_ids\" AS (\n{}\n)", sel));
4195 }
4196
4197 for (i, clr) in upd.multi_link_clears.iter().enumerate() {
4199 let exclude = ml_clear_exclusion(upd.multi_link_replaces.get(i));
4200 let del = format!(
4201 "DELETE FROM {} WHERE {} IN (SELECT id FROM \"_ids\"){}",
4202 qn(&clr.module, &clr.junction_table),
4203 qi(&clr.source_col),
4204 exclude,
4205 );
4206 cte_parts.push(format!("\"_clr_{}\" AS (\n{}\n)", i, del));
4207 }
4208
4209 for (i, app) in upd.multi_link_appends.iter().enumerate() {
4211 cte_parts.push(emit_ml_append_cte(
4212 app,
4213 "_ids",
4214 &upd.target.alias,
4215 &format!("_ml_add_{}", i),
4216 ));
4217 }
4218
4219 for (i, rem) in upd.multi_link_removals.iter().enumerate() {
4221 cte_parts.push(emit_ml_remove_cte(
4222 rem,
4223 "_ids",
4224 &upd.target.alias,
4225 &format!("_ml_rm_{}", i),
4226 ));
4227 }
4228
4229 for (i, rep) in upd.multi_link_replaces.iter().enumerate() {
4231 cte_parts.push(emit_ml_append_cte(
4232 rep,
4233 "_ids",
4234 &upd.target.alias,
4235 &format!("_ml_rep_{}", i),
4236 ));
4237 }
4238
4239 cte_parts.extend(enqueue_ctes(&upd.enqueue_vector, "_ids"));
4241 cte_parts.extend(enqueue_search_ctes(
4242 &upd.enqueue_search,
4243 "_ids",
4244 upd.enqueue_vector.len(),
4245 ));
4246
4247 let sql = format!(
4248 "WITH\n{}\nSELECT (\n {}\n) AS result\nFROM \"_ids\" AS {}",
4249 cte_parts.join(",\n"),
4250 result_expr,
4251 qi(alias),
4252 );
4253 SqlOutput {
4254 sql,
4255 shape,
4256 inference_plan: None,
4257 }
4258}
4259
4260fn emit_delete_stmt(del: &IrDelete) -> SqlOutput {
4263 if !del.poly_implementors.is_empty() {
4264 return emit_poly_delete_stmt(del);
4265 }
4266 let alias = &del.target.alias;
4267
4268 if del.enqueue_search.is_empty() {
4269 let mut sql = format!("DELETE FROM {} AS {}", target_ref(&del.target), qi(alias),);
4270 append_filter(&mut sql, &del.filter);
4271 let (shape, returning_sql) = emit_returning_shape(&del.target, &del.returning, true);
4272 if let Some(r) = returning_sql {
4273 sql.push_str(&r);
4274 }
4275 return SqlOutput {
4276 sql,
4277 shape,
4278 inference_plan: None,
4279 };
4280 }
4281
4282 let mut del_sql = format!(" DELETE FROM {} AS {}", target_ref(&del.target), qi(alias),);
4284 append_filter(&mut del_sql, &del.filter);
4285 del_sql.push_str("\n RETURNING \"id\"");
4286
4287 let mut cte_parts = vec![format!("\"_del\" AS (\n{}\n)", del_sql)];
4288 cte_parts.extend(enqueue_search_ctes(&del.enqueue_search, "_del", 0));
4289
4290 let (shape, select_sql) = shape_select_from_cte(&del.target, &del.returning, "_del");
4291 let sql = format!(
4292 "WITH\n{}\n{}",
4293 cte_parts.join(",\n"),
4294 select_sql.unwrap_or_else(|| "SELECT * FROM \"_del\"".to_string()),
4295 );
4296 SqlOutput {
4297 sql,
4298 shape,
4299 inference_plan: None,
4300 }
4301}
4302
4303fn emit_poly_delete_stmt(del: &IrDelete) -> SqlOutput {
4304 let alias = &del.target.alias;
4305 let mut cte_parts = vec![];
4306 let mut union_parts = vec![];
4307
4308 for (i, imp) in del.poly_implementors.iter().enumerate() {
4309 let cte_name = format!("_d{}", i);
4310 let mut del_sql = format!("DELETE FROM {} AS {}", qn(&imp.module, &imp.table), qi(alias),);
4311 append_filter(&mut del_sql, &del.filter);
4312 del_sql.push_str(&format!("\nRETURNING {}.\"id\"", qi(alias)));
4313 cte_parts.push(format!("\"{}\" AS (\n{}\n)", cte_name, del_sql));
4314
4315 let r_alias = format!("_r{}", i);
4316 union_parts.push(format!(
4317 "SELECT ROW({}::text, {}.\"id\") AS result FROM \"{}\" AS {}",
4318 sql_str(&imp.type_name),
4319 qi(&r_alias),
4320 cte_name,
4321 qi(&r_alias),
4322 ));
4323 }
4324
4325 let sql = format!("WITH\n{}\n{}", cte_parts.join(",\n"), union_parts.join("\nUNION ALL\n"),);
4326
4327 let (shape, _) = emit_returning_shape(&del.target, &del.returning, true);
4328 SqlOutput {
4329 sql,
4330 shape,
4331 inference_plan: None,
4332 }
4333}
4334
4335fn emit_returning_shape(
4340 target: &IrSource,
4341 returning: &[IrShapePointer],
4342 with_alias: bool,
4343) -> (ShapeDescriptor, Option<String>) {
4344 if returning.is_empty() {
4345 return (
4346 ShapeDescriptor {
4347 root: ShapeNode::Scalar {
4348 name: String::new(),
4349 position: 0,
4350 },
4351 },
4352 None,
4353 );
4354 }
4355
4356 let alias = if with_alias { target.alias.as_str() } else { "" };
4357 let (pointer_exprs, shape_pointers) = build_shape(returning, alias);
4358
4359 let mut parts = vec![type_disc(&target.type_name)];
4360 parts.extend(pointer_exprs);
4361 let tuple = parts.join(",\n ");
4362 let sql = format!("\nRETURNING (\n {}\n) AS result", tuple);
4363
4364 let root_pointers = prepend_type(shape_pointers);
4365 let shape = ShapeDescriptor {
4366 root: ShapeNode::Object {
4367 name: String::new(),
4368 type_name: Some(target.type_name.clone()),
4369 position: 0,
4370 cardinality: Cardinality::Required,
4371 pointers: root_pointers,
4372 has_implicit_id: shape_has_implicit_id(returning),
4373 },
4374 };
4375 (shape, Some(sql))
4376}
4377
4378fn emit_scalar_set(f: &IrScalarSetPointer, pos: usize) -> (String, ShapeNode) {
4381 let from_sql = if !f.poly_implementors.is_empty() {
4382 format!(
4383 "(\n{}\n) AS {}",
4384 emit_poly_union(&f.poly_implementors, &f.poly_columns),
4385 qi(&f.source.alias)
4386 )
4387 } else {
4388 format!("{} AS {}", source_ref(&f.source), qi(&f.source.alias))
4389 };
4390 let sql = format!(
4391 "(SELECT COALESCE(array_agg(ROW({})::record), ARRAY[]::record[]) FROM {})",
4392 emit_expr(&f.bool_expr),
4393 from_sql,
4394 );
4395 let node = ShapeNode::Array {
4396 name: f.alias.clone(),
4397 position: pos,
4398 element: Box::new(ShapeNode::Scalar {
4399 name: String::new(),
4400 position: 0,
4401 }),
4402 };
4403 (sql, node)
4404}
4405
4406fn shape_has_implicit_id(pointers: &[IrShapePointer]) -> bool {
4411 matches!(pointers.first(), Some(IrShapePointer::Scalar(p)) if p.implicit_id)
4412}
4413
4414fn build_shape(pointers: &[IrShapePointer], table_alias: &str) -> (Vec<String>, Vec<ShapeNode>) {
4415 let mut exprs = Vec::new();
4416 let mut nodes = Vec::new();
4417
4418 for (i, pointer) in pointers.iter().enumerate() {
4419 let (sql, node) = emit_shape_pointer(pointer, table_alias, i + 1);
4420 exprs.push(sql);
4421 nodes.push(node);
4422 }
4423
4424 (exprs, nodes)
4425}
4426
4427fn fill_template(template: &str, args: &[String]) -> String {
4433 let mut out = String::with_capacity(template.len());
4434 let mut chars = template.char_indices().peekable();
4435 while let Some((start, c)) = chars.next() {
4436 if c != '$' {
4437 out.push(c);
4438 continue;
4439 }
4440 let mut end = start + 1;
4441 while let Some((i, d)) = chars.peek().copied()
4442 && d.is_ascii_digit()
4443 {
4444 end = i + d.len_utf8();
4445 chars.next();
4446 }
4447 match template[start + 1..end].parse::<usize>() {
4448 Ok(n) if (1..=args.len()).contains(&n) => out.push_str(&args[n - 1]),
4449 _ => out.push_str(&template[start..end]),
4450 }
4451 }
4452 out
4453}
4454
4455fn assert_message_arg(message: &Option<IrExpr>) -> String {
4457 message
4458 .as_ref()
4459 .map(|m| format!(", ({})::text", emit_expr(m)))
4460 .unwrap_or_default()
4461}
4462
4463fn emit_shape_pointer(pointer: &IrShapePointer, table_alias: &str, pos: usize) -> (String, ShapeNode) {
4464 match pointer {
4465 IrShapePointer::Scalar(f) => emit_scalar(f, table_alias, pos),
4466 IrShapePointer::SingleLink(f) => emit_single_link(f, table_alias, pos),
4467 IrShapePointer::MultiLink(f) => emit_multi_link(f, table_alias, pos),
4468 IrShapePointer::Computed(f) => (emit_expr(&f.expr), expr_shape_node(&f.alias, pos, &f.expr)),
4469 IrShapePointer::ScalarSet(f) => emit_scalar_set(f, pos),
4470 IrShapePointer::Asserted(a) => {
4471 let (sql, node) = emit_shape_pointer(&a.inner, table_alias, pos);
4472 let checked_set = match &a.check {
4483 Some(check) if emits_one_object(check) => {
4484 format!("ARRAY{}", emit_shape_pointer(check, table_alias, pos).0)
4485 }
4486 Some(check) => emit_shape_pointer(check, table_alias, pos).0,
4487 None if emits_one_object(&a.inner) => format!("ARRAY{sql}"),
4488 None => "\"_a\".\"v\"".to_string(),
4489 };
4490 let checked = format!(
4491 "(SELECT \"_a\".\"v\" FROM (SELECT {sql} AS \"v\") AS \"_a\"\n WHERE cardinality(\"_pylon\".{}({checked_set}::text[]{})) >= 0)",
4492 qi(&a.fn_name),
4493 assert_message_arg(&a.message),
4494 );
4495 (checked, node)
4496 }
4497 }
4498}
4499
4500fn emits_one_object(pointer: &IrShapePointer) -> bool {
4503 match pointer {
4504 IrShapePointer::SingleLink(_) => true,
4505 IrShapePointer::Computed(c) => matches!(
4506 c.expr,
4507 IrExpr::ObjectPathSubquery(_) | IrExpr::ObjectPathUnion { multi: false, .. }
4508 ),
4509 IrShapePointer::Asserted(a) => emits_one_object(&a.inner),
4510 _ => false,
4511 }
4512}
4513
4514fn pg_quoted_to_pylon(pg_type: &str) -> String {
4517 let inner = pg_type.trim_start_matches('"');
4518 if let Some(idx) = inner.find(r#""."#) {
4519 let module = &inner[..idx];
4520 let type_name = inner[idx + 3..].trim_end_matches('"');
4521 format!("{}::{}", module, type_name)
4522 } else {
4523 pg_type.to_string()
4524 }
4525}
4526
4527struct QualifiedPgType {
4538 name: String,
4540 is_array: bool,
4541}
4542
4543impl QualifiedPgType {
4544 fn of(pg_type: &str) -> Option<Self> {
4545 let (element, is_array) = match pg_type.strip_suffix("[]") {
4546 Some(element) => (element, true),
4547 None => (pg_type, false),
4548 };
4549 element.starts_with('"').then(|| Self {
4550 name: pg_quoted_to_pylon(element),
4551 is_array,
4552 })
4553 }
4554
4555 fn text_cast(&self) -> &'static str {
4557 if self.is_array { "::text[]" } else { "::text" }
4558 }
4559
4560 fn shape_node(&self, name: String, position: usize) -> ShapeNode {
4562 let element = ShapeNode::Enum {
4563 name: name.clone(),
4564 position,
4565 enum_type: self.name.clone(),
4566 };
4567 if !self.is_array {
4568 return element;
4569 }
4570 ShapeNode::Array {
4571 name,
4572 element: Box::new(ShapeNode::Enum {
4575 name: String::new(),
4576 position: 0,
4577 enum_type: self.name.clone(),
4578 }),
4579 position,
4580 }
4581 }
4582}
4583
4584fn emit_scalar(f: &IrScalarPointer, table_alias: &str, pos: usize) -> (String, ShapeNode) {
4585 if let Some(nt_name) = f.pg_type.strip_prefix("__nt__:") {
4586 let sql = if table_alias.is_empty() {
4587 format!("{}::jsonb", qi(&f.column))
4588 } else {
4589 format!("{}.{}::jsonb", qi(table_alias), qi(&f.column))
4590 };
4591 return (
4592 sql,
4593 ShapeNode::NamedTuple {
4594 name: f.alias.clone(),
4595 position: pos,
4596 type_name: Some(nt_name.to_string()),
4597 members: f.tuple_shape.as_ref().map(|s| s.members.clone()),
4598 is_free_object: false,
4599 },
4600 );
4601 }
4602 if let Some(qualified) = QualifiedPgType::of(&f.pg_type) {
4605 let cast = qualified.text_cast();
4606 let sql = if table_alias.is_empty() {
4607 format!("{}{}", qi(&f.column), cast)
4608 } else {
4609 format!("{}.{}{}", qi(table_alias), qi(&f.column), cast)
4610 };
4611 return (sql, qualified.shape_node(f.alias.clone(), pos));
4612 }
4613 if let Some(shape) = &f.tuple_shape {
4617 let sql = if table_alias.is_empty() {
4618 format!("{}::jsonb", qi(&f.column))
4619 } else {
4620 format!("{}.{}::jsonb", qi(table_alias), qi(&f.column))
4621 };
4622 return (
4623 sql,
4624 ShapeNode::NamedTuple {
4625 name: f.alias.clone(),
4626 position: pos,
4627 type_name: shape.type_name.clone(),
4628 members: Some(shape.members.clone()),
4629 is_free_object: false,
4630 },
4631 );
4632 }
4633 let sql = if table_alias.is_empty() {
4634 format!("{}::{}", qi(&f.column), f.pg_type)
4635 } else {
4636 format!("{}.{}::{}", qi(table_alias), qi(&f.column), f.pg_type)
4637 };
4638 (
4639 sql,
4640 ShapeNode::Scalar {
4641 name: f.alias.clone(),
4642 position: pos,
4643 },
4644 )
4645}
4646
4647fn emit_single_link(f: &IrSingleLinkPointer, parent_alias: &str, pos: usize) -> (String, ShapeNode) {
4648 let sub = &f.subquery;
4649 let [IrRowSource::Bound { source, shape }] = sub.rows.as_slice() else {
4650 unreachable!("single-link subquery is always schema-bound")
4651 };
4652 let sub_alias = &source.alias;
4653
4654 let (sub_exprs, mut sub_nodes) = build_shape(shape, sub_alias);
4655 let mut parts = vec![source_type_disc(source)];
4656 parts.extend(sub_exprs);
4657
4658 for lp in &f.link_properties {
4663 parts.push(format!("\"jt\".{}", qi(&lp.name)));
4664 let pos = sub_nodes.len() + 1;
4665 sub_nodes.push(ShapeNode::Scalar {
4666 name: format!("@{}", lp.name),
4667 position: pos,
4668 });
4669 }
4670
4671 let tuple = parts.join(",\n ");
4672
4673 let (from_sql, mut where_parts) = match &f.correlation {
4674 IrSingleLinkCorrelation::Fk { fk_column, target_pk } => {
4675 let from = format!("FROM {} AS {}", source_ref(source), qi(sub_alias));
4676 let cond = format!(
4677 "{}.{} = {}.{}",
4678 qi(parent_alias),
4679 qi(fk_column),
4680 qi(sub_alias),
4681 qi(target_pk),
4682 );
4683 (from, vec![cond])
4684 }
4685 IrSingleLinkCorrelation::Junction { join, target_pk: _ } => match join {
4689 IrMultiLinkJoin::Standard { junction_table, module } => {
4690 let from = format!(
4691 "FROM {} AS \"jt\"\n INNER JOIN {} AS {}\n ON {}.id = \"jt\".target",
4692 junction_ref(module, junction_table),
4693 source_ref(source),
4694 qi(sub_alias),
4695 qi(sub_alias),
4696 );
4697 let cond = format!("\"jt\".source = {}.id", qi(parent_alias));
4698 (from, vec![cond])
4699 }
4700 IrMultiLinkJoin::Through {
4701 junction_table,
4702 module,
4703 source_col,
4704 target_col,
4705 } => {
4706 let from = format!(
4707 "FROM {} AS \"jt\"\n INNER JOIN {} AS {}\n ON {}.id = \"jt\".{}",
4708 junction_ref(module, junction_table),
4709 source_ref(source),
4710 qi(sub_alias),
4711 qi(sub_alias),
4712 qi(target_col),
4713 );
4714 let cond = format!("\"jt\".{} = {}.id", qi(source_col), qi(parent_alias));
4715 (from, vec![cond])
4716 }
4717 IrMultiLinkJoin::BacklinkFk { .. } | IrMultiLinkJoin::BacklinkJunction { .. } => {
4718 unreachable!("a junction-backed single link's own forward join is always Standard or Through")
4719 }
4720 },
4721 };
4722 if let Some(filter) = &sub.filter {
4723 where_parts.push(emit_expr(filter));
4724 }
4725
4726 let mut sql = format!(
4727 "(SELECT (\n {}\n )\n {}\n WHERE {}",
4728 tuple,
4729 from_sql,
4730 where_parts.join(" AND "),
4731 );
4732 if !sub.order_by.is_empty() {
4733 let s: Vec<_> = sub.order_by.iter().map(emit_sort_clause).collect();
4734 sql.push_str(&format!("\n ORDER BY {}", s.join(", ")));
4735 }
4736 sql.push(')');
4737
4738 let node = ShapeNode::Object {
4739 name: f.alias.clone(),
4740 type_name: Some(source.type_name.clone()),
4741 position: pos,
4742 cardinality: Cardinality::Optional,
4743 pointers: prepend_type(sub_nodes),
4744 has_implicit_id: shape_has_implicit_id(shape),
4745 };
4746 (sql, node)
4747}
4748
4749fn emit_multi_link(f: &IrMultiLinkPointer, parent_alias: &str, pos: usize) -> (String, ShapeNode) {
4750 let sub = &f.subquery;
4751 let [IrRowSource::Bound { source, shape }] = sub.rows.as_slice() else {
4752 unreachable!("multi-link subquery is always schema-bound")
4753 };
4754 let sub_alias = &source.alias;
4755
4756 let (sub_exprs, mut sub_nodes) = build_shape(shape, sub_alias);
4757 let mut row_parts = vec![source_type_disc(source)];
4758 row_parts.extend(sub_exprs);
4759
4760 for lp in &f.link_properties {
4764 row_parts.push(format!("\"jt\".{}", qi(&lp.name)));
4765 let pos = sub_nodes.len() + 1;
4768 sub_nodes.push(ShapeNode::Scalar {
4769 name: format!("@{}", lp.name),
4770 position: pos,
4771 });
4772 }
4773
4774 let row = row_parts.join(",\n ");
4775
4776 let order_sql = if !sub.order_by.is_empty() {
4778 let s: Vec<_> = sub.order_by.iter().map(emit_sort_clause).collect();
4779 format!(" ORDER BY {}", s.join(", "))
4780 } else {
4781 String::new()
4782 };
4783
4784 let (from_sql, source_cond) = match &f.join {
4785 IrMultiLinkJoin::Standard { junction_table, module } => {
4786 let from = format!(
4787 "FROM {} AS \"jt\"\n INNER JOIN {} AS {}\n ON {}.id = \"jt\".target",
4788 junction_ref(module, junction_table),
4789 source_ref(source),
4790 qi(sub_alias),
4791 qi(sub_alias),
4792 );
4793 let cond = format!("\"jt\".source = {}.id", qi(parent_alias));
4794 (from, cond)
4795 }
4796 IrMultiLinkJoin::Through {
4797 junction_table,
4798 module,
4799 source_col,
4800 target_col,
4801 } => {
4802 let from = format!(
4803 "FROM {} AS \"jt\"\n INNER JOIN {} AS {}\n ON {}.id = \"jt\".{}",
4804 junction_ref(module, junction_table),
4805 source_ref(source),
4806 qi(sub_alias),
4807 qi(sub_alias),
4808 qi(target_col),
4809 );
4810 let cond = format!("\"jt\".{} = {}.id", qi(source_col), qi(parent_alias));
4811 (from, cond)
4812 }
4813 IrMultiLinkJoin::BacklinkFk { fk_col } => {
4816 let from = format!("FROM {} AS {}", source_ref(source), qi(sub_alias));
4817 let cond = format!("{}.{} = {}.id", qi(sub_alias), qi(fk_col), qi(parent_alias));
4818 (from, cond)
4819 }
4820 IrMultiLinkJoin::BacklinkJunction {
4825 junction_table,
4826 module,
4827 owner_col,
4828 current_col,
4829 } => {
4830 let from = format!(
4831 "FROM {} AS \"jt\"\n INNER JOIN {} AS {}\n ON {}.id = \"jt\".{}",
4832 junction_ref(module, junction_table),
4833 source_ref(source),
4834 qi(sub_alias),
4835 qi(sub_alias),
4836 qi(owner_col),
4837 );
4838 let cond = format!("\"jt\".{} = {}.id", qi(current_col), qi(parent_alias));
4839 (from, cond)
4840 }
4841 };
4842
4843 let mut where_parts = vec![source_cond];
4844 if let Some(filter) = &sub.filter {
4845 where_parts.push(emit_expr(filter));
4846 }
4847
4848 if f.single {
4849 let mut sql = format!(
4850 "(SELECT (\n {}\n )\n {}\n WHERE {}",
4851 row,
4852 from_sql,
4853 where_parts.join(" AND "),
4854 );
4855 append_order_by(&mut sql, &sub.order_by);
4856 append_offset_limit(&mut sql, &sub.offset, &sub.limit);
4857 sql.push(')');
4858 let node = ShapeNode::Object {
4859 name: f.alias.clone(),
4860 type_name: Some(source.type_name.clone()),
4861 position: pos,
4862 cardinality: Cardinality::Optional,
4863 pointers: prepend_type(sub_nodes),
4864 has_implicit_id: shape_has_implicit_id(shape),
4865 };
4866 return (sql, node);
4867 }
4868
4869 if f.single {
4870 let mut sql = format!(
4871 "(SELECT (\n {}\n )\n {}\n WHERE {}",
4872 row,
4873 from_sql,
4874 where_parts.join(" AND "),
4875 );
4876 append_order_by(&mut sql, &sub.order_by);
4877 append_offset_limit(&mut sql, &sub.offset, &sub.limit);
4878 sql.push(')');
4879 let node = ShapeNode::Object {
4880 name: f.alias.clone(),
4881 type_name: Some(source.type_name.clone()),
4882 position: pos,
4883 cardinality: Cardinality::Optional,
4884 pointers: prepend_type(sub_nodes),
4885 has_implicit_id: shape_has_implicit_id(shape),
4886 };
4887 return (sql, node);
4888 }
4889
4890 let sql = if sub.limit.is_some() || sub.offset.is_some() {
4891 let mut cols: Vec<String> = row_parts
4896 .iter()
4897 .enumerate()
4898 .map(|(i, part)| format!("{} AS \"c{}\"", part, i))
4899 .collect();
4900 let agg_row: Vec<String> = (0..row_parts.len()).map(|i| format!("\"__lim\".\"c{}\"", i)).collect();
4901 let mut agg_order: Vec<String> = vec![];
4902 for (i, s) in sub.order_by.iter().enumerate() {
4903 cols.push(format!("{} AS \"s{}\"", emit_expr(&s.expr), i));
4904 agg_order.push(emit_sort_clause(&IrSort {
4905 expr: IrExpr::ColumnRef {
4906 alias: "__lim".to_string(),
4907 column: format!("s{}", i),
4908 pg_type: String::new(),
4909 },
4910 direction: s.direction.clone(),
4911 nulls: s.nulls.clone(),
4912 }));
4913 }
4914 let mut inner = format!(
4915 "SELECT {}\n {}\n WHERE {}",
4916 cols.join(",\n "),
4917 from_sql,
4918 where_parts.join(" AND "),
4919 );
4920 append_order_by(&mut inner, &sub.order_by);
4921 append_offset_limit(&mut inner, &sub.offset, &sub.limit);
4922 let agg_order_sql = if agg_order.is_empty() {
4923 String::new()
4924 } else {
4925 format!(" ORDER BY {}", agg_order.join(", "))
4926 };
4927 format!(
4928 "(SELECT COALESCE(\n array_agg(ROW(\n {}\n )::record{}),\n ARRAY[]::record[]\n )\n FROM ({}) AS \"__lim\")",
4929 agg_row.join(",\n "),
4930 agg_order_sql,
4931 inner,
4932 )
4933 } else {
4934 format!(
4935 "(SELECT COALESCE(\n array_agg(ROW(\n {}\n )::record{}),\n ARRAY[]::record[]\n )\n {}\n WHERE {})",
4936 row,
4937 order_sql,
4938 from_sql,
4939 where_parts.join(" AND "),
4940 )
4941 };
4942
4943 let node = ShapeNode::Array {
4944 name: f.alias.clone(),
4945 position: pos,
4946 element: Box::new(ShapeNode::Object {
4947 name: String::new(),
4948 type_name: Some(source.type_name.clone()),
4949 position: 0,
4950 cardinality: Cardinality::Required,
4951 pointers: prepend_type(sub_nodes),
4952 has_implicit_id: shape_has_implicit_id(shape),
4953 }),
4954 };
4955 (sql, node)
4956}
4957
4958fn prepend_type(nodes: Vec<ShapeNode>) -> Vec<ShapeNode> {
4961 let mut out = vec![ShapeNode::Scalar {
4962 name: "__type__".into(),
4963 position: 0,
4964 }];
4965 out.extend(nodes);
4966 out
4967}
4968
4969fn append_filter(sql: &mut String, filter: &Option<IrExpr>) {
4972 if let Some(f) = filter {
4973 sql.push_str(&format!("\nWHERE {}", emit_expr(f)));
4974 }
4975}
4976
4977fn append_order_by(sql: &mut String, order_by: &[IrSort]) {
4978 if !order_by.is_empty() {
4979 let s: Vec<_> = order_by.iter().map(emit_sort_clause).collect();
4980 sql.push_str(&format!("\nORDER BY {}", s.join(", ")));
4981 }
4982}
4983
4984fn append_offset_limit(sql: &mut String, offset: &Option<IrExpr>, limit: &Option<IrExpr>) {
4985 if let Some(o) = offset {
4986 sql.push_str(&format!("\nOFFSET {}", emit_expr(o)));
4987 }
4988 if let Some(l) = limit {
4989 sql.push_str(&format!("\nLIMIT {}", emit_expr(l)));
4990 }
4991}
4992
4993fn append_lock_clause(sql: &mut String, lock: &Option<IrLockClause>) {
5000 let Some(lock) = lock else { return };
5001 let strength = match lock.strength {
5002 IrLockStrength::Update => "UPDATE",
5003 IrLockStrength::NoKeyUpdate => "NO KEY UPDATE",
5004 IrLockStrength::Share => "SHARE",
5005 IrLockStrength::KeyShare => "KEY SHARE",
5006 };
5007 sql.push_str(&format!("\nFOR {}", strength));
5008 match lock.wait {
5009 IrLockWait::Block => {}
5010 IrLockWait::NoWait => sql.push_str(" NOWAIT"),
5011 IrLockWait::SkipLocked => sql.push_str(" SKIP LOCKED"),
5012 }
5013}
5014
5015fn emit_sort_clause(s: &IrSort) -> String {
5016 let dir = match s.direction {
5017 IrSortDir::Asc => "ASC",
5018 IrSortDir::Desc => "DESC",
5019 };
5020 let nulls = match s.nulls {
5021 IrNulls::First => "NULLS FIRST",
5022 IrNulls::Last => "NULLS LAST",
5023 };
5024 format!("{} {} {}", emit_expr(&s.expr), dir, nulls)
5025}
5026
5027fn set_operand(expr: &IrExpr) -> String {
5032 match expr {
5033 IrExpr::ArrayFromSelect(_) => format!("SELECT unnest({})", emit_expr(expr)),
5034 IrExpr::CteRef { name, scalar, .. } => {
5035 format!("SELECT {} FROM {}", if *scalar { "\"v\"" } else { "\"id\"" }, qi(name))
5036 }
5037 _ => format!("SELECT {}", emit_expr(expr)),
5038 }
5039}
5040
5041pub fn emit_expr_with_fanouts(expr: &IrExpr, fanouts: &HashMap<(String, String), IrPolyFanout>) -> String {
5044 with_subtype_fanouts(fanouts, || emit_expr(expr))
5045}
5046
5047pub fn emit_expr(expr: &IrExpr) -> String {
5048 match expr {
5049 IrExpr::ColumnRef { alias, column, .. } => {
5050 if alias.is_empty() {
5051 qi(column)
5052 } else if alias == "NEW" || alias == "OLD" {
5053 format!("{alias}.{}", qi(column))
5064 } else {
5065 format!("{}.{}", qi(alias), qi(column))
5066 }
5067 }
5068 IrExpr::Param { index } => format!("${}", index + 1),
5069 IrExpr::Literal(lit) => emit_literal(lit),
5070 IrExpr::BinOp(op) => {
5071 let l = emit_expr(&op.left);
5072 let r = emit_expr(&op.right);
5073 match op.op {
5074 BinOpKind::Add => format!("({} + {})", l, r),
5075 BinOpKind::Sub => format!("({} - {})", l, r),
5076 BinOpKind::Mul => format!("({} * {})", l, r),
5077 BinOpKind::Div => format!("({} / {})", l, r),
5078 BinOpKind::FloorDiv => {
5079 if is_integer_expr(&op.left) && is_integer_expr(&op.right) {
5080 format!("({} / {})", l, r)
5081 } else {
5082 format!("floor(({}) / ({}))", l, r)
5083 }
5084 }
5085 BinOpKind::Mod => format!("({} % {})", l, r),
5086 BinOpKind::Pow => format!("power({}, {})", l, r),
5087 BinOpKind::Eq => format!("({} = {})", l, r),
5088 BinOpKind::Ne => format!("({} <> {})", l, r),
5089 BinOpKind::Lt => format!("({} < {})", l, r),
5090 BinOpKind::Le => format!("({} <= {})", l, r),
5091 BinOpKind::Gt => format!("({} > {})", l, r),
5092 BinOpKind::Ge => format!("({} >= {})", l, r),
5093 BinOpKind::And => format!("({} AND {})", l, r),
5094 BinOpKind::Or => format!("({} OR {})", l, r),
5095 BinOpKind::Like => format!("({} LIKE {})", l, r),
5096 BinOpKind::Ilike => format!("({} ILIKE {})", l, r),
5097 BinOpKind::NotLike => format!("({} NOT LIKE {})", l, r),
5098 BinOpKind::NotIlike => format!("({} NOT ILIKE {})", l, r),
5099 BinOpKind::In => format!("({} = ANY({}))", l, unwrap_unnest_for_any(&op.right, &r)),
5105 BinOpKind::NotIn => format!("({} <> ALL({}))", l, unwrap_unnest_for_any(&op.right, &r)),
5106 BinOpKind::Coalesce => format!("COALESCE({}, {})", l, r),
5107 BinOpKind::CoalesceEq => format!("({} IS NOT DISTINCT FROM {})", l, r),
5108 BinOpKind::CoalesceNe => format!("({} IS DISTINCT FROM {})", l, r),
5109 BinOpKind::Concat => format!("({} || {})", l, r),
5110 }
5111 }
5112 IrExpr::UnaryOp(op) => {
5113 let inner = emit_expr(&op.operand);
5114 match op.op {
5115 UnaryOpKind::Not => format!("(NOT {})", inner),
5116 UnaryOpKind::Minus => format!("(-{})", inner),
5117 UnaryOpKind::Exists => format!("EXISTS({})", inner),
5118 UnaryOpKind::Distinct => inner,
5123 }
5124 }
5125 IrExpr::FunctionCall(f) => {
5126 let args: Vec<_> = f.args.iter().map(emit_expr).collect();
5127 if let Some(tmpl) = &f.sql_template {
5128 return fill_template(tmpl, &args);
5129 }
5130 let name = match &f.schema {
5131 Some(s) => format!("{}.{}", pg_schema(s), qi(&f.name)),
5132 None => f.name.clone(),
5133 };
5134 format!("{}({})", name, args.join(", "))
5135 }
5136 IrExpr::TypeCast(c) => {
5137 if c.pg_type == "jsonb" {
5140 match &c.expr {
5141 IrExpr::Param { .. } => format!("({})::jsonb", emit_expr(&c.expr)),
5152 IrExpr::Literal(IrLiteral::Str(_)) => {
5153 format!("to_jsonb({}::text)", emit_expr(&c.expr))
5154 }
5155 _ => format!("to_jsonb({})", emit_expr(&c.expr)),
5156 }
5157 } else if yields_jsonb(&c.expr) {
5158 format!("(({}) #>> '{{}}')::{}", emit_expr(&c.expr), c.pg_type)
5162 } else {
5163 format!("({})::{}", emit_expr(&c.expr), c.pg_type)
5164 }
5165 }
5166 IrExpr::IfElse(ie) => format!(
5167 "CASE WHEN {} THEN {} ELSE {} END",
5168 emit_expr(&ie.condition),
5169 emit_expr(&ie.if_),
5170 emit_expr(&ie.else_),
5171 ),
5172 IrExpr::Array(elems) => {
5173 if elems.is_empty() {
5174 "ARRAY[]::text[]".to_string()
5175 } else {
5176 let parts: Vec<String> = elems.iter().map(emit_expr).collect();
5177 format!("ARRAY[{}]", parts.join(", "))
5178 }
5179 }
5180 IrExpr::Null => "NULL".to_string(),
5181 IrExpr::AggOverSet {
5182 fn_name,
5183 schema: _,
5184 elems,
5185 } => {
5186 let union_all = elems
5187 .iter()
5188 .map(|e| format!("SELECT {}", emit_expr(e)))
5189 .collect::<Vec<_>>()
5190 .join(" UNION ALL ");
5191 format!("(SELECT {}(v) FROM ({}) AS _set(v))", fn_name, union_all)
5192 }
5193 IrExpr::AggOverCte { fn_name, cte, column } => format!(
5194 "(SELECT {}({}) FROM {})",
5195 fn_name,
5196 column.as_deref().map(qi).unwrap_or_else(|| "*".to_string()),
5197 qi(cte),
5198 ),
5199 IrExpr::ExistsOverCte { cte, column } => match column {
5200 Some(column) => format!("EXISTS(SELECT 1 FROM {} WHERE {} IS NOT NULL)", qi(cte), qi(column)),
5201 None => format!("EXISTS(SELECT 1 FROM {})", qi(cte)),
5202 },
5203 IrExpr::AggOverQuery { fn_name, inner } => {
5204 let inner_sql = emit_select_stmt(inner, &[]).sql;
5205 format!("(SELECT {}(*) FROM ({}) _agg)", fn_name, inner_sql)
5206 }
5207 IrExpr::ArrayFromSelect(src) => emit_array_source(src),
5208 IrExpr::ScalarSubquery(sel) => {
5209 format!(
5210 "(SELECT \"v\" FROM (\n{}\n) AS \"_scalar_sub\")",
5211 emit_select_stmt(sel, &[]).sql
5212 )
5213 }
5214
5215 IrExpr::SetOp { op, left, right, mode } => {
5216 let set = format!("({}) {} ({})", set_operand(left), op.sql(), set_operand(right));
5217 match mode {
5218 crate::ir::SetOpMode::Exists => format!("EXISTS({set})"),
5219 crate::ir::SetOpMode::Array => format!("ARRAY({set})"),
5220 crate::ir::SetOpMode::Aggregate(f) => {
5221 format!("(SELECT {f}(\"_s\".\"v\") FROM ({set}) AS \"_s\"(\"v\"))")
5222 }
5223 }
5224 }
5225
5226 IrExpr::CteRef { name, scalar, .. } => {
5227 let col = if *scalar { "v" } else { "id" };
5230 format!("(SELECT \"{}\" FROM \"{}\")", col, name)
5231 }
5232
5233 IrExpr::CteFieldRef { name, field, .. } => {
5234 format!("(SELECT {} FROM {})", qi(field), qi(name))
5235 }
5236
5237 IrExpr::ForVar { name, .. } => format!("\"_for_{}\".\"v\"", name),
5238
5239 IrExpr::EnumLiteral { pg_type, variant } => {
5240 format!("'{}'::{}", variant.replace('\'', "''"), pg_type)
5241 }
5242
5243 IrExpr::GlobalParam { index, pg_type } => {
5244 format!("(${}::{})", index + 1, pg_type)
5245 }
5246
5247 IrExpr::GlobalRef { cte_name } => {
5248 format!("(SELECT \"value\" FROM \"{}\")", cte_name)
5249 }
5250
5251 IrExpr::NamedTuple { fields, .. } => {
5252 let pairs: Vec<String> = fields
5253 .iter()
5254 .flat_map(|(k, v)| [format!("'{}'", k.replace('\'', "''")), emit_expr(v)])
5255 .collect();
5256 format!("jsonb_build_object({})", pairs.join(", "))
5257 }
5258
5259 IrExpr::Tuple(elems) => {
5260 let items: Vec<String> = elems.iter().map(emit_expr).collect();
5261 format!("jsonb_build_array({})", items.join(", "))
5262 }
5263
5264 IrExpr::Subscript { expr, index, is_array } => {
5265 let e = emit_expr(expr);
5266 let i = emit_expr(index);
5267 if *is_array {
5268 format!("_pylon.array_subscript({}, ({})::bigint)", e, i)
5269 } else {
5270 format!("_pylon.str_subscript({}, ({})::bigint)", e, i)
5271 }
5272 }
5273
5274 IrExpr::Slice {
5275 expr,
5276 lower,
5277 upper,
5278 is_array,
5279 } => {
5280 let e = emit_expr(expr);
5281 if *is_array {
5282 let lo = lower
5283 .as_deref()
5284 .map(|x| format!("({}) + 1", emit_expr(x)))
5285 .unwrap_or_else(|| "1".to_string());
5286 let hi = upper.as_deref().map(emit_expr).unwrap_or_default();
5287 if hi.is_empty() {
5288 format!("({})[{}:]", e, lo)
5289 } else {
5290 format!("({})[{}:{}]", e, lo, hi)
5291 }
5292 } else {
5293 let start = lower
5295 .as_deref()
5296 .map(|x| format!("({}) + 1", emit_expr(x)))
5297 .unwrap_or_else(|| "1".to_string());
5298 match upper.as_deref() {
5299 Some(hi_expr) => {
5300 let lo_val = lower.as_deref().map(emit_expr).unwrap_or_else(|| "0".to_string());
5301 format!(
5303 "substr({}, {}, GREATEST(0, ({}) - ({})))",
5304 e,
5305 start,
5306 emit_expr(hi_expr),
5307 lo_val
5308 )
5309 }
5310 None => format!("substr({}, {})", e, start),
5311 }
5312 }
5313 }
5314
5315 IrExpr::JsonbField { expr, field } => {
5316 format!("({}->{})", emit_expr(expr), sql_str(field))
5317 }
5318
5319 IrExpr::JsonbIndex { expr, index } => {
5320 format!("({}->{})", emit_expr(expr), index)
5321 }
5322
5323 IrExpr::FnParam { name, .. } => qi(name),
5324
5325 IrExpr::FnSubquery(fs) => {
5326 let scalar = match fs.shape.as_slice() {
5327 [IrShapePointer::Computed(c)] => emit_expr(&c.expr),
5328 _ => unreachable!("a function scalar subquery always projects exactly one computed pointer"),
5329 };
5330 let args_sql = fs.fn_args.iter().map(emit_expr).collect::<Vec<_>>().join(", ");
5331 let mut sql = format!(
5332 "(SELECT {}{}\nFROM {}.{}({}) AS {}",
5333 if fs.distinct { "DISTINCT " } else { "" },
5334 scalar,
5335 pg_schema(&fs.fn_module),
5336 qi(&fs.fn_name),
5337 args_sql,
5338 qi(&fs.alias),
5339 );
5340 append_filter(&mut sql, &fs.filter);
5341 append_order_by(&mut sql, &fs.order_by);
5342 append_offset_limit(&mut sql, &fs.offset, &fs.limit);
5343 sql.push(')');
5344 sql
5345 }
5346
5347 IrExpr::PathSubquery(ps) => {
5348 let scalar = match &ps.result {
5349 IrPathResult::Scalar(e, _) => emit_expr(e),
5350 IrPathResult::Object { alias, .. } => format!("{}.\"id\"", qi(alias)),
5351 };
5352 let from_sql = emit_path_joins(&ps.root, &ps.joins);
5353 let mut sql = format!(
5354 "(SELECT {}{}\nFROM {}",
5355 if ps.distinct { "DISTINCT " } else { "" },
5356 scalar,
5357 from_sql
5358 );
5359 append_filter(&mut sql, &ps.filter);
5360 append_order_by(&mut sql, &ps.order_by);
5361 append_offset_limit(&mut sql, &ps.offset, &ps.limit);
5362 sql.push(')');
5363 sql
5364 }
5365
5366 IrExpr::ObjectPathUnion { branches, limit, multi } => {
5367 let arms: Vec<String> = branches
5368 .iter()
5369 .map(|ps| {
5370 let IrPathResult::Object {
5371 alias,
5372 type_name,
5373 shape,
5374 } = &ps.result
5375 else {
5376 unreachable!("an object path union's branches always land on objects")
5377 };
5378 let (pointer_exprs, _) = build_shape(shape, alias);
5379 let mut parts = vec![result_type_disc(ps, alias, type_name)];
5380 parts.extend(pointer_exprs);
5381 let mut sql = format!(
5382 "SELECT (\n {}\n) AS \"r\"\nFROM {}",
5383 parts.join(",\n "),
5384 emit_path_joins(&ps.root, &ps.joins),
5385 );
5386 append_filter(&mut sql, &ps.filter);
5387 sql
5388 })
5389 .collect();
5390 let mut sql = format!(
5391 "{}(SELECT \"r\" FROM (\n{}\n) AS \"_u\"",
5392 if *multi { "ARRAY" } else { "" },
5393 arms.join("\nUNION ALL\n")
5394 );
5395 if let Some(limit) = limit {
5396 sql.push_str(&format!("\nLIMIT {}", emit_expr(limit)));
5397 }
5398 sql.push(')');
5399 sql
5400 }
5401
5402 IrExpr::ObjectPathSubquery(ps) => {
5403 let IrPathResult::Object {
5404 alias,
5405 type_name,
5406 shape,
5407 } = &ps.result
5408 else {
5409 unreachable!("an object path subquery always lands on an object")
5410 };
5411 let (pointer_exprs, _) = build_shape(shape, alias);
5412 let mut parts = vec![result_type_disc(ps, alias, type_name)];
5413 parts.extend(pointer_exprs);
5414 let mut sql = format!(
5415 "(SELECT (\n {}\n)\nFROM {}",
5416 parts.join(",\n "),
5417 emit_path_joins(&ps.root, &ps.joins),
5418 );
5419 append_filter(&mut sql, &ps.filter);
5420 append_order_by(&mut sql, &ps.order_by);
5421 append_offset_limit(&mut sql, &ps.offset, &ps.limit);
5422 sql.push(')');
5423 sql
5424 }
5425
5426 IrExpr::ObjectSubquery(sel) => {
5427 let [IrRowSource::Bound { source, shape }] = sel.rows.as_slice() else {
5428 unreachable!("an object subquery is always schema-bound")
5429 };
5430 let alias = &source.alias;
5431 let (sub_exprs, _) = build_shape(shape, alias);
5432 let mut row_parts = vec![source_type_disc(source)];
5433 row_parts.extend(sub_exprs);
5434 let mut sql = format!(
5435 "(SELECT (\n {}\n )\n FROM {} AS {}",
5436 row_parts.join(",\n "),
5437 source_ref(source),
5438 qi(alias),
5439 );
5440 append_filter(&mut sql, &sel.filter);
5441 append_order_by(&mut sql, &sel.order_by);
5442 append_offset_limit(&mut sql, &sel.offset, &sel.limit);
5443 sql.push(')');
5444 sql
5445 }
5446
5447 IrExpr::Subquery(sel) => {
5448 let [IrRowSource::Bound { source, shape }] = sel.rows.as_slice() else {
5449 unreachable!("scalar/exists subquery is always schema-bound")
5450 };
5451 let alias = &source.alias;
5452 let mut sql = if shape.is_empty() {
5453 format!("(SELECT 1\nFROM {} AS {}", source_ref(source), qi(alias))
5455 } else if let Some(c) = shape.iter().find_map(|f| {
5456 if let IrShapePointer::Computed(c) = f {
5457 Some(c)
5458 } else {
5459 None
5460 }
5461 }) {
5462 format!(
5467 "(SELECT {}\nFROM {} AS {}",
5468 emit_expr(&c.expr),
5469 source_ref(source),
5470 qi(alias)
5471 )
5472 } else {
5473 let pk_col = shape
5475 .iter()
5476 .find_map(|f| {
5477 if let IrShapePointer::Scalar(s) = f {
5478 Some(s.column.as_str())
5479 } else {
5480 None
5481 }
5482 })
5483 .unwrap_or("id");
5484 format!(
5485 "(SELECT {}.{}\nFROM {} AS {}",
5486 qi(alias),
5487 qi(pk_col),
5488 source_ref(source),
5489 qi(alias),
5490 )
5491 };
5492 append_filter(&mut sql, &sel.filter);
5493 append_order_by(&mut sql, &sel.order_by);
5494 append_offset_limit(&mut sql, &sel.offset, &sel.limit);
5495 sql.push(')');
5496 sql
5497 }
5498 IrExpr::RawSql(s) => format!("({s})"),
5499 }
5500}
5501
5502fn emit_vector_search(vs: &IrVectorSearch) -> SqlOutput {
5505 let alias = &vs.source.alias;
5506 let dist_sql = format!(
5507 "{}.{} {} {}",
5508 qi(alias),
5509 qi(&vs.vector_col),
5510 vs.distance_op,
5511 emit_expr(&vs.query_expr),
5512 );
5513
5514 let (obj_tuple, object_shape_nodes) = if vs.object_shape.is_empty() {
5519 let type_expr = type_disc(&vs.source.type_name);
5521 let id_expr = format!("{}.\"id\"", qi(alias));
5522 let tuple = format!("{},\n {}", type_expr, id_expr);
5523 let id_node = ShapeNode::Scalar {
5524 name: "id".to_string(),
5525 position: 1,
5526 };
5527 (tuple, vec![id_node])
5528 } else {
5529 let (pointer_exprs, shape_pointers) = build_shape(&vs.object_shape, alias);
5530 let mut parts = vec![type_disc(&vs.source.type_name)];
5531 parts.extend(pointer_exprs);
5532 (parts.join(",\n "), prepend_type(shape_pointers))
5533 };
5534
5535 let outer = format!(
5537 "NULL::text,\n ROW(\n {}\n )::record,\n {}",
5538 obj_tuple, dist_sql,
5539 );
5540 let mut sql = format!(
5541 "SELECT (\n {}\n) AS result\nFROM {} AS {}",
5542 outer,
5543 source_ref(&vs.source),
5544 qi(alias),
5545 );
5546 append_filter(&mut sql, &vs.filter);
5547
5548 if let Some(dir) = &vs.order_by_distance {
5550 let dir_sql = match dir {
5551 IrSortDir::Asc => "ASC",
5552 IrSortDir::Desc => "DESC",
5553 };
5554 sql.push_str(&format!("\nORDER BY {} {}", dist_sql, dir_sql));
5555 }
5556 append_offset_limit(&mut sql, &vs.offset, &vs.limit);
5557
5558 let object_node = ShapeNode::Object {
5559 name: "object".to_string(),
5560 type_name: Some(vs.source.type_name.clone()),
5561 position: 1,
5562 cardinality: Cardinality::Many,
5563 pointers: object_shape_nodes,
5564 has_implicit_id: shape_has_implicit_id(&vs.object_shape),
5565 };
5566 let shape = ShapeDescriptor {
5567 root: ShapeNode::VectorSearch {
5568 object_position: 1,
5569 distance_position: 2,
5570 object_node: Box::new(object_node),
5571 },
5572 };
5573 let inference_plan = vs.inference_model.as_ref().map(|model_name| InferencePlan::Embedding {
5574 model_name: model_name.clone(),
5575 type_name: vs.inference_type_name.clone().unwrap_or_default(),
5576 index_name: vs.inference_index_name.clone().unwrap_or(None),
5577 query_param_name: vs.inference_query_param_name.clone().unwrap_or_default(),
5578 query_literal: vs.inference_query_literal.clone(),
5579 });
5580 SqlOutput {
5581 sql,
5582 shape,
5583 inference_plan,
5584 }
5585}
5586
5587fn emit_fts_search(fs: &IrFtsSearch) -> SqlOutput {
5590 use crate::schema::SearchBackend;
5591 if fs.backend != SearchBackend::Postgres {
5592 return emit_fts_search_deferred(fs);
5593 }
5594
5595 let alias = &fs.source.alias;
5596 let search_col = format!("{}.{}", qi(alias), qi(&fs.search_col));
5597 let query_sql = emit_expr(&fs.query_expr);
5598 let tsquery = format!("{}('english', {})", fs.tsquery_fn, query_sql);
5599 let rank_sql = format!("ts_rank({}, {})", search_col, tsquery);
5600
5601 let (obj_tuple, object_shape_nodes) = if fs.object_shape.is_empty() {
5602 let type_expr = type_disc(&fs.source.type_name);
5603 let id_expr = format!("{}.\"id\"", qi(alias));
5604 let tuple = format!("{},\n {}", type_expr, id_expr);
5605 let id_node = ShapeNode::Scalar {
5606 name: "id".to_string(),
5607 position: 1,
5608 };
5609 (tuple, vec![id_node])
5610 } else {
5611 let (pointer_exprs, shape_pointers) = build_shape(&fs.object_shape, alias);
5612 let mut parts = vec![type_disc(&fs.source.type_name)];
5613 parts.extend(pointer_exprs);
5614 (parts.join(",\n "), prepend_type(shape_pointers))
5615 };
5616
5617 let outer = format!(
5618 "NULL::text,\n ROW(\n {}\n )::record,\n {}",
5619 obj_tuple, rank_sql,
5620 );
5621 let mut sql = format!(
5622 "SELECT (\n {}\n) AS result\nFROM {} AS {}\nWHERE {} @@ {}",
5623 outer,
5624 source_ref(&fs.source),
5625 qi(alias),
5626 search_col,
5627 tsquery,
5628 );
5629 if let Some(f) = &fs.filter {
5630 sql.push_str(&format!(" AND ({})", emit_expr(f)));
5631 }
5632 if let Some(dir) = &fs.order_by_rank {
5633 let dir_sql = match dir {
5634 IrSortDir::Asc => "ASC",
5635 IrSortDir::Desc => "DESC",
5636 };
5637 sql.push_str(&format!("\nORDER BY {} {}", rank_sql, dir_sql));
5638 }
5639 append_offset_limit(&mut sql, &fs.offset, &fs.limit);
5640
5641 let object_node = ShapeNode::Object {
5642 name: "object".to_string(),
5643 type_name: Some(fs.source.type_name.clone()),
5644 position: 1,
5645 cardinality: Cardinality::Many,
5646 pointers: object_shape_nodes,
5647 has_implicit_id: shape_has_implicit_id(&fs.object_shape),
5648 };
5649 let shape = ShapeDescriptor {
5650 root: ShapeNode::FtsSearch {
5651 object_position: 1,
5652 rank_position: 2,
5653 object_node: Box::new(object_node),
5654 },
5655 };
5656 SqlOutput {
5657 sql,
5658 shape,
5659 inference_plan: None,
5660 }
5661}
5662
5663fn emit_fts_search_deferred(fs: &IrFtsSearch) -> SqlOutput {
5664 let alias = &fs.source.alias;
5665 let ids_idx = fs
5666 .deferred_ids_param
5667 .expect("deferred_ids_param must be set for deferred backend");
5668 let scores_idx = fs
5669 .deferred_scores_param
5670 .expect("deferred_scores_param must be set for deferred backend");
5671 let ids_param = format!("${}", ids_idx + 1);
5672 let scores_param = format!("${}", scores_idx + 1);
5673
5674 let (obj_tuple, object_shape_nodes) = if fs.object_shape.is_empty() {
5675 let type_expr = type_disc(&fs.source.type_name);
5676 let id_expr = format!("{}.\"id\"", qi(alias));
5677 let tuple = format!("{},\n {}", type_expr, id_expr);
5678 let id_node = ShapeNode::Scalar {
5679 name: "id".to_string(),
5680 position: 1,
5681 };
5682 (tuple, vec![id_node])
5683 } else {
5684 let (pointer_exprs, shape_pointers) = build_shape(&fs.object_shape, alias);
5685 let mut parts = vec![type_disc(&fs.source.type_name)];
5686 parts.extend(pointer_exprs);
5687 (parts.join(",\n "), prepend_type(shape_pointers))
5688 };
5689
5690 let outer = format!(
5691 "NULL::text,\n ROW(\n {}\n )::record,\n \"_os\".\"score\"",
5692 obj_tuple,
5693 );
5694 let mut sql = format!(
5695 concat!(
5696 "SELECT (\n {}\n) AS result\n",
5697 "FROM {} AS {}\n",
5698 "JOIN UNNEST({}::uuid[], {}::float8[]) AS \"_os\"(\"id\", \"score\")\n",
5699 " ON \"_os\".\"id\" = {}.\"id\"",
5700 ),
5701 outer,
5702 source_ref(&fs.source),
5703 qi(alias),
5704 ids_param,
5705 scores_param,
5706 qi(alias),
5707 );
5708 if let Some(f) = &fs.filter {
5709 sql.push_str(&format!("\nWHERE ({})", emit_expr(f)));
5710 }
5711 if let Some(dir) = &fs.order_by_rank {
5712 let dir_sql = match dir {
5713 IrSortDir::Asc => "ASC",
5714 IrSortDir::Desc => "DESC",
5715 };
5716 sql.push_str(&format!("\nORDER BY \"_os\".\"score\" {}", dir_sql));
5717 }
5718 let size = fs.limit.as_ref().and_then(|lim| {
5720 if let IrExpr::Literal(IrLiteral::Int(n)) = lim {
5721 Some(*n as usize)
5722 } else {
5723 None
5724 }
5725 });
5726
5727 let object_node = ShapeNode::Object {
5728 name: "object".to_string(),
5729 type_name: Some(fs.source.type_name.clone()),
5730 position: 1,
5731 cardinality: Cardinality::Many,
5732 pointers: object_shape_nodes,
5733 has_implicit_id: shape_has_implicit_id(&fs.object_shape),
5734 };
5735 let shape = ShapeDescriptor {
5736 root: ShapeNode::FtsSearch {
5737 object_position: 1,
5738 rank_position: 2,
5739 object_node: Box::new(object_node),
5740 },
5741 };
5742 let backend_str = match fs.backend {
5743 crate::schema::SearchBackend::Meilisearch => "meilisearch",
5744 _ => "opensearch",
5745 };
5746 let inference_plan = Some(InferencePlan::Search {
5747 backend: backend_str.to_string(),
5748 index_name: fs.deferred_index_name.clone().unwrap_or_default(),
5749 query_param_name: fs.deferred_query_param_name.clone().unwrap_or_default(),
5750 query_literal: fs.deferred_query_literal.clone(),
5751 size,
5752 });
5753 SqlOutput {
5754 sql,
5755 shape,
5756 inference_plan,
5757 }
5758}
5759
5760fn emit_function_select(sel: &IrFunctionSelect) -> SqlOutput {
5763 let alias = &sel.alias;
5764 let (pointer_exprs, shape_pointers) = build_shape(&sel.shape, alias);
5765
5766 let type_expr = if sel.polymorphic {
5767 format!("{}.\"__type__\"", qi(alias))
5768 } else {
5769 type_disc(&sel.type_name)
5770 };
5771 let mut parts = vec![type_expr];
5772 parts.extend(pointer_exprs);
5773 let tuple = parts.join(",\n ");
5774 let distinct = if sel.distinct { "DISTINCT " } else { "" };
5775
5776 let args_sql = sel.fn_args.iter().map(emit_expr).collect::<Vec<_>>().join(", ");
5777 let fn_call = format!("{}.{}({})", pg_schema(&sel.fn_module), qi(&sel.fn_name), args_sql);
5778
5779 let from_clause = if sel.polymorphic {
5780 format!("{} AS {}", fn_call, qi(alias))
5783 } else {
5784 format!("{} AS {}", fn_call, qi(alias))
5785 };
5786
5787 let mut sql = format!("SELECT {}(\n {}\n) AS result\nFROM {}", distinct, tuple, from_clause,);
5788 append_filter(&mut sql, &sel.filter);
5789 append_order_by(&mut sql, &sel.order_by);
5790 append_offset_limit(&mut sql, &sel.offset, &sel.limit);
5791
5792 let root_pointers = prepend_type(shape_pointers);
5793 SqlOutput {
5794 sql,
5795 shape: ShapeDescriptor {
5796 root: ShapeNode::Object {
5797 name: String::new(),
5798 type_name: Some(sel.type_name.clone()),
5799 position: 0,
5800 cardinality: Cardinality::Many,
5801 pointers: root_pointers,
5802 has_implicit_id: shape_has_implicit_id(&sel.shape),
5803 },
5804 },
5805 inference_plan: None,
5806 }
5807}
5808
5809pub fn emit_fn_body(ir: &crate::ir::IrOutput) -> String {
5815 with_subtype_fanouts(&ir.subtype_fanouts, || emit_output_fn_body(ir))
5816}
5817
5818fn emit_output_fn_body(ir: &crate::ir::IrOutput) -> String {
5819 let body = match &ir.stmt {
5820 IrStmt::Select(sel) if matches!(sel.rows.as_slice(), [IrRowSource::Free(IrFreeExpr::Scalar(_))]) => {
5821 let IrRowSource::Free(IrFreeExpr::Scalar(e)) = &sel.rows[0] else {
5822 unreachable!()
5823 };
5824 format!("SELECT {}", emit_expr(e))
5825 }
5826 other => emit_dml_as_cte_source(other),
5827 };
5828 if ir.ctes.is_empty() {
5829 body
5830 } else {
5831 let cte_prefix = emit_cte_prefix(&ir.ctes);
5832 format!("{}{}", cte_prefix, body)
5833 }
5834}
5835
5836fn emit_literal(lit: &IrLiteral) -> String {
5837 match lit {
5838 IrLiteral::Str(s) => sql_str(s),
5839 IrLiteral::Int(i) => i.to_string(),
5840 IrLiteral::Float(f) => {
5841 let s = f.to_string();
5850 let s = if s.contains('.') || s.contains('e') {
5851 s
5852 } else {
5853 format!("{}.0", s)
5854 };
5855 format!("({}::float8)", s)
5856 }
5857 IrLiteral::Bool(b) => {
5858 if *b {
5859 "TRUE".into()
5860 } else {
5861 "FALSE".into()
5862 }
5863 }
5864 }
5865}
5866
5867#[cfg(test)]
5870mod tests {
5871 use super::*;
5872 use crate::ir;
5873 use crate::parse;
5874 use crate::schema::{
5875 FunctionDescriptor, FunctionParamDescriptor, GlobalDescriptor, LinkDescriptor, MultiLinkDescriptor,
5876 NamedTupleDescriptor, PropertyDescriptor, SchemaDescriptor, TypeDescriptor,
5877 };
5878
5879 #[test]
5880 fn a_template_argument_keeps_the_parameters_it_carries() {
5881 let filled = fill_template("f($1, $2, $10)", &["($2)::int".to_string(), "b".to_string()]);
5884 assert_eq!(filled, "f(($2)::int, b, $10)");
5885 }
5886
5887 fn make_schema() -> SchemaDescriptor {
5888 SchemaDescriptor {
5889 types: vec![
5890 TypeDescriptor {
5891 name: "Person".into(),
5892 module: "default".into(),
5893 table: "Person".into(),
5894 abstract_: false,
5895 materialized: false,
5896 description: None,
5897 parents: vec![],
5898 interfaces: vec![],
5899 bases: vec![],
5900 properties: vec![
5901 PropertyDescriptor {
5902 name: "id".into(),
5903 pg_type: "uuid".into(),
5904 nullable: false,
5905 default_sql: Some("uuidv7()".into()),
5906 default_pyql: None,
5907 description: None,
5908 check_constraints: vec![],
5909 is_exclusive: true,
5910 is_pk: true,
5911 is_readonly: true,
5912 rewrites: vec![],
5913 tuple_members: None,
5914 column_type: None,
5915 },
5916 PropertyDescriptor {
5917 name: "name".into(),
5918 pg_type: "text".into(),
5919 nullable: false,
5920 default_sql: None,
5921 default_pyql: None,
5922 description: None,
5923 check_constraints: vec![],
5924 is_exclusive: false,
5925 is_pk: false,
5926 is_readonly: false,
5927 rewrites: vec![],
5928 tuple_members: None,
5929 column_type: None,
5930 },
5931 PropertyDescriptor {
5932 name: "age".into(),
5933 pg_type: "int8".into(),
5934 nullable: true,
5935 default_sql: None,
5936 default_pyql: None,
5937 description: None,
5938 check_constraints: vec![],
5939 is_exclusive: false,
5940 is_pk: false,
5941 is_readonly: false,
5942 rewrites: vec![],
5943 tuple_members: None,
5944 column_type: None,
5945 },
5946 ],
5947 links: vec![LinkDescriptor {
5948 name: "company".into(),
5949 target: "default::Company".into(),
5950 nullable: true,
5951 through: None,
5952 description: None,
5953 default_pyql: None,
5954 is_exclusive: false,
5955 is_readonly: false,
5956 rewrites: vec![],
5957 on_delete: vec![],
5958 }],
5959 multilinks: vec![MultiLinkDescriptor {
5960 name: "posts".into(),
5961 target: "default::Post".into(),
5962 through: None,
5963 nullable: false,
5964 description: None,
5965 default_pyql: None,
5966 on_delete: vec![],
5967 is_exclusive: false,
5968 }],
5969 computed: vec![],
5970 constraints: vec![],
5971 indexes: vec![],
5972 partition: None,
5973 vector_indexes: vec![],
5974 search_indexes: vec![],
5975 triggers: vec![],
5976 junction: false,
5977 signals: vec![],
5978 },
5979 TypeDescriptor {
5980 name: "Company".into(),
5981 module: "default".into(),
5982 table: "Company".into(),
5983 abstract_: false,
5984 materialized: false,
5985 description: None,
5986 parents: vec![],
5987 interfaces: vec![],
5988 bases: vec![],
5989 properties: vec![PropertyDescriptor {
5990 name: "name".into(),
5991 pg_type: "text".into(),
5992 nullable: false,
5993 default_sql: None,
5994 default_pyql: None,
5995 description: None,
5996 check_constraints: vec![],
5997 is_exclusive: false,
5998 is_pk: false,
5999 is_readonly: false,
6000 rewrites: vec![],
6001 tuple_members: None,
6002 column_type: None,
6003 }],
6004 links: vec![],
6005 multilinks: vec![],
6006 computed: vec![],
6007 constraints: vec![],
6008 indexes: vec![],
6009 partition: None,
6010 vector_indexes: vec![],
6011 search_indexes: vec![],
6012 triggers: vec![],
6013 junction: false,
6014 signals: vec![],
6015 },
6016 TypeDescriptor {
6017 name: "Post".into(),
6018 module: "default".into(),
6019 table: "Post".into(),
6020 abstract_: false,
6021 materialized: false,
6022 description: None,
6023 parents: vec![],
6024 interfaces: vec![],
6025 bases: vec![],
6026 properties: vec![PropertyDescriptor {
6027 name: "title".into(),
6028 pg_type: "text".into(),
6029 nullable: false,
6030 default_sql: None,
6031 default_pyql: None,
6032 description: None,
6033 check_constraints: vec![],
6034 is_exclusive: false,
6035 is_pk: false,
6036 is_readonly: false,
6037 rewrites: vec![],
6038 tuple_members: None,
6039 column_type: None,
6040 }],
6041 links: vec![],
6042 multilinks: vec![],
6043 computed: vec![],
6044 constraints: vec![],
6045 indexes: vec![],
6046 partition: None,
6047 vector_indexes: vec![],
6048 search_indexes: vec![],
6049 triggers: vec![],
6050 junction: false,
6051 signals: vec![],
6052 },
6053 ],
6054 scalars: vec![],
6055 enums: vec![],
6056 named_tuples: vec![],
6057 globals: vec![],
6058 functions: vec![],
6059 aliases: vec![],
6060 channels: vec![],
6061 ..Default::default()
6062 }
6063 }
6064
6065 fn compile_and_emit(query: &str) -> SqlOutput {
6066 let schema = make_schema();
6067 compile_and_emit_with(query, &schema)
6068 }
6069
6070 fn compile_and_emit_with(query: &str, schema: &SchemaDescriptor) -> SqlOutput {
6071 let ast = parse::parse(query).expect("parse failed");
6072 let ir = ir::compile(&ast, schema).expect("IR compile failed");
6073 emit(&ir)
6074 }
6075
6076 #[test]
6077 fn a_nested_read_inside_a_with_opens_no_second_with() {
6078 let out = compile_and_emit_with(
6079 "WITH a := (SELECT Person FILTER .age > 1), b := (SELECT Person LIMIT 1) SELECT (SELECT (a UNION b))",
6080 &make_schema(),
6081 );
6082 assert_eq!(out.sql.matches("WITH").count(), 1, "{}", out.sql);
6083 }
6084
6085 #[test]
6086 fn a_comparison_against_a_multi_row_with_binding_tests_membership() {
6087 let out = compile_and_emit_with(
6091 "WITH companies := (SELECT Company FILTER .name = 'x') SELECT Person FILTER .company = companies",
6092 &make_schema(),
6093 );
6094 assert!(
6095 out.sql.contains("= ANY((SELECT \"id\" FROM \"companies\"))"),
6096 "{}",
6097 out.sql
6098 );
6099
6100 let out = compile_and_emit_with(
6101 "WITH companies := (SELECT Company) SELECT Person FILTER any(.company = companies)",
6102 &make_schema(),
6103 );
6104 assert!(
6105 out.sql.contains("= ANY((SELECT \"id\" FROM \"companies\"))"),
6106 "{}",
6107 out.sql
6108 );
6109
6110 let out = compile_and_emit_with(
6111 "WITH companies := (SELECT Company) SELECT Person FILTER .company != companies",
6112 &make_schema(),
6113 );
6114 assert!(out.sql.contains("NOT (\"t1\".\"company_id\" = ANY("), "{}", out.sql);
6115 }
6116
6117 #[test]
6118 fn a_comparison_against_a_single_row_with_binding_stays_a_scalar_read() {
6119 let out = compile_and_emit_with(
6120 "WITH company := (SELECT Company LIMIT 1) SELECT Person FILTER .company = company",
6121 &make_schema(),
6122 );
6123 assert!(out.sql.contains("= (SELECT \"id\" FROM \"company\")"), "{}", out.sql);
6124 }
6125
6126 #[test]
6127 fn an_aggregate_over_a_backlink_counts_inside_a_subquery() {
6128 let out = compile_and_emit_with("SELECT Post { n := count(.<posts) }", &make_schema());
6129 assert!(
6130 !out.sql.contains("count((SELECT"),
6131 "an aggregate over a scalar subquery:\n{}",
6132 out.sql
6133 );
6134 assert!(out.sql.contains("count("), "{}", out.sql);
6135 }
6136
6137 #[test]
6138 fn an_aggregate_over_a_relative_subselect_counts_inside_a_subquery() {
6139 let out = compile_and_emit_with(
6140 "SELECT Post { n := count((SELECT .<posts FILTER .age > 1)) }",
6141 &make_schema(),
6142 );
6143 assert!(
6144 !out.sql.contains("count((SELECT"),
6145 "an aggregate over a scalar subquery:\n{}",
6146 out.sql
6147 );
6148 assert!(
6149 out.sql.contains(r#"(SELECT count("_s"."v") FROM unnest(ARRAY("#),
6150 "{}",
6151 out.sql
6152 );
6153 }
6154
6155 #[test]
6156 fn any_over_a_link_type_check_tests_each_element() {
6157 let out = compile_and_emit_with(
6158 "SELECT Company { name } FILTER any(.<company.posts IS Post)",
6159 &make_schema(),
6160 );
6161 assert!(
6162 !out.sql.contains("bool_or(ARRAY"),
6163 "an aggregate over outer rows:\n{}",
6164 out.sql
6165 );
6166 assert!(
6167 out.sql
6168 .contains("FROM unnest(ARRAY(SELECT ('default::Post' = 'default::Post')"),
6169 "{}",
6170 out.sql
6171 );
6172 }
6173
6174 #[test]
6175 fn a_link_type_check_reads_the_linked_object() {
6176 let out = compile_and_emit_with("SELECT Person { name } FILTER .company IS Company", &make_schema());
6177 assert!(!out.sql.contains("WHERE FALSE"), "{}", out.sql);
6178 assert!(out.sql.contains("'default::Company'"), "{}", out.sql);
6179 }
6180
6181 fn schema_with_a_subtype() -> SchemaDescriptor {
6183 let mut schema = make_schema();
6184 let mut admin = schema.types[0].clone();
6185 admin.name = "Admin".into();
6186 admin.table = "Admin".into();
6187 admin.bases = vec!["default::Person".into()];
6188 schema.types.push(admin);
6189 schema
6190 }
6191
6192 #[test]
6193 fn a_type_with_subtypes_reads_their_rows_too() {
6194 let out = compile_and_emit_with("SELECT Person { name }", &schema_with_a_subtype());
6195 assert!(
6196 out.sql.contains("'default::Admin'::text AS \"__type__\"") && out.sql.contains("FROM \"public\".\"Admin\""),
6197 "expected the subtype's table in the read:\n{}",
6198 out.sql
6199 );
6200 }
6201
6202 #[test]
6203 fn a_path_from_a_type_with_subtypes_reads_their_rows_too() {
6204 let out = compile_and_emit_with("SELECT Person.name", &schema_with_a_subtype());
6205 assert!(out.sql.contains("FROM \"public\".\"Admin\""), "{}", out.sql);
6206 }
6207
6208 #[test]
6209 fn a_multilink_of_a_type_with_subtypes_reads_their_junctions_too() {
6210 let out = compile_and_emit_with("SELECT Person { posts: { title } }", &schema_with_a_subtype());
6211 assert!(
6212 out.sql.contains(
6213 "(SELECT \"source\", \"target\" FROM \"public\".\"Person.posts\" UNION ALL \
6214 SELECT \"source\", \"target\" FROM \"public\".\"Admin.posts\")"
6215 ),
6216 "expected the junctions unioned:\n{}",
6217 out.sql
6218 );
6219 }
6220
6221 #[test]
6222 fn is_a_supertype_matches_its_subtypes() {
6223 let out = compile_and_emit_with(
6224 "SELECT Person { name } FILTER Person IS Admin",
6225 &schema_with_a_subtype(),
6226 );
6227 assert!(out.sql.contains("\"__type__\" = 'default::Admin'"), "{}", out.sql);
6228 }
6229
6230 #[test]
6231 fn a_write_to_a_type_with_subtypes_reaches_their_tables() {
6232 let out = compile_and_emit_with("UPDATE Person SET { age := 1 }", &schema_with_a_subtype());
6233 assert!(
6234 out.sql.contains("UPDATE \"public\".\"Person\"") && out.sql.contains("UPDATE \"public\".\"Admin\""),
6235 "expected both tables updated:\n{}",
6236 out.sql
6237 );
6238 assert!(
6239 !out.sql.contains("UPDATE (\n"),
6240 "a write never targets the union:\n{}",
6241 out.sql
6242 );
6243 }
6244
6245 #[test]
6246 fn test_comparing_a_multilink_itself_to_an_object() {
6247 let out = compile_and_emit_with(
6251 "WITH p := (SELECT Post LIMIT 1) SELECT Person { id } FILTER any(.posts = p)",
6252 &make_schema(),
6253 );
6254 assert!(
6255 out.sql.contains("EXISTS((SELECT 1\nFROM \"public\".\"Person.posts\""),
6256 "expected a junction EXISTS:\n{}",
6257 out.sql
6258 );
6259 assert!(
6260 out.sql.contains("\"target\" = (SELECT \"id\" FROM \"p\")"),
6261 "expected the junction target compared to the bound object's id:\n{}",
6262 out.sql
6263 );
6264 }
6265
6266 #[test]
6267 fn test_comparing_a_multilink_itself_warns_without_any() {
6268 let ast = parse::parse("WITH p := (SELECT Post LIMIT 1) SELECT Person { id } FILTER .posts = p").unwrap();
6269 let ir = ir::compile(&ast, &make_schema()).unwrap();
6270 assert!(
6271 ir.warnings.iter().any(|w| w.contains("multi-link '.posts'")),
6272 "expected the set-valued FILTER warning, got: {:?}",
6273 ir.warnings
6274 );
6275 }
6276
6277 #[test]
6278 fn test_detached_names_the_type_of_a_nested_select() {
6279 let out = compile_and_emit_with(
6283 "INSERT Person { name := $n, company := (SELECT detached Company FILTER .name = $c LIMIT 1) }",
6284 &make_schema(),
6285 );
6286 assert!(
6287 out.sql.contains("INSERT INTO \"public\".\"Person\""),
6288 "expected the insert to compile:\n{}",
6289 out.sql
6290 );
6291 assert!(
6292 out.sql.contains("FROM \"public\".\"Company\""),
6293 "expected the detached select to read the Company table:\n{}",
6294 out.sql
6295 );
6296 }
6297
6298 #[test]
6299 fn test_select_over_an_update_on_a_with_binding() {
6300 let out = compile_and_emit_with(
6305 "WITH p := (SELECT Person FILTER .id = $a LIMIT 1) SELECT (UPDATE p SET { name := $n }) { id }",
6306 &make_schema(),
6307 );
6308 assert!(
6309 out.sql.contains("UPDATE \"public\".\"Person\""),
6310 "expected the update to resolve to the bound type's table:\n{}",
6311 out.sql
6312 );
6313 }
6314
6315 #[test]
6316 fn test_a_mutation_as_a_free_object_field() {
6317 let out = compile_and_emit_with(
6321 "SELECT { renamed := (UPDATE Person FILTER .id = $a SET { name := $n }), n := 1 }",
6322 &make_schema(),
6323 );
6324 assert!(
6325 out.sql.contains("UPDATE \"public\".\"Person\""),
6326 "expected the mutation to become a data-modifying CTE:\n{}",
6327 out.sql
6328 );
6329 assert!(
6330 out.sql
6331 .contains("(SELECT \"t1\".\"id\"\nFROM \"_nested_dml_0\" AS \"t1\") AS \"_f0\""),
6332 "expected the field to carry the rows the mutation touched:\n{}",
6333 out.sql
6334 );
6335 }
6336
6337 #[test]
6338 fn test_a_sub_selects_filter_stays_on_its_own_subject_when_a_field_chain_projects_off_it() {
6339 let out = compile_and_emit_with("SELECT (SELECT Person FILTER .name = $n).company.name", &make_schema());
6343 assert!(
6344 out.sql.contains("WHERE (\"t0\".\"name\" = $1)"),
6345 "the inner filter belongs to the Person alias:\n{}",
6346 out.sql
6347 );
6348 }
6349
6350 #[test]
6351 fn test_reading_back_a_nested_insert_through_its_own_statement() {
6352 let out = compile_and_emit_with(
6353 "SELECT (INSERT Person { name := $n, posts := (INSERT Post { title := $t }) }) { id, posts: { title } }",
6354 &make_schema(),
6355 );
6356 assert!(out.sql.contains("__ml_add_0"), "got:\n{}", out.sql);
6357 }
6358
6359 #[test]
6360 fn test_reading_back_a_nested_insert_within_its_own_statement() {
6361 let out = compile_and_emit_with(
6365 "SELECT (INSERT Person { name := $n, posts := (INSERT Post { title := $t }) }) \
6366 { id, posts: { title } }",
6367 &make_schema(),
6368 );
6369 assert!(
6370 out.sql.contains("FROM \"_dml__ml_add_0\" AS \"jt\""),
6371 "the junction rows must come from the CTE that wrote them:\n{}",
6372 out.sql
6373 );
6374 assert!(
6375 out.sql.contains("\"_nested_dml_0\""),
6376 "and the targets from the nested insert's own CTE:\n{}",
6377 out.sql
6378 );
6379 }
6380
6381 #[test]
6382 fn test_a_nested_single_link_insert_reads_back_from_its_cte() {
6383 let out = compile_and_emit_with(
6384 "SELECT (INSERT Person { name := $n, company := (INSERT Company { name := $c }) }) \
6385 { id, company: { name } }",
6386 &make_schema(),
6387 );
6388 assert!(
6389 out.sql.contains("FROM \"_nested_dml_0\""),
6390 "the linked row must be read from the CTE that inserted it:\n{}",
6391 out.sql
6392 );
6393 }
6394
6395 #[test]
6396 fn test_a_free_object_field_holding_an_object_keeps_it_an_object() {
6397 let out = compile_and_emit_with(
6401 "WITH c := (SELECT Company LIMIT 1) SELECT { co := c { name }, n := 1 }",
6402 &make_schema(),
6403 );
6404 assert!(
6405 out.sql.contains("'default::Company'::text"),
6406 "the field must carry the object's own row:\n{}",
6407 out.sql
6408 );
6409 }
6410
6411 #[test]
6412 fn test_projecting_an_object_off_a_free_object() {
6413 let out = compile_and_emit_with(
6416 "WITH c := (SELECT Company LIMIT 1) SELECT { co := c { name }, n := 1 }.co",
6417 &make_schema(),
6418 );
6419 assert!(
6420 !out.sql.contains("jsonb_build_object"),
6421 "projecting a pointer must not go through jsonb:\n{}",
6422 out.sql
6423 );
6424 assert!(
6425 out.sql.contains("SELECT v AS result, v FROM"),
6426 "the object row is already the result row:\n{}",
6427 out.sql
6428 );
6429 }
6430
6431 #[test]
6432 fn test_a_shape_on_a_single_valued_object_path() {
6433 let out = compile_and_emit_with("SELECT Person { c := .company { name } }", &make_schema());
6438 assert!(
6439 out.sql.contains("'default::Company'::text"),
6440 "the pointer must carry the object's own row:\n{}",
6441 out.sql
6442 );
6443 assert!(
6444 !out.sql.contains("ARRAY(SELECT"),
6445 "a single-valued walk is one object, not an array of one:\n{}",
6446 out.sql
6447 );
6448 }
6449
6450 #[test]
6451 fn test_a_shapeless_single_link_still_reads_as_its_value() {
6452 let out = compile_and_emit_with("SELECT Person { c := .company }", &make_schema());
6455 assert!(
6456 !out.sql.contains("'default::Company'::text"),
6457 "a bare link reference must not grow an object row:\n{}",
6458 out.sql
6459 );
6460 }
6461
6462 #[test]
6463 fn test_a_tuple_element_holding_a_shaped_object() {
6464 let out = compile_and_emit_with(
6468 "WITH c := (SELECT Company LIMIT 1) SELECT (c { name }, 1)",
6469 &make_schema(),
6470 );
6471 assert!(
6472 out.sql.contains("'default::Company'::text"),
6473 "the element must carry the object's own row:\n{}",
6474 out.sql
6475 );
6476 }
6477
6478 #[test]
6479 fn test_an_object_returning_function_as_a_link_value() {
6480 let mut schema = make_schema();
6485 let company = schema
6487 .types
6488 .iter_mut()
6489 .find(|t| t.name == "Company")
6490 .expect("make_schema declares Company");
6491 company.properties.push(PropertyDescriptor {
6492 name: "id".into(),
6493 pg_type: "uuid".into(),
6494 nullable: false,
6495 default_sql: None,
6496 default_pyql: None,
6497 description: None,
6498 check_constraints: vec![],
6499 is_exclusive: true,
6500 is_pk: true,
6501 is_readonly: true,
6502 rewrites: vec![],
6503 tuple_members: None,
6504 column_type: None,
6505 });
6506 schema.functions.push(crate::schema::FunctionDescriptor {
6507 name: "current_company".into(),
6508 module: "default".into(),
6509 params: vec![],
6510 return_pg_type: "default::Company".into(),
6511 return_is_object: true,
6512 return_is_set: false,
6513 return_is_polymorphic: false,
6514 volatility: "stable".into(),
6515 body: "select Company limit 1".into(),
6516 });
6517 let out = compile_and_emit_with("INSERT Person { name := $n, company := current_company() }", &schema);
6518 assert!(
6519 out.sql.contains("FROM \"public\".\"current_company\"()"),
6520 "the function must supply the foreign key:\n{}",
6521 out.sql
6522 );
6523 }
6524
6525 fn make_schema_with_an_object_returning_default() -> SchemaDescriptor {
6529 let mut schema = make_schema();
6530 let company = schema
6531 .types
6532 .iter_mut()
6533 .find(|t| t.name == "Company")
6534 .expect("make_schema declares Company");
6535 company.properties.push(PropertyDescriptor {
6536 name: "id".into(),
6537 pg_type: "uuid".into(),
6538 nullable: false,
6539 default_sql: None,
6540 default_pyql: None,
6541 description: None,
6542 check_constraints: vec![],
6543 is_exclusive: true,
6544 is_pk: true,
6545 is_readonly: true,
6546 rewrites: vec![],
6547 tuple_members: None,
6548 column_type: None,
6549 });
6550 schema.functions.push(crate::schema::FunctionDescriptor {
6551 name: "current_company".into(),
6552 module: "default".into(),
6553 params: vec![],
6554 return_pg_type: "default::Company".into(),
6555 return_is_object: true,
6556 return_is_set: false,
6557 return_is_polymorphic: false,
6558 volatility: "stable".into(),
6559 body: "select Company limit 1".into(),
6560 });
6561 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
6562 let company_link = person.links.iter_mut().find(|l| l.name == "company").unwrap();
6563 company_link.default_pyql = Some("current_company()".into());
6564 schema
6565 }
6566
6567 #[test]
6568 fn an_object_returning_link_default_is_applied_by_the_insert() {
6569 let schema = make_schema_with_an_object_returning_default();
6573 let out = compile_and_emit_with("INSERT Person { name := $n }", &schema);
6574 assert!(
6575 out.sql.contains("\"company_id\"") && out.sql.contains("current_company"),
6576 "the default must supply the foreign key:\n{}",
6577 out.sql
6578 );
6579 }
6580
6581 #[test]
6582 fn an_explicit_value_wins_over_an_inlined_default() {
6583 let schema = make_schema_with_an_object_returning_default();
6584 let out = compile_and_emit_with("INSERT Person { name := $n, company := {} }", &schema);
6585 assert!(
6586 !out.sql.contains("current_company"),
6587 "the shape named the pointer, so the default does not apply:\n{}",
6588 out.sql
6589 );
6590 }
6591
6592 #[test]
6593 fn an_object_returning_link_default_gets_no_column_default() {
6594 let schema = make_schema_with_an_object_returning_default();
6597 let ddl = crate::export::export_schema(&schema).unwrap();
6598 assert!(
6599 !ddl.contains("\"company_id\" uuid DEFAULT"),
6600 "the column must carry no default:\n{ddl}"
6601 );
6602 crate::validate::validate_schema_types(&schema).expect("an inlined default is not an error");
6603 }
6604
6605 #[test]
6606 fn a_column_expressible_default_stays_in_the_ddl() {
6607 let mut schema = make_schema();
6609 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
6610 let age = person.properties.iter_mut().find(|p| p.name == "age").unwrap();
6611 age.default_pyql = Some("21".into());
6612 assert!(
6613 crate::ir::inlined_pointer_defaults(schema.types.iter().find(|t| t.name == "Person").unwrap(), &schema)
6614 .is_empty()
6615 );
6616 let ddl = crate::export::export_schema(&schema).unwrap();
6617 assert!(ddl.contains("DEFAULT 21"), "{ddl}");
6618 let out = compile_and_emit_with("INSERT Person { name := $n }", &schema);
6619 assert!(
6620 !out.sql.contains("21"),
6621 "the column DEFAULT still applies it:\n{}",
6622 out.sql
6623 );
6624 }
6625
6626 fn make_schema_with_an_inherited_self_naming_computed() -> SchemaDescriptor {
6630 let mut schema = make_schema();
6631 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
6632 person.computed.push(crate::schema::ComputedDescriptor {
6633 name: "own_name".into(),
6634 expression: "default::Person.name".into(),
6635 return_type: Some("text".into()),
6636 link_target: None,
6637 link_multi: false,
6638 });
6639 let mut employee = person.clone();
6640 employee.name = "Employee".into();
6641 employee.table = "Employee".into();
6642 employee.bases = vec!["default::Person".into()];
6643 schema.types.push(employee);
6644 schema
6645 }
6646
6647 #[test]
6648 fn an_inherited_computed_naming_its_declaring_type_still_means_the_row() {
6649 let schema = make_schema_with_an_inherited_self_naming_computed();
6654 let out = compile_and_emit_with("SELECT Employee { own_name }", &schema);
6655 assert!(
6656 !out.sql.contains("\"Person\""),
6657 "the computed must read the Employee row, not Person's table:\n{}",
6658 out.sql
6659 );
6660 crate::validate::validate_schema_types(&schema).expect("an inherited computed is single-valued");
6661 }
6662
6663 #[test]
6664 fn an_inherited_type_check_computed_tests_the_row_it_is_read_on() {
6665 let mut schema = make_schema();
6671 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
6672 person.computed.push(crate::schema::ComputedDescriptor {
6673 name: "employed".into(),
6674 expression: "(default::Person is default::Employee)".into(),
6675 return_type: Some("boolean".into()),
6676 link_target: None,
6677 link_multi: false,
6678 });
6679 let mut employee = person.clone();
6680 employee.name = "Employee".into();
6681 employee.table = "Employee".into();
6682 employee.bases = vec!["default::Person".into()];
6683 schema.types.push(employee);
6684
6685 crate::validate::validate_schema_types(&schema).expect("the check is single-valued on both types");
6686 let out = compile_and_emit_with("SELECT Employee { employed }", &schema);
6687 assert!(
6688 !out.sql.contains("\"Person\""),
6689 "the check must read the Employee row's own type:\n{}",
6690 out.sql
6691 );
6692 }
6693
6694 #[test]
6695 fn a_shape_writing_the_computed_out_itself_does_not_get_the_anchor() {
6696 let schema = make_schema_with_an_inherited_self_naming_computed();
6704 let ast = parse::parse("SELECT Employee { own_name := default::Person.name }").expect("parse failed");
6705 let Err(error) = ir::compile(&ast, &schema) else {
6706 panic!("an absolute path has nothing to anchor to here")
6707 };
6708 assert!(
6709 format!("{error:?}").contains("absolute paths are not valid"),
6710 "{error:?}"
6711 );
6712 }
6713
6714 #[test]
6715 fn a_default_that_compiles_nowhere_is_still_an_error() {
6716 let mut schema = make_schema();
6719 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
6720 let age = person.properties.iter_mut().find(|p| p.name == "age").unwrap();
6721 age.default_pyql = Some("sequence_next(INTROSPECT default::AgeSeq)".into());
6722 let errs = crate::validate::validate_schema_types(&schema).unwrap_err();
6723 let (_, msg, _) = errs[0].class_name_message_position();
6724 assert!(msg.contains("default"), "{msg}");
6725 }
6726
6727 #[test]
6728 fn test_comparing_a_link_with_an_object_returning_function() {
6729 let mut schema = make_schema();
6733 let company = schema
6735 .types
6736 .iter_mut()
6737 .find(|t| t.name == "Company")
6738 .expect("make_schema declares Company");
6739 company.properties.push(PropertyDescriptor {
6740 name: "id".into(),
6741 pg_type: "uuid".into(),
6742 nullable: false,
6743 default_sql: None,
6744 default_pyql: None,
6745 description: None,
6746 check_constraints: vec![],
6747 is_exclusive: true,
6748 is_pk: true,
6749 is_readonly: true,
6750 rewrites: vec![],
6751 tuple_members: None,
6752 column_type: None,
6753 });
6754 schema.functions.push(crate::schema::FunctionDescriptor {
6755 name: "current_company".into(),
6756 module: "default".into(),
6757 params: vec![],
6758 return_pg_type: "default::Company".into(),
6759 return_is_object: true,
6760 return_is_set: false,
6761 return_is_polymorphic: false,
6762 volatility: "stable".into(),
6763 body: "select Company limit 1".into(),
6764 });
6765 let out = compile_and_emit_with("SELECT Person FILTER .company = current_company()", &schema);
6766 assert!(
6767 out.sql.contains("\"company_id\"") && out.sql.contains("current_company"),
6768 "the link's key against the function's row:\n{}",
6769 out.sql
6770 );
6771 }
6772
6773 #[test]
6774 fn test_choosing_between_two_object_sets() {
6775 let out = compile_and_emit_with(
6779 "WITH a := (SELECT Person FILTER .name = $n), b := (SELECT Person) \
6780 SELECT (a IF EXISTS a ELSE b) { name }",
6781 &make_schema(),
6782 );
6783 assert!(
6784 out.sql.contains("UNION ALL"),
6785 "expected one branch per side:\n{}",
6786 out.sql
6787 );
6788 }
6789
6790 #[test]
6791 fn test_a_union_operand_written_inline() {
6792 let out = compile_and_emit_with(
6796 "SELECT ((SELECT Person FILTER .name = $n) UNION (SELECT Person)) { name }",
6797 &make_schema(),
6798 );
6799 assert!(
6800 out.sql.starts_with("WITH"),
6801 "expected the operands hoisted:\n{}",
6802 out.sql
6803 );
6804 assert!(out.sql.contains("UNION ALL"), "{}", out.sql);
6805 }
6806
6807 #[test]
6808 fn test_a_for_loop_variable_over_objects() {
6809 let out = compile_and_emit_with("FOR p IN (SELECT Person) UNION (SELECT p.name)", &make_schema());
6813 assert!(
6814 out.sql.contains("FROM \"public\".\"Person\"") && out.sql.contains("\"_for_p\".\"v\""),
6815 "the body must read the row the variable names:\n{}",
6816 out.sql
6817 );
6818 }
6819
6820 #[test]
6821 fn test_a_shape_on_a_for_loop_variable() {
6822 let out = compile_and_emit_with("FOR p IN (SELECT Person) UNION (SELECT p { name })", &make_schema());
6823 assert!(
6824 out.sql.contains("'default::Person'::text"),
6825 "a shape on the variable yields its object:\n{}",
6826 out.sql
6827 );
6828 }
6829
6830 #[test]
6831 fn test_a_for_loop_variable_over_values_is_still_a_scalar() {
6832 let out = compile_and_emit_with("FOR n IN {1, 2} UNION (SELECT n)", &make_schema());
6833 assert!(out.sql.contains("VALUES"), "{}", out.sql);
6834 }
6835
6836 #[test]
6837 fn test_exists_over_a_sub_select_on_a_relative_path() {
6838 let out = compile_and_emit_with(
6843 "SELECT Person { id } FILTER EXISTS ((SELECT .posts FILTER .title = $t))",
6844 &make_schema(),
6845 );
6846 assert!(out.sql.contains("EXISTS("), "{}", out.sql);
6847 assert!(
6848 out.sql.contains("\"Person.posts\""),
6849 "the junction join must survive into the EXISTS:\n{}",
6850 out.sql
6851 );
6852 assert!(
6853 out.sql.contains("\"t0\".\"id\""),
6854 "and it must correlate to the enclosing row:\n{}",
6855 out.sql
6856 );
6857 }
6858
6859 #[test]
6860 fn test_counting_the_rows_a_mutation_touched() {
6861 let out = compile_and_emit_with("SELECT count((DELETE Person FILTER .name = $n))", &make_schema());
6865 assert!(out.sql.starts_with("WITH"), "{}", out.sql);
6866 assert!(out.sql.contains("DELETE FROM \"public\".\"Person\""), "{}", out.sql);
6867 assert!(out.sql.contains("count(*)"), "{}", out.sql);
6868 }
6869
6870 #[test]
6871 fn test_a_conflict_target_naming_several_columns() {
6872 let out = compile_and_emit_with(
6876 "INSERT Person { name := $n } UNLESS CONFLICT ON (.name, .id) ELSE (UPDATE Person SET { name := $n })",
6877 &make_schema(),
6878 );
6879 assert!(
6880 out.sql.contains("ON CONFLICT (\"name\", \"id\")"),
6881 "expected a column list:\n{}",
6882 out.sql
6883 );
6884 }
6885
6886 #[test]
6887 fn test_a_for_loop_over_a_path() {
6888 let out = compile_and_emit_with(
6892 "WITH p := (SELECT Person LIMIT 1) FOR post IN p.posts UNION (SELECT post.title)",
6893 &make_schema(),
6894 );
6895 assert!(out.sql.contains("CROSS JOIN LATERAL"), "{}", out.sql);
6896 }
6897
6898 #[test]
6899 fn test_a_relative_sub_select_keeps_its_enclosing_object() {
6900 let out = compile_and_emit_with(
6905 "SELECT Person { ps := assert_exists((SELECT .posts { title })) }",
6906 &make_schema(),
6907 );
6908 assert!(out.sql.contains("assert_exists"), "{}", out.sql);
6909 assert!(
6910 out.sql.contains("\"Person.posts\""),
6911 "the walk must reach the junction:\n{}",
6912 out.sql
6913 );
6914 }
6915
6916 #[test]
6917 fn test_a_relative_sub_select_as_a_multi_link_value() {
6918 let out = compile_and_emit_with(
6919 "UPDATE Person FILTER .name = $n SET { posts := (SELECT .posts FILTER .title = $t) }",
6920 &make_schema(),
6921 );
6922 assert!(
6923 out.sql.contains("\"Person.posts\""),
6924 "the value must read the row's own posts:\n{}",
6925 out.sql
6926 );
6927 }
6928
6929 #[test]
6930 fn test_a_shape_written_after_a_sub_select() {
6931 let out = compile_and_emit_with(
6935 "SELECT Person { ps := (SELECT Post FILTER .title = $t) { title } }",
6936 &make_schema(),
6937 );
6938 assert!(
6939 out.sql.contains("'default::Post'::text"),
6940 "the pointer must carry the object rows:\n{}",
6941 out.sql
6942 );
6943 }
6944
6945 #[test]
6946 fn test_a_shape_after_a_sub_select_with_a_with_block() {
6947 let out = compile_and_emit_with(
6948 "SELECT Person { ps := (WITH t := $t SELECT Post FILTER .title = t) { title } }",
6949 &make_schema(),
6950 );
6951 assert!(out.sql.contains("'default::Post'::text"), "{}", out.sql);
6952 }
6953
6954 #[test]
6955 fn test_a_computed_declared_as_an_object_returning_call() {
6956 let mut schema = make_schema();
6961 schema.functions.push(crate::schema::FunctionDescriptor {
6962 name: "recent".into(),
6963 module: "default".into(),
6964 params: vec![],
6965 return_pg_type: "default::Post".into(),
6966 return_is_object: true,
6967 return_is_set: true,
6968 return_is_polymorphic: false,
6969 volatility: "stable".into(),
6970 body: "select Post".into(),
6971 });
6972 schema.types[0].computed.push(crate::schema::ComputedDescriptor {
6973 name: "recent".into(),
6974 expression: "default::recent()".into(),
6975 return_type: None,
6976 link_target: None,
6977 link_multi: false,
6978 });
6979 let out = compile_and_emit_with("SELECT Person { recent }", &schema);
6980 assert!(
6981 out.sql.contains("ARRAY(SELECT") && out.sql.contains("'default::Post'::text"),
6982 "the pointer must carry the function's rows:\n{}",
6983 out.sql
6984 );
6985 }
6986
6987 #[test]
6988 fn test_one_branch_of_an_object_if_else_is_empty() {
6989 let out = compile_and_emit_with(
6993 "WITH p := (SELECT Person LIMIT 1) SELECT p { name } IF EXISTS p ELSE {}",
6994 &make_schema(),
6995 );
6996 assert!(!out.sql.contains("UNION ALL"), "nothing to union with:\n{}", out.sql);
6997 assert!(out.sql.contains("'default::Person'::text"), "{}", out.sql);
6998 }
6999
7000 #[test]
7001 fn test_a_conditional_insert_is_guarded_by_its_condition() {
7002 let out = compile_and_emit_with("SELECT (INSERT Person { name := $n }) IF FALSE ELSE {}", &make_schema());
7007 assert!(
7008 out.sql.contains("SELECT") && out.sql.contains("WHERE"),
7009 "a guarded insert selects its row rather than listing values:\n{}",
7010 out.sql
7011 );
7012 assert!(!out.sql.contains("VALUES"), "VALUES always yields a row:\n{}", out.sql);
7013 }
7014
7015 #[test]
7016 fn test_a_conditional_update_carries_its_condition() {
7017 let out = compile_and_emit(
7022 "SELECT (UPDATE Person FILTER .name = $n SET { name := $m }) IF EXISTS (SELECT Company) ELSE {}",
7023 );
7024 let update = out.sql.find("UPDATE").expect("an update is emitted");
7025 let where_clause = out.sql[update..].find("WHERE").expect("the update is filtered");
7026 assert!(
7027 out.sql[update + where_clause..].contains("EXISTS"),
7028 "the condition narrows the rows the update touches:\n{}",
7029 out.sql
7030 );
7031 }
7032
7033 #[test]
7034 fn test_a_conditional_delete_carries_its_condition() {
7035 let out = compile_and_emit("SELECT (DELETE Person FILTER .name = $n) IF FALSE ELSE {}");
7039 let delete = out
7040 .sql
7041 .split("DELETE FROM")
7042 .nth(1)
7043 .expect("the delete should still be emitted");
7044 let where_clause = delete.split("WHERE").nth(1).expect("the delete should be filtered");
7045 assert!(
7046 where_clause.contains("FALSE") || where_clause.contains("false"),
7047 "the condition must narrow the delete itself:\n{}",
7048 out.sql
7049 );
7050 }
7051
7052 #[test]
7053 fn test_a_pointer_a_binding_declared_in_its_own_shape() {
7054 let out = compile_and_emit_with(
7058 "WITH p := (SELECT Person { co := .company { name } } LIMIT 1) SELECT p { name, co: { name } }",
7059 &make_schema(),
7060 );
7061 assert!(
7062 out.sql.contains("'default::Company'::text"),
7063 "the declared pointer must be inlined:\n{}",
7064 out.sql
7065 );
7066 }
7067
7068 #[test]
7069 fn test_a_binding_declared_pointer_read_from_a_tuple_element() {
7070 let out = compile_and_emit_with(
7071 "WITH p := (SELECT Person { co := .company { name } } LIMIT 1) SELECT (p { co: { name } }, 1)",
7072 &make_schema(),
7073 );
7074 assert!(out.sql.contains("'default::Company'::text"), "{}", out.sql);
7075 }
7076
7077 #[test]
7078 fn test_a_for_loop_variable_shaped_inside_a_tuple() {
7079 let out = compile_and_emit_with(
7083 "WITH p := (SELECT Person LIMIT 1) FOR q IN p.posts UNION (SELECT (a := q { title }, b := 1))",
7084 &make_schema(),
7085 );
7086 assert!(
7087 out.sql.contains("\"_for_q\".\"v\""),
7088 "the element must be narrowed to the loop's row:\n{}",
7089 out.sql
7090 );
7091 }
7092
7093 #[test]
7094 fn test_choosing_between_two_walks() {
7095 let out = compile_and_emit_with(
7098 "WITH p := (SELECT Person LIMIT 1), q := (SELECT p.posts LIMIT 1) IF TRUE ELSE (SELECT p.posts) \
7099 SELECT q { title }",
7100 &make_schema(),
7101 );
7102 assert!(out.sql.contains("UNION ALL"), "one branch per side:\n{}", out.sql);
7103 }
7104
7105 #[test]
7106 fn test_a_bare_coalesce_of_correlated_walks_reaching_many_objects() {
7107 let out = compile_and_emit_with(
7110 "SELECT Post { owners := .<posts[is Person] ?? .<posts[is Person] }",
7111 &make_schema(),
7112 );
7113 assert!(
7114 !out.sql.contains("COALESCE("),
7115 "not a value-by-value choice:\n{}",
7116 out.sql
7117 );
7118 assert!(
7119 out.sql.contains("ARRAY(SELECT \"r\" FROM ("),
7120 "the arms are aggregated:\n{}",
7121 out.sql
7122 );
7123 }
7124
7125 #[test]
7126 fn test_a_union_of_correlated_walks_reaching_many() {
7127 let out = compile_and_emit_with(
7132 "SELECT Post { owners := (.<posts[is Person] UNION .<posts[is Person]) { name } }",
7133 &make_schema(),
7134 );
7135 assert!(
7136 out.sql.contains("ARRAY(SELECT \"r\" FROM ("),
7137 "the arms are aggregated:\n{}",
7138 out.sql
7139 );
7140 }
7141
7142 #[test]
7143 fn test_a_coalesce_of_correlated_walks_reaching_many() {
7144 let out = compile_and_emit_with(
7147 "SELECT Post { owners := (.<posts[is Person] ?? .<posts[is Person]) { name } }",
7148 &make_schema(),
7149 );
7150 assert!(
7151 out.sql.contains("ARRAY(SELECT \"r\" FROM ("),
7152 "the arms are aggregated:\n{}",
7153 out.sql
7154 );
7155 }
7156
7157 #[test]
7158 fn test_a_union_of_correlated_walks() {
7159 let out = compile_and_emit_with(
7163 "SELECT Post { owners := (SELECT (.<posts[is Person] UNION .<posts[is Person]) { name } LIMIT 1) }",
7164 &make_schema(),
7165 );
7166 assert!(out.sql.contains("UNION ALL"), "one arm per operand:\n{}", out.sql);
7167 assert!(
7168 out.sql.contains("AS \"_u\""),
7169 "the arms are read as one set:\n{}",
7170 out.sql
7171 );
7172 }
7173
7174 #[test]
7175 fn test_a_for_loop_whose_body_updates() {
7176 let out = compile_and_emit_with(
7180 "FOR p IN (SELECT Person) UNION (UPDATE Person FILTER .id = p.id SET { name := $n })",
7181 &make_schema(),
7182 );
7183 assert!(out.sql.contains("UPDATE \"public\".\"Person\""), "{}", out.sql);
7184 assert!(
7185 out.sql.contains("FROM \"_for_p\""),
7186 "driven from the iteration:\n{}",
7187 out.sql
7188 );
7189 assert!(
7190 !out.sql.contains("LATERAL"),
7191 "DML cannot sit in a LATERAL:\n{}",
7192 out.sql
7193 );
7194 }
7195
7196 #[test]
7197 fn test_a_for_loop_body_appends_the_row_it_is_iterating() {
7198 let out = compile_and_emit(
7199 "WITH t := (SELECT Post) FOR q IN t UNION (UPDATE Person FILTER .id = $i SET { posts += q })",
7200 );
7201 assert!(
7204 out.sql.contains("\"_iter\"") && out.sql.contains("FROM \"_ids\""),
7205 "junction rows driven from the iteration:\n{}",
7206 out.sql
7207 );
7208 assert!(
7209 !out.sql.contains("LATERAL"),
7210 "DML cannot sit in a LATERAL:\n{}",
7211 out.sql
7212 );
7213 }
7214
7215 #[test]
7216 fn test_select_on_a_for_loop_variable_reads_only_its_row() {
7217 let out = compile_and_emit("FOR p IN (SELECT Person) UNION (SELECT (SELECT p) { name })");
7218 assert!(
7219 out.sql.contains("= \"_for_p\".\"v\""),
7220 "narrowed to the row the variable holds:\n{}",
7221 out.sql
7222 );
7223 }
7224
7225 #[test]
7226 fn test_a_walk_off_a_for_loop_variable_reads_the_binding() {
7227 let out = compile_and_emit("WITH ps := (SELECT Post) FOR q IN ps UNION (SELECT Person FILTER .name = q.title)");
7230 assert!(
7231 out.sql.contains("FROM \"ps\""),
7232 "the walk reads the binding:\n{}",
7233 out.sql
7234 );
7235 }
7236
7237 #[test]
7238 fn test_a_mutating_for_loop_binding_becomes_sibling_ctes() {
7239 let out = compile_and_emit(
7240 "WITH made := (FOR n IN {'a', 'b'} UNION (INSERT Post { title := n })) SELECT count(made)",
7241 );
7242 assert!(
7243 out.sql.contains("\"made__ids\" AS (\nINSERT INTO"),
7244 "the insert is its own top-level CTE:\n{}",
7245 out.sql
7246 );
7247 assert!(
7248 !out.sql.contains("LATERAL"),
7249 "DML cannot sit in a LATERAL:\n{}",
7250 out.sql
7251 );
7252 }
7253
7254 #[test]
7255 fn test_count_of_a_binding_counts_its_rows() {
7256 let out = compile_and_emit("WITH people := (SELECT Person) SELECT count(people)");
7259 assert!(out.sql.contains("(SELECT count(*) FROM \"people\")"), "{}", out.sql);
7260 }
7261
7262 #[test]
7263 fn test_dividing_integers_yields_a_float() {
7264 for (query, expected) in [
7265 ("SELECT 100 / 133", "((100)::float8 / 133)"),
7266 ("SELECT Person { a := .age / 2 }", "::float8 / 2)"),
7267 ("SELECT Person.age / 2", "::float8 / 2)"),
7268 (
7269 "WITH total := max({133}), current := 38 - 1 SELECT 100 / total * current",
7270 "(((100)::float8 / (SELECT \"v\" FROM \"total\")) * (SELECT \"v\" FROM \"current\"))",
7271 ),
7272 (
7273 "WITH p := (SELECT Person LIMIT 1), index := p.age - 1, total := max(Person.age) SELECT 100 / total * index",
7274 "((100)::float8 / (SELECT \"v\" FROM \"total\"))",
7275 ),
7276 ] {
7277 let out = compile_and_emit(query);
7278 assert!(out.sql.contains(expected), "{query}:\n{}", out.sql);
7279 }
7280 }
7281
7282 #[test]
7283 fn test_dividing_anything_but_two_integers_is_left_to_postgres() {
7284 for query in [
7285 "SELECT 100 // 133",
7286 "SELECT 100 / 1.5",
7287 "SELECT <decimal>100 / <decimal>3",
7288 "SELECT 100 / <decimal>3",
7289 ] {
7290 let out = compile_and_emit(query);
7291 assert!(!out.sql.contains("::float8 /"), "{query}:\n{}", out.sql);
7292 }
7293 }
7294
7295 #[test]
7296 fn test_a_free_object_under_a_condition_stays_an_object() {
7297 for query in [
7298 "SELECT { a := 1 } IF true ELSE {}",
7299 "SELECT {} IF true ELSE { a := 1 }",
7300 "SELECT { a := 1 } IF true ELSE {} LIMIT 1",
7301 ] {
7302 let out = compile_and_emit(query);
7303 assert!(
7304 matches!(&out.shape.root, ShapeNode::Object { type_name: None, pointers, .. } if pointers.len() == 1),
7305 "{query}: {:?}\n{}",
7306 out.shape.root,
7307 out.sql
7308 );
7309 }
7310 }
7311
7312 #[test]
7313 fn test_exists_of_a_binding_asks_for_any_row() {
7314 let out = compile_and_emit("WITH people := (SELECT Person) SELECT exists people");
7315 assert!(out.sql.contains("EXISTS(SELECT 1 FROM \"people\")"), "{}", out.sql);
7316
7317 let out = compile_and_emit("WITH names := {'a', 'b'} SELECT exists names");
7318 assert!(
7319 out.sql
7320 .contains("EXISTS(SELECT 1 FROM \"names\" WHERE \"v\" IS NOT NULL)"),
7321 "{}",
7322 out.sql
7323 );
7324 }
7325
7326 #[test]
7327 fn test_a_conditional_insert_beside_a_read_branch_unions_both() {
7328 let out = compile_and_emit(
7329 "WITH existing := (SELECT Person FILTER .name = $n LIMIT 1) \
7330 SELECT (existing IF EXISTS existing ELSE (INSERT Person { name := $n })) { name }",
7331 );
7332 assert!(out.sql.contains("UNION ALL"), "{}", out.sql);
7333 assert!(
7334 out.sql.contains("INSERT INTO") && out.sql.contains("WHERE (NOT EXISTS(SELECT 1 FROM \"existing\"))"),
7335 "the insert carries the condition itself:\n{}",
7336 out.sql
7337 );
7338 }
7339
7340 fn make_schema_with_a_plain_abstract() -> SchemaDescriptor {
7345 let mut schema = make_schema();
7346 let mut archivable = schema
7347 .types
7348 .iter()
7349 .find(|t| t.name == "Person")
7350 .expect("Person is in the test schema")
7351 .clone();
7352 archivable.name = "Archivable".into();
7353 archivable.table = "Archivable".into();
7354 archivable.abstract_ = true;
7355 archivable.materialized = false;
7356 archivable.links = vec![];
7357 archivable.multilinks = vec![];
7358 schema.types.push(archivable);
7359 let person = schema
7360 .types
7361 .iter_mut()
7362 .find(|t| t.name == "Person")
7363 .expect("just found");
7364 person.parents.push("default::Archivable".into());
7365 schema
7366 }
7367
7368 #[test]
7369 fn test_an_intersection_with_a_plain_abstract_fans_out() {
7370 let schema = make_schema_with_a_plain_abstract();
7371 let out = compile_and_emit_with("SELECT Post { who := [is default::Archivable].name }", &schema);
7372 assert!(
7373 !out.sql.contains("\"Archivable\""),
7374 "a plain abstract backs no relation:\n{}",
7375 out.sql
7376 );
7377 assert!(
7378 out.sql.contains("\"public\".\"Person\""),
7379 "read from the type that carries the column:\n{}",
7380 out.sql
7381 );
7382 }
7383
7384 fn make_schema_with_a_linking_plain_abstract() -> SchemaDescriptor {
7388 let mut schema = make_schema_with_a_plain_abstract();
7389 let person_links = schema
7390 .types
7391 .iter()
7392 .find(|t| t.name == "Person")
7393 .expect("Person is in the test schema")
7394 .links
7395 .clone();
7396 let archivable = schema
7397 .types
7398 .iter_mut()
7399 .find(|t| t.name == "Archivable")
7400 .expect("just added");
7401 archivable.links = person_links;
7402 schema
7403 }
7404
7405 #[test]
7406 fn test_a_plain_abstract_in_a_subquery_fans_out() {
7407 let schema = make_schema_with_a_plain_abstract();
7408 let out = compile_and_emit_with(
7409 "SELECT Person FILTER .id IN (SELECT default::Archivable FILTER .age > 1).id",
7410 &schema,
7411 );
7412 assert!(
7413 !out.sql.contains("\"Archivable\""),
7414 "a plain abstract backs no relation:\n{}",
7415 out.sql
7416 );
7417 }
7418
7419 #[test]
7420 fn test_a_backlink_narrowed_to_a_plain_abstract_fans_out() {
7421 let schema = make_schema_with_a_linking_plain_abstract();
7422 let out = compile_and_emit_with(
7423 "SELECT Company FILTER .<company[is default::Archivable].name = 'a'",
7424 &schema,
7425 );
7426 assert!(
7427 !out.sql.contains("\"Archivable\""),
7428 "a plain abstract backs no relation:\n{}",
7429 out.sql
7430 );
7431 }
7432
7433 fn make_interface_schema_with_a_plain_abstract() -> SchemaDescriptor {
7437 let mut schema = make_interface_schema();
7438 let mut archivable = schema
7439 .types
7440 .iter()
7441 .find(|t| t.name == "Individual")
7442 .expect("Individual is in the interface schema")
7443 .clone();
7444 archivable.name = "Archivable".into();
7445 archivable.table = "Archivable".into();
7446 archivable.abstract_ = true;
7447 archivable.materialized = false;
7448 archivable.interfaces = vec![];
7449 archivable.links = vec![];
7450 archivable.multilinks = vec![];
7451 archivable.computed = vec![];
7452 schema.types.push(archivable);
7453 for t in schema.types.iter_mut() {
7454 match t.name.as_str() {
7455 "Account" => t.materialized = true,
7457 "Individual" => t.parents.push("default::Archivable".into()),
7458 _ => {}
7459 }
7460 }
7461 schema
7462 }
7463
7464 #[test]
7465 fn test_a_splat_over_a_plain_abstract_fans_out() {
7466 let schema = make_interface_schema_with_a_plain_abstract();
7467 let out = compile_and_emit_with("SELECT Account { id, [is default::Archivable].* }", &schema);
7468 assert!(
7469 !out.sql.contains("\"Archivable\""),
7470 "a plain abstract backs no relation:\n{}",
7471 out.sql
7472 );
7473 assert!(
7474 out.sql.contains("first_name"),
7475 "the splat still reaches the mixin's own pointers:\n{}",
7476 out.sql
7477 );
7478 }
7479
7480 #[test]
7481 fn test_an_aggregate_over_a_relative_walk_runs_inside_the_subquery() {
7482 let out = compile_and_emit("SELECT Person { latest := max(.posts.title) }");
7485 assert!(out.sql.contains("(SELECT max("), "{}", out.sql);
7486 assert!(!out.sql.contains("max(ARRAY("), "{}", out.sql);
7487 }
7488
7489 fn make_schema_with_a_computed_backlink() -> SchemaDescriptor {
7492 use crate::schema::ComputedDescriptor;
7493 let mut schema = make_schema();
7494 let company = schema
7495 .types
7496 .iter_mut()
7497 .find(|t| t.name == "Company")
7498 .expect("Company is in the test schema");
7499 company.computed = vec![ComputedDescriptor {
7500 name: "staff".into(),
7501 expression: ".<company[is default::Person]".into(),
7502 return_type: None,
7503 link_target: None,
7504 link_multi: false,
7505 }];
7506 schema
7507 }
7508
7509 #[test]
7510 fn test_a_computed_backlink_reads_the_objects_it_lands_on() {
7511 let schema = make_schema_with_a_computed_backlink();
7512 let out = compile_and_emit_with("SELECT Company { staff: { name } }", &schema);
7513 assert!(
7514 out.sql.contains("\"public\".\"Person\""),
7515 "the walk reaches Person:\n{}",
7516 out.sql
7517 );
7518 }
7519
7520 #[test]
7521 fn test_a_computed_backlinks_modifiers_belong_to_what_it_lands_on() {
7522 let schema = make_schema_with_a_computed_backlink();
7525 let out = compile_and_emit_with("SELECT Company.staff { name } FILTER .name = 'ada'", &schema);
7526 assert!(out.sql.contains("'ada'"), "{}", out.sql);
7527 assert!(
7528 out.sql.contains("\"public\".\"Person\""),
7529 "filtered on the Person the walk lands on:\n{}",
7530 out.sql
7531 );
7532 }
7533
7534 #[test]
7535 fn test_an_unreserved_keyword_names_a_free_shape_field() {
7536 let out = compile_and_emit("SELECT { last := 2, first := 1, order := 3 }");
7539 assert!(out.sql.contains("\"last\""), "{}", out.sql);
7540 assert!(out.sql.contains("\"first\""), "{}", out.sql);
7541 assert!(out.sql.contains("\"order\""), "{}", out.sql);
7542 }
7543
7544 #[test]
7545 fn test_a_shaped_sub_select_over_a_type_is_an_object() {
7546 let out = compile_and_emit("SELECT Person { latest := (SELECT Post { title } LIMIT 1) }");
7549 assert!(out.sql.contains("\"title\""), "{}", out.sql);
7550 }
7551
7552 #[test]
7553 fn test_a_walk_inside_a_path_selects_shape_finds_its_row() {
7554 let out = compile_and_emit("SELECT Person.posts { mine := (SELECT Post FILTER .title = .title LIMIT 1) }");
7557 assert!(out.sql.contains("\"public\".\"Post\""), "{}", out.sql);
7558 }
7559
7560 #[test]
7561 fn test_a_walk_off_a_binding_in_a_free_select() {
7562 let out = compile_and_emit("WITH p := (SELECT Person) SELECT { n := p.company.name }");
7565 assert!(out.sql.contains("\"p\""), "{}", out.sql);
7566 assert!(out.sql.contains("\"public\".\"Company\""), "{}", out.sql);
7567 }
7568
7569 #[test]
7570 fn test_a_free_selects_filter_can_root_at_a_binding() {
7571 let out = compile_and_emit("WITH p := (SELECT Person) SELECT { n := 1 } FILTER p.company.name = 'Acme'");
7572 assert!(out.sql.contains("'Acme'"), "{}", out.sql);
7573 }
7574
7575 #[test]
7576 fn test_a_trailing_shape_on_a_field_access_select() {
7577 let out = compile_and_emit("SELECT (SELECT Person).company { name }");
7578 assert!(out.sql.contains("\"name\""), "{}", out.sql);
7579 assert!(out.sql.contains("\"public\".\"Company\""), "{}", out.sql);
7580 }
7581
7582 #[test]
7583 fn test_a_union_of_walks_names_each_branch() {
7584 let out = compile_and_emit(
7587 "WITH p := (SELECT Person FILTER .name = $a), q := (SELECT Person FILTER .name = $b), \
7588 t := (SELECT p.posts UNION q.posts) SELECT t { title }",
7589 );
7590 assert!(out.sql.contains("UNION ALL"), "{}", out.sql);
7591 assert!(out.sql.contains("\"title\""), "{}", out.sql);
7592 }
7593
7594 #[test]
7595 fn test_sibling_for_loops_each_get_their_own_iterator() {
7596 let out = compile_and_emit(
7599 "WITH a := (SELECT Post), b := (SELECT Post), \
7600 x := (FOR p IN a UNION (INSERT Person { name := p.title, age := 1 })), \
7601 y := (FOR p IN b UNION (INSERT Person { name := p.title, age := 2 })) \
7602 SELECT x",
7603 );
7604 assert!(out.sql.contains("\"_for_p\" AS"), "{}", out.sql);
7605 assert!(out.sql.contains("\"_for_p__1\" AS"), "{}", out.sql);
7606 }
7607
7608 #[test]
7609 fn test_a_nested_for_carries_the_outer_loops_key() {
7610 let out = compile_and_emit(
7613 "WITH made := (FOR p IN (SELECT Person) UNION ( \
7614 FOR q IN p.posts UNION (INSERT Company { name := q.title }) \
7615 )) SELECT count(made)",
7616 );
7617 assert!(
7618 out.sql.contains("\"_outer\""),
7619 "the inner iterator carries the bond:\n{}",
7620 out.sql
7621 );
7622 assert!(
7623 out.sql.contains("JOIN \"_for_p\" ON"),
7624 "the insert reads both loop variables:\n{}",
7625 out.sql
7626 );
7627 assert!(
7628 !out.sql.contains("LATERAL (\nINSERT"),
7629 "DML cannot sit in a LATERAL:\n{}",
7630 out.sql
7631 );
7632 }
7633
7634 #[test]
7635 fn test_a_link_value_can_be_a_walk_off_a_binding() {
7636 let mut schema = make_schema();
7639 let company = schema
7640 .types
7641 .iter_mut()
7642 .find(|t| t.name == "Company")
7643 .expect("make_schema declares Company");
7644 company.properties.push(PropertyDescriptor {
7645 name: "id".into(),
7646 pg_type: "uuid".into(),
7647 nullable: false,
7648 default_sql: None,
7649 default_pyql: None,
7650 description: None,
7651 check_constraints: vec![],
7652 is_exclusive: true,
7653 is_pk: true,
7654 is_readonly: true,
7655 rewrites: vec![],
7656 tuple_members: None,
7657 column_type: None,
7658 });
7659 let out = compile_and_emit_with(
7660 "WITH others := (SELECT Person) \
7661 SELECT (INSERT Person { name := $n, age := 1, company := (SELECT others.company LIMIT 1) })",
7662 &schema,
7663 );
7664 assert!(out.sql.contains("\"company_id\""), "{}", out.sql);
7665 }
7666
7667 #[test]
7668 fn test_a_shape_over_a_coalesce_of_walks() {
7669 let schema = make_schema_with_computed_links();
7672 let out = compile_and_emit_with("SELECT Person { x := (.published ?? .plain) { title } }", &schema);
7673 assert!(out.sql.contains("UNION ALL"), "{}", out.sql);
7674 assert!(
7675 out.sql.contains("NOT EXISTS"),
7676 "the later branch only stands in when the first is empty:\n{}",
7677 out.sql
7678 );
7679 }
7680
7681 #[test]
7682 fn test_an_aggregate_over_a_walk_onto_objects_counts_its_rows() {
7683 let out = compile_and_emit("WITH c := (SELECT Company) SELECT c { n := count(.<company[is default::Person]) }");
7687 assert!(out.sql.contains("count("), "{}", out.sql);
7688 assert!(out.sql.contains("\"public\".\"Person\""), "{}", out.sql);
7689 }
7690
7691 #[test]
7692 fn an_aggregate_over_a_computed_set_counts_inside_a_subquery() {
7693 let schema = make_schema_with_computed_links();
7699 let out = compile_and_emit_with("SELECT Person { n := count(.plain) }", &schema);
7700 assert!(
7701 !out.sql.contains("count((SELECT"),
7702 "the aggregate belongs inside the walk's subquery, not around it:\n{}",
7703 out.sql
7704 );
7705 assert!(
7706 out.sql.contains("(SELECT count(") && out.sql.contains("\"public\".\"Post\""),
7707 "it counts the rows the computed's own path lands on:\n{}",
7708 out.sql
7709 );
7710 }
7711
7712 #[test]
7715 fn an_aggregate_over_a_stored_multi_link_still_counts_its_junction_rows() {
7716 let schema = make_schema_with_computed_links();
7717 let out = compile_and_emit_with("SELECT Person { n := count(.posts) }", &schema);
7718 assert!(out.sql.contains("\"public\".\"Person.posts\""), "{}", out.sql);
7719 assert!(
7720 !out.sql.contains("JOIN \"public\".\"Post\""),
7721 "counting junction rows needs no join onto the targets:\n{}",
7722 out.sql
7723 );
7724 }
7725
7726 #[test]
7727 fn test_a_select_can_name_its_own_result() {
7728 let out = compile_and_emit("SELECT oldest := max(Person.age)");
7731 assert!(out.sql.contains("max("), "{}", out.sql);
7732 let filtered = compile_and_emit("SELECT n := Person.age FILTER n > 18");
7733 assert!(
7734 filtered.sql.contains("18"),
7735 "the alias resolves to the result:\n{}",
7736 filtered.sql
7737 );
7738 }
7739
7740 #[test]
7741 fn test_exists_sees_a_computed_pointer() {
7742 let schema = make_schema_with_computed_links();
7746 let out = compile_and_emit_with("SELECT Person FILTER EXISTS .published", &schema);
7747 assert!(out.sql.contains("EXISTS"), "{}", out.sql);
7748 assert!(
7749 out.sql.contains("\"public\".\"Post\""),
7750 "the computed's own path:\n{}",
7751 out.sql
7752 );
7753 }
7754
7755 #[test]
7756 fn test_walking_through_a_computed_field_access() {
7757 use crate::schema::ComputedDescriptor;
7758 let mut schema = make_schema_with_computed_links();
7761 let person = schema.types.iter_mut().find(|t| t.name == "Person").expect("Person");
7762 person.computed.push(ComputedDescriptor {
7763 name: "latest_title".into(),
7764 expression: "(select .posts order by .title desc limit 1).title".into(),
7765 return_type: None,
7766 link_target: None,
7767 link_multi: false,
7768 });
7769 let out = compile_and_emit_with("SELECT Person { x := .latest_title }", &schema);
7770 assert!(out.sql.contains("\"title\""), "{}", out.sql);
7771 assert!(
7772 out.sql.contains("LIMIT"),
7773 "the computed's own limit survives:\n{}",
7774 out.sql
7775 );
7776 }
7777
7778 #[test]
7779 fn test_a_computed_field_access_onto_an_object_takes_a_shape() {
7780 use crate::schema::ComputedDescriptor;
7781 let mut schema = make_schema_with_computed_links();
7784 let post = schema.types.iter_mut().find(|t| t.name == "Post").expect("Post");
7785 post.links.push(LinkDescriptor {
7786 name: "author".into(),
7787 target: "default::Person".into(),
7788 nullable: true,
7789 through: None,
7790 description: None,
7791 default_pyql: None,
7792 is_exclusive: false,
7793 is_readonly: false,
7794 rewrites: vec![],
7795 on_delete: vec![],
7796 });
7797 let person = schema.types.iter_mut().find(|t| t.name == "Person").expect("Person");
7798 person.computed.retain(|c| c.name != "looper");
7799 person.computed.push(ComputedDescriptor {
7800 name: "latest_author".into(),
7801 expression: "((select .posts order by .title desc limit 1)).author".into(),
7802 return_type: None,
7803 link_target: None,
7804 link_multi: false,
7805 });
7806 let out = compile_and_emit_with("SELECT Person { latest_author: { name } }", &schema);
7807 assert!(out.sql.contains("\"name\""), "{}", out.sql);
7808 assert!(
7809 out.sql.contains("\"title\" DESC"),
7810 "the order stays on the posts:\n{}",
7811 out.sql
7812 );
7813 assert!(
7814 out.sql.contains("LIMIT"),
7815 "the computed's own limit survives:\n{}",
7816 out.sql
7817 );
7818 let splat = compile_and_emit_with("SELECT Person { * }", &schema);
7819 assert!(
7820 !splat.sql.contains("latest_author"),
7821 "`*` leaves links out:\n{}",
7822 splat.sql
7823 );
7824 }
7825
7826 #[test]
7827 fn test_a_narrowed_pointer_can_carry_a_shape() {
7828 let out = compile_and_emit("SELECT Company { x := 1, [is default::Person].posts: { title } }");
7832 assert!(out.sql.contains("\"title\""), "{}", out.sql);
7833 assert!(out.sql.contains("\"public\".\"Post\""), "{}", out.sql);
7834 }
7835
7836 #[test]
7837 fn test_select_type_name_as_a_path_step() {
7838 let out = compile_and_emit("SELECT Person.__type__");
7839 assert!(out.sql.contains("ROW('default::Person')"), "{}", out.sql);
7840 assert!(out.sql.contains("\"Person\" AS \"t0\""), "{}", out.sql);
7841 }
7842
7843 #[test]
7844 fn test_aggregate_over_a_with_bound_object_set() {
7845 let out = compile_and_emit("WITH people := (SELECT Person) SELECT max(people.age)");
7846 assert!(out.sql.contains("\"people\" AS ("));
7847 assert!(out.sql.contains("max("));
7848 assert!(!out.sql.contains("FROM \"default\".\"Person\" AS \"t1\""));
7850 }
7851
7852 #[test]
7853 fn test_select_union_of_two_object_bindings() {
7854 let out = compile_and_emit("WITH a := (SELECT Person LIMIT 1), b := (SELECT Person) SELECT (a UNION b)");
7855 assert!(out.sql.contains("SELECT * FROM \"a\""));
7856 assert!(out.sql.contains("UNION ALL"));
7857 assert!(out.sql.contains("SELECT * FROM \"b\""));
7858 }
7859
7860 #[test]
7861 fn test_select_union_of_different_object_types_is_rejected() {
7862 let schema = make_schema();
7863 let ast = parse::parse("WITH a := (SELECT Person), b := (SELECT Company) SELECT (a UNION b)").unwrap();
7864 let Err(err) = ir::compile(&ast, &schema) else {
7865 panic!("union of unrelated types must be rejected")
7866 };
7867 assert!(format!("{err}").contains("UNION"));
7868 }
7869
7870 #[test]
7871 fn test_for_union_as_a_function_body() {
7872 let schema = make_schema();
7873 let descriptor = FunctionDescriptor {
7874 name: "recent".into(),
7875 module: "default".into(),
7876 params: vec![],
7877 return_pg_type: "default::Person".into(),
7878 return_is_object: true,
7879 return_is_set: true,
7880 return_is_polymorphic: false,
7881 volatility: "stable".into(),
7882 body: "for n in {1, 2} union (select Person filter .age = n)".into(),
7883 };
7884 let ir = ir::compile_fn_body(&descriptor, &schema).expect("function body must compile");
7885 let body = emit_fn_body(&ir);
7886 assert!(body.contains("VALUES"), "{body}");
7887 assert!(body.contains("CROSS JOIN LATERAL"), "{body}");
7888 assert!(body.contains("\"_body\".*"), "{body}");
7889 }
7890
7891 #[test]
7892 fn test_with_binding_in_a_computed_reads_the_enclosing_object() {
7893 let out = compile_and_emit("SELECT Person { n := (WITH own := .name SELECT own) }");
7894 assert!(out.sql.contains("\"name\""), "{}", out.sql);
7895 }
7896
7897 #[test]
7898 fn test_subject_path_update_names_its_own_alias() {
7899 let mut schema = make_schema();
7900 let company = schema.types.iter_mut().find(|t| t.name == "Company").unwrap();
7901 company.properties.insert(
7902 0,
7903 PropertyDescriptor {
7904 name: "id".into(),
7905 pg_type: "uuid".into(),
7906 nullable: false,
7907 default_sql: Some("uuidv7()".into()),
7908 default_pyql: None,
7909 description: None,
7910 check_constraints: vec![],
7911 is_exclusive: true,
7912 is_pk: true,
7913 is_readonly: true,
7914 rewrites: vec![],
7915 tuple_members: None,
7916 column_type: None,
7917 },
7918 );
7919 let out = compile_and_emit_with("SELECT (UPDATE Person.company SET { name := 'x' }) { name }", &schema);
7923 assert!(out.sql.contains("\"t1\".\"id\" = ANY("), "{}", out.sql);
7924 }
7925
7926 #[test]
7927 fn test_an_update_past_a_from_returns_only_its_own_columns() {
7928 let out = compile_and_emit(
7932 "SELECT { (UPDATE Person FILTER .name = 'a' SET { company := (INSERT Company { name := 'c' }) }) }",
7933 );
7934 assert!(out.sql.contains("FROM \"_nested_dml_0\""), "{}", out.sql);
7935 assert!(
7936 out.sql.contains("RETURNING \"t0\".*"),
7937 "the update names its own target:\n{}",
7938 out.sql
7939 );
7940 }
7941
7942 #[test]
7943 fn test_nested_insert_in_a_mutation_written_as_a_free_set() {
7944 let out = compile_and_emit(
7945 "SELECT { (UPDATE Person FILTER .name = 'a' SET { company := (INSERT Company { name := 'c' }) }) }",
7946 );
7947 assert!(
7948 out.sql.contains("INSERT INTO \"public\".\"Company\""),
7949 "the hoisted insert is defined, not just referenced:\n{}",
7950 out.sql
7951 );
7952 }
7953
7954 #[test]
7955 fn test_nested_insert_as_a_link_value_in_an_update() {
7956 let out = compile_and_emit(
7960 "SELECT (UPDATE Person FILTER .name = 'a' SET { company := (INSERT Company { name := 'c' }) }) { name }",
7961 );
7962 assert!(out.sql.contains("INSERT INTO \"public\".\"Company\""), "{}", out.sql);
7963 assert!(out.sql.contains("_nested_dml_0"), "{}", out.sql);
7964 assert!(
7965 out.sql.contains("FROM \"_nested_dml_0\""),
7966 "the update reads the hoisted CTE:\n{}",
7967 out.sql
7968 );
7969 }
7970
7971 #[test]
7972 fn test_nested_insert_as_a_link_value() {
7973 let out = compile_and_emit(
7974 "SELECT (INSERT Person { name := 'a', company := (INSERT Company { name := 'c' }) }) { name }",
7975 );
7976 assert!(out.sql.contains("_nested_dml_0"), "{}", out.sql);
7977 assert!(out.sql.contains("INSERT INTO \"public\".\"Company\""), "{}", out.sql);
7978 }
7979
7980 #[test]
7981 fn test_nested_insert_in_a_one_element_set_is_that_insert() {
7982 let out = compile_and_emit(
7983 "SELECT (INSERT Person { name := 'a', company := { (INSERT Company { name := 'c' }) } }) { name }",
7984 );
7985 assert!(out.sql.contains("INSERT INTO \"public\".\"Company\""), "{}", out.sql);
7986 }
7987
7988 #[test]
7989 fn test_insert_with_no_assignments_uses_default_values() {
7990 let out = compile_and_emit("SELECT (INSERT Person { name := 'a', company := (INSERT Company {}) }) { name }");
7991 assert!(out.sql.contains("DEFAULT VALUES"), "{}", out.sql);
7992 }
7993
7994 #[test]
7995 fn test_backlink_narrowed_to_an_interface_its_implementor_declares() {
7996 use crate::schema::{LinkDescriptor, TypeDescriptor};
7999 let mut schema = make_interface_schema();
8000 let individual = schema.types.iter().find(|t| t.name == "Individual").unwrap().clone();
8001 schema.types.push(TypeDescriptor {
8002 name: "Note".into(),
8003 module: "default".into(),
8004 table: "Note".into(),
8005 abstract_: false,
8006 materialized: true,
8007 description: None,
8008 parents: vec![],
8009 interfaces: vec![],
8010 bases: vec![],
8011 properties: individual.properties[..1].to_vec(),
8012 links: vec![],
8013 multilinks: vec![],
8014 computed: vec![],
8015 constraints: vec![],
8016 indexes: vec![],
8017 partition: None,
8018 vector_indexes: vec![],
8019 search_indexes: vec![],
8020 triggers: vec![],
8021 junction: false,
8022 signals: vec![],
8023 });
8024 schema
8025 .types
8026 .iter_mut()
8027 .find(|t| t.name == "Individual")
8028 .unwrap()
8029 .links
8030 .push(LinkDescriptor {
8031 name: "note".into(),
8032 target: "default::Note".into(),
8033 nullable: true,
8034 description: None,
8035 default_pyql: None,
8036 is_exclusive: false,
8037 is_readonly: false,
8038 rewrites: vec![],
8039 on_delete: vec![],
8040 through: None,
8041 });
8042 let out = compile_and_emit_with("SELECT Note FILTER EXISTS .<note[is Account]", &schema);
8043 assert!(out.sql.contains("\"Individual\""), "{}", out.sql);
8044 }
8045
8046 #[test]
8047 fn test_subject_reads_the_row_a_constraint_checks() {
8048 let schema = make_schema();
8049 let sql = ir::compile_constraint_expr("__subject__.age > 18", "default::Person", &schema)
8050 .expect("__subject__ names the row under check");
8051 assert!(sql.contains("\"age\""), "{sql}");
8052 }
8053
8054 #[test]
8055 fn test_free_select_filter_gates_the_result_and_warns() {
8056 let schema = make_schema();
8057 let ast = parse::parse("SELECT count(Person) FILTER (Person.age > 18)").unwrap();
8058 let ir = ir::compile(&ast, &schema).unwrap();
8059 let out = emit(&ir);
8060 assert!(
8061 out.sql.contains("EXISTS("),
8062 "the condition gates the count:\n{}",
8063 out.sql
8064 );
8065 assert!(
8066 ir.warnings.iter().any(|w| w.contains("FILTER clause")),
8067 "a set-valued filter warns: {:?}",
8068 ir.warnings
8069 );
8070 }
8071
8072 #[test]
8073 fn test_free_sub_select_with_modifiers_reads_inline() {
8074 let out = compile_and_emit(
8077 "SELECT Person { n := (WITH mine := .name SELECT count(Post) FILTER (Post.title = mine)) }",
8078 );
8079 assert!(out.sql.contains("_scalar_sub"), "{}", out.sql);
8080 assert!(!out.sql.contains("WITH"), "nothing is hoisted:\n{}", out.sql);
8081 }
8082
8083 #[test]
8084 fn test_correlated_with_binding_is_not_hoisted() {
8085 let out = compile_and_emit("SELECT Person { n := (WITH own := .name SELECT own) }");
8086 assert!(
8087 !out.sql.contains("\"own\" AS ("),
8088 "the binding is inlined:\n{}",
8089 out.sql
8090 );
8091 assert!(out.sql.contains("\"name\""), "{}", out.sql);
8092 }
8093
8094 #[test]
8095 fn test_array_literal_binding_is_readable_and_typed() {
8096 let out = compile_and_emit("WITH order := ['a', 'b'] SELECT std::find(order, 'b')");
8097 assert!(out.sql.contains("array_position"), "its type is known:\n{}", out.sql);
8098 assert!(
8099 out.sql.contains("AS v"),
8100 "the binding exposes a value column:\n{}",
8101 out.sql
8102 );
8103 }
8104
8105 #[test]
8106 fn test_any_over_a_single_boolean_is_that_boolean() {
8107 let out = compile_and_emit("SELECT Person FILTER std::any(.age > 18)");
8108 assert!(!out.sql.contains("bool_or"), "no aggregate in WHERE:\n{}", out.sql);
8109 assert!(out.sql.contains("> 18"), "{}", out.sql);
8110 }
8111
8112 #[test]
8113 fn test_comparing_a_value_to_a_set_is_membership() {
8114 let schema = make_schema_with_computed_links();
8115 let out = compile_and_emit_with("SELECT Person FILTER .name = Person.posts.title", &schema);
8116 assert!(out.sql.contains("= ANY("), "{}", out.sql);
8117 }
8118
8119 #[test]
8120 fn test_for_over_a_with_binding_iterates_every_row() {
8121 let out = compile_and_emit(
8122 "WITH names := (SELECT Person.name) FOR n IN names UNION (SELECT Person FILTER .name = n)",
8123 );
8124 assert!(out.sql.contains("FROM \"names\""), "{}", out.sql);
8125 assert!(!out.sql.contains("VALUES"), "the whole set is iterated:\n{}", out.sql);
8126 }
8127
8128 #[test]
8129 fn test_for_body_bindings_stay_inside_the_body() {
8130 let out = compile_and_emit(
8133 "WITH names := (SELECT Person.name) \
8134 FOR n IN names UNION (WITH matching := (SELECT Person FILTER .name = n) SELECT matching)",
8135 );
8136 let lateral = out.sql.find("CROSS JOIN LATERAL").expect("a lateral body");
8137 let binding = out.sql.find("\"matching\" AS (").expect("the body's binding");
8138 assert!(binding > lateral, "the binding sits inside the body:\n{}", out.sql);
8139 }
8140
8141 #[test]
8142 fn test_array_valued_binding_picks_the_array_overload() {
8143 let out = compile_and_emit("WITH l := <array<std::str>>$0 SELECT std::find(l, 'a')");
8146 assert!(out.sql.contains("array_position"), "{}", out.sql);
8147 assert!(!out.sql.contains("strpos"), "{}", out.sql);
8148 }
8149
8150 #[test]
8151 fn test_aggregate_over_a_path_in_a_filter_is_a_subquery() {
8152 let out = compile_and_emit("SELECT Person FILTER .age = std::max(Person.age)");
8153 assert!(
8154 out.sql.contains("(SELECT max("),
8155 "an aggregate in WHERE needs its own query:\n{}",
8156 out.sql
8157 );
8158 }
8159
8160 #[test]
8161 fn test_distinct_over_a_single_value_is_that_value() {
8162 let out = compile_and_emit("SELECT Person { n := (DISTINCT .name) }");
8163 assert!(out.sql.contains("\"name\""), "{}", out.sql);
8164 assert!(!out.sql.contains("DISTINCT \""), "{}", out.sql);
8165 }
8166
8167 #[test]
8168 fn test_narrowing_an_interface_joins_the_implementor_table() {
8169 let mut schema = make_interface_schema();
8172 let account = schema.types.iter_mut().find(|t| t.name == "Account").unwrap();
8173 account.materialized = true;
8174 let out = compile_and_emit_with("SELECT Account[is Individual].first_name", &schema);
8175 assert!(
8176 out.sql.contains("JOIN \"public\".\"Individual\""),
8177 "the implementor's own table is joined:\n{}",
8178 out.sql
8179 );
8180 assert!(out.sql.contains("\"first_name\""), "{}", out.sql);
8181 }
8182
8183 #[test]
8184 fn test_sub_select_shape_declares_names_its_filter_and_projection_read() {
8185 let out = compile_and_emit("SELECT Person { n := (SELECT .posts { t := .title } FILTER .t = 'x' LIMIT 1).t }");
8186 assert!(out.sql.contains("'x'"), "{}", out.sql);
8187 assert!(out.sql.contains("\"title\""), "{}", out.sql);
8188 }
8189
8190 #[test]
8191 fn test_assert_over_an_object_set_returns_rows() {
8192 let out = compile_and_emit("SELECT std::assert_distinct((SELECT Person))");
8193 assert!(
8194 out.sql.contains("\"assert_distinct\"(ARRAY(SELECT \"id\""),
8195 "{}",
8196 out.sql
8197 );
8198 assert!(out.sql.contains("= ANY("), "{}", out.sql);
8199 assert!(out.sql.contains("\"Person\" AS \"t1\""), "{}", out.sql);
8200 assert!(!out.sql.contains("unnest("), "{}", out.sql);
8201 }
8202
8203 #[test]
8204 fn test_assert_over_a_for_union_reads_its_rows() {
8205 let out =
8206 compile_and_emit("SELECT std::assert_distinct((FOR n IN {1, 2} UNION (SELECT Person FILTER .age = n)))");
8207 assert!(out.sql.contains("CROSS JOIN LATERAL"), "{}", out.sql);
8208 assert!(out.sql.contains("= ANY("), "{}", out.sql);
8209 }
8210
8211 #[test]
8212 fn test_declared_computed_with_binding_reads_its_own_object() {
8213 use crate::schema::ComputedDescriptor;
8216 let schema = make_schema();
8217 let cd = ComputedDescriptor {
8218 name: "shouted".into(),
8219 expression: "(with own := .name select std::str_upper(own))".into(),
8220 return_type: None,
8221 link_target: None,
8222 link_multi: false,
8223 };
8224 let ir = ir::compile_computed_in_type(&cd, "default::Person", &schema)
8225 .expect("a computed may read the object it is declared on");
8226 assert!(ir.is_some());
8227 }
8228
8229 #[test]
8230 fn test_sub_select_modifiers_scope_to_its_own_subject() {
8231 let mut schema = make_schema();
8234 let post = schema
8235 .types
8236 .iter_mut()
8237 .find(|t| t.name == "Post")
8238 .expect("Post is in the test schema");
8239 post.links.push(LinkDescriptor {
8240 name: "author".into(),
8241 target: "default::Person".into(),
8242 nullable: true,
8243 description: None,
8244 default_pyql: None,
8245 is_exclusive: false,
8246 is_readonly: false,
8247 rewrites: vec![],
8248 on_delete: vec![],
8249 through: None,
8250 });
8251 let out = compile_and_emit_with(
8252 "SELECT Person { a := (SELECT .posts FILTER .title = 'x' LIMIT 1).author.name }",
8253 &schema,
8254 );
8255 assert!(out.sql.contains("'x'"), "{}", out.sql);
8256 }
8257
8258 #[test]
8259 fn test_free_select_set_literal() {
8260 let schema = make_schema();
8261 let ast = parse::parse("SELECT {1, 2, 3}").unwrap();
8262 let ir = ir::compile(&ast, &schema).unwrap();
8263 let out = emit(&ir);
8264 assert_eq!(out.sql.matches("UNION ALL").count(), 2);
8266 assert!(out.sql.contains("1 AS v"));
8267 assert!(out.sql.contains("2 AS v"));
8268 assert!(out.sql.contains("3 AS v"));
8269 assert!(out.sql.contains("ROW(v) AS result"));
8270 assert!(matches!(out.shape.root, crate::query::ShapeNode::Scalar { .. }));
8271 }
8272
8273 #[test]
8274 fn test_free_select_free_object() {
8275 let schema = make_schema();
8276 let ast = parse::parse("SELECT { foo := 'bar', n := 42 }").unwrap();
8277 let ir = ir::compile(&ast, &schema).unwrap();
8278 let out = emit(&ir);
8279 assert!(out.sql.contains("'bar'"));
8280 assert!(out.sql.contains("42"));
8281 assert!(out.sql.contains("AS result"));
8282 let crate::query::ShapeNode::Object {
8284 pointers, type_name, ..
8285 } = &out.shape.root
8286 else {
8287 panic!("expected Object shape")
8288 };
8289 assert!(type_name.is_none());
8290 assert_eq!(pointers.len(), 2);
8291 assert!(matches!(&pointers[0], crate::query::ShapeNode::Scalar { name, position: 0 } if name == "foo"));
8292 assert!(matches!(&pointers[1], crate::query::ShapeNode::Scalar { name, position: 1 } if name == "n"));
8293 }
8294
8295 #[test]
8296 fn test_free_select_object_with_enum_field_casts_to_text_and_tags_shape() {
8297 let mut schema = make_schema();
8305 schema.enums.push(crate::schema::EnumDescriptor {
8306 name: "Gender".into(),
8307 module: "default".into(),
8308 members: vec!["Male".into(), "Female".into()],
8309 });
8310 let out = compile_and_emit_with("select { gender := default::Gender.Male }", &schema);
8311 assert!(
8318 out.sql.contains("'Male'::\"public\".\"Gender\""),
8319 "expected the enum literal, got:\n{}",
8320 out.sql
8321 );
8322 assert!(
8323 out.sql.contains("ROW(\"_f0\"::text) AS result"),
8324 "expected the ROW composite to cast the enum field to text, got:\n{}",
8325 out.sql
8326 );
8327 let crate::query::ShapeNode::Object { pointers, .. } = &out.shape.root else {
8328 panic!("expected Object shape")
8329 };
8330 assert_eq!(pointers.len(), 1);
8331 assert!(
8332 matches!(
8333 &pointers[0],
8334 crate::query::ShapeNode::Enum { name, position: 0, enum_type }
8335 if name == "gender" && enum_type == "public::Gender"
8336 ),
8337 "expected Enum-tagged shape, got: {:?}",
8338 pointers[0]
8339 );
8340 }
8341
8342 #[test]
8343 fn test_free_select_bare_enum_literal_casts_to_text_inside_row() {
8344 let mut schema = make_schema();
8348 schema.enums.push(crate::schema::EnumDescriptor {
8349 name: "Gender".into(),
8350 module: "default".into(),
8351 members: vec!["Male".into(), "Female".into()],
8352 });
8353 let out = compile_and_emit_with("select default::Gender.Male", &schema);
8354 assert!(
8355 out.sql.contains("ROW(v::text) AS result"),
8356 "expected ROW(v::text), got:\n{}",
8357 out.sql
8358 );
8359 assert!(
8360 matches!(
8361 &out.shape.root,
8362 crate::query::ShapeNode::Enum { enum_type, .. } if enum_type == "public::Gender"
8363 ),
8364 "expected Enum-tagged shape, got: {:?}",
8365 out.shape.root
8366 );
8367 }
8368
8369 #[test]
8372 fn test_for_update_defaults_to_blocking() {
8373 let out = compile_and_emit("SELECT Person FOR UPDATE");
8374 assert!(out.sql.trim_end().ends_with("FOR UPDATE"), "got:\n{}", out.sql);
8375 }
8376
8377 #[test]
8378 fn test_for_update_skip_locked() {
8379 let out = compile_and_emit("SELECT Person FOR UPDATE SKIP LOCKED");
8380 assert!(
8381 out.sql.trim_end().ends_with("FOR UPDATE SKIP LOCKED"),
8382 "got:\n{}",
8383 out.sql
8384 );
8385 }
8386
8387 #[test]
8388 fn test_for_update_nowait() {
8389 let out = compile_and_emit("SELECT Person FOR UPDATE NOWAIT");
8390 assert!(out.sql.trim_end().ends_with("FOR UPDATE NOWAIT"), "got:\n{}", out.sql);
8391 }
8392
8393 #[test]
8394 fn test_for_no_key_update_skip_locked() {
8395 let out = compile_and_emit("SELECT Person FOR NO KEY UPDATE SKIP LOCKED");
8396 assert!(
8397 out.sql.trim_end().ends_with("FOR NO KEY UPDATE SKIP LOCKED"),
8398 "got:\n{}",
8399 out.sql
8400 );
8401 }
8402
8403 #[test]
8404 fn test_for_share() {
8405 let out = compile_and_emit("SELECT Person FOR SHARE");
8406 assert!(out.sql.trim_end().ends_with("FOR SHARE"), "got:\n{}", out.sql);
8407 }
8408
8409 #[test]
8410 fn test_for_key_share_nowait() {
8411 let out = compile_and_emit("SELECT Person FOR KEY SHARE NOWAIT");
8412 assert!(
8413 out.sql.trim_end().ends_with("FOR KEY SHARE NOWAIT"),
8414 "got:\n{}",
8415 out.sql
8416 );
8417 }
8418
8419 #[test]
8420 fn test_for_update_comes_after_order_by_limit_offset_in_emitted_sql() {
8421 let out = compile_and_emit("SELECT Person { name } ORDER BY .name OFFSET 1 LIMIT 5 FOR UPDATE SKIP LOCKED");
8424 let order_pos = out.sql.find("ORDER BY").unwrap();
8425 let offset_pos = out.sql.find("OFFSET").unwrap();
8426 let limit_pos = out.sql.find("LIMIT").unwrap();
8427 let for_pos = out.sql.find("FOR UPDATE").unwrap();
8428 assert!(
8429 order_pos < offset_pos && offset_pos < limit_pos && limit_pos < for_pos,
8430 "got:\n{}",
8431 out.sql
8432 );
8433 }
8434
8435 #[test]
8436 fn test_for_update_combined_with_distinct_is_rejected() {
8437 let ast = parse::parse("SELECT DISTINCT Person FOR UPDATE").expect("parse failed");
8438 let err = ir::compile(&ast, &make_schema())
8439 .err()
8440 .expect("expected a compile error");
8441 assert!(err.to_string().contains("DISTINCT"), "unexpected: {err}");
8442 }
8443
8444 #[test]
8445 fn test_for_update_combined_with_select_over_insert_is_rejected() {
8446 let ast = parse::parse("SELECT (INSERT Person { name := 'Alice' }) { name } FOR UPDATE").expect("parse failed");
8447 let err = ir::compile(&ast, &make_schema())
8448 .err()
8449 .expect("expected a compile error");
8450 assert!(err.to_string().contains("INSERT"), "unexpected: {err}");
8451 }
8452
8453 #[test]
8454 fn test_for_update_on_an_interface_type_is_rejected() {
8455 fn id_prop() -> PropertyDescriptor {
8456 PropertyDescriptor {
8457 name: "id".into(),
8458 pg_type: "uuid".into(),
8459 nullable: false,
8460 default_sql: Some("uuidv7()".into()),
8461 default_pyql: None,
8462 description: None,
8463 check_constraints: vec![],
8464 is_exclusive: true,
8465 is_pk: true,
8466 is_readonly: true,
8467 rewrites: vec![],
8468 tuple_members: None,
8469 column_type: None,
8470 }
8471 }
8472 let schema = SchemaDescriptor {
8473 types: vec![
8474 TypeDescriptor {
8475 name: "Account".into(),
8476 module: "default".into(),
8477 table: "Account".into(),
8478 abstract_: true,
8479 materialized: true,
8480 description: None,
8481 parents: vec![],
8482 interfaces: vec![],
8483 bases: vec![],
8484 properties: vec![id_prop()],
8485 links: vec![],
8486 multilinks: vec![],
8487 computed: vec![],
8488 constraints: vec![],
8489 indexes: vec![],
8490 partition: None,
8491 vector_indexes: vec![],
8492 search_indexes: vec![],
8493 triggers: vec![],
8494 junction: false,
8495 signals: vec![],
8496 },
8497 TypeDescriptor {
8498 name: "Individual".into(),
8499 module: "default".into(),
8500 table: "Individual".into(),
8501 abstract_: false,
8502 materialized: true,
8503 description: None,
8504 parents: vec![],
8505 interfaces: vec!["default::Account".into()],
8506 bases: vec![],
8507 properties: vec![id_prop()],
8508 links: vec![],
8509 multilinks: vec![],
8510 computed: vec![],
8511 constraints: vec![],
8512 indexes: vec![],
8513 partition: None,
8514 vector_indexes: vec![],
8515 search_indexes: vec![],
8516 triggers: vec![],
8517 junction: false,
8518 signals: vec![],
8519 },
8520 ],
8521 scalars: vec![],
8522 enums: vec![],
8523 named_tuples: vec![],
8524 globals: vec![],
8525 functions: vec![],
8526 aliases: vec![],
8527 channels: vec![],
8528 ..Default::default()
8529 };
8530 let ast = parse::parse("SELECT Account FOR UPDATE").expect("parse failed");
8531 let err = ir::compile(&ast, &schema).err().expect("expected a compile error");
8532 assert!(err.to_string().contains("interface"), "unexpected: {err}");
8533 }
8534
8535 #[test]
8536 fn test_schema_select_distinct_emits_distinct_keyword() {
8537 let out = compile_and_emit("SELECT DISTINCT Person { name }");
8542 assert!(out.sql.contains("DISTINCT"), "expected DISTINCT in SQL:\n{}", out.sql);
8543 }
8544
8545 #[test]
8546 fn test_schema_select_detached_compiles_as_ordinary_select() {
8547 let out = compile_and_emit("SELECT DETACHED Person { name }");
8551 assert!(
8552 out.sql.contains("\"name\""),
8553 "expected name column in SQL:\n{}",
8554 out.sql
8555 );
8556 }
8557
8558 #[test]
8559 fn test_detached_subquery_correlates_against_the_outer_row() {
8560 let out = compile_and_emit(
8567 "SELECT Person { name } FILTER NOT EXISTS (\
8568 SELECT DETACHED Person FILTER .name = Person.name AND .age > Person.age)",
8569 );
8570 assert!(
8571 out.sql.contains("\"t1\".\"name\" = \"t0\".\"name\""),
8572 "the inner row should be compared against the outer one:\n{}",
8573 out.sql
8574 );
8575 assert!(
8576 !out.sql.contains("\"t1\".\"name\" = \"t1\".\"name\""),
8577 "self-comparison means the correlation was lost:\n{}",
8578 out.sql
8579 );
8580 }
8581
8582 #[test]
8583 fn test_an_assert_on_a_pointer_checks_its_rows() {
8584 let out = compile_and_emit("SELECT Person { name, p := assert_exists(.posts { title }) }");
8588 assert!(
8589 out.sql
8590 .contains(r#"cardinality("_pylon"."assert_exists"("_a"."v"::text[]))"#),
8591 "the assert should read the aggregated rows:\n{}",
8592 out.sql
8593 );
8594 assert_eq!(
8595 out.sql.matches("array_agg(ROW(").count(),
8596 1,
8597 "the pointer's subquery should be evaluated once, not once per use:\n{}",
8598 out.sql
8599 );
8600 }
8601
8602 #[test]
8603 fn test_an_assert_on_a_scalar_stays_an_ordinary_call() {
8604 let out = compile_and_emit("SELECT Person { n := assert_exists(.name) }");
8607 assert!(
8608 !out.sql.contains(r#""_a"."v""#),
8609 "a scalar assert should not be wrapped as a pointer check:\n{}",
8610 out.sql
8611 );
8612 }
8613
8614 #[test]
8615 fn test_a_backlink_tail_walks_past_the_first_step() {
8616 let out = compile_and_emit("SELECT Post FILTER any(.<posts[is Person].posts.title = $t)");
8621 assert!(
8622 out.sql.contains(r#""public"."Person.posts""#),
8623 "the tail should walk the multi-link's junction:\n{}",
8624 out.sql
8625 );
8626 }
8627
8628 #[test]
8629 fn test_an_intersection_narrows_the_rest_of_a_multilink_walk() {
8630 let out = compile_and_emit("SELECT Person FILTER any(.posts[is Post].title = $t)");
8634 assert!(
8635 out.sql.contains(r#""public"."Post""#),
8636 "the walk should continue from the narrowed type:\n{}",
8637 out.sql
8638 );
8639 }
8640
8641 #[test]
8642 fn test_a_union_of_bindings_emits_selects_not_bare_names() {
8643 let out = compile_and_emit_with(
8647 "WITH a := (INSERT Post { title := $t1 }), b := (INSERT Post { title := $t2 }) \
8648 SELECT (INSERT Person { name := $n, posts := (a UNION b) })",
8649 &make_schema(),
8650 );
8651 assert!(
8652 out.sql.contains(r#"(SELECT "_s"."id" FROM "a" AS "_s")"#),
8653 "each union operand should be a select:\n{}",
8654 out.sql
8655 );
8656 }
8657
8658 #[test]
8659 fn test_a_bare_narrowing_reads_as_a_type_check_and_as_an_object() {
8660 for query in [
8664 "SELECT Person FILTER EXISTS [is Person]",
8665 "SELECT Person { p := [is Person] { name } }",
8666 ] {
8667 let ast = parse::parse(query).unwrap_or_else(|e| panic!("{query}: {e}"));
8668 assert!(ir::compile(&ast, &make_schema()).is_ok(), "{query} should compile");
8669 }
8670 }
8671
8672 #[test]
8673 fn test_a_multilink_takes_a_set_wrapped_in_a_select() {
8674 let out = compile_and_emit_with(
8679 "WITH a := (INSERT Post { title := $t1 }), b := (INSERT Post { title := $t2 }) \
8680 SELECT (INSERT Person { name := $n, posts := (SELECT { a, b }) })",
8681 &make_schema(),
8682 );
8683 assert!(
8684 out.sql.contains(r#""public"."Person.posts""#),
8685 "both targets should reach the junction:\n{}",
8686 out.sql
8687 );
8688 }
8689
8690 #[test]
8691 fn test_an_aggregate_over_a_subselected_path() {
8692 let out = compile_and_emit("SELECT array_agg((SELECT Person.name))");
8698 assert!(
8699 out.sql.contains("array_agg") && out.sql.contains(r#""public"."Person""#),
8700 "the aggregate should wrap the walk's column:\n{}",
8701 out.sql
8702 );
8703 }
8704
8705 #[test]
8706 fn test_a_distinct_aggregate_argument_reaches_the_aggregate() {
8707 for query in [
8713 "SELECT array_agg(DISTINCT Person.name)",
8714 "SELECT array_agg((SELECT DISTINCT Person.name))",
8715 ] {
8716 let out = compile_and_emit(query);
8717 assert!(
8718 out.sql.contains("array_agg(DISTINCT "),
8719 "{query} should deduplicate inside the aggregate:\n{}",
8720 out.sql
8721 );
8722 }
8723 }
8724
8725 #[test]
8726 fn test_is_not_negates_the_type_check() {
8727 let positive = compile_and_emit("SELECT Person FILTER Person IS Person");
8731 let negative = compile_and_emit("SELECT Person FILTER Person IS NOT Person");
8732 assert!(
8733 negative.sql.contains("NOT") && !positive.sql.contains("NOT"),
8734 "the negation should reach the SQL:\npositive:\n{}\nnegative:\n{}",
8735 positive.sql,
8736 negative.sql
8737 );
8738 }
8739
8740 #[test]
8741 fn test_a_cast_takes_a_prefix_operator_as_its_operand() {
8742 for query in ["SELECT <bool>EXISTS (SELECT Person LIMIT 1)", "SELECT <int64>-1"] {
8746 let ast = parse::parse(query).unwrap_or_else(|e| panic!("{query} should parse: {e}"));
8747 assert!(ir::compile(&ast, &make_schema()).is_ok(), "{query} should compile");
8748 }
8749 }
8750
8751 #[test]
8752 fn test_a_bare_multilink_reads_as_a_set() {
8753 let out =
8758 compile_and_emit("WITH p := (SELECT DETACHED Post LIMIT 1) SELECT Person { name } FILTER p IN .posts");
8759 assert!(
8760 out.sql.contains(r#""public"."Person.posts""#),
8761 "the membership test should read the link's junction:\n{}",
8762 out.sql
8763 );
8764 }
8765
8766 #[test]
8767 fn test_a_select_reads_its_own_shape_computed_in_its_clauses() {
8768 let out = compile_and_emit("SELECT Person { n := .name } ORDER BY .n ASC");
8772 assert!(
8773 out.sql.contains("ORDER BY"),
8774 "the order by should compile against the declared pointer:\n{}",
8775 out.sql
8776 );
8777 }
8778
8779 #[test]
8780 fn test_a_declared_pointer_can_be_walked_through() {
8781 let out = compile_and_emit_with(
8785 "WITH p := (SELECT Person { boss := .company } LIMIT 1) SELECT p { name, boss_name := .boss.name }",
8786 &make_schema(),
8787 );
8788 assert!(
8789 out.sql.contains(r#""public"."Company""#),
8790 "the walk should reach the declared pointer's own target table:\n{}",
8791 out.sql
8792 );
8793 }
8794
8795 #[test]
8796 fn test_a_replaced_subject_shape_stays_in_scope() {
8797 let out = compile_and_emit("SELECT Person { c := (SELECT .company { n := .name }) { n } }");
8802 assert!(
8803 out.sql.contains(r#"'default::Company'::text"#) && out.sql.contains(r#""t2"."name""#),
8804 "`n` should resolve to the company's own column:\n{}",
8805 out.sql
8806 );
8807 }
8808
8809 #[test]
8810 fn test_a_sibling_shape_pointer_is_not_in_scope() {
8811 let schema = make_schema();
8814 let ast = parse::parse("SELECT Person { n := .name, copy := .n }").unwrap();
8815 assert!(ir::compile(&ast, &schema).is_err());
8816 }
8817
8818 #[test]
8819 fn test_shape_over_a_with_binding_reads_the_binding() {
8820 let out = compile_and_emit("WITH c := (SELECT Company LIMIT 1) SELECT Person { name, employer := c { name } }");
8825 assert!(
8826 out.sql.contains("\"c\""),
8827 "the pointer should read the binding's CTE:\n{}",
8828 out.sql
8829 );
8830 }
8831
8832 #[test]
8833 fn test_shape_over_a_bare_type_name_is_still_rejected() {
8834 let schema = make_schema();
8837 let ast = parse::parse("SELECT Person { name, other := Company { name } }").unwrap();
8838 assert!(ir::compile(&ast, &schema).is_err());
8839 }
8840
8841 #[test]
8842 fn test_nested_shape_reads_a_prefix_bound_by_the_outer_select() {
8843 let out = compile_and_emit("SELECT Person { name, posts: { title, who := Person.name } }");
8848 assert!(
8849 out.sql.contains(r#""t0"."name""#),
8850 "the nested computed should read the outer Person alias:\n{}",
8851 out.sql
8852 );
8853 assert!(
8854 !out.sql.contains(r#"FROM "public"."Person" "t2""#),
8855 "the prefix should not open a second Person source:\n{}",
8856 out.sql
8857 );
8858 }
8859
8860 #[test]
8861 fn test_prefix_naming_a_type_out_of_scope_is_rejected() {
8862 let schema = make_schema();
8865 let ast = parse::parse("SELECT Person { name, who := Company.name }").unwrap();
8866 assert!(ir::compile(&ast, &schema).is_err());
8867 }
8868
8869 #[test]
8870 fn test_free_select_tuple() {
8871 let schema = make_schema();
8872 let ast = parse::parse("SELECT (1, 2)").unwrap();
8873 let ir = ir::compile(&ast, &schema).unwrap();
8874 let out = emit(&ir);
8875 assert!(out.sql.contains("1"));
8876 assert!(out.sql.contains("2"));
8877 assert!(out.sql.contains("AS result"));
8878 assert!(matches!(out.shape.root, crate::query::ShapeNode::Tuple { .. }));
8879 }
8880
8881 #[test]
8882 fn test_free_select_scalar_literal() {
8883 let schema = make_schema();
8884 let ast = parse::parse("SELECT 'hello'").unwrap();
8885 let ir = ir::compile(&ast, &schema).unwrap();
8886 let out = emit(&ir);
8887 assert!(out.sql.contains("SELECT 'hello' AS v"));
8888 assert!(out.sql.contains("ROW(v) AS result"));
8889 assert!(matches!(out.shape.root, crate::query::ShapeNode::Scalar { .. }));
8890 }
8891
8892 #[test]
8893 fn test_float_literal_casts_to_float8() {
8894 let out = compile_and_emit("SELECT 1.0");
8901 assert!(
8902 out.sql.contains("(1.0::float8)"),
8903 "expected explicit float8 cast, got:\n{}",
8904 out.sql
8905 );
8906 }
8907
8908 #[test]
8909 fn test_free_select_array_literal() {
8910 let schema = make_schema();
8911 let ast = parse::parse("SELECT [1, 2, 3]").unwrap();
8912 let ir = ir::compile(&ast, &schema).unwrap();
8913 let out = emit(&ir);
8914 assert!(out.sql.contains("SELECT ARRAY[1, 2, 3] AS v"), "{}", out.sql);
8915 assert!(out.sql.contains("SELECT v AS result, v FROM"), "{}", out.sql);
8916 assert!(matches!(out.shape.root, crate::query::ShapeNode::RawScalar));
8917 }
8918
8919 #[test]
8920 fn test_select_scalars() {
8921 let out = compile_and_emit("SELECT Person { name, age }");
8922 assert!(out.sql.contains("'default::Person'::text"));
8923 assert!(out.sql.contains("\"name\"::text"));
8924 assert!(out.sql.contains("\"age\"::int8"));
8925 assert!(out.sql.contains("FROM \"public\".\"Person\""));
8926 assert!(out.sql.contains(") AS result"));
8927 }
8928
8929 #[test]
8930 fn test_select_filter_param() {
8931 let out = compile_and_emit("SELECT Person { name } FILTER .name = $name");
8932 assert!(out.sql.contains("WHERE"));
8933 assert!(out.sql.contains("$1"));
8934 }
8935
8936 #[test]
8937 fn test_filter_in_set_literal_compiles_to_any_array() {
8938 let out = compile_and_emit("SELECT Person { name } FILTER .name IN {'Carol'}");
8939 assert!(out.sql.contains("= ANY(ARRAY['Carol'])"), "got:\n{}", out.sql);
8940 }
8941
8942 #[test]
8943 fn test_filter_in_multi_element_set_literal_compiles_to_any_array() {
8944 let out = compile_and_emit("SELECT Person { name } FILTER .name IN {'Carol', 'Bob'}");
8945 assert!(out.sql.contains("= ANY(ARRAY['Carol', 'Bob'])"), "got:\n{}", out.sql);
8946 }
8947
8948 #[test]
8955 fn test_filter_in_array_unpack_passes_the_array_straight_to_any() {
8956 let out = compile_and_emit("SELECT Person { name } FILTER .name IN std::array_unpack(<array<str>>$names)");
8957 assert!(
8958 !out.sql.contains("ANY(unnest("),
8959 "unnest inside ANY is invalid in WHERE:\n{}",
8960 out.sql
8961 );
8962 assert!(out.sql.contains("= ANY("), "got:\n{}", out.sql);
8963 }
8964
8965 #[test]
8966 fn test_filter_not_in_array_unpack_passes_the_array_straight_to_all() {
8967 let out = compile_and_emit("SELECT Person { name } FILTER .name NOT IN std::array_unpack(<array<str>>$names)");
8968 assert!(
8969 !out.sql.contains("ALL(unnest("),
8970 "unnest inside ALL is invalid in WHERE:\n{}",
8971 out.sql
8972 );
8973 assert!(out.sql.contains("<> ALL("), "got:\n{}", out.sql);
8974 }
8975
8976 #[test]
8981 fn test_all_over_unpacked_array_aggregates_the_array_not_the_multilink() {
8982 let out = compile_and_emit(
8983 "SELECT Person { name } FILTER all(std::array_unpack(<array<str>>$titles) IN .posts.title)",
8984 );
8985 assert!(out.sql.contains("bool_and("), "got:\n{}", out.sql);
8986 assert!(out.sql.contains("_unnested"), "got:\n{}", out.sql);
8987 }
8988
8989 #[test]
8993 fn test_any_over_a_multilink_against_an_unpacked_array_still_folds_into_any() {
8994 let out = compile_and_emit(
8995 "SELECT Person { name } FILTER any(.posts.title IN std::array_unpack(<array<str>>$titles))",
8996 );
8997 assert!(!out.sql.contains("unnest("), "got:\n{}", out.sql);
8998 assert!(out.sql.contains("= ANY("), "got:\n{}", out.sql);
8999 }
9000
9001 #[test]
9002 fn test_any_over_unpacked_array_aggregates_the_array_not_the_multilink() {
9003 let out = compile_and_emit(
9004 "SELECT Person { name } FILTER any(std::array_unpack(<array<str>>$titles) IN .posts.title)",
9005 );
9006 assert!(out.sql.contains("bool_or("), "got:\n{}", out.sql);
9007 assert!(out.sql.contains("_unnested"), "got:\n{}", out.sql);
9008 }
9009
9010 fn schema_with_array_property() -> SchemaDescriptor {
9012 let mut schema = make_schema();
9013 schema.types[0].properties.push(crate::schema::PropertyDescriptor {
9014 name: "perms".into(),
9015 pg_type: "text[]".into(),
9016 nullable: true,
9017 default_sql: None,
9018 default_pyql: None,
9019 description: None,
9020 check_constraints: vec![],
9021 is_exclusive: false,
9022 is_pk: false,
9023 is_readonly: false,
9024 rewrites: vec![],
9025 tuple_members: None,
9026 column_type: None,
9027 });
9028 schema
9029 }
9030
9031 #[test]
9037 fn test_aggregate_over_an_unpacked_walk_unnests_in_a_row_source() {
9038 let out = compile_and_emit_with(
9039 "WITH teams := (SELECT Person { perms } FILTER .age > 1) SELECT std::array_agg(std::array_unpack(teams.perms))",
9040 &schema_with_array_property(),
9041 );
9042 assert!(!out.sql.contains("array_agg(unnest("), "got:\n{}", out.sql);
9043 assert!(
9044 out.sql.contains(
9045 r#"coalesce(array_agg("_s"."v"), '{}') FROM unnest(ARRAY(SELECT unnest("t1"."perms") FROM "teams" AS "t1"))"#
9046 ),
9047 "got:\n{}",
9048 out.sql
9049 );
9050 }
9051
9052 #[test]
9053 fn test_aggregate_over_an_unpacked_type_rooted_path_unnests_in_a_row_source() {
9054 let out = compile_and_emit_with(
9055 "SELECT std::count(std::array_unpack(Person.perms))",
9056 &schema_with_array_property(),
9057 );
9058 assert!(!out.sql.contains("count(unnest("), "got:\n{}", out.sql);
9059 assert!(
9060 out.sql
9061 .contains(r#"FROM unnest(ARRAY(SELECT unnest("t0"."perms") FROM "public"."Person" AS "t0"))"#),
9062 "got:\n{}",
9063 out.sql
9064 );
9065 }
9066
9067 #[test]
9070 fn test_aggregate_over_an_unpacked_parameter_unnests_the_array_itself() {
9071 let out = compile_and_emit("SELECT std::array_agg(std::array_unpack(<array<str>>$names))");
9072 assert!(!out.sql.contains("array_agg(unnest("), "got:\n{}", out.sql);
9073 assert!(
9074 out.sql.contains(r#"FROM unnest(($1)::text[]) AS "_s"("v")"#),
9075 "got:\n{}",
9076 out.sql
9077 );
9078 }
9079
9080 #[test]
9081 fn test_aggregate_over_an_unpacked_property_unnests_that_row_s_array() {
9082 let out = compile_and_emit_with(
9083 "SELECT Person { n := std::array_agg(std::array_unpack(.perms)) }",
9084 &schema_with_array_property(),
9085 );
9086 assert!(!out.sql.contains("array_agg(unnest("), "got:\n{}", out.sql);
9087 assert!(
9088 out.sql.contains(r#"FROM unnest("t0"."perms") AS "_s"("v")"#),
9089 "got:\n{}",
9090 out.sql
9091 );
9092 }
9093
9094 #[test]
9097 fn test_array_agg_over_nothing_is_an_empty_array_not_null() {
9098 for query in [
9099 "SELECT std::array_agg(Person.name)",
9100 "SELECT std::array_agg((SELECT Person).name)",
9101 ] {
9102 let out = compile_and_emit(query);
9103 assert!(
9104 out.sql.contains("coalesce(") && out.sql.contains("'{}'"),
9105 "{query} got:\n{}",
9106 out.sql
9107 );
9108 }
9109 }
9110
9111 #[test]
9115 fn test_an_array_agg_binding_stays_an_array_through_its_coalesce() {
9116 let out = compile_and_emit(
9117 "WITH ids := std::array_agg(Person.id) SELECT Person { name } FILTER std::contains(ids, .id)",
9118 );
9119 assert!(
9120 out.sql.contains("@> ARRAY["),
9121 "expected the array overload, got:\n{}",
9122 out.sql
9123 );
9124 assert!(
9125 !out.sql.contains("strpos("),
9126 "the string overload must not win, got:\n{}",
9127 out.sql
9128 );
9129 }
9130
9131 #[test]
9134 fn test_aggregate_over_a_plain_walk_keeps_its_row_source() {
9135 let out = compile_and_emit("SELECT std::array_agg(Person.name)");
9136 assert!(
9137 out.sql.contains(r#"array_agg("t0"."name")"#) && out.sql.contains(r#"FROM "public"."Person""#),
9138 "got:\n{}",
9139 out.sql
9140 );
9141 }
9142
9143 #[test]
9146 fn test_the_cte_order_check_catches_a_forward_reference() {
9147 let bad = "WITH \"b\" AS (\n SELECT * FROM \"a\"\n),\n\"a\" AS (\n SELECT 1\n)\nSELECT 1";
9148 assert!(
9149 super::forward_cte_reference(bad).is_some(),
9150 "expected a forward reference"
9151 );
9152 let good = "WITH \"a\" AS (\n SELECT 1\n),\n\"b\" AS (\n SELECT * FROM \"a\"\n)\nSELECT 1";
9153 assert_eq!(super::forward_cte_reference(good), None, "ordered CTEs must pass");
9154 }
9155
9156 #[test]
9160 fn test_a_cast_unpacked_array_still_folds_into_any() {
9161 let out = compile_and_emit("SELECT Person { name } FILTER .name IN <str>std::array_unpack(<array<str>>$names)");
9162 assert!(
9163 !out.sql.contains("unnest("),
9164 "the unpack must fold into ANY, got:\n{}",
9165 out.sql
9166 );
9167 assert!(out.sql.contains("= ANY("), "got:\n{}", out.sql);
9168 }
9169
9170 #[test]
9173 fn test_array_unpack_outside_in_still_unnests() {
9174 let out = compile_and_emit("SELECT std::array_unpack(<array<str>>$names)");
9175 assert!(out.sql.contains("unnest("), "got:\n{}", out.sql);
9176 }
9177
9178 #[test]
9179 fn test_filter_not_in_set_literal_compiles_to_all_array() {
9180 let out = compile_and_emit("SELECT Person { name } FILTER .name NOT IN {'Carol'}");
9181 assert!(out.sql.contains("<> ALL(ARRAY['Carol'])"), "got:\n{}", out.sql);
9182 }
9183
9184 #[test]
9185 fn test_deep_splat_fetches_single_link_target_properties_not_just_id() {
9186 let out = compile_and_emit("SELECT Person { ** }");
9191 assert!(
9192 out.sql.contains("\"name\""),
9193 "expected Company.name pulled in via .company's ** expansion, got:\n{}",
9194 out.sql
9195 );
9196 }
9197
9198 #[test]
9199 fn test_deep_splat_fetches_multilink_target_properties_not_just_id() {
9200 let out = compile_and_emit("SELECT Person { ** }");
9201 assert!(
9202 out.sql.contains("\"title\""),
9203 "expected Post.title pulled in via .posts' ** expansion, got:\n{}",
9204 out.sql
9205 );
9206 }
9207
9208 #[test]
9209 fn test_schema_type_cast_select() {
9210 let out = compile_and_emit("SELECT <default::Person><uuid>'019ef1bb-0d42-7a9f-8f6b-b38d028a49ba'");
9211 assert!(out.sql.contains("FROM \"public\".\"Person\""));
9212 assert!(out.sql.contains("WHERE"));
9213 assert!(out.sql.contains("'019ef1bb-0d42-7a9f-8f6b-b38d028a49ba'"));
9214 }
9215
9216 #[test]
9217 fn test_select_single_link() {
9218 let out = compile_and_emit("SELECT Person { name, company { name } }");
9219 assert!(out.sql.contains("'default::Company'::text"));
9220 assert!(out.sql.contains("FROM \"public\".\"Company\""));
9221 assert!(out.sql.contains("\"company_id\" = "));
9223 }
9224
9225 #[test]
9226 fn test_select_multi_link() {
9227 let out = compile_and_emit("SELECT Person { name, posts { title } }");
9228 assert!(out.sql.contains("array_agg(ROW("));
9229 assert!(out.sql.contains("ARRAY[]::record[]"));
9230 assert!(out.sql.contains("'default::Post'::text"));
9231 assert!(out.sql.contains("\"Person.posts\""));
9232 }
9233
9234 fn compile_err(query: &str) -> String {
9235 let schema = make_schema();
9236 let ast = parse::parse(query).expect("parse failed");
9237 match ir::compile(&ast, &schema) {
9238 Ok(_) => panic!("expected a compile error"),
9239 Err(e) => format!("{e}"),
9240 }
9241 }
9242
9243 #[test]
9244 fn test_multi_link_limit_cuts_rows_before_aggregating() {
9245 let out = compile_and_emit("SELECT Person { posts: { title } filter .title = 'x' limit 1 }");
9246 assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
9249 assert!(out.sql.contains("AS \"__lim\""), "{}", out.sql);
9250 let agg = out.sql.find("array_agg").unwrap();
9251 let limit = out.sql.find("LIMIT 1").unwrap();
9252 assert!(agg < limit, "{}", out.sql);
9253 assert!(out.sql.contains("\"t1\".\"title\"::text AS \"c1\""), "{}", out.sql);
9254 }
9255
9256 #[test]
9257 fn test_multi_link_order_by_with_limit_orders_the_aggregate_too() {
9258 let out = compile_and_emit("SELECT Person { posts: { title } order by .title desc offset 1 limit 2 }");
9259 assert!(out.sql.contains("\"t1\".\"title\" AS \"s0\""), "{}", out.sql);
9262 assert!(
9263 out.sql.contains("ORDER BY \"__lim\".\"s0\" DESC NULLS LAST"),
9264 "{}",
9265 out.sql
9266 );
9267 assert!(out.sql.contains("OFFSET 1"), "{}", out.sql);
9268 assert!(out.sql.contains("LIMIT 2"), "{}", out.sql);
9269 }
9270
9271 #[test]
9272 fn test_multi_link_without_limit_keeps_the_flat_aggregate() {
9273 let out = compile_and_emit("SELECT Person { posts: { title } filter .title = 'x' }");
9274 assert!(!out.sql.contains("__lim"), "{}", out.sql);
9275 assert!(out.sql.contains("array_agg(ROW("), "{}", out.sql);
9276 }
9277
9278 #[test]
9279 fn test_sub_select_as_computed_pointer_is_a_link_pointer() {
9280 let out = compile_and_emit("SELECT Person { recent := (select .posts filter .title = 'x' limit 1) { title } }");
9281 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
9282 panic!()
9283 };
9284 let ShapeNode::Object {
9287 name,
9288 pointers: elem_pointers,
9289 ..
9290 } = &pointers[2]
9291 else {
9292 panic!("{:?}", pointers[2])
9293 };
9294 assert_eq!(name, "recent");
9295 assert!(matches!(&elem_pointers[1], ShapeNode::Scalar { name, .. } if name == "title"));
9296 assert!(out.sql.contains("\"jt\".source = \"t0\".id"), "{}", out.sql);
9297 assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
9298 }
9299
9300 #[test]
9301 fn test_sub_select_shape_inside_the_parens_is_equivalent() {
9302 let outer = compile_and_emit("SELECT Person { recent := (select .posts limit 1) { title } }");
9303 let inner = compile_and_emit("SELECT Person { recent := (select .posts { title } limit 1) }");
9304 assert_eq!(outer.sql, inner.sql);
9305 }
9306
9307 #[test]
9308 fn test_sub_select_field_access_is_a_correlated_scalar_subquery() {
9309 let out = compile_and_emit("SELECT Person { t := (select .posts filter .title = 'x' limit 1).title }");
9310 assert!(out.sql.contains("(\"t1\".\"id\" = \"t0\".\"id\")"), "{}", out.sql);
9313 assert!(out.sql.contains("\"t2\".\"title\""), "{}", out.sql);
9314 assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
9315 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
9316 panic!()
9317 };
9318 assert!(matches!(&pointers[2], ShapeNode::Scalar { name, .. } if name == "t"));
9319 }
9320
9321 #[test]
9322 fn test_sub_select_field_access_over_a_single_link() {
9323 let out = compile_and_emit("SELECT Person { c := (select .company).name }");
9324 assert!(out.sql.contains("\"t1\".\"company_id\" = \"t2\".\"id\""), "{}", out.sql);
9325 assert!(out.sql.contains("(\"t1\".\"id\" = \"t0\".\"id\")"), "{}", out.sql);
9326 }
9327
9328 #[test]
9329 fn test_sub_select_field_access_in_a_filter() {
9330 let out = compile_and_emit("SELECT Person { name } filter (select .posts limit 1).title = 'x'");
9331 assert!(out.sql.contains("WHERE ((SELECT \"t2\".\"title\""), "{}", out.sql);
9332 assert!(out.sql.contains("LIMIT 1) = 'x')"), "{}", out.sql);
9333 }
9334
9335 #[test]
9336 fn test_sub_select_over_an_absolute_path_is_not_correlated() {
9337 let out = compile_and_emit("SELECT Person { n := (select Company.name limit 1) }");
9338 assert!(out.sql.contains("FROM \"public\".\"Company\""), "{}", out.sql);
9339 assert!(!out.sql.contains("= \"t0\".\"id\""), "{}", out.sql);
9340 }
9341
9342 #[test]
9343 fn test_backlink_sub_select_carries_its_modifiers() {
9344 let out = compile_and_emit("SELECT Post { authors := (select .<posts[is Person] { name } limit 2) }");
9345 assert!(out.sql.contains("LIMIT 2"), "{}", out.sql);
9346 assert!(out.sql.contains("AS \"__lim\""), "{}", out.sql);
9347 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
9348 panic!()
9349 };
9350 assert!(matches!(&pointers[1], ShapeNode::Array { name, .. } if name == "authors"));
9351 }
9352
9353 #[test]
9354 fn test_with_bound_scalar_is_typed_for_overload_resolution() {
9355 let out = compile_and_emit("WITH xs := (select Person.name) SELECT contains(xs, 'a')");
9358 assert!(
9359 out.sql.contains("strpos((SELECT \"v\" FROM \"xs\"), 'a')"),
9360 "{}",
9361 out.sql
9362 );
9363 }
9364
9365 fn make_schema_with_computed_links() -> SchemaDescriptor {
9368 use crate::schema::ComputedDescriptor;
9369 let mut schema = make_schema();
9370 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
9371 person.computed = vec![
9372 ComputedDescriptor {
9373 name: "published".into(),
9374 expression: "(select .posts filter .title != '')".into(),
9375 return_type: None,
9376 link_target: None,
9377 link_multi: false,
9378 },
9379 ComputedDescriptor {
9380 name: "plain".into(),
9381 expression: ".posts".into(),
9382 return_type: None,
9383 link_target: None,
9384 link_multi: false,
9385 },
9386 ComputedDescriptor {
9387 name: "featured".into(),
9388 expression: "(select .published filter .title != 'draft')".into(),
9389 return_type: None,
9390 link_target: None,
9391 link_multi: false,
9392 },
9393 ComputedDescriptor {
9394 name: "looper".into(),
9395 expression: "(select .looper)".into(),
9396 return_type: None,
9397 link_target: None,
9398 link_multi: false,
9399 },
9400 ComputedDescriptor {
9401 name: "capped".into(),
9402 expression: "(select .posts limit 1)".into(),
9403 return_type: None,
9404 link_target: None,
9405 link_multi: false,
9406 },
9407 ];
9408 schema
9409 }
9410
9411 fn make_schema_with_object_fn_computed() -> SchemaDescriptor {
9414 use crate::schema::{ComputedDescriptor, FunctionDescriptor, FunctionParamDescriptor};
9415 let mut schema = make_schema();
9416 schema.functions = vec![FunctionDescriptor {
9417 name: "latest".into(),
9418 module: "default".into(),
9419 params: vec![FunctionParamDescriptor {
9420 name: "owner".into(),
9421 pg_type: "uuid".into(),
9422 }],
9423 return_pg_type: "default::Post".into(),
9424 return_is_object: true,
9425 return_is_set: true,
9426 return_is_polymorphic: false,
9427 volatility: "stable".into(),
9428 body: String::new(),
9429 }];
9430 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
9431 person.computed = vec![
9432 ComputedDescriptor {
9433 name: "recent".into(),
9434 expression: "latest(.id)".into(),
9435 return_type: None,
9436 link_target: None,
9437 link_multi: false,
9438 },
9439 ComputedDescriptor {
9440 name: "vetted".into(),
9441 expression: "(select latest(.id) filter .title != '')".into(),
9442 return_type: None,
9443 link_target: None,
9444 link_multi: false,
9445 },
9446 ];
9447 schema
9448 }
9449
9450 #[test]
9451 fn test_path_traverses_through_a_function_backed_computed() {
9452 let schema = make_schema_with_object_fn_computed();
9456 let out = compile_and_emit_with("SELECT Person { t := .recent.title }", &schema);
9457 assert!(
9458 out.sql
9459 .contains("CROSS JOIN LATERAL \"public\".\"latest\"(\"t1\".\"id\") AS \"t2\""),
9460 "{}",
9461 out.sql
9462 );
9463 assert!(out.sql.contains("ARRAY(SELECT \"t2\".\"title\""), "{}", out.sql);
9465 }
9466
9467 #[test]
9468 fn test_sub_select_over_a_function_backed_computed() {
9469 let schema = make_schema_with_object_fn_computed();
9472 let out = compile_and_emit_with(
9473 "SELECT Person { t := (select .recent filter .title = 'x' limit 1).title }",
9474 &schema,
9475 );
9476 assert!(
9477 out.sql.contains("CROSS JOIN LATERAL \"public\".\"latest\""),
9478 "{}",
9479 out.sql
9480 );
9481 assert!(out.sql.contains("\"t2\".\"title\" = 'x'"), "{}", out.sql);
9482 assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
9483 assert!(!out.sql.contains("ARRAY("), "{}", out.sql);
9484 }
9485
9486 #[test]
9487 fn test_function_backed_computed_carries_its_own_filter() {
9488 let schema = make_schema_with_object_fn_computed();
9489 let out = compile_and_emit_with(
9490 "SELECT Person { t := (select .vetted filter .title = 'x' limit 1).title }",
9491 &schema,
9492 );
9493 assert!(
9494 out.sql
9495 .contains("(\"t2\".\"title\" = 'x') AND (\"t2\".\"title\" <> '')"),
9496 "{}",
9497 out.sql
9498 );
9499 }
9500
9501 #[test]
9502 fn test_function_backed_computed_checks_its_argument_count() {
9503 use crate::schema::ComputedDescriptor;
9504 let mut schema = make_schema_with_object_fn_computed();
9505 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
9506 person.computed = vec![ComputedDescriptor {
9507 name: "recent".into(),
9508 expression: "latest()".into(),
9509 return_type: None,
9510 link_target: None,
9511 link_multi: false,
9512 }];
9513 let ast = parse::parse("SELECT Person { t := .recent.title }").unwrap();
9514 let err = match ir::compile(&ast, &schema) {
9515 Ok(_) => panic!("expected a compile error"),
9516 Err(e) => format!("{e}"),
9517 };
9518 assert!(err.contains("expects 1 argument(s), got 0"), "{err}");
9519 }
9520
9521 #[test]
9522 fn test_path_traverses_through_a_computed_pointer() {
9523 let schema = make_schema_with_computed_links();
9526 let out = compile_and_emit_with("SELECT Person { t := .published.title }", &schema);
9527 assert!(out.sql.contains("\"public\".\"Person.posts\""), "{}", out.sql);
9528 assert!(
9529 out.sql
9530 .contains("(\"t1\".\"id\" = \"t0\".\"id\") AND (\"t2\".\"title\" <> '')"),
9531 "{}",
9532 out.sql
9533 );
9534 }
9535
9536 #[test]
9537 fn test_sub_select_over_a_computed_pointer_projects_a_property() {
9538 let schema = make_schema_with_computed_links();
9541 let out = compile_and_emit_with(
9542 "SELECT Person { t := (select .published filter .title = 'a' limit 1).title }",
9543 &schema,
9544 );
9545 assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
9546 assert!(out.sql.contains("\"t2\".\"title\" = 'a'"), "{}", out.sql);
9548 assert!(out.sql.contains("\"t2\".\"title\" <> ''"), "{}", out.sql);
9549 }
9550
9551 #[test]
9552 fn test_chained_computed_pointers_each_contribute_their_filter() {
9553 let schema = make_schema_with_computed_links();
9554 let out = compile_and_emit_with("SELECT Person { t := .featured.title }", &schema);
9555 assert_eq!(out.sql.matches("\"public\".\"Person.posts\"").count(), 1, "{}", out.sql);
9556 assert!(out.sql.contains("\"t2\".\"title\" <> 'draft'"), "{}", out.sql);
9557 assert!(out.sql.contains("\"t2\".\"title\" <> ''"), "{}", out.sql);
9558 }
9559
9560 #[test]
9561 fn test_computed_pointer_naming_a_bare_link_is_traversable() {
9562 let schema = make_schema_with_computed_links();
9563 let out = compile_and_emit_with("SELECT Person { t := .plain.title }", &schema);
9564 assert!(out.sql.contains("ARRAY(SELECT \"t2\".\"title\""), "{}", out.sql);
9565 }
9566
9567 #[test]
9568 fn test_a_cycle_of_computed_pointers_is_reported_not_hung_on() {
9569 let schema = make_schema_with_computed_links();
9570 let ast = parse::parse("SELECT Person { t := .looper.title }").unwrap();
9571 let err = match ir::compile(&ast, &schema) {
9572 Ok(_) => panic!("expected a compile error"),
9573 Err(e) => format!("{e}"),
9574 };
9575 assert!(err.contains("expands into itself"), "{err}");
9576 }
9577
9578 #[test]
9579 fn test_a_cycle_of_computed_pointers_read_as_a_value_is_reported_not_hung_on() {
9580 let schema = make_schema_with_computed_links();
9583 for query in ["SELECT Person { looper }", "SELECT Person { * }"] {
9584 let ast = parse::parse(query).unwrap();
9585 let err = match ir::compile(&ast, &schema) {
9586 Ok(_) => panic!("expected a compile error for {query}"),
9587 Err(e) => format!("{e}"),
9588 };
9589 assert!(err.contains("expands into itself"), "{query}: {err}");
9590 }
9591 }
9592
9593 #[test]
9594 fn test_traversing_through_a_limited_computed_uses_a_lateral() {
9595 let schema = make_schema_with_computed_links();
9599 let out = compile_and_emit_with("SELECT Person { t := .capped.title }", &schema);
9600 assert!(out.sql.contains("JOIN LATERAL ("), "{}", out.sql);
9601 assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
9602 assert!(!out.sql.contains("ARRAY(SELECT"), "{}", out.sql);
9603 }
9604
9605 #[test]
9606 fn test_backlink_from_a_type_whose_interface_the_link_targets() {
9607 use crate::schema::{LinkDescriptor, TypeDescriptor};
9608 let mut schema = make_interface_schema();
9609 let individual = schema.types.iter().find(|t| t.name == "Individual").unwrap().clone();
9610 schema.types.push(TypeDescriptor {
9611 name: "Note".into(),
9612 module: "default".into(),
9613 table: "Note".into(),
9614 abstract_: false,
9615 materialized: true,
9616 description: None,
9617 parents: vec![],
9618 interfaces: vec![],
9619 bases: vec![],
9620 properties: individual.properties[..1].to_vec(),
9621 links: vec![LinkDescriptor {
9623 name: "owner".into(),
9624 target: "default::Account".into(),
9625 nullable: true,
9626 through: None,
9627 description: None,
9628 default_pyql: None,
9629 is_exclusive: false,
9630 is_readonly: false,
9631 rewrites: vec![],
9632 on_delete: vec![],
9633 }],
9634 multilinks: vec![],
9635 computed: vec![],
9636 constraints: vec![],
9637 indexes: vec![],
9638 partition: None,
9639 vector_indexes: vec![],
9640 search_indexes: vec![],
9641 triggers: vec![],
9642 junction: false,
9643 signals: vec![],
9644 });
9645 let out = compile_and_emit_with("SELECT Individual { notes := .<owner[is Note] }", &schema);
9648 assert!(out.sql.contains("\"public\".\"Note\""), "{}", out.sql);
9649 assert!(out.sql.contains("\"owner_id\" = \"t0\".id"), "{}", out.sql);
9650 }
9651
9652 #[test]
9653 fn test_with_block_in_expression_position_hoists_its_bindings() {
9654 let out = compile_and_emit("SELECT Person { t := (with c := (select Company.name limit 1) select c) }");
9657 assert!(out.sql.starts_with("WITH\n\"c\" AS ("), "{}", out.sql);
9658 assert!(out.sql.contains("(SELECT \"v\" FROM \"c\")"), "{}", out.sql);
9659 }
9660
9661 #[test]
9662 fn test_hoisted_binding_is_usable_in_the_expression_around_it() {
9663 let out =
9664 compile_and_emit("SELECT Person { t := (with c := (select Company.name limit 1) select .name ++ c) }");
9665 assert!(
9666 out.sql.contains("(\"t0\".\"name\" || (SELECT \"v\" FROM \"c\"))"),
9667 "{}",
9668 out.sql
9669 );
9670 }
9671
9672 #[test]
9673 fn test_hoisted_binding_joins_the_statements_own_with_clause() {
9674 let out = compile_and_emit(
9675 "WITH top := (select Person.name limit 1) \
9676 SELECT Person { t := (with c := (select Company.name limit 1) select c) }",
9677 );
9678 let top = out.sql.find("\"top\" AS (").expect("top-level binding");
9679 let inner = out.sql.find("\"c\" AS (").expect("hoisted binding");
9680 assert!(top < inner, "the statement's own bindings come first:\n{}", out.sql);
9681 }
9682
9683 #[test]
9684 fn test_a_binding_named_after_a_keyword_is_usable() {
9685 let out = compile_and_emit("WITH order := (select Person.name) SELECT Person { name } FILTER .name = order");
9690 assert!(out.sql.contains("(SELECT \"v\" FROM \"order\")"), "{}", out.sql);
9691 }
9692
9693 #[test]
9694 fn test_computed_declared_on_an_interface_is_visible_from_an_implementor() {
9695 use crate::schema::ComputedDescriptor;
9696 let mut schema = make_interface_schema();
9697 let account = schema.types.iter_mut().find(|t| t.name == "Account").unwrap();
9698 account.computed = vec![ComputedDescriptor {
9699 name: "tier".into(),
9700 expression: "'gold'".into(),
9701 return_type: None,
9702 link_target: None,
9703 link_multi: false,
9704 }];
9705 let individual = schema.types.iter_mut().find(|t| t.name == "Individual").unwrap();
9706 individual.computed = vec![];
9707 for q in ["SELECT Individual { tier }", "SELECT Individual { t := .tier }"] {
9710 let out = compile_and_emit_with(q, &schema);
9711 assert!(out.sql.contains("'gold'"), "{q}: {}", out.sql);
9712 }
9713 }
9714
9715 #[test]
9716 fn test_computed_may_carry_trailing_modifiers_without_a_select() {
9717 use crate::schema::ComputedDescriptor;
9718 let mut schema = make_schema();
9719 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
9720 person.computed = vec![ComputedDescriptor {
9721 name: "recent".into(),
9722 expression: ".posts order by .title desc limit 5".into(),
9723 return_type: None,
9724 link_target: None,
9725 link_multi: false,
9726 }];
9727 let out = compile_and_emit_with("SELECT Person { recent { title } }", &schema);
9730 assert!(out.sql.contains("LIMIT 5"), "{}", out.sql);
9731 assert!(out.sql.contains("ORDER BY \"t1\".\"title\" DESC"), "{}", out.sql);
9732 }
9733
9734 #[test]
9735 fn test_computed_may_lead_with_a_bare_select() {
9736 use crate::schema::ComputedDescriptor;
9737 let mut schema = make_schema();
9738 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
9739 person.computed = vec![ComputedDescriptor {
9740 name: "recent".into(),
9741 expression: "select .posts order by .title desc limit 5".into(),
9742 return_type: None,
9743 link_target: None,
9744 link_multi: false,
9745 }];
9746 let out = compile_and_emit_with("SELECT Person { recent { title } }", &schema);
9747 assert!(out.sql.contains("LIMIT 5"), "{}", out.sql);
9748 }
9749
9750 #[test]
9751 fn test_sub_select_with_a_shape_may_project_a_property_off_it() {
9752 let out = compile_and_emit("SELECT Person { t := (select .posts { title } limit 1).title }");
9753 assert!(out.sql.contains("\"t2\".\"title\""), "{}", out.sql);
9754 assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
9755 }
9756
9757 #[test]
9758 fn test_sub_statement_error_names_what_it_got() {
9759 let err = compile_err("SELECT Person { t := (insert Company { name := 'a' }) }");
9760 assert!(err.contains("an insert cannot stand in for a value"), "{err}");
9761 }
9762
9763 #[test]
9764 fn test_with_bound_scalar_path_select_emits_a_real_select() {
9765 let out = compile_and_emit("WITH xs := (select Person.name) SELECT Person { name }");
9768 assert!(
9769 out.sql
9770 .contains("SELECT ROW(v) AS result, v FROM (SELECT \"t0\".\"name\" AS v"),
9771 "{}",
9772 out.sql
9773 );
9774 }
9775
9776 #[test]
9777 fn test_with_bound_scalar_path_select_is_typed_as_its_result() {
9778 let out = compile_and_emit("WITH xs := (select Person.name) SELECT Person { name } FILTER .name IN xs");
9781 assert!(out.sql.contains("ANY((SELECT \"v\" FROM \"xs\"))"), "{}", out.sql);
9782 }
9783
9784 #[test]
9785 fn test_distinct_survives_a_with_binding() {
9786 let out = compile_and_emit("WITH xs := (select distinct Person.name) SELECT Person { name }");
9787 assert!(out.sql.contains("SELECT DISTINCT \"t0\".\"name\" AS v"), "{}", out.sql);
9788 }
9789
9790 #[test]
9791 fn test_for_over_a_derived_set_iterates_every_row() {
9792 let out = compile_and_emit("FOR x IN (select Person.name) UNION (SELECT Person { name } FILTER .name = x)");
9795 assert!(!out.sql.contains("VALUES"), "{}", out.sql);
9796 assert!(out.sql.contains(") AS \"_for_x\""), "{}", out.sql);
9797 assert!(out.sql.contains("CROSS JOIN LATERAL"), "{}", out.sql);
9798 assert!(out.sql.contains("\"t1\".\"name\" = \"_for_x\".\"v\""), "{}", out.sql);
9799 }
9800
9801 #[test]
9802 fn test_for_insert_over_a_derived_set_uses_it_as_the_cte() {
9803 let out = compile_and_emit("FOR x IN (select Person.name) UNION (INSERT Company { name := x })");
9804 assert!(out.sql.starts_with("WITH \"_for_x\" AS ("), "{}", out.sql);
9805 assert!(
9806 out.sql.contains("SELECT \"_for_x\".\"v\" FROM \"_for_x\""),
9807 "{}",
9808 out.sql
9809 );
9810 }
9811
9812 #[test]
9813 fn test_for_over_objects_binds_their_id() {
9814 let out = compile_and_emit("FOR p IN (select Person) UNION (INSERT Company { name := <str>p })");
9815 assert!(out.sql.contains("SELECT \"id\" AS v FROM ("), "{}", out.sql);
9816 }
9817
9818 #[test]
9819 fn test_for_over_a_non_select_statement_is_rejected() {
9820 let ast = parse::parse("FOR x IN (INSERT Company { name := 'a' }) UNION (SELECT Person { name })").unwrap();
9821 let schema = make_schema();
9822 let err = match ir::compile(&ast, &schema) {
9823 Ok(_) => panic!("expected a compile error"),
9824 Err(e) => format!("{e}"),
9825 };
9826 assert!(err.contains("only a select can be iterated"), "{err}");
9827 }
9828
9829 #[test]
9830 fn test_multi_link_path_outside_a_comparison_is_an_array() {
9831 let out = compile_and_emit("SELECT Person { t := .posts.title }");
9834 assert!(out.sql.contains("ARRAY(SELECT \"t2\".\"title\""), "{}", out.sql);
9835 assert!(out.sql.contains("(\"t1\".\"id\" = \"t0\".\"id\")"), "{}", out.sql);
9836 }
9837
9838 #[test]
9839 fn test_multi_link_path_inside_a_comparison_is_still_exists() {
9840 let out = compile_and_emit("SELECT Person { name } filter .posts.title = 'x'");
9841 assert!(out.sql.contains("WHERE EXISTS("), "{}", out.sql);
9842 assert!(!out.sql.contains("ARRAY("), "{}", out.sql);
9843 }
9844
9845 #[test]
9846 fn test_path_traversal_deeper_than_two_steps() {
9847 let schema = make_schema_with_through();
9848 let out = compile_and_emit_with("SELECT Person { t := .friends.friends.name }", &schema);
9849 assert_eq!(out.sql.matches("\"public\".\"PersonFriend\"").count(), 2, "{}", out.sql);
9851 assert!(out.sql.contains("ARRAY(SELECT \"t4\".\"name\""), "{}", out.sql);
9852 }
9853
9854 #[test]
9855 fn test_coalesce_over_backlinked_values_is_a_set_choice() {
9856 let out = compile_and_emit("SELECT Post ORDER BY max(.<posts[is Person].age ?? .<posts[is Person].age)");
9859 assert!(!out.sql.contains("EXISTS"), "{}", out.sql);
9860 assert!(!out.sql.contains("COALESCE("), "{}", out.sql);
9861 assert!(out.sql.contains("CASE WHEN (cardinality("), "{}", out.sql);
9862 assert!(
9863 out.sql
9864 .contains("(SELECT coalesce(max(\"_s\".\"v\"), NULL) FROM unnest("),
9865 "{}",
9866 out.sql
9867 );
9868 }
9869
9870 #[test]
9871 fn test_coalesce_over_multilink_values_is_a_set_choice() {
9872 let out = compile_and_emit("SELECT Person { t := .posts.title ?? .posts.title }");
9873 assert!(!out.sql.contains("EXISTS"), "{}", out.sql);
9874 assert!(out.sql.contains("CASE WHEN (cardinality("), "{}", out.sql);
9875 }
9876
9877 #[test]
9878 fn test_coalesce_of_a_set_with_a_single_value_falls_back_to_it() {
9879 let out = compile_and_emit("SELECT Post { a := .<posts[is Person].age ?? 0 }");
9880 assert!(out.sql.contains("ELSE array_remove(ARRAY[0], NULL) END"), "{}", out.sql);
9881 }
9882
9883 #[test]
9884 fn test_traversal_after_a_backlink() {
9885 let out = compile_and_emit("SELECT Post { t := .<posts[is Person].name }");
9886 assert!(out.sql.contains("ARRAY(SELECT \"t2\".\"name\""), "{}", out.sql);
9887 assert!(out.sql.contains("\"t3\".\"target\" = \"t1\".\"id\""), "{}", out.sql);
9888 }
9889
9890 #[test]
9891 fn test_computed_pointer_on_a_linked_type_is_reachable() {
9892 use crate::schema::ComputedDescriptor;
9893 let mut schema = make_schema();
9894 let company = schema.types.iter_mut().find(|t| t.name == "Company").unwrap();
9895 company.computed = vec![ComputedDescriptor {
9896 name: "shout".into(),
9897 expression: ".name ++ '!'".into(),
9898 return_type: Some("text".into()),
9899 link_target: None,
9900 link_multi: false,
9901 }];
9902 let out = compile_and_emit_with("SELECT Person { t := .company.shout }", &schema);
9906 assert!(out.sql.contains("(\"t2\".\"name\" || '!')"), "{}", out.sql);
9907 }
9908
9909 #[test]
9910 fn test_computed_pointer_behind_a_type_intersection() {
9911 let schema = make_interface_schema();
9912 let out = compile_and_emit_with("SELECT Account { n := [is Individual].full_name }", &schema);
9913 assert!(out.sql.contains("upper(\"t1\".\"first_name\")"), "{}", out.sql);
9914 assert!(out.sql.contains("FROM \"public\".\"Individual\""), "{}", out.sql);
9915 assert!(out.sql.contains("(\"t1\".\"id\" = \"t0\".\"id\")"), "{}", out.sql);
9916 }
9917
9918 #[test]
9919 fn test_computed_pointer_cannot_be_traversed_through() {
9920 use crate::schema::ComputedDescriptor;
9921 let mut schema = make_schema();
9922 let company = schema.types.iter_mut().find(|t| t.name == "Company").unwrap();
9923 company.computed = vec![ComputedDescriptor {
9924 name: "shout".into(),
9925 expression: ".name ++ '!'".into(),
9926 return_type: Some("text".into()),
9927 link_target: None,
9928 link_multi: false,
9929 }];
9930 let ast = parse::parse("SELECT Person { t := .company.shout.nope }").unwrap();
9931 let err = match ir::compile(&ast, &schema) {
9932 Ok(_) => panic!("expected a compile error"),
9933 Err(e) => format!("{e}"),
9934 };
9935 assert!(err.contains("is a computed pointer"), "{err}");
9936 }
9937
9938 #[test]
9939 fn test_sub_select_over_a_multi_link_path_keeps_its_modifiers() {
9940 let out = compile_and_emit("SELECT Person { t := (select .posts.title order by .title desc limit 2) }");
9941 assert!(out.sql.contains("ARRAY(SELECT"), "{}", out.sql);
9942 assert!(out.sql.contains("ORDER BY \"t2\".\"title\" DESC"), "{}", out.sql);
9943 assert!(out.sql.contains("LIMIT 2"), "{}", out.sql);
9944 }
9945
9946 #[test]
9947 fn test_limit_one_over_a_multi_link_path_stays_a_scalar() {
9948 let out = compile_and_emit("SELECT Person { t := (select .posts.title limit 1) }");
9951 assert!(!out.sql.contains("ARRAY("), "{}", out.sql);
9952 assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
9953 }
9954
9955 #[test]
9956 fn test_computed_multilink_can_carry_a_nested_shape() {
9957 let out = compile_and_emit("SELECT Person { p := .posts { title } }");
9958 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
9959 panic!()
9960 };
9961 let ShapeNode::Array { name, element, .. } = &pointers[2] else {
9962 panic!("{:?}", pointers[2])
9963 };
9964 assert_eq!(name, "p");
9965 let ShapeNode::Object {
9966 pointers: elem_pointers,
9967 ..
9968 } = element.as_ref()
9969 else {
9970 panic!()
9971 };
9972 assert!(matches!(&elem_pointers[1], ShapeNode::Scalar { name, .. } if name == "title"));
9973 }
9974
9975 #[test]
9976 fn test_schema_declared_backlink_computed_is_an_object_pointer() {
9977 use crate::schema::ComputedDescriptor;
9978 let mut schema = make_schema();
9979 let post = schema.types.iter_mut().find(|t| t.name == "Post").unwrap();
9980 post.computed = vec![ComputedDescriptor {
9981 name: "authors".into(),
9982 expression: ".<posts[is Person]".into(),
9983 return_type: None,
9984 link_target: None,
9985 link_multi: false,
9986 }];
9987 let out = compile_and_emit_with("SELECT Post { authors { name } }", &schema);
9990 assert!(out.sql.contains("array_agg(ROW("), "{}", out.sql);
9991 assert!(out.sql.contains("\"t1\".\"name\"::text"), "{}", out.sql);
9992 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
9993 panic!()
9994 };
9995 assert!(matches!(&pointers[1], ShapeNode::Array { name, .. } if name == "authors"));
9996 }
9997
9998 #[test]
9999 fn test_schema_declared_multilink_computed_defaults_to_ids_without_a_shape() {
10000 use crate::schema::ComputedDescriptor;
10001 let mut schema = make_schema();
10002 schema.types[0].computed = vec![ComputedDescriptor {
10003 name: "everything".into(),
10004 expression: ".posts".into(),
10005 return_type: None,
10006 link_target: None,
10007 link_multi: false,
10008 }];
10009 let out = compile_and_emit_with("SELECT Person { everything }", &schema);
10010 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
10011 panic!()
10012 };
10013 assert!(matches!(&pointers[2], ShapeNode::Array { name, .. } if name == "everything"));
10014 }
10015
10016 #[test]
10017 fn test_schema_declared_computed_sub_select_is_a_link_pointer() {
10018 use crate::schema::ComputedDescriptor;
10019 let mut schema = make_schema();
10020 schema.types[0].computed = vec![
10021 ComputedDescriptor {
10022 name: "recent".into(),
10023 expression: "(select .posts order by .title desc limit 1)".into(),
10024 return_type: None,
10025 link_target: None,
10026 link_multi: false,
10027 },
10028 ComputedDescriptor {
10029 name: "recent_title".into(),
10030 expression: "(select .posts order by .title desc limit 1).title".into(),
10031 return_type: Some("text".into()),
10032 link_target: None,
10033 link_multi: false,
10034 },
10035 ];
10036 let out = compile_and_emit_with("SELECT Person { recent { title }, recent_title }", &schema);
10037 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
10038 panic!()
10039 };
10040 assert!(matches!(&pointers[2], ShapeNode::Object { name, .. } if name == "recent"));
10043 assert!(matches!(&pointers[3], ShapeNode::Scalar { name, .. } if name == "recent_title"));
10044 assert!(out.sql.contains("\"jt\".source = \"t0\".id"), "{}", out.sql);
10045 assert_eq!(out.sql.matches("LIMIT 1").count(), 2, "{}", out.sql);
10046 crate::validate::validate_schema_types(&schema).expect("schema should validate");
10047 }
10048
10049 #[test]
10054 fn test_a_mutation_read_through_a_path_runs_as_a_hoisted_cte() {
10055 let out = compile_and_emit("SELECT (INSERT Person { name := 'x' }).name");
10056 assert!(
10057 out.sql.contains("\"_nested_dml_0\" AS (") && out.sql.contains("INSERT INTO"),
10058 "the mutation must run as a CTE:\n{}",
10059 out.sql
10060 );
10061 assert!(
10062 out.sql.contains("FROM \"_nested_dml_0\""),
10063 "the projection must read the mutation's own rows:\n{}",
10064 out.sql
10065 );
10066 }
10067
10068 #[test]
10069 fn test_dml_sub_statement_in_expression_position_still_rejected() {
10070 let err = compile_err("SELECT Person { x := (insert Company { name := 'c' }).name }");
10071 assert!(err.contains("an insert cannot stand in for a value"), "{err}");
10072 }
10073
10074 #[test]
10075 fn test_sub_select_with_a_shape_in_expression_position_is_rejected() {
10076 let err = compile_err("SELECT Person { name } filter (select .posts { title }) = 1");
10080 assert!(err.contains("sub-select with a shape"), "{err}");
10081 }
10082
10083 fn make_schema_with_through() -> SchemaDescriptor {
10084 let id_prop = || PropertyDescriptor {
10085 name: "id".into(),
10086 pg_type: "uuid".into(),
10087 nullable: false,
10088 default_sql: Some("gen_random_uuid()".into()),
10089 description: None,
10090 default_pyql: None,
10091 check_constraints: vec![],
10092 is_exclusive: true,
10093 is_pk: true,
10094 is_readonly: true,
10095 rewrites: vec![],
10096 tuple_members: None,
10097 column_type: None,
10098 };
10099 let name_prop = || PropertyDescriptor {
10100 name: "name".into(),
10101 pg_type: "text".into(),
10102 nullable: false,
10103 default_sql: None,
10104 description: None,
10105 check_constraints: vec![],
10106 default_pyql: None,
10107 is_exclusive: false,
10108 is_pk: false,
10109 is_readonly: false,
10110 rewrites: vec![],
10111 tuple_members: None,
10112 column_type: None,
10113 };
10114 SchemaDescriptor {
10115 types: vec![
10116 TypeDescriptor {
10117 name: "Person".into(),
10118 module: "default".into(),
10119 table: "Person".into(),
10120 abstract_: false,
10121 materialized: false,
10122 description: None,
10123 parents: vec![],
10124 interfaces: vec![],
10125 bases: vec![],
10126 properties: vec![id_prop(), name_prop()],
10127 links: vec![],
10128 multilinks: vec![MultiLinkDescriptor {
10129 name: "friends".into(),
10130 target: "default::Person".into(),
10131 through: Some("default::PersonFriend".into()),
10132 nullable: false,
10133 description: None,
10134 default_pyql: None,
10135 on_delete: vec![],
10136 is_exclusive: false,
10137 }],
10138 computed: vec![],
10139 constraints: vec![],
10140 indexes: vec![],
10141 partition: None,
10142 vector_indexes: vec![],
10143 search_indexes: vec![],
10144 triggers: vec![],
10145 junction: false,
10146 signals: vec![],
10147 },
10148 TypeDescriptor {
10149 name: "PersonFriend".into(),
10150 module: "default".into(),
10151 table: "PersonFriend".into(),
10152 abstract_: false,
10153 materialized: false,
10154 description: None,
10155 parents: vec![],
10156 interfaces: vec![],
10157 bases: vec![],
10158 properties: vec![id_prop()],
10159 links: vec![
10160 LinkDescriptor {
10161 name: "person".into(),
10162 target: "default::Person".into(),
10163 nullable: false,
10164 through: None,
10165 description: None,
10166 default_pyql: None,
10167 is_exclusive: false,
10168 is_readonly: false,
10169 rewrites: vec![],
10170 on_delete: vec![],
10171 },
10172 LinkDescriptor {
10173 name: "friend".into(),
10174 target: "default::Person".into(),
10175 nullable: false,
10176 through: None,
10177 description: None,
10178 default_pyql: None,
10179 is_exclusive: false,
10180 is_readonly: false,
10181 rewrites: vec![],
10182 on_delete: vec![],
10183 },
10184 ],
10185 multilinks: vec![],
10186 computed: vec![],
10187 constraints: vec![],
10188 indexes: vec![],
10189 partition: None,
10190 vector_indexes: vec![],
10191 search_indexes: vec![],
10192 triggers: vec![],
10193 junction: false,
10194 signals: vec![],
10195 },
10196 ],
10197 scalars: vec![],
10198 enums: vec![],
10199 named_tuples: vec![],
10200 globals: vec![],
10201 functions: vec![],
10202 aliases: vec![],
10203 channels: vec![],
10204 ..Default::default()
10205 }
10206 }
10207
10208 #[test]
10209 fn test_select_through_multi_link() {
10210 let schema = make_schema_with_through();
10211 let ast = crate::parse::parse("SELECT Person { name, friends { name } }").unwrap();
10212 let ir = crate::ir::compile(&ast, &schema).unwrap();
10213 let out = emit(&ir);
10214 assert!(out.sql.contains("\"public\".\"PersonFriend\""));
10216 assert!(out.sql.contains("\"friend\""));
10218 assert!(out.sql.contains("\"person\""));
10219 assert!(out.sql.contains("array_agg(ROW("));
10221 }
10222
10223 #[test]
10229 fn test_limited_multi_link_keeps_a_nested_multi_link_in_scope() {
10230 let schema = make_schema_with_through();
10233 let ast = crate::parse::parse("SELECT Person { friends: { name, friends { name } } limit 1 }").unwrap();
10234 let ir = crate::ir::compile(&ast, &schema).unwrap();
10235 let out = emit(&ir);
10236 assert_eq!(out.sql.matches("array_agg(ROW(").count(), 2, "{}", out.sql);
10237 let derived = out.sql.find("AS \"__lim\"").unwrap();
10238 let nested = out.sql.rfind("array_agg(ROW(").unwrap();
10239 assert!(
10240 nested < derived,
10241 "nested aggregate must sit inside the derived table:\n{}",
10242 out.sql
10243 );
10244 }
10245
10246 fn make_schema_with_junction_backed_link() -> SchemaDescriptor {
10247 let id_prop = || PropertyDescriptor {
10248 name: "id".into(),
10249 pg_type: "uuid".into(),
10250 nullable: false,
10251 default_sql: Some("gen_random_uuid()".into()),
10252 description: None,
10253 default_pyql: None,
10254 check_constraints: vec![],
10255 is_exclusive: true,
10256 is_pk: true,
10257 is_readonly: true,
10258 rewrites: vec![],
10259 tuple_members: None,
10260 column_type: None,
10261 };
10262 let name_prop = || PropertyDescriptor {
10263 name: "name".into(),
10264 pg_type: "text".into(),
10265 nullable: false,
10266 default_sql: None,
10267 description: None,
10268 check_constraints: vec![],
10269 default_pyql: None,
10270 is_exclusive: false,
10271 is_pk: false,
10272 is_readonly: false,
10273 rewrites: vec![],
10274 tuple_members: None,
10275 column_type: None,
10276 };
10277 SchemaDescriptor {
10278 types: vec![
10279 TypeDescriptor {
10280 name: "Person".into(),
10281 module: "default".into(),
10282 table: "Person".into(),
10283 abstract_: false,
10284 materialized: false,
10285 description: None,
10286 parents: vec![],
10287 interfaces: vec![],
10288 bases: vec![],
10289 properties: vec![id_prop(), name_prop()],
10290 links: vec![LinkDescriptor {
10291 name: "spouse".into(),
10292 target: "default::Org".into(),
10293 nullable: true,
10294 through: Some("default::Marriage".into()),
10295 description: None,
10296 default_pyql: None,
10297 is_exclusive: true,
10298 is_readonly: false,
10299 rewrites: vec![],
10300 on_delete: vec![],
10301 }],
10302 multilinks: vec![],
10303 computed: vec![],
10304 constraints: vec![],
10305 indexes: vec![],
10306 partition: None,
10307 vector_indexes: vec![],
10308 search_indexes: vec![],
10309 triggers: vec![],
10310 junction: false,
10311 signals: vec![],
10312 },
10313 TypeDescriptor {
10314 name: "Org".into(),
10315 module: "default".into(),
10316 table: "Org".into(),
10317 abstract_: false,
10318 materialized: false,
10319 description: None,
10320 parents: vec![],
10321 interfaces: vec![],
10322 bases: vec![],
10323 properties: vec![id_prop(), name_prop()],
10324 links: vec![],
10325 multilinks: vec![],
10326 computed: vec![],
10327 constraints: vec![],
10328 indexes: vec![],
10329 partition: None,
10330 vector_indexes: vec![],
10331 search_indexes: vec![],
10332 triggers: vec![],
10333 junction: false,
10334 signals: vec![],
10335 },
10336 TypeDescriptor {
10337 name: "Marriage".into(),
10338 module: "default".into(),
10339 table: "Person.spouse".into(),
10340 abstract_: false,
10341 materialized: false,
10342 description: None,
10343 parents: vec![],
10344 interfaces: vec![],
10345 bases: vec![],
10346 properties: vec![
10347 id_prop(),
10348 PropertyDescriptor {
10349 name: "since".into(),
10350 pg_type: "int8".into(),
10351 nullable: true,
10352 default_sql: None,
10353 description: None,
10354 check_constraints: vec![],
10355 default_pyql: None,
10356 is_exclusive: false,
10357 is_pk: false,
10358 is_readonly: false,
10359 rewrites: vec![],
10360 tuple_members: None,
10361 column_type: None,
10362 },
10363 ],
10364 links: vec![
10365 LinkDescriptor {
10366 name: "source".into(),
10367 target: "default::Person".into(),
10368 nullable: false,
10369 through: None,
10370 description: None,
10371 default_pyql: None,
10372 is_exclusive: false,
10373 is_readonly: false,
10374 rewrites: vec![],
10375 on_delete: vec![],
10376 },
10377 LinkDescriptor {
10378 name: "target".into(),
10379 target: "default::Org".into(),
10380 nullable: false,
10381 through: None,
10382 description: None,
10383 default_pyql: None,
10384 is_exclusive: false,
10385 is_readonly: false,
10386 rewrites: vec![],
10387 on_delete: vec![],
10388 },
10389 ],
10390 multilinks: vec![],
10391 computed: vec![],
10392 constraints: vec![],
10393 indexes: vec![],
10394 partition: None,
10395 vector_indexes: vec![],
10396 search_indexes: vec![],
10397 triggers: vec![],
10398 junction: true,
10399 signals: vec![],
10400 },
10401 ],
10402 scalars: vec![],
10403 enums: vec![],
10404 named_tuples: vec![],
10405 globals: vec![],
10406 functions: vec![],
10407 aliases: vec![],
10408 channels: vec![],
10409 ..Default::default()
10410 }
10411 }
10412
10413 #[test]
10414 fn test_select_shape_over_junction_backed_single_link() {
10415 let schema = make_schema_with_junction_backed_link();
10416 let ast = crate::parse::parse("SELECT Person { name, spouse { name } }").unwrap();
10417 let ir = crate::ir::compile(&ast, &schema).unwrap();
10418 let out = emit(&ir);
10419 assert!(out.sql.contains("\"public\".\"Person.spouse\""), "got:\n{}", out.sql);
10421 assert!(out.sql.contains("\"jt\".source"), "got:\n{}", out.sql);
10422 assert!(out.sql.contains("\"jt\".target"), "got:\n{}", out.sql);
10423 assert!(!out.sql.contains("array_agg"), "got:\n{}", out.sql);
10425 assert!(!out.sql.contains("spouse_id"), "got:\n{}", out.sql);
10426 }
10427
10428 #[test]
10429 fn test_select_shape_over_junction_backed_single_link_with_link_property() {
10430 let schema = make_schema_with_junction_backed_link();
10436 let ast = crate::parse::parse("SELECT Person { name, spouse { name, @since } }").unwrap();
10437 let ir = crate::ir::compile(&ast, &schema).unwrap();
10438 let out = emit(&ir);
10439 assert!(out.sql.contains("\"jt\".\"since\""), "got:\n{}", out.sql);
10440 assert!(!out.sql.contains("array_agg"), "got:\n{}", out.sql);
10441 }
10442
10443 #[test]
10444 fn test_select_path_over_junction_backed_single_link() {
10445 let schema = make_schema_with_junction_backed_link();
10446 let ast = crate::parse::parse("SELECT Person.spouse { name }").unwrap();
10447 let ir = crate::ir::compile(&ast, &schema).unwrap();
10448 let out = emit(&ir);
10449 assert!(out.sql.contains("\"public\".\"Person.spouse\""), "got:\n{}", out.sql);
10450 assert!(out.sql.contains("\"source\""), "got:\n{}", out.sql);
10451 assert!(out.sql.contains("\"target\""), "got:\n{}", out.sql);
10452 assert!(!out.sql.contains("spouse_id"), "got:\n{}", out.sql);
10453 }
10454
10455 #[test]
10456 fn test_insert_with_junction_backed_single_link() {
10457 let schema = make_schema_with_junction_backed_link();
10458 let out = compile_and_emit_with(
10459 "INSERT Person { name := $name, \
10460 spouse := (SELECT Org FILTER .id = $oid) { @since := <int64>$since } }",
10461 &schema,
10462 );
10463 assert!(out.sql.contains("\"public\".\"Person.spouse\""), "got:\n{}", out.sql);
10464 assert!(out.sql.contains("\"since\""), "got:\n{}", out.sql);
10465 assert!(!out.sql.contains("spouse_id"), "got:\n{}", out.sql);
10466 assert!(out.sql.contains("ON CONFLICT (\"source\") DO UPDATE SET \"target\" = EXCLUDED.\"target\", \"since\" = EXCLUDED.\"since\""), "got:\n{}", out.sql);
10473 }
10474
10475 #[test]
10476 fn test_update_replace_junction_backed_single_link() {
10477 let schema = make_schema_with_junction_backed_link();
10478 let out = compile_and_emit_with(
10479 "UPDATE Person FILTER .id = $id SET { \
10480 spouse := (SELECT Org FILTER .id = $oid) { @since := <int64>$since } }",
10481 &schema,
10482 );
10483 assert!(
10485 out.sql.contains("DELETE FROM \"public\".\"Person.spouse\""),
10486 "got:\n{}",
10487 out.sql
10488 );
10489 assert!(
10490 out.sql.contains("INSERT INTO \"public\".\"Person.spouse\""),
10491 "got:\n{}",
10492 out.sql
10493 );
10494 assert!(out.sql.contains("ON CONFLICT (\"source\") DO UPDATE SET \"target\" = EXCLUDED.\"target\", \"since\" = EXCLUDED.\"since\""), "got:\n{}", out.sql);
10495 assert!(out.sql.contains("NOT IN"), "got:\n{}", out.sql);
10500 }
10501
10502 #[test]
10507 fn test_unless_conflict_else_update_keeps_its_filter() {
10508 let out = compile_and_emit(
10509 "INSERT Person { name := 'a' } UNLESS CONFLICT ON .name \
10510 ELSE (UPDATE Person FILTER .age < 30 SET { name := 'b' })",
10511 );
10512 assert!(
10513 out.sql.contains("DO UPDATE SET") && out.sql.contains("WHERE"),
10514 "the ELSE UPDATE's filter must survive as a DO UPDATE predicate:\n{}",
10515 out.sql
10516 );
10517 assert!(
10518 out.sql.contains("\"Person\".\"age\""),
10519 "the predicate must read the existing row, not `excluded`:\n{}",
10520 out.sql
10521 );
10522 }
10523
10524 #[test]
10526 fn test_unless_conflict_else_update_without_a_filter_has_no_predicate() {
10527 let out = compile_and_emit(
10528 "INSERT Person { name := 'a' } UNLESS CONFLICT ON .name ELSE (UPDATE Person SET { name := 'b' })",
10529 );
10530 let after_set = out.sql.split("DO UPDATE SET").nth(1).unwrap_or_default();
10531 assert!(
10532 !after_set.contains("WHERE"),
10533 "got a predicate we never asked for:\n{}",
10534 out.sql
10535 );
10536 }
10537
10538 #[test]
10539 fn test_junction_backed_single_link_rejected_in_unless_conflict_else() {
10540 let schema = make_schema_with_junction_backed_link();
10547 let ast = parse::parse(
10548 "INSERT Person { name := $name } \
10549 UNLESS CONFLICT ON .name ELSE (UPDATE Person SET { \
10550 spouse := (SELECT Org FILTER .id = $oid) })",
10551 )
10552 .unwrap();
10553 assert!(ir::compile(&ast, &schema).is_err());
10554 }
10555
10556 #[test]
10557 fn test_update_clear_junction_backed_single_link() {
10558 let schema = make_schema_with_junction_backed_link();
10559 let out = compile_and_emit_with("UPDATE Person FILTER .id = $id SET { spouse := {} }", &schema);
10560 assert!(
10561 out.sql.contains("DELETE FROM \"public\".\"Person.spouse\""),
10562 "got:\n{}",
10563 out.sql
10564 );
10565 assert!(
10566 !out.sql.contains("INSERT INTO \"public\".\"Person.spouse\""),
10567 "clearing must not also insert:\n{}",
10568 out.sql
10569 );
10570 }
10571
10572 #[test]
10573 fn test_update_clear_junction_backed_single_link_with_cast_empty_set() {
10574 let schema = make_schema_with_junction_backed_link();
10582 let out = compile_and_emit_with("UPDATE Person FILTER .id = $id SET { spouse := <Org>{} }", &schema);
10583 assert!(
10584 out.sql.contains("DELETE FROM \"public\".\"Person.spouse\""),
10585 "got:\n{}",
10586 out.sql
10587 );
10588 assert!(
10589 !out.sql.contains("INSERT INTO \"public\".\"Person.spouse\""),
10590 "clearing must not also insert:\n{}",
10591 out.sql
10592 );
10593 }
10594
10595 fn make_schema_with_through_and_prop() -> SchemaDescriptor {
10603 let id_prop = || PropertyDescriptor {
10604 name: "id".into(),
10605 pg_type: "uuid".into(),
10606 nullable: false,
10607 default_sql: Some("gen_random_uuid()".into()),
10608 description: None,
10609 default_pyql: None,
10610 check_constraints: vec![],
10611 is_exclusive: true,
10612 is_pk: true,
10613 is_readonly: true,
10614 rewrites: vec![],
10615 tuple_members: None,
10616 column_type: None,
10617 };
10618 let name_prop = || PropertyDescriptor {
10619 name: "name".into(),
10620 pg_type: "text".into(),
10621 nullable: false,
10622 default_sql: None,
10623 description: None,
10624 check_constraints: vec![],
10625 default_pyql: None,
10626 is_exclusive: false,
10627 is_pk: false,
10628 is_readonly: false,
10629 rewrites: vec![],
10630 tuple_members: None,
10631 column_type: None,
10632 };
10633 SchemaDescriptor {
10634 types: vec![
10635 TypeDescriptor {
10636 name: "Product".into(),
10637 module: "default".into(),
10638 table: "Product".into(),
10639 abstract_: false,
10640 materialized: false,
10641 description: None,
10642 parents: vec![],
10643 interfaces: vec![],
10644 bases: vec![],
10645 properties: vec![id_prop(), name_prop()],
10646 links: vec![],
10647 multilinks: vec![MultiLinkDescriptor {
10648 name: "tags".into(),
10649 target: "default::Tag".into(),
10650 through: Some("default::ProductTag".into()),
10651 nullable: false,
10652 description: None,
10653 default_pyql: None,
10654 on_delete: vec![],
10655 is_exclusive: false,
10656 }],
10657 computed: vec![],
10658 constraints: vec![],
10659 indexes: vec![],
10660 partition: None,
10661 vector_indexes: vec![],
10662 search_indexes: vec![],
10663 triggers: vec![],
10664 junction: false,
10665 signals: vec![],
10666 },
10667 TypeDescriptor {
10668 name: "Tag".into(),
10669 module: "default".into(),
10670 table: "Tag".into(),
10671 abstract_: false,
10672 materialized: false,
10673 description: None,
10674 parents: vec![],
10675 interfaces: vec![],
10676 bases: vec![],
10677 properties: vec![id_prop(), name_prop()],
10678 links: vec![],
10679 multilinks: vec![],
10680 computed: vec![],
10681 constraints: vec![],
10682 indexes: vec![],
10683 partition: None,
10684 vector_indexes: vec![],
10685 search_indexes: vec![],
10686 triggers: vec![],
10687 junction: false,
10688 signals: vec![],
10689 },
10690 TypeDescriptor {
10691 name: "ProductTag".into(),
10692 module: "default".into(),
10693 table: "Product.tags".into(),
10694 abstract_: false,
10695 materialized: false,
10696 description: None,
10697 parents: vec![],
10698 interfaces: vec![],
10699 bases: vec![],
10700 properties: vec![
10701 id_prop(),
10702 PropertyDescriptor {
10703 name: "weight".into(),
10704 pg_type: "float8".into(),
10705 nullable: false,
10706 default_sql: None,
10707 default_pyql: None,
10708 description: None,
10709 check_constraints: vec![],
10710 is_exclusive: false,
10711 is_pk: false,
10712 is_readonly: false,
10713 rewrites: vec![],
10714 tuple_members: None,
10715 column_type: None,
10716 },
10717 ],
10718 links: vec![],
10722 multilinks: vec![],
10723 computed: vec![],
10724 constraints: vec![],
10725 indexes: vec![],
10726 partition: None,
10727 vector_indexes: vec![],
10728 search_indexes: vec![],
10729 triggers: vec![],
10730 junction: true,
10731 signals: vec![],
10732 },
10733 ],
10734 scalars: vec![],
10735 enums: vec![],
10736 named_tuples: vec![],
10737 globals: vec![],
10738 functions: vec![],
10739 aliases: vec![],
10740 channels: vec![],
10741 ..Default::default()
10742 }
10743 }
10744
10745 #[test]
10746 fn test_link_property_in_a_links_own_filter() {
10747 let schema = make_schema_with_through_and_prop();
10751 let out = compile_and_emit_with("SELECT Product { tags: { } filter @weight > 0.5 }", &schema);
10752 assert!(out.sql.contains("\"jt\".\"weight\" > (0.5::float8)"), "{}", out.sql);
10753 }
10754
10755 #[test]
10756 fn test_exists_on_a_link_property() {
10757 let schema = make_schema_with_through_and_prop();
10760 let out = compile_and_emit_with("SELECT Product { tags: { } filter exists @weight }", &schema);
10761 assert!(out.sql.contains("\"jt\".\"weight\" IS NOT NULL"), "{}", out.sql);
10762 }
10763
10764 #[test]
10765 fn test_link_property_in_a_computed_link_filter() {
10766 let schema = make_schema_with_through_and_prop();
10767 let out = compile_and_emit_with(
10768 "SELECT Product { best := (select .tags filter @weight > 0.5 limit 1) { @weight } }",
10769 &schema,
10770 );
10771 assert!(out.sql.contains("\"jt\".\"weight\" > (0.5::float8)"), "{}", out.sql);
10772 assert!(out.sql.contains("LIMIT 1"), "{}", out.sql);
10773 }
10774
10775 #[test]
10776 fn test_link_property_in_a_projected_sub_select_reads_the_junction() {
10777 let schema = make_schema_with_through_and_prop();
10780 let out = compile_and_emit_with(
10781 "SELECT Product { t := (select .tags filter @weight > 0.5 limit 1).id }",
10782 &schema,
10783 );
10784 assert!(out.sql.contains("\"t3\".\"weight\" > (0.5::float8)"), "{}", out.sql);
10785 }
10786
10787 #[test]
10788 fn test_unknown_link_property_names_the_through_type() {
10789 let schema = make_schema_with_through_and_prop();
10790 let ast = parse::parse("SELECT Product { tags: { } filter @nope > 0.5 }").unwrap();
10791 let err = match ir::compile(&ast, &schema) {
10792 Ok(_) => panic!("expected a compile error"),
10793 Err(e) => format!("{e}"),
10794 };
10795 assert!(err.contains("default::ProductTag") && err.contains("nope"), "{err}");
10796 }
10797
10798 #[test]
10799 fn test_link_property_outside_a_link_says_where_it_belongs() {
10800 let schema = make_schema_with_through_and_prop();
10801 let ast = parse::parse("SELECT Product { t := @weight }").unwrap();
10802 let err = match ir::compile(&ast, &schema) {
10803 Ok(_) => panic!("expected a compile error"),
10804 Err(e) => format!("{e}"),
10805 };
10806 assert!(err.contains("is a link property"), "{err}");
10807 }
10808
10809 #[test]
10810 fn test_multilink_append_with_link_property() {
10811 let schema = make_schema_with_through_and_prop();
10812 let out = compile_and_emit_with(
10813 "UPDATE Product FILTER .id = $id SET { tags += (SELECT Tag FILTER .id = $tid) { @weight := <float64>$w } }",
10814 &schema,
10815 );
10816 assert!(out.sql.contains("\"weight\""), "missing weight column:\n{}", out.sql);
10818 assert!(
10821 out.sql
10822 .contains("ON CONFLICT (\"source\", \"target\") DO UPDATE SET \"weight\" = EXCLUDED.\"weight\""),
10823 "missing upsert conflict clause:\n{}",
10824 out.sql
10825 );
10826 }
10827
10828 #[test]
10829 fn test_multilink_append_union_with_different_link_property_values() {
10830 let schema = make_schema_with_through_and_prop();
10834 let out = compile_and_emit_with(
10835 "UPDATE Product FILTER .id = $id SET { \
10836 tags += (SELECT Tag FILTER .id = $aid) { @weight := <float64>$w1 } \
10837 union (SELECT Tag FILTER .id = $bid) { @weight := <float64>$w2 } \
10838 }",
10839 &schema,
10840 );
10841 assert!(
10842 out.sql.contains("UNION ALL"),
10843 "expected a UNION ALL between the two shaped targets:\n{}",
10844 out.sql
10845 );
10846 assert_eq!(
10849 out.sql.matches("AS \"weight\"").count(),
10850 2,
10851 "each union branch must project its own weight:\n{}",
10852 out.sql
10853 );
10854 }
10855
10856 #[test]
10857 fn test_multilink_append_without_link_property_keeps_do_nothing() {
10858 let schema = make_schema_with_through_and_prop();
10861 let out = compile_and_emit_with(
10862 "UPDATE Product FILTER .id = $id SET { tags += (SELECT Tag FILTER .id = $tid) }",
10863 &schema,
10864 );
10865 assert!(
10866 out.sql.contains("ON CONFLICT DO NOTHING"),
10867 "expected plain DO NOTHING when no link properties are set:\n{}",
10868 out.sql
10869 );
10870 assert!(
10871 !out.sql.contains("\"weight\""),
10872 "unexpected weight column with no link property assignment:\n{}",
10873 out.sql
10874 );
10875 }
10876
10877 #[test]
10878 fn test_multilink_link_property_rejected_on_standard_junction() {
10879 let ast = crate::parse::parse(
10882 "UPDATE Person FILTER .id = $id SET { posts += (SELECT Post FILTER .title = $t) { @weight := <float64>$w } }",
10883 ).unwrap();
10884 match crate::ir::compile(&ast, &make_schema()) {
10885 Ok(_) => panic!("expected a compile error for link property on a Standard junction"),
10886 Err(e) => assert!(
10887 e.to_string().contains("Through"),
10888 "expected a Through[...]-related error, got: {e}"
10889 ),
10890 }
10891 }
10892
10893 #[test]
10894 fn test_multilink_link_property_rejected_on_remove() {
10895 let schema = make_schema_with_through_and_prop();
10896 let ast = crate::parse::parse(
10897 "UPDATE Product FILTER .id = $id SET { tags -= (SELECT Tag FILTER .id = $tid) { @weight := <float64>$w } }",
10898 )
10899 .unwrap();
10900 match crate::ir::compile(&ast, &schema) {
10901 Ok(_) => panic!("expected a compile error for link property on a remove (-=)"),
10902 Err(e) => assert!(
10903 e.to_string().contains("removing"),
10904 "expected a remove-related error, got: {e}"
10905 ),
10906 }
10907 }
10908
10909 #[test]
10910 fn test_insert_with_multilink_assignment() {
10911 let schema = make_schema_with_through_and_prop();
10916 let out = compile_and_emit_with(
10917 "INSERT Product { name := $name, tags := (SELECT Tag FILTER .id = $tid) { @weight := <float64>$w } }",
10918 &schema,
10919 );
10920 assert!(
10922 out.sql.contains("\"_w__ids\" AS (\nINSERT INTO"),
10923 "missing row-insert CTE:\n{}",
10924 out.sql
10925 );
10926 assert!(
10927 out.sql.contains("\"_w__ml_add_0\" AS ("),
10928 "missing junction-append CTE:\n{}",
10929 out.sql
10930 );
10931 assert!(out.sql.contains("\"weight\""), "missing weight column:\n{}", out.sql);
10932 assert!(
10933 out.sql.contains("\"_w\" AS (\n SELECT * FROM \"_w__ids\"\n)"),
10934 "missing _w passthrough:\n{}",
10935 out.sql
10936 );
10937 assert_eq!(
10938 out.sql.matches("WITH\n").count(),
10939 1,
10940 "must be a single flat top-level WITH block:\n{}",
10941 out.sql
10942 );
10943 }
10944
10945 #[test]
10953 fn test_user_with_binding_merges_into_a_statements_own_ctes() {
10954 let schema = make_schema_with_through_and_prop();
10955 for query in [
10956 "with t := (select Tag filter .id = $tid) insert Product { name := $name, tags := t }",
10957 "with t := (select Tag filter .id = $tid) \
10958 update Product filter .name = $name set { tags += t }",
10959 ] {
10960 let out = compile_and_emit_with(query, &schema);
10961 let flat: String = out.sql.split_whitespace().collect::<Vec<_>>().join(" ");
10962 assert!(
10963 !flat.contains(") WITH "),
10964 "a second WITH clause is invalid SQL:\n{}",
10965 out.sql
10966 );
10967 assert_eq!(
10968 out.sql.matches("WITH").count(),
10969 1,
10970 "exactly one WITH keyword expected:\n{}",
10971 out.sql
10972 );
10973 }
10974 }
10975
10976 #[test]
10977 fn test_with_bound_insert_with_multilink_assignment() {
10978 let schema = make_schema_with_through_and_prop();
10982 let out = compile_and_emit_with(
10983 "with insert0 := (insert Product { name := $name, tags := (select Tag filter .id = $tid) }) select insert0",
10984 &schema,
10985 );
10986 assert!(
10987 out.sql.contains("\"insert0__ids\" AS (\nINSERT INTO"),
10988 "missing row-insert CTE:\n{}",
10989 out.sql
10990 );
10991 assert!(
10992 out.sql.contains("\"insert0__ml_add_0\" AS ("),
10993 "missing junction-append CTE:\n{}",
10994 out.sql
10995 );
10996 assert!(
10997 out.sql
10998 .contains("\"insert0\" AS (\n SELECT * FROM \"insert0__ids\"\n)"),
10999 "missing insert0 passthrough:\n{}",
11000 out.sql
11001 );
11002 assert_eq!(
11003 out.sql.matches("WITH\n").count(),
11004 1,
11005 "must be a single flat top-level WITH block:\n{}",
11006 out.sql
11007 );
11008 }
11009
11010 #[test]
11011 fn test_with_block_cte_over_computed_global_merges_into_single_with_clause() {
11012 let mut schema = make_schema();
11021 schema.globals.push(GlobalDescriptor {
11022 name: "current_user_id".into(),
11023 module: "default".into(),
11024 scalar_type: "std::uuid".into(),
11025 required: false,
11026 default_expr: None,
11027 computed_expr: None,
11028 });
11029 schema.globals.push(GlobalDescriptor {
11030 name: "current_user".into(),
11031 module: "default".into(),
11032 scalar_type: "Person".into(),
11033 required: false,
11034 default_expr: None,
11035 computed_expr: Some("select default::Person filter .id = global current_user_id".into()),
11036 });
11037 let out = compile_and_emit_with("with\n user := (select global current_user)\nselect user;", &schema);
11038 assert_eq!(
11039 out.sql.matches("WITH").count(),
11040 1,
11041 "must be a single WITH clause, got:\n{}",
11042 out.sql
11043 );
11044 }
11045
11046 #[test]
11047 fn test_several_mutations_in_a_free_set_each_become_a_cte() {
11048 let schema = make_schema();
11049 let out = compile_and_emit_with(
11050 "select { (update Person filter .name = 'a' set { age := 1 }), \
11051 (update Company filter .name = 'b' set { name := 'c' }) }",
11052 &schema,
11053 );
11054 assert_eq!(
11055 out.sql.matches("UPDATE").count(),
11056 2,
11057 "both mutations have to run, got:\n{}",
11058 out.sql
11059 );
11060 assert!(out.sql.contains("WITH"), "each becomes a CTE, got:\n{}", out.sql);
11061 }
11062
11063 #[test]
11064 fn test_updating_a_binding_touches_only_its_own_rows() {
11065 let schema = make_schema();
11068 let out = compile_and_emit_with(
11069 "with mine := (select Person filter .name = 'a') select { (update mine set { age := 1 }) }",
11070 &schema,
11071 );
11072 assert!(
11073 out.sql.contains("UPDATE") && out.sql.contains("= ANY(ARRAY(SELECT"),
11074 "expected the update narrowed to the binding's rows, got:\n{}",
11075 out.sql
11076 );
11077 }
11078
11079 #[test]
11080 fn test_updating_a_traversal_touches_only_the_rows_it_lands_on() {
11081 let mut schema = make_schema();
11082 let company = schema
11085 .types
11086 .iter_mut()
11087 .find(|t| t.name == "Company")
11088 .expect("test schema has a Company type");
11089 company.properties.push(crate::schema::PropertyDescriptor {
11090 name: "id".into(),
11091 pg_type: "uuid".into(),
11092 nullable: false,
11093 default_sql: Some("uuidv7()".into()),
11094 default_pyql: None,
11095 description: None,
11096 check_constraints: vec![],
11097 is_exclusive: true,
11098 is_pk: true,
11099 is_readonly: true,
11100 rewrites: vec![],
11101 tuple_members: None,
11102 column_type: None,
11103 });
11104 let out = compile_and_emit_with(
11105 "with mine := (select Person filter .name = 'a') select { (update mine.company set { name := 'x' }) }",
11106 &schema,
11107 );
11108 assert!(
11109 out.sql.contains("= ANY(ARRAY(SELECT"),
11110 "expected the update narrowed to the traversal's rows, got:\n{}",
11111 out.sql
11112 );
11113 assert!(
11114 out.sql.contains("\"Company\""),
11115 "expected the table the traversal ends on, got:\n{}",
11116 out.sql
11117 );
11118 }
11119
11120 #[test]
11121 fn test_aggregate_over_a_multi_valued_path_keeps_the_set_flat() {
11122 let schema = make_schema();
11127 let out = compile_and_emit_with(
11128 "with a := (select Person limit 1) select { titles := array_agg(a.posts.title) }",
11129 &schema,
11130 );
11131 assert!(
11132 !out.sql.contains("array_agg(ARRAY("),
11133 "the aggregate must take the set, not the array standing for it, got:\n{}",
11134 out.sql
11135 );
11136 assert!(
11137 out.sql.contains("array_agg(") && out.sql.contains("\"title\""),
11138 "expected the aggregate over the traversal's own column, got:\n{}",
11139 out.sql
11140 );
11141 assert!(
11144 out.sql.contains("ROW(") && !out.sql.contains("jsonb_build_object"),
11145 "expected a composite row, got:\n{}",
11146 out.sql
11147 );
11148 }
11149
11150 #[test]
11151 fn test_with_bound_root_read_by_name_inside_a_nested_select() {
11152 let schema = make_schema();
11157 let out = compile_and_emit_with(
11158 "with
11159 owner := (select Person limit 1)
11160select owner { posts := (select owner.posts.title) };",
11161 &schema,
11162 );
11163 assert!(
11164 out.sql.contains("WITH") && out.sql.contains("\"title\""),
11165 "expected the nested select to traverse the binding, got:\n{}",
11166 out.sql
11167 );
11168 }
11169
11170 #[test]
11171 fn test_path_traversal_into_with_bound_cte_of_object_type() {
11172 let mut schema = make_schema();
11178 schema.globals.push(GlobalDescriptor {
11179 name: "current_user_id".into(),
11180 module: "default".into(),
11181 scalar_type: "std::uuid".into(),
11182 required: false,
11183 default_expr: None,
11184 computed_expr: None,
11185 });
11186 schema.globals.push(GlobalDescriptor {
11187 name: "current_user".into(),
11188 module: "default".into(),
11189 scalar_type: "Person".into(),
11190 required: false,
11191 default_expr: None,
11192 computed_expr: Some("select default::Person filter .id = global current_user_id".into()),
11193 });
11194 let out = compile_and_emit_with(
11195 "with\n user := (select global current_user)\nselect user.name;",
11196 &schema,
11197 );
11198 assert!(
11199 out.sql.contains("FROM \"user\""),
11200 "expected path traversal from the CTE, got:\n{}",
11201 out.sql
11202 );
11203 assert_eq!(
11204 out.sql.matches("WITH").count(),
11205 1,
11206 "must be a single WITH clause, got:\n{}",
11207 out.sql
11208 );
11209 }
11210
11211 #[test]
11212 fn test_with_bound_schema_object_property_access_in_filter_expression() {
11213 let schema = make_schema();
11221 let out = compile_and_emit_with(
11222 "with\n person := (select detached Person filter .id = <uuid>$id)\n\
11223 select Person filter .id not in person.id;",
11224 &schema,
11225 );
11226 assert!(
11227 out.sql.contains("<> ALL((SELECT") && out.sql.contains(".\"id\"") && out.sql.contains("FROM \"person\""),
11228 "got:\n{}",
11229 out.sql
11230 );
11231 }
11232
11233 #[test]
11234 fn test_with_bound_schema_object_scalar_property_access() {
11235 let schema = make_schema();
11239 let out = compile_and_emit_with(
11240 "with\n person := (select detached Person filter .id = <uuid>$id)\n\
11241 select Person filter .name = person.name;",
11242 &schema,
11243 );
11244 assert!(
11245 out.sql.contains("(SELECT") && out.sql.contains(".\"name\"") && out.sql.contains("FROM \"person\""),
11246 "got:\n{}",
11247 out.sql
11248 );
11249 }
11250
11251 #[test]
11252 fn test_with_bound_schema_object_link_traversal_reaches_nested_property() {
11253 let schema = make_schema();
11260 let out = compile_and_emit_with(
11261 "with\n person := (select detached Person filter .id = <uuid>$id)\n\
11262 select Company filter .name = person.company.name;",
11263 &schema,
11264 );
11265 assert!(
11266 out.sql.contains("(SELECT") && out.sql.contains("\"name\"") && out.sql.contains("\"Company\""),
11267 "got:\n{}",
11268 out.sql
11269 );
11270 }
11271
11272 #[test]
11273 fn test_with_bound_schema_object_bare_link_reduces_to_id() {
11274 let schema = make_schema();
11278 let out = compile_and_emit_with(
11279 "with\n person := (select detached Person filter .id = <uuid>$id)\n\
11280 select Person filter .id = person.company;",
11281 &schema,
11282 );
11283 assert!(
11284 out.sql.contains("(SELECT") && out.sql.contains("\"id\""),
11285 "got:\n{}",
11286 out.sql
11287 );
11288 }
11289
11290 #[test]
11291 fn test_with_bound_schema_object_unknown_property_suggests_a_close_match() {
11292 let schema = make_schema();
11293 let ast = parse::parse(
11294 "with\n person := (select detached Person filter .id = <uuid>$id)\n\
11295 select Person filter .name = person.nam;",
11296 )
11297 .unwrap();
11298 match ir::compile(&ast, &schema) {
11299 Err(err) => assert!(format!("{err}").contains("Did you mean 'name'"), "got: {err}"),
11300 Ok(_) => panic!("expected a compile error"),
11301 }
11302 }
11303
11304 #[test]
11305 fn test_with_bound_free_object_passthrough_preserves_all_fields() {
11306 let out = compile_and_emit("with\n test := { test2 := 1.0, test3 := 'str' }\nselect test;");
11312 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
11313 panic!("expected Object shape, got {:?}", out.shape.root)
11314 };
11315 assert_eq!(pointers.len(), 2);
11316 assert!(matches!(&pointers[0], ShapeNode::Scalar { name, .. } if name == "test2"));
11317 assert!(matches!(&pointers[1], ShapeNode::Scalar { name, .. } if name == "test3"));
11318 }
11319
11320 #[test]
11321 fn test_with_bound_free_object_field_access() {
11322 let out = compile_and_emit("with\n test := { test2 := 1.0, test3 := 'str' }\nselect test.test2;");
11327 assert!(out.sql.contains("\"test2\" FROM \"test\""), "got:\n{}", out.sql);
11328 }
11329
11330 #[test]
11331 fn test_with_bound_free_object_nested_field_access_chain() {
11332 let out =
11337 compile_and_emit("with\n test := { test2 := 1.0, test3 := { foo := 'bar' } }\nselect test.test3.foo;");
11338 assert!(out.sql.contains("\"test3\" FROM \"test\""), "got:\n{}", out.sql);
11339 assert!(
11340 out.sql.contains("->'foo'"),
11341 "expected jsonb field extraction, got:\n{}",
11342 out.sql
11343 );
11344 }
11345
11346 #[test]
11347 fn test_with_bound_free_object_nested_field_access_wrong_field_errors() {
11348 let schema = make_schema();
11351 let ast = parse::parse("with\n test := { test2 := 1.0, test3 := { foo := 'bar' } }\nselect test.test3.nope;")
11352 .unwrap();
11353 assert!(ir::compile(&ast, &schema).is_err());
11354 }
11355
11356 #[test]
11357 fn test_nested_free_object_literal_in_computed_shape_element() {
11358 let out = compile_and_emit("select default::Person { id, test := { foo := 'bar' } };");
11364 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
11365 panic!("expected Object shape")
11366 };
11367 let test_node = pointers
11368 .iter()
11369 .find(|p| matches!(p, ShapeNode::NamedTuple { name, .. } if name == "test"))
11370 .unwrap_or_else(|| panic!("expected a NamedTuple shape node for 'test', got {:?}", pointers));
11371 assert!(matches!(
11372 test_node,
11373 ShapeNode::NamedTuple {
11374 is_free_object: true,
11375 ..
11376 }
11377 ));
11378 }
11379
11380 #[test]
11381 fn test_bare_free_cte_reference_in_computed_shape_collapses_to_empty() {
11382 let out = compile_and_emit(
11387 "with\n test := { test2 := 1.0, test3 := 'str' }\n\
11388 select default::Person { id, test := test };",
11389 );
11390 assert!(
11391 out.sql.contains("jsonb_build_object()"),
11392 "expected an empty free object, got:\n{}",
11393 out.sql
11394 );
11395 }
11396
11397 #[test]
11398 fn test_shaped_free_cte_reference_projects_fields() {
11399 let out = compile_and_emit(
11403 "with\n test := { test2 := 1.0, test3 := 'str' }\n\
11404 select default::Person { id, test := test { test2 } };",
11405 );
11406 assert!(out.sql.contains("jsonb_build_object('test2'"), "got:\n{}", out.sql);
11407 assert!(
11408 !out.sql.contains("'test3'"),
11409 "test3 should not be projected, got:\n{}",
11410 out.sql
11411 );
11412 }
11413
11414 #[test]
11415 fn test_insert_multilink_remove_rejected() {
11416 let schema = make_schema_with_through_and_prop();
11417 let ast =
11418 crate::parse::parse("INSERT Product { name := $name, tags -= (SELECT Tag FILTER .id = $tid) }").unwrap();
11419 match crate::ir::compile(&ast, &schema) {
11420 Ok(_) => panic!("expected a compile error for `-=` on a multi-link at insert time"),
11421 Err(e) => assert!(
11422 e.to_string().contains("nothing to remove"),
11423 "expected a 'nothing to remove yet' error, got: {e}"
11424 ),
11425 }
11426 }
11427
11428 #[test]
11429 fn test_multilink_junction_info_disambiguates_self_referencing_through_type() {
11430 let schema = make_schema_with_through();
11437 let out = compile_and_emit_with(
11438 "UPDATE Person FILTER .id = $id SET { friends += (SELECT Person FILTER .id = $fid) }",
11439 &schema,
11440 );
11441 assert!(
11442 out.sql.contains("(\"person\", \"friend\")"),
11443 "expected two distinct FK columns:\n{}",
11444 out.sql
11445 );
11446 assert!(
11447 !out.sql.contains("(\"person\", \"person\")"),
11448 "source/target collapsed to the same column:\n{}",
11449 out.sql
11450 );
11451 }
11452
11453 #[test]
11454 fn test_shape_descriptor_scalars() {
11455 let out = compile_and_emit("SELECT Person { name, age }");
11456 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
11457 panic!()
11458 };
11459 assert_eq!(pointers.len(), 4); assert!(matches!(&pointers[0], ShapeNode::Scalar { name, position: 0 } if name == "__type__"));
11461 assert!(matches!(&pointers[1], ShapeNode::Scalar { name, position: 1 } if name == "id"));
11462 assert!(matches!(&pointers[2], ShapeNode::Scalar { name, position: 2 } if name == "name"));
11463 assert!(matches!(&pointers[3], ShapeNode::Scalar { name, position: 3 } if name == "age"));
11464 }
11465
11466 #[test]
11467 fn test_shape_descriptor_multi_link() {
11468 let out = compile_and_emit("SELECT Person { name, posts { title } }");
11469 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
11470 panic!()
11471 };
11472 assert_eq!(pointers.len(), 4);
11474 let ShapeNode::Array {
11475 name,
11476 position,
11477 element,
11478 } = &pointers[3]
11479 else {
11480 panic!()
11481 };
11482 assert_eq!(name, "posts");
11483 assert_eq!(*position, 3);
11484 let ShapeNode::Object {
11485 pointers: elem_pointers,
11486 ..
11487 } = element.as_ref()
11488 else {
11489 panic!()
11490 };
11491 assert_eq!(elem_pointers.len(), 2);
11494 }
11495
11496 #[test]
11497 fn test_select_order_by_limit() {
11498 let out = compile_and_emit("SELECT Person { name } ORDER BY .name ASC LIMIT 10");
11499 assert!(out.sql.contains("ORDER BY"));
11500 assert!(out.sql.contains("LIMIT 10"));
11501 }
11502
11503 #[test]
11504 fn test_insert_returning() {
11505 let out = compile_and_emit("INSERT Person { name := 'Alice', age := 30 }");
11506 assert!(out.sql.contains("INSERT INTO \"public\".\"Person\""));
11507 assert!(out.sql.contains("RETURNING"));
11508 assert!(out.sql.contains("'default::Person'::text"));
11509 assert!(out.sql.contains(") AS result"));
11510 let ShapeNode::Object {
11512 cardinality, pointers, ..
11513 } = &out.shape.root
11514 else {
11515 panic!()
11516 };
11517 assert_eq!(*cardinality, Cardinality::Required);
11518 assert!(
11520 pointers
11521 .iter()
11522 .any(|f| matches!(f, ShapeNode::Scalar { name, .. } if name == "id"))
11523 );
11524 assert!(
11525 !pointers
11526 .iter()
11527 .any(|f| matches!(f, ShapeNode::Scalar { name, .. } if name == "name"))
11528 );
11529 }
11530
11531 #[test]
11532 fn test_update_returning() {
11533 let out = compile_and_emit("UPDATE Person FILTER .name = $name SET { age := 31 }");
11534 assert!(out.sql.contains("UPDATE \"public\".\"Person\""));
11535 assert!(out.sql.contains("SET"));
11536 assert!(out.sql.contains("RETURNING"));
11538 assert!(out.sql.contains("'default::Person'::text"));
11539 assert!(!out.sql.contains("\"name\"::text"), "bare UPDATE must not return name");
11540 }
11541
11542 #[test]
11543 fn test_update_set_tuple_param_cast_uses_direct_jsonb_cast_not_to_jsonb() {
11544 let out = compile_and_emit("UPDATE Person FILTER .id = $id SET { age := <tuple<x: float64>>$val }");
11551 assert!(
11552 out.sql.contains(")::jsonb"),
11553 "expected a direct ::jsonb cast, got:\n{}",
11554 out.sql
11555 );
11556 assert!(
11557 !out.sql.contains("to_jsonb($"),
11558 "must not pass a bare param straight into to_jsonb(): got:\n{}",
11559 out.sql
11560 );
11561 }
11562
11563 #[test]
11564 fn test_empty_set_cast_to_object_type_clears_optional_link() {
11565 let out = compile_and_emit("UPDATE Person FILTER .id = $id SET { company := <default::Company>{} }");
11574 assert!(out.sql.contains("\"company_id\" = NULL"), "got:\n{}", out.sql);
11575 }
11576
11577 #[test]
11578 fn test_an_aggregate_over_a_set_literal_inside_a_shape() {
11579 let out = compile_and_emit("UPDATE Person FILTER .id = $id SET { age := min({<int64>100, .age}) }");
11585 assert!(out.sql.contains("min(v)"), "got:\n{}", out.sql);
11586 assert!(
11587 out.sql.contains("UNION ALL"),
11588 "both operands must reach the aggregate:\n{}",
11589 out.sql
11590 );
11591 }
11592
11593 #[test]
11594 fn test_an_aggregate_over_a_set_literal_reads_the_subject() {
11595 let out = compile_and_emit("SELECT Person { capped := min({<int64>100, .age}) }");
11598 assert!(out.sql.contains("min(v)"), "got:\n{}", out.sql);
11599 assert!(
11600 out.sql.contains("\"age\""),
11601 "the relative path must reach the column:\n{}",
11602 out.sql
11603 );
11604 }
11605
11606 #[test]
11607 fn test_an_aggregate_over_a_union_inside_a_shape() {
11608 let out = compile_and_emit("SELECT Person { capped := min((<int64>100 union .age)) }");
11611 assert!(out.sql.contains("min(v)"), "got:\n{}", out.sql);
11612 assert!(
11613 out.sql.contains("UNION ALL"),
11614 "both arms must reach the aggregate:\n{}",
11615 out.sql
11616 );
11617 }
11618
11619 #[test]
11620 fn test_an_aggregate_over_a_union_of_three_arms_flattens() {
11621 let out = compile_and_emit("SELECT Person { capped := min((<int64>1 union <int64>2 union .age)) }");
11622 assert_eq!(
11623 out.sql.matches("UNION ALL").count(),
11624 2,
11625 "three arms means two joins:\n{}",
11626 out.sql
11627 );
11628 }
11629
11630 #[test]
11631 fn test_an_aggregate_over_an_empty_set_emits_no_row_source() {
11632 for query in ["SELECT Person { capped := min({}) }", "SELECT min({})"] {
11636 let ast = parse::parse(query).expect("parse failed");
11637 let sql = ir::compile(&ast, &make_schema())
11638 .map(|ir| emit(&ir).sql)
11639 .unwrap_or_default();
11640 assert!(
11641 !sql.contains("FROM () AS"),
11642 "{query} emitted an empty row source:\n{sql}"
11643 );
11644 }
11645 }
11646
11647 #[test]
11648 fn test_delete_returning() {
11649 let out = compile_and_emit("DELETE Person FILTER .id = $id");
11650 assert!(out.sql.contains("DELETE FROM \"public\".\"Person\""));
11651 assert!(out.sql.contains("RETURNING"));
11653 assert!(out.sql.contains("'default::Person'::text"));
11654 assert!(!out.sql.contains("\"name\"::text"), "bare DELETE must not return name");
11655 }
11656
11657 #[test]
11658 fn test_select_over_insert() {
11659 let out = compile_and_emit("SELECT (INSERT Person { name := $name, age := $age }) { id, name }");
11660 assert!(out.sql.contains("WITH\n\"_dml\" AS ("));
11662 assert!(out.sql.contains("INSERT INTO"));
11663 assert!(out.sql.contains("RETURNING *"));
11664 assert!(out.sql.contains("'default::Person'::text"));
11666 assert!(out.sql.contains("\"name\"::text"));
11667 }
11668
11669 #[test]
11670 fn test_select_over_update() {
11671 let out = compile_and_emit("SELECT (UPDATE Person FILTER .id = $id SET { name := $name }) { id, name }");
11672 assert!(out.sql.contains("WITH\n\"_dml\" AS ("));
11673 assert!(out.sql.contains("UPDATE"));
11674 assert!(out.sql.contains("RETURNING \"t1\".*"), "{}", out.sql);
11675 assert!(out.sql.contains("\"name\"::text"));
11676 }
11677
11678 #[test]
11679 fn test_insert_user_specified_id_denied_by_default() {
11680 let schema = make_schema();
11681 let ast = parse::parse("INSERT Person { id := <uuid>$id, name := $name, age := $age }").unwrap();
11682 match ir::compile_with_config(&ast, &schema, &ir::SessionConfig::default()) {
11683 Err(err) => assert!(err.to_string().contains("cannot assign to property 'id'"), "got: {err}"),
11684 Ok(_) => panic!("expected id assignment to be denied by default"),
11685 }
11686 }
11687
11688 #[test]
11689 fn test_insert_user_specified_id_allowed_when_configured() {
11690 let schema = make_schema();
11691 let ast = parse::parse("INSERT Person { id := <uuid>$id, name := $name, age := $age }").unwrap();
11692 let config = ir::SessionConfig {
11693 allow_user_specified_id: true,
11694 };
11695 let ir_out = ir::compile_with_config(&ast, &schema, &config)
11696 .expect("expected id assignment to be allowed with allow_user_specified_id");
11697 let out = emit(&ir_out);
11698 assert!(out.sql.contains("INSERT INTO"));
11699 }
11700
11701 #[test]
11702 fn test_update_user_specified_id_denied_even_when_configured() {
11703 let schema = make_schema();
11704 let ast = parse::parse("UPDATE Person FILTER .name = $name SET { id := <uuid>$id }").unwrap();
11705 let config = ir::SessionConfig {
11706 allow_user_specified_id: true,
11707 };
11708 match ir::compile_with_config(&ast, &schema, &config) {
11709 Err(err) => assert!(err.to_string().contains("cannot assign to property 'id'"), "got: {err}"),
11710 Ok(_) => panic!("expected UPDATE to always deny reassigning id"),
11711 }
11712 }
11713
11714 #[test]
11715 fn test_select_over_update_multilink_only() {
11716 let out = compile_and_emit(
11726 "SELECT (UPDATE Person FILTER .id = $id SET { posts += (SELECT Post FILTER .title = $title) }) { id, name }",
11727 );
11728 assert!(
11729 out.sql.contains("\"_dml__ml_add_0\""),
11730 "missing junction-append CTE:\n{}",
11731 out.sql
11732 );
11733 assert!(out.sql.contains("INSERT INTO"), "missing junction INSERT:\n{}", out.sql);
11734 assert!(
11737 out.sql.contains("\"_dml__ids\" AS (\nSELECT"),
11738 "expected SELECT-based _ids CTE:\n{}",
11739 out.sql
11740 );
11741 assert!(
11742 !out.sql.contains("SET\n\nWHERE") && !out.sql.contains("SET \nWHERE"),
11743 "empty SET clause regression:\n{}",
11744 out.sql
11745 );
11746 assert_eq!(
11749 out.sql.matches("WITH\n").count(),
11750 1,
11751 "junction CTE must not be nested in a second WITH:\n{}",
11752 out.sql
11753 );
11754 assert!(
11756 out.sql.contains("\"_dml\" AS (\n SELECT * FROM \"_dml__ids\"\n)"),
11757 "missing _dml passthrough:\n{}",
11758 out.sql
11759 );
11760 }
11761
11762 #[test]
11763 fn test_select_over_update_scalar_and_multilink() {
11764 let out = compile_and_emit(
11768 "SELECT (UPDATE Person FILTER .id = $id SET { name := $name, posts += (SELECT Post FILTER .title = $title) }) { id, name }",
11769 );
11770 assert!(
11771 out.sql.contains("\"_dml__ml_add_0\""),
11772 "missing junction-append CTE:\n{}",
11773 out.sql
11774 );
11775 assert!(
11776 out.sql.contains("\"_dml__ids\" AS (\nUPDATE"),
11777 "expected UPDATE-based _ids CTE:\n{}",
11778 out.sql
11779 );
11780 assert!(
11781 out.sql.contains("\"name\" = "),
11782 "missing scalar SET assignment:\n{}",
11783 out.sql
11784 );
11785 assert_eq!(
11786 out.sql.matches("WITH\n").count(),
11787 1,
11788 "junction CTE must not be nested in a second WITH:\n{}",
11789 out.sql
11790 );
11791 }
11792
11793 #[test]
11794 fn test_with_bound_insert_and_multilink_update_forward_ref() {
11795 let out = compile_and_emit(
11803 "with insert0 := (insert Post { title := $title }), update0 := (update Person filter .id = $id set { posts += (select insert0) }) select { insert0, update0 }",
11804 );
11805 assert!(
11806 out.sql.contains("\"insert0\" AS (\n INSERT INTO"),
11807 "missing insert0 CTE:\n{}",
11808 out.sql
11809 );
11810 assert!(
11811 out.sql.contains("\"update0__ml_add_0\""),
11812 "missing junction-append CTE for update0:\n{}",
11813 out.sql
11814 );
11815 assert!(
11816 out.sql.contains("\"update0__ids\" AS (\nSELECT"),
11817 "expected SELECT-based update0 ids CTE (no scalar changes):\n{}",
11818 out.sql
11819 );
11820 assert!(
11821 out.sql
11822 .contains("\"update0\" AS (\n SELECT * FROM \"update0__ids\"\n)"),
11823 "missing update0 passthrough:\n{}",
11824 out.sql
11825 );
11826 assert_eq!(
11829 out.sql.matches("WITH\n").count(),
11830 1,
11831 "must be a single flat top-level WITH block:\n{}",
11832 out.sql
11833 );
11834 }
11835
11836 #[test]
11837 fn test_select_over_delete() {
11838 let out = compile_and_emit("SELECT (DELETE Person FILTER .id = $id) { id, name }");
11839 assert!(out.sql.contains("WITH\n\"_dml\" AS ("));
11840 assert!(out.sql.contains("DELETE FROM"));
11841 assert!(out.sql.contains("RETURNING *"));
11842 assert!(out.sql.contains("\"name\"::text"));
11843 }
11844
11845 fn make_schema_with_rewrite() -> SchemaDescriptor {
11846 use crate::schema::RewriteEntry;
11847 let mut schema = make_schema();
11848 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
11850 person.properties.push(PropertyDescriptor {
11851 name: "slug".into(),
11852 pg_type: "text".into(),
11853 nullable: true,
11854 default_sql: None,
11855 default_pyql: None,
11856 description: None,
11857 check_constraints: vec![],
11858 is_exclusive: false,
11859 is_pk: false,
11860 is_readonly: false,
11861 rewrites: vec![
11862 RewriteEntry {
11863 on: 1,
11864 handler: "str_lower(.name)".into(),
11865 }, RewriteEntry {
11867 on: 2,
11868 handler: "str_lower(.name)".into(),
11869 }, ],
11871 tuple_members: None,
11872 column_type: None,
11873 });
11874 schema
11875 }
11876
11877 #[test]
11878 fn a_link_assigned_from_a_select_keeps_its_clauses() {
11879 let out = compile_and_emit(
11880 "INSERT Person { name := 'a', age := 1, company := (SELECT Company ORDER BY .name LIMIT 1) }",
11881 );
11882 assert!(
11883 out.sql.contains("ORDER BY") && out.sql.contains("LIMIT 1"),
11884 "{}",
11885 out.sql
11886 );
11887 }
11888
11889 #[test]
11890 fn a_rewrite_leaves_the_statement_alone() {
11891 let schema = make_schema_with_rewrite();
11893 for query in [
11894 "INSERT Person { name := $name, age := 30 }",
11895 "UPDATE Person FILTER .id = $id SET { name := $name }",
11896 ] {
11897 let out = compile_and_emit_with(query, &schema);
11898 assert!(!out.sql.contains("\"slug\""), "{}", out.sql);
11899 }
11900 }
11901
11902 #[test]
11903 fn a_rewrite_reads_the_row_being_written() {
11904 let schema = make_schema_with_rewrite();
11905 let assignments = crate::ir::compile_rewrite_assignments("default::Person", 1, &schema).unwrap();
11906 let [assignment] = assignments.as_slice() else {
11907 panic!("one rewrite on insert")
11908 };
11909 assert_eq!(assignment.column, "slug");
11910 assert!(assignment.sql.contains("NEW.\"name\""), "{}", assignment.sql);
11911 }
11912
11913 #[test]
11914 fn rewrites_become_before_triggers() {
11915 let ddl = crate::export::export_schema(&make_schema_with_rewrite()).unwrap();
11916 for event in ["BEFORE INSERT", "BEFORE UPDATE"] {
11917 assert!(
11918 ddl.contains(&format!("{event} ON \"public\".\"Person\"")),
11919 "{event}:\n{ddl}"
11920 );
11921 }
11922 assert!(ddl.contains("NEW.\"slug\" := _pylon_rewrites.\"v0\";"), "{ddl}");
11923 }
11924
11925 #[test]
11926 fn a_rewrite_walking_a_link_starts_from_the_new_row() {
11927 use crate::schema::RewriteEntry;
11929 let mut schema = make_schema_with_rewrite();
11930 let person = schema.types.iter_mut().find(|t| t.name == "Person").unwrap();
11931 let slug = person.properties.iter_mut().find(|p| p.name == "slug").unwrap();
11932 slug.rewrites = vec![RewriteEntry {
11933 on: 1,
11934 handler: "<str>(.company is Company) ++ .company.name".into(),
11935 }];
11936 let assignments = crate::ir::compile_rewrite_assignments("default::Person", 1, &schema).unwrap();
11937 let sql = &assignments[0].sql;
11938 assert!(!sql.contains("\"public\".\"Person\""), "{sql}");
11939 assert!(
11940 sql.contains("(SELECT (NEW).*)") && sql.contains("NEW.\"company_id\""),
11941 "{sql}"
11942 );
11943 }
11944
11945 #[test]
11946 fn test_unless_conflict_do_nothing() {
11947 let out = compile_and_emit("INSERT Person { name := $name } UNLESS CONFLICT");
11948 assert!(out.sql.contains("ON CONFLICT DO NOTHING"));
11949 }
11950
11951 #[test]
11952 fn test_unless_conflict_on_do_nothing() {
11953 let out = compile_and_emit("INSERT Person { name := $name } UNLESS CONFLICT ON .name");
11954 assert!(out.sql.contains("ON CONFLICT (\"name\") DO NOTHING"));
11955 }
11956
11957 #[test]
11958 fn test_unless_conflict_do_update() {
11959 let out = compile_and_emit(
11960 "INSERT Person { name := $name, age := $age } \
11961 UNLESS CONFLICT ON .name \
11962 ELSE (UPDATE Person SET { age := $age })",
11963 );
11964 assert!(out.sql.contains("ON CONFLICT (\"name\") DO UPDATE SET"));
11965 assert!(out.sql.contains("\"age\" = $2"));
11966 assert!(!out.sql.contains("DO NOTHING"));
11968 }
11969
11970 #[test]
11971 fn test_unless_conflict_else_update_self_reference_is_qualified() {
11972 let out = compile_and_emit(
11980 "INSERT Person { name := $name, age := 0 } \
11981 UNLESS CONFLICT ON .name \
11982 ELSE (UPDATE Person SET { age := .age + 1 })",
11983 );
11984 assert!(
11985 out.sql.contains("\"Person\".\"age\""),
11986 "self-reference must be qualified with the table's own name, got:\n{}",
11987 out.sql
11988 );
11989 assert!(
11990 !out.sql.contains("SET \"age\" = (\"age\""),
11991 "must not emit an unqualified (ambiguous) self-reference, got:\n{}",
11992 out.sql
11993 );
11994 }
11995
11996 #[test]
11997 fn test_unless_conflict_do_update_no_on() {
11998 let out = compile_and_emit(
11999 "INSERT Person { name := $name } \
12000 UNLESS CONFLICT \
12001 ELSE (UPDATE Person SET { age := 0 })",
12002 );
12003 assert!(out.sql.contains("ON CONFLICT DO UPDATE SET"));
12004 assert!(out.sql.contains("\"age\" = 0"));
12005 }
12006
12007 #[test]
12008 fn test_select_over_select() {
12009 let out = compile_and_emit("SELECT (SELECT Person FILTER .age > 18) { name }");
12010 assert!(out.sql.contains("WITH\n\"_dml\" AS ("));
12012 assert!(out.sql.contains("SELECT *"));
12014 assert!(out.sql.contains("FROM \"public\".\"Person\""));
12015 assert!(out.sql.contains("WHERE"));
12017 assert!(out.sql.contains("'default::Person'::text"));
12019 assert!(out.sql.contains("\"name\"::text"));
12020 }
12021
12022 #[test]
12023 fn test_select_over_select_with_outer_filter() {
12024 let out = compile_and_emit("SELECT (SELECT Person FILTER .age > 18) { name } FILTER .name = $name");
12025 assert!(out.sql.contains("WITH\n\"_dml\" AS ("));
12026 assert!(out.sql.contains("SELECT *"));
12027 assert_eq!(out.sql.matches("WHERE").count(), 2);
12029 assert!(out.sql.contains("$1"));
12030 }
12031
12032 #[test]
12033 fn test_insert_link_subquery() {
12034 let out = compile_and_emit("INSERT Person { name := $name, company := (SELECT Company FILTER .name = $co) }");
12035 assert!(out.sql.contains("\"company_id\""));
12037 assert!(out.sql.contains("SELECT"));
12038 assert!(out.sql.contains("\"id\""));
12040 assert!(out.sql.contains("FROM \"public\".\"Company\""));
12041 assert!(out.sql.contains("$2")); }
12044
12045 #[test]
12046 fn test_update_link_subquery() {
12047 let out =
12048 compile_and_emit("UPDATE Person FILTER .id = $id SET { company := (SELECT Company FILTER .name = $co) }");
12049 assert!(out.sql.contains("\"company_id\""));
12050 assert!(out.sql.contains("SELECT"));
12051 assert!(out.sql.contains("FROM \"public\".\"Company\""));
12052 }
12053
12054 #[test]
12055 fn test_insert_link_value_from_nested_insert_hoists_a_with_cte() {
12056 let out = compile_and_emit(
12064 "INSERT Person { name := 'Alice', company := (select (insert Company { name := 'Acme' }) { id }) }",
12065 );
12066 assert!(
12067 out.sql.starts_with("WITH"),
12068 "expected a WITH-hoisted CTE, got:\n{}",
12069 out.sql
12070 );
12071 assert!(
12072 out.sql.contains("INSERT INTO \"public\".\"Company\""),
12073 "expected the nested insert to be its own CTE, got:\n{}",
12074 out.sql,
12075 );
12076 assert!(
12079 out.sql.contains("\"company_id\") SELECT") && out.sql.contains(".\"id\" FROM"),
12080 "expected the outer insert to switch from VALUES to SELECT ... FROM <cte>, got:\n{}",
12081 out.sql,
12082 );
12083 assert!(
12084 !out.sql.contains("FROM \"public\".\"Company\" AS"),
12085 "must not select from the real Company table, got:\n{}",
12086 out.sql
12087 );
12088 }
12089
12090 #[test]
12091 fn test_update_link_value_from_nested_insert_hoists_a_with_cte_and_adds_from_clause() {
12092 let out = compile_and_emit(
12093 "UPDATE Person FILTER .id = $id SET { company := (select (insert Company { name := 'Acme' }) { id }) }",
12094 );
12095 assert!(
12096 out.sql.starts_with("WITH"),
12097 "expected a WITH-hoisted CTE, got:\n{}",
12098 out.sql
12099 );
12100 assert!(
12101 out.sql.contains("INSERT INTO \"public\".\"Company\""),
12102 "expected the nested insert to be its own CTE, got:\n{}",
12103 out.sql,
12104 );
12105 assert!(
12108 out.sql.contains("\nFROM \""),
12109 "expected a FROM clause referencing the nested CTE, got:\n{}",
12110 out.sql
12111 );
12112 assert!(out.sql.contains("SET \"company_id\" = "), "got:\n{}", out.sql);
12113 }
12114
12115 #[test]
12116 fn test_update_link_value_from_nested_insert_combines_with_multilink_mutation() {
12117 let out = compile_and_emit(
12122 "UPDATE Person FILTER .id = $id SET { \
12123 company := (select (insert Company { name := 'Acme' }) { id }), \
12124 posts += (SELECT Post FILTER .title = $t) \
12125 }",
12126 );
12127 assert!(out.sql.starts_with("WITH"), "got:\n{}", out.sql);
12128 assert!(
12129 out.sql.contains("INSERT INTO \"public\".\"Company\""),
12130 "expected the nested insert to be its own CTE, got:\n{}",
12131 out.sql,
12132 );
12133 assert!(
12134 out.sql.contains("\"_ids\" AS (\nUPDATE") && out.sql.contains("\nFROM \""),
12135 "expected the _ids UPDATE to gain a FROM clause referencing the nested CTE, got:\n{}",
12136 out.sql,
12137 );
12138 assert!(
12139 out.sql.contains("\"_ml_add_0\""),
12140 "expected the junction-append CTE to still be present, got:\n{}",
12141 out.sql
12142 );
12143 }
12144
12145 #[test]
12146 fn test_update_link_value_from_nested_insert_combines_with_interface_target() {
12147 fn id_prop() -> PropertyDescriptor {
12153 PropertyDescriptor {
12154 name: "id".into(),
12155 pg_type: "uuid".into(),
12156 nullable: false,
12157 default_sql: Some("uuidv7()".into()),
12158 default_pyql: None,
12159 description: None,
12160 check_constraints: vec![],
12161 is_exclusive: true,
12162 is_pk: true,
12163 is_readonly: true,
12164 rewrites: vec![],
12165 tuple_members: None,
12166 column_type: None,
12167 }
12168 }
12169 fn text_prop(name: &str) -> PropertyDescriptor {
12170 PropertyDescriptor {
12171 name: name.into(),
12172 pg_type: "text".into(),
12173 nullable: false,
12174 default_sql: None,
12175 default_pyql: None,
12176 description: None,
12177 check_constraints: vec![],
12178 is_exclusive: false,
12179 is_pk: false,
12180 is_readonly: false,
12181 rewrites: vec![],
12182 tuple_members: None,
12183 column_type: None,
12184 }
12185 }
12186 fn company_link() -> LinkDescriptor {
12187 LinkDescriptor {
12188 name: "company".into(),
12189 target: "default::Company".into(),
12190 nullable: true,
12191 through: None,
12192 description: None,
12193 default_pyql: None,
12194 is_exclusive: false,
12195 is_readonly: false,
12196 rewrites: vec![],
12197 on_delete: vec![],
12198 }
12199 }
12200 let schema = SchemaDescriptor {
12201 types: vec![
12202 TypeDescriptor {
12203 name: "Company".into(),
12204 module: "default".into(),
12205 table: "Company".into(),
12206 abstract_: false,
12207 materialized: true,
12208 description: None,
12209 parents: vec![],
12210 interfaces: vec![],
12211 bases: vec![],
12212 properties: vec![id_prop(), text_prop("name")],
12213 links: vec![],
12214 multilinks: vec![],
12215 computed: vec![],
12216 constraints: vec![],
12217 indexes: vec![],
12218 partition: None,
12219 vector_indexes: vec![],
12220 search_indexes: vec![],
12221 triggers: vec![],
12222 junction: false,
12223 signals: vec![],
12224 },
12225 TypeDescriptor {
12226 name: "Account".into(),
12227 module: "default".into(),
12228 table: "Account".into(),
12229 abstract_: true,
12230 materialized: true,
12231 description: None,
12232 parents: vec![],
12233 interfaces: vec![],
12234 bases: vec![],
12235 properties: vec![id_prop(), text_prop("email")],
12236 links: vec![company_link()],
12237 multilinks: vec![],
12238 computed: vec![],
12239 constraints: vec![],
12240 indexes: vec![],
12241 partition: None,
12242 vector_indexes: vec![],
12243 search_indexes: vec![],
12244 triggers: vec![],
12245 junction: false,
12246 signals: vec![],
12247 },
12248 TypeDescriptor {
12249 name: "Individual".into(),
12250 module: "default".into(),
12251 table: "Individual".into(),
12252 abstract_: false,
12253 materialized: true,
12254 description: None,
12255 parents: vec![],
12256 interfaces: vec!["default::Account".into()],
12257 bases: vec![],
12258 properties: vec![id_prop(), text_prop("email"), text_prop("first_name")],
12259 links: vec![company_link()],
12260 multilinks: vec![],
12261 computed: vec![],
12262 constraints: vec![],
12263 indexes: vec![],
12264 partition: None,
12265 vector_indexes: vec![],
12266 search_indexes: vec![],
12267 triggers: vec![],
12268 junction: false,
12269 signals: vec![],
12270 },
12271 ],
12272 scalars: vec![],
12273 enums: vec![],
12274 named_tuples: vec![],
12275 globals: vec![],
12276 functions: vec![],
12277 aliases: vec![],
12278 channels: vec![],
12279 ..Default::default()
12280 };
12281 let out = compile_and_emit_with(
12282 "UPDATE Account FILTER .email = $email \
12283 SET { company := (select (insert Company { name := 'Acme' }) { id }) }",
12284 &schema,
12285 );
12286 assert!(out.sql.starts_with("WITH"), "got:\n{}", out.sql);
12287 assert!(
12288 out.sql.contains("INSERT INTO \"public\".\"Company\""),
12289 "expected the nested insert to be its own CTE, got:\n{}",
12290 out.sql,
12291 );
12292 assert!(
12293 out.sql.contains("UPDATE \"public\".\"Individual\"") && out.sql.contains("\nFROM \"_nested_dml_0\""),
12294 "expected the per-implementor UPDATE to gain a FROM clause referencing the nested CTE, got:\n{}",
12295 out.sql,
12296 );
12297 }
12298
12299 #[test]
12300 fn test_a_backlink_narrowed_to_a_supertype() {
12301 let mut schema = interface_link_schema();
12305 schema
12308 .types
12309 .iter_mut()
12310 .find(|t| t.name == "Account")
12311 .expect("the helper declares Account")
12312 .links
12313 .clear();
12314 let out = compile_and_emit_with("SELECT Company { owners := .<company[is Account] { email } }", &schema);
12315 assert!(
12316 out.sql.contains("\"public\".\"Individual\""),
12317 "the backlink must read the type that declares it:\n{}",
12318 out.sql
12319 );
12320 }
12321
12322 #[test]
12323 fn a_path_through_a_backlink_narrowed_to_a_supertype() {
12324 let mut schema = interface_link_schema();
12325 schema
12326 .types
12327 .iter_mut()
12328 .find(|t| t.name == "Account")
12329 .expect("the helper declares Account")
12330 .links
12331 .clear();
12332 let out = compile_and_emit_with(
12333 "SELECT Company { name } FILTER EXISTS (SELECT .<company[is Account])",
12334 &schema,
12335 );
12336 assert!(out.sql.contains("\"public\".\"Individual\""), "{}", out.sql);
12337 }
12338
12339 fn interface_link_schema() -> SchemaDescriptor {
12343 fn id_prop() -> PropertyDescriptor {
12349 PropertyDescriptor {
12350 name: "id".into(),
12351 pg_type: "uuid".into(),
12352 nullable: false,
12353 default_sql: None,
12354 default_pyql: None,
12355 description: None,
12356 check_constraints: vec![],
12357 is_exclusive: true,
12358 is_pk: true,
12359 is_readonly: true,
12360 rewrites: vec![],
12361 tuple_members: None,
12362 column_type: None,
12363 }
12364 }
12365 fn text_prop(name: &str) -> PropertyDescriptor {
12366 PropertyDescriptor {
12367 name: name.into(),
12368 pg_type: "text".into(),
12369 nullable: false,
12370 default_sql: None,
12371 default_pyql: None,
12372 description: None,
12373 check_constraints: vec![],
12374 is_exclusive: false,
12375 is_pk: false,
12376 is_readonly: false,
12377 rewrites: vec![],
12378 tuple_members: None,
12379 column_type: None,
12380 }
12381 }
12382 fn link(name: &str, target: &str) -> LinkDescriptor {
12383 LinkDescriptor {
12384 name: name.into(),
12385 target: target.into(),
12386 nullable: true,
12387 through: None,
12388 description: None,
12389 default_pyql: None,
12390 is_exclusive: false,
12391 is_readonly: false,
12392 rewrites: vec![],
12393 on_delete: vec![],
12394 }
12395 }
12396 fn ty(
12397 name: &str,
12398 abstract_: bool,
12399 interfaces: Vec<String>,
12400 properties: Vec<PropertyDescriptor>,
12401 links: Vec<LinkDescriptor>,
12402 ) -> TypeDescriptor {
12403 TypeDescriptor {
12404 name: name.into(),
12405 module: "default".into(),
12406 table: name.into(),
12407 abstract_,
12408 materialized: true,
12409 description: None,
12410 parents: vec![],
12411 interfaces,
12412 bases: vec![],
12413 properties,
12414 links,
12415 multilinks: vec![],
12416 computed: vec![],
12417 constraints: vec![],
12418 indexes: vec![],
12419 partition: None,
12420 vector_indexes: vec![],
12421 search_indexes: vec![],
12422 triggers: vec![],
12423 junction: false,
12424 signals: vec![],
12425 }
12426 }
12427 SchemaDescriptor {
12428 types: vec![
12429 ty("Company", false, vec![], vec![id_prop(), text_prop("name")], vec![]),
12430 ty(
12431 "Account",
12432 true,
12433 vec![],
12434 vec![id_prop(), text_prop("email")],
12435 vec![link("company", "default::Company")],
12436 ),
12437 ty(
12438 "Individual",
12439 false,
12440 vec!["default::Account".into()],
12441 vec![id_prop(), text_prop("email")],
12442 vec![link("company", "default::Company")],
12443 ),
12444 ty(
12445 "Token",
12446 false,
12447 vec![],
12448 vec![id_prop(), text_prop("value")],
12449 vec![link("account", "default::Account")],
12450 ),
12451 ],
12452 scalars: vec![],
12453 enums: vec![],
12454 named_tuples: vec![],
12455 globals: vec![],
12456 functions: vec![],
12457 aliases: vec![],
12458 channels: vec![],
12459 ..Default::default()
12460 }
12461 }
12462
12463 fn two_hop_interface_schema() -> SchemaDescriptor {
12467 let mut schema = interface_link_schema();
12468 let token = schema
12469 .types
12470 .iter()
12471 .find(|t| t.name == "Token")
12472 .expect("the helper declares Token")
12473 .clone();
12474 let mut connector = token.clone();
12475 connector.name = "Connector".into();
12476 connector.table = "Connector".into();
12477 connector.links[0].name = "provider".into();
12478 let mut installation = token;
12479 installation.name = "Installation".into();
12480 installation.table = "Installation".into();
12481 installation.links[0].name = "connector".into();
12482 installation.links[0].target = "default::Connector".into();
12483 schema.types.push(connector);
12484 schema.types.push(installation);
12485 schema
12486 }
12487
12488 #[test]
12489 fn a_narrowed_two_hop_walk_off_a_pinned_row_is_one_value() {
12490 let out = compile_and_emit_with(
12496 "SELECT { s := ((SELECT Installation FILTER .id = <uuid>$0).connector.provider[is Individual].email) ?? 'y' }",
12497 &two_hop_interface_schema(),
12498 );
12499 assert!(
12500 !out.sql.contains("ARRAY(SELECT"),
12501 "expected a value, not a set:\n{}",
12502 out.sql
12503 );
12504 assert!(
12505 out.sql.contains("COALESCE("),
12506 "the default must stay a scalar one:\n{}",
12507 out.sql
12508 );
12509 }
12510
12511 #[test]
12512 fn a_narrowed_two_hop_walk_off_an_unpinned_row_stays_a_set() {
12513 let out = compile_and_emit_with(
12514 "SELECT { s := (SELECT Installation FILTER .value = 'x').connector.provider[is Individual].email }",
12515 &two_hop_interface_schema(),
12516 );
12517 assert!(
12518 out.sql.contains("ARRAY(SELECT"),
12519 "a walk off many rows is a set:\n{}",
12520 out.sql
12521 );
12522 }
12523
12524 #[test]
12531 fn a_walk_ending_in_a_type_intersection_is_valid_in_a_filter() {
12532 let out = compile_and_emit_with(
12533 "SELECT Token { id } FILTER EXISTS .account[is Individual]",
12534 &interface_link_schema(),
12535 );
12536 assert!(
12537 out.sql.contains("\"public\".\"Individual\""),
12538 "the narrowing must reach the implementor's table:\n{}",
12539 out.sql
12540 );
12541 assert!(
12542 out.sql.contains("IS NOT NULL"),
12543 "exists over the narrowed walk is a nullness test:\n{}",
12544 out.sql
12545 );
12546 }
12547
12548 #[test]
12549 fn a_walk_ending_in_a_type_intersection_is_valid_off_a_binding() {
12550 let out = compile_and_emit_with(
12551 "WITH t := (SELECT Token LIMIT 1) SELECT { e := EXISTS t.account[is Individual] }",
12552 &interface_link_schema(),
12553 );
12554 assert!(
12555 out.sql.contains("\"public\".\"Individual\""),
12556 "the narrowing must reach the implementor's table:\n{}",
12557 out.sql
12558 );
12559 }
12560
12561 #[test]
12562 fn test_reading_a_link_through_an_interface_target() {
12563 let schema = interface_link_schema();
12564 let out = compile_and_emit_with("SELECT Token { account: { email, company: { name } } }", &schema);
12565 assert!(
12566 out.sql
12567 .contains("\"email\", \"company_id\" FROM \"public\".\"Individual\""),
12568 "the fanned-out interface must carry its own link columns:\n{}",
12569 out.sql
12570 );
12571 }
12572
12573 #[test]
12574 fn test_computed_pointer_in_shape_emits_expression() {
12575 let mut schema = make_schema();
12576 schema.types[0].computed.push(crate::schema::ComputedDescriptor {
12577 name: "upper_name".into(),
12578 expression: "str_upper(.name)".into(),
12579 return_type: Some("text".into()),
12580 link_target: None,
12581 link_multi: false,
12582 });
12583 let out = compile_and_emit_with("SELECT Person { upper_name }", &schema);
12584 assert!(
12585 out.sql.to_lowercase().contains("upper"),
12586 "expected upper() in SQL, got:\n{}",
12587 out.sql
12588 );
12589 }
12590
12591 fn make_interface_schema() -> SchemaDescriptor {
12594 fn id_prop() -> PropertyDescriptor {
12595 PropertyDescriptor {
12596 name: "id".into(),
12597 pg_type: "uuid".into(),
12598 nullable: false,
12599 default_sql: Some("uuidv7()".into()),
12600 default_pyql: None,
12601 description: None,
12602 check_constraints: vec![],
12603 is_exclusive: true,
12604 is_pk: true,
12605 is_readonly: true,
12606 rewrites: vec![],
12607 tuple_members: None,
12608 column_type: None,
12609 }
12610 }
12611 SchemaDescriptor {
12612 types: vec![
12613 TypeDescriptor {
12614 name: "Account".into(),
12615 module: "default".into(),
12616 table: "Account".into(),
12617 abstract_: true,
12618 materialized: false,
12619 description: None,
12620 parents: vec![],
12621 interfaces: vec![],
12622 bases: vec![],
12623 properties: vec![
12624 id_prop(),
12625 PropertyDescriptor {
12626 name: "email".into(),
12627 pg_type: "text".into(),
12628 nullable: false,
12629 default_sql: None,
12630 default_pyql: None,
12631 description: None,
12632 check_constraints: vec![],
12633 is_exclusive: false,
12634 is_pk: false,
12635 is_readonly: false,
12636 rewrites: vec![],
12637 tuple_members: None,
12638 column_type: None,
12639 },
12640 ],
12641 links: vec![],
12642 multilinks: vec![],
12643 computed: vec![],
12644 constraints: vec![],
12645 indexes: vec![],
12646 partition: None,
12647 vector_indexes: vec![],
12648 search_indexes: vec![],
12649 triggers: vec![],
12650 junction: false,
12651 signals: vec![],
12652 },
12653 TypeDescriptor {
12654 name: "Individual".into(),
12655 module: "default".into(),
12656 table: "Individual".into(),
12657 abstract_: false,
12658 materialized: true,
12659 description: None,
12660 parents: vec![],
12661 interfaces: vec!["default::Account".into()],
12662 bases: vec![],
12663 properties: vec![
12664 id_prop(),
12665 PropertyDescriptor {
12666 name: "first_name".into(),
12667 pg_type: "text".into(),
12668 nullable: false,
12669 default_sql: None,
12670 default_pyql: None,
12671 description: None,
12672 check_constraints: vec![],
12673 is_exclusive: false,
12674 is_pk: false,
12675 is_readonly: false,
12676 rewrites: vec![],
12677 tuple_members: None,
12678 column_type: None,
12679 },
12680 ],
12681 links: vec![],
12682 multilinks: vec![],
12683 computed: vec![crate::schema::ComputedDescriptor {
12684 name: "full_name".into(),
12685 expression: "str_upper(.first_name)".into(),
12686 return_type: Some("text".into()),
12687 link_target: None,
12688 link_multi: false,
12689 }],
12690 constraints: vec![],
12691 indexes: vec![],
12692 partition: None,
12693 vector_indexes: vec![],
12694 search_indexes: vec![],
12695 triggers: vec![],
12696 junction: false,
12697 signals: vec![],
12698 },
12699 ],
12700 scalars: vec![],
12701 enums: vec![],
12702 named_tuples: vec![],
12703 globals: vec![],
12704 functions: vec![],
12705 aliases: vec![],
12706 channels: vec![],
12707 ..Default::default()
12708 }
12709 }
12710
12711 #[test]
12712 fn test_type_intersection_splat_includes_concrete_computed_pointers() {
12713 let schema = make_interface_schema();
12718 let out = compile_and_emit_with("SELECT Account { *, [is Individual].* }", &schema);
12719 assert!(
12720 out.sql.to_lowercase().contains("upper"),
12721 "expected the concrete type's computed pointer (str_upper(...)) in the shape, got:\n{}",
12722 out.sql
12723 );
12724 assert!(
12725 out.sql.contains("\"first_name\""),
12726 "expected the concrete type's stored property too, got:\n{}",
12727 out.sql
12728 );
12729 }
12730
12731 #[test]
12732 fn test_enum_array_property_casts_to_text_array_and_shapes_as_a_list() {
12733 let mut schema = make_schema();
12740 schema.enums.push(crate::schema::EnumDescriptor {
12741 name: "Gender".into(),
12742 module: "default".into(),
12743 members: vec!["Male".into(), "Female".into()],
12744 });
12745 schema.types[0].properties.push(crate::schema::PropertyDescriptor {
12746 name: "genders".into(),
12747 pg_type: r#""default"."Gender"[]"#.into(),
12748 nullable: true,
12749 default_sql: None,
12750 default_pyql: None,
12751 description: None,
12752 check_constraints: vec![],
12753 is_exclusive: false,
12754 is_pk: false,
12755 is_readonly: false,
12756 rewrites: vec![],
12757 tuple_members: None,
12758 column_type: None,
12759 });
12760
12761 let out = compile_and_emit_with("SELECT Person { genders }", &schema);
12762 assert!(
12763 out.sql.contains(r#""genders"::text[]"#),
12764 "expected a text[] cast, got:\n{}",
12765 out.sql
12766 );
12767
12768 let crate::query::ShapeNode::Object { pointers, .. } = &out.shape.root else {
12769 panic!("expected Object shape, got {:?}", out.shape.root)
12770 };
12771 let genders = pointers
12772 .iter()
12773 .find(|node| matches!(node, crate::query::ShapeNode::Array { name, .. } if name == "genders"))
12774 .unwrap_or_else(|| panic!("expected an Array-shaped pointer, got {pointers:?}"));
12775 let crate::query::ShapeNode::Array { element, .. } = genders else {
12776 unreachable!()
12777 };
12778 assert!(
12779 matches!(
12780 element.as_ref(),
12781 crate::query::ShapeNode::Enum { enum_type, position: 0, .. }
12782 if enum_type == "default::Gender"
12783 ),
12784 "expected the elements to be enum-shaped, got {element:?}",
12785 );
12786 }
12787
12788 #[test]
12789 fn test_scalar_enum_property_still_casts_to_a_single_text() {
12790 let mut schema = make_schema();
12791 schema.enums.push(crate::schema::EnumDescriptor {
12792 name: "Gender".into(),
12793 module: "default".into(),
12794 members: vec!["Male".into(), "Female".into()],
12795 });
12796 schema.types[0].properties.push(crate::schema::PropertyDescriptor {
12797 name: "gender".into(),
12798 pg_type: r#""default"."Gender""#.into(),
12799 nullable: true,
12800 default_sql: None,
12801 default_pyql: None,
12802 description: None,
12803 check_constraints: vec![],
12804 is_exclusive: false,
12805 is_pk: false,
12806 is_readonly: false,
12807 rewrites: vec![],
12808 tuple_members: None,
12809 column_type: None,
12810 });
12811
12812 let out = compile_and_emit_with("SELECT Person { gender }", &schema);
12813 assert!(
12814 out.sql.contains(r#""gender"::text"#) && !out.sql.contains(r#""gender"::text[]"#),
12815 "expected a plain text cast, got:\n{}",
12816 out.sql
12817 );
12818 let crate::query::ShapeNode::Object { pointers, .. } = &out.shape.root else {
12819 panic!("expected Object shape")
12820 };
12821 assert!(
12822 pointers.iter().any(|node| matches!(
12823 node,
12824 crate::query::ShapeNode::Enum { name, enum_type, .. }
12825 if name == "gender" && enum_type == "default::Gender"
12826 )),
12827 "expected an Enum-shaped pointer, got {pointers:?}",
12828 );
12829 }
12830
12831 #[test]
12832 fn test_type_intersection_splat_keeps_enum_pointers_enum_shaped() {
12833 let mut schema = make_interface_schema();
12838 schema.enums.push(crate::schema::EnumDescriptor {
12839 name: "Gender".into(),
12840 module: "default".into(),
12841 members: vec!["Male".into(), "Female".into()],
12842 });
12843 let individual = schema
12846 .types
12847 .iter_mut()
12848 .find(|t| t.name == "Individual")
12849 .expect("the interface schema has an Individual type");
12850 individual.properties.push(crate::schema::PropertyDescriptor {
12851 name: "gender".into(),
12852 pg_type: r#""default"."Gender""#.into(),
12853 nullable: true,
12854 default_sql: None,
12855 default_pyql: None,
12856 description: None,
12857 check_constraints: vec![],
12858 is_exclusive: false,
12859 is_pk: false,
12860 is_readonly: false,
12861 rewrites: vec![],
12862 tuple_members: None,
12863 column_type: None,
12864 });
12865
12866 let out = compile_and_emit_with("SELECT Account { [is Individual].* }", &schema);
12867 let crate::query::ShapeNode::Object { pointers, .. } = &out.shape.root else {
12868 panic!("expected Object shape, got {:?}", out.shape.root)
12869 };
12870 assert!(
12871 pointers.iter().any(|node| matches!(
12872 node,
12873 crate::query::ShapeNode::Enum { name, enum_type, .. }
12874 if name == "gender" && enum_type == "default::Gender"
12875 )),
12876 "expected the enum property to stay enum-shaped through the intersection, got {pointers:?}",
12877 );
12878 }
12879
12880 #[test]
12881 fn test_shallow_splat_leaves_object_valued_computeds_to_the_deep_form() {
12882 let mut schema = make_schema();
12887 schema.types[0].computed.push(crate::schema::ComputedDescriptor {
12888 name: "authors".into(),
12889 expression: ".posts".into(),
12890 return_type: None,
12891 link_target: None,
12892 link_multi: false,
12893 });
12894 schema.types[0].computed.push(crate::schema::ComputedDescriptor {
12895 name: "age_next".into(),
12896 expression: ".age + 1".into(),
12897 return_type: Some("int8".into()),
12898 link_target: None,
12899 link_multi: false,
12900 });
12901
12902 let shallow = compile_and_emit_with("SELECT Person { * }", &schema);
12903 let crate::query::ShapeNode::Object { pointers, .. } = &shallow.shape.root else {
12904 panic!("expected Object shape")
12905 };
12906 let names: Vec<&str> = pointers.iter().map(shape_pointer_name).collect();
12907 assert!(
12908 names.contains(&"age_next"),
12909 "a computed property belongs in `*`: {names:?}"
12910 );
12911 assert!(!names.contains(&"authors"), "a computed link does not: {names:?}");
12912
12913 let deep = compile_and_emit_with("SELECT Person { ** }", &schema);
12914 let crate::query::ShapeNode::Object { pointers, .. } = &deep.shape.root else {
12915 panic!("expected Object shape")
12916 };
12917 let names: Vec<&str> = pointers.iter().map(shape_pointer_name).collect();
12918 assert!(
12919 names.contains(&"authors"),
12920 "`**` includes links, computed ones included: {names:?}"
12921 );
12922 }
12923
12924 fn shape_pointer_name(node: &crate::query::ShapeNode) -> &str {
12925 use crate::query::ShapeNode;
12926 match node {
12927 ShapeNode::Scalar { name, .. }
12928 | ShapeNode::Enum { name, .. }
12929 | ShapeNode::NamedTuple { name, .. }
12930 | ShapeNode::Object { name, .. }
12931 | ShapeNode::Array { name, .. } => name,
12932 _ => "",
12933 }
12934 }
12935
12936 #[test]
12937 fn test_any_over_a_multilink_comparison_does_not_warn() {
12938 let schema = make_schema();
12943 let ast = parse::parse("SELECT Person FILTER any(.posts.title = 'x')").unwrap();
12944 let ir = ir::compile(&ast, &schema).unwrap();
12945 assert!(
12946 !ir.warnings.iter().any(|w| w.contains("FILTER clause")),
12947 "any() states the intent, so nothing should warn: {:?}",
12948 ir.warnings
12949 );
12950 }
12951
12952 #[test]
12953 fn test_bare_multilink_comparison_still_warns() {
12954 let schema = make_schema();
12955 let ast = parse::parse("SELECT Person FILTER .posts.title = 'x'").unwrap();
12956 let ir = ir::compile(&ast, &schema).unwrap();
12957 assert!(
12958 ir.warnings.iter().any(|w| w.contains("FILTER clause")),
12959 "an unwrapped set-valued comparison still warns: {:?}",
12960 ir.warnings
12961 );
12962 }
12963
12964 #[test]
12965 fn test_link_to_an_interface_expands_over_its_implementors() {
12966 let mut schema = make_interface_schema();
12972 let account = schema
12973 .types
12974 .iter_mut()
12975 .find(|t| t.name == "Account")
12976 .expect("the interface schema has an Account type");
12977 account.materialized = true;
12978 let mut organization = schema
12980 .types
12981 .iter()
12982 .find(|t| t.name == "Individual")
12983 .expect("the interface schema has an Individual type")
12984 .clone();
12985 organization.name = "Organization".into();
12986 organization.table = "Organization".into();
12987 organization.computed.clear();
12988 schema.types.push(organization);
12989
12990 let owner = TypeDescriptor {
12991 name: "Note".into(),
12992 module: "default".into(),
12993 table: "Note".into(),
12994 abstract_: false,
12995 materialized: false,
12996 description: None,
12997 parents: vec![],
12998 interfaces: vec![],
12999 bases: vec![],
13000 properties: vec![],
13001 links: vec![LinkDescriptor {
13002 name: "owner".into(),
13003 target: "default::Account".into(),
13004 nullable: true,
13005 description: None,
13006 default_pyql: None,
13007 is_exclusive: false,
13008 is_readonly: false,
13009 rewrites: vec![],
13010 on_delete: vec![],
13011 through: None,
13012 }],
13013 multilinks: vec![],
13014 computed: vec![],
13015 constraints: vec![],
13016 indexes: vec![],
13017 partition: None,
13018 vector_indexes: vec![],
13019 search_indexes: vec![],
13020 triggers: vec![],
13021 junction: false,
13022 signals: vec![],
13023 };
13024 schema.types.push(owner);
13025
13026 let out = compile_and_emit_with("SELECT Note { owner: { id } }", &schema);
13027 assert!(
13028 out.sql.contains("UNION ALL"),
13029 "the link's target should expand over its implementors, got:\n{}",
13030 out.sql
13031 );
13032 assert!(
13033 out.sql.contains("'default::Individual'::text AS \"__type__\""),
13034 "each branch should carry its own discriminator, got:\n{}",
13035 out.sql
13036 );
13037 assert!(
13038 !out.sql.contains("FROM \"default\".\"Account\" AS"),
13039 "the interface's own view should no longer be read directly, got:\n{}",
13040 out.sql
13041 );
13042 }
13043
13044 fn make_two_implementor_schema() -> SchemaDescriptor {
13047 let mut schema = make_interface_schema();
13048 let mut organization = schema
13049 .types
13050 .iter()
13051 .find(|t| t.name == "Individual")
13052 .expect("the interface schema has an Individual type")
13053 .clone();
13054 organization.name = "Organization".into();
13055 organization.table = "Organization".into();
13056 organization.computed.clear();
13057 schema.types.push(organization);
13058 schema
13059 }
13060
13061 fn assert_rows_carry_their_concrete_type(query: &str) {
13064 let out = compile_and_emit_with(query, &make_two_implementor_schema());
13065 assert!(
13066 !out.sql.contains("'default::Account'::text"),
13067 "rows are labelled with the interface instead of their own type:\n{}",
13068 out.sql
13069 );
13070 assert!(
13071 out.sql.contains("'default::Individual'::text AS \"__type__\""),
13072 "the rows should come from the implementors, each tagging its own:\n{}",
13073 out.sql
13074 );
13075 }
13076
13077 #[test]
13078 fn an_aggregated_select_of_an_interface_carries_the_concrete_type() {
13079 assert_rows_carry_their_concrete_type("SELECT (xs := array_agg((SELECT Account { id })))");
13080 }
13081
13082 #[test]
13083 fn an_aggregated_binding_of_an_interface_carries_the_concrete_type() {
13084 assert_rows_carry_their_concrete_type("WITH a := (SELECT Account) SELECT (xs := array_agg((SELECT a { id })))");
13085 }
13086
13087 #[test]
13088 fn a_single_object_read_off_a_binding_carries_the_concrete_type() {
13089 assert_rows_carry_their_concrete_type("WITH a := (SELECT Account) SELECT (x := (SELECT a { id } LIMIT 1))");
13090 }
13091
13092 #[test]
13093 fn grouped_elements_of_an_interface_carry_the_concrete_type() {
13094 assert_rows_carry_their_concrete_type("group Account { id } by .email");
13095 }
13096
13097 fn make_note_schema() -> SchemaDescriptor {
13100 fn link_to(name: &str, target: &str) -> LinkDescriptor {
13101 LinkDescriptor {
13102 name: name.into(),
13103 target: target.into(),
13104 nullable: true,
13105 description: None,
13106 default_pyql: None,
13107 is_exclusive: false,
13108 is_readonly: false,
13109 rewrites: vec![],
13110 on_delete: vec![],
13111 through: None,
13112 }
13113 }
13114 let mut schema = make_two_implementor_schema();
13115 for account in schema
13116 .types
13117 .iter_mut()
13118 .filter(|t| ["Account", "Individual", "Organization"].contains(&t.name.as_str()))
13119 {
13120 account.links.push(link_to("pinned", "default::Note"));
13121 }
13122 schema.types.push(TypeDescriptor {
13123 name: "Note".into(),
13124 module: "default".into(),
13125 table: "Note".into(),
13126 abstract_: false,
13127 materialized: false,
13128 description: None,
13129 parents: vec![],
13130 interfaces: vec![],
13131 bases: vec![],
13132 properties: vec![],
13133 links: vec![link_to("owner", "default::Account")],
13134 multilinks: vec![],
13135 computed: vec![],
13136 constraints: vec![],
13137 indexes: vec![],
13138 partition: None,
13139 vector_indexes: vec![],
13140 search_indexes: vec![],
13141 triggers: vec![],
13142 junction: false,
13143 signals: vec![],
13144 });
13145 schema
13146 }
13147
13148 #[test]
13149 fn a_backlink_to_an_interface_carries_the_concrete_type() {
13150 let out = compile_and_emit_with(
13151 "SELECT Note { pinners := .<pinned[is Account] { id } }",
13152 &make_note_schema(),
13153 );
13154 assert!(
13155 !out.sql.contains("'default::Account'::text"),
13156 "the backlink's rows are labelled with the interface instead of their own type:\n{}",
13157 out.sql
13158 );
13159 }
13160
13161 #[test]
13162 fn a_link_reached_through_a_deep_splat_carries_the_concrete_type() {
13163 let out = compile_and_emit_with("SELECT Note { ** }", &make_note_schema());
13164 assert!(
13165 !out.sql.contains("'default::Account'::text"),
13166 "the splatted link's rows are labelled with the interface instead of their own type:\n{}",
13167 out.sql
13168 );
13169 }
13170
13171 #[test]
13172 fn test_path_traversal_onto_an_interface_carries_the_concrete_type() {
13173 let mut schema = make_interface_schema();
13178 let account = schema
13179 .types
13180 .iter_mut()
13181 .find(|t| t.name == "Account")
13182 .expect("the interface schema has an Account type");
13183 account.materialized = true;
13184 let mut organization = schema
13185 .types
13186 .iter()
13187 .find(|t| t.name == "Individual")
13188 .expect("the interface schema has an Individual type")
13189 .clone();
13190 organization.name = "Organization".into();
13191 organization.table = "Organization".into();
13192 organization.computed.clear();
13193 schema.types.push(organization);
13194 schema.types.push(TypeDescriptor {
13195 name: "Note".into(),
13196 module: "default".into(),
13197 table: "Note".into(),
13198 abstract_: false,
13199 materialized: false,
13200 description: None,
13201 parents: vec![],
13202 interfaces: vec![],
13203 bases: vec![],
13204 properties: vec![],
13205 links: vec![LinkDescriptor {
13206 name: "owner".into(),
13207 target: "default::Account".into(),
13208 nullable: true,
13209 description: None,
13210 default_pyql: None,
13211 is_exclusive: false,
13212 is_readonly: false,
13213 rewrites: vec![],
13214 on_delete: vec![],
13215 through: None,
13216 }],
13217 multilinks: vec![],
13218 computed: vec![],
13219 constraints: vec![],
13220 indexes: vec![],
13221 partition: None,
13222 vector_indexes: vec![],
13223 search_indexes: vec![],
13224 triggers: vec![],
13225 junction: false,
13226 signals: vec![],
13227 });
13228
13229 let out = compile_and_emit_with("SELECT Note.owner { id }", &schema);
13230 assert!(
13231 out.sql.contains("UNION ALL") && out.sql.contains("'default::Individual'::text AS \"__type__\""),
13232 "the traversal's target should fan out over implementors, got:\n{}",
13233 out.sql
13234 );
13235 assert!(
13236 !out.sql.contains("'default::Account'::text,"),
13237 "the row's type should be read off the row, not fixed to the interface, got:\n{}",
13238 out.sql
13239 );
13240 }
13241
13242 #[test]
13243 fn test_computed_multilink_chain_comes_back_as_rows() {
13244 let mut schema = make_schema();
13249 schema.types[0].computed.push(crate::schema::ComputedDescriptor {
13250 name: "coauthors".into(),
13251 expression: ".posts.author".into(),
13252 return_type: None,
13253 link_target: None,
13254 link_multi: false,
13255 });
13256 let post = schema
13257 .types
13258 .iter_mut()
13259 .find(|t| t.name == "Post")
13260 .expect("test schema has a Post type");
13261 post.links.push(LinkDescriptor {
13262 name: "author".into(),
13263 target: "default::Person".into(),
13264 nullable: true,
13265 description: None,
13266 default_pyql: None,
13267 is_exclusive: false,
13268 is_readonly: false,
13269 rewrites: vec![],
13270 on_delete: vec![],
13271 through: None,
13272 });
13273
13274 let out = compile_and_emit_with("SELECT Person { coauthors: { name } }", &schema);
13275 assert!(
13276 out.sql.contains("ARRAY(SELECT"),
13277 "the chain should aggregate into an array, got:\n{}",
13278 out.sql
13279 );
13280 assert!(
13281 out.sql.contains("\"name\""),
13282 "the requested sub-shape should survive, got:\n{}",
13283 out.sql
13284 );
13285
13286 let crate::query::ShapeNode::Object { pointers, .. } = &out.shape.root else {
13287 panic!("expected Object shape, got {:?}", out.shape.root)
13288 };
13289 let coauthors = pointers
13290 .iter()
13291 .find(|node| matches!(node, crate::query::ShapeNode::Array { name, .. } if name == "coauthors"))
13292 .unwrap_or_else(|| panic!("expected an Array-shaped pointer, got {pointers:?}"));
13293 let crate::query::ShapeNode::Array { element, .. } = coauthors else {
13294 unreachable!()
13295 };
13296 assert!(
13297 matches!(
13298 element.as_ref(),
13299 crate::query::ShapeNode::Object { type_name: Some(t), .. } if t == "default::Person"
13300 ),
13301 "the elements should hydrate as objects, got {element:?}",
13302 );
13303 }
13304
13305 #[test]
13306 fn test_multi_sort_with_then_emits_two_order_keys() {
13307 let out = compile_and_emit("SELECT Person { name } ORDER BY .name THEN .age DESC");
13308 assert!(out.sql.contains("ORDER BY"), "expected ORDER BY");
13309 assert!(out.sql.contains("\"name\""));
13311 assert!(out.sql.contains("\"age\""));
13312 assert!(out.sql.contains("DESC"));
13313 }
13314
13315 #[test]
13316 fn test_string_index_emits_str_subscript() {
13317 let out = compile_and_emit("SELECT 'hello'[1]");
13318 assert!(
13319 out.sql.contains("_pylon.str_subscript"),
13320 "expected _pylon.str_subscript() for string index, got:\n{}",
13321 out.sql
13322 );
13323 }
13324
13325 #[test]
13328 fn test_for_over_a_set_returning_call_is_not_a_values_clause() {
13329 let out = compile_and_emit("FOR n IN array_unpack(<array<int64>>$ns) UNION (SELECT n)");
13330 assert!(
13331 !out.sql.contains("VALUES (unnest"),
13332 "a set-returning iterator must not land in VALUES, got:\n{}",
13333 out.sql
13334 );
13335 assert!(
13336 out.sql.contains("SELECT unnest"),
13337 "expected the iterator in a select list, got:\n{}",
13338 out.sql
13339 );
13340 }
13341
13342 #[test]
13345 fn test_for_over_array_unpack_binds_the_element_type() {
13346 let out = compile_and_emit("FOR j IN array_unpack(<array<json>>$rows) UNION (SELECT <str>j['k'])");
13347 assert!(
13348 out.sql.contains("::jsonb AS v"),
13349 "the loop variable should carry the element type, got:\n{}",
13350 out.sql
13351 );
13352 }
13353
13354 #[test]
13357 fn test_a_binding_keeps_its_array_type_through_an_aggregate_and_a_condition() {
13358 for query in [
13359 "WITH ids := ([<uuid>$a] IF true ELSE [<uuid>$b]) SELECT Person FILTER contains(ids, .id)",
13360 "WITH ns := array_agg((SELECT Person.name)) SELECT Person FILTER contains(ns, .name)",
13361 ] {
13362 let out = compile_and_emit(query);
13363 assert!(
13364 out.sql.contains("@> ARRAY["),
13365 "expected the array overload of contains for `{query}`, got:\n{}",
13366 out.sql
13367 );
13368 }
13369 }
13370
13371 #[test]
13375 fn test_an_array_literals_walk_element_is_read_as_one_value() {
13376 let out = compile_and_emit("WITH p := (SELECT Person LIMIT 1) SELECT [<uuid>$a, p.id]");
13377 assert!(
13378 !out.sql.contains("ARRAY(SELECT"),
13379 "the element should be a scalar subquery, not an array of its own:\n{}",
13380 out.sql
13381 );
13382 }
13383
13384 #[test]
13388 fn test_ordering_a_walk_leaves_the_inner_filter_on_its_own_subject() {
13389 let out = compile_and_emit("SELECT (SELECT Person FILTER .name = 'x' LIMIT 1).posts { title } ORDER BY .title");
13390 assert!(
13391 out.sql.contains("WHERE (\"t0\".\"name\" = 'x')"),
13392 "the filter belongs to the Person the walk starts from, got:\n{}",
13393 out.sql
13394 );
13395 assert!(
13396 out.sql.contains("\"title\" ASC"),
13397 "the ordering belongs to the Post the walk lands on, got:\n{}",
13398 out.sql
13399 );
13400 }
13401
13402 #[test]
13406 fn test_a_walks_head_keeps_its_own_limit() {
13407 let out = compile_and_emit("SELECT (SELECT Person FILTER .name = 'x' LIMIT 1).posts { title }");
13408 let (head, walk) = out.sql.split_once(")\nSELECT").expect("expected a bound head");
13409 assert!(
13410 head.contains("LIMIT 1"),
13411 "the head keeps its own row count, got:\n{}",
13412 out.sql
13413 );
13414 assert!(
13415 !walk.contains("LIMIT"),
13416 "the walk keeps every row it reaches, got:\n{}",
13417 out.sql
13418 );
13419 }
13420
13421 #[test]
13425 fn test_for_over_json_array_unpack_binds_json() {
13426 let out = compile_and_emit(
13427 "FOR entry IN json_array_unpack(to_json(<str>$rows)) UNION (SELECT <str>json_get(entry, 'k'))",
13428 );
13429 assert!(
13430 out.sql.contains("::jsonb AS v"),
13431 "the loop variable should carry json, got:\n{}",
13432 out.sql
13433 );
13434 }
13435
13436 #[test]
13439 fn test_casting_json_get_reads_the_value_not_its_json_spelling() {
13440 let out = compile_and_emit("SELECT <uuid><str>json_get(<json>$p, 'id')");
13441 assert!(
13442 out.sql.contains("#>> '{}'"),
13443 "expected the json value to be read out, got:\n{}",
13444 out.sql
13445 );
13446 }
13447
13448 #[test]
13451 fn test_indexing_json_emits_a_jsonb_accessor() {
13452 let out = compile_and_emit("SELECT (<json>$payload)['label']");
13453 assert!(
13454 out.sql.contains("->'label'"),
13455 "expected a jsonb field access, got:\n{}",
13456 out.sql
13457 );
13458 assert!(
13459 !out.sql.contains("str_subscript"),
13460 "json must not be subscripted as a string, got:\n{}",
13461 out.sql
13462 );
13463 }
13464
13465 #[test]
13468 fn test_casting_json_to_a_scalar_unwraps_it() {
13469 let out = compile_and_emit("SELECT <str>to_json(<str>$s)");
13470 assert!(
13471 out.sql.contains("#>> '{}'"),
13472 "expected the json value to be extracted, got:\n{}",
13473 out.sql
13474 );
13475 }
13476
13477 #[test]
13481 fn test_a_binding_that_reads_the_loop_variable_is_keyed_by_iteration() {
13482 let out = compile_and_emit(
13483 "WITH made := (FOR p IN (SELECT Person) UNION ( \
13484 WITH mine := (SELECT p.posts LIMIT 1) \
13485 INSERT Company { name := mine.title } \
13486 )) SELECT count(made)",
13487 );
13488 assert!(
13489 out.sql.contains("\"mine\" AS (\nSELECT \"_for_p\".\"v\" AS \"_outer\""),
13490 "the binding must be evaluated per iteration, got:\n{}",
13491 out.sql
13492 );
13493 assert!(
13494 out.sql.contains("\"mine\".\"_outer\" = \"_for_p\".\"v\""),
13495 "a read of it must pin to the iteration in scope, got:\n{}",
13496 out.sql
13497 );
13498 }
13499
13500 #[test]
13504 fn test_a_loop_nested_in_a_loop_pairs_its_rows_by_iteration() {
13505 let out = compile_and_emit(
13506 "FOR n IN {'a', 'b'} UNION (INSERT Person { name := n, posts := (FOR t IN {n} UNION (INSERT Post { title := t })) })",
13507 );
13508 assert!(
13509 out.sql.contains("\"_outer\""),
13510 "the nested rows must carry an iteration key, got:\n{}",
13511 out.sql
13512 );
13513 assert!(
13514 !out.sql.contains("CROSS JOIN \"_nested_dml_0\""),
13515 "they must not be cross-joined onto every row, got:\n{}",
13516 out.sql
13517 );
13518 }
13519
13520 #[test]
13523 fn test_for_bodied_insert_writes_its_multi_link() {
13524 let out = compile_and_emit("FOR n IN {'a', 'b'} UNION (INSERT Person { name := n, posts := (SELECT Post) })");
13525 assert!(
13526 out.sql.contains("INSERT INTO \"public\".\"Person.posts\""),
13527 "the junction rows must be written too, got:\n{}",
13528 out.sql
13529 );
13530 }
13531
13532 #[test]
13533 fn test_string_slice_emits_substr() {
13534 let out = compile_and_emit("SELECT 'hello'[1:3]");
13535 assert!(
13536 out.sql.contains("substr"),
13537 "expected substr() for string slice, got:\n{}",
13538 out.sql
13539 );
13540 }
13541
13542 #[test]
13543 fn test_array_index_emits_subscript() {
13544 let out = compile_and_emit("SELECT [1, 2, 3][1]");
13545 assert!(
13546 out.sql.contains("_pylon.array_subscript"),
13547 "expected _pylon.array_subscript() for array index, got:\n{}",
13548 out.sql
13549 );
13550 }
13551
13552 #[test]
13553 fn test_index_into_array_returning_function_emits_array_subscript() {
13554 let out = compile_and_emit("SELECT str_split('a::b', '::')[-1]");
13555 assert!(
13556 out.sql.contains("_pylon.array_subscript(string_to_array"),
13557 "expected _pylon.array_subscript() over str_split, got:\n{}",
13558 out.sql
13559 );
13560 }
13561
13562 #[test]
13563 fn test_array_slice_emits_subscript() {
13564 let out = compile_and_emit("SELECT [1, 2, 3][0:2]");
13565 assert!(
13566 !out.sql.contains("substr"),
13567 "should not use substr for array, got:\n{}",
13568 out.sql
13569 );
13570 assert!(out.sql.contains(")["), "expected array slice syntax, got:\n{}", out.sql);
13571 }
13572
13573 #[test]
13574 fn test_open_ended_string_slice_emits_substr_no_length() {
13575 let out = compile_and_emit("SELECT 'hello'[2:]");
13576 assert!(out.sql.contains("substr"), "expected substr(), got:\n{}", out.sql);
13578 let substr_idx = out.sql.find("substr").unwrap();
13580 let after = &out.sql[substr_idx..];
13581 let commas = after.chars().take_while(|&c| c != ')').filter(|&c| c == ',').count();
13582 assert_eq!(commas, 1, "open-ended slice should use 2-arg substr, got:\n{}", out.sql);
13583 }
13584
13585 #[test]
13586 fn test_group_by_single_key() {
13587 let out = compile_and_emit("group Person { name } by .age");
13588 assert!(out.sql.contains("GROUP BY"), "expected GROUP BY, got:\n{}", out.sql);
13590 assert!(out.sql.contains("\"age\""), "expected age column, got:\n{}", out.sql);
13592 assert!(
13594 out.sql.contains("array_agg(ROW("),
13595 "expected array_agg, got:\n{}",
13596 out.sql
13597 );
13598 assert!(
13600 out.sql.contains("ARRAY['age']"),
13601 "expected grouping array, got:\n{}",
13602 out.sql
13603 );
13604 assert!(matches!(out.shape.root, crate::query::ShapeNode::Group { .. }));
13606 if let crate::query::ShapeNode::Group {
13607 key_nodes,
13608 grouping_position,
13609 elements_position,
13610 ..
13611 } = &out.shape.root
13612 {
13613 assert_eq!(key_nodes.len(), 1);
13614 assert!(matches!(&key_nodes[0], crate::query::ShapeNode::Scalar { name, position: 1 } if name == "age"));
13615 assert_eq!(*grouping_position, 2);
13616 assert_eq!(*elements_position, 3);
13617 }
13618 }
13619
13620 #[test]
13621 fn test_group_using_alias() {
13622 let out = compile_and_emit("group Person using decade := .age // 10 by decade");
13623 assert!(out.sql.contains("GROUP BY"), "expected GROUP BY, got:\n{}", out.sql);
13624 assert!(
13625 out.sql.contains("ARRAY['decade']"),
13626 "expected grouping array, got:\n{}",
13627 out.sql
13628 );
13629 if let crate::query::ShapeNode::Group { key_nodes, .. } = &out.shape.root {
13630 assert_eq!(key_nodes.len(), 1);
13631 assert!(matches!(&key_nodes[0], crate::query::ShapeNode::Scalar { name, .. } if name == "decade"));
13632 }
13633 }
13634
13635 #[test]
13636 fn test_group_orders_elements_within_each_group() {
13637 let out = compile_and_emit("group Person { name } by .age order by .name desc");
13638 assert!(
13639 out.sql.contains(")::record ORDER BY \"t0\".\"name\" DESC NULLS LAST)"),
13640 "{}",
13641 out.sql
13642 );
13643 assert!(!out.sql.contains("row_number()"), "{}", out.sql);
13644 }
13645
13646 #[test]
13647 fn test_group_limit_trims_each_group_not_the_result() {
13648 let out = compile_and_emit("group Person { name } by .age order by .name desc limit 1");
13651 assert!(
13652 out.sql
13653 .contains("row_number() OVER (PARTITION BY \"t0\".\"age\" ORDER BY \"t0\".\"name\" DESC NULLS LAST)"),
13654 "{}",
13655 out.sql
13656 );
13657 assert!(
13658 out.sql.contains("WHERE \"__rk\" > 0 AND \"__rk\" <= 0 + 1"),
13659 "{}",
13660 out.sql
13661 );
13662 assert!(!out.sql.contains("\nLIMIT"), "{}", out.sql);
13663 }
13664
13665 #[test]
13666 fn test_group_by_two_keys_with_a_per_group_limit() {
13667 let out = compile_and_emit(
13668 "group Person { name } by .age, .name filter .age > 18 order by .name desc offset 1 limit 2",
13669 );
13670 assert!(
13671 out.sql.contains("PARTITION BY \"t0\".\"age\", \"t0\".\"name\""),
13672 "{}",
13673 out.sql
13674 );
13675 let where_pos = out.sql.find("WHERE (\"t0\".\"age\" > 18)").expect("filter");
13678 let rank_pos = out.sql.find("WHERE \"__rk\"").expect("rank filter");
13679 assert!(where_pos < rank_pos, "{}", out.sql);
13680 assert!(
13681 out.sql.contains("GROUP BY \"t0\".\"age\", \"t0\".\"name\""),
13682 "{}",
13683 out.sql
13684 );
13685 }
13686
13687 #[test]
13688 fn test_group_filter_without_a_limit_is_a_plain_where() {
13689 let out = compile_and_emit("group Person { name } by .age filter .age > 18");
13690 assert!(out.sql.contains("WHERE (\"t0\".\"age\" > 18)"), "{}", out.sql);
13691 assert!(!out.sql.contains("row_number()"), "{}", out.sql);
13692 }
13693
13694 #[test]
13695 fn test_abs_path_concat_same_type() {
13696 let out = compile_and_emit("SELECT Person.name ++ ' ' ++ Person.name");
13697 assert!(out.sql.contains("\"name\""), "expected name column, got:\n{}", out.sql);
13698 assert!(out.sql.contains("||"), "expected concat operator, got:\n{}", out.sql);
13699 assert!(out.sql.contains("FROM"), "expected FROM clause, got:\n{}", out.sql);
13700 }
13701
13702 #[test]
13703 fn test_abs_path_single_property() {
13704 let out = compile_and_emit("SELECT Person.name");
13705 assert!(out.sql.contains("\"name\""), "expected name column, got:\n{}", out.sql);
13706 assert!(out.sql.contains("FROM"), "expected FROM clause, got:\n{}", out.sql);
13707 }
13708
13709 #[test]
13710 fn test_pgvector_cast_emits_vector_type() {
13711 let out = compile_and_emit("SELECT <pgvector::vector>[1.0, 2.0, 3.0]");
13712 assert!(
13713 out.sql.contains("::vector"),
13714 "expected ::vector cast, got:\n{}",
13715 out.sql
13716 );
13717 assert!(out.sql.contains("ARRAY["), "expected ARRAY literal, got:\n{}", out.sql);
13718 }
13719
13720 #[test]
13721 fn test_pgvector_euclidean_distance_emits_l2_operator() {
13722 let out = compile_and_emit(
13723 "SELECT pgvector::euclidean_distance(<pgvector::vector>[1.0, 2.0], <pgvector::vector>[3.0, 4.0])",
13724 );
13725 assert!(out.sql.contains("<->"), "expected <-> operator, got:\n{}", out.sql);
13726 }
13727
13728 #[test]
13729 fn test_pgvector_cosine_distance_emits_cosine_operator() {
13730 let out = compile_and_emit(
13731 "SELECT pgvector::cosine_distance(<pgvector::vector>[1.0, 2.0], <pgvector::vector>[3.0, 4.0])",
13732 );
13733 assert!(out.sql.contains("<=>"), "expected <=> operator, got:\n{}", out.sql);
13734 }
13735
13736 #[test]
13737 fn test_pgvector_neg_inner_product_emits_ip_operator() {
13738 let out = compile_and_emit(
13739 "SELECT pgvector::neg_inner_product(<pgvector::vector>[1.0, 2.0], <pgvector::vector>[3.0, 4.0])",
13740 );
13741 assert!(out.sql.contains("<#>"), "expected <#> operator, got:\n{}", out.sql);
13742 }
13743
13744 #[test]
13745 fn test_pgvector_inner_product_negates_ip_operator() {
13746 let out = compile_and_emit(
13747 "SELECT pgvector::inner_product(<pgvector::vector>[1.0, 2.0], <pgvector::vector>[3.0, 4.0])",
13748 );
13749 assert!(out.sql.contains("<#>"), "expected <#> operator, got:\n{}", out.sql);
13750 assert!(out.sql.contains("0.0"), "expected negation of <#>, got:\n{}", out.sql);
13751 }
13752
13753 #[test]
13754 fn test_crypto_digest_str_and_bytes_overloads_both_use_pgcrypto_digest() {
13755 let out = compile_and_emit("SELECT crypto::digest('hello', 'sha256')");
13756 assert!(
13757 out.sql.contains("digest("),
13758 "expected pgcrypto's digest(), got:\n{}",
13759 out.sql
13760 );
13761
13762 let out = compile_and_emit("SELECT crypto::digest(std::from_hex('68656c6c6f'), 'sha256')");
13763 assert!(
13764 out.sql.contains("digest("),
13765 "expected pgcrypto's digest(), got:\n{}",
13766 out.sql
13767 );
13768 }
13769
13770 #[test]
13771 fn test_crypto_hmac_str_and_bytes_overloads_both_use_pgcrypto_hmac() {
13772 let out = compile_and_emit("SELECT crypto::hmac('hello', 'key', 'sha256')");
13773 assert!(
13774 out.sql.contains("hmac("),
13775 "expected pgcrypto's hmac(), got:\n{}",
13776 out.sql
13777 );
13778
13779 let out =
13780 compile_and_emit("SELECT crypto::hmac(std::from_hex('68656c6c6f'), std::from_hex('6b6579'), 'sha256')");
13781 assert!(
13782 out.sql.contains("hmac("),
13783 "expected pgcrypto's hmac(), got:\n{}",
13784 out.sql
13785 );
13786 }
13787
13788 #[test]
13789 fn test_crypto_gen_salt_zero_arg_defaults_to_blowfish() {
13790 let out = compile_and_emit("SELECT crypto::gen_salt()");
13791 assert!(
13792 out.sql.contains("gen_salt('bf')"),
13793 "expected default 'bf' salt type, got:\n{}",
13794 out.sql
13795 );
13796 }
13797
13798 #[test]
13799 fn test_crypto_gen_salt_one_arg_passes_type_through() {
13800 let out = compile_and_emit("SELECT crypto::gen_salt('xdes')");
13801 assert!(
13802 out.sql.contains("gen_salt("),
13803 "expected gen_salt() call, got:\n{}",
13804 out.sql
13805 );
13806 }
13807
13808 #[test]
13809 fn test_crypto_gen_salt_iter_count_casts_to_int4() {
13810 let out = compile_and_emit("SELECT crypto::gen_salt('xdes', 5)");
13811 assert!(
13812 out.sql.contains("::int4"),
13813 "expected int8 -> int4 narrowing cast, got:\n{}",
13814 out.sql
13815 );
13816 }
13817
13818 #[test]
13819 fn test_crypto_crypt_uses_pgcrypto_crypt() {
13820 let out = compile_and_emit("SELECT crypto::crypt('hunter2', crypto::gen_salt())");
13821 assert!(
13822 out.sql.contains("crypt("),
13823 "expected pgcrypto's crypt(), got:\n{}",
13824 out.sql
13825 );
13826 }
13827
13828 #[test]
13829 fn test_postgis_cast_emits_geometry_type() {
13830 let out = compile_and_emit("SELECT <postgis::geometry>'POINT(1 2)'");
13831 assert!(
13832 out.sql.contains("::geometry"),
13833 "expected ::geometry cast, got:\n{}",
13834 out.sql
13835 );
13836 }
13837
13838 #[test]
13839 fn test_postgis_x_uses_st_x_builtin() {
13840 let out = compile_and_emit("SELECT postgis::x(<postgis::geometry>'POINT(1 2)')");
13841 assert!(out.sql.contains("st_x("), "expected st_x() call, got:\n{}", out.sql);
13842 }
13843
13844 #[test]
13845 fn test_postgis_area_geometry_and_geography_overloads() {
13846 let out = compile_and_emit("SELECT postgis::area(<postgis::geometry>'POINT(1 2)')");
13847 assert!(
13848 out.sql.contains("st_area("),
13849 "expected st_area() call, got:\n{}",
13850 out.sql
13851 );
13852
13853 let out = compile_and_emit("SELECT postgis::area(<postgis::geography>'POINT(1 2)', true)");
13854 assert!(
13855 out.sql.contains("st_area("),
13856 "expected st_area() call, got:\n{}",
13857 out.sql
13858 );
13859 }
13860
13861 #[test]
13862 fn test_postgis_setsrid_casts_int64_arg_to_int4() {
13863 let out = compile_and_emit("SELECT postgis::setsrid(<postgis::geometry>'POINT(1 2)', 4326)");
13864 assert!(
13865 out.sql.contains("st_setsrid("),
13866 "expected st_setsrid() call, got:\n{}",
13867 out.sql
13868 );
13869 assert!(
13870 out.sql.contains("::int4"),
13871 "expected int8 -> int4 narrowing cast, got:\n{}",
13872 out.sql
13873 );
13874 }
13875
13876 #[test]
13877 fn test_postgis_quantizecoordinates_default_arity_variants_compile() {
13878 let out = compile_and_emit("SELECT postgis::quantizecoordinates(<postgis::geometry>'POINT(1 2)', 5)");
13883 assert!(out.sql.contains("st_quantizecoordinates("), "got:\n{}", out.sql);
13884
13885 let out = compile_and_emit("SELECT postgis::quantizecoordinates(<postgis::geometry>'POINT(1 2)', 5, 5, 5)");
13886 assert!(out.sql.contains("st_quantizecoordinates("), "got:\n{}", out.sql);
13887 }
13888
13889 #[test]
13890 fn test_postgis_op_contains_emits_infix_operator_not_function_call() {
13891 let out = compile_and_emit(
13896 "SELECT postgis::op_contains(<postgis::geometry>'POINT(1 2)', <postgis::geometry>'POINT(3 4)')",
13897 );
13898 assert!(out.sql.contains(" ~ "), "expected infix ~ operator, got:\n{}", out.sql);
13899 assert!(
13900 !out.sql.contains("op_contains("),
13901 "must not call a literal op_contains function, got:\n{}",
13902 out.sql
13903 );
13904 }
13905
13906 #[test]
13907 fn test_postgis_op_overlaps_geometry_and_geography_overloads() {
13908 let out = compile_and_emit(
13909 "SELECT postgis::op_overlaps(<postgis::geometry>'POINT(1 2)', <postgis::geometry>'POINT(3 4)')",
13910 );
13911 assert!(
13912 out.sql.contains(" && "),
13913 "expected infix && operator, got:\n{}",
13914 out.sql
13915 );
13916
13917 let out = compile_and_emit(
13918 "SELECT postgis::op_overlaps(<postgis::geography>'POINT(1 2)', <postgis::geography>'POINT(3 4)')",
13919 );
13920 assert!(
13921 out.sql.contains(" && "),
13922 "expected infix && operator, got:\n{}",
13923 out.sql
13924 );
13925 }
13926
13927 fn make_schema_with_fns() -> SchemaDescriptor {
13930 let mut s = make_schema();
13931 s.functions = vec![
13932 FunctionDescriptor {
13933 name: "mysum".into(),
13934 module: "default".into(),
13935 params: vec![
13936 FunctionParamDescriptor {
13937 name: "a".into(),
13938 pg_type: "int8".into(),
13939 },
13940 FunctionParamDescriptor {
13941 name: "b".into(),
13942 pg_type: "int8".into(),
13943 },
13944 ],
13945 return_pg_type: "int8".into(),
13946 return_is_object: false,
13947 return_is_set: false,
13948 return_is_polymorphic: false,
13949 volatility: "immutable".into(),
13950 body: "a + b".into(),
13951 },
13952 FunctionDescriptor {
13953 name: "adults".into(),
13954 module: "default".into(),
13955 params: vec![],
13956 return_pg_type: "default::Person".into(),
13957 return_is_object: true,
13958 return_is_set: true,
13959 return_is_polymorphic: false,
13960 volatility: "stable".into(),
13961 body: "select Person filter .age > 18".into(),
13962 },
13963 FunctionDescriptor {
13970 name: "mysum".into(),
13971 module: "default".into(),
13972 params: vec![
13973 FunctionParamDescriptor {
13974 name: "a".into(),
13975 pg_type: "int8".into(),
13976 },
13977 FunctionParamDescriptor {
13978 name: "b".into(),
13979 pg_type: "int8".into(),
13980 },
13981 FunctionParamDescriptor {
13982 name: "c".into(),
13983 pg_type: "int8".into(),
13984 },
13985 ],
13986 return_pg_type: "int8".into(),
13987 return_is_object: false,
13988 return_is_set: false,
13989 return_is_polymorphic: false,
13990 volatility: "immutable".into(),
13991 body: "a + b + c".into(),
13992 },
13993 ];
13994 s
13995 }
13996
13997 #[test]
13998 fn test_user_fn_scalar_call() {
13999 let schema = make_schema_with_fns();
14000 let out = compile_and_emit_with("SELECT mysum(1, 2)", &schema);
14001 assert!(out.sql.contains("\"public\".\"mysum\""), "got:\n{}", out.sql);
14002 }
14003
14004 #[test]
14005 fn test_user_fn_overload_resolved_by_argument_count() {
14006 let schema = make_schema_with_fns();
14011 let out = compile_and_emit_with("SELECT mysum(1, 2, 3)", &schema);
14012 assert!(
14013 out.sql
14014 .contains("\"public\".\"mysum\"((1)::int8, (2)::int8, (3)::int8)"),
14015 "got:\n{}",
14016 out.sql,
14017 );
14018
14019 let out = compile_and_emit_with("SELECT mysum(1, 2)", &schema);
14021 assert!(
14022 out.sql.contains("\"public\".\"mysum\"((1)::int8, (2)::int8)"),
14023 "got:\n{}",
14024 out.sql,
14025 );
14026 }
14027
14028 #[test]
14029 fn test_user_fn_object_select_no_shape() {
14030 let schema = make_schema_with_fns();
14031 let out = compile_and_emit_with("SELECT adults()", &schema);
14032 assert!(out.sql.contains("\"public\".\"adults\"()"), "got:\n{}", out.sql);
14033 assert!(out.sql.contains("FROM"), "got:\n{}", out.sql);
14034 }
14035
14036 #[test]
14037 fn test_user_fn_object_select_with_shape() {
14038 let schema = make_schema_with_fns();
14039 let out = compile_and_emit_with("SELECT adults() { name }", &schema);
14040 assert!(out.sql.contains("\"public\".\"adults\"()"), "got:\n{}", out.sql);
14041 assert!(out.sql.contains("\"name\""), "got:\n{}", out.sql);
14042 }
14043
14044 #[test]
14045 fn test_object_fn_projected_to_a_property_inside_an_expression() {
14046 let schema = make_schema_with_fns();
14047 let out = compile_and_emit_with("SELECT Person { n := adults().name }", &schema);
14048 assert!(out.sql.contains("FROM \"public\".\"adults\"() AS"), "got:\n{}", out.sql);
14049 assert!(out.sql.contains("\".\"name\"\n"), "got:\n{}", out.sql);
14050 }
14051
14052 #[test]
14053 fn test_object_fn_sub_select_carries_its_modifiers() {
14054 let schema = make_schema_with_fns();
14055 let out = compile_and_emit_with(
14056 "SELECT Person { n := (select adults() filter .age > 21 limit 1).name }",
14057 &schema,
14058 );
14059 assert!(out.sql.contains("FROM \"public\".\"adults\"() AS"), "got:\n{}", out.sql);
14060 assert!(out.sql.contains("\"age\" > 21"), "got:\n{}", out.sql);
14061 assert!(out.sql.contains("LIMIT 1"), "got:\n{}", out.sql);
14062 }
14063
14064 #[test]
14065 fn test_object_fn_without_a_property_still_explains_the_restriction() {
14066 let schema = make_schema_with_fns();
14067 let ast = parse::parse("SELECT Person { n := adults() + 1 }").unwrap();
14068 let err = match ir::compile(&ast, &schema) {
14069 Ok(_) => panic!("expected a compile error"),
14070 Err(e) => format!("{e}"),
14071 };
14072 assert!(err.contains("returns objects"), "{err}");
14073 }
14074
14075 #[test]
14076 fn test_object_fn_projected_to_an_unknown_property_is_rejected() {
14077 let schema = make_schema_with_fns();
14078 let ast = parse::parse("SELECT Person { n := adults().nope }").unwrap();
14079 let err = match ir::compile(&ast, &schema) {
14080 Ok(_) => panic!("expected a compile error"),
14081 Err(e) => format!("{e}"),
14082 };
14083 assert!(err.contains("nope"), "{err}");
14084 }
14085
14086 #[test]
14087 fn test_user_fn_in_cte_exposes_raw_columns() {
14088 let schema = make_schema_with_fns();
14091 let out = compile_and_emit_with("WITH persons := adults() SELECT persons FILTER .age > 25", &schema);
14092 assert!(
14093 out.sql.contains("SELECT * FROM \"public\".\"adults\"()"),
14094 "CTE source must be SELECT * FROM fn(), got:\n{}",
14095 out.sql,
14096 );
14097 assert!(
14098 out.sql.contains("\"age\""),
14099 "outer filter must reference raw column, got:\n{}",
14100 out.sql
14101 );
14102 }
14103
14104 fn make_schema_with_vector() -> SchemaDescriptor {
14107 use crate::schema::VectorIndexDescriptor;
14108 let mut s = make_schema();
14109 if let Some(td) = s.types.iter_mut().find(|t| t.name == "Person") {
14110 td.vector_indexes.push(VectorIndexDescriptor {
14111 index_name: None,
14112 pointers: vec!["name".into()],
14113 model: "test-embed".into(),
14114 metric: "cosine".into(),
14115 dimensions: 4,
14116 });
14117 }
14118 s
14119 }
14120
14121 #[test]
14122 fn test_vector_search_bare_type_name() {
14123 let schema = make_schema_with_vector();
14124 let out = compile_and_emit_with(
14125 "WITH search := vector::search(Person, <pgvector::vector>[1.0, 2.0, 3.0, 4.0]) \
14126 SELECT search { object { name }, distance }",
14127 &schema,
14128 );
14129 assert!(
14130 out.sql.contains("\"Person\""),
14131 "expected Person table, got:\n{}",
14132 out.sql
14133 );
14134 assert!(out.sql.contains("<=>"), "expected cosine operator, got:\n{}", out.sql);
14135 }
14136
14137 #[test]
14138 fn test_vector_search_qualified_type_name() {
14139 let schema = make_schema_with_vector();
14140 let out = compile_and_emit_with(
14141 "WITH search := vector::search(default::Person, <pgvector::vector>[1.0, 2.0, 3.0, 4.0]) \
14142 SELECT search { object { name }, distance }",
14143 &schema,
14144 );
14145 assert!(
14146 out.sql.contains("\"Person\""),
14147 "expected Person table, got:\n{}",
14148 out.sql
14149 );
14150 assert!(out.sql.contains("<=>"), "expected cosine operator, got:\n{}", out.sql);
14151 }
14152
14153 #[test]
14154 fn test_vector_search_subquery_filter_included_in_where() {
14155 let schema = make_schema_with_vector();
14156 let out = compile_and_emit_with(
14157 "WITH search := vector::search((select Person filter .name = 'Alice'), <pgvector::vector>[1.0, 2.0, 3.0, 4.0]) \
14158 SELECT search { object { name }, distance }",
14159 &schema,
14160 );
14161 assert!(
14162 out.sql.contains("\"Person\""),
14163 "expected Person table, got:\n{}",
14164 out.sql
14165 );
14166 assert!(out.sql.contains("\"name\""), "expected name filter, got:\n{}", out.sql);
14167 assert!(out.sql.contains("Alice"), "expected filter value, got:\n{}", out.sql);
14168 assert!(out.sql.contains("<=>"), "expected cosine operator, got:\n{}", out.sql);
14169 }
14170
14171 #[test]
14172 fn test_vector_search_subquery_filter_combined_with_outer_property_filter() {
14173 let schema = make_schema_with_vector();
14174 let out = compile_and_emit_with(
14175 "WITH search := vector::search((select Person filter .age > 18), <pgvector::vector>[1.0, 2.0, 3.0, 4.0]) \
14176 SELECT search { object { name }, distance }",
14177 &schema,
14178 );
14179 assert!(
14180 out.sql.contains("\"age\""),
14181 "expected age pre-filter, got:\n{}",
14182 out.sql
14183 );
14184 assert!(out.sql.contains("18"), "expected filter value 18, got:\n{}", out.sql);
14185 assert!(out.sql.contains("<=>"), "expected cosine operator, got:\n{}", out.sql);
14186 }
14187
14188 #[test]
14189 fn test_vector_search_text_overload_with_subquery_filter() {
14190 let schema = make_schema_with_vector();
14191 let out = compile_and_emit_with(
14192 "WITH search := vector::search((select Person filter .name = 'Alice'), query := $q) \
14193 SELECT search { object { name }, distance }",
14194 &schema,
14195 );
14196 assert!(
14197 out.sql.contains("\"Person\""),
14198 "expected Person table, got:\n{}",
14199 out.sql
14200 );
14201 assert!(
14202 out.sql.contains("Alice"),
14203 "expected pre-filter value, got:\n{}",
14204 out.sql
14205 );
14206 assert!(
14207 out.sql.contains("float8[]"),
14208 "expected float8[] cast for deferred vec param, got:\n{}",
14209 out.sql
14210 );
14211 assert!(out.sql.contains("<=>"), "expected cosine operator, got:\n{}", out.sql);
14212 }
14213
14214 #[test]
14215 fn test_count_type_ref_compiles_to_agg_over_query() {
14216 let out = compile_and_emit("SELECT count(Person)");
14217 assert!(out.sql.contains("count(*)"), "expected count(*), got:\n{}", out.sql);
14218 assert!(
14219 out.sql.contains("\"Person\""),
14220 "expected Person table, got:\n{}",
14221 out.sql
14222 );
14223 }
14224
14225 #[test]
14226 fn test_count_qualified_type_ref_compiles_to_agg_over_query() {
14227 let out = compile_and_emit("SELECT count(default::Person)");
14228 assert!(out.sql.contains("count(*)"), "expected count(*), got:\n{}", out.sql);
14229 assert!(
14230 out.sql.contains("\"Person\""),
14231 "expected Person table, got:\n{}",
14232 out.sql
14233 );
14234 }
14235
14236 #[test]
14237 fn test_count_subquery_compiles_to_agg_over_query() {
14238 let out = compile_and_emit("SELECT count((select Person))");
14239 assert!(out.sql.contains("count(*)"), "expected count(*), got:\n{}", out.sql);
14240 assert!(
14241 out.sql.contains("\"Person\""),
14242 "expected Person table, got:\n{}",
14243 out.sql
14244 );
14245 }
14246
14247 #[test]
14248 fn test_count_subquery_with_filter() {
14249 let out = compile_and_emit("SELECT count((select Person filter .name = 'Alice'))");
14250 assert!(out.sql.contains("count(*)"), "expected count(*), got:\n{}", out.sql);
14251 assert!(
14252 out.sql.contains("\"name\""),
14253 "expected filter on name, got:\n{}",
14254 out.sql
14255 );
14256 }
14257
14258 #[test]
14259 fn test_positional_param_compiles_to_dollar_n() {
14260 let out = compile_and_emit("SELECT Person FILTER .name = $0");
14261 assert!(out.sql.contains("$1"), "expected $1 placeholder, got:\n{}", out.sql);
14262 }
14263
14264 #[test]
14265 fn test_multiple_positional_params_compile_in_order() {
14266 let out = compile_and_emit("SELECT Person FILTER .name = $0 AND .age > $1");
14267 assert!(out.sql.contains("$1"), "expected $1, got:\n{}", out.sql);
14268 assert!(out.sql.contains("$2"), "expected $2, got:\n{}", out.sql);
14269 }
14270
14271 #[test]
14272 fn test_repeated_positional_param_reuses_slot() {
14273 let out = compile_and_emit("SELECT Person FILTER .name = $0 OR .name = $0");
14274 assert_eq!(
14275 out.sql.matches("$1").count(),
14276 2,
14277 "both uses must reference $1, got:\n{}",
14278 out.sql
14279 );
14280 }
14281
14282 #[test]
14283 fn test_cast_to_nonexistent_type_names_full_type() {
14284 let schema = make_schema();
14285 let ast = parse::parse("SELECT Person FILTER .name = <default::Ghost>$name").unwrap();
14286 match ir::compile(&ast, &schema) {
14287 Ok(_) => panic!("expected compile error for unknown type"),
14288 Err(e) => {
14289 let msg = e.to_string();
14290 assert!(
14291 msg.contains("unknown type 'default::Ghost'"),
14292 "expected full type name in error, got: {msg}",
14293 );
14294 }
14295 }
14296 }
14297
14298 #[test]
14299 fn test_cast_to_nonexistent_unqualified_type_names_type() {
14300 let schema = make_schema();
14301 let ast = parse::parse("SELECT Person FILTER .name = <Ghost>$name").unwrap();
14302 match ir::compile(&ast, &schema) {
14303 Ok(_) => panic!("expected compile error for unknown type"),
14304 Err(e) => {
14305 let msg = e.to_string();
14306 assert!(
14307 msg.contains("unknown type 'Ghost'"),
14308 "expected type name in error, got: {msg}",
14309 );
14310 }
14311 }
14312 }
14313
14314 #[test]
14315 fn test_top_level_cast_to_registered_scalar_uses_its_own_domain() {
14316 use crate::schema::ScalarDescriptor;
14322 let mut schema = make_schema();
14323 schema.scalars.push(ScalarDescriptor {
14324 name: "Email".into(),
14325 module: "default".into(),
14326 base: "Str".into(),
14327 pg_type: "text".into(),
14328 check_constraints: vec!["value ~ '^[^@]+@[^@]+\\.[^@]+$'".into()],
14329 is_sequence: false,
14330 });
14331 let out = compile_and_emit_with("SELECT <default::Email>'test@test.de'", &schema);
14332 assert!(
14333 out.sql.contains("\"public\".\"Email\""),
14334 "expected a cast to the scalar's own domain, got:\n{}",
14335 out.sql
14336 );
14337 assert!(
14338 !out.sql.to_lowercase().contains("\"person\""),
14339 "must not be misrouted to an object-type lookup, got:\n{}",
14340 out.sql
14341 );
14342 }
14343
14344 #[test]
14345 fn test_top_level_cast_to_unknown_type_names_full_type() {
14346 let schema = make_schema();
14351 let ast = parse::parse("SELECT <default::Ghost>$name").unwrap();
14352 match ir::compile(&ast, &schema) {
14353 Ok(_) => panic!("expected compile error for unknown type"),
14354 Err(e) => {
14355 let msg = e.to_string();
14356 assert!(
14357 msg.contains("unknown type 'default::Ghost'"),
14358 "expected full type name in error, got: {msg}",
14359 );
14360 }
14361 }
14362 }
14363
14364 #[test]
14365 fn test_structural_tuple_cast_unnamed_resolves_to_jsonb() {
14366 let out = compile_and_emit("SELECT <tuple<str, bool>>$p");
14367 assert!(out.sql.contains("($1)::jsonb"), "got:\n{}", out.sql);
14368 }
14369
14370 #[test]
14371 fn test_jsonb_to_uuid_cast_extracts_via_text() {
14372 let out = compile_and_emit("SELECT <uuid>(<json>$p)");
14374 assert!(out.sql.contains("#>> '{}'"), "got:\n{}", out.sql);
14375 assert!(out.sql.contains("::uuid"), "got:\n{}", out.sql);
14376 }
14377
14378 #[test]
14379 fn test_jsonb_to_datetime_cast_extracts_via_text() {
14380 let out = compile_and_emit("SELECT <datetime>(<json>$p)");
14381 assert!(out.sql.contains("#>> '{}'"), "got:\n{}", out.sql);
14382 assert!(out.sql.contains("::timestamptz"), "got:\n{}", out.sql);
14383 }
14384
14385 #[test]
14386 fn test_jsonb_to_duration_cast_extracts_via_text() {
14387 let out = compile_and_emit("SELECT <duration>(<json>$p)");
14388 assert!(out.sql.contains("#>> '{}'"), "got:\n{}", out.sql);
14389 assert!(out.sql.contains("::interval"), "got:\n{}", out.sql);
14390 }
14391
14392 #[test]
14393 fn test_jsonb_to_array_cast_unpacks_each_element() {
14394 let out = compile_and_emit("SELECT <array<int64>>(<json>$p)");
14395 assert!(out.sql.contains("jsonb_array_elements("), "got:\n{}", out.sql);
14396 assert!(out.sql.contains("#>> '{}'"), "got:\n{}", out.sql);
14397 assert!(out.sql.contains("::int8"), "got:\n{}", out.sql);
14398 }
14399
14400 #[test]
14401 fn test_non_jsonb_cast_is_unaffected_by_jsonb_extraction() {
14402 let out = compile_and_emit("SELECT <uuid>$p");
14405 assert!(!out.sql.contains("#>>"), "got:\n{}", out.sql);
14406 assert!(out.sql.contains("::uuid"), "got:\n{}", out.sql);
14407 }
14408
14409 #[test]
14410 fn test_datetime_plus_duration_type_checks() {
14411 let out = compile_and_emit("SELECT <datetime>$p + <duration>$d");
14416 assert!(out.sql.contains(" + "), "got:\n{}", out.sql);
14417 }
14418
14419 #[test]
14420 fn test_duration_plus_datetime_type_checks_either_order() {
14421 let out = compile_and_emit("SELECT <duration>$d + <datetime>$p");
14422 assert!(out.sql.contains(" + "), "got:\n{}", out.sql);
14423 }
14424
14425 #[test]
14426 fn test_local_date_plus_duration_type_checks() {
14427 let out = compile_and_emit("SELECT <cal::local_date>$p + <duration>$d");
14428 assert!(out.sql.contains(" + "), "got:\n{}", out.sql);
14429 }
14430
14431 #[test]
14432 fn test_datetime_minus_duration_type_checks() {
14433 let out = compile_and_emit("SELECT <datetime>$p - <duration>$d");
14434 assert!(out.sql.contains(" - "), "got:\n{}", out.sql);
14435 }
14436
14437 #[test]
14438 fn test_local_time_plus_duration_type_checks() {
14439 let out = compile_and_emit("SELECT <cal::local_time>$p + <duration>$d");
14440 assert!(out.sql.contains(" + "), "got:\n{}", out.sql);
14441 }
14442
14443 #[test]
14444 fn test_datetime_plus_duration_only_allowed_for_add_and_sub() {
14445 let schema = make_schema();
14448 let ast = parse::parse("SELECT <datetime>$p = <duration>$d").unwrap();
14449 match ir::compile(&ast, &schema) {
14450 Err(err) => assert!(format!("{err}").contains("cannot be applied"), "got: {err}"),
14451 Ok(_) => panic!("expected a compile error"),
14452 }
14453 }
14454
14455 #[test]
14456 fn test_int_and_float_mix_is_allowed() {
14457 let out = compile_and_emit("SELECT <int16>1 + <float32>2.0");
14461 assert!(out.sql.contains(" + "), "got:\n{}", out.sql);
14462 }
14463
14464 #[test]
14465 fn test_int_and_decimal_mix_is_allowed() {
14466 let out = compile_and_emit("SELECT <int64>1 + <decimal>2.0");
14470 assert!(out.sql.contains(" + "), "got:\n{}", out.sql);
14471 }
14472
14473 #[test]
14474 fn test_float_and_decimal_mix_still_rejected() {
14475 let schema = make_schema();
14479 let ast = parse::parse("SELECT <decimal>$p + <float64>$n").unwrap();
14480 match ir::compile(&ast, &schema) {
14481 Err(err) => assert!(format!("{err}").contains("cannot be applied"), "got: {err}"),
14482 Ok(_) => panic!("expected a compile error"),
14483 }
14484 }
14485
14486 #[test]
14487 fn test_unknown_pointer_suggests_a_close_match() {
14488 let schema = make_schema();
14492 let ast = parse::parse("SELECT Person { nam }").unwrap();
14493 match ir::compile(&ast, &schema) {
14494 Err(err) => assert_eq!(
14495 format!("{err}"),
14496 "object type 'default::Person' has no link or property 'nam'. Did you mean 'name'?"
14497 ),
14498 Ok(_) => panic!("expected a compile error"),
14499 }
14500 }
14501
14502 #[test]
14503 fn test_unrelated_unknown_pointer_gets_no_suggestion() {
14504 let schema = make_schema();
14507 let ast = parse::parse("SELECT Person { xyzxyzxyz }").unwrap();
14508 match ir::compile(&ast, &schema) {
14509 Err(err) => {
14510 let msg = format!("{err}");
14511 assert!(msg.contains("has no link or property 'xyzxyzxyz'"), "got: {msg}");
14512 assert!(!msg.contains("Did you mean"), "got: {msg}");
14513 }
14514 Ok(_) => panic!("expected a compile error"),
14515 }
14516 }
14517
14518 #[test]
14519 fn test_multilink_sourced_backlink_exists_filter_compiles() {
14520 let out = compile_and_emit("SELECT Post filter exists .<posts[is Person]");
14530 assert!(out.sql.contains("EXISTS"), "got:\n{}", out.sql);
14531 }
14532
14533 #[test]
14534 fn test_backlink_as_computed_shape_pointer_compiles() {
14535 let out = compile_and_emit("SELECT Post { title, authors := .<posts[is Person] { name } }");
14548 assert!(out.sql.contains("array_agg(ROW("), "got:\n{}", out.sql);
14549 }
14550
14551 fn schema_with_exclusive_posts() -> SchemaDescriptor {
14552 let mut schema = make_schema();
14553 for td in schema.types.iter_mut().filter(|td| td.name == "Person") {
14554 for ml in td.multilinks.iter_mut().filter(|ml| ml.name == "posts") {
14555 ml.is_exclusive = true;
14556 }
14557 }
14558 schema
14559 }
14560
14561 #[test]
14562 fn test_backlink_through_an_exclusive_multilink_is_single() {
14563 let out = compile_and_emit_with(
14564 "SELECT Post { title, author := .<posts[is Person] { name } }",
14565 &schema_with_exclusive_posts(),
14566 );
14567 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
14568 panic!("expected an object root, got {:?}", out.shape.root)
14569 };
14570 assert!(
14571 matches!(pointers.last(), Some(ShapeNode::Object { name, .. }) if name == "author"),
14572 "got {pointers:?}"
14573 );
14574 }
14575
14576 #[test]
14577 fn test_a_path_through_an_exclusive_backlink_is_single() {
14578 let out = compile_and_emit_with(
14579 "SELECT Post { title, author_name := .<posts[is Person].name }",
14580 &schema_with_exclusive_posts(),
14581 );
14582 let ShapeNode::Object { pointers, .. } = &out.shape.root else {
14583 panic!("expected an object root, got {:?}", out.shape.root)
14584 };
14585 assert!(
14586 matches!(pointers.last(), Some(ShapeNode::Scalar { name, .. }) if name == "author_name"),
14587 "got {pointers:?}"
14588 );
14589 assert!(!out.sql.contains("ARRAY("), "got:\n{}", out.sql);
14590 }
14591
14592 #[test]
14593 fn test_structural_tuple_cast_named_resolves_to_jsonb() {
14594 let out = compile_and_emit("SELECT <tuple<x: float64, y: float64>>$p");
14595 assert!(out.sql.contains("($1)::jsonb"), "got:\n{}", out.sql);
14596 }
14597
14598 #[test]
14599 fn test_structural_tuple_cast_nested_resolves_to_jsonb() {
14600 let out = compile_and_emit("SELECT <tuple<point: tuple<x: float64, y: float64>, label: str>>$p");
14601 assert!(out.sql.contains("($1)::jsonb"), "got:\n{}", out.sql);
14602 }
14603
14604 #[test]
14605 fn test_nominal_named_tuple_cast_resolves_to_jsonb() {
14606 let mut schema = make_schema();
14607 schema.named_tuples.push(NamedTupleDescriptor {
14608 name: "Point".into(),
14609 module: "default".into(),
14610 members: vec![],
14611 });
14612 let out = compile_and_emit_with("SELECT <default::Point>$p", &schema);
14613 assert!(out.sql.contains("($1)::jsonb"), "got:\n{}", out.sql);
14614 }
14615
14616 #[test]
14617 fn test_array_literal_cast_resolves_to_native_pg_array_not_jsonb() {
14618 let out = compile_and_emit("SELECT <array<str>>['foo', 'bar']");
14622 assert!(
14623 out.sql.contains("::text[]") || out.sql.contains("ARRAY["),
14624 "got:\n{}",
14625 out.sql
14626 );
14627 assert!(
14628 !out.sql.contains("jsonb"),
14629 "arrays must not use jsonb, got:\n{}",
14630 out.sql
14631 );
14632 }
14633
14634 #[test]
14635 fn test_array_literal_cast_applies_per_element_cast() {
14636 let out = compile_and_emit("SELECT <array<int64>>['1', '2']");
14640 assert!(out.sql.contains("ARRAY[('1')::int8, ('2')::int8]"), "got:\n{}", out.sql);
14641 }
14642
14643 #[test]
14644 fn test_array_param_cast_uses_direct_suffix_cast() {
14645 let out = compile_and_emit("SELECT <array<int64>>$p");
14646 assert!(out.sql.contains("::int8[]"), "got:\n{}", out.sql);
14647 }
14648
14649 #[test]
14650 fn test_array_of_named_tuple_element_casts_to_jsonb_array() {
14651 let out = compile_and_emit("SELECT <array<tuple<x: float64, y: float64>>>$p");
14655 assert!(out.sql.contains("::jsonb[]"), "got:\n{}", out.sql);
14656 }
14657
14658 #[test]
14659 fn test_contains_on_array_literal_cast_uses_array_overload_not_strpos() {
14660 let out = compile_and_emit("SELECT contains(<array<str>>[1, 2], '2')");
14666 assert!(out.sql.contains("@> ARRAY["), "got:\n{}", out.sql);
14667 assert!(
14668 !out.sql.contains("strpos"),
14669 "must not fall back to the str/str overload, got:\n{}",
14670 out.sql
14671 );
14672 }
14673
14674 #[test]
14675 fn test_nested_array_type_rejected_at_parse_time() {
14676 match parse::parse("SELECT <array<array<str>>>$p") {
14677 Ok(_) => panic!("expected parse error for nested array type"),
14678 Err(e) => assert!(e.to_string().contains("nested arrays are not supported"), "got: {}", e),
14679 }
14680 }
14681
14682 #[test]
14683 fn test_array_cast_in_computed_shape_field_schema_bound_context() {
14684 let out = compile_and_emit("SELECT Person { name, tags := <array<str>>['a', 'b'] }");
14687 assert!(out.sql.contains("ARRAY[('a')::text, ('b')::text]"), "got:\n{}", out.sql);
14688 }
14689
14690 #[test]
14691 fn test_nominal_named_tuple_cast_shape_carries_real_members() {
14692 use crate::schema::{TupleMemberDescriptor, TupleMemberKind};
14693 let mut schema = make_schema();
14694 schema.named_tuples.push(NamedTupleDescriptor {
14695 name: "Point".into(),
14696 module: "default".into(),
14697 members: vec![
14698 TupleMemberDescriptor {
14699 name: Some("x".into()),
14700 kind: TupleMemberKind::Scalar {
14701 pg_type: "float8".into(),
14702 },
14703 },
14704 TupleMemberDescriptor {
14705 name: Some("y".into()),
14706 kind: TupleMemberKind::Scalar {
14707 pg_type: "float8".into(),
14708 },
14709 },
14710 ],
14711 });
14712 let out = compile_and_emit_with("SELECT <default::Point>$p", &schema);
14713 match &out.shape.root {
14714 crate::query::ShapeNode::NamedTuple { type_name, members, .. } => {
14715 assert_eq!(type_name.as_deref(), Some("default::Point"));
14716 let members = members.as_ref().expect("expected resolved members");
14717 assert_eq!(members.len(), 2);
14718 assert_eq!(members[0].key.as_deref(), Some("x"));
14719 assert_eq!(members[1].key.as_deref(), Some("y"));
14720 }
14721 other => panic!("expected ShapeNode::NamedTuple, got {other:?}"),
14722 }
14723 }
14724
14725 #[test]
14726 fn test_structural_tuple_property_read_shape_carries_real_members() {
14727 use crate::schema::{TupleMemberDescriptor, TupleMemberKind};
14728 let schema = SchemaDescriptor {
14729 types: vec![TypeDescriptor {
14730 name: "Person".into(),
14731 module: "default".into(),
14732 table: "Person".into(),
14733 abstract_: false,
14734 materialized: false,
14735 description: None,
14736 parents: vec![],
14737 interfaces: vec![],
14738 bases: vec![],
14739 properties: vec![PropertyDescriptor {
14740 name: "address".into(),
14741 pg_type: "jsonb".into(),
14742 nullable: true,
14743 default_sql: None,
14744 default_pyql: None,
14745 description: None,
14746 check_constraints: vec![],
14747 is_exclusive: false,
14748 is_pk: false,
14749 is_readonly: false,
14750 rewrites: vec![],
14751 tuple_members: Some(vec![
14752 TupleMemberDescriptor {
14753 name: Some("street".into()),
14754 kind: TupleMemberKind::Scalar { pg_type: "text".into() },
14755 },
14756 TupleMemberDescriptor {
14757 name: Some("zip".into()),
14758 kind: TupleMemberKind::Scalar { pg_type: "text".into() },
14759 },
14760 ]),
14761 column_type: None,
14762 }],
14763 links: vec![],
14764 multilinks: vec![],
14765 computed: vec![],
14766 constraints: vec![],
14767 indexes: vec![],
14768 partition: None,
14769 vector_indexes: vec![],
14770 search_indexes: vec![],
14771 triggers: vec![],
14772 junction: false,
14773 signals: vec![],
14774 }],
14775 scalars: vec![],
14776 enums: vec![],
14777 named_tuples: vec![],
14778 globals: vec![],
14779 functions: vec![],
14780 aliases: vec![],
14781 channels: vec![],
14782 ..Default::default()
14783 };
14784 let out = compile_and_emit_with("SELECT Person { address }", &schema);
14785 assert!(out.sql.contains("::jsonb"), "got:\n{}", out.sql);
14786 match &out.shape.root {
14787 crate::query::ShapeNode::Object { pointers, .. } => {
14788 let address = pointers
14789 .iter()
14790 .find(|p| matches!(p, crate::query::ShapeNode::NamedTuple { name, .. } if name == "address"))
14791 .expect("expected address pointer in shape");
14792 match address {
14793 crate::query::ShapeNode::NamedTuple { type_name, members, .. } => {
14794 assert_eq!(*type_name, None);
14795 let members = members.as_ref().expect("expected resolved members");
14796 assert_eq!(members.len(), 2);
14797 assert_eq!(members[0].key.as_deref(), Some("street"));
14798 assert_eq!(members[1].key.as_deref(), Some("zip"));
14799 }
14800 other => panic!("expected NamedTuple, got {other:?}"),
14801 }
14802 }
14803 other => panic!("expected ShapeNode::Object, got {other:?}"),
14804 }
14805 }
14806
14807 #[test]
14808 fn test_bare_path_select_structural_tuple_property_shape_carries_real_members() {
14809 use crate::schema::{TupleMemberDescriptor, TupleMemberKind};
14819 let schema = SchemaDescriptor {
14820 types: vec![TypeDescriptor {
14821 name: "Person".into(),
14822 module: "default".into(),
14823 table: "Person".into(),
14824 abstract_: false,
14825 materialized: false,
14826 description: None,
14827 parents: vec![],
14828 interfaces: vec![],
14829 bases: vec![],
14830 properties: vec![PropertyDescriptor {
14831 name: "address".into(),
14832 pg_type: "jsonb".into(),
14833 nullable: true,
14834 default_sql: None,
14835 default_pyql: None,
14836 description: None,
14837 check_constraints: vec![],
14838 is_exclusive: false,
14839 is_pk: false,
14840 is_readonly: false,
14841 rewrites: vec![],
14842 tuple_members: Some(vec![
14843 TupleMemberDescriptor {
14844 name: Some("street".into()),
14845 kind: TupleMemberKind::Scalar { pg_type: "text".into() },
14846 },
14847 TupleMemberDescriptor {
14848 name: Some("zip".into()),
14849 kind: TupleMemberKind::Scalar { pg_type: "text".into() },
14850 },
14851 ]),
14852 column_type: None,
14853 }],
14854 links: vec![],
14855 multilinks: vec![],
14856 computed: vec![],
14857 constraints: vec![],
14858 indexes: vec![],
14859 partition: None,
14860 vector_indexes: vec![],
14861 search_indexes: vec![],
14862 triggers: vec![],
14863 junction: false,
14864 signals: vec![],
14865 }],
14866 scalars: vec![],
14867 enums: vec![],
14868 named_tuples: vec![],
14869 globals: vec![],
14870 functions: vec![],
14871 aliases: vec![],
14872 channels: vec![],
14873 ..Default::default()
14874 };
14875 let out = compile_and_emit_with("SELECT Person.address", &schema);
14876 match &out.shape.root {
14877 crate::query::ShapeNode::NamedTuple { type_name, members, .. } => {
14878 assert_eq!(*type_name, None);
14879 let members = members.as_ref().expect("expected resolved members");
14880 assert_eq!(members.len(), 2);
14881 assert_eq!(members[0].key.as_deref(), Some("street"));
14882 assert_eq!(members[1].key.as_deref(), Some("zip"));
14883 }
14884 other => panic!("expected ShapeNode::NamedTuple, got {other:?}"),
14885 }
14886 }
14887
14888 #[test]
14889 fn test_path_traversal_into_structural_tuple_property() {
14890 use crate::schema::{TupleMemberDescriptor, TupleMemberKind};
14891 let schema = SchemaDescriptor {
14892 types: vec![TypeDescriptor {
14893 name: "Person".into(),
14894 module: "default".into(),
14895 table: "Person".into(),
14896 abstract_: false,
14897 materialized: false,
14898 description: None,
14899 parents: vec![],
14900 interfaces: vec![],
14901 bases: vec![],
14902 properties: vec![PropertyDescriptor {
14903 name: "address".into(),
14904 pg_type: "jsonb".into(),
14905 nullable: true,
14906 default_sql: None,
14907 default_pyql: None,
14908 description: None,
14909 check_constraints: vec![],
14910 is_exclusive: false,
14911 is_pk: false,
14912 is_readonly: false,
14913 rewrites: vec![],
14914 tuple_members: Some(vec![
14915 TupleMemberDescriptor {
14916 name: Some("street".into()),
14917 kind: TupleMemberKind::Scalar { pg_type: "text".into() },
14918 },
14919 TupleMemberDescriptor {
14920 name: Some("zip".into()),
14921 kind: TupleMemberKind::Scalar { pg_type: "text".into() },
14922 },
14923 ]),
14924 column_type: None,
14925 }],
14926 links: vec![],
14927 multilinks: vec![],
14928 computed: vec![],
14929 constraints: vec![],
14930 indexes: vec![],
14931 partition: None,
14932 vector_indexes: vec![],
14933 search_indexes: vec![],
14934 triggers: vec![],
14935 junction: false,
14936 signals: vec![],
14937 }],
14938 scalars: vec![],
14939 enums: vec![],
14940 named_tuples: vec![],
14941 globals: vec![],
14942 functions: vec![],
14943 aliases: vec![],
14944 channels: vec![],
14945 ..Default::default()
14946 };
14947 let out = compile_and_emit_with("SELECT default::Person.address.street", &schema);
14953 assert!(out.sql.contains("\"address\"->'street'"), "got:\n{}", out.sql);
14954 }
14955
14956 #[test]
14957 fn test_structural_tuple_cast_shape_carries_real_members() {
14958 let out = compile_and_emit("SELECT <tuple<street: str, zip: str>>$p");
14959 match &out.shape.root {
14960 crate::query::ShapeNode::NamedTuple { type_name, members, .. } => {
14961 assert_eq!(*type_name, None);
14962 let members = members.as_ref().expect("expected resolved members");
14963 assert_eq!(members.len(), 2);
14964 assert_eq!(members[0].key.as_deref(), Some("street"));
14965 assert_eq!(members[1].key.as_deref(), Some("zip"));
14966 }
14967 other => panic!("expected ShapeNode::NamedTuple, got {other:?}"),
14968 }
14969 }
14970
14971 #[test]
14972 fn test_tuple_cast_mixed_named_and_unnamed_elements_rejected() {
14973 match parse::parse("SELECT <tuple<x: float64, bool>>$p") {
14974 Ok(_) => panic!("expected parse error for mixed named/unnamed tuple elements"),
14975 Err(e) => assert!(e.to_string().contains("all named or all unnamed"), "got: {}", e),
14976 }
14977 }
14978
14979 #[test]
14980 fn test_is_with_tuple_type_rejected() {
14981 let schema = make_schema();
14982 let ast = parse::parse("SELECT Person FILTER Person is tuple<x: float64, y: float64>").unwrap();
14983 match ir::compile(&ast, &schema) {
14984 Ok(_) => panic!("expected error for IS with a tuple type"),
14985 Err(e) => {
14986 assert!(
14987 e.to_string().contains("cannot use IS with a tuple or array type"),
14988 "got: {}",
14989 e
14990 );
14991 }
14992 }
14993 }
14994
14995 #[test]
14996 fn test_is_with_array_type_rejected() {
14997 let schema = make_schema();
14998 let ast = parse::parse("SELECT Person FILTER Person is array<str>").unwrap();
14999 match ir::compile(&ast, &schema) {
15000 Ok(_) => panic!("expected error for IS with an array type"),
15001 Err(e) => {
15002 assert!(
15003 e.to_string().contains("cannot use IS with a tuple or array type"),
15004 "got: {}",
15005 e
15006 );
15007 }
15008 }
15009 }
15010
15011 #[test]
15012 fn test_tuple_index_on_non_literal_falls_back_to_jsonb_index() {
15013 let out = compile_and_emit("SELECT (<tuple<int64, str>>('1', 3)).1");
15017 assert!(out.sql.contains("->1"), "got:\n{}", out.sql);
15018 }
15019
15020 #[test]
15021 fn test_tuple_index_out_of_bounds_on_cast_target_errors_at_compile_time() {
15022 let ast = parse::parse("SELECT (<tuple<int64, str>>('1', 3)).2").unwrap();
15028 let schema = make_schema();
15029 match ir::compile(&ast, &schema) {
15030 Ok(_) => panic!("expected out-of-bounds tuple index error"),
15031 Err(e) => {
15032 assert!(
15033 e.to_string()
15034 .contains("2 is not a member of tuple<std::int64, std::str>"),
15035 "got: {}",
15036 e
15037 );
15038 }
15039 }
15040 }
15041
15042 #[test]
15043 fn test_positional_tuple_literal_cast_to_tuple_type_compiles() {
15044 let out = compile_and_emit("SELECT <tuple<int64, str>>(1, 'x')");
15048 assert!(
15049 out.sql.contains("jsonb_build_array((1)::int8, ('x')::text)"),
15050 "got:\n{}",
15051 out.sql
15052 );
15053 }
15054
15055 #[test]
15056 fn test_positional_tuple_literal_cast_coerces_mismatched_literal_types() {
15057 let out = compile_and_emit("SELECT <tuple<int64, str>>('1', 3)");
15061 assert!(
15062 out.sql.contains("jsonb_build_array(('1')::int8, (3)::text)"),
15063 "got:\n{}",
15064 out.sql
15065 );
15066 }
15067
15068 #[test]
15069 fn test_nested_tuple_literal_cast_applies_casts_recursively() {
15070 let out = compile_and_emit(
15071 "SELECT <tuple<point: tuple<x: float64, y: float64>, label: str>>(point := ('1', 2), label := 5)",
15072 );
15073 assert!(
15074 out.sql.contains(
15075 "jsonb_build_object('point', jsonb_build_object('x', ('1')::float8, 'y', (2)::float8), 'label', (5)::text)"
15076 ),
15077 "got:\n{}",
15078 out.sql
15079 );
15080 }
15081
15082 #[test]
15083 fn test_positional_tuple_literal_nested_inside_named_tuple_compiles() {
15084 let out = compile_and_emit("SELECT (point := (1, 2), label := 'origin')");
15085 assert!(out.sql.contains("jsonb_build_array(1, 2)"), "got:\n{}", out.sql);
15086 assert!(out.sql.contains("jsonb_build_object("), "got:\n{}", out.sql);
15087 }
15088
15089 #[test]
15090 fn test_positional_tuple_literal_in_schema_bound_shape_field_compiles() {
15091 let out = compile_and_emit("SELECT Person { name, pair := (1, 2) }");
15092 assert!(out.sql.contains("jsonb_build_array(1, 2)"), "got:\n{}", out.sql);
15093 }
15094
15095 fn make_schema_with_search_index(backend: crate::schema::SearchBackend) -> SchemaDescriptor {
15104 use crate::schema::{SearchIndexDescriptor, SearchPointerDescriptor, SearchWeight};
15105 let mut s = make_schema();
15106 if let Some(td) = s.types.iter_mut().find(|t| t.name == "Person") {
15107 td.search_indexes.push(SearchIndexDescriptor {
15108 index_name: None,
15109 backend,
15110 pointers: vec![SearchPointerDescriptor {
15111 name: "name".into(),
15112 weight: SearchWeight::A,
15113 }],
15114 });
15115 }
15116 s
15117 }
15118
15119 #[test]
15120 fn test_insert_enqueues_a_meilisearch_outbox_row() {
15121 let schema = make_schema_with_search_index(crate::schema::SearchBackend::Meilisearch);
15122 let out = compile_and_emit_with("INSERT Person { name := 'Alice', age := 30 }", &schema);
15123 assert!(
15124 out.sql.contains("'Meilisearch'::_pylon.\"IndexKind\""),
15125 "expected a Meilisearch outbox enqueue CTE, got:\n{}",
15126 out.sql,
15127 );
15128 assert!(
15129 out.sql.contains("INSERT INTO _pylon.\"IndexOutbox\""),
15130 "got:\n{}",
15131 out.sql
15132 );
15133 }
15134
15135 #[test]
15136 fn test_insert_enqueues_an_opensearch_outbox_row() {
15137 let schema = make_schema_with_search_index(crate::schema::SearchBackend::OpenSearch);
15138 let out = compile_and_emit_with("INSERT Person { name := 'Alice', age := 30 }", &schema);
15139 assert!(
15140 out.sql.contains("'OpenSearch'::_pylon.\"IndexKind\""),
15141 "expected an OpenSearch outbox enqueue CTE, got:\n{}",
15142 out.sql,
15143 );
15144 }
15145
15146 #[test]
15147 fn test_insert_does_not_enqueue_an_outbox_row_for_a_postgres_backed_search_index() {
15148 let schema = make_schema_with_search_index(crate::schema::SearchBackend::Postgres);
15151 let out = compile_and_emit_with("INSERT Person { name := 'Alice', age := 30 }", &schema);
15152 assert!(
15153 !out.sql.contains("_pylon.\"IndexOutbox\""),
15154 "did not expect an outbox enqueue, got:\n{}",
15155 out.sql
15156 );
15157 }
15158
15159 #[test]
15160 fn test_update_enqueues_a_meilisearch_outbox_row() {
15161 let schema = make_schema_with_search_index(crate::schema::SearchBackend::Meilisearch);
15162 let out = compile_and_emit_with("UPDATE Person FILTER .name = 'Alice' SET { age := 31 }", &schema);
15163 assert!(
15164 out.sql.contains("'Meilisearch'::_pylon.\"IndexKind\""),
15165 "expected a Meilisearch outbox enqueue CTE, got:\n{}",
15166 out.sql,
15167 );
15168 }
15169
15170 #[test]
15171 fn test_delete_enqueues_a_meilisearch_outbox_delete_job() {
15172 let schema = make_schema_with_search_index(crate::schema::SearchBackend::Meilisearch);
15173 let out = compile_and_emit_with("DELETE Person FILTER .name = 'Alice'", &schema);
15174 assert!(
15175 out.sql.contains("'Meilisearch'::_pylon.\"IndexKind\""),
15176 "expected a Meilisearch outbox enqueue CTE, got:\n{}",
15177 out.sql,
15178 );
15179 assert!(
15180 out.sql.contains("'delete'"),
15181 "expected the delete operation literal, got:\n{}",
15182 out.sql
15183 );
15184 }
15185
15186 #[test]
15187 fn test_range_intrinsic_resolves_int_literals_to_int8range() {
15188 let out = compile_and_emit("SELECT std::overlaps(std::range(1, 3), std::range(2, 5))");
15193 assert!(out.sql.contains("int8range(1, 3)"), "got:\n{}", out.sql);
15194 assert!(out.sql.contains("int8range(2, 5)"), "got:\n{}", out.sql);
15195 assert!(
15196 out.sql.contains(" && "),
15197 "expected infix && for overlaps, got:\n{}",
15198 out.sql
15199 );
15200 assert!(
15201 !out.sql.contains("\"std\""),
15202 "must not emit a literal std schema call, got:\n{}",
15203 out.sql
15204 );
15205 }
15206
15207 #[test]
15208 fn test_range_intrinsic_resolves_datetime_to_tstzrange() {
15209 let out =
15210 compile_and_emit("SELECT std::range(<datetime>'2024-01-01T00:00:00Z', <datetime>'2024-06-01T00:00:00Z')");
15211 assert!(out.sql.contains("tstzrange("), "got:\n{}", out.sql);
15212 }
15213
15214 #[test]
15217 fn test_range_intrinsic_named_bounds_become_a_bounds_string() {
15218 let out = compile_and_emit("SELECT std::range(1, 3, inc_lower := true, inc_upper := true)");
15219 assert!(out.sql.contains("int8range(1, 3, '[]')"), "got:\n{}", out.sql);
15220 let out = compile_and_emit("SELECT std::range(1, 3, inc_lower := false)");
15221 assert!(out.sql.contains("int8range(1, 3, '()')"), "got:\n{}", out.sql);
15222 }
15223
15224 #[test]
15226 fn test_range_intrinsic_computes_a_bounds_string_from_an_expression() {
15227 let out = compile_and_emit("SELECT std::range(1, 3, inc_upper := <bool>$closed)");
15228 assert!(out.sql.contains("CASE WHEN"), "got:\n{}", out.sql);
15229 }
15230
15231 #[test]
15233 fn test_range_intrinsic_leaves_a_missing_upper_bound_unbounded() {
15234 let out = compile_and_emit("SELECT std::range(1)");
15235 assert!(out.sql.contains("int8range(1, NULL)"), "got:\n{}", out.sql);
15236 }
15237
15238 #[test]
15241 fn test_a_named_only_parameter_is_reported_as_named_only() {
15242 assert!(
15243 compile_err("SELECT std::range(1, 5, true, false)").contains("takes 'inc_lower' as a named argument only"),
15244 "{}",
15245 compile_err("SELECT std::range(1, 5, true, false)")
15246 );
15247 assert!(
15248 compile_err("SELECT std::json_set(std::to_json('{}'), 'a', std::to_json('1'))")
15249 .contains("requires the named argument 'value'"),
15250 "{}",
15251 compile_err("SELECT std::json_set(std::to_json('{}'), 'a', std::to_json('1'))")
15252 );
15253 assert!(
15255 compile_err("SELECT std::range()").contains("takes 1 or 2 argument(s), got 0"),
15256 "{}",
15257 compile_err("SELECT std::range()")
15258 );
15259 let message = compile_err("SELECT std::json_get(std::to_json('{}'), 'a', 1, 2)");
15261 assert!(message.contains("default := optional<json>"), "{message}");
15262 }
15263
15264 #[test]
15265 fn test_range_intrinsic_builds_the_empty_range() {
15266 let out = compile_and_emit("SELECT std::range(1, 3, empty := true)");
15267 assert!(out.sql.contains("'empty'::int8range"), "got:\n{}", out.sql);
15268 }
15269
15270 #[test]
15271 fn test_multirange_intrinsic_resolves_from_range_element() {
15272 let out = compile_and_emit("SELECT std::multirange([std::range(1, 3), std::range(5, 7)])");
15273 assert!(out.sql.contains("int8multirange(VARIADIC "), "got:\n{}", out.sql);
15274 assert!(
15275 !out.sql.contains("\"std\""),
15276 "must not emit a literal std schema call, got:\n{}",
15277 out.sql
15278 );
15279 }
15280}