1use crate::error::{
21 Position, PyQLError, PyQLResolutionError, PyQLTypeError, PyQLUnknownFieldError, PyQLUnknownTypeError,
22};
23use crate::parse::ast::{self, Expr, Literal, NonesOrder, ShapeElement, ShapeOp, SortDirection, Stmt};
24use crate::schema::{
25 FunctionDescriptor, LinkDescriptor, MultiLinkDescriptor, PropertyDescriptor, SchemaDescriptor, SearchBackend,
26 TypeDescriptor,
27};
28
29use std::collections::{HashMap, HashSet};
30
31use super::{
32 IrArraySource, IrAssertedPointer, IrBinOp, IrComputedGlobalCte, IrComputedPointer, IrConflict, IrCteDef, IrDelete,
33 IrExpr, IrFor, IrForIterator, IrFreeExpr, IrFtsSearch, IrFunctionCall, IrFunctionSelect, IrGlobalCte, IrGroup,
34 IrGroupOutput, IrGroupProjection, IrIfElse, IrInsert, IrLinkProp, IrLiteral, IrLockClause, IrLockStrength,
35 IrLockWait, IrMultiLinkClear, IrMultiLinkJoin, IrMultiLinkMutation, IrMultiLinkPointer, IrMultiLinkValueSource,
36 IrMultiLinkValues, IrNulls, IrOutput, IrPathJoin, IrPathResult, IrPathSelect, IrPolyFanout, IrPolyImplementor,
37 IrRowSource, IrScalarPointer, IrScalarSetPointer, IrSelect, IrSessionGlobalCte, IrShapePointer,
38 IrSingleLinkCorrelation, IrSingleLinkPointer, IrSort, IrSortDir, IrSource, IrStmt, IrTypeCast, IrUnaryOp, IrUpdate,
39 IrVectorSearch, SearchEnqueueInfo, TupleCastShape, VectorEnqueueInfo,
40};
41
42type SelectModifiers = (Option<IrExpr>, Vec<IrSort>, Option<IrExpr>, Option<IrExpr>);
51
52type SearchModifiers = (Option<IrExpr>, Option<IrSortDir>, Option<IrExpr>, Option<IrExpr>);
56
57type TypeAndShape<'e> = (String, &'e [ShapeElement], Option<&'e Stmt>, Option<String>);
60
61pub fn compile(stmt: &Stmt, schema: &SchemaDescriptor) -> Result<IrOutput, PyQLError> {
70 compile_with_config(stmt, schema, &crate::ir::SessionConfig::default())
71}
72
73pub fn compile_with_config(
76 stmt: &Stmt,
77 schema: &SchemaDescriptor,
78 config: &crate::ir::SessionConfig,
79) -> Result<IrOutput, PyQLError> {
80 let mut c = Compiler::with_config(schema, config.clone());
81
82 let (ctes, ir) = if let Stmt::With(w) = stmt {
84 if let Some(ir) = try_compile_vs_with_pattern(&mut c, w)? {
87 return Ok(IrOutput {
88 subtype_fanouts: c.subtype_fanouts(),
89 stmt: ir,
90 params: c.params,
91 param_tuple_types: c.param_tuple_types,
92 ctes: vec![],
93 global_ctes: c.global_ctes,
94 warnings: c.warnings,
95 uses_globals_arg: c.used_globals_arg,
96 });
97 }
98 if let Some(ir) = try_compile_fts_with_pattern(&mut c, w)? {
100 return Ok(IrOutput {
101 subtype_fanouts: c.subtype_fanouts(),
102 stmt: ir,
103 params: c.params,
104 param_tuple_types: c.param_tuple_types,
105 ctes: vec![],
106 global_ctes: c.global_ctes,
107 warnings: c.warnings,
108 uses_globals_arg: c.used_globals_arg,
109 });
110 }
111
112 let mut cte_defs = vec![];
113 for alias in &w.aliases {
114 if let Some(declared) = declared_pointers_of(&alias.expr) {
115 c.cte_declared_pointers.insert(alias.name.clone(), declared);
116 }
117 if c.bind_group(&alias.name, &alias.expr)? {
118 continue;
119 }
120 let ir_stmt = compile_cte_binding(&mut c, &alias.expr)?;
121 let sql_name = c.claim_cte_sql_name(&alias.name);
122 let type_name = c.register_cte(&alias.name, &ir_stmt);
123 cte_defs.append(&mut c.hoisted_ctes);
126 cte_defs.push(IrCteDef {
127 name: sql_name,
128 stmt: ir_stmt,
129 type_name,
130 correlated_to: None,
131 });
132 }
133 let main = c.compile_stmt(&w.stmt)?;
134 (cte_defs, main)
135 } else {
136 (vec![], c.compile_stmt(stmt)?)
137 };
138
139 let mut ctes = ctes;
140 ctes.extend(std::mem::take(&mut c.hoisted_ctes));
141 Ok(IrOutput {
142 subtype_fanouts: c.subtype_fanouts(),
143 stmt: ir,
144 params: c.params,
145 param_tuple_types: c.param_tuple_types,
146 ctes,
147 global_ctes: c.global_ctes,
148 warnings: c.warnings,
149 uses_globals_arg: c.used_globals_arg,
150 })
151}
152
153fn try_compile_vs_with_pattern(c: &mut Compiler<'_>, w: &ast::WithStmt) -> Result<Option<IrStmt>, PyQLError> {
156 if w.aliases.len() != 1 {
158 return Ok(None);
159 }
160 let alias_def = &w.aliases[0];
161 let fc = match &alias_def.expr {
162 Expr::FunctionCall(fc) => fc,
163 _ => return Ok(None),
164 };
165 if fc.module.as_deref() != Some("vector") || fc.name != "search" {
166 return Ok(None);
167 }
168
169 let select_stmt = match w.stmt.as_ref() {
171 Stmt::Select(s) => s,
172 _ => return Ok(None),
173 };
174 let (elements, result_inner): (&[ast::ShapeElement], &Expr) = match &select_stmt.result {
176 Expr::Shape(sh) => {
177 let inner = sh.expr.as_ref().unwrap_or(&select_stmt.result);
178 (sh.elements.as_slice(), inner)
179 }
180 other => (&[], other),
181 };
182 match result_inner {
184 Expr::Path(p) if !p.partial && p.steps.len() == 1 => {
185 if let ast::PathStep::Name(n) = &p.steps[0] {
186 if n != &alias_def.name {
187 return Ok(None);
188 }
189 } else {
190 return Ok(None);
191 }
192 }
193 _ => return Ok(None),
194 }
195
196 if let Some(ir) = c.try_compile_vector_search(fc, elements, select_stmt)? {
197 Ok(Some(IrStmt::VectorSearch(ir)))
198 } else {
199 Ok(None)
200 }
201}
202
203fn try_compile_fts_with_pattern(c: &mut Compiler<'_>, w: &ast::WithStmt) -> Result<Option<IrStmt>, PyQLError> {
204 if w.aliases.len() != 1 {
205 return Ok(None);
206 }
207 let alias_def = &w.aliases[0];
208 let fc = match &alias_def.expr {
209 Expr::FunctionCall(fc) => fc,
210 _ => return Ok(None),
211 };
212 if fc.module.as_deref() != Some("fts") || fc.name != "search" {
213 return Ok(None);
214 }
215
216 let select_stmt = match w.stmt.as_ref() {
217 Stmt::Select(s) => s,
218 _ => return Ok(None),
219 };
220 let (elements, result_inner): (&[ast::ShapeElement], &Expr) = match &select_stmt.result {
221 Expr::Shape(sh) => {
222 let inner = sh.expr.as_ref().unwrap_or(&select_stmt.result);
223 (sh.elements.as_slice(), inner)
224 }
225 other => (&[], other),
226 };
227 match result_inner {
228 Expr::Path(p) if !p.partial && p.steps.len() == 1 => {
229 if let ast::PathStep::Name(n) = &p.steps[0] {
230 if n != &alias_def.name {
231 return Ok(None);
232 }
233 } else {
234 return Ok(None);
235 }
236 }
237 _ => return Ok(None),
238 }
239
240 if let Some(ir) = c.try_compile_fts_search(fc, elements, select_stmt)? {
241 Ok(Some(IrStmt::FtsSearch(ir)))
242 } else {
243 Ok(None)
244 }
245}
246
247fn subject_default_insert(handler: &str, pointer: &str) -> Option<Expr> {
259 fn holds_insert(expr: &Expr) -> bool {
260 match expr {
261 Expr::SubQuery(stmt) => match stmt.as_ref() {
262 Stmt::Insert(_) => true,
263 Stmt::Select(sel) => holds_insert(&sel.result),
264 _ => false,
265 },
266 _ => false,
267 }
268 }
269 let Ok(Expr::BinOp(b)) = crate::parse::parse_pointer_expr(handler) else {
270 return None;
271 };
272 let reads_itself = matches!(&b.left, Expr::Path(p) if matches!(p.steps.as_slice(),
273 [ast::PathStep::Name(subject), ast::PathStep::Name(name)] if subject == "__subject__" && name == pointer));
274 (matches!(b.op, ast::BinOpKind::Coalesce) && reads_itself && holds_insert(&b.right)).then_some(b.right)
275}
276
277fn limits_to_one(sel: Option<&ast::SelectStmt>) -> bool {
279 sel.is_some_and(|sel| matches!(sel.limit, Some(Expr::Literal(ast::Literal::Int(1)))))
280}
281
282fn selects_at_most_one(sel: &ast::SelectStmt, td: &TypeDescriptor) -> bool {
285 fn pins_exclusive(expr: &Expr, td: &TypeDescriptor) -> bool {
286 let Expr::BinOp(b) = expr else { return false };
287 match b.op {
288 ast::BinOpKind::And => pins_exclusive(&b.left, td) || pins_exclusive(&b.right, td),
289 ast::BinOpKind::Eq => [&b.left, &b.right].into_iter().any(|side| {
290 matches!(side, Expr::Path(p) if p.partial
291 && matches!(p.steps.as_slice(), [ast::PathStep::Name(name)]
292 if td.properties.iter().any(|d| &d.name == name && (d.is_exclusive || d.is_pk))))
293 }),
294 _ => false,
295 }
296 }
297 matches!(sel.limit, Some(Expr::Literal(ast::Literal::Int(1))))
298 || sel.filter.as_ref().is_some_and(|f| pins_exclusive(f, td))
299}
300
301fn guarded_object_branch(expr: &Expr) -> Option<Expr> {
307 fn reads_relative(expr: &Expr) -> bool {
308 match expr {
309 Expr::Path(p) => p.partial,
310 Expr::Literal(_) | Expr::Parameter(_) | Expr::Global(_) => false,
311 Expr::BinOp(b) => reads_relative(&b.left) || reads_relative(&b.right),
312 Expr::UnaryOp(u) => reads_relative(&u.operand),
313 Expr::TypeCast(c) => reads_relative(&c.expr),
314 Expr::FunctionCall(f) => f.args.iter().chain(f.kwargs.iter().map(|(_, v)| v)).any(reads_relative),
315 Expr::IfElse(ie) => {
316 reads_relative(&ie.if_expr) || reads_relative(&ie.condition) || reads_relative(&ie.else_expr)
317 }
318 _ => true,
319 }
320 }
321 fn empty(expr: &Expr) -> bool {
322 match expr {
323 Expr::Set(items) => items.is_empty(),
324 Expr::TypeCast(c) => matches!(&c.expr, Expr::Set(items) if items.is_empty()),
325 _ => false,
326 }
327 }
328 let (ie, outer_shape) = match expr {
329 Expr::IfElse(ie) => (ie.as_ref(), None),
330 Expr::Shape(sh) => match sh.expr.as_ref()? {
331 Expr::IfElse(ie) => (ie.as_ref(), Some(sh.elements.clone())),
332 _ => return None,
333 },
334 _ => return None,
335 };
336 if reads_relative(&ie.condition) {
337 return None;
338 }
339 let (branch, condition) = match (empty(&ie.if_expr), empty(&ie.else_expr)) {
340 (false, true) => (&ie.if_expr, ie.condition.clone()),
341 (true, false) => (
342 &ie.else_expr,
343 Expr::UnaryOp(Box::new(ast::UnaryOp {
344 op: ast::UnaryOpKind::Not,
345 operand: ie.condition.clone(),
346 })),
347 ),
348 _ => return None,
349 };
350 let (branch, branch_shape) = match branch {
351 Expr::Shape(sh) => (sh.expr.as_ref()?, Some(sh.elements.clone())),
352 other => (other, None),
353 };
354 let mut select = match branch {
355 Expr::Path(p) if p.partial => ast::SelectStmt {
356 result: branch.clone(),
357 filter: None,
358 order_by: vec![],
359 offset: None,
360 limit: None,
361 lock: None,
362 },
363 Expr::SubQuery(stmt) => match stmt.as_ref() {
364 Stmt::Select(sel) if matches!(&sel.result, Expr::Path(p) if p.partial) => sel.clone(),
365 _ => return None,
366 },
367 _ => return None,
368 };
369 select.filter = Some(match select.filter.take() {
370 Some(filter) => Expr::BinOp(Box::new(ast::BinOp {
371 left: filter,
372 op: ast::BinOpKind::And,
373 right: condition,
374 })),
375 None => condition,
376 });
377 let guarded = Expr::SubQuery(Box::new(Stmt::Select(select)));
378 Some(match outer_shape.or(branch_shape) {
379 Some(elements) => Expr::Shape(Box::new(ast::ShapeExpr {
380 expr: Some(guarded),
381 elements,
382 marker_offset: None,
383 })),
384 None => guarded,
385 })
386}
387
388fn per_element_of_one_multilink(condition: &Expr, td: &TypeDescriptor) -> Option<(Vec<ast::PathStep>, Expr)> {
395 fn prefix_len(steps: &[ast::PathStep]) -> Option<usize> {
398 match steps {
399 [ast::PathStep::Name(_), ..] => Some(1),
400 [ast::PathStep::Backlink(_), ast::PathStep::TypeIntersection(_), ..] => Some(2),
401 _ => None,
402 }
403 }
404 fn rewrite(expr: &Expr, link: &mut Option<Vec<ast::PathStep>>) -> Option<Expr> {
405 Some(match expr {
406 Expr::Path(p) if p.partial => {
407 let split = prefix_len(&p.steps)?;
408 let prefix = p.steps[..split].to_vec();
409 if *link.get_or_insert_with(|| prefix.clone()) != prefix {
410 return None;
411 }
412 let rest = p.steps[split..].to_vec();
416 if rest.is_empty() {
417 return None;
418 }
419 Expr::Path(ast::Path {
420 steps: rest,
421 partial: true,
422 })
423 }
424 Expr::Path(_) | Expr::Literal(_) | Expr::Parameter(_) | Expr::Global(_) => expr.clone(),
425 Expr::BinOp(b) => Expr::BinOp(Box::new(ast::BinOp {
426 left: rewrite(&b.left, link)?,
427 op: b.op.clone(),
428 right: match (&b.op, &b.right) {
434 (ast::BinOpKind::In | ast::BinOpKind::NotIn, set @ Expr::Set(_)) => set.clone(),
435 (_, other) => rewrite(other, link)?,
436 },
437 })),
438 Expr::UnaryOp(u) => Expr::UnaryOp(Box::new(ast::UnaryOp {
439 op: u.op.clone(),
440 operand: rewrite(&u.operand, link)?,
441 })),
442 Expr::TypeCast(c) => Expr::TypeCast(Box::new(ast::TypeCast {
443 expr: rewrite(&c.expr, link)?,
444 ty: c.ty.clone(),
445 })),
446 Expr::TypeIs { expr, ty } => Expr::TypeIs {
447 expr: Box::new(rewrite(expr, link)?),
448 ty: ty.clone(),
449 },
450 Expr::FunctionCall(f)
454 if crate::stdlib::lookup(f.module.as_deref().unwrap_or("std"), &f.name)
455 .iter()
456 .any(|d| d.returns_set()) =>
457 {
458 return None;
459 }
460 Expr::FunctionCall(f) => Expr::FunctionCall(ast::FunctionCall {
461 module: f.module.clone(),
462 name: f.name.clone(),
463 args: f.args.iter().map(|a| rewrite(a, link)).collect::<Option<_>>()?,
464 kwargs: f
465 .kwargs
466 .iter()
467 .map(|(k, v)| Some((k.clone(), rewrite(v, link)?)))
468 .collect::<Option<_>>()?,
469 }),
470 _ => return None,
471 })
472 }
473 let mut link = None;
474 let element_condition = rewrite(condition, &mut link)?;
475 let link = link?;
476 let many = match link.first()? {
477 ast::PathStep::Name(name) => td.multilinks.iter().any(|m| &m.name == name),
478 ast::PathStep::Backlink(_) => true,
479 _ => false,
480 };
481 many.then_some((link, element_condition))
482}
483
484fn flatten_shape_subject(expr: &Expr) -> Option<Expr> {
494 let Expr::Shape(outer) = expr else { return None };
495 match outer.expr.as_ref()? {
496 Expr::SubQuery(stmt) => {
497 let Stmt::Select(sel) = stmt.as_ref() else { return None };
498 let bare = sel.filter.is_none()
499 && sel.order_by.is_empty()
500 && sel.offset.is_none()
501 && sel.limit.is_none()
502 && sel.lock.is_none();
503 if !bare || !matches!(sel.result, Expr::IfElse(_) | Expr::Union(_, _) | Expr::Shape(_)) {
504 return None;
505 }
506 let mut flat = outer.as_ref().clone();
507 flat.expr = Some(sel.result.clone());
508 Some(Expr::Shape(Box::new(flat)))
509 }
510 Expr::Shape(inner) => {
511 let elements = outer
512 .elements
513 .iter()
514 .map(|el| {
515 let declared = match (&el.compexpr, el.path.steps.as_slice()) {
516 (None, [ast::PathStep::Name(name)]) => inner.elements.iter().find(|d| {
517 d.compexpr.is_some()
518 && matches!(d.path.steps.as_slice(), [ast::PathStep::Name(n)] if n == name)
519 }),
520 _ => None,
521 };
522 match declared {
523 Some(d) => ShapeElement {
524 nested: el.nested.clone().or_else(|| d.nested.clone()),
525 filter: el.filter.clone().or_else(|| d.filter.clone()),
526 order_by: if el.order_by.is_empty() {
527 d.order_by.clone()
528 } else {
529 el.order_by.clone()
530 },
531 offset: el.offset.clone().or_else(|| d.offset.clone()),
532 limit: el.limit.clone().or_else(|| d.limit.clone()),
533 ..d.clone()
534 },
535 None => el.clone(),
536 }
537 })
538 .collect();
539 Some(Expr::Shape(Box::new(ast::ShapeExpr {
540 expr: inner.expr.clone(),
541 elements,
542 marker_offset: inner.marker_offset,
543 })))
544 }
545 _ => None,
546 }
547}
548
549fn group_key_binding(name: &str) -> String {
552 format!("<group key {name}>")
553}
554
555fn and_conditions(filter: Option<IrExpr>, extra: Vec<IrExpr>) -> Option<IrExpr> {
556 extra.into_iter().fold(filter, |acc, cond| match acc {
557 Some(existing) => Some(IrExpr::BinOp(Box::new(IrBinOp {
558 left: existing,
559 op: ast::BinOpKind::And,
560 right: cond,
561 }))),
562 None => Some(cond),
563 })
564}
565
566const MAX_COMPUTED_SPLICES: usize = 32;
567
568fn aggregate_over_nothing_sql(name: &str) -> Option<&'static str> {
573 match name {
574 "sum" => Some("0"),
575 "any" => Some("false"),
576 "all" => Some("true"),
577 "array_agg" => Some("'{}'"),
578 _ => None,
579 }
580}
581
582fn aggregate_over_nothing(name: &str, aggregate: IrExpr) -> IrExpr {
583 let Some(value) = aggregate_over_nothing_sql(name) else {
584 return aggregate;
585 };
586 IrExpr::FunctionCall(IrFunctionCall {
587 return_pg_type: None,
588 schema: None,
589 name: "coalesce".to_string(),
590 args: vec![aggregate, IrExpr::RawSql(value.to_string())],
591 sql_template: None,
592 })
593}
594
595fn set_walk_as_scalar(expr: IrExpr) -> IrExpr {
611 match expr {
612 IrExpr::ArrayFromSelect(source) => match *source {
613 IrArraySource::PathSelect(ps) => IrExpr::PathSubquery(ps),
614 other => IrExpr::ArrayFromSelect(Box::new(other)),
615 },
616 other => other,
617 }
618}
619
620fn yields_array(expr: &IrExpr) -> bool {
621 match expr {
622 IrExpr::ArrayFromSelect(_) => true,
623 IrExpr::IfElse(ie) => {
624 (yields_array(&ie.if_) || matches!(ie.if_, IrExpr::Null))
625 && (yields_array(&ie.else_) || matches!(ie.else_, IrExpr::Null))
626 && !(matches!(ie.if_, IrExpr::Null) && matches!(ie.else_, IrExpr::Null))
627 }
628 _ => false,
629 }
630}
631
632fn through_coalesce(expr: &IrExpr) -> &IrExpr {
636 match expr {
637 IrExpr::FunctionCall(f) if f.schema.is_none() && f.name == "coalesce" => {
638 f.args.first().map(through_coalesce).unwrap_or(expr)
639 }
640 other => other,
641 }
642}
643
644fn ir_value_type_name(expr: &IrExpr) -> String {
645 ir_value_type(expr).unwrap_or_default()
646}
647
648fn ir_value_type(expr: &IrExpr) -> Option<String> {
653 let expr = through_coalesce(expr);
654 match expr {
655 IrExpr::Array(elements) => {
656 let element = elements.first().and_then(infer_ir_type).unwrap_or("text");
657 Some(format!("{}[]", literal_sentinel_to_pg(element)))
658 }
659 IrExpr::FunctionCall(f) if f.schema.is_none() && f.name == "array_agg" => {
662 let element = f.args.first().and_then(infer_ir_type)?;
663 Some(format!("{}[]", literal_sentinel_to_pg(element)))
664 }
665 IrExpr::PathSubquery(ps) => match &ps.result {
668 IrPathResult::Scalar(inner, _) => ir_value_type(inner),
669 IrPathResult::Object { .. } => None,
670 },
671 IrExpr::ArrayFromSelect(source) => array_source_element_type(source).map(|element| format!("{element}[]")),
672 IrExpr::IfElse(ie) => ir_value_type(&ie.if_).or_else(|| ir_value_type(&ie.else_)),
675 other => infer_ir_type(other).map(|t| t.to_string()),
676 }
677}
678
679fn array_source_element_type(source: &IrArraySource) -> Option<String> {
682 match source {
683 IrArraySource::Select(sel) => match sel.rows.as_slice() {
684 [IrRowSource::Bound { shape, .. }] => match shape.first() {
685 Some(IrShapePointer::Scalar(scalar)) => Some(scalar.pg_type.clone()),
688 _ => Some("uuid".to_string()),
689 },
690 _ => None,
691 },
692 IrArraySource::PathSelect(ps) => match &ps.result {
693 IrPathResult::Scalar(expr, _) => infer_ir_type(expr).map(|t| literal_sentinel_to_pg(t).to_string()),
694 IrPathResult::Object { .. } => None,
695 },
696 _ => None,
697 }
698}
699
700fn declared_pointers_of(expr: &Expr) -> Option<Vec<ShapeElement>> {
702 if let Expr::IfElse(ie) = expr {
704 return declared_pointers_of(&ie.if_expr).or_else(|| declared_pointers_of(&ie.else_expr));
705 }
706 let Expr::SubQuery(stmt) = expr else {
707 return None;
708 };
709 let shape = match innermost_select(stmt)? {
710 ast::SelectStmt {
711 result: Expr::Shape(sh),
712 ..
713 } => sh,
714 _ => return None,
715 };
716 let declared: Vec<ShapeElement> = shape
717 .elements
718 .iter()
719 .filter(|el| el.compexpr.is_some())
720 .cloned()
721 .collect();
722 (!declared.is_empty()).then_some(declared)
723}
724
725fn innermost_select(stmt: &Stmt) -> Option<&ast::SelectStmt> {
727 match stmt {
728 Stmt::Select(sel) => Some(sel),
729 Stmt::With(w) => innermost_select(&w.stmt),
730 _ => None,
731 }
732}
733
734fn cte_stmt_type(stmt: &IrStmt) -> String {
735 match stmt {
736 IrStmt::Insert(ins) => ins.target.type_name.clone(),
737 IrStmt::Update(upd) => upd.target.type_name.clone(),
738 IrStmt::Delete(del) => del.target.type_name.clone(),
739 IrStmt::Select(sel) => match sel.rows.first() {
740 Some(IrRowSource::Bound { source, .. }) => source.type_name.clone(),
741 Some(IrRowSource::Free(IrFreeExpr::Scalar(expr))) => ir_value_type_name(expr),
745 _ => String::new(),
746 },
747 IrStmt::PathSelect(ps) => match &ps.result {
752 IrPathResult::Scalar(expr, _) => ir_value_type_name(expr),
753 IrPathResult::Object { type_name, .. } => type_name.clone(),
754 },
755 IrStmt::For(f) => cte_stmt_type(&f.body),
756 IrStmt::ScalarUnion(branches) => branches.first().map(cte_stmt_type).unwrap_or_default(),
757 IrStmt::Group(g) => g.source.type_name.clone(),
758 IrStmt::FunctionSelect(fs) => fs.type_name.clone(),
759 IrStmt::VectorSearch(vs) => format!("__vs__{}", vs.source.type_name),
760 IrStmt::FtsSearch(fs) => format!("__fts__{}", fs.source.type_name),
761 }
762}
763
764fn compile_cte_binding(c: &mut Compiler<'_>, expr: &Expr) -> Result<IrStmt, PyQLError> {
767 let stmt = compile_cte_binding_stmt(c, expr)?;
768 Ok(hoist_binding_dml(c, stmt))
769}
770
771fn hoist_binding_dml(c: &mut Compiler<'_>, stmt: IrStmt) -> IrStmt {
778 let IrStmt::Select(mut select) = stmt else {
779 return stmt;
780 };
781 let Some(dml) = select
782 .dml_source
783 .take_if(|dml| matches!(dml.as_ref(), IrStmt::Insert(_) | IrStmt::Update(_) | IrStmt::Delete(_)))
784 else {
785 return IrStmt::Select(select);
786 };
787 let cte_name = c.fresh_nested_cte_name();
788 for row in &mut select.rows {
789 if let IrRowSource::Bound { source, .. } = row {
790 source.table = format!("@cte:{cte_name}");
791 source.poly = None;
794 }
795 }
796 c.hoisted_ctes.push(IrCteDef {
797 name: cte_name,
798 type_name: cte_stmt_type(&dml),
799 stmt: *dml,
800 correlated_to: None,
801 });
802 IrStmt::Select(select)
803}
804
805fn compile_cte_binding_stmt(c: &mut Compiler<'_>, expr: &Expr) -> Result<IrStmt, PyQLError> {
806 if let Expr::SubQuery(s) = expr {
807 let stmt = c.compile_stmt(s)?;
808 if let IrStmt::Group(grp) = &stmt
809 && !matches!(grp.output, IrGroupOutput::Elements)
810 {
811 return Err(c.type_err("a `group` cannot be bound in a `with` except to iterate it with `for`"));
812 }
813 return Ok(stmt);
814 }
815 let expr = match expr {
819 Expr::BinOp(b)
820 if b.op == ast::BinOpKind::Coalesce
821 && matches!(&b.right, Expr::SubQuery(s) if matches!(s.as_ref(), Stmt::Insert(_) | Stmt::Update(_) | Stmt::Delete(_))) =>
822 {
823 &Expr::IfElse(Box::new(ast::IfElse {
824 if_expr: b.left.clone(),
825 condition: Expr::UnaryOp(Box::new(ast::UnaryOp {
826 op: ast::UnaryOpKind::Exists,
827 operand: b.left.clone(),
828 })),
829 else_expr: b.right.clone(),
830 }))
831 }
832 other => other,
833 };
834 let fake_sel = ast::SelectStmt {
837 result: expr.clone(),
838 filter: None,
839 order_by: vec![],
840 offset: None,
841 limit: None,
842 lock: None,
843 };
844 c.compile_stmt(&Stmt::Select(fake_sel))
845}
846
847pub fn functions_needing_globals(schema: &SchemaDescriptor) -> &std::collections::HashSet<String> {
855 schema
856 .functions_needing_globals
857 .get_or_init(|| find_functions_needing_globals(schema))
858}
859
860fn find_functions_needing_globals(schema: &SchemaDescriptor) -> std::collections::HashSet<String> {
861 let mut needs: std::collections::HashSet<String> = std::collections::HashSet::new();
862 loop {
863 let mut changed = false;
864 for fd in &schema.functions {
865 let qualified = format!("{}::{}", fd.module, fd.name);
866 if needs.contains(&qualified) {
867 continue;
868 }
869 if let Ok(out) = compile_fn_body_with(fd, schema, &needs)
870 && out.uses_globals_arg
871 {
872 needs.insert(qualified);
873 changed = true;
874 }
875 }
876 if !changed {
877 return needs;
878 }
879 }
880}
881
882pub fn compile_fn_body(
887 fn_desc: &crate::schema::FunctionDescriptor,
888 schema: &SchemaDescriptor,
889) -> Result<super::IrOutput, crate::error::PyQLError> {
890 compile_fn_body_with(fn_desc, schema, functions_needing_globals(schema))
891}
892
893pub fn compile_fn_body_with(
896 fn_desc: &crate::schema::FunctionDescriptor,
897 schema: &SchemaDescriptor,
898 fns_needing_globals: &std::collections::HashSet<String>,
899) -> Result<super::IrOutput, crate::error::PyQLError> {
900 use crate::parse;
901 use crate::parse::ast::Stmt;
902
903 let body = fn_desc.body.trim().to_string();
904 let starts_a_stmt = ["select", "with", "for", "group", "insert", "update", "delete"]
908 .iter()
909 .any(|keyword| {
910 body.len() > keyword.len()
911 && body[..keyword.len()].eq_ignore_ascii_case(keyword)
912 && !body.as_bytes()[keyword.len()].is_ascii_alphanumeric()
913 && body.as_bytes()[keyword.len()] != b'_'
914 });
915 let body = if starts_a_stmt {
916 body
917 } else {
918 format!("select {}", body)
919 };
920
921 let ast = parse::parse(&body)?;
922 let mut c = Compiler::new(schema);
923 c.in_fn_body = true;
924 c.fns_needing_globals = Some(fns_needing_globals.clone());
925 for p in &fn_desc.params {
926 c.fn_params.insert(p.name.clone(), p.pg_type.clone());
927 }
928
929 let (ctes, ir) = if let Stmt::With(w) = &ast {
930 let mut cte_defs = vec![];
931 for alias in &w.aliases {
932 let ir_stmt = compile_cte_binding(&mut c, &alias.expr)?;
933 let type_name = cte_stmt_type(&ir_stmt);
934 c.cte_types.insert(alias.name.clone(), type_name.clone());
935 c.note_cte_cardinality(&alias.name, &ir_stmt);
936 cte_defs.push(super::IrCteDef {
937 name: alias.name.clone(),
938 stmt: ir_stmt,
939 type_name,
940 correlated_to: None,
941 });
942 }
943 let main = c.compile_stmt(&w.stmt)?;
944 (cte_defs, main)
945 } else {
946 (vec![], c.compile_stmt(&ast)?)
947 };
948
949 let mut ctes = ctes;
950 ctes.extend(std::mem::take(&mut c.hoisted_ctes));
951 Ok(super::IrOutput {
952 subtype_fanouts: c.subtype_fanouts(),
953 stmt: ir,
954 params: c.params,
955 param_tuple_types: c.param_tuple_types,
956 ctes,
957 global_ctes: c.global_ctes,
958 warnings: c.warnings,
959 uses_globals_arg: c.used_globals_arg,
960 })
961}
962
963pub struct RewriteAssignment {
978 pub pointer: String,
979 pub column: String,
980 pub ir: IrExpr,
981 pub sql: String,
982 pub reads_a_multi_link: bool,
987}
988
989pub fn compile_rewrite_assignments(
995 type_name: &str,
996 on_mask: u8,
997 schema: &SchemaDescriptor,
998) -> Result<Vec<RewriteAssignment>, crate::error::PyQLError> {
999 let mut c = Compiler::new(schema);
1000 c.in_fn_body = true;
1001 let owner_td = c.resolve_type(type_name)?;
1002 c.special_anchors
1003 .insert("__new__".to_string(), (owner_td, "NEW".to_string()));
1004 if on_mask & 1 == 0 {
1005 c.special_anchors
1006 .insert("__old__".to_string(), (owner_td, "OLD".to_string()));
1007 }
1008 let pointers = owner_td
1009 .properties
1010 .iter()
1011 .map(|p| (p.name.clone(), p.name.clone(), &p.rewrites))
1012 .chain(
1013 owner_td
1014 .links
1015 .iter()
1016 .filter(|l| !l.is_junction_backed())
1017 .map(|l| (l.name.clone(), format!("{}_id", l.name), &l.rewrites)),
1018 );
1019 let fanouts = c.subtype_fanouts();
1020 let mut out = Vec::new();
1021 for (name, column, rewrites) in pointers {
1022 for rw in rewrites.iter().filter(|rw| rw.on & on_mask != 0) {
1023 if on_mask == 1 && subject_default_insert(&rw.handler, &name).is_some() {
1025 continue;
1026 }
1027 let expr = crate::parse::parse_pointer_expr(&rw.handler)?;
1028 let ir = c.compile_expr(&expr, owner_td, "NEW")?;
1029 if !c.params.is_empty() || !c.hoisted_ctes.is_empty() {
1030 return Err(PyQLError::Type(PyQLTypeError {
1031 message: format!(
1032 "the rewrite of '{type_name}.{name}' has nowhere to bind a parameter or a binding"
1033 ),
1034 position: Position { line: 0, col: 0 },
1035 }));
1036 }
1037 let sql = crate::sql::emit_expr_with_fanouts(&ir, &fanouts);
1038 let junctions = c.junction_tables_of(owner_td)?;
1043 let reads_a_multi_link = junctions.iter().any(|table| sql.contains(table.as_str()));
1044 out.push(RewriteAssignment {
1045 pointer: name.clone(),
1046 column: column.clone(),
1047 ir,
1048 sql,
1049 reads_a_multi_link,
1050 });
1051 }
1052 }
1053 Ok(out)
1054}
1055
1056pub fn compile_trigger_handler(
1057 handler: &str,
1058 type_name: &str,
1059 on_mask: u8,
1060 schema: &SchemaDescriptor,
1061) -> Result<super::IrOutput, crate::error::PyQLError> {
1062 use crate::parse;
1063 use crate::parse::ast::Stmt;
1064
1065 let ast = parse::parse(handler.trim())?;
1066 let mut c = Compiler::new(schema);
1067 c.in_fn_body = true;
1070 let owner_td = c.resolve_type(type_name)?;
1075 if on_mask & 4 == 0 {
1076 c.special_anchors
1077 .insert("__new__".to_string(), (owner_td, "NEW".to_string()));
1078 }
1079 if on_mask & 1 == 0 {
1080 c.special_anchors
1081 .insert("__old__".to_string(), (owner_td, "OLD".to_string()));
1082 }
1083
1084 let (ctes, ir) = if let Stmt::With(w) = &ast {
1085 let mut cte_defs = vec![];
1086 for alias in &w.aliases {
1087 if !matches!(alias.expr, ast::Expr::SubQuery(_)) {
1091 let anchor = if on_mask & 4 == 0 { "NEW" } else { "OLD" };
1092 let value = c.compile_expr(&alias.expr, owner_td, anchor)?;
1093 c.inline_bindings.insert(alias.name.clone(), value);
1094 continue;
1095 }
1096 let ir_stmt = compile_cte_binding(&mut c, &alias.expr)?;
1097 let sql_name = c.claim_cte_sql_name(&alias.name);
1098 let cte_type_name = cte_stmt_type(&ir_stmt);
1099 c.cte_types.insert(alias.name.clone(), cte_type_name.clone());
1100 c.note_cte_cardinality(&alias.name, &ir_stmt);
1101 cte_defs.push(super::IrCteDef {
1102 name: sql_name,
1103 stmt: ir_stmt,
1104 type_name: cte_type_name,
1105 correlated_to: None,
1106 });
1107 }
1108 let main = c.compile_stmt(&w.stmt)?;
1109 (cte_defs, main)
1110 } else {
1111 (vec![], c.compile_stmt(&ast)?)
1112 };
1113
1114 let recursive_kind = recursive_dml_event(&ir, type_name)
1115 .or_else(|| ctes.iter().find_map(|cte| recursive_dml_event(&cte.stmt, type_name)));
1116 if let Some(kind) = recursive_kind
1117 && kind & on_mask != 0
1118 {
1119 return Err(crate::error::PyQLError::Fragment(crate::error::PyQLFragmentError {
1120 message: format!(
1121 "trigger on {type_name} is recursive: its handler {}s its own type, \
1122 which this trigger also fires on",
1123 dml_event_word(kind),
1124 ),
1125 context: type_name.to_string(),
1126 position: crate::error::Position { line: 0, col: 0 },
1127 }));
1128 }
1129
1130 let mut ctes = ctes;
1131 ctes.extend(std::mem::take(&mut c.hoisted_ctes));
1132 Ok(super::IrOutput {
1133 subtype_fanouts: c.subtype_fanouts(),
1134 stmt: ir,
1135 params: c.params,
1136 param_tuple_types: c.param_tuple_types,
1137 ctes,
1138 global_ctes: c.global_ctes,
1139 warnings: c.warnings,
1140 uses_globals_arg: c.used_globals_arg,
1141 })
1142}
1143
1144fn dml_event_word(kind: u8) -> &'static str {
1145 match kind {
1146 1 => "insert",
1147 2 => "update",
1148 4 => "delete",
1149 _ => "mutate",
1150 }
1151}
1152
1153fn recursive_dml_event(stmt: &IrStmt, owner_type: &str) -> Option<u8> {
1169 match stmt {
1170 IrStmt::Insert(ins) if ins.target.type_name == owner_type => Some(1),
1171 IrStmt::Update(upd) if upd.target.type_name == owner_type => Some(2),
1172 IrStmt::Delete(del) if del.target.type_name == owner_type => Some(4),
1173 IrStmt::Select(sel) => sel
1174 .dml_source
1175 .as_deref()
1176 .and_then(|inner| recursive_dml_event(inner, owner_type)),
1177 IrStmt::For(for_stmt) => recursive_dml_event(&for_stmt.body, owner_type),
1178 _ => None,
1179 }
1180}
1181
1182pub fn compile_expr_in_type(
1185 expr: &Expr,
1186 type_name: &str,
1187 schema: &SchemaDescriptor,
1188) -> Result<(IrExpr, Vec<String>), PyQLError> {
1189 let mut c = Compiler::new(schema);
1190 let td = c.resolve_type(type_name)?;
1191 let alias = c.fresh_alias();
1192 let ir = c.compile_expr(expr, td, &alias)?;
1193 Ok((ir, c.params))
1194}
1195
1196pub fn compile_computed_in_type(
1204 cd: &crate::schema::ComputedDescriptor,
1205 type_name: &str,
1206 schema: &SchemaDescriptor,
1207) -> Result<Option<IrExpr>, PyQLError> {
1208 let mut c = Compiler::new(schema);
1209 let td = c.resolve_type(type_name)?;
1210 let module = td.module.clone();
1211 let alias = c.fresh_alias();
1212 match c.compile_declared_computed(cd, td, &alias, &module, None, &[])? {
1213 IrShapePointer::Computed(p) => Ok(Some(p.expr)),
1214 _ => Ok(None),
1215 }
1216}
1217
1218pub fn compile_expr_unaliased(
1222 expr: &Expr,
1223 type_name: &str,
1224 schema: &SchemaDescriptor,
1225) -> Result<(IrExpr, Vec<String>), PyQLError> {
1226 let mut c = Compiler::new(schema);
1227 let td = c.resolve_type(type_name)?;
1228 let ir = c.compile_expr(expr, td, "")?;
1229 Ok((ir, c.params))
1230}
1231
1232pub fn compile_scalar_default(pyql: &str, schema: &SchemaDescriptor) -> Result<String, String> {
1237 compile_scalar_default_typed(pyql, schema).map(|(sql, _ir)| sql)
1238}
1239
1240pub fn compile_constraint_expr(
1249 pyql: &str,
1250 type_name: &str,
1251 schema: &SchemaDescriptor,
1252) -> Result<String, crate::error::PyQLError> {
1253 use crate::parse::ast::Stmt;
1254 let ast = crate::parse::parse(&format!("SELECT {pyql}"))?;
1255 let Stmt::Select(sel) = &ast else {
1256 return Err(PyQLError::Type(PyQLTypeError {
1257 message: "constraint expression must be an expression".into(),
1258 position: Position { line: 0, col: 0 },
1259 }));
1260 };
1261 let (ir, _params) = compile_expr_unaliased(&sel.result, type_name, schema)?;
1262 Ok(crate::sql::emit_expr(&ir))
1263}
1264
1265pub fn compile_scalar_default_typed(pyql: &str, schema: &SchemaDescriptor) -> Result<(String, IrExpr), String> {
1266 use crate::parse::ast::Stmt;
1267 let full = format!("SELECT {}", pyql);
1268 let ast = crate::parse::parse(&full).map_err(|e| e.message)?;
1269 let Stmt::Select(sel) = &ast else {
1270 return Err("default expression must be a select statement".into());
1271 };
1272 let mut c = Compiler::new(schema);
1273 let ir = c.compile_free_expr(&sel.result).map_err(|e| e.to_string())?;
1274 let sql = crate::sql::emit_expr(&ir);
1275 Ok((sql, ir))
1276}
1277
1278pub fn default_blocker(expr: &IrExpr) -> Option<&'static str> {
1284 use IrExpr as E;
1285 match expr {
1286 E::Subquery(_)
1287 | E::ObjectSubquery(_)
1288 | E::ObjectPathSubquery(_)
1289 | E::ObjectPathUnion { .. }
1290 | E::PathSubquery(_)
1291 | E::FnSubquery(_)
1292 | E::ArrayFromSelect(_)
1293 | E::ScalarSubquery(_)
1294 | E::AggOverQuery { .. }
1295 | E::AggOverCte { .. }
1296 | E::ExistsOverCte { .. }
1297 | E::SetOp { .. }
1298 | E::AggOverSet { .. }
1299 | E::CteRef { .. }
1300 | E::CteFieldRef { .. }
1301 | E::GlobalRef { .. } => Some("a sub-select"),
1302 E::ColumnRef { .. } => Some("a reference to another pointer"),
1303 E::Param { .. } | E::GlobalParam { .. } => Some("a query parameter"),
1304 E::ForVar { .. } => Some("a for-loop variable"),
1305 E::FnParam { .. } => Some("a function parameter"),
1306 E::BinOp(b) => default_blocker(&b.left).or_else(|| default_blocker(&b.right)),
1307 E::UnaryOp(u) => default_blocker(&u.operand),
1308 E::TypeCast(c) => default_blocker(&c.expr),
1309 E::IfElse(i) => default_blocker(&i.condition)
1310 .or_else(|| default_blocker(&i.if_))
1311 .or_else(|| default_blocker(&i.else_)),
1312 E::FunctionCall(f) => f.args.iter().find_map(default_blocker),
1313 E::Array(items) | E::Tuple(items) => items.iter().find_map(default_blocker),
1314 E::NamedTuple { fields, .. } => fields.iter().find_map(|(_, e)| default_blocker(e)),
1315 E::Subscript { expr, index, .. } => default_blocker(expr).or_else(|| default_blocker(index)),
1316 E::JsonbField { expr, .. } | E::JsonbIndex { expr, .. } => default_blocker(expr),
1317 E::Slice { expr, lower, upper, .. } => default_blocker(expr)
1318 .or_else(|| lower.as_deref().and_then(default_blocker))
1319 .or_else(|| upper.as_deref().and_then(default_blocker)),
1320 E::Literal(_) | E::Null | E::EnumLiteral { .. } | E::RawSql(_) => None,
1321 }
1322}
1323
1324pub fn column_default_sql(pyql: &str, schema: &SchemaDescriptor) -> Option<String> {
1329 let (sql, ir) = compile_scalar_default_typed(pyql, schema).ok()?;
1330 default_blocker(&ir).is_none().then_some(sql)
1331}
1332
1333pub fn inlined_pointer_defaults(td: &TypeDescriptor, schema: &SchemaDescriptor) -> Vec<(String, String)> {
1342 let properties = td
1343 .properties
1344 .iter()
1345 .filter(|p| !p.is_pk)
1346 .filter_map(|p| p.default_pyql.as_ref().map(|d| (p.name.clone(), d.clone())));
1347 let links = td
1348 .links
1349 .iter()
1350 .filter(|l| !l.is_junction_backed())
1351 .filter_map(|l| l.default_pyql.as_ref().map(|d| (l.name.clone(), d.clone())));
1352 properties
1353 .chain(links)
1354 .filter(|(_, pyql)| column_default_sql(pyql, schema).is_none())
1355 .collect()
1356}
1357
1358fn default_shape_element(pointer: &str, value: Expr) -> ShapeElement {
1360 ShapeElement {
1361 path: ast::Path::relative(pointer),
1362 splat: None,
1363 nested: None,
1364 compexpr: Some(value),
1365 op: ShapeOp::Assign,
1366 filter: None,
1367 order_by: vec![],
1368 offset: None,
1369 limit: None,
1370 marker_offset: None,
1371 }
1372}
1373
1374pub fn compile_inlined_default(
1381 type_name: &str,
1382 pointer: &str,
1383 pyql: &str,
1384 schema: &SchemaDescriptor,
1385) -> Result<(String, IrExpr), PyQLError> {
1386 let mut c = Compiler::new(schema);
1387 let td = c.resolve_type(type_name)?;
1388 let alias = c.fresh_alias();
1389 let element = default_shape_element(pointer, crate::parse::parse_pointer_expr(pyql)?);
1390 let mut assignments = c.compile_assignments(&[element], td, &alias)?;
1391 Ok(assignments.remove(0))
1392}
1393
1394struct Compiler<'a> {
1397 schema: &'a SchemaDescriptor,
1398 params: Vec<String>,
1400 param_tuple_types: Vec<Option<crate::query::ParamTupleType>>,
1403 alias_counter: usize,
1404 cte_types: HashMap<String, String>,
1406 group_bindings: HashMap<String, IrGroup>,
1410 cte_free_items: HashMap<String, IrFreeExpr>,
1415 multi_row_ctes: HashSet<String>,
1420 for_vars: HashMap<String, String>,
1422 for_var_types: HashMap<String, String>,
1426 for_var_ctes: HashMap<String, String>,
1431 for_var_slots: HashMap<String, String>,
1435 pending_insert_guard: Option<Expr>,
1438 pending_update_guard: Option<Expr>,
1442 pending_delete_guard: Option<Expr>,
1444 cte_declared_pointers: HashMap<String, Vec<ShapeElement>>,
1448 active_declared_pointers: Vec<ShapeElement>,
1450 expanding_computeds: Vec<String>,
1454 anchors: Vec<SelectAnchor>,
1460 hoisted_ctes: Vec<IrCteDef>,
1464 hoisted_binding_sources: HashMap<String, Expr>,
1468 cte_sql_names: HashMap<String, String>,
1473 cte_namespace: std::collections::HashSet<String>,
1477 for_vars_read: std::collections::HashSet<String>,
1480 correlated_bindings: HashMap<String, String>,
1483 correlated_reads: std::cell::RefCell<std::collections::HashSet<String>>,
1487 for_scope: Vec<String>,
1489 link_prop_scope: Vec<Option<(String, String)>>,
1494 pending_detached: bool,
1497 modifier_anchor: Option<(String, String)>,
1502 tail_sorts: Vec<ast::SortExpr>,
1507 inline_bindings: std::collections::HashMap<String, IrExpr>,
1511 in_fn_body: bool,
1516 fns_needing_globals: Option<std::collections::HashSet<String>>,
1525 used_globals_arg: bool,
1528 fn_params: HashMap<String, String>,
1530 special_anchors: HashMap<String, (&'a TypeDescriptor, String)>,
1543 global_ctes: Vec<IrGlobalCte>,
1545 pending_nested_ctes: Vec<IrCteDef>,
1553 junction_read_overrides: HashMap<String, JunctionReadOverride>,
1559 nested_cte_counter: usize,
1560 warnings: Vec<String>,
1562 explicit_set_depth: usize,
1566 universal_set_depth: usize,
1571 config: crate::ir::SessionConfig,
1574 implicit_id_in_shapes: bool,
1581 value_position: bool,
1588}
1589
1590struct SelectAnchor {
1592 type_name: String,
1593 qualified: String,
1594 alias: String,
1595 detached: bool,
1596 declared_on: Option<(String, String)>,
1610}
1611
1612impl SelectAnchor {
1613 fn answers_to(&self, root: &str) -> bool {
1615 self.type_name == root
1616 || self.qualified == root
1617 || self
1618 .declared_on
1619 .as_ref()
1620 .is_some_and(|(name, qualified)| name == root || qualified == root)
1621 }
1622}
1623
1624pub const GLOBALS_ARG: &str = "__pylon_json_globals__";
1634
1635fn globals_arg_read(qualified: &str, pg_type: &str) -> String {
1637 let key = qualified.replace('\'', "''");
1638 if let Some(element) = pg_type.strip_suffix("[]") {
1639 format!(
1645 "(case when jsonb_typeof({GLOBALS_ARG} -> '{key}') = 'array' \
1646 then coalesce((select array_agg(value::{element}) \
1647 from jsonb_array_elements_text({GLOBALS_ARG} -> '{key}') as value), '{{}}'::{pg_type}) \
1648 else null end)"
1649 )
1650 } else {
1651 format!("(({GLOBALS_ARG} ->> '{key}')::{pg_type})")
1652 }
1653}
1654
1655impl<'a> Compiler<'a> {
1656 fn new(schema: &'a SchemaDescriptor) -> Self {
1657 Self::with_config(schema, crate::ir::SessionConfig::default())
1658 }
1659
1660 fn with_config(schema: &'a SchemaDescriptor, config: crate::ir::SessionConfig) -> Self {
1661 Compiler {
1662 schema,
1663 params: vec![],
1664 param_tuple_types: vec![],
1665 alias_counter: 0,
1666 cte_types: HashMap::new(),
1667 group_bindings: HashMap::new(),
1668 cte_free_items: HashMap::new(),
1669 multi_row_ctes: HashSet::new(),
1670 for_vars: HashMap::new(),
1671 for_var_types: HashMap::new(),
1672 for_var_ctes: HashMap::new(),
1673 for_var_slots: HashMap::new(),
1674 pending_insert_guard: None,
1675 pending_update_guard: None,
1676 pending_delete_guard: None,
1677 cte_declared_pointers: HashMap::new(),
1678 active_declared_pointers: vec![],
1679 expanding_computeds: vec![],
1680 fn_params: HashMap::new(),
1681 special_anchors: HashMap::new(),
1682 global_ctes: vec![],
1683 pending_nested_ctes: vec![],
1684 junction_read_overrides: HashMap::new(),
1685 nested_cte_counter: 0,
1686 warnings: vec![],
1687 explicit_set_depth: 0,
1688 universal_set_depth: 0,
1689 config,
1690 implicit_id_in_shapes: true,
1691 value_position: false,
1692 anchors: Vec::new(),
1693 link_prop_scope: Vec::new(),
1694 hoisted_ctes: Vec::new(),
1695 hoisted_binding_sources: HashMap::new(),
1696 cte_sql_names: HashMap::new(),
1697 cte_namespace: std::collections::HashSet::new(),
1698 for_vars_read: std::collections::HashSet::new(),
1699 correlated_bindings: HashMap::new(),
1700 correlated_reads: std::cell::RefCell::new(std::collections::HashSet::new()),
1701 for_scope: Vec::new(),
1702 pending_detached: false,
1703 modifier_anchor: None,
1704 tail_sorts: vec![],
1705 inline_bindings: std::collections::HashMap::new(),
1706 in_fn_body: false,
1707 fns_needing_globals: None,
1708 used_globals_arg: false,
1709 }
1710 }
1711
1712 fn is_value_binding(&self, name: &str) -> bool {
1719 self.inline_bindings.contains_key(name) || self.cte_types.get(name).is_some_and(|t| !t.contains("::"))
1720 }
1721
1722 fn bind_inline_if_correlated(
1729 &mut self,
1730 name: &str,
1731 expr: &Expr,
1732 ctx: Option<(&TypeDescriptor, &str)>,
1733 ) -> Result<bool, PyQLError> {
1734 if (self.anchors.is_empty() && ctx.is_none()) || !matches!(expr, Expr::Path(p) if p.partial) {
1735 return Ok(false);
1736 }
1737 let ir = self.compile_expr_ctx(expr, ctx)?;
1738 self.inline_bindings.insert(name.to_string(), ir);
1739 Ok(true)
1740 }
1741
1742 fn cte_sql_name(&self, name: &str) -> String {
1744 let sql_name = self
1745 .cte_sql_names
1746 .get(name)
1747 .cloned()
1748 .unwrap_or_else(|| name.to_string());
1749 if let Some(slot) = self.correlated_bindings.get(&sql_name) {
1750 self.correlated_reads.borrow_mut().insert(slot.clone());
1751 }
1752 sql_name
1753 }
1754
1755 fn cte_source_table(&self, cte: &str) -> String {
1758 if let Some(slot) = self.correlated_bindings.get(cte) {
1759 self.correlated_reads.borrow_mut().insert(slot.clone());
1760 }
1761 format!("@cte:{cte}")
1762 }
1763
1764 fn compile_binding_in_scope(&mut self, expr: &Expr) -> Result<(IrStmt, Option<String>), PyQLError> {
1768 let before = std::mem::take(&mut self.for_vars_read);
1769 let before_correlated = std::mem::take(&mut *self.correlated_reads.borrow_mut());
1770 let ir_stmt = compile_cte_binding(self, expr);
1771 let mut read = std::mem::replace(&mut self.for_vars_read, before);
1772 let correlated_read = std::mem::replace(&mut *self.correlated_reads.borrow_mut(), before_correlated);
1773 read.extend(correlated_read.iter().cloned());
1777 let correlated_to = self
1780 .for_scope
1781 .iter()
1782 .rev()
1783 .find(|slot| read.contains(*slot))
1784 .map(|slot| format!("_for_{slot}"));
1785 self.for_vars_read.extend(read);
1786 self.correlated_reads.borrow_mut().extend(correlated_read);
1787 Ok((ir_stmt?, correlated_to))
1788 }
1789
1790 fn claim_in_cte_namespace(&mut self, base: &str) -> String {
1795 let taken = |namespace: &std::collections::HashSet<String>, candidate: &String| {
1798 namespace.contains(candidate)
1799 || namespace
1800 .iter()
1801 .any(|claimed| claimed != base && candidate.starts_with(&format!("{claimed}__")))
1802 };
1803 let mut candidate = base.to_string();
1804 let mut suffix = 1;
1805 while taken(&self.cte_namespace, &candidate) {
1806 candidate = format!("{base}__{suffix}");
1807 suffix += 1;
1808 }
1809 self.cte_namespace.insert(candidate.clone());
1810 candidate
1811 }
1812
1813 fn claim_generated_cte_name(&mut self, preferred: &str) -> String {
1817 self.claim_in_cte_namespace(preferred)
1818 }
1819
1820 fn claim_cte_sql_name(&mut self, name: &str) -> String {
1823 let base = match name.starts_with('_') {
1827 true => format!("w{name}"),
1828 false => name.to_string(),
1829 };
1830 let claimed = self.claim_in_cte_namespace(&base);
1831 if claimed == name {
1832 self.cte_sql_names.remove(name);
1833 } else {
1834 self.cte_sql_names.insert(name.to_string(), claimed.clone());
1835 }
1836 claimed
1837 }
1838
1839 fn register_cte(&mut self, name: &str, ir_stmt: &IrStmt) -> String {
1840 let type_name = cte_stmt_type(ir_stmt);
1841 self.cte_types.insert(name.to_string(), type_name.clone());
1842 if let IrStmt::Select(sel) = ir_stmt
1843 && let [IrRowSource::Free(item)] = sel.rows.as_slice()
1844 {
1845 self.cte_free_items.insert(name.to_string(), item.clone());
1846 }
1847 self.note_cte_cardinality(name, ir_stmt);
1848 type_name
1849 }
1850
1851 fn pins_an_exclusive_property(&self, sel: &IrSelect) -> bool {
1856 let [IrRowSource::Bound { source, .. }] = sel.rows.as_slice() else {
1857 return false;
1858 };
1859 let Some(filter) = &sel.filter else { return false };
1860 let Ok(td) = self.resolve_type(&source.type_name) else {
1861 return false;
1862 };
1863 Self::pins_exclusive(td, filter)
1864 }
1865
1866 fn pins_exclusive(td: &TypeDescriptor, filter: &IrExpr) -> bool {
1870 let IrExpr::BinOp(binop) = filter else { return false };
1871 match binop.op {
1872 ast::BinOpKind::And => Self::pins_exclusive(td, &binop.left) || Self::pins_exclusive(td, &binop.right),
1873 ast::BinOpKind::Eq => {
1874 let column = match (&binop.left, &binop.right) {
1875 (IrExpr::ColumnRef { column, .. }, _) => column,
1876 (_, IrExpr::ColumnRef { column, .. }) => column,
1877 _ => return false,
1878 };
1879 td.properties
1880 .iter()
1881 .any(|p| &p.name == column && (p.is_exclusive || p.is_pk))
1882 }
1883 _ => false,
1884 }
1885 }
1886
1887 fn note_cte_cardinality(&mut self, name: &str, ir_stmt: &IrStmt) {
1893 let single = match ir_stmt {
1894 IrStmt::Insert(_) => true,
1895 IrStmt::Select(sel) => {
1896 matches!(sel.limit, Some(IrExpr::Literal(IrLiteral::Int(1))))
1897 || matches!(
1898 sel.rows.as_slice(),
1899 [IrRowSource::Free(
1900 IrFreeExpr::Scalar(_)
1901 | IrFreeExpr::FreeObject(_)
1902 | IrFreeExpr::NamedTupleRow(_)
1903 | IrFreeExpr::Tuple(_)
1904 )]
1905 )
1906 || self.pins_an_exclusive_property(sel)
1907 }
1908 _ => false,
1909 };
1910 if !single {
1911 self.multi_row_ctes.insert(name.to_string());
1912 }
1913 }
1914
1915 fn resolve_cte_field_chain(&self, root: &str, steps: &[&str]) -> Option<Result<IrExpr, PyQLError>> {
1926 let (first, rest) = steps.split_first()?;
1927 let fields = match self.cte_free_items.get(root)? {
1928 IrFreeExpr::FreeObject(fields) => fields,
1929 _ => return None,
1930 };
1931 let mut current: &IrExpr = match fields.iter().find(|(n, _)| n == first) {
1932 Some((_, e)) => e,
1933 None => {
1934 return Some(Err(
1935 self.type_err(&format!("free object '{root}' has no field '{first}'"))
1936 ));
1937 }
1938 };
1939 let mut expr = IrExpr::CteFieldRef {
1940 name: root.to_string(),
1941 field: first.to_string(),
1942 pg_type: infer_ir_type(current).map(str::to_string),
1943 };
1944 for step in rest {
1945 if let IrExpr::NamedTuple {
1946 fields: nested,
1947 is_free_object: true,
1948 } = current
1949 {
1950 match nested.iter().find(|(n, _)| n == step) {
1951 Some((_, next)) => current = next,
1952 None => {
1953 return Some(Err(
1954 self.type_err(&format!("{step} is not a member of the nested free object"))
1955 ));
1956 }
1957 }
1958 }
1959 expr = IrExpr::JsonbField {
1960 expr: Box::new(expr),
1961 field: step.to_string(),
1962 };
1963 }
1964 Some(Ok(expr))
1965 }
1966
1967 fn resolve_cte_path(&self, p: &ast::Path) -> Option<Result<IrExpr, PyQLError>> {
1972 if p.partial || p.steps.len() < 2 {
1973 return None;
1974 }
1975 let ast::PathStep::Name(root) = &p.steps[0] else {
1976 return None;
1977 };
1978 let mut steps = Vec::with_capacity(p.steps.len() - 1);
1979 for step in &p.steps[1..] {
1980 match step {
1981 ast::PathStep::Name(n) => steps.push(n.as_str()),
1982 _ => return None,
1983 }
1984 }
1985 self.resolve_cte_field_chain(root, &steps)
1986 }
1987
1988 fn resolve_cte_name<'e>(&self, expr: &'e Expr) -> Option<&'e str> {
1990 if let Expr::Path(p) = expr
1991 && !p.partial
1992 && p.steps.len() == 1
1993 && let ast::PathStep::Name(n) = &p.steps[0]
1994 && self.cte_types.contains_key(n.as_str())
1995 {
1996 return Some(n.as_str());
1997 }
1998 None
1999 }
2000
2001 fn for_var_ref(&mut self, name: &str) -> IrExpr {
2006 let slot = self
2007 .for_var_slots
2008 .get(name)
2009 .cloned()
2010 .unwrap_or_else(|| name.to_string());
2011 self.for_vars_read.insert(slot.clone());
2012 let pg_type = self.for_vars.get(name).cloned();
2013 IrExpr::ForVar { name: slot, pg_type }
2014 }
2015
2016 fn fresh_alias(&mut self) -> String {
2017 let a = format!("t{}", self.alias_counter);
2018 self.alias_counter += 1;
2019 a
2020 }
2021
2022 fn fresh_nested_cte_name(&mut self) -> String {
2026 let n = self.nested_cte_counter;
2027 self.nested_cte_counter += 1;
2028 self.claim_generated_cte_name(&format!("_nested_dml_{n}"))
2029 }
2030
2031 fn param_index(&mut self, name: &str) -> usize {
2033 if let Some(i) = self.params.iter().position(|n| n == name) {
2034 return i;
2035 }
2036 let i = self.params.len();
2037 self.params.push(name.to_string());
2038 self.param_tuple_types.push(None);
2039 i
2040 }
2041
2042 fn tuple_cast(&mut self, expr: IrExpr, pg_type: String, tuple_shape: Option<TupleCastShape>) -> IrExpr {
2052 if let IrExpr::Param { index } = &expr
2053 && let Some(shape) = &tuple_shape
2054 {
2055 self.param_tuple_types[*index] = Some(crate::query::ParamTupleType {
2056 is_array: pg_type.ends_with("[]"),
2057 type_name: shape.type_name.clone(),
2058 members: shape.members.clone(),
2059 });
2060 }
2061 IrExpr::TypeCast(Box::new(IrTypeCast {
2062 expr,
2063 pg_type,
2064 tuple_shape,
2065 }))
2066 }
2067
2068 fn resolve_global_pg_type(&self, scalar_type: &str) -> String {
2076 let builtin = match scalar_type.strip_prefix("std::").unwrap_or(scalar_type) {
2078 "str" => Some("text"),
2079 "int16" => Some("int2"),
2080 "int32" => Some("int4"),
2081 "int64" => Some("int8"),
2082 "float32" => Some("float4"),
2083 "float64" => Some("float8"),
2084 "decimal" => Some("numeric"),
2085 "bool" => Some("boolean"),
2086 "datetime" => Some("timestamptz"),
2087 "cal::local_datetime" => Some("timestamp"),
2088 "cal::local_date" => Some("date"),
2089 "cal::local_time" => Some("time"),
2090 "uuid" => Some("uuid"),
2091 "bytes" => Some("bytea"),
2092 "json" => Some("jsonb"),
2093 "duration" => Some("interval"),
2094 _ => None,
2095 };
2096 if let Some(t) = builtin {
2097 return t.to_string();
2098 }
2099 if let Some(inner) = scalar_type.strip_prefix("array<").and_then(|s| s.strip_suffix('>')) {
2100 return format!("{}[]", self.resolve_global_pg_type(inner));
2101 }
2102 if scalar_type.starts_with("tuple<") {
2103 return "jsonb".to_string();
2104 }
2105 if let Some(ed) = self.resolve_enum(scalar_type) {
2106 return format!("{}.\"{}\"", crate::sql::pg_schema_str(&ed.module), ed.name);
2107 }
2108 if self.resolve_named_tuple(scalar_type).is_some() {
2109 return "jsonb".to_string();
2110 }
2111 self.schema
2113 .scalars
2114 .iter()
2115 .find(|s| s.name == scalar_type || format!("{}::{}", s.module, s.name) == scalar_type)
2116 .map(|s| s.pg_type.clone())
2117 .unwrap_or_else(|| "text".to_string())
2118 }
2119
2120 fn try_compile_global_select(
2124 &mut self,
2125 outer: &ast::SelectStmt,
2126 result: &Expr,
2127 _distinct: bool,
2128 ) -> Result<Option<IrStmt>, PyQLError> {
2129 let (global_name, shape_elements): (&str, &[ast::ShapeElement]) = match result {
2130 Expr::Global(name) => (name.as_str(), &[]),
2131 Expr::Shape(sh) => match sh.expr.as_ref() {
2132 Some(Expr::Global(name)) => (name.as_str(), sh.elements.as_slice()),
2133 _ => return Ok(None),
2134 },
2135 _ => return Ok(None),
2136 };
2137
2138 let global = self
2139 .schema
2140 .globals
2141 .iter()
2142 .find(|g| g.name == global_name || format!("{}::{}", g.module, g.name) == global_name);
2143 let global = match global {
2144 Some(g) => g.clone(),
2145 None => return Ok(None),
2146 };
2147 let computed_expr = match global.computed_expr {
2148 Some(e) => e,
2149 None => return Ok(None), };
2151
2152 let inner_ast = crate::parse::parse(&computed_expr)?;
2153 let inner_sel = match inner_ast {
2154 Stmt::Select(sel) => sel,
2155 _ => return Ok(None),
2156 };
2157
2158 let merged_result = if shape_elements.is_empty() {
2159 inner_sel.result.clone()
2160 } else {
2161 Expr::Shape(Box::new(ast::ShapeExpr {
2162 expr: Some(inner_sel.result.clone()),
2163 elements: shape_elements.to_vec(),
2164 marker_offset: None,
2165 }))
2166 };
2167
2168 let merged_filter = match (&inner_sel.filter, &outer.filter) {
2169 (Some(a), Some(b)) => Some(Expr::BinOp(Box::new(ast::BinOp {
2170 left: a.clone(),
2171 op: ast::BinOpKind::And,
2172 right: b.clone(),
2173 }))),
2174 (Some(a), None) => Some(a.clone()),
2175 (None, b) => b.clone(),
2176 };
2177
2178 let merged = ast::SelectStmt {
2179 result: merged_result,
2180 filter: merged_filter,
2181 order_by: if outer.order_by.is_empty() {
2182 inner_sel.order_by.clone()
2183 } else {
2184 outer.order_by.clone()
2185 },
2186 offset: outer.offset.clone().or(inner_sel.offset.clone()),
2187 limit: outer.limit.clone().or(inner_sel.limit.clone()),
2188 lock: outer.lock.clone().or(inner_sel.lock.clone()),
2189 };
2190
2191 let ir = self.compile_stmt(&Stmt::Select(merged))?;
2192 Ok(Some(ir))
2193 }
2194
2195 fn project_free_object_field(expr: IrExpr, field: &str) -> IrExpr {
2203 if let IrExpr::NamedTuple {
2204 fields,
2205 is_free_object: true,
2206 } = &expr
2207 && let Some((_, value)) = fields.iter().find(|(name, _)| name == field)
2208 {
2209 return value.clone();
2210 }
2211 IrExpr::JsonbField {
2212 expr: Box::new(expr),
2213 field: field.to_string(),
2214 }
2215 }
2216
2217 fn union_of_relative_paths(expr: &Expr) -> Option<Vec<ast::Path>> {
2220 fn walk(expr: &Expr, out: &mut Vec<ast::Path>) -> bool {
2221 match expr {
2222 Expr::Union(a, b) => walk(a, out) && walk(b, out),
2223 Expr::Path(p) if p.partial => {
2224 out.push(p.clone());
2225 true
2226 }
2227 _ => false,
2228 }
2229 }
2230 if !matches!(expr, Expr::Union(_, _)) {
2231 return None;
2232 }
2233 let mut out = vec![];
2234 walk(expr, &mut out).then_some(out)
2235 }
2236
2237 fn coalesce_of_relative_paths(expr: &Expr) -> Option<Vec<ast::Path>> {
2242 fn walk(expr: &Expr, out: &mut Vec<ast::Path>) -> bool {
2243 match expr {
2244 Expr::BinOp(b) if b.op == ast::BinOpKind::Coalesce => walk(&b.left, out) && walk(&b.right, out),
2245 Expr::Path(p) if p.partial => {
2246 out.push(p.clone());
2247 true
2248 }
2249 _ => false,
2250 }
2251 }
2252 if !matches!(expr, Expr::BinOp(b) if b.op == ast::BinOpKind::Coalesce) {
2253 return None;
2254 }
2255 let mut out = vec![];
2256 walk(expr, &mut out).then_some(out)
2257 }
2258
2259 fn shape_over_subquery(&mut self, sh: &ast::ShapeExpr) -> Result<Option<IrExpr>, PyQLError> {
2266 fn push_shape(stmt: &Stmt, elements: &[ShapeElement]) -> Option<Stmt> {
2267 match stmt {
2268 Stmt::Select(sel) => {
2269 if matches!(sel.result, Expr::Shape(_)) {
2270 return None;
2271 }
2272 let mut shaped = sel.clone();
2273 shaped.result = Expr::Shape(Box::new(ast::ShapeExpr {
2274 expr: Some(sel.result.clone()),
2275 elements: elements.to_vec(),
2276 marker_offset: None,
2277 }));
2278 Some(Stmt::Select(shaped))
2279 }
2280 Stmt::With(w) => {
2281 let inner = push_shape(&w.stmt, elements)?;
2282 let mut carried = w.clone();
2283 carried.stmt = Box::new(inner);
2284 Some(Stmt::With(carried))
2285 }
2286 _ => None,
2287 }
2288 }
2289 let Some(Expr::SubQuery(stmt)) = sh.expr.as_ref() else {
2290 return Ok(None);
2291 };
2292 let Some(shaped) = push_shape(stmt.as_ref(), &sh.elements) else {
2293 return Ok(None);
2294 };
2295 let single = matches!(
2296 innermost_select(&shaped).and_then(|s| s.limit.as_ref()),
2297 Some(Expr::Literal(ast::Literal::Int(1)))
2298 );
2299 let IrStmt::Select(select) = self.compile_stmt(&shaped)? else {
2300 return Ok(None);
2301 };
2302 if !matches!(select.rows.as_slice(), [IrRowSource::Bound { .. }]) {
2303 return Ok(None);
2304 }
2305 Ok(Some(if single {
2306 IrExpr::ObjectSubquery(Box::new(select))
2307 } else {
2308 IrExpr::ArrayFromSelect(Box::new(IrArraySource::ObjectSelect(Box::new(select))))
2309 }))
2310 }
2311
2312 fn shape_over_subquery_projection(sh: &ast::ShapeExpr) -> Option<(&Stmt, Vec<String>)> {
2317 let inner = sh.expr.as_ref()?;
2318 if !matches!(inner, Expr::FieldAccess { .. }) {
2319 return None;
2320 }
2321 let (base, fields) = Self::peel_field_access_chain(inner);
2322 match base {
2323 Expr::SubQuery(stmt) => Some((stmt.as_ref(), fields)),
2324 _ => None,
2325 }
2326 }
2327
2328 fn junction_overrides_for(dml: &IrStmt, cte_name: &str) -> HashMap<String, JunctionReadOverride> {
2338 let appends = match dml {
2339 IrStmt::Insert(ins) => &ins.multi_link_appends,
2340 IrStmt::Update(upd) => &upd.multi_link_appends,
2341 _ => return HashMap::new(),
2342 };
2343 appends
2344 .iter()
2345 .enumerate()
2346 .filter(|(_, append)| append.values.link_props.is_empty())
2347 .map(|(i, append)| {
2348 let targets = match &append.values.source {
2351 IrMultiLinkValueSource::CteRef(target_cte) => Some(format!("@cte:{target_cte}")),
2352 _ => None,
2353 };
2354 (
2355 append.junction_table.clone(),
2356 JunctionReadOverride {
2357 junction: format!("@cte:{cte_name}__ml_add_{i}"),
2358 targets,
2359 },
2360 )
2361 })
2362 .collect()
2363 }
2364
2365 fn try_compile_walk_off_subquery(
2376 &mut self,
2377 expr: &Expr,
2378 ctx: Option<(&TypeDescriptor, &str)>,
2379 ) -> Result<Option<IrExpr>, PyQLError> {
2380 if !matches!(expr, Expr::PathStepOn { .. } | Expr::FieldAccess { .. }) {
2381 return Ok(None);
2382 }
2383 let (base, steps) = Self::peel_path_step_chain(expr);
2384 if !steps.iter().any(|step| !matches!(step, ast::PathStep::Name(_))) {
2385 return Ok(None);
2386 }
2387 let Expr::SubQuery(inner_stmt) = base else {
2388 return Ok(None);
2389 };
2390 let inner = compile_cte_binding(self, &Expr::SubQuery(inner_stmt.clone()))?;
2392 let cte_name = self.fresh_nested_cte_name();
2393 let overrides = Self::junction_overrides_for(&inner, &cte_name);
2394 let type_name = self.register_cte(&cte_name, &inner);
2395 self.hoisted_ctes.push(IrCteDef {
2396 name: cte_name.clone(),
2397 stmt: inner,
2398 type_name,
2399 correlated_to: None,
2400 });
2401 let mut path_steps = vec![ast::PathStep::Name(cte_name)];
2402 path_steps.extend(steps);
2403 let path = ast::Path {
2404 steps: path_steps,
2405 partial: false,
2406 };
2407 let previous = std::mem::replace(&mut self.junction_read_overrides, overrides);
2408 let ir = match ctx {
2409 Some((td, alias)) => self.compile_path(&path, td, alias),
2410 None => self.compile_free_path(&path),
2411 };
2412 self.junction_read_overrides = previous;
2413 Ok(Some(ir?))
2414 }
2415
2416 fn peel_path_step_chain(expr: &Expr) -> (&Expr, Vec<ast::PathStep>) {
2421 let mut steps = Vec::new();
2422 let mut current = expr;
2423 loop {
2424 match current {
2425 Expr::FieldAccess { expr: inner, field } => {
2426 steps.push(ast::PathStep::Name(field.clone()));
2427 current = inner;
2428 }
2429 Expr::PathStepOn { expr: inner, step } => {
2430 steps.push((**step).clone());
2431 current = inner;
2432 }
2433 _ => break,
2434 }
2435 }
2436 steps.reverse();
2437 (current, steps)
2438 }
2439
2440 fn peel_field_access_chain(expr: &Expr) -> (&Expr, Vec<String>) {
2441 let mut fields = Vec::new();
2442 let mut current = expr;
2443 while let Expr::FieldAccess { expr: inner, field } = current {
2444 fields.push(field.clone());
2445 current = inner;
2446 }
2447 fields.reverse();
2448 (current, fields)
2449 }
2450
2451 fn resolve_field_owner_select(&self, expr: &Expr) -> Option<ast::SelectStmt> {
2459 let sel = match expr {
2460 Expr::SubQuery(stmt) => match stmt.as_ref() {
2461 Stmt::Select(sel) => sel.clone(),
2462 _ => return None,
2463 },
2464 Expr::Global(name) => {
2465 let global = self
2466 .schema
2467 .globals
2468 .iter()
2469 .find(|g| g.name == *name || format!("{}::{}", g.module, g.name) == *name)?;
2470 let computed_expr = global.computed_expr.as_ref()?;
2471 match crate::parse::parse(computed_expr).ok()? {
2472 Stmt::Select(sel) => sel,
2473 _ => return None,
2474 }
2475 }
2476 _ => return None,
2477 };
2478 let mut sel = sel;
2482 if let Expr::Detached(inner) = &sel.result {
2483 sel.result = inner.as_ref().clone();
2484 }
2485 match &sel.result {
2486 Expr::Path(p) if !p.partial => Some(sel),
2487 _ => None,
2488 }
2489 }
2490
2491 fn compile_walk_off_bound_head(
2505 &mut self,
2506 outer: &ast::SelectStmt,
2507 inner_sel: &ast::SelectStmt,
2508 fields: &[String],
2509 trailing_shape: &[ShapeElement],
2510 ) -> Result<Option<IrStmt>, PyQLError> {
2511 let head = Expr::SubQuery(Box::new(Stmt::Select(inner_sel.clone())));
2512 let inner = compile_cte_binding(self, &head)?;
2513 let cte_name = self.fresh_nested_cte_name();
2514 let overrides = Self::junction_overrides_for(&inner, &cte_name);
2515 let type_name = self.register_cte(&cte_name, &inner);
2516 self.hoisted_ctes.push(IrCteDef {
2517 name: cte_name.clone(),
2518 stmt: inner,
2519 type_name,
2520 correlated_to: None,
2521 });
2522 let mut steps = vec![ast::PathStep::Name(cte_name)];
2523 steps.extend(fields.iter().cloned().map(ast::PathStep::Name));
2524 let walk = Expr::Path(ast::Path { steps, partial: false });
2525 let result = if trailing_shape.is_empty() {
2526 walk
2527 } else {
2528 Expr::Shape(Box::new(ast::ShapeExpr {
2529 expr: Some(walk),
2530 elements: trailing_shape.to_vec(),
2531 marker_offset: None,
2532 }))
2533 };
2534 let rerooted = ast::SelectStmt {
2535 result,
2536 filter: outer.filter.clone(),
2537 order_by: outer.order_by.clone(),
2538 offset: outer.offset.clone(),
2539 limit: outer.limit.clone(),
2540 lock: outer.lock.clone(),
2541 };
2542 let previous = std::mem::replace(&mut self.junction_read_overrides, overrides);
2543 let ir = self.compile_stmt(&Stmt::Select(rerooted));
2544 self.junction_read_overrides = previous;
2545 Ok(Some(ir?))
2546 }
2547
2548 fn try_compile_field_access_select(
2549 &mut self,
2550 outer: &ast::SelectStmt,
2551 result: &Expr,
2552 ) -> Result<Option<IrStmt>, PyQLError> {
2553 let (result, trailing_shape): (&Expr, &[ShapeElement]) = match result {
2557 Expr::Shape(sh) => match sh.expr.as_ref() {
2558 Some(inner @ Expr::FieldAccess { .. }) => (inner, sh.elements.as_slice()),
2559 _ => (result, &[]),
2560 },
2561 other => (other, &[]),
2562 };
2563 let (root, fields) = Self::peel_field_access_chain(result);
2564 if fields.is_empty() {
2565 return Ok(None);
2566 }
2567 let inner_sel = match self.resolve_field_owner_select(root) {
2568 Some(sel) => sel,
2569 None => return Ok(None),
2570 };
2571 let Expr::Path(type_path) = &inner_sel.result else {
2572 return Ok(None);
2573 };
2574 if inner_sel.limit.is_some() || inner_sel.offset.is_some() {
2580 return self.compile_walk_off_bound_head(outer, &inner_sel, &fields, trailing_shape);
2581 }
2582 let mut steps = type_path.steps.clone();
2583 let field_count = fields.len();
2584 steps.extend(fields.into_iter().map(ast::PathStep::Name));
2585 let merged_result = Expr::Path(ast::Path { steps, partial: false });
2586
2587 let merged_filter = match (&inner_sel.filter, &outer.filter) {
2588 (Some(a), Some(b)) => Some(Expr::BinOp(Box::new(ast::BinOp {
2589 left: a.clone(),
2590 op: ast::BinOpKind::And,
2591 right: b.clone(),
2592 }))),
2593 (Some(a), None) => Some(a.clone()),
2594 (None, b) => b.clone(),
2595 };
2596
2597 let merged = ast::SelectStmt {
2598 result: merged_result,
2599 filter: merged_filter,
2600 order_by: if outer.order_by.is_empty() {
2601 inner_sel.order_by.clone()
2602 } else {
2603 outer.order_by.clone()
2604 },
2605 offset: outer.offset.clone().or(inner_sel.offset.clone()),
2606 limit: outer.limit.clone().or(inner_sel.limit.clone()),
2607 lock: outer.lock.clone().or(inner_sel.lock.clone()),
2608 };
2609
2610 if outer.filter.is_none() {
2619 let merged = ast::SelectStmt {
2620 order_by: inner_sel.order_by.clone(),
2621 ..merged
2622 };
2623 let Expr::Path(merged_path) = &merged.result else {
2624 unreachable!("built as a path just above")
2625 };
2626 let ps = self.compile_path_select_with_tail(
2627 &merged,
2628 merged_path,
2629 trailing_shape,
2630 false,
2631 field_count,
2632 &outer.order_by,
2633 )?;
2634 return Ok(Some(IrStmt::PathSelect(ps)));
2635 }
2636
2637 let merged = if trailing_shape.is_empty() {
2638 merged
2639 } else {
2640 ast::SelectStmt {
2641 result: Expr::Shape(Box::new(ast::ShapeExpr {
2642 expr: Some(merged.result.clone()),
2643 elements: trailing_shape.to_vec(),
2644 marker_offset: None,
2645 })),
2646 ..merged
2647 }
2648 };
2649 let ir = self.compile_stmt(&Stmt::Select(merged))?;
2650 Ok(Some(ir))
2651 }
2652
2653 fn try_compile_alias_select(
2654 &mut self,
2655 outer: &ast::SelectStmt,
2656 result: &Expr,
2657 distinct: bool,
2658 ) -> Result<Option<IrStmt>, PyQLError> {
2659 let (path_name, shape_elements): (&str, &[ast::ShapeElement]) = match result {
2661 Expr::Path(p) if !p.partial && p.steps.len() == 1 => {
2662 if let ast::PathStep::Name(n) = &p.steps[0] {
2663 (n.as_str(), &[])
2664 } else {
2665 return Ok(None);
2666 }
2667 }
2668 Expr::Shape(sh) => match sh.expr.as_ref() {
2669 Some(Expr::Path(p)) if !p.partial && p.steps.len() == 1 => {
2670 if let ast::PathStep::Name(n) = &p.steps[0] {
2671 (n.as_str(), sh.elements.as_slice())
2672 } else {
2673 return Ok(None);
2674 }
2675 }
2676 _ => return Ok(None),
2677 },
2678 _ => return Ok(None),
2679 };
2680
2681 let alias = self
2683 .schema
2684 .aliases
2685 .iter()
2686 .find(|a| a.name == path_name || format!("{}::{}", a.module, a.name) == path_name);
2687 let alias = match alias {
2688 Some(a) => a.clone(),
2689 None => return Ok(None),
2690 };
2691
2692 let inner_ast = crate::parse::parse(&alias.expr)?;
2693 let inner_sel = match inner_ast {
2694 Stmt::Select(sel) => sel,
2695 _ => return Err(self.type_err(&format!("alias '{}' expression must be a select statement", alias.name))),
2696 };
2697
2698 let inner_base = match &inner_sel.result {
2710 Expr::Shape(sh) => sh.expr.clone(),
2711 other => Some(other.clone()),
2712 };
2713 let merged_result = if shape_elements.is_empty() {
2714 inner_sel.result.clone()
2715 } else {
2716 Expr::Shape(Box::new(ast::ShapeExpr {
2717 expr: inner_base,
2718 elements: shape_elements.to_vec(),
2719 marker_offset: None,
2720 }))
2721 };
2722
2723 let merged_filter = match (&inner_sel.filter, &outer.filter) {
2724 (Some(a), Some(b)) => Some(Expr::BinOp(Box::new(ast::BinOp {
2725 left: a.clone(),
2726 op: ast::BinOpKind::And,
2727 right: b.clone(),
2728 }))),
2729 (Some(a), None) => Some(a.clone()),
2730 (None, b) => b.clone(),
2731 };
2732
2733 let merged = ast::SelectStmt {
2734 result: merged_result,
2735 filter: merged_filter,
2736 order_by: if outer.order_by.is_empty() {
2737 inner_sel.order_by.clone()
2738 } else {
2739 outer.order_by.clone()
2740 },
2741 offset: outer.offset.clone().or(inner_sel.offset.clone()),
2742 limit: outer.limit.clone().or(inner_sel.limit.clone()),
2743 lock: outer.lock.clone().or(inner_sel.lock.clone()),
2744 };
2745
2746 let _ = distinct; let ir = self.compile_stmt(&Stmt::Select(merged))?;
2748 Ok(Some(ir))
2749 }
2750
2751 fn compile_global(&mut self, raw_name: &str) -> Result<IrExpr, PyQLError> {
2752 let global = self
2753 .schema
2754 .globals
2755 .iter()
2756 .find(|g| g.name == raw_name || format!("{}::{}", g.module, g.name) == raw_name);
2757 let global = global
2758 .ok_or_else(|| {
2759 PyQLError::Resolution(PyQLResolutionError::UnknownField(PyQLUnknownFieldError {
2760 message: format!("unknown global: {:?}", raw_name),
2761 position: Position { line: 0, col: 0 },
2762 }))
2763 })?
2764 .clone();
2765
2766 let qualified = format!("{}::{}", global.module, global.name);
2767 let cte_name = format!("__global__{}", qualified);
2768
2769 if let Some(computed_expr) = global.computed_expr {
2770 if self.global_ctes.iter().any(|g| g.cte_name() == cte_name) {
2772 return Ok(IrExpr::GlobalRef { cte_name });
2773 }
2774 let inner_ast = crate::parse::parse(&computed_expr)?;
2776 let inner_stmt = self.compile_stmt(&inner_ast)?;
2777 self.global_ctes
2778 .push(IrGlobalCte::Computed(Box::new(IrComputedGlobalCte {
2779 cte_name: cte_name.clone(),
2780 qualified_name: qualified,
2781 stmt: inner_stmt,
2782 })));
2783 Ok(IrExpr::GlobalRef { cte_name })
2784 } else if self.in_fn_body {
2785 let pg_type = self.resolve_global_pg_type(&global.scalar_type);
2789 self.used_globals_arg = true;
2790 Ok(IrExpr::TypeCast(Box::new(super::IrTypeCast {
2795 expr: IrExpr::RawSql(globals_arg_read(&qualified, &pg_type)),
2796 pg_type,
2797 tuple_shape: None,
2798 })))
2799 } else {
2800 let pg_type = self.resolve_global_pg_type(&global.scalar_type);
2802 let param_name = format!("__global__{}", qualified);
2803 let index = self.param_index(¶m_name);
2804 if !self.global_ctes.iter().any(|g| g.cte_name() == cte_name) {
2806 self.global_ctes.push(IrGlobalCte::Session(IrSessionGlobalCte {
2807 cte_name,
2808 qualified_name: qualified,
2809 param_index: index,
2810 pg_type: pg_type.clone(),
2811 }));
2812 }
2813 Ok(IrExpr::GlobalParam { index, pg_type })
2814 }
2815 }
2816
2817 fn cte_object_type(&self, name: &str) -> Option<String> {
2822 self.cte_types.get(name).filter(|t| t.contains("::")).cloned()
2823 }
2824
2825 fn drop_empty_results(&self, path_select: &mut IrPathSelect) {
2838 let IrPathResult::Scalar(expr, _) = &path_select.result else {
2839 return;
2840 };
2841 if !self.result_can_be_empty(path_select, expr) {
2842 return;
2843 }
2844 let guard = ir_is_not_null(expr.clone());
2845 path_select.filter = and_conditions(path_select.filter.take(), vec![guard]);
2846 }
2847
2848 fn result_can_be_empty(&self, path_select: &IrPathSelect, expr: &IrExpr) -> bool {
2855 match expr {
2856 IrExpr::JsonbField { .. } | IrExpr::JsonbIndex { .. } | IrExpr::Subquery(_) => true,
2857 IrExpr::TypeCast(cast) => self.result_can_be_empty(path_select, &cast.expr),
2858 IrExpr::ColumnRef { alias, column, .. } => self
2859 .path_step_type(path_select, alias)
2860 .and_then(|td| td.properties.iter().find(|p| p.name == *column))
2861 .is_none_or(|property| property.nullable),
2865 _ => false,
2866 }
2867 }
2868
2869 fn path_step_type(&self, path_select: &IrPathSelect, alias: &str) -> Option<&'a TypeDescriptor> {
2872 let source = std::iter::once(&path_select.root)
2873 .chain(path_select.joins.iter().map(IrPathJoin::target))
2874 .find(|source| source.alias == alias)?;
2875 self.resolve_type(&source.type_name).ok()
2876 }
2877
2878 fn resolve_type(&self, name: &str) -> Result<&'a TypeDescriptor, PyQLError> {
2879 self.schema
2881 .types
2882 .iter()
2883 .find(|t| t.name == name || format!("{}::{}", t.module, t.name) == name)
2884 .ok_or_else(|| {
2885 PyQLError::Resolution(PyQLResolutionError::UnknownType(PyQLUnknownTypeError {
2886 message: format!("unknown type '{name}'"),
2887 position: Position { line: 0, col: 0 },
2888 }))
2889 })
2890 }
2891
2892 fn resolve_enum(&self, name: &str) -> Option<&'a crate::schema::EnumDescriptor> {
2897 self.schema
2898 .enums
2899 .iter()
2900 .find(|e| e.name == name || format!("{}::{}", e.module, e.name) == name)
2901 .or_else(|| crate::stdlib::lookup_enum(name))
2902 }
2903
2904 fn resolve_channel(&self, name: &str) -> Option<&'a crate::schema::ChannelDescriptor> {
2908 self.schema.find_channel(name)
2909 }
2910
2911 fn resolve_scalar(&self, name: &str) -> Option<&'a crate::schema::ScalarDescriptor> {
2917 self.schema
2918 .scalars
2919 .iter()
2920 .find(|s| s.name == name || format!("{}::{}", s.module, s.name) == name)
2921 }
2922
2923 fn resolve_named_tuple(&self, name: &str) -> Option<&'a crate::schema::NamedTupleDescriptor> {
2927 self.schema
2928 .named_tuples
2929 .iter()
2930 .find(|nt| nt.name == name || format!("{}::{}", nt.module, nt.name) == name)
2931 }
2932
2933 fn tuple_member_to_json_member(&self, m: &crate::schema::TupleMemberDescriptor) -> crate::query::JsonMember {
2937 use crate::schema::TupleMemberKind;
2938 let kind = match &m.kind {
2939 TupleMemberKind::Scalar { pg_type } if pg_type == "numeric" => crate::query::JsonMemberKind::Decimal,
2940 TupleMemberKind::Scalar { .. } => crate::query::JsonMemberKind::Scalar,
2941 TupleMemberKind::Enum { module, name } => crate::query::JsonMemberKind::Enum {
2942 enum_type: format!("{}::{}", module, name),
2943 },
2944 TupleMemberKind::NamedTuple { module, name } => {
2945 let qname = format!("{}::{}", module, name);
2946 let nested_members = self
2947 .resolve_named_tuple(&qname)
2948 .map(|nt| {
2949 nt.members
2950 .iter()
2951 .map(|mm| self.tuple_member_to_json_member(mm))
2952 .collect()
2953 })
2954 .unwrap_or_default();
2955 crate::query::JsonMemberKind::Tuple {
2956 type_name: Some(qname),
2957 members: nested_members,
2958 }
2959 }
2960 TupleMemberKind::Tuple { members } => crate::query::JsonMemberKind::Tuple {
2961 type_name: None,
2962 members: members.iter().map(|mm| self.tuple_member_to_json_member(mm)).collect(),
2963 },
2964 };
2965 crate::query::JsonMember {
2966 key: m.name.clone(),
2967 kind,
2968 }
2969 }
2970
2971 fn ast_tuple_element_to_json_member(&self, elem: &ast::TupleTypeElement) -> crate::query::JsonMember {
2974 crate::query::JsonMember {
2975 key: elem.name.clone(),
2976 kind: self.ast_type_expr_to_json_member_kind(&elem.ty),
2977 }
2978 }
2979
2980 fn ast_type_expr_to_json_member_kind(&self, ty: &ast::TypeExpr) -> crate::query::JsonMemberKind {
2981 if let ast::TypeExpr::Tuple { elements } = ty {
2982 return crate::query::JsonMemberKind::Tuple {
2983 type_name: None,
2984 members: elements
2985 .iter()
2986 .map(|e| self.ast_tuple_element_to_json_member(e))
2987 .collect(),
2988 };
2989 }
2990 let Some((module, name)) = ty.as_named() else {
2991 return crate::query::JsonMemberKind::Scalar;
2992 };
2993 let qname = match module {
2994 Some(m) => format!("{}::{}", m, name),
2995 None => name.to_string(),
2996 };
2997 if let Some(ed) = self.resolve_enum(&qname) {
2998 return crate::query::JsonMemberKind::Enum {
2999 enum_type: format!("{}::{}", ed.module, ed.name),
3000 };
3001 }
3002 if let Some(nt) = self.resolve_named_tuple(&qname) {
3003 return crate::query::JsonMemberKind::Tuple {
3004 type_name: Some(format!("{}::{}", nt.module, nt.name)),
3005 members: nt.members.iter().map(|m| self.tuple_member_to_json_member(m)).collect(),
3006 };
3007 }
3008 if matches!(type_expr_to_pg(ty).as_deref(), Ok("numeric")) {
3009 return crate::query::JsonMemberKind::Decimal;
3010 }
3011 crate::query::JsonMemberKind::Scalar
3012 }
3013
3014 fn resolve_tuple_cast_shape(&self, ty: &ast::TypeExpr) -> Option<TupleCastShape> {
3024 match ty {
3025 ast::TypeExpr::Array { element } => self.resolve_tuple_cast_shape(element),
3026 ast::TypeExpr::Tuple { elements } => Some(TupleCastShape {
3027 type_name: None,
3028 members: elements
3029 .iter()
3030 .map(|e| self.ast_tuple_element_to_json_member(e))
3031 .collect(),
3032 }),
3033 _ => {
3034 let (module, name) = ty.as_named()?;
3035 let qname = match module {
3036 Some(m) => format!("{}::{}", m, name),
3037 None => name.to_string(),
3038 };
3039 let nt = self.resolve_named_tuple(&qname)?;
3040 Some(TupleCastShape {
3041 type_name: Some(format!("{}::{}", nt.module, nt.name)),
3042 members: nt.members.iter().map(|m| self.tuple_member_to_json_member(m)).collect(),
3043 })
3044 }
3045 }
3046 }
3047
3048 fn type_expr_to_display_str(&self, ty: &ast::TypeExpr) -> String {
3052 match ty {
3053 ast::TypeExpr::Tuple { elements } => {
3054 let inner = elements
3055 .iter()
3056 .map(|e| match &e.name {
3057 Some(n) => format!("{}: {}", n, self.type_expr_to_display_str(&e.ty)),
3058 None => self.type_expr_to_display_str(&e.ty),
3059 })
3060 .collect::<Vec<_>>()
3061 .join(", ");
3062 format!("tuple<{}>", inner)
3063 }
3064 ast::TypeExpr::Array { element } => format!("array<{}>", self.type_expr_to_display_str(element)),
3065 ast::TypeExpr::Named { module, name } => match module {
3066 Some(m) => format!("{}::{}", m, name),
3067 None => type_expr_to_pg(ty)
3068 .map(|pg| pg_type_to_pyql(&pg).to_string())
3069 .unwrap_or_else(|_| name.clone()),
3070 },
3071 }
3072 }
3073
3074 fn resolve_property_tuple_shape(&self, prop: &PropertyDescriptor) -> Option<TupleCastShape> {
3083 let marker = prop.pg_type.strip_suffix("[]").unwrap_or(&prop.pg_type);
3084 if let Some(qname) = marker.strip_prefix("__nt__:") {
3085 let nt = self.resolve_named_tuple(qname)?;
3086 return Some(TupleCastShape {
3087 type_name: Some(qname.to_string()),
3088 members: nt.members.iter().map(|m| self.tuple_member_to_json_member(m)).collect(),
3089 });
3090 }
3091 prop.tuple_members.as_ref().map(|members| TupleCastShape {
3092 type_name: None,
3093 members: members.iter().map(|m| self.tuple_member_to_json_member(m)).collect(),
3094 })
3095 }
3096
3097 fn scalar_cast_free_select(
3104 &mut self,
3105 tc: &ast::TypeCast,
3106 pg_type: String,
3107 distinct: bool,
3108 ) -> Result<IrStmt, PyQLError> {
3109 let cast_expr = match &tc.ty {
3110 ast::TypeExpr::Tuple { elements } => {
3111 let tuple_shape = self.resolve_tuple_cast_shape(&tc.ty);
3112 match self.try_compile_tuple_literal_cast_ctx(elements, &tc.expr, None)? {
3113 Some(ir) => IrExpr::TypeCast(Box::new(IrTypeCast {
3118 expr: ir,
3119 pg_type,
3120 tuple_shape,
3121 })),
3122 None => {
3123 let inner = self.compile_expr_ctx(&tc.expr, None)?;
3124 self.tuple_cast(inner, pg_type, tuple_shape)
3125 }
3126 }
3127 }
3128 _ => {
3129 let inner = self.compile_expr_ctx(&tc.expr, None)?;
3130 let tuple_shape = self.resolve_tuple_cast_shape(&tc.ty);
3131 self.tuple_cast(inner, pg_type, tuple_shape)
3132 }
3133 };
3134 Ok(IrStmt::Select(IrSelect {
3135 rows: vec![IrRowSource::Free(IrFreeExpr::Scalar(cast_expr))],
3136 filter: None,
3137 order_by: vec![],
3138 offset: None,
3139 limit: None,
3140 distinct,
3141 dml_source: None,
3142 polymorphic: false,
3143 poly_implementors: vec![],
3144 poly_columns: vec![],
3145 lock: None,
3146 }))
3147 }
3148
3149 fn resolve_cast_pg_type(&self, ty: &ast::TypeExpr) -> Result<String, PyQLError> {
3158 if matches!(ty, ast::TypeExpr::Tuple { .. }) {
3159 return Ok("jsonb".to_string());
3160 }
3161 if let ast::TypeExpr::Array { element } = ty {
3162 let element_pg = self.resolve_cast_pg_type(element)?;
3163 return Ok(format!("{}[]", element_pg));
3164 }
3165 let (module, name) = ty.as_named().expect("checked above: not Tuple/Array");
3166 let qname = match module {
3167 Some(m) => format!("{}::{}", m, name),
3168 None => name.to_string(),
3169 };
3170 if let Some(ed) = self.resolve_enum(&qname) {
3171 return Ok(format!("{}.\"{}\"", crate::sql::pg_schema_str(&ed.module), ed.name));
3172 }
3173 if self.resolve_named_tuple(&qname).is_some() {
3174 return Ok("jsonb".to_string());
3175 }
3176 if let Some(sd) = self.resolve_scalar(&qname) {
3182 return Ok(format!("{}.\"{}\"", crate::sql::pg_schema_str(&sd.module), sd.name));
3183 }
3184 type_expr_to_pg(ty)
3185 }
3186
3187 fn compile_tuple_element_cast_ctx(
3193 &mut self,
3194 target_ty: &ast::TypeExpr,
3195 value: &Expr,
3196 ctx: Option<(&TypeDescriptor, &str)>,
3197 ) -> Result<IrExpr, PyQLError> {
3198 if let ast::TypeExpr::Tuple { elements } = target_ty
3199 && let Some(ir) = self.try_compile_tuple_literal_cast_ctx(elements, value, ctx)?
3200 {
3201 return Ok(ir);
3202 }
3203 let inner = self.compile_expr_ctx(value, ctx)?;
3204 let pg_type = self.resolve_cast_pg_type(target_ty)?;
3205 Ok(IrExpr::TypeCast(Box::new(IrTypeCast {
3206 expr: inner,
3207 pg_type,
3208 tuple_shape: None,
3209 })))
3210 }
3211
3212 fn try_compile_tuple_literal_cast_ctx(
3221 &mut self,
3222 target_elements: &[ast::TupleTypeElement],
3223 source: &Expr,
3224 ctx: Option<(&TypeDescriptor, &str)>,
3225 ) -> Result<Option<IrExpr>, PyQLError> {
3226 let source_values: Vec<&Expr> = match source {
3227 Expr::Tuple(vals) if vals.len() == target_elements.len() => vals.iter().collect(),
3228 Expr::NamedTuple(fields) if fields.len() == target_elements.len() => {
3229 fields.iter().map(|(_, v)| v).collect()
3230 }
3231 _ => return Ok(None),
3232 };
3233 let named = target_elements.iter().all(|e| e.name.is_some());
3234 let mut casted = Vec::with_capacity(target_elements.len());
3235 for (elem, value) in target_elements.iter().zip(source_values) {
3236 casted.push(self.compile_tuple_element_cast_ctx(&elem.ty, value, ctx)?);
3237 }
3238 if named {
3239 let fields = target_elements
3240 .iter()
3241 .zip(casted)
3242 .map(|(e, v)| (e.name.clone().unwrap(), v))
3243 .collect();
3244 Ok(Some(IrExpr::NamedTuple {
3245 fields,
3246 is_free_object: false,
3247 }))
3248 } else {
3249 Ok(Some(IrExpr::Tuple(casted)))
3250 }
3251 }
3252
3253 fn try_compile_array_literal_cast_ctx(
3265 &mut self,
3266 element_ty: &ast::TypeExpr,
3267 source: &Expr,
3268 ctx: Option<(&TypeDescriptor, &str)>,
3269 ) -> Result<Option<IrExpr>, PyQLError> {
3270 let Expr::Array(elems) = source else { return Ok(None) };
3271 let casted = elems
3272 .iter()
3273 .map(|e| self.compile_tuple_element_cast_ctx(element_ty, e, ctx))
3274 .collect::<Result<Vec<_>, _>>()?;
3275 Ok(Some(IrExpr::Array(casted)))
3276 }
3277
3278 fn compile_enum_access(&self, type_ref: &str, variant: &str) -> Result<IrExpr, PyQLError> {
3279 let ed = self
3280 .resolve_enum(type_ref)
3281 .ok_or_else(|| self.type_err(&format!("unknown type '{}'", type_ref)))?;
3282 if !ed.members.iter().any(|m| m == variant) {
3283 return Err(self.type_err(&format!(
3284 "enum '{}::{}' has no member '{}'",
3285 ed.module, ed.name, variant
3286 )));
3287 }
3288 let pg_type = if crate::stdlib::lookup_enum(&format!("{}::{}", ed.module, ed.name)).is_some() {
3291 "text".to_string()
3292 } else {
3293 format!("{}.\"{}\"", crate::sql::pg_schema_str(&ed.module), ed.name)
3294 };
3295 Ok(IrExpr::EnumLiteral {
3296 pg_type,
3297 variant: variant.to_string(),
3298 })
3299 }
3300
3301 fn row_source_table(&self, name: &str, td: &TypeDescriptor) -> String {
3304 if let Some(cte) = self.for_var_ctes.get(name) {
3305 return self.cte_source_table(cte);
3306 }
3307 if self.cte_object_type(name).is_some() {
3308 return self.cte_source_table(&self.cte_sql_name(name));
3309 }
3310 td.table.clone()
3311 }
3312
3313 fn resolve_path_root(&self, root_name: &str) -> Result<&'a TypeDescriptor, PyQLError> {
3321 if let Some(qualified) = self.for_var_types.get(root_name) {
3322 return self.resolve_type(qualified);
3323 }
3324 match self.cte_types.get(root_name).filter(|t| t.contains("::")).cloned() {
3325 Some(bound) => self.resolve_type(&bound),
3326 None => self.resolve_type(root_name),
3327 }
3328 }
3329
3330 fn find_poly_implementors(&self, iface_qname: &str) -> Vec<IrPolyImplementor> {
3331 self.schema
3332 .types
3333 .iter()
3334 .filter(|t| !t.abstract_ && Self::is_or_implements(t, iface_qname))
3335 .map(|t| IrPolyImplementor {
3336 type_name: format!("{}::{}", t.module, t.name),
3337 table: t.table.clone(),
3338 module: t.module.clone(),
3339 })
3340 .collect()
3341 }
3342
3343 fn resolve_property<'t>(td: &'t TypeDescriptor, name: &str) -> Option<&'t PropertyDescriptor> {
3344 td.properties.iter().find(|p| p.name == name)
3345 }
3346
3347 fn resolve_link<'t>(td: &'t TypeDescriptor, name: &str) -> Option<&'t LinkDescriptor> {
3348 td.links.iter().find(|l| l.name == name)
3349 }
3350
3351 fn resolve_multilink<'t>(td: &'t TypeDescriptor, name: &str) -> Option<&'t MultiLinkDescriptor> {
3352 td.multilinks.iter().find(|m| m.name == name)
3353 }
3354
3355 fn compile_link_prop_ref(&mut self, prop_name: &str) -> Result<IrExpr, PyQLError> {
3359 let Some(scope) = self.link_prop_scope.last().cloned() else {
3360 return Err(self.type_err(&format!(
3361 "'@{prop_name}' is a link property, so it is only valid in the modifiers or shape of the \
3362 link it belongs to, e.g. 'locators: {{ … }} filter @{prop_name}'"
3363 )));
3364 };
3365 let Some((through_qname, junction_alias)) = scope else {
3366 return Err(self.type_err(&format!(
3367 "this link has no link properties, so '@{prop_name}' cannot be read — \
3368 declare the link with a `Through[...]` type to give it some"
3369 )));
3370 };
3371 let through_td = self.resolve_type(&through_qname)?;
3372 let Some(prop) = Self::resolve_property(through_td, prop_name) else {
3373 return Err(self.field_err(prop_name, &through_qname));
3374 };
3375 Ok(IrExpr::ColumnRef {
3376 alias: junction_alias,
3377 column: prop.name.clone(),
3378 pg_type: prop.pg_type.clone(),
3379 })
3380 }
3381
3382 fn link_target_reaches(&self, target: &str, current_qname: &str) -> bool {
3388 if target == current_qname {
3389 return true;
3390 }
3391 self.resolve_type(current_qname)
3392 .is_ok_and(|td| td.interfaces.iter().any(|i| i == target))
3393 }
3394
3395 fn resolve_computed(&self, td: &TypeDescriptor, name: &str) -> Option<crate::schema::ComputedDescriptor> {
3401 if let Some(cd) = td.computed.iter().find(|c| c.name == name) {
3402 return Some(cd.clone());
3403 }
3404 td.interfaces
3405 .iter()
3406 .filter_map(|iface| self.resolve_type(iface).ok())
3407 .find_map(|itd| itd.computed.iter().find(|c| c.name == name))
3408 .cloned()
3409 }
3410
3411 fn compile_stmt(&mut self, stmt: &Stmt) -> Result<IrStmt, PyQLError> {
3414 match stmt {
3415 Stmt::Select(s) => {
3416 let (distinct, result) = match &s.result {
3417 Expr::UnaryOp(u) if u.op == ast::UnaryOpKind::Distinct => (true, &u.operand),
3418 Expr::Detached(inner) => {
3427 self.pending_detached = true;
3428 (false, inner.as_ref())
3429 }
3430 other => (false, other),
3431 };
3432
3433 if let Expr::Shape(sh) = result
3437 && let Some(Expr::FunctionCall(assert)) = &sh.expr
3438 && assert.module.as_deref().is_none_or(|m| m == "std")
3439 && matches!(assert.name.as_str(), "assert_exists" | "assert_distinct")
3440 && let [Expr::FunctionCall(inner)] = assert.args.as_slice()
3441 && let Some(mut fs) = self.try_compile_fn_object_select(inner, &sh.elements, s, distinct)?
3442 {
3443 let message = self.assert_message(assert, None)?;
3444 let unchecked = ast::SelectStmt {
3445 result: Expr::FunctionCall(inner.clone()),
3446 filter: None,
3447 order_by: vec![],
3448 offset: None,
3449 limit: None,
3450 lock: None,
3451 };
3452 let every_row = self
3453 .try_compile_fn_object_select(inner, &[], &unchecked, false)?
3454 .ok_or_else(|| self.type_err("assert subject is not an object-returning function"))?;
3455 let checked = IrExpr::FunctionCall(IrFunctionCall {
3456 return_pg_type: None,
3457 schema: Some("_pylon".to_string()),
3458 name: assert.name.clone(),
3459 args: std::iter::once(IrExpr::ArrayFromSelect(Box::new(IrArraySource::StmtColumn {
3460 stmt: Box::new(IrStmt::FunctionSelect(every_row)),
3461 column: "id".to_string(),
3462 })))
3463 .chain(message)
3464 .collect(),
3465 sql_template: None,
3466 });
3467 let check = IrExpr::BinOp(Box::new(IrBinOp {
3468 left: IrExpr::FunctionCall(IrFunctionCall {
3469 return_pg_type: None,
3470 schema: None,
3471 name: "cardinality".to_string(),
3472 args: vec![checked],
3473 sql_template: None,
3474 }),
3475 op: ast::BinOpKind::Ge,
3476 right: IrExpr::Literal(IrLiteral::Int(0)),
3477 }));
3478 fs.filter = and_conditions(fs.filter, vec![check]);
3479 return Ok(IrStmt::FunctionSelect(fs));
3480 }
3481 if let Some(flattened) = flatten_shape_subject(result) {
3482 let mut flat = s.clone();
3483 flat.result = match distinct {
3484 true => Expr::UnaryOp(Box::new(ast::UnaryOp {
3485 op: ast::UnaryOpKind::Distinct,
3486 operand: flattened,
3487 })),
3488 false => flattened,
3489 };
3490 return self.compile_stmt(&Stmt::Select(flat));
3491 }
3492 if let Expr::Shape(sh) = result
3493 && let Some(Expr::SubQuery(inner)) = &sh.expr
3494 && let Stmt::Group(g) = inner.as_ref()
3495 {
3496 return self.compile_group_projection(s, g, &sh.elements).map(IrStmt::Group);
3497 }
3498
3499 if let Some(ir) = self.try_compile_alias_select(s, result, distinct)? {
3501 return Ok(ir);
3502 }
3503
3504 if let Some(ir) = self.try_compile_global_select(s, result, distinct)? {
3506 return Ok(ir);
3507 }
3508
3509 if let Some(ir) = self.try_compile_field_access_select(s, result)? {
3512 return Ok(ir);
3513 }
3514
3515 if let Expr::Shape(sh) = result
3517 && let Some(Expr::FunctionCall(fc)) = sh.expr.as_ref()
3518 && let Some(ir) = self.try_compile_fn_object_select(fc, &sh.elements, s, distinct)?
3519 {
3520 return Ok(IrStmt::FunctionSelect(ir));
3521 }
3522
3523 if let Expr::FunctionCall(fc) = result
3525 && let Some(ir) = self.try_compile_fn_object_select(fc, &[], s, distinct)?
3526 {
3527 return Ok(IrStmt::FunctionSelect(ir));
3528 }
3529
3530 if let Expr::Shape(sh) = result
3532 && let Some(Expr::FunctionCall(fc)) = sh.expr.as_ref()
3533 {
3534 if let Some(ir) = self.try_compile_vector_search(fc, &sh.elements, s)? {
3535 return Ok(IrStmt::VectorSearch(ir));
3536 }
3537 if let Some(ir) = self.try_compile_fts_search(fc, &sh.elements, s)? {
3538 return Ok(IrStmt::FtsSearch(ir));
3539 }
3540 }
3541 if let Expr::FunctionCall(fc) = result {
3543 if let Some(ir) = self.try_compile_vector_search(fc, &[], s)? {
3544 return Ok(IrStmt::VectorSearch(ir));
3545 }
3546 if let Some(ir) = self.try_compile_fts_search(fc, &[], s)? {
3547 return Ok(IrStmt::FtsSearch(ir));
3548 }
3549 }
3550
3551 if let Expr::Shape(sh) = result
3556 && let Some(Expr::TypeCast(tc)) = sh.expr.as_ref()
3557 && let Some((module, name)) = tc.ty.as_named()
3558 && module
3559 .map(|m| !["std", "cal", "math", "sys", "pgvector", "crypto", "postgis"].contains(&m))
3560 .unwrap_or(false)
3561 {
3562 let id_filter = Expr::BinOp(Box::new(ast::BinOp {
3563 left: Expr::Path(ast::Path::relative("id")),
3564 op: ast::BinOpKind::Eq,
3565 right: tc.expr.clone(),
3566 }));
3567 let merged_filter = match &s.filter {
3568 None => Some(id_filter),
3569 Some(existing) => Some(Expr::BinOp(Box::new(ast::BinOp {
3570 left: id_filter,
3571 op: ast::BinOpKind::And,
3572 right: existing.clone(),
3573 }))),
3574 };
3575 let qname = match module {
3578 Some(m) => format!("{}::{}", m, name),
3579 None => name.to_string(),
3580 };
3581 let synthetic = ast::SelectStmt {
3582 result: Expr::Shape(Box::new(ast::ShapeExpr {
3583 expr: Some(Expr::Path(ast::Path::absolute(&qname))),
3584 elements: sh.elements.clone(),
3585 marker_offset: None,
3586 })),
3587 filter: merged_filter,
3588 order_by: s.order_by.clone(),
3589 offset: s.offset.clone(),
3590 limit: s.limit.clone(),
3591 lock: s.lock.clone(),
3592 };
3593 return self
3594 .compile_select(&synthetic, &synthetic.result, distinct)
3595 .map(IrStmt::Select);
3596 }
3597 const STDLIB_MODULES: &[&str] = &["std", "cal", "math", "sys", "pgvector", "crypto", "postgis"];
3602 if let Expr::TypeCast(tc) = result {
3603 if matches!(&tc.ty, ast::TypeExpr::Tuple { .. }) {
3607 return self.scalar_cast_free_select(tc, "jsonb".to_string(), distinct);
3608 }
3609 if let Some((module, name)) = tc.ty.as_named() {
3610 let qname = match module {
3611 Some(m) => format!("{}::{}", m, name),
3612 None => name.to_string(),
3613 };
3614 if let Some(ed) = self.resolve_enum(&qname) {
3615 let pg_type = format!("{}.\"{}\"", crate::sql::pg_schema_str(&ed.module), ed.name);
3616 return self.scalar_cast_free_select(tc, pg_type, distinct);
3617 }
3618 if self.resolve_named_tuple(&qname).is_some() {
3619 return self.scalar_cast_free_select(tc, "jsonb".to_string(), distinct);
3620 }
3621 if let Some(sd) = self.resolve_scalar(&qname) {
3622 let pg_type = format!("{}.\"{}\"", crate::sql::pg_schema_str(&sd.module), sd.name);
3623 return self.scalar_cast_free_select(tc, pg_type, distinct);
3624 }
3625 if module.map(|m| !STDLIB_MODULES.contains(&m)).unwrap_or(false) {
3626 return self.compile_schema_cast_select(s, tc).map(IrStmt::Select);
3627 }
3628 }
3629 }
3630 if let Expr::Path(p) = result {
3632 if !p.partial
3633 && p.steps.len() == 2
3634 && let [ast::PathStep::Name(type_ref), ast::PathStep::Name(variant)] = p.steps.as_slice()
3635 && self.resolve_enum(type_ref).is_some()
3636 {
3637 let expr = self.compile_enum_access(type_ref, variant)?;
3638 return Ok(IrStmt::Select(IrSelect {
3639 rows: vec![IrRowSource::Free(IrFreeExpr::Scalar(expr))],
3640 filter: None,
3641 order_by: vec![],
3642 offset: None,
3643 limit: None,
3644 distinct,
3645 dml_source: None,
3646 polymorphic: false,
3647 poly_implementors: vec![],
3648 poly_columns: vec![],
3649 lock: None,
3650 }));
3651 }
3652 if let Some(resolved) = self.resolve_cte_path(p) {
3658 let expr = resolved?;
3659 return Ok(IrStmt::Select(IrSelect {
3660 rows: vec![IrRowSource::Free(IrFreeExpr::Scalar(expr))],
3661 filter: None,
3662 order_by: vec![],
3663 offset: None,
3664 limit: None,
3665 distinct,
3666 dml_source: None,
3667 polymorphic: false,
3668 poly_implementors: vec![],
3669 poly_columns: vec![],
3670 lock: None,
3671 }));
3672 }
3673 if !p.partial && p.steps.len() > 1 {
3674 return self.compile_path_select(s, p, &[], distinct).map(IrStmt::PathSelect);
3675 }
3676 }
3677 if let Expr::Shape(sh) = result
3678 && let Some(Expr::Path(p)) = sh.expr.as_ref()
3679 && !p.partial
3680 && p.steps.len() > 1
3681 {
3682 return self
3683 .compile_path_select(s, p, &sh.elements, distinct)
3684 .map(IrStmt::PathSelect);
3685 }
3686 if let Expr::FunctionCall(f) = result
3688 && (f.module.is_none() || f.module.as_deref() == Some("std"))
3689 && matches!(f.name.as_str(), "assert_exists" | "assert_distinct")
3690 && !f.args.is_empty()
3691 && let Expr::SubQuery(inner_stmt) = &f.args[0]
3692 {
3693 let offset = s.offset.as_ref().map(|e| self.compile_free_expr(e)).transpose()?;
3694 let limit = s.limit.as_ref().map(|e| self.compile_free_expr(e)).transpose()?;
3695 let message = self.assert_message(f, None)?;
3696 if let Ok(type_name) = self.dml_subject_type(inner_stmt)
3700 && let Ok(td) = self.resolve_type(&type_name)
3701 {
3702 let td_module = td.module.clone();
3703 let td_name = td.name.clone();
3704 let td_table = td.table.clone();
3705 let pk = td
3706 .properties
3707 .iter()
3708 .find(|p| p.is_pk)
3709 .map(|p| (p.name.clone(), p.pg_type.clone()))
3710 .unwrap_or_else(|| ("id".to_string(), "uuid".to_string()));
3711 let inner_ir = self.compile_stmt(inner_stmt)?;
3712 let alias = self.fresh_alias();
3713 let vetted = IrExpr::FunctionCall(IrFunctionCall {
3714 return_pg_type: None,
3715 schema: Some("_pylon".to_string()),
3716 name: f.name.clone(),
3717 args: std::iter::once(IrExpr::ArrayFromSelect(Box::new(IrArraySource::StmtColumn {
3718 stmt: Box::new(inner_ir),
3719 column: pk.0.clone(),
3720 })))
3721 .chain(message)
3722 .collect(),
3723 sql_template: None,
3724 });
3725 let filter = IrExpr::BinOp(Box::new(IrBinOp {
3726 left: IrExpr::ColumnRef {
3727 alias: alias.clone(),
3728 column: pk.0.clone(),
3729 pg_type: pk.1.clone(),
3730 },
3731 op: ast::BinOpKind::In,
3732 right: vetted,
3733 }));
3734 let shape = vec![IrShapePointer::Scalar(IrScalarPointer {
3735 implicit_id: false,
3736 marker_offset: None,
3737 alias: "id".to_string(),
3738 column: pk.0,
3739 pg_type: pk.1,
3740 tuple_shape: None,
3741 })];
3742 let source = IrSource {
3743 poly: None,
3744 type_name: format!("{td_module}::{td_name}"),
3745 table: td_table,
3746 alias,
3747 };
3748 let mut select = IrSelect::schema_bound(source, shape, Some(filter));
3749 select.offset = offset;
3750 select.limit = limit;
3751 select.distinct = distinct;
3752 return Ok(IrStmt::Select(select));
3753 }
3754 let inner = self.compile_subquery_to_array_source(inner_stmt)?;
3755 return Ok(IrStmt::Select(IrSelect {
3756 rows: vec![IrRowSource::Free(IrFreeExpr::AssertSet {
3757 fn_name: f.name.clone(),
3758 inner: Box::new(inner),
3759 message,
3760 })],
3761 filter: None,
3762 order_by: vec![],
3763 offset,
3764 limit,
3765 distinct,
3766 dml_source: None,
3767 polymorphic: false,
3768 poly_implementors: vec![],
3769 poly_columns: vec![],
3770 lock: None,
3771 }));
3772 }
3773 if let Some(expr) = self.aggregate_over_unpacked(s, result, distinct)? {
3777 return Ok(IrStmt::Select(IrSelect {
3778 rows: vec![IrRowSource::Free(IrFreeExpr::Scalar(expr))],
3779 filter: None,
3780 order_by: vec![],
3781 offset: None,
3782 limit: None,
3783 distinct: false,
3784 dml_source: None,
3785 polymorphic: false,
3786 poly_implementors: vec![],
3787 poly_columns: vec![],
3788 lock: None,
3789 }));
3790 }
3791 if let Expr::BinOp(b) = result
3799 && matches!(b.op, ast::BinOpKind::In | ast::BinOpKind::NotIn)
3800 && let Some(root) = self.object_set_name(&b.left)
3801 {
3802 let rewritten = Expr::BinOp(Box::new(ast::BinOp {
3803 left: Expr::Path(ast::Path {
3804 steps: vec![ast::PathStep::Name(root.clone()), ast::PathStep::Name("id".into())],
3805 partial: false,
3806 }),
3807 op: b.op.clone(),
3808 right: b.right.clone(),
3809 }));
3810 return self
3811 .compile_expr_as_path_select(s, &rewritten, &root, distinct)
3812 .map(IrStmt::PathSelect);
3813 }
3814 if let Some(root) = self.find_path_root_in_expr(result) {
3816 return self
3817 .compile_expr_as_path_select(s, result, &root, distinct)
3818 .map(IrStmt::PathSelect);
3819 }
3820 if let Expr::TypeIs { expr, ty } = result
3822 && let Expr::Path(p) = expr.as_ref()
3823 && !p.partial
3824 && p.steps.len() == 1
3825 && let ast::PathStep::Name(src_name) = &p.steps[0]
3826 && self.resolve_name_ref(src_name, true).is_none()
3827 {
3828 let src_td = self.resolve_type(src_name)?;
3829 {
3830 let src_qname = format!("{}::{}", src_td.module, src_td.name);
3831 let (ty_module, ty_name) = ty
3832 .as_named()
3833 .ok_or_else(|| self.type_err("cannot use IS with a tuple or array type"))?;
3834 let check_module = ty_module.unwrap_or(&src_td.module);
3835 let check_name = format!("{}::{}", check_module, ty_name);
3836 self.resolve_type(&check_name)?;
3837 let check_qname = check_name;
3838 let src_table = src_td.table.clone();
3839 let src_alias = self.fresh_alias();
3840 let src_td = self.resolve_type(&src_qname)?;
3841 let poly_implementors = if self.is_polymorphic(src_td) {
3842 self.find_poly_implementors(&src_qname)
3843 } else {
3844 vec![]
3845 };
3846 let bool_expr = self.type_check_bool_expr(&src_qname, &check_qname, src_td, &src_alias);
3847 let ps = IrPathSelect {
3848 root: IrSource {
3849 poly: None,
3850 type_name: src_qname,
3851 table: src_table,
3852 alias: src_alias,
3853 },
3854 joins: vec![],
3855 result: IrPathResult::Scalar(bool_expr, None),
3856 filter: s
3857 .filter
3858 .as_ref()
3859 .map(|_| Err(self.type_err("FILTER is not supported on type-is SELECT")))
3860 .transpose()?,
3861 order_by: vec![],
3862 offset: None,
3863 limit: None,
3864 distinct,
3865 poly_implementors,
3866 };
3867 return Ok(IrStmt::PathSelect(ps));
3868 }
3869 }
3870 if let Expr::Union(a, b) = result
3871 && !distinct
3872 && s.filter.is_none()
3873 && s.order_by.is_empty()
3874 && s.offset.is_none()
3875 && s.limit.is_none()
3876 && (self.is_scalar_walk(a) || self.is_scalar_walk(b))
3877 {
3878 return self.compile_scalar_union(result);
3879 }
3880 if !distinct
3882 && s.filter.is_none()
3883 && s.order_by.is_empty()
3884 && s.offset.is_none()
3885 && s.limit.is_none()
3886 && let Some(ps) = self.compile_shape_field_select(result, None)?
3887 {
3888 return Ok(IrStmt::PathSelect(ps));
3889 }
3890 if let Expr::Shape(sh) = result
3897 && let Some(subject) = sh.expr.as_ref()
3898 && !matches!(subject, Expr::Path(_))
3899 {
3900 let (base, fields) = Self::peel_field_access_chain(subject);
3901 if let Expr::SubQuery(inner_stmt) = base
3902 && !fields.is_empty()
3903 && matches!(inner_stmt.as_ref(), Stmt::Insert(_) | Stmt::Update(_) | Stmt::Delete(_))
3904 {
3905 let inner = self.compile_stmt(inner_stmt)?;
3906 let cte_name = self.fresh_nested_cte_name();
3907 let overrides = Self::junction_overrides_for(&inner, &cte_name);
3910 let type_name = self.register_cte(&cte_name, &inner);
3911 self.hoisted_ctes.push(IrCteDef {
3912 name: cte_name.clone(),
3913 stmt: inner,
3914 type_name,
3915 correlated_to: None,
3916 });
3917 let mut steps = vec![ast::PathStep::Name(cte_name)];
3918 steps.extend(fields.into_iter().map(ast::PathStep::Name));
3919 let rerooted = ast::SelectStmt {
3920 result: Expr::Shape(Box::new(ast::ShapeExpr {
3921 expr: Some(Expr::Path(ast::Path { steps, partial: false })),
3922 elements: sh.elements.clone(),
3923 marker_offset: None,
3924 })),
3925 filter: s.filter.clone(),
3926 order_by: s.order_by.clone(),
3927 offset: s.offset.clone(),
3928 limit: s.limit.clone(),
3929 lock: s.lock.clone(),
3930 };
3931 let previous = std::mem::replace(&mut self.junction_read_overrides, overrides);
3932 let compiled = self.compile_stmt(&Stmt::Select(rerooted));
3933 self.junction_read_overrides = previous;
3934 return compiled;
3935 }
3936 }
3937 if let Expr::Shape(sh) = result
3943 && let Some(Expr::SubQuery(inner_stmt)) = sh.expr.as_ref()
3944 && matches!(inner_stmt.as_ref(), Stmt::Select(_))
3945 && self.dml_subject_type(inner_stmt).is_err()
3946 {
3947 let inner = self.compile_stmt(inner_stmt)?;
3948 let cte_name = self.fresh_nested_cte_name();
3949 let type_name = self.register_cte(&cte_name, &inner);
3950 self.hoisted_ctes.push(IrCteDef {
3951 name: cte_name.clone(),
3952 stmt: inner,
3953 type_name,
3954 correlated_to: None,
3955 });
3956 let rerooted = ast::SelectStmt {
3957 result: Expr::Shape(Box::new(ast::ShapeExpr {
3958 expr: Some(Expr::Path(ast::Path {
3959 steps: vec![ast::PathStep::Name(cte_name)],
3960 partial: false,
3961 })),
3962 elements: sh.elements.clone(),
3963 marker_offset: None,
3964 })),
3965 filter: s.filter.clone(),
3966 order_by: s.order_by.clone(),
3967 offset: s.offset.clone(),
3968 limit: s.limit.clone(),
3969 lock: s.lock.clone(),
3970 };
3971 return self.compile_stmt(&Stmt::Select(rerooted));
3972 }
3973 if let Expr::SubQuery(inner) = result
3979 && matches!(inner.as_ref(), Stmt::For(_))
3980 && !distinct
3981 && s.filter.is_none()
3982 && s.order_by.is_empty()
3983 && s.offset.is_none()
3984 && s.limit.is_none()
3985 {
3986 return self.compile_stmt(inner);
3987 }
3988 self.check_union_type_compat(result)?;
3990 if self.is_free_result(result) {
3991 self.compile_free_select(s, result, distinct).map(IrStmt::Select)
3992 } else {
3993 self.compile_select(s, result, distinct).map(IrStmt::Select)
3994 }
3995 }
3996 Stmt::Insert(s) => self.compile_insert(s).map(IrStmt::Insert),
3997 Stmt::Update(s) => self.compile_update(s).map(IrStmt::Update),
3998 Stmt::Delete(s) => self.compile_delete(s).map(IrStmt::Delete),
3999 Stmt::Group(s) => self.compile_group(s).map(IrStmt::Group),
4000 Stmt::With(w) => {
4004 for alias in &w.aliases {
4005 if self.bind_inline_if_correlated(&alias.name, &alias.expr, None)?
4006 || self.bind_group(&alias.name, &alias.expr)?
4007 {
4008 continue;
4009 }
4010 let (ir_inner, correlated_to) = self.compile_binding_in_scope(&alias.expr)?;
4011 let sql_name = self.claim_cte_sql_name(&alias.name);
4012 if let Some(slot) = correlated_to.as_deref().and_then(|i| i.strip_prefix("_for_")) {
4013 self.correlated_bindings.insert(sql_name.clone(), slot.to_string());
4014 }
4015 let type_name = self.register_cte(&alias.name, &ir_inner);
4016 self.hoisted_binding_sources
4017 .insert(sql_name.clone(), alias.expr.clone());
4018 self.hoisted_ctes.push(IrCteDef {
4019 name: sql_name,
4020 stmt: ir_inner,
4021 type_name,
4022 correlated_to,
4023 });
4024 }
4025 self.compile_stmt(&w.stmt)
4026 }
4027 Stmt::For(f) => match self.compile_group_elements(f)? {
4028 Some(grp) => Ok(IrStmt::Group(grp)),
4029 None => self.compile_for(f).map(IrStmt::For),
4030 },
4031 Stmt::Analyze(inner) => self.compile_stmt(inner),
4035 }
4036 }
4037
4038 fn compile_schema_cast_select(&mut self, sel: &ast::SelectStmt, tc: &ast::TypeCast) -> Result<IrSelect, PyQLError> {
4042 let id_filter = Expr::BinOp(Box::new(ast::BinOp {
4043 left: Expr::Path(ast::Path::relative("id")),
4044 op: ast::BinOpKind::Eq,
4045 right: tc.expr.clone(),
4046 }));
4047 let merged_filter = match &sel.filter {
4048 None => Some(id_filter),
4049 Some(existing) => Some(Expr::BinOp(Box::new(ast::BinOp {
4050 left: id_filter,
4051 op: ast::BinOpKind::And,
4052 right: existing.clone(),
4053 }))),
4054 };
4055 let (module, name) = tc
4058 .ty
4059 .as_named()
4060 .ok_or_else(|| self.type_err("cannot use a tuple or array type as a schema object cast"))?;
4061 let qname = match module {
4066 Some(m) => format!("{}::{}", m, name),
4067 None => name.to_string(),
4068 };
4069 let synthetic = ast::SelectStmt {
4070 result: Expr::Path(ast::Path::absolute(&qname)),
4071 filter: merged_filter,
4072 order_by: sel.order_by.clone(),
4073 offset: sel.offset.clone(),
4074 limit: sel.limit.clone(),
4075 lock: sel.lock.clone(),
4076 };
4077 self.compile_select(&synthetic, &synthetic.result, false)
4078 }
4079
4080 fn compile_shape_anchored(
4089 &mut self,
4090 elements: &[ShapeElement],
4091 td: &'a TypeDescriptor,
4092 alias: &str,
4093 module: &str,
4094 ) -> Result<Vec<IrShapePointer>, PyQLError> {
4095 self.anchors.push(SelectAnchor {
4096 type_name: td.name.clone(),
4097 qualified: format!("{}::{}", td.module, td.name),
4098 alias: alias.to_string(),
4099 detached: false,
4100 declared_on: None,
4101 });
4102 let result = self.compile_shape(elements, td, alias, module);
4103 self.anchors.pop();
4104 result
4105 }
4106
4107 fn compile_path_select(
4108 &mut self,
4109 sel: &ast::SelectStmt,
4110 path: &ast::Path,
4111 shape_elements: &[ShapeElement],
4112 distinct: bool,
4113 ) -> Result<IrPathSelect, PyQLError> {
4114 self.compile_path_select_with_tail(sel, path, shape_elements, distinct, 0, &[])
4115 }
4116
4117 fn compile_path_select_with_tail(
4122 &mut self,
4123 sel: &ast::SelectStmt,
4124 path: &ast::Path,
4125 shape_elements: &[ShapeElement],
4126 distinct: bool,
4127 tail: usize,
4128 tail_sorts: &[ast::SortExpr],
4129 ) -> Result<IrPathSelect, PyQLError> {
4130 let outer_anchor = self.modifier_anchor.take();
4131 let outer_sorts = std::mem::replace(&mut self.tail_sorts, tail_sorts.to_vec());
4134 let mut result = self.compile_path_select_inner(sel, path, shape_elements, distinct, tail);
4135 self.tail_sorts = outer_sorts;
4136 self.modifier_anchor = outer_anchor;
4137 if let Ok(path_select) = &mut result {
4138 self.resolve_join_fanouts(path_select);
4139 self.drop_empty_results(path_select);
4140 }
4141 result
4142 }
4143
4144 fn compile_path_select_inner(
4145 &mut self,
4146 sel: &ast::SelectStmt,
4147 path: &ast::Path,
4148 shape_elements: &[ShapeElement],
4149 distinct: bool,
4150 mut tail: usize,
4151 ) -> Result<IrPathSelect, PyQLError> {
4152 use ast::PathStep;
4153
4154 let root_name = match &path.steps[0] {
4155 PathStep::Name(n) => n.as_str(),
4156 _ => return Err(self.type_err("path traversal must start with a type name")),
4157 };
4158 let root_td = self.resolve_path_root(root_name)?;
4165 let root_alias = self.fresh_alias();
4166 let root = IrSource {
4167 poly: None,
4168 type_name: format!("{}::{}", root_td.module, root_td.name),
4169 table: self.row_source_table(root_name, root_td),
4170 alias: root_alias.clone(),
4171 };
4172 let for_var_root = self.for_var_types.contains_key(root_name).then(|| {
4176 IrExpr::BinOp(Box::new(IrBinOp {
4177 left: IrExpr::ColumnRef {
4178 alias: root_alias.clone(),
4179 column: "id".to_string(),
4180 pg_type: "uuid".to_string(),
4181 },
4182 op: ast::BinOpKind::Eq,
4183 right: self.for_var_ref(root_name),
4184 }))
4185 });
4186
4187 let mut joins: Vec<IrPathJoin> = vec![];
4188 let mut current_td = root_td;
4189 let mut current_alias = root_alias;
4190
4191 let mut steps: Vec<PathStep> = path.steps[1..].to_vec();
4195 let mut pending_filters: Vec<(usize, Expr)> = vec![];
4198 let mut extra_conditions: Vec<IrExpr> = for_var_root.into_iter().collect();
4199 let mut junction_scope: Option<(String, String)> = None;
4202 let mut splices = 0usize;
4203 let mut idx = 0;
4204 if tail > 0 && tail >= steps.len() {
4205 self.modifier_anchor = Some((
4206 format!("{}::{}", current_td.module, current_td.name),
4207 current_alias.clone(),
4208 ));
4209 }
4210 while idx < steps.len() {
4211 let owned_step = steps[idx].clone();
4212 let step = &owned_step;
4213 let n_steps = steps.len();
4214 let is_last = |extra: usize| idx + extra == n_steps - 1;
4215 if tail > 0 && idx == n_steps - tail {
4216 self.modifier_anchor = Some((
4217 format!("{}::{}", current_td.module, current_td.name),
4218 current_alias.clone(),
4219 ));
4220 }
4221
4222 while let Some(pos) = pending_filters.iter().position(|(i, _)| *i + 1 == idx) {
4225 let (_, f) = pending_filters.remove(pos);
4226 let cond = self.as_condition(|c| c.compile_expr(&f, current_td, ¤t_alias))?;
4227 extra_conditions.push(cond);
4228 }
4229
4230 if let PathStep::TypeIntersection(type_ref) = step {
4232 let type_name = match &type_ref.module {
4233 Some(m) => format!("{}::{}", m, type_ref.name),
4234 None => type_ref.name.clone(),
4235 };
4236 let narrowed = self.resolve_type(&type_name)?;
4237 let narrowed_has_relation = !Self::backs_no_relation(narrowed);
4244 if narrowed_has_relation && narrowed.table != current_td.table {
4245 let target_alias = self.fresh_alias();
4246 let target = IrSource {
4247 poly: None,
4248 type_name: format!("{}::{}", narrowed.module, narrowed.name),
4249 table: narrowed.table.clone(),
4250 alias: target_alias.clone(),
4251 };
4252 joins.push(IrPathJoin::Single {
4253 source_alias: current_alias.clone(),
4254 fk_col: "id".to_string(),
4255 target,
4256 });
4257 current_alias = target_alias;
4258 }
4259 current_td = narrowed;
4260 idx += 1;
4261 continue;
4262 }
4263
4264 if let PathStep::Backlink(link_name) = step {
4266 let owner_hint = steps.get(idx + 1).and_then(|s| {
4268 if let PathStep::TypeIntersection(tr) = s {
4269 Some(tr.clone())
4270 } else {
4271 None
4272 }
4273 });
4274 let consumed_extra = if owner_hint.is_some() { 1 } else { 0 };
4275
4276 let owner_td: &TypeDescriptor = if let Some(ref tr) = owner_hint {
4277 let type_name = match &tr.module {
4278 Some(m) => format!("{}::{}", m, tr.name),
4279 None => tr.name.clone(),
4280 };
4281 let td = self.resolve_type(&type_name)?;
4282 let current_qname = format!("{}::{}", current_td.module, current_td.name);
4283 let link_targets_current =
4284 td.links
4285 .iter()
4286 .any(|l| l.name == *link_name && self.link_target_reaches(&l.target, ¤t_qname))
4287 || td.multilinks.iter().any(|ml| {
4288 ml.name == *link_name && self.link_target_reaches(&ml.target, ¤t_qname)
4289 });
4290 if link_targets_current {
4291 td
4292 } else {
4293 let narrowed = format!("{}::{}", td.module, td.name);
4296 let declaring: Vec<&'a TypeDescriptor> = self
4297 .schema
4298 .types
4299 .iter()
4300 .filter(|t| !t.abstract_ && Self::is_or_implements(t, &narrowed))
4301 .filter(|t| self.declares_backlink(t, link_name, ¤t_qname))
4302 .collect();
4303 match Self::without_inherited_owners(declaring).as_slice() {
4304 [only] => only,
4305 _ => {
4306 return Err(self.type_err(&format!(
4307 "link '{}::{}' does not target '{}'; backlink is not valid here",
4308 type_name, link_name, current_qname,
4309 )));
4310 }
4311 }
4312 }
4313 } else {
4314 let current_qname = format!("{}::{}", current_td.module, current_td.name);
4316 self.schema
4317 .types
4318 .iter()
4319 .filter(|t| {
4321 t.bases.is_empty()
4322 || !t.bases.iter().any(|base| {
4323 self.resolve_type(base).is_ok_and(|b| {
4324 b.links.iter().any(|l| l.name == *link_name)
4325 || b.multilinks.iter().any(|ml| ml.name == *link_name)
4326 })
4327 })
4328 })
4329 .find(|t| {
4330 t.links
4331 .iter()
4332 .any(|l| l.name == *link_name && self.link_target_reaches(&l.target, ¤t_qname))
4333 || t.multilinks.iter().any(|ml| {
4334 ml.name == *link_name && self.link_target_reaches(&ml.target, ¤t_qname)
4335 })
4336 })
4337 .ok_or_else(|| {
4338 self.type_err(&format!(
4339 "no type has a link '{}' targeting '{}'",
4340 link_name, current_qname,
4341 ))
4342 })?
4343 };
4344
4345 let target_alias = self.fresh_alias();
4346 let target = IrSource {
4347 poly: None,
4348 type_name: format!("{}::{}", owner_td.module, owner_td.name),
4349 table: owner_td.table.clone(),
4350 alias: target_alias.clone(),
4351 };
4352
4353 if let Some(l) = owner_td.links.iter().find(|l| l.name == *link_name) {
4357 if l.is_junction_backed() {
4358 let junction_alias = self.fresh_alias();
4359 let (junction_table, module, owner_col, current_col, _) =
4360 self.link_junction_info(owner_td, l)?;
4361 joins.push(IrPathJoin::BacklinkMulti {
4362 source_alias: current_alias.clone(),
4363 junction_alias,
4364 junction_table,
4365 module,
4366 owner_col,
4367 current_col,
4368 target,
4369 });
4370 } else {
4371 joins.push(IrPathJoin::BacklinkSingle {
4372 source_alias: current_alias.clone(),
4373 fk_col: format!("{}_id", link_name),
4374 target,
4375 });
4376 }
4377 } else {
4378 let ml = owner_td
4379 .multilinks
4380 .iter()
4381 .find(|ml| ml.name == *link_name)
4382 .unwrap()
4383 .clone();
4384 let junction_alias = self.fresh_alias();
4385 let (junction_table, module, _, _, _) = self.multilink_junction_info(owner_td, &ml)?;
4386 joins.push(IrPathJoin::BacklinkMulti {
4387 source_alias: current_alias.clone(),
4388 junction_alias,
4389 junction_table,
4390 module,
4391 owner_col: "source".to_string(),
4392 current_col: "target".to_string(),
4393 target,
4394 });
4395 }
4396
4397 if is_last(consumed_extra) {
4398 let shape =
4399 self.compile_shape_anchored(shape_elements, owner_td, &target_alias, &owner_td.module.clone())?;
4400 let result = IrPathResult::Object {
4401 alias: target_alias.clone(),
4402 type_name: format!("{}::{}", owner_td.module, owner_td.name),
4403 shape,
4404 };
4405 let (filter, order_by, offset, limit) = self.compile_path_modifiers_scoped(
4406 sel,
4407 owner_td,
4408 &target_alias,
4409 junction_scope.clone(),
4410 shape_elements,
4411 )?;
4412 return Ok(IrPathSelect {
4413 root,
4414 joins,
4415 result,
4416 filter: and_conditions(filter, extra_conditions),
4417 order_by,
4418 offset,
4419 limit,
4420 distinct,
4421 poly_implementors: vec![],
4422 });
4423 }
4424 current_td = owner_td;
4425 current_alias = target_alias;
4426 idx += 1 + consumed_extra;
4427 continue;
4428 }
4429
4430 let step_name = match step {
4431 PathStep::Name(n) => n.as_str(),
4432 _ => return Err(self.type_err("only name steps are supported in path traversal")),
4433 };
4434
4435 if step_name == "__type__" {
4439 if !is_last(0) {
4440 return Err(
4441 self.type_err("'__type__' is the type's name, not an object — it cannot be traversed further")
4442 );
4443 }
4444 let polymorphic = self.is_polymorphic(current_td);
4445 let expr = if polymorphic {
4446 IrExpr::ColumnRef {
4447 alias: current_alias.clone(),
4448 column: "__type__".to_string(),
4449 pg_type: "text".to_string(),
4450 }
4451 } else {
4452 IrExpr::Literal(IrLiteral::Str(format!("{}::{}", current_td.module, current_td.name)))
4453 };
4454 let poly_implementors = if polymorphic && joins.is_empty() && !root.table.starts_with("@cte:") {
4459 self.find_poly_implementors(&root.type_name.clone())
4460 } else {
4461 vec![]
4462 };
4463 let (filter, order_by, offset, limit) = self.compile_path_modifiers_scoped(
4464 sel,
4465 current_td,
4466 ¤t_alias,
4467 junction_scope.clone(),
4468 shape_elements,
4469 )?;
4470 return Ok(IrPathSelect {
4471 root,
4472 joins,
4473 result: IrPathResult::Scalar(expr, None),
4474 filter: and_conditions(filter, extra_conditions),
4475 order_by,
4476 offset,
4477 limit,
4478 distinct,
4479 poly_implementors,
4480 });
4481 }
4482
4483 if let Some(p) = current_td.properties.iter().find(|p| p.name == step_name) {
4485 if !is_last(0) {
4486 if (p.pg_type.starts_with("__nt__:") || p.tuple_members.is_some()) && !p.pg_type.ends_with("[]") {
4492 let base = IrExpr::ColumnRef {
4493 alias: current_alias.clone(),
4494 column: p.name.clone(),
4495 pg_type: p.pg_type.clone(),
4496 };
4497 let remaining = &steps[idx + 1..];
4498 let mut ir: IrExpr = base;
4499 for step in remaining {
4500 let field = match step {
4501 ast::PathStep::Name(n) => n.clone(),
4502 _ => return Err(self.type_err("only field name steps are valid inside a named tuple")),
4503 };
4504 ir = IrExpr::JsonbField {
4505 expr: Box::new(ir),
4506 field,
4507 };
4508 }
4509 let (filter, order_by, offset, limit) = self.compile_path_modifiers_scoped(
4510 sel,
4511 current_td,
4512 ¤t_alias,
4513 junction_scope.clone(),
4514 shape_elements,
4515 )?;
4516 return Ok(IrPathSelect {
4517 root,
4518 joins,
4519 result: IrPathResult::Scalar(ir, None),
4520 filter: and_conditions(filter, extra_conditions),
4521 order_by,
4522 offset,
4523 limit,
4524 distinct,
4525 poly_implementors: vec![],
4526 });
4527 }
4528 return Err(self.type_err(&format!(
4529 "'{step_name}' is a scalar property, not a link — cannot traverse further"
4530 )));
4531 }
4532 let tuple_shape = self.resolve_property_tuple_shape(p);
4533 let result = IrPathResult::Scalar(
4534 IrExpr::ColumnRef {
4535 alias: current_alias.clone(),
4536 column: p.name.clone(),
4537 pg_type: p.pg_type.clone(),
4538 },
4539 tuple_shape,
4540 );
4541 let (filter, order_by, offset, limit) = self.compile_path_modifiers_scoped(
4542 sel,
4543 current_td,
4544 ¤t_alias,
4545 junction_scope.clone(),
4546 shape_elements,
4547 )?;
4548 return Ok(IrPathSelect {
4549 root,
4550 joins,
4551 result,
4552 filter: and_conditions(filter, extra_conditions),
4553 order_by,
4554 offset,
4555 limit,
4556 distinct,
4557 poly_implementors: vec![],
4558 });
4559 }
4560
4561 if let Some(l) = Self::resolve_link(current_td, step_name) {
4563 let target_td = self.resolve_type(&l.target)?;
4564 let target_alias = self.fresh_alias();
4565 let target = IrSource {
4566 poly: None,
4567 type_name: format!("{}::{}", target_td.module, target_td.name),
4568 table: target_td.table.clone(),
4569 alias: target_alias.clone(),
4570 };
4571 if l.is_junction_backed() {
4572 let join = self.build_multilink_join(current_td, &l.name, &l.target, &l.through)?;
4577 let junction_alias = self.fresh_alias();
4578 junction_scope = l.through.clone().map(|t| (t, junction_alias.clone()));
4579 joins.push(IrPathJoin::Multi {
4580 source_alias: current_alias.clone(),
4581 junction_alias,
4582 join,
4583 target,
4584 });
4585 } else {
4586 joins.push(IrPathJoin::Single {
4587 source_alias: current_alias.clone(),
4588 fk_col: format!("{}_id", l.name),
4589 target,
4590 });
4591 }
4592 if is_last(0) {
4593 let shape = self.compile_shape_anchored(
4594 shape_elements,
4595 target_td,
4596 &target_alias,
4597 &target_td.module.clone(),
4598 )?;
4599 let result = IrPathResult::Object {
4600 alias: target_alias.clone(),
4601 type_name: format!("{}::{}", target_td.module, target_td.name),
4602 shape,
4603 };
4604 let (filter, order_by, offset, limit) = self.compile_path_modifiers_scoped(
4605 sel,
4606 target_td,
4607 &target_alias,
4608 junction_scope.clone(),
4609 shape_elements,
4610 )?;
4611 return Ok(IrPathSelect {
4612 root,
4613 joins,
4614 result,
4615 filter: and_conditions(filter, extra_conditions),
4616 order_by,
4617 offset,
4618 limit,
4619 distinct,
4620 poly_implementors: vec![],
4621 });
4622 }
4623 current_td = target_td;
4624 current_alias = target_alias;
4625 idx += 1;
4626 continue;
4627 }
4628
4629 if let Some(ml) = Self::resolve_multilink(current_td, step_name) {
4631 let target_td = self.resolve_type(&ml.target)?;
4632 let target_alias = self.fresh_alias();
4633 let junction_alias = self.fresh_alias();
4634 let override_for_step = self
4638 .junction_read_overrides
4639 .get(&self.owner_junction(current_td, &ml.name, None))
4640 .cloned();
4641 let target = IrSource {
4642 poly: None,
4643 type_name: format!("{}::{}", target_td.module, target_td.name),
4644 table: match override_for_step.as_ref().and_then(|o| o.targets.clone()) {
4645 Some(cte) => cte,
4646 None => target_td.table.clone(),
4647 },
4648 alias: target_alias.clone(),
4649 };
4650 let join_info = if let Some(through_qname) = &ml.through {
4651 let through_td = self.resolve_type(through_qname)?;
4652 if through_td.junction {
4653 IrMultiLinkJoin::Standard {
4656 junction_table: self.owner_junction(current_td, &ml.name, Some(through_td)),
4657 module: current_td.module.clone(),
4658 }
4659 } else {
4660 let source_qname = format!("{}::{}", current_td.module, current_td.name);
4661 let source_col = through_td
4662 .links
4663 .iter()
4664 .find(|l| l.target == source_qname)
4665 .ok_or_else(|| {
4666 PyQLError::Type(PyQLTypeError {
4667 message: format!("through type {through_qname} has no link to {source_qname}"),
4668 position: Position { line: 0, col: 0 },
4669 })
4670 })?
4671 .name
4672 .clone();
4673 let target_col = through_td
4674 .links
4675 .iter()
4676 .find(|l| l.target == ml.target && l.name != source_col)
4677 .or_else(|| through_td.links.iter().find(|l| l.target == ml.target))
4678 .ok_or_else(|| {
4679 PyQLError::Type(PyQLTypeError {
4680 message: format!(
4681 "through type {through_qname} has no link to target {}",
4682 ml.target
4683 ),
4684 position: Position { line: 0, col: 0 },
4685 })
4686 })?
4687 .name
4688 .clone();
4689 IrMultiLinkJoin::Through {
4690 junction_table: through_td.table.clone(),
4691 module: through_td.module.clone(),
4692 source_col,
4693 target_col,
4694 }
4695 }
4696 } else {
4697 let junction_table = self.owner_junction(current_td, &ml.name, None);
4698 IrMultiLinkJoin::Standard {
4699 junction_table: match &override_for_step {
4700 Some(o) => o.junction.clone(),
4701 None => junction_table,
4702 },
4703 module: current_td.module.clone(),
4704 }
4705 };
4706 junction_scope = ml.through.clone().map(|t| (t, junction_alias.clone()));
4707 joins.push(IrPathJoin::Multi {
4708 source_alias: current_alias.clone(),
4709 junction_alias,
4710 join: join_info,
4711 target,
4712 });
4713 if is_last(0) {
4714 let shape = self.compile_shape_anchored(
4715 shape_elements,
4716 target_td,
4717 &target_alias,
4718 &target_td.module.clone(),
4719 )?;
4720 let result = IrPathResult::Object {
4721 alias: target_alias.clone(),
4722 type_name: format!("{}::{}", target_td.module, target_td.name),
4723 shape,
4724 };
4725 let (filter, order_by, offset, limit) = self.compile_path_modifiers_scoped(
4726 sel,
4727 target_td,
4728 &target_alias,
4729 junction_scope.clone(),
4730 shape_elements,
4731 )?;
4732 return Ok(IrPathSelect {
4733 root,
4734 joins,
4735 result,
4736 filter: and_conditions(filter, extra_conditions),
4737 order_by,
4738 offset,
4739 limit,
4740 distinct,
4741 poly_implementors: vec![],
4742 });
4743 }
4744 current_td = target_td;
4745 current_alias = target_alias;
4746 idx += 1;
4747 continue;
4748 }
4749
4750 let declared = match self.resolve_computed(current_td, step_name) {
4755 Some(cd) => Some(crate::parse::parse_pointer_expr(&cd.expression).map_err(PyQLError::Syntax)?),
4756 None if idx == 0 => self
4757 .active_declared_pointers
4758 .iter()
4759 .find(|d| path_leaf(&d.path).is_ok_and(|n| n == step_name))
4760 .and_then(|d| d.compexpr.clone()),
4761 None => None,
4762 };
4763 if let Some(parsed) = declared {
4764 let rewritten = Self::field_access_over_select(&parsed);
4767 let expr_ast = match &rewritten {
4768 Some((sel, _)) => Expr::SubQuery(Box::new(Stmt::Select(sel.clone()))),
4769 None => parsed,
4770 };
4771 let field_steps = rewritten.as_ref().map(|(_, n)| *n).unwrap_or(0);
4772
4773 if let Some((p, _, modifiers)) = Self::pointer_subject(&expr_ast)
4785 && p.partial
4786 && !p.steps.is_empty()
4787 && (!is_last(0)
4788 || self
4789 .walk_path_types(current_td, &p.steps, MAX_COMPUTED_SPLICES)
4790 .1
4791 .is_some())
4792 {
4793 if let Some(m) = modifiers
4798 && (m.limit.is_some() || m.offset.is_some() || !m.order_by.is_empty())
4799 {
4800 let mut inner_steps =
4801 vec![PathStep::Name(format!("{}::{}", current_td.module, current_td.name))];
4802 inner_steps.extend(p.steps.iter().cloned());
4803 let inner_path = ast::Path {
4804 steps: inner_steps,
4805 partial: false,
4806 };
4807 let inner_sel = ast::SelectStmt {
4808 result: Expr::Path(inner_path.clone()),
4809 filter: m.filter.clone(),
4810 order_by: m.order_by.clone(),
4811 offset: m.offset.clone(),
4812 limit: m.limit.clone(),
4813 lock: None,
4814 };
4815 let mut inner =
4816 self.compile_path_select_with_tail(&inner_sel, &inner_path, &[], false, field_steps, &[])?;
4817 Self::correlate_path_select(&mut inner, ¤t_alias);
4818 let IrPathResult::Object { type_name, .. } = &inner.result else {
4819 return Err(self.type_err(&format!(
4820 "computed pointer '{step_name}' is a scalar — a path cannot continue through it"
4821 )));
4822 };
4823 let target_td = self.resolve_type(&type_name.clone())?;
4824 let target_alias = self.fresh_alias();
4825 let target = IrSource {
4826 poly: None,
4827 type_name: format!("{}::{}", target_td.module, target_td.name),
4828 table: target_td.table.clone(),
4829 alias: target_alias.clone(),
4830 };
4831 joins.push(IrPathJoin::Lateral {
4832 inner: Box::new(inner),
4833 target,
4834 });
4835 if is_last(0) {
4836 let module = target_td.module.clone();
4837 let shape =
4838 self.compile_shape_anchored(shape_elements, target_td, &target_alias, &module)?;
4839 let result = IrPathResult::Object {
4840 alias: target_alias.clone(),
4841 type_name: format!("{}::{}", target_td.module, target_td.name),
4842 shape,
4843 };
4844 let (filter, order_by, offset, limit) = self.compile_path_modifiers_scoped(
4845 sel,
4846 target_td,
4847 &target_alias,
4848 junction_scope,
4849 shape_elements,
4850 )?;
4851 return Ok(IrPathSelect {
4852 root,
4853 joins,
4854 result,
4855 filter: and_conditions(filter, extra_conditions),
4856 order_by,
4857 offset,
4858 limit,
4859 distinct,
4860 poly_implementors: vec![],
4861 });
4862 }
4863 current_td = target_td;
4864 current_alias = target_alias;
4865 idx += 1;
4866 continue;
4867 }
4868 splices += 1;
4869 if splices > MAX_COMPUTED_SPLICES {
4870 return Err(self.type_err(&format!(
4871 "computed pointer '{step_name}' expands into itself — \
4872 a path cannot be resolved through a cycle of computed pointers"
4873 )));
4874 }
4875 let filter = modifiers.and_then(|m| m.filter.clone());
4876 let spliced = p.steps.clone();
4877 if tail > 0 && idx >= steps.len() - tail {
4878 tail += spliced.len() - 1;
4879 }
4880 let landing = idx + spliced.len() - 1;
4881 steps.splice(idx..idx + 1, spliced);
4882 let shift = landing - idx;
4885 for (i, _) in pending_filters.iter_mut() {
4886 if *i > idx {
4887 *i += shift;
4888 }
4889 }
4890 if let Some(f) = filter {
4891 pending_filters.push((landing, f));
4892 }
4893 continue;
4894 }
4895
4896 if let Some((fc, modifiers)) = Self::function_subject(&expr_ast)
4902 && let Some(fd) = self.resolve_object_fn(fc)
4903 {
4904 if fd.params.len() != fc.args.len() {
4905 return Err(self.type_err(&format!(
4906 "function '{}::{}' expects {} argument(s), got {}",
4907 fd.module,
4908 fd.name,
4909 fd.params.len(),
4910 fc.args.len()
4911 )));
4912 }
4913 let (fn_module, fn_name, return_type_name) =
4914 (fd.module.clone(), fd.name.clone(), fd.return_pg_type.clone());
4915 let mut args = fc
4916 .args
4917 .iter()
4918 .map(|a| self.compile_expr(a, current_td, ¤t_alias))
4919 .collect::<Result<Vec<_>, _>>()?;
4920 let qualified = format!("{fn_module}::{fn_name}");
4921 if let Some(globals) = self.globals_arg_for_call(&qualified)? {
4922 args.insert(0, globals);
4923 }
4924 let target_td = self.resolve_type(&return_type_name)?;
4925 let target_alias = self.fresh_alias();
4926 let target = IrSource {
4927 poly: None,
4928 type_name: format!("{}::{}", target_td.module, target_td.name),
4929 table: target_td.table.clone(),
4930 alias: target_alias.clone(),
4931 };
4932 joins.push(IrPathJoin::Function {
4933 fn_module,
4934 fn_name,
4935 args,
4936 target,
4937 });
4938 if let Some(f) = modifiers.and_then(|m| m.filter.clone()) {
4939 let cond = self.as_condition(|c| c.compile_expr(&f, target_td, &target_alias))?;
4940 extra_conditions.push(cond);
4941 }
4942 if is_last(0) {
4943 let shape = self.compile_shape_anchored(
4944 shape_elements,
4945 target_td,
4946 &target_alias,
4947 &target_td.module.clone(),
4948 )?;
4949 let result = IrPathResult::Object {
4950 alias: target_alias.clone(),
4951 type_name: format!("{}::{}", target_td.module, target_td.name),
4952 shape,
4953 };
4954 let (filter, order_by, offset, limit) = self.compile_path_modifiers_scoped(
4955 sel,
4956 target_td,
4957 &target_alias,
4958 junction_scope.clone(),
4959 shape_elements,
4960 )?;
4961 return Ok(IrPathSelect {
4962 root,
4963 joins,
4964 result,
4965 filter: and_conditions(filter, extra_conditions),
4966 order_by,
4967 offset,
4968 limit,
4969 distinct,
4970 poly_implementors: vec![],
4971 });
4972 }
4973 current_td = target_td;
4974 current_alias = target_alias;
4975 idx += 1;
4976 continue;
4977 }
4978
4979 if !is_last(0) {
4980 return Err(self.type_err(&format!(
4981 "'{step_name}' is a computed pointer — it has no stored column to traverse further through"
4982 )));
4983 }
4984 let expanding = format!("{}::{}.{step_name}", current_td.module, current_td.name);
4985 if self.expanding_computeds.contains(&expanding) {
4986 return Err(self.type_err(&format!(
4987 "computed pointer '{step_name}' expands into itself — \
4988 a path cannot be resolved through a cycle of computed pointers"
4989 )));
4990 }
4991 self.expanding_computeds.push(expanding);
4992 let expr = self.compile_expr(&expr_ast, current_td, ¤t_alias);
4993 self.expanding_computeds.pop();
4994 let expr = expr?;
4995 let (filter, order_by, offset, limit) = self.compile_path_modifiers_scoped(
4996 sel,
4997 current_td,
4998 ¤t_alias,
4999 junction_scope.clone(),
5000 shape_elements,
5001 )?;
5002 return Ok(IrPathSelect {
5003 root,
5004 joins,
5005 result: IrPathResult::Scalar(expr, None),
5006 filter: and_conditions(filter, extra_conditions),
5007 order_by,
5008 offset,
5009 limit,
5010 distinct,
5011 poly_implementors: vec![],
5012 });
5013 }
5014
5015 return Err(self.field_err(step_name, &format!("{}::{}", current_td.module, current_td.name)));
5016 }
5017
5018 if matches!(steps.last(), Some(PathStep::TypeIntersection(_))) {
5024 let module = current_td.module.clone();
5025 let type_name = format!("{}::{}", current_td.module, current_td.name);
5026 let shape = self.compile_shape_anchored(shape_elements, current_td, ¤t_alias, &module)?;
5027 let (filter, order_by, offset, limit) = self.compile_path_modifiers_scoped(
5028 sel,
5029 current_td,
5030 ¤t_alias,
5031 junction_scope.clone(),
5032 shape_elements,
5033 )?;
5034 return Ok(IrPathSelect {
5035 root,
5036 joins,
5037 result: IrPathResult::Object {
5038 alias: current_alias,
5039 type_name,
5040 shape,
5041 },
5042 filter: and_conditions(filter, extra_conditions),
5043 order_by,
5044 offset,
5045 limit,
5046 distinct,
5047 poly_implementors: vec![],
5048 });
5049 }
5050
5051 Err(self.type_err("empty path traversal"))
5053 }
5054
5055 fn compile_path_modifiers_scoped(
5060 &mut self,
5061 sel: &ast::SelectStmt,
5062 td: &TypeDescriptor,
5063 alias: &str,
5064 junction: Option<(String, String)>,
5065 shape: &[ShapeElement],
5066 ) -> Result<SelectModifiers, PyQLError> {
5067 let pushed = junction.is_some();
5068 if pushed {
5069 self.link_prop_scope.push(junction);
5070 }
5071 let outer_declared = self.active_declared_pointers.clone();
5076 self.active_declared_pointers
5077 .extend(shape.iter().filter(|el| el.compexpr.is_some()).cloned());
5078 let anchored = self.modifier_anchor.take();
5079 let tail_sorts = std::mem::take(&mut self.tail_sorts);
5080 let result = match &anchored {
5081 Some((qualified, anchor_alias)) => {
5082 let anchor_td = self.resolve_type(qualified)?;
5083 let anchor_alias = anchor_alias.clone();
5084 self.compile_path_modifiers(sel, anchor_td, &anchor_alias)
5085 }
5086 None => self.compile_path_modifiers(sel, td, alias),
5087 };
5088 let result = result.and_then(|(filter, mut order_by, offset, limit)| {
5089 if !tail_sorts.is_empty() {
5090 let sorting = ast::SelectStmt {
5091 result: sel.result.clone(),
5092 filter: None,
5093 order_by: tail_sorts,
5094 offset: None,
5095 limit: None,
5096 lock: None,
5097 };
5098 let (_, sorts, _, _) = self.compile_path_modifiers(&sorting, td, alias)?;
5099 order_by = sorts;
5100 }
5101 Ok((filter, order_by, offset, limit))
5102 });
5103 if pushed {
5104 self.link_prop_scope.pop();
5105 }
5106 self.active_declared_pointers = outer_declared;
5107 result
5108 }
5109
5110 fn compile_path_modifiers(
5111 &mut self,
5112 sel: &ast::SelectStmt,
5113 td: &TypeDescriptor,
5114 alias: &str,
5115 ) -> Result<SelectModifiers, PyQLError> {
5116 self.anchors.push(SelectAnchor {
5117 type_name: td.name.clone(),
5118 qualified: format!("{}::{}", td.module, td.name),
5119 alias: alias.to_string(),
5120 detached: std::mem::take(&mut self.pending_detached),
5121 declared_on: None,
5122 });
5123 let result = self.compile_path_modifiers_inner(sel, td, alias);
5124 self.anchors.pop();
5125 result
5126 }
5127
5128 fn compile_path_modifiers_inner(
5129 &mut self,
5130 sel: &ast::SelectStmt,
5131 td: &TypeDescriptor,
5132 alias: &str,
5133 ) -> Result<SelectModifiers, PyQLError> {
5134 let filter = sel
5135 .filter
5136 .as_ref()
5137 .map(|f| self.as_condition(|c| c.compile_expr(f, td, alias)))
5138 .transpose()?;
5139 let order_by = sel
5140 .order_by
5141 .iter()
5142 .map(|s| self.compile_sort(s, td, alias))
5143 .collect::<Result<Vec<_>, _>>()?;
5144 let offset = sel
5145 .offset
5146 .as_ref()
5147 .map(|e| self.compile_expr(e, td, alias))
5148 .transpose()?;
5149 let limit = sel
5150 .limit
5151 .as_ref()
5152 .map(|e| self.compile_expr(e, td, alias))
5153 .transpose()?;
5154 Ok((filter, order_by, offset, limit))
5155 }
5156
5157 fn object_set_name(&self, expr: &Expr) -> Option<String> {
5164 let Expr::Path(p) = expr else { return None };
5165 if p.partial || p.steps.len() != 1 {
5166 return None;
5167 }
5168 let ast::PathStep::Name(name) = &p.steps[0] else {
5169 return None;
5170 };
5171 if self.cte_object_type(name).is_some() {
5172 return Some(name.clone());
5173 }
5174 let bound = self.for_vars.contains_key(name)
5178 || self.cte_free_items.contains_key(name)
5179 || self.inline_bindings.contains_key(name)
5180 || self.cte_types.contains_key(name)
5181 || self.fn_params.contains_key(name);
5182 if !bound && self.resolve_type(name).is_ok() {
5183 return Some(name.clone());
5184 }
5185 None
5186 }
5187
5188 fn find_path_root_in_expr(&self, expr: &Expr) -> Option<String> {
5191 match expr {
5192 Expr::Path(p) if !p.partial && p.steps.len() > 1 => {
5193 if let ast::PathStep::Name(root) = &p.steps[0]
5194 && (self.resolve_type(root).is_ok() || self.cte_object_type(root).is_some())
5195 {
5196 return Some(root.clone());
5197 }
5198 None
5199 }
5200 Expr::FunctionCall(f) => f.args.iter().find_map(|a| self.find_path_root_in_expr(a)),
5201 Expr::BinOp(b) => self
5202 .find_path_root_in_expr(&b.left)
5203 .or_else(|| self.find_path_root_in_expr(&b.right)),
5204 Expr::UnaryOp(u) => self.find_path_root_in_expr(&u.operand),
5205 _ => None,
5206 }
5207 }
5208
5209 fn rewrite_abs_to_partial(expr: Expr, root_name: &str) -> Expr {
5211 match expr {
5212 Expr::Path(ref p) if !p.partial => {
5213 if let ast::PathStep::Name(first) = &p.steps[0]
5214 && first == root_name
5215 && p.steps.len() > 1
5216 {
5217 return Expr::Path(ast::Path {
5218 steps: p.steps[1..].to_vec(),
5219 partial: true,
5220 });
5221 }
5222 expr
5223 }
5224 Expr::FunctionCall(f) => Expr::FunctionCall(ast::FunctionCall {
5225 module: f.module,
5226 name: f.name,
5227 args: f
5228 .args
5229 .into_iter()
5230 .map(|a| Self::rewrite_abs_to_partial(a, root_name))
5231 .collect(),
5232 kwargs: f.kwargs,
5233 }),
5234 Expr::BinOp(b) => Expr::BinOp(Box::new(ast::BinOp {
5235 left: Self::rewrite_abs_to_partial(b.left, root_name),
5236 op: b.op,
5237 right: Self::rewrite_abs_to_partial(b.right, root_name),
5238 })),
5239 Expr::UnaryOp(u) => Expr::UnaryOp(Box::new(ast::UnaryOp {
5240 op: u.op,
5241 operand: Self::rewrite_abs_to_partial(u.operand, root_name),
5242 })),
5243 other => other,
5244 }
5245 }
5246
5247 fn shape_alias_paths(elements: &[ShapeElement]) -> Option<Vec<(String, ast::Path)>> {
5253 let mut defs = Vec::with_capacity(elements.len());
5254 for element in elements {
5255 let [ast::PathStep::Name(name)] = element.path.steps.as_slice() else {
5256 return None;
5257 };
5258 match &element.compexpr {
5259 Some(Expr::Path(p)) if p.partial => defs.push((name.clone(), p.clone())),
5260 _ => return None,
5261 }
5262 }
5263 Some(defs)
5264 }
5265
5266 fn substitute_shape_aliases(expr: Expr, defs: &[(String, ast::Path)]) -> Expr {
5270 match expr {
5271 Expr::Path(ref p) if p.partial => {
5272 let Some(ast::PathStep::Name(first)) = p.steps.first() else {
5273 return expr;
5274 };
5275 let Some((_, definition)) = defs.iter().find(|(name, _)| name == first) else {
5276 return expr;
5277 };
5278 let mut steps = definition.steps.clone();
5279 steps.extend(p.steps[1..].iter().cloned());
5280 Expr::Path(ast::Path { steps, partial: true })
5281 }
5282 Expr::FunctionCall(f) => Expr::FunctionCall(ast::FunctionCall {
5283 module: f.module,
5284 name: f.name,
5285 args: f
5286 .args
5287 .into_iter()
5288 .map(|a| Self::substitute_shape_aliases(a, defs))
5289 .collect(),
5290 kwargs: f.kwargs,
5291 }),
5292 Expr::BinOp(b) => Expr::BinOp(Box::new(ast::BinOp {
5293 left: Self::substitute_shape_aliases(b.left, defs),
5294 op: b.op,
5295 right: Self::substitute_shape_aliases(b.right, defs),
5296 })),
5297 Expr::UnaryOp(u) => Expr::UnaryOp(Box::new(ast::UnaryOp {
5298 op: u.op,
5299 operand: Self::substitute_shape_aliases(u.operand, defs),
5300 })),
5301 other => other,
5302 }
5303 }
5304
5305 fn aggregate_over_unpacked(
5312 &mut self,
5313 sel: &ast::SelectStmt,
5314 result: &Expr,
5315 distinct: bool,
5316 ) -> Result<Option<IrExpr>, PyQLError> {
5317 let Expr::FunctionCall(f) = result else {
5318 return Ok(None);
5319 };
5320 let [Expr::FunctionCall(unpack)] = f.args.as_slice() else {
5321 return Ok(None);
5322 };
5323 if !f.kwargs.is_empty()
5324 || !unpack.kwargs.is_empty()
5325 || unpack.module.as_deref().unwrap_or("std") != "std"
5326 || unpack.name != "array_unpack"
5327 {
5328 return Ok(None);
5329 }
5330 let [array] = unpack.args.as_slice() else {
5331 return Ok(None);
5332 };
5333 let namespace = f.module.as_deref().unwrap_or("std");
5334 let Some(sql_name) =
5335 crate::stdlib::lookup(namespace, &f.name)
5336 .into_iter()
5337 .find_map(|d| match &d.impl_strategy {
5338 crate::stdlib::ImplStrategy::SqlBuiltin(sql_name) if d.is_aggregate() => Some(sql_name.to_string()),
5339 _ => None,
5340 })
5341 else {
5342 return Ok(None);
5343 };
5344
5345 let elements = match array {
5349 Expr::Path(path) if !path.partial && path.steps.len() > 1 => {
5350 let walk = ast::Path {
5351 partial: false,
5352 steps: path.steps.clone(),
5353 };
5354 let mut path_select = self.compile_path_select(sel, &walk, &[], distinct)?;
5355 let IrPathResult::Scalar(column, _) = path_select.result else {
5356 return Ok(None);
5357 };
5358 path_select.result = IrPathResult::Scalar(
5359 IrExpr::FunctionCall(IrFunctionCall {
5360 return_pg_type: None,
5361 schema: None,
5362 name: "unnest".to_string(),
5363 args: vec![column],
5364 sql_template: None,
5365 }),
5366 None,
5367 );
5368 IrExpr::ArrayFromSelect(Box::new(IrArraySource::PathSelect(Box::new(path_select))))
5369 }
5370 _ if sel.filter.is_none()
5371 && sel.order_by.is_empty()
5372 && sel.offset.is_none()
5373 && sel.limit.is_none()
5374 && !distinct =>
5375 {
5376 self.compile_expr_ctx(array, None)?
5377 }
5378 _ => return Ok(None),
5379 };
5380
5381 let over_nothing = aggregate_over_nothing_sql(&f.name).unwrap_or("NULL");
5384 Ok(Some(IrExpr::FunctionCall(IrFunctionCall {
5385 return_pg_type: None,
5386 schema: None,
5387 name: sql_name.clone(),
5388 args: vec![elements],
5389 sql_template: Some(format!(
5390 "(SELECT coalesce({sql_name}(\"_s\".\"v\"), {over_nothing}) FROM unnest($1) AS \"_s\"(\"v\"))"
5391 )),
5392 })))
5393 }
5394
5395 fn compile_expr_as_path_select(
5399 &mut self,
5400 sel: &ast::SelectStmt,
5401 result: &Expr,
5402 root_type_name: &str,
5403 distinct: bool,
5404 ) -> Result<IrPathSelect, PyQLError> {
5405 let (agg_arg, agg_distinct) = match result {
5416 Expr::FunctionCall(f) if f.args.len() == 1 => match &f.args[0] {
5417 Expr::UnaryOp(u) if matches!(u.op, ast::UnaryOpKind::Distinct) => (Some(&u.operand), true),
5418 other => (Some(other), false),
5419 },
5420 _ => (None, false),
5421 };
5422 if let Expr::FunctionCall(f) = result
5423 && f.args.len() == 1
5424 && let Some(Expr::Path(p)) = agg_arg
5425 && !p.partial
5426 && p.steps.len() > 1
5427 {
5428 let arg_path = ast::Path {
5429 partial: false,
5430 steps: p.steps.clone(),
5431 };
5432 let mut ps = self.compile_path_select(sel, &arg_path, &[], distinct)?;
5433 let aggregated = match ps.result.clone() {
5437 IrPathResult::Scalar(column, _) => Some(column),
5438 IrPathResult::Object { alias, .. } => Some(IrExpr::ColumnRef {
5439 alias,
5440 column: "id".to_string(),
5441 pg_type: "uuid".to_string(),
5442 }),
5443 };
5444 if let Some(column) = aggregated {
5445 let mut args = vec![column];
5446 if !f.kwargs.is_empty() {
5447 if !matches!(f.name.as_str(), "assert_single" | "assert_exists" | "assert_distinct") {
5448 return Err(self.type_err(&format!(
5449 "function '{}' does not take named arguments, got '{}'",
5450 f.name, f.kwargs[0].0
5451 )));
5452 }
5453 args.extend(self.assert_message(f, None)?);
5454 }
5455 let mut call = self.resolve_fn_call(f.module.as_deref(), &f.name, args)?;
5456 if agg_distinct {
5457 let IrExpr::FunctionCall(fc) = &mut call else {
5458 return Err(self.type_err(&format!(
5459 "'{}' does not take a distinct argument — it does not resolve to an aggregate",
5460 f.name
5461 )));
5462 };
5463 if fc.schema.is_some() || fc.sql_template.is_some() {
5464 return Err(self.type_err(&format!(
5465 "'distinct' inside '{}' is not supported — only a plain SQL aggregate can take it",
5466 f.name
5467 )));
5468 }
5469 fc.sql_template = Some(format!("{}(DISTINCT $1)", fc.name));
5470 }
5471 ps.result = IrPathResult::Scalar(aggregate_over_nothing(&f.name, call), None);
5472 return Ok(ps);
5473 }
5474 }
5475
5476 if let Expr::BinOp(b) = result {
5480 let (path_expr, value_expr, flip) = match (&b.left, &b.right) {
5481 (Expr::Path(p), v) if !p.partial => (p, v, false),
5482 (v, Expr::Path(p)) if !p.partial => (p, v, true),
5483 _ => return self.compile_expr_as_path_select_fallback(sel, result, root_type_name, distinct),
5484 };
5485 let root_td = match self.cte_object_type(root_type_name) {
5487 Some(t) => self.resolve_type(&t)?,
5488 None => self.resolve_type(root_type_name)?,
5489 };
5490 let ast_path = ast::Path {
5492 partial: false,
5493 steps: path_expr.steps.clone(),
5494 };
5495 let mut ps = self.compile_path_select(sel, &ast_path, &[], distinct)?;
5496 let val_ir = self.compile_expr(value_expr, root_td, &ps.root.alias)?;
5497 let scalar_col = match ps.result {
5499 IrPathResult::Scalar(e, _) => e,
5500 IrPathResult::Object { type_name, .. } => {
5501 let val_type = infer_ir_type(&val_ir).map(pg_type_to_pyql).unwrap_or("unknown");
5502 return Err(PyQLError::Type(PyQLTypeError {
5503 message: format!(
5504 "operator '{}' cannot be applied to operands of type '{}' and '{}'",
5505 b.op, type_name, val_type,
5506 ),
5507 position: Position { line: 0, col: 0 },
5508 }));
5509 }
5510 };
5511 let (l, r) = if flip {
5512 (val_ir, scalar_col)
5513 } else {
5514 (scalar_col, val_ir)
5515 };
5516 ps.result = IrPathResult::Scalar(
5517 IrExpr::BinOp(Box::new(IrBinOp {
5518 left: true_division_operand(&b.op, l, &r),
5519 op: b.op.clone(),
5520 right: r,
5521 })),
5522 None,
5523 );
5524 return Ok(ps);
5525 }
5526 self.compile_expr_as_path_select_fallback(sel, result, root_type_name, distinct)
5527 }
5528
5529 fn compile_expr_as_path_select_fallback(
5530 &mut self,
5531 sel: &ast::SelectStmt,
5532 result: &Expr,
5533 root_type_name: &str,
5534 distinct: bool,
5535 ) -> Result<IrPathSelect, PyQLError> {
5536 let cte_object_type = self.cte_object_type(root_type_name);
5537 let td = match &cte_object_type {
5538 Some(t) => self.resolve_type(t)?,
5539 None => self.resolve_type(root_type_name)?,
5540 };
5541 let alias = self.fresh_alias();
5542 let root = IrSource {
5543 poly: None,
5544 type_name: format!("{}::{}", td.module, td.name),
5545 table: match &cte_object_type {
5546 Some(_) => self.cte_source_table(root_type_name),
5547 None => td.table.clone(),
5548 },
5549 alias: alias.clone(),
5550 };
5551 let rewritten = Self::rewrite_abs_to_partial(result.clone(), root_type_name);
5552 let expr = self.compile_expr(&rewritten, td, &alias)?;
5553 let (filter, order_by, offset, limit) = self.compile_path_modifiers(sel, td, &alias)?;
5554 Ok(IrPathSelect {
5555 root,
5556 joins: vec![],
5557 result: IrPathResult::Scalar(expr, None),
5558 filter,
5559 order_by,
5560 offset,
5561 limit,
5562 distinct,
5563 poly_implementors: vec![],
5564 })
5565 }
5566
5567 fn compile_subquery_to_array_source(&mut self, stmt: &Stmt) -> Result<IrArraySource, PyQLError> {
5570 match self.compile_stmt(stmt)? {
5571 IrStmt::Select(s) if matches!(s.rows.as_slice(), [IrRowSource::Bound { .. }]) => {
5572 Ok(IrArraySource::Select(s))
5573 }
5574 IrStmt::PathSelect(ps) => Ok(IrArraySource::PathSelect(Box::new(ps))),
5575 _ => Err(self.type_err("assert functions require a schema-bound SELECT as argument")),
5576 }
5577 }
5578
5579 fn compile_exists_ctx(
5587 &mut self,
5588 operand: &Expr,
5589 ctx: Option<(&TypeDescriptor, &str)>,
5590 ) -> Result<IrExpr, PyQLError> {
5591 match operand {
5592 Expr::Parameter(name) => {
5594 let idx = self.param_index(name);
5595 Ok(ir_is_not_null(IrExpr::Param { index: idx }))
5596 }
5597 Expr::TypeCast(_) => {
5599 let inner = self.compile_expr_ctx(operand, ctx)?;
5600 Ok(ir_is_not_null(inner))
5601 }
5602
5603 Expr::Path(p)
5605 if ctx.is_some() && p.partial && matches!(p.steps.first(), Some(ast::PathStep::Backlink(_))) =>
5606 {
5607 let (td, alias) = ctx.unwrap();
5608 let current_qname = format!("{}::{}", td.module, td.name);
5609 let exists = self.compile_backlink_as_exists(&p.steps, None, ¤t_qname, alias)?;
5610 Ok(exists)
5611 }
5612
5613 Expr::Path(p) if ctx.is_some() && p.partial && p.steps.len() == 1 => {
5617 let (td, alias) = ctx.unwrap();
5618 if let ast::PathStep::LinkProp(prop_name) = &p.steps[0] {
5623 let prop = self.compile_link_prop_ref(prop_name)?;
5624 return Ok(ir_is_not_null(prop));
5625 }
5626 if let ast::PathStep::Name(n) = &p.steps[0]
5630 && Self::resolve_property(td, n).is_none()
5631 && Self::resolve_link(td, n).is_none()
5632 && Self::resolve_multilink(td, n).is_none()
5633 && self.resolve_computed(td, n).is_none()
5634 && let Some(expr) = self
5635 .active_declared_pointers
5636 .iter()
5637 .find(|d| path_leaf(&d.path).is_ok_and(|name| name == n))
5638 .and_then(|d| d.compexpr.clone())
5639 {
5640 let inner = self.compile_expr(&expr, td, alias)?;
5641 return Ok(ir_is_not_null(inner));
5642 }
5643 if let ast::PathStep::TypeIntersection(type_ref) = &p.steps[0] {
5647 let narrowed = self.resolve_type(&type_ref.qualified_name())?;
5648 let check = format!("{}::{}", narrowed.module, narrowed.name);
5649 let source = format!("{}::{}", td.module, td.name);
5650 return Ok(self.type_check_bool_expr(&source, &check, td, alias));
5651 }
5652 let pointer_name = match &p.steps[0] {
5653 ast::PathStep::Name(n) => n.as_str(),
5654 _ => return Err(self.type_err("exists: invalid path step")),
5655 };
5656 if let Some(prop) = Self::resolve_property(td, pointer_name) {
5657 return Ok(ir_is_not_null(IrExpr::ColumnRef {
5658 alias: alias.to_string(),
5659 column: prop.name.clone(),
5660 pg_type: prop.pg_type.clone(),
5661 }));
5662 }
5663 if let Some(link) = Self::resolve_link(td, pointer_name) {
5664 if link.is_junction_backed() {
5665 return self.compile_junction_link_exists_check(link, td, alias);
5666 }
5667 return Ok(ir_is_not_null(IrExpr::ColumnRef {
5668 alias: alias.to_string(),
5669 column: format!("{}_id", link.name),
5670 pg_type: "uuid".to_string(),
5671 }));
5672 }
5673 if Self::resolve_multilink(td, pointer_name).is_some() {
5674 return self.compile_multilink_exists_check(pointer_name, td, alias);
5675 }
5676 if let Some(cd) = self.resolve_computed(td, pointer_name) {
5682 let expr_ast = crate::parse::parse_pointer_expr(&cd.expression).map_err(PyQLError::Syntax)?;
5683 return self.compile_exists_ctx(&expr_ast, ctx);
5684 }
5685 Err(self.field_err(pointer_name, &format!("{}::{}", td.module, td.name)))
5686 }
5687
5688 Expr::SubQuery(stmt)
5694 if ctx.is_some()
5695 && matches!(
5696 stmt.as_ref(),
5697 Stmt::Select(inner) if matches!(&inner.result, Expr::Path(p) if p.partial)
5698 ) =>
5699 {
5700 let Stmt::Select(inner) = stmt.as_ref() else {
5701 unreachable!("checked by the guard")
5702 };
5703 let Expr::Path(path) = &inner.result else {
5704 unreachable!("checked by the guard")
5705 };
5706 let (td, alias) = ctx.expect("checked by the guard");
5707 let mut steps = vec![ast::PathStep::Name(format!("{}::{}", td.module, td.name))];
5708 steps.extend(path.steps.iter().cloned());
5709 let full_path = ast::Path { steps, partial: false };
5710 let rooted = ast::SelectStmt {
5711 result: Expr::Path(full_path.clone()),
5712 filter: inner.filter.clone(),
5713 order_by: inner.order_by.clone(),
5714 offset: inner.offset.clone(),
5715 limit: inner.limit.clone(),
5716 lock: None,
5717 };
5718 let mut ps = self.compile_path_select(&rooted, &full_path, &[], false)?;
5719 Self::correlate_path_select(&mut ps, alias);
5720 Ok(IrExpr::UnaryOp(Box::new(IrUnaryOp {
5721 op: ast::UnaryOpKind::Exists,
5722 operand: IrExpr::PathSubquery(Box::new(ps)),
5723 })))
5724 }
5725
5726 Expr::SubQuery(stmt) => self.compile_subquery_exists(stmt),
5728
5729 operand if let Some(name) = self.resolve_cte_name(operand) => {
5732 let object = self.cte_types.get(name).is_some_and(|bound| bound.contains("::"));
5733 Ok(IrExpr::ExistsOverCte {
5734 cte: name.to_string(),
5735 column: (!object).then(|| "v".to_string()),
5736 })
5737 }
5738
5739 other => {
5745 let inner = self.compile_expr_ctx(other, ctx)?;
5746 if let IrExpr::SetOp { op, left, right, .. } = inner {
5749 return Ok(IrExpr::SetOp {
5750 op,
5751 left,
5752 right,
5753 mode: super::SetOpMode::Exists,
5754 });
5755 }
5756 Ok(ir_is_not_null(inner))
5757 }
5758 }
5759 }
5760
5761 fn compile_subquery_exists(&mut self, stmt: &Stmt) -> Result<IrExpr, PyQLError> {
5763 match self.compile_stmt(stmt)? {
5764 IrStmt::Select(s) => {
5765 let mut rows = s.rows;
5766 if rows.len() != 1 {
5767 return Err(self.type_err("exists requires a schema-bound SELECT expression"));
5768 }
5769 let IrRowSource::Bound { source, .. } = rows.remove(0) else {
5770 return Err(self.type_err("exists requires a schema-bound SELECT expression"));
5771 };
5772 let inner = IrExpr::Subquery(Box::new(IrSelect::schema_bound(source, vec![], s.filter)));
5773 Ok(IrExpr::UnaryOp(Box::new(IrUnaryOp {
5774 op: ast::UnaryOpKind::Exists,
5775 operand: inner,
5776 })))
5777 }
5778 IrStmt::PathSelect(ps) => {
5779 Ok(IrExpr::UnaryOp(Box::new(IrUnaryOp {
5782 op: ast::UnaryOpKind::Exists,
5783 operand: IrExpr::Subquery(Box::new(IrSelect::schema_bound(ps.root, vec![], ps.filter))),
5784 })))
5785 }
5786 _ => Err(self.type_err("exists requires a SELECT expression")),
5787 }
5788 }
5789
5790 fn junction_correlation_select(
5799 &mut self,
5800 td: &TypeDescriptor,
5801 alias: &str,
5802 name: &str,
5803 target: &str,
5804 through: &Option<String>,
5805 ) -> Result<IrSelect, PyQLError> {
5806 let jt_alias = self.fresh_alias();
5807 let (jt_table, jt_module, jt_src_col, _, _) = self.junction_info_for(td, name, target, through)?;
5808
5809 let filter = IrExpr::BinOp(Box::new(IrBinOp {
5810 left: IrExpr::ColumnRef {
5811 alias: jt_alias.clone(),
5812 column: jt_src_col,
5813 pg_type: "uuid".to_string(),
5814 },
5815 op: ast::BinOpKind::Eq,
5816 right: IrExpr::ColumnRef {
5817 alias: alias.to_string(),
5818 column: "id".to_string(),
5819 pg_type: "uuid".to_string(),
5820 },
5821 }));
5822 Ok(IrSelect::schema_bound(
5823 IrSource {
5824 poly: None,
5825 type_name: format!("{}::__jt__", jt_module),
5826 table: jt_table,
5827 alias: jt_alias,
5828 },
5829 vec![],
5830 Some(filter),
5831 ))
5832 }
5833
5834 fn multilink_correlation_select(
5835 &mut self,
5836 ml_name: &str,
5837 td: &TypeDescriptor,
5838 alias: &str,
5839 ) -> Result<IrSelect, PyQLError> {
5840 let ml = Self::resolve_multilink(td, ml_name).unwrap();
5841 let (name, target, through) = (ml.name.clone(), ml.target.clone(), ml.through.clone());
5842 self.junction_correlation_select(td, alias, &name, &target, &through)
5843 }
5844
5845 fn compile_multilink_exists_check(
5846 &mut self,
5847 ml_name: &str,
5848 td: &TypeDescriptor,
5849 alias: &str,
5850 ) -> Result<IrExpr, PyQLError> {
5851 let inner = self.multilink_correlation_select(ml_name, td, alias)?;
5852 Ok(IrExpr::UnaryOp(Box::new(IrUnaryOp {
5853 op: ast::UnaryOpKind::Exists,
5854 operand: IrExpr::Subquery(Box::new(inner)),
5855 })))
5856 }
5857
5858 fn compile_junction_link_exists_check(
5861 &mut self,
5862 l: &LinkDescriptor,
5863 td: &TypeDescriptor,
5864 alias: &str,
5865 ) -> Result<IrExpr, PyQLError> {
5866 let (name, target, through) = (l.name.clone(), l.target.clone(), l.through.clone());
5867 let inner = self.junction_correlation_select(td, alias, &name, &target, &through)?;
5868 Ok(IrExpr::UnaryOp(Box::new(IrUnaryOp {
5869 op: ast::UnaryOpKind::Exists,
5870 operand: IrExpr::Subquery(Box::new(inner)),
5871 })))
5872 }
5873
5874 fn junction_target_id_expr(
5881 &mut self,
5882 td: &TypeDescriptor,
5883 l: &LinkDescriptor,
5884 alias: &str,
5885 ) -> Result<IrExpr, PyQLError> {
5886 let (name, target, through) = (l.name.clone(), l.target.clone(), l.through.clone());
5887 let (jt_table, jt_module, jt_src_col, jt_tgt_col, _) = self.junction_info_for(td, &name, &target, &through)?;
5888 let jt_alias = self.fresh_alias();
5889 let filter = IrExpr::BinOp(Box::new(IrBinOp {
5890 left: IrExpr::ColumnRef {
5891 alias: jt_alias.clone(),
5892 column: jt_src_col,
5893 pg_type: "uuid".to_string(),
5894 },
5895 op: ast::BinOpKind::Eq,
5896 right: IrExpr::ColumnRef {
5897 alias: alias.to_string(),
5898 column: "id".to_string(),
5899 pg_type: "uuid".to_string(),
5900 },
5901 }));
5902 let select = IrSelect::schema_bound(
5903 IrSource {
5904 poly: None,
5905 type_name: format!("{}::__jt__", jt_module),
5906 table: jt_table,
5907 alias: jt_alias,
5908 },
5909 vec![IrShapePointer::Scalar(IrScalarPointer {
5910 implicit_id: false,
5911 marker_offset: None,
5912 alias: "target".to_string(),
5913 column: jt_tgt_col,
5914 pg_type: "uuid".to_string(),
5915 tuple_shape: None,
5916 })],
5917 Some(filter),
5918 );
5919 Ok(IrExpr::Subquery(Box::new(select)))
5920 }
5921
5922 fn check_union_type_compat(&self, expr: &Expr) -> Result<(), PyQLError> {
5926 let Expr::Union(a, b) = expr else { return Ok(()) };
5927 let a_free = self.is_free_result(a);
5928 let b_free = self.is_free_result(b);
5929 if a_free != b_free {
5930 let left = self.union_operand_type_display(a);
5931 let right = self.union_operand_type_display(b);
5932 return Err(PyQLError::Type(PyQLTypeError {
5933 message: format!(
5934 "operator 'UNION' cannot be applied to operands of type '{}' and '{}'",
5935 left, right,
5936 ),
5937 position: Position { line: 0, col: 0 },
5938 }));
5939 }
5940 Ok(())
5941 }
5942
5943 fn union_operand_type_display(&self, expr: &Expr) -> String {
5944 match expr {
5945 Expr::Path(p) if !p.partial && p.steps.len() == 1 => {
5946 if let ast::PathStep::Name(n) = &p.steps[0]
5947 && let Some(t) = self.cte_types.get(n.as_str())
5948 {
5949 if t.contains("::") {
5950 return t.clone(); }
5952 if !t.is_empty() {
5953 return pg_type_to_pyql(t).to_string(); }
5955 }
5956 if let Ok(td) = self.resolve_type(
5958 p.steps
5959 .first()
5960 .and_then(|s| {
5961 if let ast::PathStep::Name(n) = s {
5962 Some(n.as_str())
5963 } else {
5964 None
5965 }
5966 })
5967 .unwrap_or(""),
5968 ) {
5969 return format!("{}::{}", td.module, td.name);
5970 }
5971 }
5972 Expr::Literal(Literal::Int(_)) => return "std::int64".to_string(),
5973 Expr::Literal(Literal::Str(_)) => return "std::str".to_string(),
5974 Expr::Literal(Literal::Float(_)) => return "std::float64".to_string(),
5975 Expr::Literal(Literal::Bool(_)) => return "std::bool".to_string(),
5976 _ => {}
5977 }
5978 "unknown".to_string()
5979 }
5980
5981 fn is_free_result(&self, expr: &Expr) -> bool {
5982 let expr = match expr {
5983 Expr::UnaryOp(u) if u.op == ast::UnaryOpKind::Distinct => &u.operand,
5984 Expr::Detached(inner) => inner.as_ref(),
5985 other => other,
5986 };
5987 match expr {
5988 Expr::Path(p) if !p.partial => {
5989 if p.steps.len() == 1
5990 && let ast::PathStep::Name(n) = &p.steps[0]
5991 {
5992 if self.for_vars.contains_key(n.as_str()) {
5996 return !self.for_var_types.contains_key(n.as_str());
5997 }
5998 if self.is_value_binding(n.as_str()) {
6000 return true;
6001 }
6002 if self.fn_params.contains_key(n.as_str()) {
6007 return true;
6008 }
6009 }
6010 false
6011 }
6012 Expr::Shape(s) if s.expr.is_some() => false,
6013 Expr::SubQuery(_) => false,
6014 Expr::Union(a, b) => self.is_free_result(a) && self.is_free_result(b),
6015 Expr::IfElse(ie) => {
6020 let empty = |e: &Expr| matches!(e, Expr::Set(items) if items.is_empty());
6021 match (empty(&ie.if_expr), empty(&ie.else_expr)) {
6022 (true, true) => true,
6023 (true, false) => self.is_free_result(&ie.else_expr),
6024 (false, true) => self.is_free_result(&ie.if_expr),
6025 (false, false) => self.is_free_result(&ie.if_expr) || self.is_free_result(&ie.else_expr),
6026 }
6027 }
6028 _ => true,
6029 }
6030 }
6031
6032 fn is_free_cte_ref(&self, expr: &Expr) -> bool {
6037 let Expr::Path(p) = expr else { return false };
6038 if p.partial || p.steps.len() != 1 {
6039 return false;
6040 }
6041 let ast::PathStep::Name(n) = &p.steps[0] else {
6042 return false;
6043 };
6044 self.is_value_binding(n.as_str())
6045 }
6046
6047 fn dml_as_value(&mut self, stmt: &Stmt) -> Result<IrExpr, PyQLError> {
6055 let (cte_name, type_name) = self.hoist_dml_as_cte(stmt)?;
6056 let source = IrSource {
6060 poly: None,
6061 type_name,
6062 table: self.cte_source_table(&cte_name),
6063 alias: self.fresh_alias(),
6064 };
6065 Ok(IrExpr::Subquery(Box::new(IrSelect::schema_bound(
6066 source,
6067 vec![IrShapePointer::Scalar(IrScalarPointer {
6070 implicit_id: false,
6071 marker_offset: None,
6072 alias: "id".to_string(),
6073 column: "id".to_string(),
6074 pg_type: "uuid".to_string(),
6075 tuple_shape: None,
6076 })],
6077 None,
6078 ))))
6079 }
6080
6081 fn collect_union_items(&mut self, expr: &Expr, items: &mut Vec<IrFreeExpr>) -> Result<(), PyQLError> {
6082 match expr {
6083 Expr::Union(a, b) => {
6084 self.collect_union_items(a, items)?;
6085 self.collect_union_items(b, items)?;
6086 }
6087 Expr::Set(exprs) => {
6088 for e in exprs {
6089 self.collect_union_items(e, items)?;
6090 }
6091 }
6092 Expr::SubQuery(stmt) if matches!(stmt.as_ref(), Stmt::Insert(_) | Stmt::Update(_) | Stmt::Delete(_)) => {
6099 items.push(IrFreeExpr::Scalar(self.dml_as_value(stmt.as_ref())?));
6100 }
6101 other => {
6102 items.push(IrFreeExpr::Scalar(self.compile_free_expr(other)?));
6103 }
6104 }
6105 Ok(())
6106 }
6107
6108 fn shape_over_binding(&mut self, expr: &Expr) -> Result<Option<IrExpr>, PyQLError> {
6114 let Expr::Shape(sh) = expr else {
6115 return Ok(None);
6116 };
6117 let Some(Expr::Path(p)) = sh.expr.as_ref() else {
6118 return Ok(None);
6119 };
6120 let Some(ast::PathStep::Name(root)) = p.steps.first() else {
6121 return Ok(None);
6122 };
6123 if p.partial || (self.cte_object_type(root).is_none() && !self.for_var_types.contains_key(root)) {
6124 return Ok(None);
6125 }
6126 self.free_object_link_field(expr)
6127 }
6128
6129 fn free_object_link_field(&mut self, expr: &Expr) -> Result<Option<IrExpr>, PyQLError> {
6135 let Expr::Shape(sh) = expr else {
6136 return Ok(None);
6137 };
6138 let Some(Expr::Path(p)) = sh.expr.as_ref() else {
6139 return Ok(None);
6140 };
6141 if p.partial {
6142 return Ok(None);
6143 }
6144 let rest_steps = p.steps.as_slice();
6145 let Some(ast::PathStep::Name(name)) = rest_steps.first() else {
6146 return Ok(None);
6147 };
6148 let cte_name = self.cte_object_type(name).map(|_| name.clone());
6149 let Ok(td) = self.resolve_path_root(name) else {
6150 return Ok(None);
6151 };
6152 let table = self.row_source_table(name, td);
6153 if rest_steps.len() > 1 {
6157 let synthetic = ast::SelectStmt {
6158 result: Expr::Path(p.clone()),
6159 filter: None,
6160 order_by: vec![],
6161 offset: None,
6162 limit: None,
6163 lock: None,
6164 };
6165 let path_select = self.compile_path_select(&synthetic, p, &sh.elements, false)?;
6166 return Ok(Some(IrExpr::ObjectPathSubquery(Box::new(path_select))));
6167 }
6168 let alias = self.fresh_alias();
6169 let source = IrSource {
6170 poly: self.poly_fanout_for(&format!("{}::{}", td.module, td.name)),
6171 type_name: format!("{}::{}", td.module, td.name),
6172 table,
6173 alias: alias.clone(),
6174 };
6175 let outer_declared = std::mem::replace(
6178 &mut self.active_declared_pointers,
6179 cte_name
6180 .as_deref()
6181 .and_then(|n| self.cte_declared_pointers.get(n).cloned())
6182 .unwrap_or_default(),
6183 );
6184 let shape = self.compile_shape(&sh.elements, td, &alias, &td.module);
6185 self.active_declared_pointers = outer_declared;
6186 let filter = self.for_var_types.contains_key(name).then(|| {
6189 IrExpr::BinOp(Box::new(IrBinOp {
6190 left: IrExpr::ColumnRef {
6191 alias: alias.clone(),
6192 column: "id".to_string(),
6193 pg_type: "uuid".to_string(),
6194 },
6195 op: ast::BinOpKind::Eq,
6196 right: self.for_var_ref(name),
6197 }))
6198 });
6199 Ok(Some(IrExpr::ObjectSubquery(Box::new(IrSelect::schema_bound(
6200 source, shape?, filter,
6201 )))))
6202 }
6203
6204 fn free_object_field(&mut self, expr: &Expr) -> Result<IrExpr, PyQLError> {
6208 if let Some(object) = self.free_object_link_field(expr)? {
6209 return Ok(object);
6210 }
6211 match expr {
6212 Expr::SubQuery(stmt) if matches!(stmt.as_ref(), Stmt::Insert(_) | Stmt::Update(_) | Stmt::Delete(_)) => {
6213 self.dml_as_value(stmt.as_ref())
6214 }
6215 other => self.compile_free_expr(other),
6216 }
6217 }
6218
6219 fn compile_free_select(
6220 &mut self,
6221 sel: &ast::SelectStmt,
6222 result_expr: &Expr,
6223 distinct: bool,
6224 ) -> Result<IrSelect, PyQLError> {
6225 if let Expr::IfElse(ie) = result_expr {
6229 let is_empty_set = |expr: &Expr| matches!(expr, Expr::Set(items) if items.is_empty());
6230 let is_free_object = |expr: &Expr| matches!(expr, Expr::Shape(sh) if sh.expr.is_none());
6231 let guarded = if is_free_object(&ie.if_expr) && is_empty_set(&ie.else_expr) {
6232 Some((&ie.if_expr, false))
6233 } else if is_empty_set(&ie.if_expr) && is_free_object(&ie.else_expr) {
6234 Some((&ie.else_expr, true))
6235 } else {
6236 None
6237 };
6238 if let Some((object, negated)) = guarded {
6239 let condition = self.compile_free_expr(&ie.condition)?;
6240 let condition = if negated {
6241 IrExpr::UnaryOp(Box::new(IrUnaryOp {
6242 op: ast::UnaryOpKind::Not,
6243 operand: condition,
6244 }))
6245 } else {
6246 condition
6247 };
6248 let mut select = self.compile_free_select(sel, object, distinct)?;
6249 select.filter = and_conditions(select.filter, vec![condition]);
6250 return Ok(select);
6251 }
6252 }
6253 let items: Vec<IrFreeExpr> = match result_expr {
6254 Expr::Union(_, _) | Expr::Set(_) => {
6255 let mut union_items = vec![];
6256 self.collect_union_items(result_expr, &mut union_items)?;
6257 let mut first: Option<(String, String)> = None; for item in &union_items {
6260 if let IrFreeExpr::Scalar(expr) = item
6261 && let Some(t) = infer_ir_type(expr)
6262 {
6263 let t = t.to_string();
6264 if let Some((ft, fq)) = &first {
6265 if !types_compatible(ft, &t) {
6266 return Err(PyQLError::Type(PyQLTypeError {
6267 message: format!(
6268 "operator 'UNION' cannot be applied to operands of type '{}' and '{}'",
6269 fq,
6270 pg_type_to_pyql(&t),
6271 ),
6272 position: Position { line: 0, col: 0 },
6273 }));
6274 }
6275 } else {
6276 first = Some((t.clone(), pg_type_to_pyql(&t).to_string()));
6277 }
6278 }
6279 }
6280 union_items
6281 }
6282 Expr::Path(p) if !p.partial && p.steps.len() == 1 => {
6283 if let ast::PathStep::Name(n) = &p.steps[0] {
6284 if self
6285 .cte_types
6286 .get(n.as_str())
6287 .map(|t| !t.contains("::"))
6288 .unwrap_or(false)
6289 {
6290 vec![IrFreeExpr::CtePassthrough(n.clone())]
6291 } else {
6292 vec![IrFreeExpr::Scalar(self.compile_free_expr(result_expr)?)]
6293 }
6294 } else {
6295 vec![IrFreeExpr::Scalar(self.compile_free_expr(result_expr)?)]
6296 }
6297 }
6298 Expr::Shape(s) if s.expr.is_none() => {
6299 let fields = s
6300 .elements
6301 .iter()
6302 .map(|el| -> Result<(String, IrExpr), PyQLError> {
6303 let name = path_leaf(&el.path)?.to_string();
6304 let expr = el.compexpr.as_ref().ok_or_else(|| {
6305 self.type_err("free object field must have a value expression (':= expr')")
6306 })?;
6307 Ok((name, self.free_object_field(expr)?))
6308 })
6309 .collect::<Result<Vec<_>, _>>()?;
6310 vec![IrFreeExpr::FreeObject(fields)]
6311 }
6312 Expr::Tuple(exprs) => {
6313 let ir = exprs
6317 .iter()
6318 .map(|e| self.free_object_field(e))
6319 .collect::<Result<_, _>>()?;
6320 vec![IrFreeExpr::Tuple(ir)]
6321 }
6322 Expr::NamedTuple(fields) => {
6323 let ir = fields
6324 .iter()
6325 .map(|(name, e)| Ok((name.clone(), self.free_object_field(e)?)))
6326 .collect::<Result<Vec<_>, PyQLError>>()?;
6327 let holds_an_object = ir.iter().any(|(_, e)| match e {
6333 IrExpr::ObjectSubquery(_) | IrExpr::ObjectPathSubquery(_) => true,
6334 IrExpr::ArrayFromSelect(source) => match source.as_ref() {
6335 IrArraySource::ObjectSelect(_) | IrArraySource::ObjectFunction(_) | IrArraySource::Group(_) => {
6336 true
6337 }
6338 IrArraySource::PathSelect(ps) => matches!(ps.result, IrPathResult::Object { .. }),
6339 _ => false,
6340 },
6341 _ => false,
6342 });
6343 if holds_an_object {
6344 vec![IrFreeExpr::NamedTupleRow(ir)]
6345 } else {
6346 vec![IrFreeExpr::Scalar(IrExpr::NamedTuple {
6347 fields: ir,
6348 is_free_object: false,
6349 })]
6350 }
6351 }
6352 other => vec![IrFreeExpr::Scalar(self.compile_free_expr(other)?)],
6353 };
6354
6355 let order_by = sel
6356 .order_by
6357 .iter()
6358 .map(|s| self.compile_sort_ctx(s, None))
6359 .collect::<Result<Vec<_>, _>>()?;
6360
6361 let offset = sel.offset.as_ref().map(|e| self.compile_free_expr(e)).transpose()?;
6362 let limit = sel.limit.as_ref().map(|e| self.compile_free_expr(e)).transpose()?;
6363 let filter = sel.filter.as_ref().map(|f| self.compile_free_filter(f)).transpose()?;
6364
6365 Ok(IrSelect {
6366 rows: items.into_iter().map(IrRowSource::Free).collect(),
6367 filter,
6368 order_by,
6369 offset,
6370 limit,
6371 distinct,
6372 dml_source: None,
6373 polymorphic: false,
6374 poly_implementors: vec![],
6375 poly_columns: vec![],
6376 lock: None,
6377 })
6378 }
6379
6380 fn compile_free_filter(&mut self, filter: &Expr) -> Result<IrExpr, PyQLError> {
6388 self.as_condition(|c| c.compile_free_filter_inner(filter))
6389 }
6390
6391 fn compile_free_filter_inner(&mut self, filter: &Expr) -> Result<IrExpr, PyQLError> {
6392 let Some(root) = self.find_path_root_in_expr(filter) else {
6393 return self.compile_free_expr(filter);
6394 };
6395 let td = self.resolve_path_root(&root)?;
6398 let table = self.row_source_table(&root, td);
6399 let alias = self.fresh_alias();
6400 let source = IrSource {
6401 poly: None,
6402 type_name: format!("{}::{}", td.module, td.name),
6403 table,
6404 alias: alias.clone(),
6405 };
6406 let condition = self.compile_expr(&Self::rewrite_abs_to_partial(filter.clone(), &root), td, &alias)?;
6407 self.warnings.push(format!(
6408 "possibly more than one element returned by an expression in a FILTER clause \
6409 (every '{root}'); wrap with any() to make intent explicit",
6410 ));
6411 Ok(IrExpr::UnaryOp(Box::new(IrUnaryOp {
6412 op: ast::UnaryOpKind::Exists,
6413 operand: IrExpr::Subquery(Box::new(IrSelect::schema_bound(source, vec![], Some(condition)))),
6414 })))
6415 }
6416
6417 fn common_union_type(&self, branches: &[(String, String)]) -> Option<String> {
6427 let covers_all = |candidate: &String| {
6428 branches.iter().all(|(t, _)| {
6429 t == candidate
6430 || self
6431 .resolve_type(t)
6432 .map(|td| Self::is_or_implements(td, candidate))
6433 .unwrap_or(false)
6434 })
6435 };
6436 if let Some(branch) = branches.iter().map(|(t, _)| t.clone()).find(&covers_all) {
6437 return Some(branch);
6438 }
6439 let first = self.resolve_type(&branches.first()?.0).ok()?;
6444 first
6445 .interfaces
6446 .iter()
6447 .chain(first.parents.iter())
6448 .find(|ancestor| covers_all(ancestor))
6449 .cloned()
6450 }
6451
6452 fn object_union_branches(&self, expr: &Expr) -> Option<Vec<(String, String)>> {
6453 fn flatten<'e>(expr: &'e Expr, out: &mut Vec<&'e Expr>) {
6454 match expr {
6455 Expr::Union(a, b) => {
6456 flatten(a, out);
6457 flatten(b, out);
6458 }
6459 other => out.push(other),
6460 }
6461 }
6462 if !matches!(expr, Expr::Union(_, _)) {
6463 return None;
6464 }
6465 let mut operands: Vec<&Expr> = vec![];
6466 flatten(expr, &mut operands);
6467
6468 let mut branches: Vec<(String, String)> = vec![];
6469 for operand in operands {
6470 let Expr::Path(path) = operand else { return None };
6471 if path.partial || path.steps.len() != 1 {
6472 return None;
6473 }
6474 let ast::PathStep::Name(name) = &path.steps[0] else {
6475 return None;
6476 };
6477 match self.cte_object_type(name) {
6478 Some(qualified) => branches.push((qualified, self.cte_source_table(&self.cte_sql_name(name)))),
6479 None => match self.resolve_type(name) {
6480 Ok(td) => branches.push((format!("{}::{}", td.module, td.name), td.table.clone())),
6481 Err(_) => return None,
6482 },
6483 }
6484 }
6485 Some(branches)
6486 }
6487
6488 fn name_union_operands(&mut self, expr: &Expr) -> Result<Option<Expr>, PyQLError> {
6504 let Expr::Union(left, right) = expr else {
6505 return Ok(None);
6506 };
6507 let mut rewritten = false;
6508 let mut name_one = |compiler: &mut Self, operand: &Expr| -> Result<Expr, PyQLError> {
6509 match operand {
6510 Expr::Union(_, _) => match compiler.name_union_operands(operand)? {
6511 Some(inner) => {
6512 rewritten = true;
6513 Ok(inner)
6514 }
6515 None => Ok(operand.clone()),
6516 },
6517 Expr::SubQuery(stmt) => {
6518 let (cte_name, type_name) = compiler.hoist_dml_as_cte(stmt.as_ref())?;
6519 let Ok(td) = compiler.resolve_type(&type_name) else {
6523 return Ok(operand.clone());
6524 };
6525 compiler
6526 .cte_types
6527 .insert(cte_name.clone(), format!("{}::{}", td.module, td.name));
6528 rewritten = true;
6529 Ok(Expr::Path(ast::Path {
6530 steps: vec![ast::PathStep::Name(cte_name)],
6531 partial: false,
6532 }))
6533 }
6534 Expr::Path(path) if !path.partial && path.steps.len() > 1 => {
6538 let synthetic = Stmt::Select(ast::SelectStmt {
6539 result: operand.clone(),
6540 filter: None,
6541 order_by: vec![],
6542 offset: None,
6543 limit: None,
6544 lock: None,
6545 });
6546 let Ok((cte_name, type_name)) = compiler.hoist_dml_as_cte(&synthetic) else {
6547 return Ok(operand.clone());
6548 };
6549 let Ok(td) = compiler.resolve_type(&type_name) else {
6550 return Ok(operand.clone());
6551 };
6552 compiler
6553 .cte_types
6554 .insert(cte_name.clone(), format!("{}::{}", td.module, td.name));
6555 rewritten = true;
6556 Ok(Expr::Path(ast::Path {
6557 steps: vec![ast::PathStep::Name(cte_name)],
6558 partial: false,
6559 }))
6560 }
6561 other => Ok(other.clone()),
6562 }
6563 };
6564 let left = name_one(self, left)?;
6565 let right = name_one(self, right)?;
6566 if !rewritten {
6567 return Ok(None);
6568 }
6569 Ok(Some(Expr::Union(Box::new(left), Box::new(right))))
6570 }
6571
6572 fn set_mutation_guard(&mut self, branch: &Expr, condition: Expr) {
6582 let stmt = match branch {
6583 Expr::SubQuery(stmt) => Some(stmt.as_ref()),
6584 Expr::Shape(sh) => match sh.expr.as_ref() {
6585 Some(Expr::SubQuery(stmt)) => Some(stmt.as_ref()),
6586 _ => None,
6587 },
6588 _ => None,
6589 };
6590 match stmt {
6591 Some(Stmt::Update(_)) => self.pending_update_guard = Some(condition),
6592 Some(Stmt::Delete(_)) => self.pending_delete_guard = Some(condition),
6593 _ => self.pending_insert_guard = Some(condition),
6594 }
6595 }
6596
6597 fn object_if_else_as_union(&mut self, expr: &Expr) -> Option<Expr> {
6598 let Expr::IfElse(ie) = expr else {
6599 return None;
6600 };
6601 fn is_empty_set(expr: &Expr) -> bool {
6602 matches!(expr, Expr::Set(items) if items.is_empty())
6603 }
6604 let yields_objects = |branch: &Expr| match branch {
6605 Expr::Path(p) if !p.partial && p.steps.len() == 1 => match &p.steps[0] {
6606 ast::PathStep::Name(n) => self.cte_object_type(n).is_some() || self.resolve_type(n).is_ok(),
6607 _ => false,
6608 },
6609 Expr::SubQuery(stmt) => {
6610 if let Some(ast::SelectStmt {
6614 result: Expr::Path(path),
6615 ..
6616 }) = innermost_select(stmt.as_ref())
6617 && !path.partial
6618 && path.steps.len() > 1
6619 && let Some(ast::PathStep::Name(root)) = path.steps.first()
6620 && let Ok(root_td) = self.resolve_path_root(root)
6621 {
6622 return matches!(
6623 self.walk_path_types(root_td, &path.steps[1..], MAX_COMPUTED_SPLICES),
6624 (_, Some(_))
6625 );
6626 }
6627 matches!(
6628 self.dml_subject_type(stmt.as_ref()),
6629 Ok(name) if self.resolve_type(&name).is_ok()
6630 )
6631 }
6632 Expr::Shape(sh) => match sh.expr.as_ref() {
6634 Some(Expr::SubQuery(stmt)) => {
6635 matches!(self.dml_subject_type(stmt.as_ref()), Ok(name) if self.resolve_type(&name).is_ok())
6636 }
6637 Some(Expr::Path(p)) if !p.partial && p.steps.len() == 1 => match &p.steps[0] {
6638 ast::PathStep::Name(n) => self.cte_object_type(n).is_some() || self.resolve_type(n).is_ok(),
6639 _ => false,
6640 },
6641 _ => false,
6642 },
6643 _ => false,
6644 };
6645 fn mutating_stmt(branch: &Expr) -> Option<&Stmt> {
6656 match branch {
6657 Expr::SubQuery(stmt) => Some(stmt.as_ref()),
6658 Expr::Shape(sh) => match sh.expr.as_ref() {
6659 Some(Expr::SubQuery(stmt)) => Some(stmt.as_ref()),
6660 _ => None,
6661 },
6662 _ => None,
6663 }
6664 .filter(|stmt| matches!(stmt, Stmt::Insert(_) | Stmt::Update(_) | Stmt::Delete(_)))
6665 }
6666 let (if_yields_objects, else_yields_objects) = (yields_objects(&ie.if_expr), yields_objects(&ie.else_expr));
6671 let guard = |branch: &Expr, condition: Expr| {
6672 Expr::SubQuery(Box::new(Stmt::Select(ast::SelectStmt {
6673 result: branch.clone(),
6674 filter: Some(condition),
6675 order_by: vec![],
6676 offset: None,
6677 limit: None,
6678 lock: None,
6679 })))
6680 };
6681 let negated = Expr::UnaryOp(Box::new(ast::UnaryOp {
6682 op: ast::UnaryOpKind::Not,
6683 operand: ie.condition.clone(),
6684 }));
6685 let guarded_insert = |branch: &Expr, other: &Expr| {
6686 mutating_stmt(branch).is_some() && matches!(other, Expr::Set(items) if items.is_empty())
6687 };
6688 if guarded_insert(&ie.if_expr, &ie.else_expr) {
6689 self.set_mutation_guard(&ie.if_expr, ie.condition.clone());
6690 return Some(ie.if_expr.clone());
6691 }
6692 if guarded_insert(&ie.else_expr, &ie.if_expr) {
6693 let negated = Expr::UnaryOp(Box::new(ast::UnaryOp {
6694 op: ast::UnaryOpKind::Not,
6695 operand: ie.condition.clone(),
6696 }));
6697 self.set_mutation_guard(&ie.else_expr, negated);
6698 return Some(ie.else_expr.clone());
6699 }
6700 if let (Some(if_stmt), Some(else_stmt)) = (mutating_stmt(&ie.if_expr), mutating_stmt(&ie.else_expr))
6707 && std::mem::discriminant(if_stmt) != std::mem::discriminant(else_stmt)
6708 {
6709 self.set_mutation_guard(&ie.if_expr, ie.condition.clone());
6710 self.set_mutation_guard(&ie.else_expr, negated);
6711 return Some(Expr::Union(
6712 Box::new(ie.if_expr.clone()),
6713 Box::new(ie.else_expr.clone()),
6714 ));
6715 }
6716 let reads_objects = |branch: &Expr, yields: bool| mutating_stmt(branch).is_none() && yields;
6721 if mutating_stmt(&ie.if_expr).is_some() && reads_objects(&ie.else_expr, else_yields_objects) {
6722 self.set_mutation_guard(&ie.if_expr, ie.condition.clone());
6723 return Some(Expr::Union(
6724 Box::new(ie.if_expr.clone()),
6725 Box::new(guard(&ie.else_expr, negated)),
6726 ));
6727 }
6728 if mutating_stmt(&ie.else_expr).is_some() && reads_objects(&ie.if_expr, if_yields_objects) {
6729 self.set_mutation_guard(&ie.else_expr, negated);
6730 return Some(Expr::Union(
6731 Box::new(guard(&ie.if_expr, ie.condition.clone())),
6732 Box::new(ie.else_expr.clone()),
6733 ));
6734 }
6735 if mutating_stmt(&ie.if_expr).is_some() || mutating_stmt(&ie.else_expr).is_some() {
6736 return None;
6737 }
6738 let (if_empty, else_empty) = (is_empty_set(&ie.if_expr), is_empty_set(&ie.else_expr));
6739 if if_empty && else_empty {
6740 return None;
6741 }
6742 if !(if_empty || if_yields_objects) || !(else_empty || else_yields_objects) {
6743 return None;
6744 }
6745 if else_empty {
6746 return Some(guard(&ie.if_expr, ie.condition.clone()));
6747 }
6748 if if_empty {
6749 return Some(guard(&ie.else_expr, negated));
6750 }
6751 Some(Expr::Union(
6752 Box::new(guard(&ie.if_expr, ie.condition.clone())),
6753 Box::new(guard(&ie.else_expr, negated)),
6754 ))
6755 }
6756
6757 fn object_coalesce_as_if_else(expr: &Expr) -> Option<Expr> {
6762 fn as_if_else(b: &ast::BinOp) -> Option<Expr> {
6763 (b.op == ast::BinOpKind::Coalesce).then(|| {
6764 Expr::IfElse(Box::new(ast::IfElse {
6765 condition: Expr::UnaryOp(Box::new(ast::UnaryOp {
6766 op: ast::UnaryOpKind::Exists,
6767 operand: b.left.clone(),
6768 })),
6769 if_expr: b.left.clone(),
6770 else_expr: b.right.clone(),
6771 }))
6772 })
6773 }
6774 match expr {
6775 Expr::BinOp(b) => as_if_else(b),
6776 Expr::Shape(sh) => match sh.expr.as_ref() {
6777 Some(Expr::BinOp(b)) => Some(Expr::Shape(Box::new(ast::ShapeExpr {
6778 expr: Some(as_if_else(b)?),
6779 elements: sh.elements.clone(),
6780 marker_offset: sh.marker_offset,
6781 }))),
6782 _ => None,
6783 },
6784 _ => None,
6785 }
6786 }
6787
6788 fn try_compile_object_union_select(
6789 &mut self,
6790 sel: &ast::SelectStmt,
6791 result_expr: &Expr,
6792 distinct: bool,
6793 ) -> Result<Option<IrSelect>, PyQLError> {
6794 let coalesced;
6795 let result_expr = match Self::object_coalesce_as_if_else(result_expr) {
6796 Some(rewritten) => {
6797 coalesced = rewritten;
6798 &coalesced
6799 }
6800 None => result_expr,
6801 };
6802 let as_union;
6803 let (union_expr, shape_elements): (&Expr, &[ShapeElement]) = match result_expr {
6804 Expr::Union(_, _) => (result_expr, &[]),
6805 Expr::IfElse(_) => match self.object_if_else_as_union(result_expr) {
6806 Some(rewritten @ Expr::SubQuery(_)) => {
6809 return self.compile_select(sel, &rewritten, distinct).map(Some);
6810 }
6811 Some(rewritten) if !matches!(rewritten, Expr::Union(_, _)) => {
6812 return self.compile_select(sel, &rewritten, distinct).map(Some);
6813 }
6814 Some(rewritten) => {
6815 as_union = rewritten;
6816 (&as_union, &[] as &[ShapeElement])
6817 }
6818 None => return Ok(None),
6819 },
6820 Expr::Shape(shape) => match &shape.expr {
6821 Some(inner @ Expr::Union(_, _)) => (inner, shape.elements.as_slice()),
6822 Some(inner @ Expr::IfElse(_)) => match self.object_if_else_as_union(inner) {
6823 Some(Expr::SubQuery(stmt)) => {
6824 let shaped = Expr::Shape(Box::new(ast::ShapeExpr {
6825 expr: Some(Expr::SubQuery(stmt)),
6826 elements: shape.elements.clone(),
6827 marker_offset: None,
6828 }));
6829 return self.compile_select(sel, &shaped, distinct).map(Some);
6830 }
6831 Some(rewritten) if !matches!(rewritten, Expr::Union(_, _)) => {
6832 let shaped = match rewritten {
6833 Expr::Shape(_) => rewritten,
6834 other => Expr::Shape(Box::new(ast::ShapeExpr {
6835 expr: Some(other),
6836 elements: shape.elements.clone(),
6837 marker_offset: None,
6838 })),
6839 };
6840 return self.compile_select(sel, &shaped, distinct).map(Some);
6841 }
6842 Some(rewritten) => {
6843 as_union = rewritten;
6844 (&as_union, shape.elements.as_slice())
6845 }
6846 None => return Ok(None),
6847 },
6848 _ => return Ok(None),
6849 },
6850 _ => return Ok(None),
6851 };
6852
6853 let named;
6854 let union_expr = match self.name_union_operands(union_expr)? {
6855 Some(rewritten) => {
6856 named = rewritten;
6857 &named
6858 }
6859 None => union_expr,
6860 };
6861 let Some(branches) = self.object_union_branches(union_expr) else {
6862 return Ok(None);
6863 };
6864
6865 let Some(first_type) = self.common_union_type(&branches) else {
6866 let (first_type, _) = &branches[0];
6867 let (other, _) = branches
6868 .iter()
6869 .find(|(t, _)| t != first_type)
6870 .expect("no common type means at least two differ");
6871 return Err(self.type_err(&format!(
6872 "operator 'UNION' cannot be applied to operands of type '{first_type}' and '{other}'"
6873 )));
6874 };
6875 let first_type = &first_type;
6876 if sel.lock.is_some() {
6877 return Err(self.type_err(
6878 "FOR UPDATE/SHARE cannot be used on a UNION — its rows come from more than one \
6879 source, which a single locking clause can't target",
6880 ));
6881 }
6882 let td = self.resolve_type(first_type)?;
6883 let alias = self.fresh_alias();
6884
6885 self.anchors.push(SelectAnchor {
6886 type_name: td.name.clone(),
6887 qualified: format!("{}::{}", td.module, td.name),
6888 alias: alias.clone(),
6889 detached: std::mem::take(&mut self.pending_detached),
6890 declared_on: None,
6891 });
6892 let clauses = (|compiler: &mut Self| -> Result<_, PyQLError> {
6893 let shape = compiler.compile_shape(shape_elements, td, &alias, &td.module)?;
6894 let filter = sel
6895 .filter
6896 .as_ref()
6897 .map(|f| compiler.compile_expr(f, td, &alias))
6898 .transpose()?;
6899 let order_by = sel
6900 .order_by
6901 .iter()
6902 .map(|o| compiler.compile_sort(o, td, &alias))
6903 .collect::<Result<Vec<_>, _>>()?;
6904 let offset = sel
6905 .offset
6906 .as_ref()
6907 .map(|e| compiler.compile_expr(e, td, &alias))
6908 .transpose()?;
6909 let limit = sel
6910 .limit
6911 .as_ref()
6912 .map(|e| compiler.compile_expr(e, td, &alias))
6913 .transpose()?;
6914 Ok((shape, filter, order_by, offset, limit))
6915 })(self);
6916 self.anchors.pop();
6917 let (shape, mut filter, order_by, offset, limit) = clauses?;
6918 if let Some(var) = Self::subject_name(result_expr).filter(|n| self.for_var_types.contains_key(n)) {
6921 let narrowed = IrExpr::BinOp(Box::new(IrBinOp {
6922 left: IrExpr::ColumnRef {
6923 alias: alias.clone(),
6924 column: "id".to_string(),
6925 pg_type: "uuid".to_string(),
6926 },
6927 op: ast::BinOpKind::Eq,
6928 right: self.for_var_ref(&var),
6929 }));
6930 filter = and_conditions(filter, vec![narrowed]);
6931 }
6932
6933 let heterogeneous = branches.iter().any(|(t, _)| t != first_type);
6938 let common_columns = if heterogeneous {
6939 self.collect_poly_info(first_type).1
6940 } else {
6941 vec![]
6942 };
6943 let rows = branches
6944 .into_iter()
6945 .map(|(type_name, table)| IrRowSource::Bound {
6946 source: IrSource {
6947 poly: if heterogeneous {
6948 self.poly_fanout_for(&type_name)
6949 } else {
6950 None
6951 },
6952 type_name,
6953 table,
6954 alias: alias.clone(),
6955 },
6956 shape: shape.clone(),
6957 })
6958 .collect();
6959
6960 Ok(Some(IrSelect {
6961 rows,
6962 filter,
6963 order_by,
6964 offset,
6965 limit,
6966 distinct,
6967 dml_source: None,
6968 polymorphic: false,
6969 poly_implementors: vec![],
6970 poly_columns: common_columns,
6971 lock: None,
6972 }))
6973 }
6974
6975 fn subject_name(result_expr: &Expr) -> Option<String> {
6978 let path = match result_expr {
6979 Expr::Path(p) => p,
6980 Expr::Shape(sh) => match sh.expr.as_ref()? {
6981 Expr::Path(p) => p,
6982 _ => return None,
6983 },
6984 _ => return None,
6985 };
6986 if path.partial {
6987 return None;
6988 }
6989 match path.steps.as_slice() {
6990 [ast::PathStep::Name(n)] => Some(n.clone()),
6991 _ => None,
6992 }
6993 }
6994
6995 fn compile_select(
6996 &mut self,
6997 sel: &ast::SelectStmt,
6998 result_expr: &Expr,
6999 distinct: bool,
7000 ) -> Result<IrSelect, PyQLError> {
7001 if let Some(union_select) = self.try_compile_object_union_select(sel, result_expr, distinct)? {
7002 return Ok(union_select);
7003 }
7004 if let Some(rerooted) = self.bind_mutating_loop_source(result_expr)? {
7005 return self.compile_select(sel, &rerooted, distinct);
7006 }
7007 let (type_name, shape_elements, inner_stmt, cte_name) = self.extract_type_and_shape(result_expr)?;
7008 let td = self.resolve_type(&type_name)?;
7009 let alias = self.fresh_alias();
7010 let table = match cte_name {
7011 Some(ref cte) => self.cte_source_table(cte),
7012 None => Self::subject_name(result_expr)
7013 .map(|name| self.row_source_table(&name, td))
7014 .unwrap_or_else(|| td.table.clone()),
7015 };
7016 let source = IrSource {
7017 poly: self.poly_fanout_for(&format!("{}::{}", td.module, td.name)),
7018 type_name: format!("{}::{}", td.module, td.name),
7019 table,
7020 alias: alias.clone(),
7021 };
7022
7023 self.anchors.push(SelectAnchor {
7027 type_name: td.name.clone(),
7028 qualified: format!("{}::{}", td.module, td.name),
7029 alias: alias.clone(),
7030 detached: std::mem::take(&mut self.pending_detached),
7031 declared_on: None,
7032 });
7033 let declared = match cte_name.as_deref() {
7037 Some(name) => self.cte_declared_pointers.get(name).cloned().unwrap_or_default(),
7038 None => inner_stmt
7042 .and_then(innermost_select)
7043 .and_then(|inner| match &inner.result {
7044 Expr::Shape(sh) => Some(sh.elements.clone()),
7045 _ => None,
7046 })
7047 .unwrap_or_default(),
7048 };
7049 let splat_over_declared: Vec<ShapeElement>;
7051 let shape_elements = if shape_elements
7052 .iter()
7053 .any(|el| el.splat.is_some() && !matches!(el.path.steps.first(), Some(ast::PathStep::TypeIntersection(_))))
7054 {
7055 let named = |name: &str| {
7056 shape_elements
7057 .iter()
7058 .any(|el| el.splat.is_none() && path_leaf(&el.path).is_ok_and(|n| n == name))
7059 };
7060 let extra: Vec<ShapeElement> = declared
7061 .iter()
7062 .filter(|d| d.compexpr.is_some())
7063 .filter_map(|d| {
7064 let name = path_leaf(&d.path).ok()?;
7065 (!named(name)).then(|| ShapeElement {
7066 path: ast::Path::relative(name),
7067 splat: None,
7068 nested: None,
7069 compexpr: None,
7070 op: ast::ShapeOp::Assign,
7071 filter: None,
7072 order_by: vec![],
7073 offset: None,
7074 limit: None,
7075 marker_offset: None,
7076 })
7077 })
7078 .collect();
7079 splat_over_declared = shape_elements.iter().cloned().chain(extra).collect();
7080 &splat_over_declared[..]
7081 } else {
7082 shape_elements
7083 };
7084 let outer_declared = std::mem::replace(&mut self.active_declared_pointers, declared);
7085 let clauses = (|compiler: &mut Self| -> Result<_, PyQLError> {
7086 let shape = compiler.compile_shape(shape_elements, td, &alias, &td.module)?;
7087 compiler
7093 .active_declared_pointers
7094 .extend(shape_elements.iter().filter(|el| el.compexpr.is_some()).cloned());
7095 let filter = sel
7096 .filter
7097 .as_ref()
7098 .map(|f| compiler.as_condition(|c| c.compile_expr(f, td, &alias)))
7099 .transpose()?;
7100 let order_by = sel
7101 .order_by
7102 .iter()
7103 .map(|s| compiler.compile_sort(s, td, &alias))
7104 .collect::<Result<Vec<_>, _>>()?;
7105 let offset = sel
7106 .offset
7107 .as_ref()
7108 .map(|e| compiler.compile_expr(e, td, &alias))
7109 .transpose()?;
7110 let limit = sel
7111 .limit
7112 .as_ref()
7113 .map(|e| compiler.compile_expr(e, td, &alias))
7114 .transpose()?;
7115 Ok((shape, filter, order_by, offset, limit))
7116 })(self);
7117 self.anchors.pop();
7118 self.active_declared_pointers = outer_declared;
7119 let (shape, mut filter, order_by, offset, limit) = clauses?;
7120
7121 if let Some(var) = Self::subject_name(result_expr).filter(|n| self.for_var_types.contains_key(n)) {
7124 let narrowed = IrExpr::BinOp(Box::new(IrBinOp {
7125 left: IrExpr::ColumnRef {
7126 alias: alias.clone(),
7127 column: "id".to_string(),
7128 pg_type: "uuid".to_string(),
7129 },
7130 op: ast::BinOpKind::Eq,
7131 right: self.for_var_ref(&var),
7132 }));
7133 filter = and_conditions(filter, vec![narrowed]);
7134 }
7135
7136 let dml_source = inner_stmt.map(|s| self.compile_stmt(s).map(Box::new)).transpose()?;
7138 let mut shape = shape;
7139 if let Some(dml) = &dml_source {
7140 let dml_cte = cte_name.as_deref().unwrap_or(crate::sql::DML_CTE);
7144 Self::read_nested_links_from_their_ctes(dml, Some(dml_cte), &mut shape);
7145 }
7146
7147 let polymorphic = self.is_polymorphic(td);
7148 let (poly_implementors, poly_columns) = if polymorphic {
7149 let iface_qname = format!("{}::{}", td.module, td.name);
7150 (self.find_poly_implementors(&iface_qname), Self::poly_dml_columns(td))
7151 } else {
7152 (vec![], vec![])
7153 };
7154
7155 let lock = match &sel.lock {
7163 None => None,
7164 Some(lc) => {
7165 if distinct {
7166 return Err(self.type_err(
7167 "FOR UPDATE/SHARE cannot be combined with DISTINCT — Postgres can't \
7168 guarantee the result rows map 1:1 to physical table rows",
7169 ));
7170 }
7171 if polymorphic {
7172 return Err(self.type_err(
7173 "FOR UPDATE/SHARE cannot be used on an interface type — its rows span \
7174 multiple underlying tables, which a single locking clause can't target",
7175 ));
7176 }
7177 if dml_source.is_some() {
7178 return Err(self.type_err(
7179 "FOR UPDATE/SHARE cannot be used on SELECT (INSERT/UPDATE/DELETE …) — \
7180 there's nothing left to lock once the DML has already run",
7181 ));
7182 }
7183 Some(IrLockClause {
7184 strength: match lc.strength {
7185 ast::LockStrength::Update => IrLockStrength::Update,
7186 ast::LockStrength::NoKeyUpdate => IrLockStrength::NoKeyUpdate,
7187 ast::LockStrength::Share => IrLockStrength::Share,
7188 ast::LockStrength::KeyShare => IrLockStrength::KeyShare,
7189 },
7190 wait: match lc.wait {
7191 ast::LockWait::Block => IrLockWait::Block,
7192 ast::LockWait::NoWait => IrLockWait::NoWait,
7193 ast::LockWait::SkipLocked => IrLockWait::SkipLocked,
7194 },
7195 })
7196 }
7197 };
7198
7199 Ok(IrSelect {
7200 rows: vec![IrRowSource::Bound { source, shape }],
7201 filter,
7202 order_by,
7203 offset,
7204 limit,
7205 distinct,
7206 dml_source,
7207 polymorphic,
7208 poly_implementors,
7209 poly_columns,
7210 lock,
7211 })
7212 }
7213
7214 fn extract_type_and_shape<'e>(&self, expr: &'e Expr) -> Result<TypeAndShape<'e>, PyQLError> {
7219 match expr {
7220 Expr::Shape(s) => {
7221 let (type_name, cte_name, inner) = match s.expr.as_ref() {
7223 Some(Expr::SubQuery(stmt)) => (self.dml_subject_type(stmt)?, None, Some(stmt.as_ref())),
7224 Some(Expr::Path(p)) if !p.partial && p.steps.len() == 1 => {
7225 if let ast::PathStep::Name(n) = &p.steps[0] {
7226 if let Some(t) = self.for_var_types.get(n.as_str()) {
7227 (t.clone(), None, None)
7228 } else if let Some(t) = self.cte_types.get(n.as_str()) {
7229 if t.contains("::") {
7230 (t.clone(), Some(n.clone()), None)
7231 } else {
7232 (self.expr_as_type_name(s.expr.as_ref().unwrap())?, None, None)
7233 }
7234 } else {
7235 (self.expr_as_type_name(s.expr.as_ref().unwrap())?, None, None)
7236 }
7237 } else {
7238 (self.expr_as_type_name(s.expr.as_ref().unwrap())?, None, None)
7239 }
7240 }
7241 Some(inner) => (self.expr_as_type_name(inner)?, None, None),
7242 None => {
7243 return Err(PyQLError::Type(PyQLTypeError {
7244 message: "shape without subject expression".into(),
7245 position: Position { line: 0, col: 0 },
7246 }));
7247 }
7248 };
7249 Ok((type_name, &s.elements, inner, cte_name))
7250 }
7251 Expr::SubQuery(stmt) => Ok((self.dml_subject_type(stmt)?, &[], Some(stmt.as_ref()), None)),
7253 Expr::Path(p) if !p.partial && p.steps.len() == 1 => {
7255 if let ast::PathStep::Name(n) = &p.steps[0] {
7256 if let Some(t) = self.for_var_types.get(n.as_str()) {
7257 return Ok((t.clone(), &[], None, None));
7258 }
7259 if let Some(t) = self.cte_types.get(n.as_str())
7260 && t.contains("::")
7261 {
7262 return Ok((t.clone(), &[], None, Some(n.clone())));
7263 }
7264 }
7265 Ok((self.expr_as_type_name(expr)?, &[], None, None))
7266 }
7267 _ => Ok((self.expr_as_type_name(expr)?, &[], None, None)),
7268 }
7269 }
7270
7271 fn is_json_expr(&self, ast: &Expr, ir: &IrExpr) -> bool {
7275 if matches!(infer_ir_type(ir), Some("jsonb")) {
7276 return true;
7277 }
7278 let Expr::Path(path) = ast else {
7279 return false;
7280 };
7281 if path.partial || path.steps.len() != 1 {
7282 return false;
7283 }
7284 let ast::PathStep::Name(name) = &path.steps[0] else {
7285 return false;
7286 };
7287 self.for_vars.get(name.as_str()).is_some_and(|t| t == "jsonb")
7288 }
7289
7290 fn set_returning_element_type(&mut self, expr: &Expr) -> Result<Option<String>, PyQLError> {
7296 if !expr_returns_set(expr) {
7297 return Ok(None);
7298 }
7299 let Expr::FunctionCall(call) = expr else {
7300 return Ok(None);
7301 };
7302 let mut declared = crate::stdlib::lookup(call.module.as_deref().unwrap_or("std"), &call.name)
7305 .into_iter()
7306 .map(|descriptor| match &descriptor.return_type {
7307 crate::stdlib::PylonType::Set(element) => element.scalar_pg_type(),
7308 _ => None,
7309 });
7310 if let Some(element) = declared.next().flatten()
7311 && declared.all(|other| other == Some(element))
7312 {
7313 return Ok(Some(element.to_string()));
7314 }
7315 let [argument] = call.args.as_slice() else {
7316 return Ok(None);
7317 };
7318 let compiled = self.compile_free_expr(argument)?;
7319 let Some(array_type) = infer_ir_type(&compiled) else {
7320 return Ok(None);
7321 };
7322 Ok(array_type
7323 .strip_suffix("[]")
7324 .map(|element| literal_sentinel_to_pg(element).to_string()))
7325 }
7326
7327 fn compile_for(&mut self, f: &ast::ForStmt) -> Result<IrFor, PyQLError> {
7328 let iterated_binding = self.resolve_cte_name(&f.iterator).map(|name| self.cte_sql_name(name));
7331 let (iterator, pg_type, yielded_object_type) = match &f.iterator {
7333 Expr::Set(elems) => {
7334 let compiled: Result<Vec<_>, _> = elems.iter().map(|e| self.compile_free_expr(e)).collect();
7335 let exprs = compiled?;
7336 let raw = exprs.first().and_then(|e| infer_ir_type(e)).unwrap_or("text");
7337 let pg_type = literal_sentinel_to_pg(raw).to_string();
7338 (
7339 IrForIterator::Values {
7340 exprs,
7341 pg_type: pg_type.clone(),
7342 },
7343 pg_type,
7344 None,
7345 )
7346 }
7347 Expr::SubQuery(stmt) => {
7350 let inner = self.compile_stmt(stmt)?;
7351 if !matches!(inner, IrStmt::Select(_) | IrStmt::PathSelect(_)) {
7352 return Err(self.type_err(
7353 "for-loop iterator: only a select can be iterated over — \
7354 bind the statement in a `with` first",
7355 ));
7356 }
7357 let yielded = cte_stmt_type(&inner);
7358 let scalar = !yielded.contains("::");
7359 let pg_type = if scalar {
7360 let raw = if yielded.is_empty() { "text" } else { yielded.as_str() };
7361 literal_sentinel_to_pg(raw).to_string()
7362 } else {
7363 "uuid".to_string()
7364 };
7365 (
7366 IrForIterator::Query {
7367 stmt: Box::new(inner),
7368 scalar,
7369 },
7370 pg_type,
7371 (!scalar).then_some(yielded),
7372 )
7373 }
7374 Expr::Path(p)
7378 if !p.partial
7379 && p.steps.len() == 1
7380 && matches!(&p.steps[0], ast::PathStep::Name(n) if self.cte_types.contains_key(n.as_str())) =>
7381 {
7382 let ast::PathStep::Name(name) = &p.steps[0] else {
7383 unreachable!("checked by the guard")
7384 };
7385 let yielded = self.cte_types.get(name.as_str()).cloned().unwrap_or_default();
7386 let scalar = !yielded.contains("::");
7387 let pg_type = if scalar {
7388 let raw = if yielded.is_empty() { "text" } else { yielded.as_str() };
7389 literal_sentinel_to_pg(raw).to_string()
7390 } else {
7391 "uuid".to_string()
7392 };
7393 let source = IrSource {
7394 poly: None,
7395 type_name: yielded.clone(),
7396 table: self.cte_source_table(&self.cte_sql_name(name)),
7397 alias: self.fresh_alias(),
7398 };
7399 (
7400 IrForIterator::Query {
7401 stmt: Box::new(IrStmt::Select(IrSelect::schema_bound(source, vec![], None))),
7402 scalar,
7403 },
7404 pg_type,
7405 (!scalar).then_some(yielded),
7406 )
7407 }
7408 Expr::Path(p) if !p.partial && p.steps.len() > 1 => {
7412 let synthetic = ast::SelectStmt {
7413 result: Expr::Path(p.clone()),
7414 filter: None,
7415 order_by: vec![],
7416 offset: None,
7417 limit: None,
7418 lock: None,
7419 };
7420 let inner = self.compile_stmt(&Stmt::Select(synthetic))?;
7421 let yielded = cte_stmt_type(&inner);
7422 let scalar = !yielded.contains("::");
7423 let pg_type = if scalar {
7424 let raw = if yielded.is_empty() { "text" } else { yielded.as_str() };
7425 literal_sentinel_to_pg(raw).to_string()
7426 } else {
7427 "uuid".to_string()
7428 };
7429 (
7430 IrForIterator::Query {
7431 stmt: Box::new(inner),
7432 scalar,
7433 },
7434 pg_type,
7435 (!scalar).then_some(yielded),
7436 )
7437 }
7438 other => {
7439 let e = self.compile_free_expr(other)?;
7440 let raw = match self.set_returning_element_type(other)? {
7444 Some(element) => element,
7445 None => literal_sentinel_to_pg(infer_ir_type(&e).unwrap_or("text")).to_string(),
7446 };
7447 let pg_type = raw;
7448 let iterator = if expr_returns_set(other) {
7449 IrForIterator::SetReturning {
7450 expr: e,
7451 pg_type: pg_type.clone(),
7452 }
7453 } else {
7454 IrForIterator::Values {
7455 exprs: vec![e],
7456 pg_type: pg_type.clone(),
7457 }
7458 };
7459 (iterator, pg_type, None)
7460 }
7461 };
7462
7463 let slot = self
7467 .claim_generated_cte_name(&format!("_for_{}", f.var))
7468 .strip_prefix("_for_")
7469 .unwrap_or(&f.var)
7470 .to_string();
7471 let prev_slot = self.for_var_slots.insert(f.var.clone(), slot.clone());
7472 let prev = self.for_vars.insert(f.var.clone(), pg_type.clone());
7474 let prev_cte = match iterated_binding {
7475 Some(name) => self.for_var_ctes.insert(f.var.clone(), name),
7476 None => self.for_var_ctes.remove(&f.var),
7477 };
7478 let prev_type = match yielded_object_type {
7479 Some(qualified) => self.for_var_types.insert(f.var.clone(), qualified),
7480 None => self.for_var_types.remove(&f.var),
7481 };
7482 let hoisted_before = self.hoisted_ctes.len();
7483 self.for_scope.push(slot.clone());
7484 let body = self.compile_stmt(&f.body);
7485 self.for_scope.pop();
7486 let body = body?;
7487 let body_ctes: Vec<IrCteDef> = self.hoisted_ctes.split_off(hoisted_before);
7488 match prev {
7490 Some(old) => {
7491 self.for_vars.insert(f.var.clone(), old);
7492 }
7493 None => {
7494 self.for_vars.remove(&f.var);
7495 }
7496 }
7497 match prev_type {
7498 Some(old) => {
7499 self.for_var_types.insert(f.var.clone(), old);
7500 }
7501 None => {
7502 self.for_var_types.remove(&f.var);
7503 }
7504 }
7505 match prev_cte {
7506 Some(old) => {
7507 self.for_var_ctes.insert(f.var.clone(), old);
7508 }
7509 None => {
7510 self.for_var_ctes.remove(&f.var);
7511 }
7512 }
7513 match prev_slot {
7514 Some(old) => {
7515 self.for_var_slots.insert(f.var.clone(), old);
7516 }
7517 None => {
7518 self.for_var_slots.remove(&f.var);
7519 }
7520 }
7521
7522 let body_kind = match &body {
7527 IrStmt::Insert(_) | IrStmt::Select(_) | IrStmt::PathSelect(_) => None,
7528 IrStmt::Update(upd)
7536 if upd.assignments.is_empty()
7537 && upd.rewrites.is_empty()
7538 && !upd.multi_link_appends.is_empty()
7539 && upd
7540 .multi_link_appends
7541 .iter()
7542 .all(|a| crate::sql::append_value_is_the_loop_variable(&a.values, &f.var))
7543 && upd.multi_link_clears.is_empty()
7544 && upd.multi_link_replaces.is_empty()
7545 && upd.multi_link_removals.is_empty()
7546 && upd.poly_implementors.is_empty() =>
7547 {
7548 None
7549 }
7550 IrStmt::Update(upd)
7553 if upd.multi_link_appends.is_empty()
7554 && upd.multi_link_clears.is_empty()
7555 && upd.multi_link_replaces.is_empty()
7556 && upd.multi_link_removals.is_empty() =>
7557 {
7558 None
7559 }
7560 IrStmt::Update(_) if std::env::var("PYLON_DBG_FORUPD").is_ok() => None,
7561 IrStmt::Update(upd)
7566 if upd.assignments.is_empty()
7567 && upd.rewrites.is_empty()
7568 && !upd.multi_link_appends.is_empty()
7569 && upd.multi_link_clears.is_empty()
7570 && upd.multi_link_replaces.is_empty()
7571 && upd.multi_link_removals.is_empty()
7572 && upd.poly_implementors.is_empty()
7573 && upd
7574 .multi_link_appends
7575 .iter()
7576 .all(|a| crate::sql::per_iteration_insert(a, &upd.nested_ctes).is_some()) =>
7577 {
7578 None
7579 }
7580 IrStmt::Update(_) => Some("update"),
7581 IrStmt::Delete(_) => Some("delete"),
7582 IrStmt::For(inner) if matches!(inner.body.as_ref(), IrStmt::Insert(_)) => None,
7587 IrStmt::For(_) => Some("nested for"),
7588 IrStmt::Group(_) => Some("group"),
7589 _ => Some("this statement"),
7590 };
7591 if let Some(kind) = body_kind {
7592 return Err(self.type_err(&format!(
7593 "for-loop body: {kind} is not supported as a `for` body — use insert or select"
7594 )));
7595 }
7596
7597 Ok(IrFor {
7598 var_name: slot,
7599 iterator,
7600 body: Box::new(body),
7601 body_ctes,
7602 })
7603 }
7604
7605 fn compile_group(&mut self, g: &ast::GroupStmt) -> Result<IrGroup, PyQLError> {
7606 let mut walked: Option<&ast::Path> = None;
7613 let (type_name, cte_name) = match &g.subject {
7614 Expr::Path(p) if !p.partial => match p.steps.as_slice() {
7615 [ast::PathStep::Name(n)] => {
7616 if let Some(t) = self.cte_types.get(n.as_str()) {
7617 (t.clone(), Some(n.clone()))
7618 } else {
7619 (n.clone(), None)
7620 }
7621 }
7622 [ast::PathStep::Name(root), rest @ ..]
7623 if !rest.is_empty()
7624 && let Ok(root_td) = self.resolve_path_root(root)
7625 && let (_, Some(target)) = self.walk_path_types(root_td, rest, MAX_COMPUTED_SPLICES) =>
7626 {
7627 walked = Some(p);
7628 (format!("{}::{}", target.module, target.name), None)
7629 }
7630 _ => {
7631 return Err(PyQLError::Type(PyQLTypeError {
7632 message: format!("unsupported group subject: {:?}", g.subject),
7633 position: Position { line: 0, col: 0 },
7634 }));
7635 }
7636 },
7637 _ => {
7638 return Err(PyQLError::Type(PyQLTypeError {
7639 message: "group subject must be a type name".to_string(),
7640 position: Position { line: 0, col: 0 },
7641 }));
7642 }
7643 };
7644
7645 let td = self.resolve_type(&type_name)?;
7646 let alias = self.fresh_alias();
7647 let fq_type_name = format!("{}::{}", td.module, td.name);
7648 let table = match cte_name {
7649 Some(ref cte) => self.cte_source_table(cte),
7650 None => td.table.clone(),
7651 };
7652 let source = IrSource {
7653 poly: self.poly_fanout_for(&fq_type_name),
7654 type_name: fq_type_name.clone(),
7655 table,
7656 alias: alias.clone(),
7657 };
7658 let module = td.module.clone();
7659
7660 let shape = self.compile_shape(g.shape.as_deref().unwrap_or(&[]), td, &alias, &module)?;
7662
7663 let td = self.resolve_type(&type_name)?;
7665 let mut using_map: HashMap<String, IrExpr> = HashMap::new();
7666 for (alias_name, expr) in &g.using {
7667 let ir = self.compile_expr(expr, td, &alias)?;
7668 using_map.insert(alias_name.clone(), ir);
7669 }
7670
7671 let mut keys: Vec<(String, IrExpr)> = vec![];
7673 let td = self.resolve_type(&type_name)?;
7674 for by_expr in &g.by {
7675 match by_expr {
7676 Expr::Path(p) if p.partial && p.steps.len() == 1 => {
7677 if let ast::PathStep::Name(prop) = &p.steps[0] {
7678 let ir = self.compile_expr(by_expr, td, &alias)?;
7680 keys.push((prop.clone(), ir));
7681 } else {
7682 return Err(PyQLError::Type(PyQLTypeError {
7683 message: "group by path must be a simple property".to_string(),
7684 position: Position { line: 0, col: 0 },
7685 }));
7686 }
7687 }
7688 Expr::Path(p) if !p.partial && p.steps.len() == 1 => {
7689 if let ast::PathStep::Name(name) = &p.steps[0] {
7690 let ir = using_map.get(name).ok_or_else(|| {
7692 PyQLError::Type(PyQLTypeError {
7693 message: format!("group by references unknown alias '{}'", name),
7694 position: Position { line: 0, col: 0 },
7695 })
7696 })?;
7697 keys.push((name.clone(), ir.clone()));
7698 } else {
7699 return Err(PyQLError::Type(PyQLTypeError {
7700 message: "group by identifier must be a simple name".to_string(),
7701 position: Position { line: 0, col: 0 },
7702 }));
7703 }
7704 }
7705 _ => {
7706 return Err(PyQLError::Type(PyQLTypeError {
7707 message: format!("unsupported group by expression: {:?}", by_expr),
7708 position: Position { line: 0, col: 0 },
7709 }));
7710 }
7711 }
7712 }
7713
7714 let td = self.resolve_type(&type_name)?;
7718 let synthetic = ast::SelectStmt {
7719 result: g.subject.clone(),
7720 filter: g.filter.clone(),
7721 order_by: g.order_by.clone(),
7722 offset: g.offset.clone(),
7723 limit: g.limit.clone(),
7724 lock: None,
7725 };
7726 let (filter, order_by, offset, limit) = self.compile_path_modifiers(&synthetic, td, &alias)?;
7727 let filter = match walked {
7730 Some(subject) => {
7731 let subject = subject.clone();
7732 let rows = self.compile_subject_path_rows(&subject, &alias)?;
7733 and_conditions(filter, vec![rows])
7734 }
7735 None => filter,
7736 };
7737
7738 Ok(IrGroup {
7739 source,
7740 shape,
7741 keys,
7742 filter,
7743 order_by,
7744 offset,
7745 limit,
7746 output: IrGroupOutput::Groups,
7747 })
7748 }
7749
7750 fn bind_group(&mut self, name: &str, expr: &Expr) -> Result<bool, PyQLError> {
7751 let Expr::SubQuery(stmt) = expr else {
7752 return Ok(false);
7753 };
7754 let Stmt::Group(g) = stmt.as_ref() else {
7755 return Ok(false);
7756 };
7757 let grp = self.compile_group(g)?;
7758 self.group_bindings.insert(name.to_string(), grp);
7759 Ok(true)
7760 }
7761
7762 fn compile_group_projection(
7768 &mut self,
7769 sel: &ast::SelectStmt,
7770 g: &ast::GroupStmt,
7771 elements: &[ShapeElement],
7772 ) -> Result<IrGroup, PyQLError> {
7773 if sel.filter.is_some() {
7774 return Err(self.type_err("a shape over a `group` does not support filter"));
7775 }
7776 if !g.order_by.is_empty() || g.offset.is_some() || g.limit.is_some() {
7777 return Err(self.type_err(
7778 "a `group` read through a shape does not support order by, offset or limit on its elements",
7779 ));
7780 }
7781 let mut grp = self.compile_group(g)?;
7782 let keys: Vec<String> = grp.keys.iter().map(|(name, _)| name.clone()).collect();
7786 for (name, key) in &grp.keys {
7787 self.inline_bindings.insert(group_key_binding(name), key.clone());
7788 }
7789 let compiled = self.compile_group_projection_parts(sel, &grp, &keys, elements);
7790 for name in &keys {
7791 self.inline_bindings.remove(&group_key_binding(name));
7792 }
7793 let (pointers, order_by, offset, limit) = compiled?;
7794 grp.output = IrGroupOutput::Projection(Box::new(IrGroupProjection {
7795 pointers,
7796 order_by,
7797 offset,
7798 limit,
7799 }));
7800 Ok(grp)
7801 }
7802
7803 #[allow(clippy::type_complexity)]
7804 fn compile_group_projection_parts(
7805 &mut self,
7806 sel: &ast::SelectStmt,
7807 grp: &IrGroup,
7808 keys: &[String],
7809 elements: &[ShapeElement],
7810 ) -> Result<(Vec<IrShapePointer>, Vec<IrSort>, Option<IrExpr>, Option<IrExpr>), PyQLError> {
7811 let td = self.resolve_type(&grp.source.type_name)?;
7812 let mut rewritten = Vec::with_capacity(elements.len());
7813 for element in elements {
7814 let (Some(compexpr), [ast::PathStep::Name(_)]) = (&element.compexpr, element.path.steps.as_slice()) else {
7815 return Err(self.type_err(
7816 "only computed pointers (`name := …`) can be read off a `group` — e.g. `k := .key.name`",
7817 ));
7818 };
7819 let mut element = element.clone();
7820 element.compexpr = Some(self.rewrite_group_refs(compexpr, keys, td)?);
7821 rewritten.push(element);
7822 }
7823 let alias = grp.source.alias.clone();
7824 let module = td.module.clone();
7825 let pointers = self.without_implicit_id(|this| this.compile_shape(&rewritten, td, &alias, &module))?;
7829 let order_by = sel
7830 .order_by
7831 .iter()
7832 .map(|sort| {
7833 Ok(ast::SortExpr {
7834 expr: self.rewrite_group_refs(&sort.expr, keys, td)?,
7835 direction: sort.direction.clone(),
7836 nones: sort.nones.clone(),
7837 })
7838 })
7839 .collect::<Result<Vec<_>, PyQLError>>()?;
7840 let synthetic = ast::SelectStmt {
7841 result: Expr::Path(ast::Path::absolute(grp.source.type_name.clone())),
7842 filter: None,
7843 order_by,
7844 offset: sel.offset.clone(),
7845 limit: sel.limit.clone(),
7846 lock: None,
7847 };
7848 let (_, order_by, offset, limit) = self.compile_path_modifiers(&synthetic, td, &alias)?;
7849 Ok((pointers, order_by, offset, limit))
7850 }
7851
7852 fn rewrite_group_refs(&self, expr: &Expr, keys: &[String], td: &TypeDescriptor) -> Result<Expr, PyQLError> {
7853 const AGGREGATES: &[&str] = &["count", "sum", "min", "max", "avg", "array_agg", "all", "any"];
7854 let rewrite = |e: &Expr| self.rewrite_group_refs(e, keys, td);
7855 Ok(match expr {
7856 Expr::Path(p) if p.partial => {
7857 let name = match p.steps.first() {
7858 Some(ast::PathStep::Name(name)) => name.as_str(),
7859 _ => "",
7860 };
7861 match (name, p.steps.get(1..).unwrap_or_default()) {
7862 ("key", [ast::PathStep::Name(key)]) if keys.contains(key) => {
7863 Expr::Path(ast::Path::absolute(group_key_binding(key)))
7864 }
7865 ("key", [ast::PathStep::Name(key)]) => {
7866 return Err(self.type_err(&format!("'{key}' is not a key of this group")));
7867 }
7868 ("elements", _) => {
7869 return Err(self.type_err(
7870 "`.elements` of a group can only be read through an aggregate over one \
7871 of their properties — e.g. `count(.elements)`, `sum(.elements.amount)`",
7872 ));
7873 }
7874 _ => {
7875 return Err(self.type_err(&format!(
7876 "a group has no pointer '{name}' — read `.key.<name>` or aggregate over `.elements`"
7877 )));
7878 }
7879 }
7880 }
7881 Expr::FunctionCall(f)
7882 if f.args.len() == 1
7883 && f.kwargs.is_empty()
7884 && f.module.as_deref().is_none_or(|m| m == "std")
7885 && AGGREGATES.contains(&f.name.as_str())
7886 && let Expr::Path(p) = &f.args[0]
7887 && p.partial
7888 && matches!(p.steps.first(), Some(ast::PathStep::Name(n)) if n == "elements") =>
7889 {
7890 let property = match p.steps.get(1..).unwrap_or_default() {
7891 [] => "id",
7892 [ast::PathStep::Name(prop)] if td.properties.iter().any(|d| &d.name == prop) => prop.as_str(),
7893 _ => {
7894 return Err(self.type_err(&format!(
7895 "'{}' over a group's elements only reads one of their own properties",
7896 f.name
7897 )));
7898 }
7899 };
7900 Expr::FunctionCall(ast::FunctionCall {
7901 module: f.module.clone(),
7902 name: f.name.clone(),
7903 args: vec![Expr::Path(ast::Path::relative(property))],
7904 kwargs: vec![],
7905 })
7906 }
7907 Expr::FunctionCall(f) => Expr::FunctionCall(ast::FunctionCall {
7908 module: f.module.clone(),
7909 name: f.name.clone(),
7910 args: f.args.iter().map(rewrite).collect::<Result<_, _>>()?,
7911 kwargs: f
7912 .kwargs
7913 .iter()
7914 .map(|(k, v)| Ok((k.clone(), rewrite(v)?)))
7915 .collect::<Result<_, PyQLError>>()?,
7916 }),
7917 Expr::BinOp(b) => Expr::BinOp(Box::new(ast::BinOp {
7918 left: rewrite(&b.left)?,
7919 op: b.op.clone(),
7920 right: rewrite(&b.right)?,
7921 })),
7922 Expr::UnaryOp(u) => Expr::UnaryOp(Box::new(ast::UnaryOp {
7923 op: u.op.clone(),
7924 operand: rewrite(&u.operand)?,
7925 })),
7926 Expr::TypeCast(c) => Expr::TypeCast(Box::new(ast::TypeCast {
7927 expr: rewrite(&c.expr)?,
7928 ty: c.ty.clone(),
7929 })),
7930 Expr::IfElse(ie) => Expr::IfElse(Box::new(ast::IfElse {
7931 if_expr: rewrite(&ie.if_expr)?,
7932 condition: rewrite(&ie.condition)?,
7933 else_expr: rewrite(&ie.else_expr)?,
7934 })),
7935 other => other.clone(),
7936 })
7937 }
7938
7939 fn compile_group_elements(&mut self, f: &ast::ForStmt) -> Result<Option<IrGroup>, PyQLError> {
7942 let Stmt::Select(body) = f.body.as_ref() else {
7943 return Ok(None);
7944 };
7945 let unmodified =
7946 body.filter.is_none() && body.order_by.is_empty() && body.offset.is_none() && body.limit.is_none();
7947 let (inner, shape) = match &body.result {
7948 Expr::Shape(sh) if unmodified => match &sh.expr {
7949 Some(Expr::SubQuery(stmt)) => match stmt.as_ref() {
7950 Stmt::Select(inner) => (inner, Some(sh.elements.as_slice())),
7951 _ => return Ok(None),
7952 },
7953 _ => return Ok(None),
7954 },
7955 _ => (body, None),
7956 };
7957 let reads_elements = matches!(&inner.result, Expr::Path(p) if !p.partial
7958 && matches!(p.steps.as_slice(), [ast::PathStep::Name(var), ast::PathStep::Name(elements)]
7959 if var == &f.var && elements == "elements"));
7960 if !reads_elements {
7961 return Ok(None);
7962 }
7963 let mut grp = match &f.iterator {
7964 Expr::SubQuery(stmt) => match stmt.as_ref() {
7965 Stmt::Group(g) => self.compile_group(g)?,
7966 _ => return Ok(None),
7967 },
7968 Expr::Path(p) if !p.partial => match p.steps.as_slice() {
7969 [ast::PathStep::Name(name)] => match self.group_bindings.get(name) {
7970 Some(grp) => grp.clone(),
7971 None => return Ok(None),
7972 },
7973 _ => return Ok(None),
7974 },
7975 _ => return Ok(None),
7976 };
7977 let ordered = !inner.order_by.is_empty() || inner.offset.is_some() || inner.limit.is_some();
7978 if ordered && (!grp.order_by.is_empty() || grp.offset.is_some() || grp.limit.is_some()) {
7979 return Err(self.type_err(
7980 "a group that already orders or limits its elements cannot be ordered or limited again by a `for` over it",
7981 ));
7982 }
7983 let td = self.resolve_type(&grp.source.type_name)?;
7984 let alias = grp.source.alias.clone();
7985 let synthetic = ast::SelectStmt {
7986 result: Expr::Path(ast::Path::absolute(grp.source.type_name.clone())),
7987 ..inner.clone()
7988 };
7989 let (filter, order_by, offset, limit) = self.compile_path_modifiers(&synthetic, td, &alias)?;
7990 grp.filter = and_conditions(grp.filter, filter.into_iter().collect());
7991 if ordered {
7992 grp.order_by = order_by;
7993 grp.offset = offset;
7994 grp.limit = limit;
7995 }
7996 if let Some(shape) = shape {
7997 let module = td.module.clone();
7998 grp.shape = self.compile_shape(shape, td, &alias, &module)?;
7999 }
8000 grp.output = IrGroupOutput::Elements;
8001 Ok(Some(grp))
8002 }
8003
8004 fn compile_link_key(&mut self, expr: &Expr, td: &TypeDescriptor, alias: &str) -> Result<Option<IrExpr>, PyQLError> {
8008 let value = match expr {
8012 Expr::Set(elements) if elements.len() == 1 => &elements[0],
8013 other => other,
8014 };
8015 match value {
8016 Expr::SubQuery(inner_stmt)
8019 if matches!(inner_stmt.as_ref(), Stmt::Select(sel) if matches!(sel.result, Expr::Set(_))) =>
8020 {
8021 let IrStmt::Select(select) = self.compile_stmt(inner_stmt)? else {
8022 return Ok(None);
8023 };
8024 if !select.rows.iter().all(|r| matches!(r, IrRowSource::Free(IrFreeExpr::Scalar(_)))) {
8025 return Ok(None);
8026 }
8027 Ok(Some(IrExpr::ScalarSubquery(Box::new(select))))
8028 }
8029 Expr::SubQuery(inner_stmt) => self.compile_link_subquery(inner_stmt).map(Some),
8032 Expr::FunctionCall(fc) => self.try_compile_fn_scalar_subquery(fc, &["id".to_string()], None, None),
8037 Expr::IfElse(ie)
8041 if [&ie.if_expr, &ie.else_expr].iter().any(|b| {
8042 matches!(b, Expr::SubQuery(s) if matches!(s.as_ref(), Stmt::Insert(_) | Stmt::Update(_) | Stmt::Delete(_)))
8043 }) =>
8044 {
8045 let Some(rewritten) = self.object_if_else_as_union(value) else {
8046 return Ok(None);
8047 };
8048 self.compile_link_key(&rewritten, td, alias)
8049 }
8050 Expr::Union(a, b) => {
8051 let (Some(left), Some(right)) = (self.compile_link_key(a, td, alias)?, self.compile_link_key(b, td, alias)?)
8052 else {
8053 return Ok(None);
8054 };
8055 let hoisted = |key: IrExpr| match key {
8059 IrExpr::ColumnRef { alias, column, .. } if column == "id" => IrExpr::CteRef {
8060 name: alias,
8061 scalar: false,
8062 pg_type: None,
8063 },
8064 other => other,
8065 };
8066 Ok(Some(IrExpr::FunctionCall(IrFunctionCall {
8067 return_pg_type: None,
8068 schema: None,
8069 name: "coalesce".to_string(),
8070 args: vec![hoisted(left), hoisted(right)],
8071 sql_template: None,
8072 })))
8073 }
8074 Expr::IfElse(ie) => {
8077 if ![&ie.if_expr, &ie.else_expr].iter().any(|b| matches!(b, Expr::FunctionCall(_))) {
8078 return Ok(None);
8079 }
8080 Ok(Some(IrExpr::IfElse(Box::new(IrIfElse {
8081 condition: self.compile_expr(&ie.condition, td, alias)?,
8082 if_: self.compile_link_branch(&ie.if_expr, td, alias)?,
8083 else_: self.compile_link_branch(&ie.else_expr, td, alias)?,
8084 }))))
8085 }
8086 _ => Ok(None),
8087 }
8088 }
8089
8090 fn compile_link_branch(&mut self, branch: &Expr, td: &TypeDescriptor, alias: &str) -> Result<IrExpr, PyQLError> {
8091 if matches!(branch, Expr::Set(elements) if elements.is_empty()) {
8092 return Ok(IrExpr::Null);
8093 }
8094 match self.compile_link_key(branch, td, alias)? {
8095 Some(key) => Ok(key),
8096 None => self.compile_expr(branch, td, alias),
8097 }
8098 }
8099
8100 fn compile_shape_field_select(
8104 &mut self,
8105 expr: &Expr,
8106 ctx: Option<(&TypeDescriptor, &str)>,
8107 ) -> Result<Option<IrPathSelect>, PyQLError> {
8108 let Expr::FieldAccess { expr: inner, field } = expr else {
8109 return Ok(None);
8110 };
8111 let Expr::Shape(sh) = inner.as_ref() else {
8112 return Ok(None);
8113 };
8114 let Some(Expr::Path(base)) = sh.expr.as_ref() else {
8115 return Ok(None);
8116 };
8117 let Some(value) = sh
8118 .elements
8119 .iter()
8120 .find(|el| matches!(el.path.steps.as_slice(), [ast::PathStep::Name(n)] if n == field))
8121 .and_then(|el| el.compexpr.clone())
8122 else {
8123 return Ok(None);
8124 };
8125 let (rooted, correlate) = if base.partial {
8126 let Some((td, alias)) = ctx else {
8127 return Ok(None);
8128 };
8129 let mut steps = vec![ast::PathStep::Name(format!("{}::{}", td.module, td.name))];
8130 steps.extend(base.steps.iter().cloned());
8131 (ast::Path { steps, partial: false }, Some(alias.to_string()))
8132 } else if base.steps.len() > 1 {
8133 (base.clone(), None)
8134 } else if let [ast::PathStep::Name(root)] = base.steps.as_slice()
8135 && let Ok(root_td) = self.resolve_path_root(root)
8136 {
8137 let narrowing = ast::PathStep::TypeIntersection(ast::ObjectRef {
8139 module: Some(root_td.module.clone()),
8140 name: root_td.name.clone(),
8141 });
8142 (
8143 ast::Path {
8144 steps: vec![base.steps[0].clone(), narrowing],
8145 partial: false,
8146 },
8147 None,
8148 )
8149 } else {
8150 return Ok(None);
8151 };
8152 let synthetic = ast::SelectStmt {
8153 result: Expr::Path(rooted.clone()),
8154 filter: None,
8155 order_by: vec![],
8156 offset: None,
8157 limit: None,
8158 lock: None,
8159 };
8160 let mut ps = self.compile_path_select(&synthetic, &rooted, &[], false)?;
8161 if let Some(alias) = correlate {
8162 Self::correlate_path_select(&mut ps, &alias);
8163 }
8164 let IrPathResult::Object { alias, type_name, .. } = &ps.result else {
8165 return Err(self.type_err(&format!(
8166 "'{field}' is read off a walk that lands on a value, which has no shape"
8167 )));
8168 };
8169 let (alias, type_name) = (alias.clone(), type_name.clone());
8170 let td = self.resolve_type(&type_name)?;
8171 let value = self.compile_expr(&value, td, &alias)?;
8172 let value = if yields_array(&value) {
8175 IrExpr::FunctionCall(IrFunctionCall {
8176 return_pg_type: None,
8177 schema: None,
8178 name: "unnest".to_string(),
8179 args: vec![value],
8180 sql_template: None,
8181 })
8182 } else {
8183 value
8184 };
8185 ps.result = IrPathResult::Scalar(value, None);
8186 Ok(Some(ps))
8187 }
8188
8189 fn is_scalar_walk(&self, expr: &Expr) -> bool {
8192 let Expr::Path(p) = expr else { return false };
8193 let (Some(ast::PathStep::Name(root)), false) = (p.steps.first(), p.partial) else {
8194 return false;
8195 };
8196 let (Some(ast::PathStep::Name(leaf)), [_, middle @ .., _]) = (p.steps.last(), p.steps.as_slice()) else {
8197 return false;
8198 };
8199 let Ok(root_td) = self.resolve_path_root(root) else {
8200 return false;
8201 };
8202 let owner = if middle.is_empty() {
8203 Some(root_td)
8204 } else {
8205 self.walk_path_types(root_td, middle, MAX_COMPUTED_SPLICES).1
8206 };
8207 owner.is_some_and(|td| Self::resolve_property(td, leaf).is_some())
8208 }
8209
8210 fn compile_scalar_union(&mut self, expr: &Expr) -> Result<IrStmt, PyQLError> {
8213 fn operands<'e>(expr: &'e Expr, out: &mut Vec<&'e Expr>) {
8214 match expr {
8215 Expr::Union(a, b) => {
8216 operands(a, out);
8217 operands(b, out);
8218 }
8219 other => out.push(other),
8220 }
8221 }
8222 let mut exprs = vec![];
8223 operands(expr, &mut exprs);
8224 let mut branches = Vec::with_capacity(exprs.len());
8225 for operand in exprs {
8226 let stmt = match operand {
8227 Expr::SubQuery(stmt) => stmt.as_ref().clone(),
8228 other => Stmt::Select(ast::SelectStmt {
8229 result: other.clone(),
8230 filter: None,
8231 order_by: vec![],
8232 offset: None,
8233 limit: None,
8234 lock: None,
8235 }),
8236 };
8237 branches.push(self.compile_stmt(&stmt)?);
8238 }
8239 let types: Vec<String> = branches.iter().map(cte_stmt_type).collect();
8240 let first = types.iter().find(|t| !t.is_empty()).cloned().unwrap_or_default();
8241 if let Some(other) = types
8242 .iter()
8243 .find(|t| t.contains("::") || (!t.is_empty() && **t != first))
8244 {
8245 let display = |t: &str| {
8246 if t.contains("::") {
8247 t.to_string()
8248 } else {
8249 pg_type_to_pyql(t).to_string()
8250 }
8251 };
8252 return Err(self.type_err(&format!(
8253 "operator 'UNION' cannot be applied to operands of type '{}' and '{}'",
8254 display(&first),
8255 display(other)
8256 )));
8257 }
8258 Ok(IrStmt::ScalarUnion(branches))
8259 }
8260
8261 fn dml_subject_type(&self, stmt: &Stmt) -> Result<String, PyQLError> {
8263 match stmt {
8264 Stmt::Insert(ins) => Ok(ins.subject.qualified_name()),
8265 Stmt::Update(upd) => self.subject_type_name(&upd.subject),
8266 Stmt::Delete(del) => self.subject_type_name(&del.subject),
8267 Stmt::With(w) => self.dml_subject_type(&w.stmt),
8268 Stmt::For(f) => self.dml_subject_type(&f.body),
8269 Stmt::Analyze(inner) => self.dml_subject_type(inner),
8270 Stmt::Group(g) => self.expr_as_type_name(&g.subject),
8271 Stmt::Select(sel) => {
8272 if let Expr::TypeCast(tc) = &sel.result
8274 && let Some((module, name)) = tc.ty.as_named()
8275 && module.map(|m| m != "std").unwrap_or(false)
8276 {
8277 return Ok(name.to_string());
8278 }
8279 if let Expr::Path(path) = &sel.result
8282 && !path.partial
8283 && path.steps.len() > 1
8284 && let Some(ast::PathStep::Name(root)) = path.steps.first()
8285 && let Ok(root_td) = self.resolve_path_root(root)
8286 && let (_, Some(target)) = self.walk_path_types(root_td, &path.steps[1..], MAX_COMPUTED_SPLICES)
8287 {
8288 return Ok(format!("{}::{}", target.module, target.name));
8289 }
8290 let (type_name, _, _, _) = self.extract_type_and_shape(&sel.result)?;
8292 Ok(type_name)
8293 }
8294 }
8295 }
8296
8297 fn compile_subject_path_rows(&mut self, subject: &ast::Path, target_alias: &str) -> Result<IrExpr, PyQLError> {
8300 let mut steps = subject.steps.clone();
8304 steps.push(ast::PathStep::Name("id".to_string()));
8305 let ids = ast::Path { steps, partial: false };
8306 let synthetic = ast::SelectStmt {
8307 result: Expr::Path(ids.clone()),
8308 filter: None,
8309 order_by: vec![],
8310 offset: None,
8311 limit: None,
8312 lock: None,
8313 };
8314 let rows = self.compile_path_select(&synthetic, &ids, &[], false)?;
8315 Ok(IrExpr::BinOp(Box::new(IrBinOp {
8316 left: IrExpr::ColumnRef {
8317 alias: target_alias.to_string(),
8318 column: "id".to_string(),
8319 pg_type: "uuid".to_string(),
8320 },
8321 op: ast::BinOpKind::In,
8322 right: IrExpr::ArrayFromSelect(Box::new(IrArraySource::PathSelect(Box::new(rows)))),
8323 })))
8324 }
8325
8326 fn subject_type_name(&self, subject: &Expr) -> Result<String, PyQLError> {
8329 if let Expr::Path(p) = subject
8330 && p.steps.len() > 1
8331 && let Some(ast::PathStep::Name(root)) = p.steps.first()
8332 && let Ok(root_td) = self.resolve_path_root(root)
8333 && let (_, Some(target)) = self.walk_path_types(root_td, &p.steps[1..], MAX_COMPUTED_SPLICES)
8334 {
8335 return Ok(format!("{}::{}", target.module, target.name));
8336 }
8337 if let Expr::Path(p) = subject
8342 && !p.partial
8343 && let [ast::PathStep::Name(root)] = p.steps.as_slice()
8344 && let Some(bound) = self.cte_object_type(root)
8345 {
8346 return Ok(bound);
8347 }
8348 self.expr_as_type_name(subject)
8349 }
8350
8351 fn expr_as_type_name(&self, expr: &Expr) -> Result<String, PyQLError> {
8352 if std::env::var("PYLON_DBG_SUBJ").is_ok() {
8353 eprintln!(
8354 "DBG subj {:.90?}
8355{}",
8356 expr,
8357 std::backtrace::Backtrace::force_capture()
8358 );
8359 }
8360 match expr {
8361 Expr::Detached(inner) => self.expr_as_type_name(inner),
8364 Expr::Path(p) if !p.partial && p.steps.len() == 1 => {
8365 if let ast::PathStep::Name(n) = &p.steps[0] {
8366 return Ok(n.clone());
8367 }
8368 Err(self.type_err("expected a type name"))
8369 }
8370 Expr::Union(_, _) => {
8373 let branches = self
8374 .object_union_branches(expr)
8375 .ok_or_else(|| self.type_err("expected a type name as SELECT subject"))?;
8376 self.common_union_type(&branches)
8377 .ok_or_else(|| self.type_err("expected a type name as SELECT subject"))
8378 }
8379 Expr::Path(p)
8383 if !p.partial
8384 && p.steps.len() > 1
8385 && let Some(ast::PathStep::Name(root)) = p.steps.first()
8386 && let Ok(root_td) = self.resolve_path_root(root)
8387 && let (_, Some(target)) = self.walk_path_types(root_td, &p.steps[1..], MAX_COMPUTED_SPLICES) =>
8388 {
8389 Ok(format!("{}::{}", target.module, target.name))
8390 }
8391 _ => Err(self.type_err("expected a type name as SELECT subject")),
8392 }
8393 }
8394
8395 fn compile_insert(&mut self, ins: &ast::InsertStmt) -> Result<IrInsert, PyQLError> {
8398 let pending_guard = self.pending_insert_guard.take();
8402 let outer_pending_nested_ctes = std::mem::take(&mut self.pending_nested_ctes);
8408 let type_name = ins.subject.qualified_name();
8409 let td = self.resolve_type(&type_name)?;
8410 if td.abstract_ && td.materialized {
8411 return Err(self.type_err(&format!(
8412 "cannot insert into interface type '{}::{}'; insert into a concrete type instead",
8413 td.module, td.name
8414 )));
8415 }
8416 let alias = self.fresh_alias();
8417 let target = IrSource {
8418 poly: None,
8419 type_name: format!("{}::{}", td.module, td.name),
8420 table: td.table.clone(),
8421 alias: alias.clone(),
8422 };
8423
8424 let mut shape = ins.shape.clone();
8428 for link in &td.links {
8429 let unset = !shape.iter().any(|el| path_leaf(&el.path).is_ok_and(|n| n == link.name));
8430 for rw in link.rewrites.iter().filter(|rw| rw.on & 1 != 0) {
8431 if let Some(value) = subject_default_insert(&rw.handler, &link.name)
8432 && unset
8433 {
8434 shape.push(ShapeElement {
8435 path: ast::Path::relative(&link.name),
8436 splat: None,
8437 nested: None,
8438 compexpr: Some(value),
8439 op: ShapeOp::Assign,
8440 filter: None,
8441 order_by: vec![],
8442 offset: None,
8443 limit: None,
8444 marker_offset: None,
8445 });
8446 }
8447 }
8448 }
8449 for (pointer, pyql) in inlined_pointer_defaults(td, self.schema) {
8455 if shape.iter().any(|el| path_leaf(&el.path).is_ok_and(|n| n == pointer)) {
8456 continue;
8457 }
8458 let value = crate::parse::parse_pointer_expr(&pyql)?;
8459 shape.push(default_shape_element(&pointer, value));
8460 }
8461 let mut multi_link_appends = vec![];
8462 let mut scalar_elements: Vec<ShapeElement> = vec![];
8463 for el in &shape {
8464 let pointer_name = match path_leaf(&el.path) {
8465 Ok(n) => n,
8466 Err(_) => {
8467 scalar_elements.push(el.clone());
8468 continue;
8469 }
8470 };
8471 if let Some(ml) = Self::resolve_multilink(td, pointer_name) {
8472 match el.op {
8473 ShapeOp::Remove => {
8474 return Err(self.type_err(&format!(
8475 "cannot use `-=` for multi-link '{pointer_name}' in an insert; \
8476 there is nothing to remove from yet"
8477 )));
8478 }
8479 ShapeOp::Assign | ShapeOp::Append => {
8480 if let Some(expr) = &el.compexpr {
8481 let (jt, module, src_col, tgt_col, through_td) =
8482 self.own_multilink_junction_info(td, ml)?;
8483 let values = self.compile_multilink_values(expr, td, &alias, through_td)?;
8484 multi_link_appends.push(IrMultiLinkMutation {
8485 junction_table: jt,
8486 module,
8487 source_col: src_col,
8488 target_col: tgt_col,
8489 values,
8490 single: false,
8491 });
8492 }
8493 }
8494 }
8495 } else if let Some(l) = Self::resolve_link(td, pointer_name).filter(|l| l.is_junction_backed()) {
8496 if matches!(el.op, ShapeOp::Remove) {
8501 return Err(self.type_err(&format!(
8502 "cannot use `-=` for link '{pointer_name}' in an insert; \
8503 there is nothing to remove from yet"
8504 )));
8505 }
8506 if let Some(expr) = &el.compexpr {
8507 if !is_empty_set_expr(expr) {
8511 let (jt, module, src_col, tgt_col, through_td) = self.own_link_junction_info(td, l)?;
8512 let values = self.compile_multilink_values(expr, td, &alias, through_td)?;
8513 multi_link_appends.push(IrMultiLinkMutation {
8514 junction_table: jt,
8515 module,
8516 source_col: src_col,
8517 target_col: tgt_col,
8518 values,
8519 single: true,
8520 });
8521 }
8522 }
8523 } else {
8524 scalar_elements.push(el.clone());
8525 }
8526 }
8527
8528 let assignments = self.compile_assignments(&scalar_elements, td, &alias)?;
8529 let rewrites = vec![];
8532 let unless_conflict = match ins.unless_conflict.as_ref() {
8533 Some(uc) => {
8534 let (conflict, else_appends) = self.compile_conflict(uc, td)?;
8535 multi_link_appends.extend(else_appends);
8536 Some(conflict)
8537 }
8538 None => None,
8539 };
8540 let returning = Self::pk_returning(td);
8541 let type_name = format!("{}::{}", td.module, td.name);
8542 let enqueue_vector = td
8543 .vector_indexes
8544 .iter()
8545 .map(|vi| VectorEnqueueInfo {
8546 type_name: type_name.clone(),
8547 index_name: vi.index_name.clone(),
8548 })
8549 .collect();
8550 let enqueue_search = collect_search_enqueue(td, &type_name, "index");
8551 let mut nested_ctes = std::mem::replace(&mut self.pending_nested_ctes, outer_pending_nested_ctes);
8552
8553 let guard = match pending_guard {
8554 Some(condition) => Some(self.compile_expr(&condition, td, &alias)?),
8555 None => None,
8556 };
8557 if let Some(condition) = &guard {
8562 fn cte_refs(appends: &[IrMultiLinkMutation], into: &mut Vec<String>) {
8563 into.extend(appends.iter().filter_map(|append| match &append.values.source {
8564 IrMultiLinkValueSource::CteRef(name) => Some(name.clone()),
8565 _ => None,
8566 }));
8567 }
8568 fn carry_guard(stmt: &mut IrStmt, condition: &IrExpr, pending: &mut Vec<String>) {
8569 if let IrStmt::For(loop_) = stmt {
8572 for def in &mut loop_.body_ctes {
8573 carry_guard(&mut def.stmt, condition, pending);
8574 }
8575 }
8576 let nested = match stmt {
8577 IrStmt::Insert(nested) => Some(nested),
8578 IrStmt::For(loop_) => match loop_.body.as_mut() {
8579 IrStmt::Insert(nested) => Some(nested),
8580 _ => None,
8581 },
8582 _ => None,
8583 };
8584 let Some(nested) = nested else { return };
8585 if nested.guard.is_none() {
8586 nested.guard = Some(condition.clone());
8587 }
8588 cte_refs(&nested.multi_link_appends, pending);
8589 for def in &mut nested.nested_ctes {
8590 carry_guard(&mut def.stmt, condition, pending);
8591 }
8592 }
8593 let mut pending: Vec<String> = vec![];
8596 cte_refs(&multi_link_appends, &mut pending);
8597 for def in &mut nested_ctes {
8598 carry_guard(&mut def.stmt, condition, &mut pending);
8599 }
8600 let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
8601 while let Some(name) = pending.pop() {
8602 if !seen.insert(name.clone()) {
8603 continue;
8604 }
8605 if let Some(index) = self.hoisted_ctes.iter().position(|def| def.name == name) {
8606 carry_guard(&mut self.hoisted_ctes[index].stmt, condition, &mut pending);
8607 }
8608 }
8609 }
8610 let id_default_sql =
8614 Self::resolve_property(td, "id").map(|p| p.default_sql.clone().unwrap_or_else(|| "uuidv7()".to_string()));
8615 Ok(IrInsert {
8616 guard,
8617 target,
8618 assignments,
8619 unless_conflict,
8620 rewrites,
8621 id_default_sql,
8622 returning,
8623 enqueue_vector,
8624 enqueue_search,
8625 multi_link_appends,
8626 nested_ctes,
8627 })
8628 }
8629
8630 fn compile_assignments(
8631 &mut self,
8632 elements: &[ShapeElement],
8633 td: &TypeDescriptor,
8634 alias: &str,
8635 ) -> Result<Vec<(String, IrExpr)>, PyQLError> {
8636 self.compile_assignments_inner(elements, td, alias, false)
8637 }
8638
8639 fn compile_assignments_for_update(
8640 &mut self,
8641 elements: &[ShapeElement],
8642 td: &TypeDescriptor,
8643 alias: &str,
8644 ) -> Result<Vec<(String, IrExpr)>, PyQLError> {
8645 self.compile_assignments_inner(elements, td, alias, true)
8646 }
8647
8648 fn compile_assignments_inner(
8649 &mut self,
8650 elements: &[ShapeElement],
8651 td: &TypeDescriptor,
8652 alias: &str,
8653 deny_readonly: bool,
8654 ) -> Result<Vec<(String, IrExpr)>, PyQLError> {
8655 self.without_implicit_id(|this| this.compile_assignments_rows(elements, td, alias, deny_readonly))
8659 }
8660
8661 fn compile_assignments_rows(
8662 &mut self,
8663 elements: &[ShapeElement],
8664 td: &TypeDescriptor,
8665 alias: &str,
8666 deny_readonly: bool,
8667 ) -> Result<Vec<(String, IrExpr)>, PyQLError> {
8668 elements
8669 .iter()
8670 .map(|el| {
8671 let pointer_name = path_leaf(&el.path)?;
8672 let expr = el.compexpr.as_ref().ok_or_else(|| {
8673 PyQLError::Type(PyQLTypeError {
8674 message: format!("INSERT pointer '{pointer_name}' has no value expression"),
8675 position: Position { line: 0, col: 0 },
8676 })
8677 })?;
8678
8679 let column = if let Some(p) = Self::resolve_property(td, pointer_name) {
8681 if p.is_pk && (deny_readonly || !self.config.allow_user_specified_id) {
8686 return Err(PyQLError::Type(PyQLTypeError {
8687 message: "cannot assign to property 'id'".to_string(),
8688 position: Position { line: 0, col: 0 },
8689 }));
8690 }
8691 if deny_readonly && p.is_readonly {
8692 return Err(PyQLError::Type(PyQLTypeError {
8693 message: format!("cannot update property '{pointer_name}': it is declared as read-only"),
8694 position: Position { line: 0, col: 0 },
8695 }));
8696 }
8697 p.name.clone()
8698 } else if let Some(l) = Self::resolve_link(td, pointer_name) {
8699 if deny_readonly && l.is_readonly {
8700 return Err(PyQLError::Type(PyQLTypeError {
8701 message: format!("cannot update link '{pointer_name}': it is declared as read-only"),
8702 position: Position { line: 0, col: 0 },
8703 }));
8704 }
8705 if l.is_junction_backed() {
8706 return Err(PyQLError::Type(PyQLTypeError {
8713 message: format!(
8714 "'{pointer_name}' is a junction-backed link and cannot be \
8715 assigned inside an UNLESS CONFLICT ELSE clause"
8716 ),
8717 position: Position { line: 0, col: 0 },
8718 }));
8719 }
8720 let fk_col = format!("{}_id", l.name);
8726 if let Some(ir_expr) = self.compile_link_key(expr, td, alias)? {
8727 return Ok((fk_col, ir_expr));
8728 }
8729 fk_col
8730 } else if Self::resolve_multilink(td, pointer_name).is_some() {
8731 return Err(PyQLError::Type(PyQLTypeError {
8739 message: format!(
8740 "'{pointer_name}' is a multi-link and cannot be mutated inside an \
8741 UNLESS CONFLICT ELSE clause"
8742 ),
8743 position: Position { line: 0, col: 0 },
8744 }));
8745 } else if Self::resolve_multilink(td, pointer_name).is_some() {
8746 return Err(self.field_err(pointer_name, &format!("{}::{}", td.module, td.name)));
8747 } else if self.resolve_computed(td, pointer_name).is_some() {
8748 return Err(self.type_err(&format!(
8753 "cannot assign to '{pointer_name}': it is a computed pointer on \
8754 {}::{}, which has no stored column to write",
8755 td.module, td.name
8756 )));
8757 } else {
8758 return Err(self.field_err(pointer_name, &format!("{}::{}", td.module, td.name)));
8759 };
8760
8761 let ir_expr = if matches!(expr, Expr::Set(v) if v.is_empty()) {
8763 IrExpr::Null
8764 } else {
8765 self.compile_expr(expr, td, alias)?
8766 };
8767 Ok((column, ir_expr))
8768 })
8769 .collect()
8770 }
8771
8772 fn compile_update(&mut self, upd: &ast::UpdateStmt) -> Result<IrUpdate, PyQLError> {
8775 let pending_guard = self.pending_update_guard.take();
8778 let outer_pending_nested_ctes = std::mem::take(&mut self.pending_nested_ctes);
8780 if !matches!(&upd.subject, Expr::Path(_)) {
8786 let (base, fields) = Self::peel_field_access_chain(&upd.subject);
8787 if let Expr::SubQuery(inner_stmt) = base
8788 && !fields.is_empty()
8789 && matches!(inner_stmt.as_ref(), Stmt::Select(_))
8790 {
8791 let inner = self.compile_stmt(inner_stmt)?;
8792 let cte_name = self.fresh_nested_cte_name();
8793 let type_name = self.register_cte(&cte_name, &inner);
8794 self.hoisted_ctes.push(IrCteDef {
8795 name: cte_name.clone(),
8796 stmt: inner,
8797 type_name,
8798 correlated_to: None,
8799 });
8800 let mut steps = vec![ast::PathStep::Name(cte_name)];
8801 steps.extend(fields.into_iter().map(ast::PathStep::Name));
8802 let rerooted = ast::UpdateStmt {
8803 subject: Expr::Path(ast::Path { steps, partial: false }),
8804 filter: upd.filter.clone(),
8805 shape: upd.shape.clone(),
8806 };
8807 self.pending_nested_ctes = outer_pending_nested_ctes;
8808 self.pending_update_guard = pending_guard;
8809 return self.compile_update(&rerooted);
8810 }
8811 }
8812 if let Expr::Path(subject) = &upd.subject
8817 && subject.steps.len() > 1
8818 && let Some(ast::PathStep::Name(root)) = subject.steps.first()
8819 && let Ok(root_td) = self.resolve_path_root(root)
8820 && let (_, Some(target_td)) = self.walk_path_types(root_td, &subject.steps[1..], MAX_COMPUTED_SPLICES)
8821 {
8822 let narrowed = ast::UpdateStmt {
8823 subject: Expr::Path(ast::Path {
8824 steps: vec![ast::PathStep::Name(format!("{}::{}", target_td.module, target_td.name))],
8825 partial: false,
8826 }),
8827 filter: upd.filter.clone(),
8828 shape: upd.shape.clone(),
8829 };
8830 self.pending_nested_ctes = outer_pending_nested_ctes;
8831 self.pending_update_guard = pending_guard;
8832 let mut ir = self.compile_update(&narrowed)?;
8833 let rows = self.compile_subject_path_rows(subject, &ir.target.alias)?;
8837 ir.filter = Some(and_conditions(ir.filter.take(), vec![rows]).expect("row set is present"));
8838 return Ok(ir);
8839 }
8840 let type_name = self.expr_as_type_name(&upd.subject)?;
8841 let bound_rows = self.cte_object_type(&type_name);
8846 let iterated = self
8850 .for_var_types
8851 .contains_key(&type_name)
8852 .then(|| self.for_var_ref(&type_name));
8853 let td = self.resolve_path_root(&type_name)?;
8854 let alias = self.fresh_alias();
8855 let target = IrSource {
8856 poly: None,
8857 type_name: format!("{}::{}", td.module, td.name),
8858 table: td.table.clone(),
8859 alias: alias.clone(),
8860 };
8861
8862 let declared_filter = upd
8863 .filter
8864 .as_ref()
8865 .map(|f| self.as_condition(|c| c.compile_expr(f, td, &alias)))
8866 .transpose()?;
8867 let filter = match bound_rows {
8868 Some(_) => {
8869 let membership = IrExpr::BinOp(Box::new(IrBinOp {
8870 left: IrExpr::ColumnRef {
8871 alias: alias.clone(),
8872 column: "id".to_string(),
8873 pg_type: "uuid".to_string(),
8874 },
8875 op: ast::BinOpKind::In,
8876 right: IrExpr::ArrayFromSelect(Box::new(IrArraySource::Select(IrSelect::schema_bound(
8877 IrSource {
8878 poly: None,
8879 type_name: format!("{}::{}", td.module, td.name),
8880 table: format!("@cte:{type_name}"),
8881 alias: self.fresh_alias(),
8882 },
8883 vec![],
8884 None,
8885 )))),
8886 }));
8887 Some(and_conditions(declared_filter, vec![membership]).expect("membership is present"))
8888 }
8889 None => declared_filter,
8890 };
8891 let filter = match iterated {
8892 Some(value) => {
8893 let this_row = IrExpr::BinOp(Box::new(IrBinOp {
8894 left: IrExpr::ColumnRef {
8895 alias: alias.clone(),
8896 column: "id".to_string(),
8897 pg_type: "uuid".to_string(),
8898 },
8899 op: ast::BinOpKind::Eq,
8900 right: value,
8901 }));
8902 Some(and_conditions(filter, vec![this_row]).expect("the row condition is present"))
8903 }
8904 None => filter,
8905 };
8906 let filter = match pending_guard {
8910 Some(condition) => {
8911 let guard = self.compile_expr(&condition, td, &alias)?;
8912 and_conditions(filter, vec![guard])
8913 }
8914 None => filter,
8915 };
8916
8917 let mut multi_link_clears = vec![];
8919 let mut multi_link_replaces = vec![];
8920 let mut multi_link_appends = vec![];
8921 let mut multi_link_removals = vec![];
8922 let mut scalar_elements: Vec<ShapeElement> = vec![];
8923
8924 for el in &upd.shape {
8925 let pointer_name = match path_leaf(&el.path) {
8926 Ok(n) => n,
8927 Err(_) => {
8928 scalar_elements.push(el.clone());
8929 continue;
8930 }
8931 };
8932
8933 if let Some(ml) = Self::resolve_multilink(td, pointer_name) {
8934 let (jt, module, src_col, tgt_col, through_td) = self.own_multilink_junction_info(td, ml)?;
8935
8936 match el.op {
8937 ShapeOp::Assign => {
8938 let is_empty = el
8939 .compexpr
8940 .as_ref()
8941 .map(|e| matches!(e, Expr::Set(v) if v.is_empty()))
8942 .unwrap_or(false);
8943 if is_empty {
8944 multi_link_clears.push(IrMultiLinkClear {
8946 junction_table: jt,
8947 module,
8948 source_col: src_col,
8949 });
8950 } else if let Some(expr) = &el.compexpr {
8951 multi_link_clears.push(IrMultiLinkClear {
8953 junction_table: jt.clone(),
8954 module: module.clone(),
8955 source_col: src_col.clone(),
8956 });
8957 let values = self.compile_multilink_values(expr, td, &alias, through_td)?;
8958 multi_link_replaces.push(IrMultiLinkMutation {
8959 junction_table: jt,
8960 module,
8961 source_col: src_col,
8962 target_col: tgt_col,
8963 values,
8964 single: false,
8965 });
8966 }
8967 }
8968 ShapeOp::Append => {
8969 if let Some(expr) = &el.compexpr {
8970 let values = self.compile_multilink_values(expr, td, &alias, through_td)?;
8971 multi_link_appends.push(IrMultiLinkMutation {
8972 junction_table: jt,
8973 module,
8974 source_col: src_col,
8975 target_col: tgt_col,
8976 values,
8977 single: false,
8978 });
8979 }
8980 }
8981 ShapeOp::Remove => {
8982 if let Some(expr) = &el.compexpr {
8983 let values = self.compile_multilink_values(expr, td, &alias, through_td)?;
8984 if has_any_link_props(&values) {
8985 return Err(self.type_err(
8986 "link properties (`@prop := value`) cannot be assigned \
8987 when removing a link (`-=`)",
8988 ));
8989 }
8990 multi_link_removals.push(IrMultiLinkMutation {
8991 junction_table: jt,
8992 module,
8993 source_col: src_col,
8994 target_col: tgt_col,
8995 values,
8996 single: false,
8997 });
8998 }
8999 }
9000 }
9001 } else if let Some(l) = Self::resolve_link(td, pointer_name).filter(|l| l.is_junction_backed()) {
9002 if !matches!(el.op, ShapeOp::Assign) {
9006 return Err(self.type_err(&format!(
9007 "'{pointer_name}' is a single link; only `:=` is supported, not `+=`/`-=`"
9008 )));
9009 }
9010 let (jt, module, src_col, tgt_col, through_td) = self.own_link_junction_info(td, l)?;
9011 let is_empty = el.compexpr.as_ref().map(is_empty_set_expr).unwrap_or(false);
9012 if is_empty {
9013 multi_link_clears.push(IrMultiLinkClear {
9015 junction_table: jt,
9016 module,
9017 source_col: src_col,
9018 });
9019 } else if let Some(expr) = &el.compexpr {
9020 multi_link_clears.push(IrMultiLinkClear {
9021 junction_table: jt.clone(),
9022 module: module.clone(),
9023 source_col: src_col.clone(),
9024 });
9025 let values = self.compile_multilink_values(expr, td, &alias, through_td)?;
9026 multi_link_replaces.push(IrMultiLinkMutation {
9027 junction_table: jt,
9028 module,
9029 source_col: src_col,
9030 target_col: tgt_col,
9031 values,
9032 single: true,
9033 });
9034 }
9035 } else {
9036 scalar_elements.push(el.clone());
9037 }
9038 }
9039
9040 let assignments = self.compile_assignments_for_update(&scalar_elements, td, &alias)?;
9041 let rewrites = vec![];
9043 let returning = Self::pk_returning(td);
9044
9045 let (poly_implementors, poly_columns) = if self.is_polymorphic(td) {
9046 (
9047 self.find_poly_implementors(&format!("{}::{}", td.module, td.name)),
9048 Self::poly_dml_columns(td),
9049 )
9050 } else {
9051 (vec![], vec![])
9052 };
9053
9054 let written_cols: std::collections::HashSet<&str> = assignments.iter().map(|(c, _)| c.as_str()).collect();
9056 let type_name = format!("{}::{}", td.module, td.name);
9057 let enqueue_vector: Vec<VectorEnqueueInfo> = td
9058 .vector_indexes
9059 .iter()
9060 .filter(|vi| vi.pointers.iter().any(|f| written_cols.contains(f.as_str())))
9061 .map(|vi| VectorEnqueueInfo {
9062 type_name: type_name.clone(),
9063 index_name: vi.index_name.clone(),
9064 })
9065 .collect();
9066 let enqueue_search = collect_search_enqueue(td, &type_name, "index");
9067 let nested_ctes = std::mem::replace(&mut self.pending_nested_ctes, outer_pending_nested_ctes);
9073
9074 Ok(IrUpdate {
9075 target,
9076 filter,
9077 assignments,
9078 rewrites,
9079 returning,
9080 multi_link_clears,
9081 multi_link_replaces,
9082 multi_link_appends,
9083 multi_link_removals,
9084 poly_implementors,
9085 poly_columns,
9086 enqueue_vector,
9087 enqueue_search,
9088 nested_ctes,
9089 })
9090 }
9091
9092 fn junction_info_for(
9101 &mut self,
9102 td: &TypeDescriptor,
9103 name: &str,
9104 target: &str,
9105 through: &Option<String>,
9106 ) -> Result<(String, String, String, String, Option<&'a TypeDescriptor>), PyQLError> {
9107 let (junction_table, module, source_col, target_col, through_td) =
9108 self.own_junction_info_for(td, name, target, through)?;
9109 let owner_derived = through_td.is_none_or(|through_td| through_td.junction);
9110 let junction_table = if owner_derived {
9111 self.owner_junction(td, name, through_td)
9112 } else {
9113 junction_table
9114 };
9115 Ok((junction_table, module, source_col, target_col, through_td))
9116 }
9117
9118 fn junction_tables_of(&mut self, td: &TypeDescriptor) -> Result<Vec<String>, PyQLError> {
9123 let mut tables = vec![];
9124 for multilink in &td.multilinks {
9125 match &multilink.through {
9126 Some(through) => {
9127 let through_td = self.resolve_type(through)?;
9128 tables.push(format!("\"{}\"", through_td.table));
9129 }
9130 None => {
9131 tables.push(format!("\"{}.{}\"", td.table, multilink.name));
9132 for implementor in self.find_poly_implementors(&format!("{}::{}", td.module, td.name)) {
9133 tables.push(format!("\"{}.{}\"", implementor.table, multilink.name));
9134 }
9135 }
9136 }
9137 }
9138 Ok(tables)
9139 }
9140
9141 fn owner_junction(&self, td: &TypeDescriptor, name: &str, through_td: Option<&TypeDescriptor>) -> String {
9144 let own = format!("{}.{}", td.table, name);
9145 if !self.has_subtypes(td) {
9146 return own;
9147 }
9148 let tables = self
9149 .find_poly_implementors(&format!("{}::{}", td.module, td.name))
9150 .into_iter()
9151 .map(|implementor| (implementor.module, format!("{}.{}", implementor.table, name)))
9152 .collect::<Vec<_>>();
9153 let mut columns = vec!["source".to_string(), "target".to_string()];
9154 if let Some(through_td) = through_td {
9155 columns.extend(Self::poly_dml_columns(through_td).into_iter().filter(|c| c != "id"));
9156 }
9157 super::inherited_junction(&tables, &columns)
9158 }
9159
9160 fn own_junction_info_for(
9163 &mut self,
9164 td: &TypeDescriptor,
9165 name: &str,
9166 target: &str,
9167 through: &Option<String>,
9168 ) -> Result<(String, String, String, String, Option<&'a TypeDescriptor>), PyQLError> {
9169 match through {
9170 None => Ok((
9171 format!("{}.{}", td.table, name),
9172 td.module.clone(),
9173 "source".to_string(),
9174 "target".to_string(),
9175 None,
9176 )),
9177 Some(through_qname) => {
9178 let through_td = self.resolve_type(through_qname)?;
9179 let src_type = format!("{}::{}", td.module, td.name);
9180 let source_col = through_td
9181 .links
9182 .iter()
9183 .find(|l| l.target == src_type)
9184 .map(|l| l.name.clone())
9185 .unwrap_or_else(|| "source".to_string());
9186 let target_col = through_td
9193 .links
9194 .iter()
9195 .find(|l| l.target == target && l.name != source_col)
9196 .or_else(|| through_td.links.iter().find(|l| l.target == target))
9197 .map(|l| l.name.clone())
9198 .unwrap_or_else(|| "target".to_string());
9199 let (junction_table, junction_module) = if through_td.junction {
9214 (format!("{}.{}", td.table, name), td.module.clone())
9215 } else {
9216 (through_td.table.clone(), through_td.module.clone())
9217 };
9218 Ok((
9219 junction_table,
9220 junction_module,
9221 source_col,
9222 target_col,
9223 Some(through_td),
9224 ))
9225 }
9226 }
9227 }
9228
9229 fn multilink_junction_info(
9230 &mut self,
9231 td: &TypeDescriptor,
9232 ml: &MultiLinkDescriptor,
9233 ) -> Result<(String, String, String, String, Option<&'a TypeDescriptor>), PyQLError> {
9234 self.junction_info_for(td, &ml.name, &ml.target, &ml.through)
9235 }
9236
9237 fn own_multilink_junction_info(
9238 &mut self,
9239 td: &TypeDescriptor,
9240 ml: &MultiLinkDescriptor,
9241 ) -> Result<(String, String, String, String, Option<&'a TypeDescriptor>), PyQLError> {
9242 self.own_junction_info_for(td, &ml.name, &ml.target, &ml.through)
9243 }
9244
9245 fn own_link_junction_info(
9246 &mut self,
9247 td: &TypeDescriptor,
9248 l: &LinkDescriptor,
9249 ) -> Result<(String, String, String, String, Option<&'a TypeDescriptor>), PyQLError> {
9250 self.own_junction_info_for(td, &l.name, &l.target, &l.through)
9251 }
9252
9253 fn link_junction_info(
9255 &mut self,
9256 td: &TypeDescriptor,
9257 l: &LinkDescriptor,
9258 ) -> Result<(String, String, String, String, Option<&'a TypeDescriptor>), PyQLError> {
9259 self.junction_info_for(td, &l.name, &l.target, &l.through)
9260 }
9261
9262 fn build_multilink_join(
9267 &mut self,
9268 td: &TypeDescriptor,
9269 name: &str,
9270 target: &str,
9271 through: &Option<String>,
9272 ) -> Result<IrMultiLinkJoin, PyQLError> {
9273 if let Some(through_qname) = through {
9274 let through_td = self.resolve_type(through_qname)?;
9275 if through_td.junction {
9276 Ok(IrMultiLinkJoin::Standard {
9281 junction_table: self.owner_junction(td, name, Some(through_td)),
9282 module: td.module.clone(),
9283 })
9284 } else {
9285 let source_qname = format!("{}::{}", td.module, td.name);
9286 let source_col = through_td
9287 .links
9288 .iter()
9289 .find(|l| l.target == source_qname)
9290 .ok_or_else(|| {
9291 PyQLError::Type(PyQLTypeError {
9292 message: format!("through type {through_qname} has no link to {source_qname}"),
9293 position: Position { line: 0, col: 0 },
9294 })
9295 })?
9296 .name
9297 .clone();
9298 let target_col = through_td
9299 .links
9300 .iter()
9301 .find(|l| l.target == target && l.name != source_col)
9302 .or_else(|| through_td.links.iter().find(|l| l.target == target))
9303 .ok_or_else(|| {
9304 PyQLError::Type(PyQLTypeError {
9305 message: format!("through type {through_qname} has no link to target {target}"),
9306 position: Position { line: 0, col: 0 },
9307 })
9308 })?
9309 .name
9310 .clone();
9311 Ok(IrMultiLinkJoin::Through {
9312 junction_table: through_td.table.clone(),
9313 module: through_td.module.clone(),
9314 source_col,
9315 target_col,
9316 })
9317 }
9318 } else {
9319 let junction_table = self.owner_junction(td, name, None);
9320 Ok(IrMultiLinkJoin::Standard {
9321 junction_table: match self.junction_read_overrides.get(&junction_table) {
9322 Some(o) => o.junction.clone(),
9323 None => junction_table,
9324 },
9325 module: td.module.clone(),
9326 })
9327 }
9328 }
9329
9330 fn compile_multilink_values(
9339 &mut self,
9340 expr: &Expr,
9341 td: &'a TypeDescriptor,
9342 alias: &str,
9343 through_td: Option<&'a TypeDescriptor>,
9344 ) -> Result<IrMultiLinkValues, PyQLError> {
9345 self.without_implicit_id(|this| this.compile_multilink_values_inner(expr, td, alias, through_td))
9349 }
9350
9351 fn compile_multilink_values_inner(
9352 &mut self,
9353 expr: &Expr,
9354 td: &'a TypeDescriptor,
9355 alias: &str,
9356 through_td: Option<&'a TypeDescriptor>,
9357 ) -> Result<IrMultiLinkValues, PyQLError> {
9358 if let Expr::Union(a, b) = expr {
9361 let left = self.compile_multilink_values(a, td, alias, through_td)?;
9362 let right = self.compile_multilink_values(b, td, alias, through_td)?;
9363 return Ok(IrMultiLinkValues {
9364 source: IrMultiLinkValueSource::Union(Box::new(left), Box::new(right)),
9365 link_props: vec![],
9366 });
9367 }
9368
9369 if matches!(expr, Expr::IfElse(_))
9374 && let Some(rewritten) = self.object_if_else_as_union(expr)
9375 {
9376 return self.compile_multilink_values(&rewritten, td, alias, through_td);
9377 }
9378
9379 if let Expr::SubQuery(inner) = expr
9384 && let Stmt::Select(sel) = inner.as_ref()
9385 && let Expr::Shape(sh) = &sel.result
9386 && !sh.elements.is_empty()
9387 && sh
9388 .elements
9389 .iter()
9390 .all(|el| matches!(el.path.steps.as_slice(), [ast::PathStep::LinkProp(_)]))
9391 && let Some(base) = sh.expr.clone()
9392 {
9393 let lifted = Expr::Shape(Box::new(ast::ShapeExpr {
9394 expr: Some(Expr::SubQuery(Box::new(Stmt::Select(ast::SelectStmt {
9395 result: base,
9396 ..sel.clone()
9397 })))),
9398 elements: sh.elements.clone(),
9399 marker_offset: sh.marker_offset,
9400 }));
9401 return self.compile_multilink_values(&lifted, td, alias, through_td);
9402 }
9403
9404 if let Expr::Shape(shape) = expr {
9407 let inner_expr = shape
9408 .expr
9409 .as_ref()
9410 .ok_or_else(|| self.type_err("multilink value shape must have a base expression"))?;
9411 let mut inner = self.compile_multilink_values(inner_expr, td, alias, through_td)?;
9412 let walked_junction = match (&inner.source, inner_expr) {
9416 (IrMultiLinkValueSource::PathSelect(ps), Expr::SubQuery(stmt)) => {
9417 match (stmt.as_ref(), ps.joins.last()) {
9418 (Stmt::Select(sel), Some(IrPathJoin::Multi { junction_alias, .. })) => match &sel.result {
9419 Expr::Path(p) if p.partial => match p.steps.as_slice() {
9420 [ast::PathStep::Name(link)] => Self::resolve_multilink(td, link)
9421 .and_then(|m| m.through.clone())
9422 .map(|through| (through, junction_alias.clone())),
9423 _ => None,
9424 },
9425 _ => None,
9426 },
9427 _ => None,
9428 }
9429 }
9430 _ => None,
9431 };
9432
9433 let Some(through) = through_td else {
9434 return Err(self.type_err(
9435 "link properties (`@prop := value`) are only valid on a multi-link \
9436 declared with `Through[...]`",
9437 ));
9438 };
9439
9440 for el in &shape.elements {
9441 let prop_name = match el.path.steps.as_slice() {
9442 [ast::PathStep::LinkProp(name)] => name.clone(),
9443 _ => return Err(self.type_err("only `@prop := value` link-property assignments are valid here")),
9444 };
9445 let prop = Self::resolve_property(through, &prop_name)
9446 .ok_or_else(|| self.field_err(&prop_name, &format!("{}::{}", through.module, through.name)))?;
9447 if prop.is_readonly {
9448 return Err(self.type_err(&format!(
9449 "cannot set link property '{prop_name}': it is declared as read-only"
9450 )));
9451 }
9452 let value_expr = el
9453 .compexpr
9454 .as_ref()
9455 .ok_or_else(|| self.type_err(&format!("link property '{prop_name}' must be assigned a value")))?;
9456 let scoped = walked_junction.is_some();
9457 if scoped {
9458 self.link_prop_scope.push(walked_junction.clone());
9459 }
9460 let ir_expr = self.compile_expr(value_expr, td, alias);
9461 if scoped {
9462 self.link_prop_scope.pop();
9463 }
9464 inner.link_props.push((prop.name.clone(), ir_expr?));
9465 }
9466 return Ok(inner);
9467 }
9468
9469 if let Expr::FunctionCall(f) = expr
9472 && (f.module.is_none() || f.module.as_deref() == Some("std"))
9473 && matches!(f.name.as_str(), "assert_exists" | "assert_distinct")
9474 && let [arg] = f.args.as_slice()
9475 {
9476 let arg = arg.clone();
9477 let inner = self.compile_multilink_values(&arg, td, alias, through_td)?;
9478 let mut props = vec![];
9482 crate::sql::collect_link_prop_names(&inner, &mut props);
9483 if !props.is_empty() {
9484 return Err(self.type_err(&format!(
9485 "'{}' cannot be applied to a link that carries link properties ({}) — the check reads the targets alone",
9486 f.name,
9487 props.join(", "),
9488 )));
9489 }
9490 let message = self.assert_message(f, Some((td, alias)))?;
9491 return Ok(IrMultiLinkValues {
9492 source: IrMultiLinkValueSource::Asserted {
9493 fn_name: f.name.clone(),
9494 inner: Box::new(inner),
9495 message,
9496 },
9497 link_props: vec![],
9498 });
9499 }
9500
9501 if let Expr::SubQuery(inner) = expr
9507 && let Stmt::Select(sel) = inner.as_ref()
9508 && sel.filter.is_none()
9509 && sel.order_by.is_empty()
9510 && sel.offset.is_none()
9511 && sel.limit.is_none()
9512 && matches!(sel.result, Expr::Set(_) | Expr::Union(_, _))
9513 {
9514 let result = sel.result.clone();
9515 return self.compile_multilink_values(&result, td, alias, through_td);
9516 }
9517
9518 if let Some(name) = self.resolve_cte_name(expr) {
9520 return Ok(IrMultiLinkValues {
9521 source: IrMultiLinkValueSource::CteRef(name.to_string()),
9522 link_props: vec![],
9523 });
9524 }
9525
9526 if let Expr::Set(elements) = expr
9528 && !elements.is_empty()
9529 {
9530 let mut combined: Option<IrMultiLinkValues> = None;
9531 for element in elements {
9532 let one = self.compile_multilink_values(element, td, alias, through_td)?;
9533 combined = Some(match combined {
9534 None => one,
9535 Some(previous) => IrMultiLinkValues {
9536 source: IrMultiLinkValueSource::Union(Box::new(previous), Box::new(one)),
9537 link_props: vec![],
9538 },
9539 });
9540 }
9541 return Ok(combined.expect("elements is non-empty"));
9542 }
9543
9544 if let Expr::SubQuery(inner) = expr
9548 && let Stmt::For(_) = inner.as_ref()
9549 {
9550 let (cte_name, _) = self.hoist_dml_as_cte(inner.as_ref())?;
9551 return Ok(IrMultiLinkValues {
9552 source: IrMultiLinkValueSource::CteRef(cte_name),
9553 link_props: vec![],
9554 });
9555 }
9556
9557 if let Expr::SubQuery(inner) = expr
9560 && matches!(inner.as_ref(), Stmt::Insert(_) | Stmt::Update(_) | Stmt::Delete(_))
9561 {
9562 let cte_name = self.hoist_nested_dml(inner)?;
9563 return Ok(IrMultiLinkValues {
9564 source: IrMultiLinkValueSource::CteRef(cte_name),
9565 link_props: vec![],
9566 });
9567 }
9568
9569 if let Expr::SubQuery(inner) = expr
9574 && let Stmt::Select(sel) = inner.as_ref()
9575 && let Expr::Path(path) = &sel.result
9576 && path.partial
9577 {
9578 let mut steps = vec![ast::PathStep::Name(format!("{}::{}", td.module, td.name))];
9579 steps.extend(path.steps.iter().cloned());
9580 let rooted = ast::Path { steps, partial: false };
9581 let synthetic = ast::SelectStmt {
9582 result: Expr::Path(rooted.clone()),
9583 filter: sel.filter.clone(),
9584 order_by: sel.order_by.clone(),
9585 offset: sel.offset.clone(),
9586 limit: sel.limit.clone(),
9587 lock: None,
9588 };
9589 let mut ps = self.compile_path_select(&synthetic, &rooted, &[], false)?;
9590 Self::correlate_path_select(&mut ps, alias);
9591 return Ok(IrMultiLinkValues {
9592 source: IrMultiLinkValueSource::PathSelect(Box::new(ps)),
9593 link_props: vec![],
9594 });
9595 }
9596
9597 if let Expr::SubQuery(inner) = expr {
9599 return match self.compile_stmt(inner)? {
9600 IrStmt::Select(s) if matches!(s.rows.as_slice(), [IrRowSource::Bound { .. }]) => {
9601 Ok(IrMultiLinkValues {
9602 source: IrMultiLinkValueSource::Select(Box::new(s)),
9603 link_props: vec![],
9604 })
9605 }
9606 IrStmt::PathSelect(ps) => Ok(IrMultiLinkValues {
9607 source: IrMultiLinkValueSource::PathSelect(Box::new(ps)),
9608 link_props: vec![],
9609 }),
9610 _ => Err(self.type_err("multilink value must resolve to a SELECT or path query")),
9611 };
9612 }
9613
9614 if let Expr::FunctionCall(fc) = expr {
9615 let synthetic = ast::SelectStmt {
9616 result: expr.clone(),
9617 filter: None,
9618 order_by: vec![],
9619 offset: None,
9620 limit: None,
9621 lock: None,
9622 };
9623 if let Some(fs) = self.try_compile_fn_object_select(fc, &[], &synthetic, false)? {
9624 return Ok(IrMultiLinkValues {
9625 source: IrMultiLinkValueSource::Function(Box::new(fs)),
9626 link_props: vec![],
9627 });
9628 }
9629 }
9630
9631 if let Expr::Path(p) = expr
9633 && !p.partial
9634 {
9635 let fake_sel = ast::SelectStmt {
9636 result: expr.clone(),
9637 filter: None,
9638 order_by: vec![],
9639 offset: None,
9640 limit: None,
9641 lock: None,
9642 };
9643 return match self.compile_stmt(&Stmt::Select(fake_sel))? {
9644 IrStmt::PathSelect(ps) => Ok(IrMultiLinkValues {
9645 source: IrMultiLinkValueSource::PathSelect(Box::new(ps)),
9646 link_props: vec![],
9647 }),
9648 IrStmt::Select(s) if matches!(s.rows.as_slice(), [IrRowSource::Bound { .. }]) => {
9649 Ok(IrMultiLinkValues {
9650 source: IrMultiLinkValueSource::Select(Box::new(s)),
9651 link_props: vec![],
9652 })
9653 }
9654 _ => Err(self.type_err("expected a path expression for multilink value")),
9655 };
9656 }
9657
9658 Err(self.type_err("multilink value must be a CTE reference, parenthesised subquery, or type path"))
9659 }
9660
9661 fn compile_delete(&mut self, del: &ast::DeleteStmt) -> Result<IrDelete, PyQLError> {
9664 let pending_guard = self.pending_delete_guard.take();
9667 let type_name = self.expr_as_type_name(&del.subject)?;
9668 let bound_rows = self.cte_object_type(&type_name);
9673 let td = self.resolve_path_root(&type_name)?;
9674 let alias = self.fresh_alias();
9675 let target = IrSource {
9676 poly: None,
9677 type_name: format!("{}::{}", td.module, td.name),
9678 table: td.table.clone(),
9679 alias: alias.clone(),
9680 };
9681
9682 let declared_filter = del
9683 .filter
9684 .as_ref()
9685 .map(|f| self.as_condition(|c| c.compile_expr(f, td, &alias)))
9686 .transpose()?;
9687 let filter = match bound_rows {
9688 Some(_) => {
9689 let membership = IrExpr::BinOp(Box::new(IrBinOp {
9690 left: IrExpr::ColumnRef {
9691 alias: alias.clone(),
9692 column: "id".to_string(),
9693 pg_type: "uuid".to_string(),
9694 },
9695 op: ast::BinOpKind::In,
9696 right: IrExpr::ArrayFromSelect(Box::new(IrArraySource::Select(IrSelect::schema_bound(
9697 IrSource {
9698 poly: None,
9699 type_name: format!("{}::{}", td.module, td.name),
9700 table: format!("@cte:{type_name}"),
9701 alias: self.fresh_alias(),
9702 },
9703 vec![],
9704 None,
9705 )))),
9706 }));
9707 Some(and_conditions(declared_filter, vec![membership]).expect("membership is present"))
9708 }
9709 None => declared_filter,
9710 };
9711 let filter = match pending_guard {
9712 Some(condition) => {
9713 let guard = self.compile_expr(&condition, td, &alias)?;
9714 and_conditions(filter, vec![guard])
9715 }
9716 None => filter,
9717 };
9718
9719 let returning = Self::pk_returning(td);
9720
9721 let (poly_implementors, poly_columns) = if self.is_polymorphic(td) {
9722 (
9723 self.find_poly_implementors(&format!("{}::{}", td.module, td.name)),
9724 Self::poly_dml_columns(td),
9725 )
9726 } else {
9727 (vec![], vec![])
9728 };
9729 let qname = format!("{}::{}", td.module, td.name);
9730 let enqueue_search = collect_search_enqueue(td, &qname, "delete");
9731
9732 Ok(IrDelete {
9733 target,
9734 filter,
9735 returning,
9736 poly_implementors,
9737 poly_columns,
9738 enqueue_search,
9739 })
9740 }
9741
9742 fn compile_shape(
9745 &mut self,
9746 elements: &[ShapeElement],
9747 td: &TypeDescriptor,
9748 alias: &str,
9749 module: &str,
9750 ) -> Result<Vec<IrShapePointer>, PyQLError> {
9751 if elements.is_empty() {
9752 return Ok(Self::pk_returning(td));
9754 }
9755
9756 let mut pointers = vec![];
9757 for el in elements {
9758 if let Some(splat) = &el.splat {
9759 if let Some(ast::PathStep::TypeIntersection(type_ref)) = el.path.steps.first() {
9761 let type_ref = type_ref.clone();
9762 pointers.extend(self.compile_type_intersection_splat(&type_ref, splat, td, alias)?);
9763 } else {
9764 pointers.extend(self.compile_splat(splat, td, alias, module)?);
9765 }
9766 } else {
9767 pointers.push(self.compile_shape_element(el, td, alias, module)?);
9768 }
9769 }
9770 self.prepend_implicit_id(&mut pointers, td);
9771 Ok(pointers)
9772 }
9773
9774 fn prepend_implicit_id(&self, pointers: &mut Vec<IrShapePointer>, td: &TypeDescriptor) {
9783 if !self.implicit_id_in_shapes {
9784 return;
9785 }
9786 let Some(pk) = td.properties.iter().find(|p| p.is_pk) else {
9787 return;
9788 };
9789 if pointers.iter().any(|p| p.alias() == pk.name) {
9790 return;
9791 }
9792 pointers.insert(
9793 0,
9794 IrShapePointer::Scalar(IrScalarPointer {
9795 implicit_id: true,
9796 marker_offset: None,
9797 alias: pk.name.clone(),
9798 column: pk.name.clone(),
9799 pg_type: pk.pg_type.clone(),
9800 tuple_shape: None,
9801 }),
9802 );
9803 }
9804
9805 fn without_implicit_id<T>(&mut self, body: impl FnOnce(&mut Self) -> Result<T, PyQLError>) -> Result<T, PyQLError> {
9808 let saved = std::mem::replace(&mut self.implicit_id_in_shapes, false);
9809 let result = body(self);
9810 self.implicit_id_in_shapes = saved;
9811 result
9812 }
9813
9814 fn compile_splat(
9816 &mut self,
9817 splat: &ast::Splat,
9818 td: &TypeDescriptor,
9819 alias: &str,
9820 module: &str,
9821 ) -> Result<Vec<IrShapePointer>, PyQLError> {
9822 let mut pointers: Vec<IrShapePointer> = td
9823 .properties
9824 .iter()
9825 .map(|p| {
9826 IrShapePointer::Scalar(IrScalarPointer {
9827 implicit_id: false,
9828 marker_offset: None,
9829 alias: p.name.clone(),
9830 column: p.name.clone(),
9831 pg_type: p.pg_type.clone(),
9832 tuple_shape: self.resolve_property_tuple_shape(p),
9833 })
9834 })
9835 .collect();
9836
9837 for cd in &td.computed.clone() {
9838 if matches!(splat, ast::Splat::Shallow) && self.computed_is_object_valued(cd, td) {
9842 continue;
9843 }
9844 pointers.push(self.compile_declared_computed(cd, td, alias, module, None, &[])?);
9845 }
9846
9847 if matches!(splat, ast::Splat::Deep) {
9848 for l in &td.links {
9849 let target_td = self.resolve_type(&l.target)?;
9850 let sub_alias = self.fresh_alias();
9851 let target_module = target_td.module.clone();
9857 let sub_shape = self.compile_splat(&ast::Splat::Shallow, target_td, &sub_alias, &target_module)?;
9858 let subquery = IrSelect::schema_bound(
9859 IrSource {
9860 poly: self.link_target_fanout(target_td),
9861 type_name: format!("{}::{}", target_td.module, target_td.name),
9862 table: target_td.table.clone(),
9863 alias: sub_alias.clone(),
9864 },
9865 sub_shape,
9866 None,
9867 );
9868 let correlation = if l.is_junction_backed() {
9869 let join = self.build_multilink_join(td, &l.name, &l.target, &l.through)?;
9870 IrSingleLinkCorrelation::Junction {
9871 join,
9872 target_pk: "id".to_string(),
9873 }
9874 } else {
9875 IrSingleLinkCorrelation::Fk {
9876 fk_column: format!("{}_id", l.name),
9877 target_pk: "id".to_string(),
9878 }
9879 };
9880 pointers.push(IrShapePointer::SingleLink(IrSingleLinkPointer {
9881 marker_offset: None,
9882 alias: l.name.clone(),
9883 correlation,
9884 subquery,
9885 link_properties: vec![],
9886 }));
9887 }
9888
9889 for ml in &td.multilinks {
9890 let sub_alias = self.fresh_alias();
9891 let target_td = self.resolve_type(&ml.target)?;
9892 let target_module = target_td.module.clone();
9895 let sub_shape = self.compile_splat(&ast::Splat::Shallow, target_td, &sub_alias, &target_module)?;
9896
9897 let join = if let Some(through_qname) = &ml.through {
9898 let through_td = self.resolve_type(through_qname)?;
9899 if through_td.junction {
9900 IrMultiLinkJoin::Standard {
9902 junction_table: self.owner_junction(td, &ml.name, Some(through_td)),
9903 module: td.module.clone(),
9904 }
9905 } else {
9906 let source_qname = format!("{}::{}", td.module, td.name);
9907 let source_col = through_td
9908 .links
9909 .iter()
9910 .find(|l| l.target == source_qname)
9911 .ok_or_else(|| {
9912 PyQLError::Type(PyQLTypeError {
9913 message: format!(
9914 "through type {through_qname} has no link to source type {source_qname}"
9915 ),
9916 position: Position { line: 0, col: 0 },
9917 })
9918 })?
9919 .name
9920 .clone();
9921 let target_col = through_td
9922 .links
9923 .iter()
9924 .find(|l| l.target == ml.target && l.name != source_col)
9925 .or_else(|| through_td.links.iter().find(|l| l.target == ml.target))
9926 .ok_or_else(|| {
9927 PyQLError::Type(PyQLTypeError {
9928 message: format!(
9929 "through type {through_qname} has no link to target type {}",
9930 ml.target
9931 ),
9932 position: Position { line: 0, col: 0 },
9933 })
9934 })?
9935 .name
9936 .clone();
9937 IrMultiLinkJoin::Through {
9938 junction_table: through_td.table.clone(),
9939 module: through_td.module.clone(),
9940 source_col,
9941 target_col,
9942 }
9943 }
9944 } else {
9945 IrMultiLinkJoin::Standard {
9949 junction_table: self.owner_junction(td, &ml.name, None),
9950 module: td.module.clone(),
9951 }
9952 };
9953
9954 let subquery = self.link_target_select(
9955 target_td,
9956 IrSource {
9957 poly: None,
9958 type_name: format!("{}::{}", target_td.module, target_td.name),
9959 table: target_td.table.clone(),
9960 alias: sub_alias.clone(),
9961 },
9962 sub_shape,
9963 );
9964
9965 let link_properties = self.splat_link_properties(ml)?;
9966 pointers.push(IrShapePointer::MultiLink(IrMultiLinkPointer {
9967 marker_offset: None,
9968 alias: ml.name.clone(),
9969 join,
9970 subquery,
9971 link_properties,
9972 single: false,
9973 }));
9974 }
9975 }
9976
9977 Ok(pointers)
9978 }
9979
9980 fn splat_link_properties(&self, ml: &MultiLinkDescriptor) -> Result<Vec<IrLinkProp>, PyQLError> {
9983 let Some(through_qname) = &ml.through else {
9984 return Ok(vec![]);
9985 };
9986 let through_td = self.resolve_type(through_qname)?;
9987 if !through_td.junction {
9988 return Ok(vec![]);
9989 }
9990 Ok(through_td
9991 .properties
9992 .iter()
9993 .filter(|p| p.name != "id")
9994 .map(|p| IrLinkProp { name: p.name.clone() })
9995 .collect())
9996 }
9997
9998 fn compile_type_intersection_splat(
10002 &mut self,
10003 type_ref: &ast::ObjectRef,
10004 splat: &ast::Splat,
10005 parent_td: &TypeDescriptor,
10006 parent_alias: &str,
10007 ) -> Result<Vec<IrShapePointer>, PyQLError> {
10008 let type_name = match &type_ref.module {
10009 Some(m) => format!("{}::{}", m, type_ref.name),
10010 None => type_ref.name.clone(),
10011 };
10012 let concrete_td = self.resolve_type(&type_name)?;
10013 let concrete_qname = format!("{}::{}", concrete_td.module, concrete_td.name);
10014 let concrete_table = concrete_td.table.clone();
10015
10016 let interface_props: std::collections::HashSet<String> =
10018 parent_td.properties.iter().map(|p| p.name.clone()).collect();
10019
10020 let props: Vec<_> = concrete_td
10022 .properties
10023 .iter()
10024 .filter(|p| !interface_props.contains(&p.name))
10025 .cloned()
10026 .collect();
10027
10028 let interface_computed: std::collections::HashSet<String> =
10032 parent_td.computed.iter().map(|c| c.name.clone()).collect();
10033 let computed: Vec<_> = concrete_td
10034 .computed
10035 .iter()
10036 .filter(|c| !interface_computed.contains(&c.name))
10037 .filter(|c| !matches!(splat, ast::Splat::Shallow) || !self.computed_is_object_valued(c, concrete_td))
10038 .cloned()
10039 .collect();
10040
10041 let links: Vec<_> = if matches!(splat, ast::Splat::Deep) {
10043 concrete_td.links.to_vec()
10044 } else {
10045 vec![]
10046 };
10047
10048 let mut pointers = vec![];
10049 for prop in props {
10050 let sub_alias = self.fresh_alias();
10051 let filter = IrExpr::BinOp(Box::new(IrBinOp {
10052 left: IrExpr::ColumnRef {
10053 alias: sub_alias.clone(),
10054 column: "id".to_string(),
10055 pg_type: "uuid".to_string(),
10056 },
10057 op: ast::BinOpKind::Eq,
10058 right: IrExpr::ColumnRef {
10059 alias: parent_alias.to_string(),
10060 column: "id".to_string(),
10061 pg_type: "uuid".to_string(),
10062 },
10063 }));
10064 let subquery = IrSelect::schema_bound(
10065 IrSource {
10066 poly: None,
10067 type_name: concrete_qname.clone(),
10068 table: concrete_table.clone(),
10069 alias: sub_alias,
10070 },
10071 vec![IrShapePointer::Scalar(IrScalarPointer {
10072 implicit_id: false,
10073 marker_offset: None,
10074 alias: prop.name.clone(),
10075 column: prop.name.clone(),
10076 pg_type: prop.pg_type.clone(),
10077 tuple_shape: self.resolve_property_tuple_shape(&prop),
10078 })],
10079 Some(filter),
10080 );
10081 pointers.push(IrShapePointer::Computed(IrComputedPointer {
10082 marker_offset: None,
10083 alias: prop.name.clone(),
10084 expr: IrExpr::Subquery(Box::new(subquery)),
10085 }));
10086 }
10087
10088 for cd in computed {
10089 let sub_alias = self.fresh_alias();
10090 let filter = IrExpr::BinOp(Box::new(IrBinOp {
10091 left: IrExpr::ColumnRef {
10092 alias: sub_alias.clone(),
10093 column: "id".to_string(),
10094 pg_type: "uuid".to_string(),
10095 },
10096 op: ast::BinOpKind::Eq,
10097 right: IrExpr::ColumnRef {
10098 alias: parent_alias.to_string(),
10099 column: "id".to_string(),
10100 pg_type: "uuid".to_string(),
10101 },
10102 }));
10103 let expr_ast = crate::parse::parse_pointer_expr(&cd.expression).map_err(PyQLError::Syntax)?;
10104 let inner_ir = self.compile_expr(&expr_ast, concrete_td, &sub_alias)?;
10105 let subquery = IrSelect::schema_bound(
10106 IrSource {
10107 poly: None,
10108 type_name: concrete_qname.clone(),
10109 table: concrete_table.clone(),
10110 alias: sub_alias,
10111 },
10112 vec![IrShapePointer::Computed(IrComputedPointer {
10113 marker_offset: None,
10114 alias: cd.name.clone(),
10115 expr: inner_ir,
10116 })],
10117 Some(filter),
10118 );
10119 pointers.push(IrShapePointer::Computed(IrComputedPointer {
10120 marker_offset: None,
10121 alias: cd.name.clone(),
10122 expr: IrExpr::Subquery(Box::new(subquery)),
10123 }));
10124 }
10125
10126 for link in links {
10128 let target_td = self.resolve_type(&link.target)?;
10129 let sub_alias = self.fresh_alias();
10130 let filter = if link.is_junction_backed() {
10131 let (jt_table, jt_module, jt_src_col, jt_tgt_col, _) =
10132 self.junction_info_for(concrete_td, &link.name, &link.target, &link.through)?;
10133 let jt_alias = self.fresh_alias();
10134 let jt_filter = IrExpr::BinOp(Box::new(IrBinOp {
10135 left: IrExpr::BinOp(Box::new(IrBinOp {
10136 left: IrExpr::ColumnRef {
10137 alias: jt_alias.clone(),
10138 column: jt_src_col,
10139 pg_type: "uuid".to_string(),
10140 },
10141 op: ast::BinOpKind::Eq,
10142 right: IrExpr::ColumnRef {
10143 alias: parent_alias.to_string(),
10144 column: "id".to_string(),
10145 pg_type: "uuid".to_string(),
10146 },
10147 })),
10148 op: ast::BinOpKind::And,
10149 right: IrExpr::BinOp(Box::new(IrBinOp {
10150 left: IrExpr::ColumnRef {
10151 alias: jt_alias.clone(),
10152 column: jt_tgt_col,
10153 pg_type: "uuid".to_string(),
10154 },
10155 op: ast::BinOpKind::Eq,
10156 right: IrExpr::ColumnRef {
10157 alias: sub_alias.clone(),
10158 column: "id".to_string(),
10159 pg_type: "uuid".to_string(),
10160 },
10161 })),
10162 }));
10163 let exists_select = IrSelect::schema_bound(
10164 IrSource {
10165 poly: None,
10166 type_name: format!("{}::__jt__", jt_module),
10167 table: jt_table,
10168 alias: jt_alias,
10169 },
10170 vec![],
10171 Some(jt_filter),
10172 );
10173 IrExpr::UnaryOp(Box::new(IrUnaryOp {
10174 op: ast::UnaryOpKind::Exists,
10175 operand: IrExpr::Subquery(Box::new(exists_select)),
10176 }))
10177 } else {
10178 IrExpr::BinOp(Box::new(IrBinOp {
10179 left: IrExpr::ColumnRef {
10180 alias: sub_alias.clone(),
10181 column: "id".to_string(),
10182 pg_type: "uuid".to_string(),
10183 },
10184 op: ast::BinOpKind::Eq,
10185 right: IrExpr::ColumnRef {
10186 alias: parent_alias.to_string(),
10187 column: format!("{}_id", link.name),
10188 pg_type: "uuid".to_string(),
10189 },
10190 }))
10191 };
10192 let sub_shape = Self::pk_returning(target_td);
10193 let subquery = IrSelect::schema_bound(
10194 IrSource {
10195 poly: self.link_target_fanout(target_td),
10196 type_name: format!("{}::{}", target_td.module, target_td.name),
10197 table: target_td.table.clone(),
10198 alias: sub_alias,
10199 },
10200 sub_shape,
10201 Some(filter),
10202 );
10203 pointers.push(IrShapePointer::Computed(IrComputedPointer {
10204 marker_offset: None,
10205 alias: link.name.clone(),
10206 expr: IrExpr::Subquery(Box::new(subquery)),
10207 }));
10208 }
10209
10210 Ok(pointers)
10211 }
10212
10213 fn compile_type_intersection_pointer(
10215 &mut self,
10216 type_ref: &ast::ObjectRef,
10217 tail_steps: &[ast::PathStep],
10218 parent_alias: &str,
10219 marker_offset: Option<usize>,
10220 ) -> Result<IrShapePointer, PyQLError> {
10221 let expr = self.compile_type_intersection_expr_steps(type_ref, tail_steps, parent_alias)?;
10222 let alias = match tail_steps.last() {
10224 Some(ast::PathStep::Name(n)) => n.clone(),
10225 _ => return Err(self.type_err("type intersection must end with a pointer name")),
10226 };
10227 Ok(IrShapePointer::Computed(IrComputedPointer {
10228 alias,
10229 expr,
10230 marker_offset,
10231 }))
10232 }
10233
10234 fn compile_type_intersection_expr(
10236 &mut self,
10237 steps: &[ast::PathStep],
10238 td: &TypeDescriptor,
10239 parent_alias: &str,
10240 ) -> Result<IrExpr, PyQLError> {
10241 use ast::PathStep;
10242 let type_ref = match steps.first() {
10243 Some(PathStep::TypeIntersection(tr)) => tr.clone(),
10244 _ => return Err(self.type_err("expected type intersection")),
10245 };
10246 match self.compile_type_intersection_expr_steps(&type_ref, &steps[1..], parent_alias) {
10251 Ok(ir) => Ok(ir),
10252 Err(fast_path_err) => {
10253 let p = ast::Path {
10254 steps: steps.to_vec(),
10255 partial: true,
10256 };
10257 self.compile_partial_path_as_subquery(&p, td, parent_alias)
10258 .map_err(|_| fast_path_err)
10259 }
10260 }
10261 }
10262
10263 fn compile_type_intersection_expr_steps(
10265 &mut self,
10266 type_ref: &ast::ObjectRef,
10267 tail_steps: &[ast::PathStep],
10268 parent_alias: &str,
10269 ) -> Result<IrExpr, PyQLError> {
10270 use ast::PathStep;
10271 let type_name = match &type_ref.module {
10272 Some(m) => format!("{}::{}", m, type_ref.name),
10273 None => type_ref.name.clone(),
10274 };
10275 let concrete_td = self.resolve_type(&type_name)?;
10276 let concrete_qname = format!("{}::{}", concrete_td.module, concrete_td.name);
10277 let concrete_table = concrete_td.table.clone();
10278
10279 let pointer_name = match tail_steps.first() {
10280 Some(PathStep::Name(n)) => n.as_str(),
10281 _ => return Err(self.type_err("type intersection must be followed by a pointer name, e.g. [is Type].name")),
10282 };
10283
10284 let prop = concrete_td
10285 .properties
10286 .iter()
10287 .find(|p| p.name == pointer_name)
10288 .ok_or_else(|| self.field_err(pointer_name, &concrete_qname))?;
10289 let prop_name = prop.name.clone();
10290 let prop_type = prop.pg_type.clone();
10291
10292 let sub_alias = self.fresh_alias();
10293 let filter = IrExpr::BinOp(Box::new(IrBinOp {
10294 left: IrExpr::ColumnRef {
10295 alias: sub_alias.clone(),
10296 column: "id".to_string(),
10297 pg_type: "uuid".to_string(),
10298 },
10299 op: ast::BinOpKind::Eq,
10300 right: IrExpr::ColumnRef {
10301 alias: parent_alias.to_string(),
10302 column: "id".to_string(),
10303 pg_type: "uuid".to_string(),
10304 },
10305 }));
10306
10307 let poly = self.poly_fanout_for(&concrete_qname);
10308 Ok(IrExpr::Subquery(Box::new(IrSelect::schema_bound(
10309 IrSource {
10310 poly,
10311 type_name: concrete_qname,
10312 table: concrete_table,
10313 alias: sub_alias,
10314 },
10315 vec![IrShapePointer::Scalar(IrScalarPointer {
10316 implicit_id: false,
10317 marker_offset: None,
10318 alias: prop_name.clone(),
10319 column: prop_name,
10320 pg_type: prop_type,
10321 tuple_shape: self.resolve_property_tuple_shape(prop),
10322 })],
10323 Some(filter),
10324 ))))
10325 }
10326
10327 fn compile_shape_element(
10328 &mut self,
10329 el: &ShapeElement,
10330 td: &TypeDescriptor,
10331 alias: &str,
10332 module: &str,
10333 ) -> Result<IrShapePointer, PyQLError> {
10334 if let Some(ast::PathStep::TypeIntersection(type_ref)) = el.path.steps.first()
10336 && el.compexpr.is_none()
10337 && el.path.steps.len() >= 2
10338 {
10339 if let Some(nested) = el.nested.as_deref()
10343 && !nested.is_empty()
10344 && let Some(ast::PathStep::Name(leaf)) = el.path.steps.last()
10345 && let Some(ptr) = self.try_compile_pointer_expr(
10346 &leaf.clone(),
10347 &Expr::Path(el.path.clone()),
10348 td,
10349 alias,
10350 module,
10351 el.marker_offset,
10352 nested,
10353 )?
10354 {
10355 return Ok(ptr);
10356 }
10357 let type_ref = type_ref.clone();
10358 return self.compile_type_intersection_pointer(&type_ref, &el.path.steps[1..], alias, el.marker_offset);
10359 }
10360
10361 let pointer_name = path_leaf(&el.path)?;
10362
10363 if pointer_name == "__type__" && el.compexpr.is_none() {
10367 let expr = if self.is_polymorphic(td) {
10368 IrExpr::ColumnRef {
10369 alias: alias.to_string(),
10370 column: "__type__".to_string(),
10371 pg_type: "text".to_string(),
10372 }
10373 } else {
10374 IrExpr::Literal(IrLiteral::Str(format!("{}::{}", td.module, td.name)))
10375 };
10376 return Ok(IrShapePointer::Computed(IrComputedPointer {
10377 marker_offset: el.marker_offset,
10378 alias: "__type__".to_string(),
10379 expr,
10380 }));
10381 }
10382
10383 if let Some(compexpr) = &el.compexpr
10390 && let Some((call, value_expr, check_expr)) =
10391 Self::asserted_pointer_expr(compexpr, el.nested.as_deref().unwrap_or(&[]))
10392 {
10393 let element_for = |expr: Expr| ShapeElement {
10394 compexpr: Some(expr),
10395 nested: None,
10396 ..el.clone()
10397 };
10398 if let Ok(ptr) = self.compile_shape_element(&element_for(value_expr), td, alias, module)
10403 && ptr.is_object_pointer()
10404 {
10405 let check = match check_expr {
10406 Some(expr) => Some(self.compile_shape_element(&element_for(expr), td, alias, module)?),
10407 None => None,
10408 };
10409 let message = self.assert_message(&call, Some((td, alias)))?;
10410 return Ok(IrShapePointer::Asserted(Box::new(IrAssertedPointer {
10411 fn_name: call.name,
10412 inner: ptr,
10413 check,
10414 message,
10415 })));
10416 }
10417 }
10418
10419 if let Some(compexpr) = &el.compexpr
10423 && let Some(guarded) = guarded_object_branch(compexpr)
10424 {
10425 let element = ShapeElement {
10426 compexpr: Some(guarded),
10427 ..el.clone()
10428 };
10429 return self.compile_shape_element(&element, td, alias, module);
10430 }
10431
10432 if let Some(compexpr) = &el.compexpr {
10434 let inner_declared: Vec<ShapeElement> = Self::replaced_subject_shape(compexpr)
10442 .iter()
10443 .filter(|e| e.compexpr.is_some())
10444 .cloned()
10445 .collect();
10446 let restore = (!inner_declared.is_empty()).then(|| {
10447 let mut scope = self.active_declared_pointers.clone();
10448 scope.extend(inner_declared);
10449 std::mem::replace(&mut self.active_declared_pointers, scope)
10450 });
10451 let pointer = self.try_compile_pointer_expr(
10452 pointer_name,
10453 compexpr,
10454 td,
10455 alias,
10456 module,
10457 el.marker_offset,
10458 el.nested.as_deref().unwrap_or(&[]),
10459 );
10460 if let Some(previous) = restore {
10461 self.active_declared_pointers = previous;
10462 }
10463 if let Some(ptr) = pointer? {
10464 return Ok(ptr);
10465 }
10466 if let Some((fc, call_nested)) = Self::object_call_with_shape(compexpr, el.nested.as_deref())
10471 && let Some(fs) = self.compile_fn_object_source(fc, &call_nested)?
10472 {
10473 return Ok(IrShapePointer::Computed(IrComputedPointer {
10474 marker_offset: el.marker_offset,
10475 alias: pointer_name.to_string(),
10476 expr: IrExpr::ArrayFromSelect(Box::new(IrArraySource::ObjectFunction(Box::new(fs)))),
10477 }));
10478 }
10479 let ir = match compexpr {
10483 Expr::TypeIs { expr, ty } => self.compile_type_is_set(expr, ty, Some((td, alias)))?,
10484 _ => self.in_value_position(|c| c.compile_expr(compexpr, td, alias))?,
10485 };
10486 if let IrExpr::ArrayFromSelect(src) = ir {
10488 if let IrArraySource::RawExpr {
10489 source,
10490 poly_implementors,
10491 poly_columns,
10492 expr,
10493 } = *src
10494 {
10495 return Ok(IrShapePointer::ScalarSet(IrScalarSetPointer {
10496 alias: pointer_name.to_string(),
10497 source,
10498 poly_implementors,
10499 poly_columns,
10500 bool_expr: expr,
10501 }));
10502 }
10503 return Ok(IrShapePointer::Computed(IrComputedPointer {
10504 marker_offset: el.marker_offset,
10505 alias: pointer_name.to_string(),
10506 expr: IrExpr::ArrayFromSelect(src),
10507 }));
10508 }
10509 return Ok(IrShapePointer::Computed(IrComputedPointer {
10510 marker_offset: el.marker_offset,
10511 alias: pointer_name.to_string(),
10512 expr: ir,
10513 }));
10514 }
10515
10516 if let Some(p) = Self::resolve_property(td, pointer_name) {
10518 return Ok(IrShapePointer::Scalar(IrScalarPointer {
10519 implicit_id: false,
10520 marker_offset: el.marker_offset,
10521 alias: pointer_name.to_string(),
10522 column: p.name.clone(),
10523 pg_type: p.pg_type.clone(),
10524 tuple_shape: self.resolve_property_tuple_shape(p),
10525 }));
10526 }
10527
10528 if let Some(l) = Self::resolve_link(td, pointer_name) {
10530 let target_td = self.resolve_type(&l.target)?;
10531 let sub_alias = self.fresh_alias();
10532 let nested_elements = el.nested.as_deref().unwrap_or(&[]);
10533
10534 let (regular_els, link_properties): (Vec<ShapeElement>, Vec<IrLinkProp>) = if l.is_junction_backed() {
10539 let mut regular = Vec::new();
10540 let mut props = Vec::new();
10541 for nel in nested_elements {
10542 if let [ast::PathStep::LinkProp(name)] = nel.path.steps.as_slice() {
10543 props.push(IrLinkProp { name: name.clone() });
10544 } else {
10545 regular.push(nel.clone());
10546 }
10547 }
10548 (regular, props)
10549 } else {
10550 (nested_elements.to_vec(), vec![])
10551 };
10552
10553 let sub_shape = self.compile_shape(®ular_els, target_td, &sub_alias, &target_td.module.clone())?;
10554 let subquery = self.link_target_select(
10555 target_td,
10556 IrSource {
10557 poly: None,
10558 type_name: format!("{}::{}", target_td.module, target_td.name),
10559 table: target_td.table.clone(),
10560 alias: sub_alias,
10561 },
10562 sub_shape,
10563 );
10564 let correlation = if l.is_junction_backed() {
10565 let join = self.build_multilink_join(td, &l.name, &l.target, &l.through)?;
10566 IrSingleLinkCorrelation::Junction {
10567 join,
10568 target_pk: "id".to_string(),
10569 }
10570 } else {
10571 IrSingleLinkCorrelation::Fk {
10572 fk_column: format!("{}_id", l.name),
10573 target_pk: "id".to_string(),
10574 }
10575 };
10576 return Ok(IrShapePointer::SingleLink(IrSingleLinkPointer {
10577 marker_offset: el.marker_offset,
10578 alias: pointer_name.to_string(),
10579 correlation,
10580 subquery,
10581 link_properties,
10582 }));
10583 }
10584
10585 if Self::resolve_multilink(td, pointer_name).is_some() {
10587 return self.compile_multilink_pointer(pointer_name, pointer_name, td, alias, module, el);
10588 }
10589
10590 if let Some(cd) = self.resolve_computed(td, pointer_name) {
10592 return self.compile_declared_computed(
10593 &cd,
10594 td,
10595 alias,
10596 module,
10597 el.marker_offset,
10598 el.nested.as_deref().unwrap_or(&[]),
10599 );
10600 }
10601
10602 if let Some(declared) = self
10605 .active_declared_pointers
10606 .iter()
10607 .find(|d| path_leaf(&d.path).is_ok_and(|n| n == pointer_name))
10608 .cloned()
10609 && let Some(expr) = declared.compexpr.clone()
10610 {
10611 let nested = if el.nested.as_deref().unwrap_or(&[]).is_empty() {
10612 declared.nested.clone().unwrap_or_default()
10613 } else {
10614 el.nested.clone().unwrap_or_default()
10615 };
10616 return self.compile_computed_expr(pointer_name, &expr, td, alias, module, el.marker_offset, &nested, None);
10617 }
10618
10619 Err(self.field_err(pointer_name, &format!("{}::{}", td.module, td.name)))
10620 }
10621
10622 #[allow(clippy::too_many_arguments)]
10629 fn compile_chained_link_pointer(
10630 &mut self,
10631 pointer_name: &str,
10632 path: &ast::Path,
10633 td: &TypeDescriptor,
10634 alias: &str,
10635 nested: &[ShapeElement],
10636 modifiers: Option<&ast::SelectStmt>,
10637 multi: bool,
10638 tail: usize,
10639 ) -> Result<IrShapePointer, PyQLError> {
10640 let mut steps = vec![ast::PathStep::Name(format!("{}::{}", td.module, td.name))];
10641 steps.extend(path.steps.iter().cloned());
10642 let full_path = ast::Path { steps, partial: false };
10643 let synthetic = ast::SelectStmt {
10644 result: Expr::Path(full_path.clone()),
10645 filter: modifiers.and_then(|m| m.filter.clone()),
10646 order_by: modifiers.map(|m| m.order_by.clone()).unwrap_or_default(),
10647 offset: modifiers.and_then(|m| m.offset.clone()),
10648 limit: modifiers.and_then(|m| m.limit.clone()),
10649 lock: None,
10650 };
10651 let mut path_select = self.compile_path_select_with_tail(&synthetic, &full_path, nested, false, tail, &[])?;
10652 Self::correlate_path_select(&mut path_select, alias);
10653 let expr = if multi {
10657 IrExpr::ArrayFromSelect(Box::new(IrArraySource::PathSelect(Box::new(path_select))))
10658 } else {
10659 IrExpr::ObjectPathSubquery(Box::new(path_select))
10660 };
10661 Ok(IrShapePointer::Computed(IrComputedPointer {
10662 marker_offset: None,
10663 alias: pointer_name.to_string(),
10664 expr,
10665 }))
10666 }
10667
10668 fn compile_multilink_pointer(
10669 &mut self,
10670 output_alias: &str,
10671 ml_name: &str,
10672 td: &TypeDescriptor,
10673 _parent_alias: &str,
10674 module: &str,
10675 el: &ShapeElement,
10676 ) -> Result<IrShapePointer, PyQLError> {
10677 let ml = Self::resolve_multilink(td, ml_name)
10678 .expect("caller verified multilink exists")
10679 .clone();
10680 let target_td = self.resolve_type(&ml.target)?;
10681 let sub_alias = self.fresh_alias();
10682 let nested_elements = el.nested.as_deref().unwrap_or(&[]);
10683
10684 let mut link_properties: Vec<IrLinkProp> = Vec::new();
10686 let mut regular_els: Vec<ShapeElement> = Vec::new();
10687 for nel in nested_elements {
10688 if let [ast::PathStep::LinkProp(name)] = nel.path.steps.as_slice() {
10689 link_properties.push(IrLinkProp { name: name.clone() });
10690 } else {
10691 regular_els.push(nel.clone());
10692 }
10693 }
10694 let has_splat = nested_elements.iter().any(|nel| {
10695 nel.splat.is_some() && !matches!(nel.path.steps.first(), Some(ast::PathStep::TypeIntersection(_)))
10696 });
10697 if has_splat {
10698 for prop in self.splat_link_properties(&ml)? {
10699 if !link_properties.iter().any(|existing| existing.name == prop.name) {
10700 link_properties.push(prop);
10701 }
10702 }
10703 }
10704
10705 let sub_shape = self.compile_shape(®ular_els, target_td, &sub_alias, &target_td.module.clone())?;
10706
10707 let join = if let Some(through_qname) = &ml.through {
10708 let through_td = self.resolve_type(through_qname)?;
10709 if through_td.junction {
10710 IrMultiLinkJoin::Standard {
10714 junction_table: self.owner_junction(td, ml_name, Some(through_td)),
10715 module: td.module.clone(),
10716 }
10717 } else {
10718 let source_qname = format!("{}::{}", td.module, td.name);
10719 let source_col = through_td
10720 .links
10721 .iter()
10722 .find(|l| l.target == source_qname)
10723 .ok_or_else(|| {
10724 PyQLError::Type(PyQLTypeError {
10725 message: format!("through type {through_qname} has no link to source type {source_qname}"),
10726 position: Position { line: 0, col: 0 },
10727 })
10728 })?
10729 .name
10730 .clone();
10731 let target_col = through_td
10732 .links
10733 .iter()
10734 .find(|l| l.target == ml.target && l.name != source_col)
10735 .or_else(|| through_td.links.iter().find(|l| l.target == ml.target))
10736 .ok_or_else(|| {
10737 PyQLError::Type(PyQLTypeError {
10738 message: format!("through type {through_qname} has no link to target type {}", ml.target),
10739 position: Position { line: 0, col: 0 },
10740 })
10741 })?
10742 .name
10743 .clone();
10744 IrMultiLinkJoin::Through {
10745 junction_table: through_td.table.clone(),
10746 module: through_td.module.clone(),
10747 source_col,
10748 target_col,
10749 }
10750 }
10751 } else {
10752 IrMultiLinkJoin::Standard {
10753 junction_table: self.owner_junction(td, ml_name, None),
10754 module: module.to_string(),
10755 }
10756 };
10757
10758 self.link_prop_scope
10761 .push(ml.through.clone().map(|t| (t, "jt".to_string())));
10762 let modifiers = (|c: &mut Self| -> Result<SelectModifiers, PyQLError> {
10763 Ok((
10764 el.filter
10765 .as_ref()
10766 .map(|f| c.compile_expr(f, target_td, &sub_alias))
10767 .transpose()?,
10768 el.order_by
10769 .iter()
10770 .map(|s| c.compile_sort(s, target_td, &sub_alias))
10771 .collect::<Result<_, _>>()?,
10772 el.offset
10773 .as_ref()
10774 .map(|e| c.compile_expr(e, target_td, &sub_alias))
10775 .transpose()?,
10776 el.limit
10777 .as_ref()
10778 .map(|e| c.compile_expr(e, target_td, &sub_alias))
10779 .transpose()?,
10780 ))
10781 })(self);
10782 self.link_prop_scope.pop();
10783 let (filter, order_by, offset, limit) = modifiers?;
10784 let subquery = IrSelect {
10785 rows: vec![IrRowSource::Bound {
10786 source: IrSource {
10787 poly: self.link_target_fanout(target_td),
10788 type_name: format!("{}::{}", target_td.module, target_td.name),
10789 table: target_td.table.clone(),
10790 alias: sub_alias.clone(),
10791 },
10792 shape: sub_shape,
10793 }],
10794 filter,
10795 order_by,
10796 offset,
10797 limit,
10798 distinct: false,
10799 dml_source: None,
10800 polymorphic: false,
10801 poly_implementors: vec![],
10802 poly_columns: vec![],
10803 lock: None,
10804 };
10805
10806 Ok(IrShapePointer::MultiLink(IrMultiLinkPointer {
10807 marker_offset: el.marker_offset,
10808 alias: output_alias.to_string(),
10809 join,
10810 subquery,
10811 link_properties,
10812 single: false,
10813 }))
10814 }
10815
10816 fn compile_backlink_pointer(
10826 &mut self,
10827 output_alias: &str,
10828 path: &ast::Path,
10829 current_qname: &str,
10830 nested_elements: &[ShapeElement],
10831 marker_offset: Option<usize>,
10832 modifiers: Option<&ast::SelectStmt>,
10833 ) -> Result<IrShapePointer, PyQLError> {
10834 use ast::PathStep;
10835
10836 let backlink_name = match path.steps.first() {
10837 Some(PathStep::Backlink(n)) => n.clone(),
10838 _ => return Err(self.type_err("internal: expected backlink step")),
10839 };
10840 let type_ref = match path.steps.get(1) {
10841 Some(PathStep::TypeIntersection(tr)) => tr,
10842 _ => {
10843 return Err(PyQLError::Type(PyQLTypeError {
10844 message: format!(
10845 "backlink '.< {backlink_name}' requires a type intersection, \
10846 e.g.: .< {backlink_name}[is SomeType]"
10847 ),
10848 position: Position { line: 0, col: 0 },
10849 }));
10850 }
10851 };
10852 if path.steps.len() > 2 {
10853 return Err(self.type_err(
10854 "further path traversal after a backlink shape is not yet supported \
10855 (e.g. '.<link[is Type].property') — attach a nested shape instead: \
10856 '.<link[is Type] { property }'",
10857 ));
10858 }
10859
10860 let type_name = match &type_ref.module {
10861 Some(m) => format!("{}::{}", m, type_ref.name),
10862 None => type_ref.name.clone(),
10863 };
10864 let owner_td = self.backlink_owner(self.resolve_type(&type_name)?, &backlink_name, current_qname)?;
10865 let owner_qname = format!("{}::{}", owner_td.module, owner_td.name);
10866
10867 let join = if let Some(l) = owner_td
10868 .links
10869 .iter()
10870 .find(|l| l.name == backlink_name && self.link_target_reaches(&l.target, current_qname))
10871 {
10872 if l.is_junction_backed() {
10873 let (junction_table, module, owner_col, current_col, _) = self.link_junction_info(owner_td, l)?;
10874 IrMultiLinkJoin::BacklinkJunction {
10875 junction_table,
10876 module,
10877 owner_col,
10878 current_col,
10879 }
10880 } else {
10881 IrMultiLinkJoin::BacklinkFk {
10882 fk_col: format!("{}_id", backlink_name),
10883 }
10884 }
10885 } else if let Some(ml) = owner_td
10886 .multilinks
10887 .iter()
10888 .find(|ml| ml.name == backlink_name && self.link_target_reaches(&ml.target, current_qname))
10889 .cloned()
10890 {
10891 let (junction_table, module, _, _, _) = self.multilink_junction_info(owner_td, &ml)?;
10892 IrMultiLinkJoin::BacklinkJunction {
10893 junction_table,
10894 module,
10895 owner_col: "source".to_string(),
10896 current_col: "target".to_string(),
10897 }
10898 } else {
10899 return Err(PyQLError::Type(PyQLTypeError {
10900 message: format!(
10901 "type {} has no link or multi-link '{}' pointing to {}",
10902 owner_qname, backlink_name, current_qname,
10903 ),
10904 position: Position { line: 0, col: 0 },
10905 }));
10906 };
10907
10908 let sub_alias = self.fresh_alias();
10909 let sub_shape = self.compile_shape(nested_elements, owner_td, &sub_alias, &owner_td.module.clone())?;
10910
10911 let (filter, order_by, offset, limit) = match modifiers {
10920 Some(sel) => self.compile_path_modifiers(sel, owner_td, &sub_alias)?,
10921 None => (None, vec![], None, None),
10922 };
10923 let subquery = IrSelect {
10924 rows: vec![IrRowSource::Bound {
10925 source: IrSource {
10926 poly: self.poly_fanout_for(&owner_qname),
10927 type_name: owner_qname,
10928 table: owner_td.table.clone(),
10929 alias: sub_alias.clone(),
10930 },
10931 shape: sub_shape,
10932 }],
10933 filter,
10934 order_by,
10935 offset,
10936 limit,
10937 distinct: false,
10938 dml_source: None,
10939 polymorphic: false,
10940 poly_implementors: vec![],
10941 poly_columns: vec![],
10942 lock: None,
10943 };
10944
10945 Ok(IrShapePointer::MultiLink(IrMultiLinkPointer {
10946 alias: output_alias.to_string(),
10947 join,
10948 subquery,
10949 link_properties: vec![],
10950 marker_offset,
10951 single: self.backlink_is_single(owner_td, &backlink_name, current_qname) || limits_to_one(modifiers),
10952 }))
10953 }
10954
10955 fn compile_fn_object_source(
10963 &mut self,
10964 fc: &ast::FunctionCall,
10965 nested: &[ShapeElement],
10966 ) -> Result<Option<IrFunctionSelect>, PyQLError> {
10967 let Some(fd) = self.schema.functions.iter().find(|f| {
10968 let module_matches = fc.module.as_deref().map(|m| m == f.module.as_str()).unwrap_or(true);
10969 module_matches && f.name == fc.name && f.return_is_object
10970 }) else {
10971 return Ok(None);
10972 };
10973 let (fn_module, fn_name, return_type_name) = (fd.module.clone(), fd.name.clone(), fd.return_pg_type.clone());
10974 let return_td = self.resolve_type(&return_type_name)?;
10975 let alias = self.fresh_alias();
10976 let shape = if nested.is_empty() {
10977 Self::pk_returning(return_td)
10978 } else {
10979 self.compile_shape(nested, return_td, &alias, &return_td.module)?
10980 };
10981 let mut args = fc
10982 .args
10983 .iter()
10984 .map(|a| self.compile_free_expr(a))
10985 .collect::<Result<Vec<_>, _>>()?;
10986 if let Some(globals) = self.globals_arg_for_call(&format!("{fn_module}::{fn_name}"))? {
10989 args.insert(0, globals);
10990 }
10991 Ok(Some(IrFunctionSelect {
10992 fn_module,
10993 fn_name,
10994 fn_args: args,
10995 alias,
10996 type_name: format!("{}::{}", return_td.module, return_td.name),
10997 polymorphic: false,
10998 poly_implementors: vec![],
10999 poly_columns: vec![],
11000 shape,
11001 filter: None,
11002 order_by: vec![],
11003 offset: None,
11004 limit: None,
11005 distinct: false,
11006 }))
11007 }
11008
11009 fn asserted_pointer_expr(expr: &Expr, nested: &[ShapeElement]) -> Option<(ast::FunctionCall, Expr, Option<Expr>)> {
11017 if let Expr::Shape(sh) = expr
11025 && let Some(subquery @ Expr::SubQuery(_)) = sh.expr.as_ref()
11026 {
11027 return Self::asserted_pointer_expr(subquery, &sh.elements);
11028 }
11029 if let Expr::SubQuery(stmt) = expr
11030 && let Stmt::Select(sel) = stmt.as_ref()
11031 {
11032 let (call, inner, _) = Self::asserted_pointer_expr(&sel.result, nested)?;
11033 let rebuild = |offset: Option<Expr>, limit: Option<Expr>| {
11034 Expr::SubQuery(Box::new(Stmt::Select(ast::SelectStmt {
11035 result: inner.clone(),
11036 filter: sel.filter.clone(),
11037 order_by: sel.order_by.clone(),
11038 offset,
11039 limit,
11040 lock: sel.lock.clone(),
11041 })))
11042 };
11043 let value = rebuild(sel.offset.clone(), sel.limit.clone());
11044 let check = (sel.offset.is_some() || sel.limit.is_some()).then(|| rebuild(None, None));
11045 return Some((call, value, check));
11046 }
11047 let (call, nested) = match expr {
11051 Expr::Shape(sh) => match sh.expr.as_ref() {
11052 Some(Expr::FunctionCall(f)) => (f, sh.elements.clone()),
11053 _ => return None,
11054 },
11055 Expr::FunctionCall(f) => (f, nested.to_vec()),
11056 _ => return None,
11057 };
11058 if call.module.is_some() && call.module.as_deref() != Some("std") {
11059 return None;
11060 }
11061 if !matches!(call.name.as_str(), "assert_exists" | "assert_distinct") {
11062 return None;
11063 }
11064 let [arg] = call.args.as_slice() else {
11065 return None;
11066 };
11067 let inner = match (&arg, nested.is_empty()) {
11072 (Expr::Shape(_), _) | (_, true) => arg.clone(),
11073 _ => Expr::Shape(Box::new(ast::ShapeExpr {
11074 expr: Some(arg.clone()),
11075 elements: nested.clone(),
11076 marker_offset: None,
11077 })),
11078 };
11079 Some((call.clone(), inner, None))
11080 }
11081
11082 fn object_call_with_shape<'e>(
11086 expr: &'e Expr,
11087 nested: Option<&[ShapeElement]>,
11088 ) -> Option<(&'e ast::FunctionCall, Vec<ShapeElement>)> {
11089 match expr {
11090 Expr::Shape(sh) => match sh.expr.as_ref() {
11091 Some(Expr::FunctionCall(f)) => Some((f, sh.elements.clone())),
11092 _ => None,
11093 },
11094 Expr::FunctionCall(f) => Some((f, nested.unwrap_or(&[]).to_vec())),
11095 _ => None,
11096 }
11097 }
11098
11099 fn compile_declared_computed(
11100 &mut self,
11101 cd: &crate::schema::ComputedDescriptor,
11102 td: &TypeDescriptor,
11103 alias: &str,
11104 module: &str,
11105 marker_offset: Option<usize>,
11106 nested: &[ShapeElement],
11107 ) -> Result<IrShapePointer, PyQLError> {
11108 let expr_ast = crate::parse::parse_pointer_expr(&cd.expression).map_err(PyQLError::Syntax)?;
11109 let declared_on = self.computed_declared_on(td, &cd.name);
11110 self.compile_computed_expr(
11111 &cd.name,
11112 &expr_ast,
11113 td,
11114 alias,
11115 module,
11116 marker_offset,
11117 nested,
11118 declared_on,
11119 )
11120 }
11121
11122 fn computed_declared_on(&self, td: &TypeDescriptor, name: &str) -> Option<(String, String)> {
11125 td.bases
11126 .iter()
11127 .chain(td.parents.iter())
11128 .chain(td.interfaces.iter())
11129 .filter_map(|qualified| self.resolve_type(qualified).ok())
11130 .find(|ancestor| ancestor.computed.iter().any(|c| c.name == name))
11131 .map(|ancestor| (ancestor.name.clone(), format!("{}::{}", ancestor.module, ancestor.name)))
11132 }
11133
11134 #[allow(clippy::too_many_arguments)]
11137 fn compile_computed_expr(
11138 &mut self,
11139 name: &str,
11140 expr_ast: &Expr,
11141 td: &TypeDescriptor,
11142 alias: &str,
11143 module: &str,
11144 marker_offset: Option<usize>,
11145 nested: &[ShapeElement],
11146 declared_on: Option<(String, String)>,
11147 ) -> Result<IrShapePointer, PyQLError> {
11148 self.anchors.push(SelectAnchor {
11152 type_name: td.name.clone(),
11153 qualified: format!("{}::{}", td.module, td.name),
11154 alias: alias.to_string(),
11155 detached: false,
11156 declared_on,
11157 });
11158 let result = (|compiler: &mut Self| -> Result<IrShapePointer, PyQLError> {
11159 if let Some(ptr) =
11160 compiler.try_compile_pointer_expr(name, expr_ast, td, alias, module, marker_offset, nested)?
11161 {
11162 return Ok(ptr);
11163 }
11164 if let Expr::FunctionCall(fc) = expr_ast
11169 && let Some(fs) = compiler.compile_fn_object_source(fc, nested)?
11170 {
11171 return Ok(IrShapePointer::Computed(IrComputedPointer {
11172 marker_offset,
11173 alias: name.to_string(),
11174 expr: IrExpr::ArrayFromSelect(Box::new(IrArraySource::ObjectFunction(Box::new(fs)))),
11175 }));
11176 }
11177 let ir = compiler.in_value_position(|c| c.compile_expr(expr_ast, td, alias))?;
11178 Ok(IrShapePointer::Computed(IrComputedPointer {
11179 marker_offset,
11180 alias: name.to_string(),
11181 expr: ir,
11182 }))
11183 })(self);
11184 self.anchors.pop();
11185 result
11186 }
11187
11188 fn split_path_result(result: &Expr) -> Option<(&ast::Path, &[ShapeElement])> {
11192 match result {
11193 Expr::Path(p) => Some((p, &[])),
11194 Expr::Detached(inner) => Self::split_path_result(inner),
11198 Expr::Shape(sh) => match &sh.expr {
11199 Some(Expr::Path(p)) => Some((p, sh.elements.as_slice())),
11200 _ => None,
11201 },
11202 _ => None,
11203 }
11204 }
11205
11206 fn function_subject(e: &Expr) -> Option<(&ast::FunctionCall, Option<&ast::SelectStmt>)> {
11210 match e {
11211 Expr::FunctionCall(fc) => Some((fc, None)),
11212 Expr::SubQuery(stmt) => match stmt.as_ref() {
11213 Stmt::Select(sel) => match &sel.result {
11214 Expr::FunctionCall(fc) => Some((fc, Some(sel))),
11215 _ => None,
11216 },
11217 _ => None,
11218 },
11219 _ => None,
11220 }
11221 }
11222
11223 fn resolve_object_fn(&self, fc: &ast::FunctionCall) -> Option<&'a FunctionDescriptor> {
11225 self.schema.functions.iter().find(|f| {
11226 let module_matches = fc.module.as_deref().map(|m| m == f.module.as_str()).unwrap_or(true);
11227 module_matches && f.name == fc.name && f.return_is_object
11228 })
11229 }
11230
11231 fn replaced_subject_shape(e: &Expr) -> &[ShapeElement] {
11236 let Expr::Shape(sh) = e else { return &[] };
11237 if sh.elements.is_empty() {
11238 return &[];
11239 }
11240 match sh.expr.as_ref() {
11241 Some(inner) => Self::pointer_subject(inner).map(|(_, nested, _)| nested).unwrap_or(&[]),
11242 None => &[],
11243 }
11244 }
11245
11246 fn pointer_subject(e: &Expr) -> Option<(&ast::Path, &[ShapeElement], Option<&ast::SelectStmt>)> {
11251 match e {
11252 Expr::Path(p) => Some((p, &[], None)),
11253 Expr::SubQuery(stmt) => match stmt.as_ref() {
11254 Stmt::Select(sel) => {
11255 let (p, inner) = Self::split_path_result(&sel.result)?;
11256 Some((p, inner, Some(sel)))
11257 }
11258 _ => None,
11259 },
11260 Expr::Shape(sh) => {
11261 let (path, inner, modifiers) = Self::pointer_subject(sh.expr.as_ref()?)?;
11262 let nested = if sh.elements.is_empty() {
11263 inner
11264 } else {
11265 sh.elements.as_slice()
11266 };
11267 Some((path, nested, modifiers))
11268 }
11269 _ => None,
11270 }
11271 }
11272
11273 fn field_access_over_select(expr: &Expr) -> Option<(ast::SelectStmt, usize)> {
11279 let (root, fields) = Self::peel_field_access_chain(expr);
11280 if fields.is_empty() {
11281 return None;
11282 }
11283 let Expr::SubQuery(stmt) = root else { return None };
11284 let Stmt::Select(sel) = stmt.as_ref() else {
11285 return None;
11286 };
11287 let Expr::Path(path) = &sel.result else { return None };
11288 if !path.partial {
11289 return None;
11290 }
11291 let mut steps = path.steps.clone();
11292 let count = fields.len();
11293 steps.extend(fields.into_iter().map(ast::PathStep::Name));
11294 Some((
11295 ast::SelectStmt {
11296 result: Expr::Path(ast::Path { steps, partial: true }),
11297 ..sel.clone()
11298 },
11299 count,
11300 ))
11301 }
11302
11303 #[allow(clippy::too_many_arguments)]
11324 fn try_compile_pointer_expr(
11325 &mut self,
11326 pointer_name: &str,
11327 compexpr: &Expr,
11328 td: &TypeDescriptor,
11329 alias: &str,
11330 module: &str,
11331 marker_offset: Option<usize>,
11332 nested_override: &[ShapeElement],
11333 ) -> Result<Option<IrShapePointer>, PyQLError> {
11334 if let Some((sel, tail)) = Self::field_access_over_select(compexpr)
11344 && let Expr::Path(path) = &sel.result
11345 {
11346 let (head_steps, tail_steps) = path.steps.split_at(path.steps.len() - tail);
11347 let (head_multi, head_target) = self.walk_path_types(td, head_steps, MAX_COMPUTED_SPLICES);
11348 let Some(head_target) = head_target else {
11349 return Ok(None);
11350 };
11351 let (tail_multi, target) = self.walk_path_types(head_target, tail_steps, MAX_COMPUTED_SPLICES);
11352 if target.is_none() {
11353 return Ok(None);
11354 }
11355 let multi = tail_multi || (head_multi && !limits_to_one(Some(&sel)));
11356 if !multi && nested_override.is_empty() {
11357 return Ok(None);
11358 }
11359 let path = path.clone();
11360 return self
11361 .compile_chained_link_pointer(pointer_name, &path, td, alias, nested_override, Some(&sel), multi, tail)
11362 .map(Some);
11363 }
11364 let Some((path, declared_nested, modifiers)) = Self::pointer_subject(compexpr) else {
11365 return Ok(None);
11366 };
11367 if !path.partial {
11368 return Ok(None);
11369 }
11370 let nested = if nested_override.is_empty() {
11374 declared_nested
11375 } else {
11376 nested_override
11377 };
11378
11379 let ml_name = match path.steps.as_slice() {
11383 [ast::PathStep::Name(n)] if Self::resolve_multilink(td, n).is_some() => n.clone(),
11384 [ast::PathStep::Backlink(_)] | [ast::PathStep::Backlink(_), ast::PathStep::TypeIntersection(_)] => {
11389 let current_qname = format!("{}::{}", td.module, td.name);
11390 let path = path.clone();
11391 let nested = nested.to_vec();
11392 return self
11393 .compile_backlink_pointer(pointer_name, &path, ¤t_qname, &nested, marker_offset, modifiers)
11394 .map(Some);
11395 }
11396 steps
11406 if matches!(
11407 steps.first(),
11408 Some(ast::PathStep::Name(_) | ast::PathStep::TypeIntersection(_))
11409 ) =>
11410 {
11411 let (multi, target) = self.walk_path_types(td, steps, MAX_COMPUTED_SPLICES);
11412 if target.is_none() || (!multi && nested.is_empty()) {
11418 return Ok(None);
11419 }
11420 let path = path.clone();
11421 let nested = nested.to_vec();
11422 return self
11423 .compile_chained_link_pointer(
11424 pointer_name,
11425 &path,
11426 td,
11427 alias,
11428 &nested,
11429 modifiers,
11430 multi && !limits_to_one(modifiers),
11431 0,
11432 )
11433 .map(Some);
11434 }
11435 _ => return Ok(None),
11436 };
11437
11438 let synthetic = ShapeElement {
11439 path: path.clone(),
11440 splat: None,
11441 nested: Some(nested.to_vec()),
11442 compexpr: None,
11443 op: ast::ShapeOp::Assign,
11444 filter: modifiers.and_then(|s| s.filter.clone()),
11445 order_by: modifiers.map(|s| s.order_by.clone()).unwrap_or_default(),
11446 offset: modifiers.and_then(|s| s.offset.clone()),
11447 limit: modifiers.and_then(|s| s.limit.clone()),
11448 marker_offset,
11449 };
11450 let mut pointer = self.compile_multilink_pointer(pointer_name, &ml_name, td, alias, module, &synthetic)?;
11451 if let IrShapePointer::MultiLink(link) = &mut pointer {
11452 link.single = limits_to_one(modifiers);
11453 }
11454 Ok(Some(pointer))
11455 }
11456
11457 fn compile_subquery_expr(
11477 &mut self,
11478 stmt: &Stmt,
11479 extra_fields: &[String],
11480 ctx: Option<(&TypeDescriptor, &str)>,
11481 outer_shape: &[ShapeElement],
11482 ) -> Result<IrExpr, PyQLError> {
11483 if let Stmt::With(w) = stmt {
11487 for alias in &w.aliases {
11488 if self.bind_inline_if_correlated(&alias.name, &alias.expr, ctx)?
11489 || self.bind_group(&alias.name, &alias.expr)?
11490 {
11491 continue;
11492 }
11493 match self.hoisted_binding_sources.get(&alias.name) {
11501 Some(existing) if *existing == alias.expr => continue,
11502 Some(_) => {
11503 return Err(self.type_err(&format!(
11504 "two different `with` bindings named '{}' end up in one statement; \
11505 rename one of them",
11506 alias.name
11507 )));
11508 }
11509 None => {}
11510 }
11511 let (ir_stmt, correlated_to) = self.compile_binding_in_scope(&alias.expr)?;
11512 let sql_name = self.claim_cte_sql_name(&alias.name);
11513 if let Some(slot) = correlated_to.as_deref().and_then(|i| i.strip_prefix("_for_")) {
11514 self.correlated_bindings.insert(sql_name.clone(), slot.to_string());
11515 }
11516 let type_name = self.register_cte(&alias.name, &ir_stmt);
11517 self.hoisted_binding_sources
11518 .insert(alias.name.clone(), alias.expr.clone());
11519 self.hoisted_ctes.push(IrCteDef {
11520 name: sql_name,
11521 stmt: ir_stmt,
11522 type_name,
11523 correlated_to,
11524 });
11525 }
11526 let inner = (*w.stmt).clone();
11527 return self.compile_subquery_expr(&inner, extra_fields, ctx, outer_shape);
11528 }
11529
11530 if extra_fields.is_empty()
11531 && let Stmt::Select(sel) = stmt
11532 && let Expr::Shape(sh) = &sel.result
11533 && let Some(Expr::SubQuery(subject)) = &sh.expr
11534 && matches!(subject.as_ref(), Stmt::Group(_))
11535 && let IrStmt::Group(grp) = self.compile_stmt(stmt)?
11536 {
11537 return Ok(IrExpr::ArrayFromSelect(Box::new(IrArraySource::Group(Box::new(grp)))));
11538 }
11539
11540 if let Stmt::Select(inner) = stmt
11544 && let Expr::Shape(sh) = &inner.result
11545 && let Some(subject) = sh.expr.as_ref()
11546 && let Some(operands) = Self::union_of_relative_paths(subject)
11547 && let Some((td, alias)) = ctx
11548 {
11549 let elements = sh.elements.clone();
11550 let operands_reach_many = self.relative_paths_reach_many(td, &operands);
11551 let mut branches = Vec::with_capacity(operands.len());
11552 for path in operands {
11553 let mut steps = vec![ast::PathStep::Name(format!("{}::{}", td.module, td.name))];
11554 steps.extend(path.steps.iter().cloned());
11555 let rooted = ast::Path { steps, partial: false };
11556 let synthetic = ast::SelectStmt {
11557 result: Expr::Path(rooted.clone()),
11558 filter: inner.filter.clone(),
11559 order_by: vec![],
11560 offset: None,
11561 limit: None,
11562 lock: None,
11563 };
11564 let mut ps = self.compile_path_select(&synthetic, &rooted, &elements, false)?;
11565 Self::correlate_path_select(&mut ps, alias);
11566 branches.push(ps);
11567 }
11568 let limit = inner
11569 .limit
11570 .as_ref()
11571 .map(|l| self.compile_free_expr(l))
11572 .transpose()?
11573 .map(Box::new);
11574 let multi = !matches!(limit.as_deref(), Some(IrExpr::Literal(IrLiteral::Int(1)))) && operands_reach_many;
11575 return Ok(IrExpr::ObjectPathUnion { branches, limit, multi });
11576 }
11577 if matches!(stmt, Stmt::Insert(_) | Stmt::Update(_) | Stmt::Delete(_))
11588 && !extra_fields.is_empty()
11589 && ctx.is_none()
11590 {
11591 let type_name = self.dml_subject_type(stmt)?;
11592 let inner = self.compile_stmt(stmt)?;
11593 let cte_name = self.fresh_nested_cte_name();
11594 self.register_cte(&cte_name, &inner);
11600 self.hoisted_ctes.push(IrCteDef {
11601 name: cte_name.clone(),
11602 stmt: inner,
11603 type_name,
11604 correlated_to: None,
11605 });
11606 let mut steps = vec![ast::PathStep::Name(cte_name)];
11607 steps.extend(extra_fields.iter().cloned().map(ast::PathStep::Name));
11608 return self.compile_free_path(&ast::Path { steps, partial: false });
11609 }
11610
11611 let Stmt::Select(sel) = stmt else {
11612 return Err(self.subquery_expr_err(stmt));
11613 };
11614 let has_modifiers =
11615 sel.filter.is_some() || !sel.order_by.is_empty() || sel.offset.is_some() || sel.limit.is_some();
11616
11617 let Some((path, shape_els)) = Self::split_path_result(&sel.result) else {
11618 if let Expr::FunctionCall(fc) = &sel.result {
11621 let fc = fc.clone();
11622 if let Some(ir) = self.try_compile_fn_scalar_subquery(&fc, extra_fields, Some(sel), ctx)? {
11623 return Ok(ir);
11624 }
11625 }
11626 if has_modifiers {
11631 let inner = self.compile_stmt(stmt)?;
11632 let IrStmt::Select(select) = inner else {
11633 return Err(self.subquery_expr_err(stmt));
11634 };
11635 if !select
11636 .rows
11637 .iter()
11638 .all(|r| matches!(r, IrRowSource::Free(IrFreeExpr::Scalar(_))))
11639 {
11640 return Err(self.subquery_expr_err(stmt));
11641 }
11642 let mut ir = IrExpr::ScalarSubquery(Box::new(select));
11643 for field in extra_fields {
11644 ir = Self::project_free_object_field(ir, field);
11645 }
11646 return Ok(ir);
11647 }
11648 let mut ir = self.compile_expr_ctx(&sel.result, ctx)?;
11649 for field in extra_fields {
11650 ir = Self::project_free_object_field(ir, field);
11651 }
11652 return Ok(ir);
11653 };
11654 let rewritten_sel;
11661 let mut sel = sel;
11662 let mut shape_els = shape_els;
11663 let mut extra_steps: Vec<ast::PathStep> = extra_fields.iter().map(|f| ast::PathStep::Name(f.clone())).collect();
11664 if !shape_els.is_empty()
11665 && let Some(defs) = Self::shape_alias_paths(shape_els)
11666 {
11667 extra_steps = extra_fields
11668 .iter()
11669 .flat_map(|field| match defs.iter().find(|(name, _)| name == field) {
11670 Some((_, definition)) => definition.steps.clone(),
11671 None => vec![ast::PathStep::Name(field.clone())],
11672 })
11673 .collect();
11674 rewritten_sel = ast::SelectStmt {
11675 result: sel.result.clone(),
11676 filter: sel.filter.clone().map(|f| Self::substitute_shape_aliases(f, &defs)),
11677 order_by: sel
11678 .order_by
11679 .iter()
11680 .map(|o| ast::SortExpr {
11681 expr: Self::substitute_shape_aliases(o.expr.clone(), &defs),
11682 direction: o.direction.clone(),
11683 nones: o.nones.clone(),
11684 })
11685 .collect(),
11686 offset: sel.offset.clone(),
11687 limit: sel.limit.clone(),
11688 lock: sel.lock.clone(),
11689 };
11690 sel = &rewritten_sel;
11691 shape_els = &[];
11692 }
11693
11694 if !shape_els.is_empty()
11705 && extra_fields.is_empty()
11706 && !path.partial
11707 && let [ast::PathStep::Name(type_name)] = path.steps.as_slice()
11708 && let Some(root_td) = match self.cte_object_type(type_name) {
11709 Some(_) => self.resolve_path_root(type_name).ok(),
11710 None => self.resolve_type(type_name).ok(),
11711 }
11712 {
11713 let alias = self.fresh_alias();
11714 let binding = self.cte_object_type(type_name).map(|_| type_name.clone());
11715 let table = match &binding {
11716 Some(name) => self.row_source_table(name, root_td),
11717 None => root_td.table.clone(),
11718 };
11719 let outer_declared = binding.as_ref().map(|name| {
11720 let declared = self.cte_declared_pointers.get(name).cloned().unwrap_or_default();
11721 std::mem::replace(&mut self.active_declared_pointers, declared)
11722 });
11723 let modifiers = self.compile_path_modifiers(sel, root_td, &alias);
11724 let module = root_td.module.clone();
11725 let shape = self.compile_shape(shape_els, root_td, &alias, &module);
11726 if let Some(outer) = outer_declared {
11727 self.active_declared_pointers = outer;
11728 }
11729 let (filter, order_by, offset, limit) = modifiers?;
11730 let shape = shape?;
11731 let source = IrSource {
11732 poly: self.poly_fanout_for(&format!("{}::{}", root_td.module, root_td.name)),
11733 type_name: format!("{}::{}", root_td.module, root_td.name),
11734 table,
11735 alias,
11736 };
11737 let single = selects_at_most_one(sel, root_td);
11738 let mut select = IrSelect::schema_bound(source, shape, filter);
11739 select.order_by = order_by;
11740 select.offset = offset;
11741 select.limit = limit;
11742 return Ok(if single {
11745 IrExpr::ObjectSubquery(Box::new(select))
11746 } else {
11747 IrExpr::ArrayFromSelect(Box::new(IrArraySource::ObjectSelect(Box::new(select))))
11748 });
11749 }
11750
11751 if !shape_els.is_empty() && extra_fields.is_empty() {
11752 return Err(self.type_err(
11753 "a sub-select with a shape is not valid in expression context — \
11754 assign it to a computed pointer instead, or project a property \
11755 off it, e.g. '(select .emails limit 1).address'",
11756 ));
11757 }
11758
11759 if !path.partial
11762 && let [ast::PathStep::Name(name)] = path.steps.as_slice()
11763 && self.is_value_binding(name)
11764 {
11765 let mut ir = self.compile_expr_ctx(&sel.result, ctx)?;
11766 for field in extra_fields {
11767 ir = Self::project_free_object_field(ir, field);
11768 }
11769 return Ok(ir);
11770 }
11771
11772 if !path.partial
11776 && extra_fields.is_empty()
11777 && let [ast::PathStep::Name(type_name)] = path.steps.as_slice()
11778 && let Ok(root_td) = self.resolve_type(type_name)
11779 {
11780 let alias = self.fresh_alias();
11781 let (filter, order_by, offset, limit) = self.compile_path_modifiers(sel, root_td, &alias)?;
11782 let source = IrSource {
11783 poly: None,
11784 type_name: format!("{}::{}", root_td.module, root_td.name),
11785 table: root_td.table.clone(),
11786 alias,
11787 };
11788 let pk = root_td.properties.iter().find(|p| p.is_pk);
11792 let shape = vec![IrShapePointer::Scalar(IrScalarPointer {
11793 implicit_id: false,
11794 marker_offset: None,
11795 alias: "id".to_string(),
11796 column: pk.map(|p| p.name.clone()).unwrap_or_else(|| "id".to_string()),
11797 pg_type: pk.map(|p| p.pg_type.clone()).unwrap_or_else(|| "uuid".to_string()),
11798 tuple_shape: None,
11799 })];
11800 let mut select = IrSelect::schema_bound(source, shape, filter);
11801 select.order_by = order_by;
11802 select.offset = offset;
11803 select.limit = limit;
11804 return Ok(IrExpr::Subquery(Box::new(select)));
11805 }
11806
11807 let mut steps = path.steps.clone();
11808 let correlate = if path.partial {
11809 let Some((td, alias)) = ctx else {
11810 return Err(self.type_err(
11811 "a relative path in a sub-select needs an enclosing object — \
11812 write the type name explicitly, e.g. '(select Person.name)'",
11813 ));
11814 };
11815 steps.insert(0, ast::PathStep::Name(format!("{}::{}", td.module, td.name)));
11816 Some(alias.to_string())
11817 } else {
11818 None
11819 };
11820 steps.extend(extra_steps.iter().cloned());
11821 let full_path = ast::Path { steps, partial: false };
11822
11823 let mut ps = self.compile_path_select_with_tail(sel, &full_path, outer_shape, false, extra_steps.len(), &[])?;
11824 if let Some(outer_alias) = correlate {
11825 Self::correlate_path_select(&mut ps, &outer_alias);
11826 }
11827 if !outer_shape.is_empty() && matches!(ps.result, IrPathResult::Object { .. }) {
11832 return Ok(IrExpr::ObjectPathSubquery(Box::new(ps)));
11833 }
11834 let single = matches!(&sel.limit, Some(Expr::Literal(ast::Literal::Int(1))));
11840 let multi = match &full_path.steps[0] {
11841 ast::PathStep::Name(root) => {
11842 let root_td = self.resolve_path_root(root)?;
11843 let every_row_of_a_type = !path.partial
11846 && self.cte_object_type(root).is_none()
11847 && self.resolve_type(root).is_ok()
11848 && !selects_at_most_one(sel, root_td);
11849 every_row_of_a_type || self.path_crosses_multi(root_td, &full_path.steps[1..])
11850 }
11851 _ => false,
11852 };
11853 if multi && !single && matches!(ps.result, IrPathResult::Scalar(..)) {
11854 return Ok(IrExpr::ArrayFromSelect(Box::new(IrArraySource::PathSelect(Box::new(
11855 ps,
11856 )))));
11857 }
11858 Ok(IrExpr::PathSubquery(Box::new(ps)))
11859 }
11860
11861 fn try_compile_fn_scalar_subquery(
11876 &mut self,
11877 fc: &ast::FunctionCall,
11878 fields: &[String],
11879 modifiers: Option<&ast::SelectStmt>,
11880 ctx: Option<(&TypeDescriptor, &str)>,
11881 ) -> Result<Option<IrExpr>, PyQLError> {
11882 let fd = self.schema.functions.iter().find(|f| {
11883 let module_matches = fc.module.as_deref().map(|m| m == f.module.as_str()).unwrap_or(true);
11884 module_matches && f.name == fc.name && f.return_is_object
11885 });
11886 let Some(fd) = fd else { return Ok(None) };
11887 let (fn_module, fn_name, return_type_name, polymorphic, params) = (
11888 fd.module.clone(),
11889 fd.name.clone(),
11890 fd.return_pg_type.clone(),
11891 fd.return_is_polymorphic,
11892 fd.params.clone(),
11893 );
11894
11895 let qualified = format!("{fn_module}::{fn_name}");
11896 let [field] = fields else {
11897 return Err(self.type_err(&format!(
11898 "function '{qualified}' returns objects, so using it inside an expression needs \
11899 one of its properties, e.g. '{qualified}(…).name'"
11900 )));
11901 };
11902 if params.len() != fc.args.len() {
11903 return Err(self.type_err(&format!(
11904 "function '{qualified}' expects {} argument(s), got {}",
11905 params.len(),
11906 fc.args.len()
11907 )));
11908 }
11909
11910 let mut fn_args = fc
11911 .args
11912 .iter()
11913 .map(|a| self.compile_expr_ctx(a, ctx))
11914 .collect::<Result<Vec<_>, _>>()?;
11915 if let Some(globals) = self.globals_arg_for_call(&qualified)? {
11916 fn_args.insert(0, globals);
11917 }
11918
11919 let td = self.resolve_type(&return_type_name)?;
11920 let alias = self.fresh_alias();
11921 let Some(prop) = Self::resolve_property(td, field) else {
11922 return Err(self.field_err(field, &return_type_name));
11923 };
11924 let projected = IrShapePointer::Computed(IrComputedPointer {
11925 marker_offset: None,
11926 alias: field.clone(),
11927 expr: IrExpr::ColumnRef {
11928 alias: alias.clone(),
11929 column: prop.name.clone(),
11930 pg_type: prop.pg_type.clone(),
11931 },
11932 });
11933
11934 let (poly_implementors, poly_columns) = if polymorphic {
11935 self.collect_poly_info(&return_type_name)
11936 } else {
11937 (vec![], vec![])
11938 };
11939 let (filter, order_by, offset, limit) = match modifiers {
11940 Some(sel) => {
11941 let td = self.resolve_type(&return_type_name)?;
11942 self.compile_path_modifiers(sel, td, &alias)?
11943 }
11944 None => (None, vec![], None, None),
11945 };
11946
11947 Ok(Some(IrExpr::FnSubquery(Box::new(IrFunctionSelect {
11948 fn_module,
11949 fn_name,
11950 fn_args,
11951 alias,
11952 type_name: return_type_name,
11953 polymorphic,
11954 poly_implementors,
11955 poly_columns,
11956 shape: vec![projected],
11957 filter,
11958 order_by,
11959 offset,
11960 limit,
11961 distinct: false,
11962 }))))
11963 }
11964
11965 fn computed_is_object_valued(&self, cd: &crate::schema::ComputedDescriptor, td: &TypeDescriptor) -> bool {
11973 let Ok(expr) = crate::parse::parse_pointer_expr(&cd.expression) else {
11974 return false;
11975 };
11976 let expr = match Self::field_access_over_select(&expr) {
11977 Some((sel, _)) => Expr::SubQuery(Box::new(Stmt::Select(sel))),
11978 None => expr,
11979 };
11980 let Some((path, _, _)) = Self::pointer_subject(&expr) else {
11981 return false;
11982 };
11983 if !path.partial {
11984 return false;
11985 }
11986 match path.steps.as_slice() {
11987 [ast::PathStep::Backlink(_)] | [ast::PathStep::Backlink(_), ast::PathStep::TypeIntersection(_)] => true,
11991 steps => self.walk_path_types(td, steps, MAX_COMPUTED_SPLICES).1.is_some(),
11992 }
11993 }
11994
11995 fn path_crosses_multi(&self, td: &TypeDescriptor, steps: &[ast::PathStep]) -> bool {
12001 self.walk_path_types(td, steps, MAX_COMPUTED_SPLICES).0
12002 }
12003
12004 fn coalesce_path_branches(
12008 &mut self,
12009 td: &TypeDescriptor,
12010 alias: &str,
12011 operands: &[ast::Path],
12012 elements: &[ast::ShapeElement],
12013 ) -> Result<Vec<IrPathSelect>, PyQLError> {
12014 let mut branches: Vec<IrPathSelect> = Vec::with_capacity(operands.len());
12015 for path in operands {
12016 let mut steps = vec![ast::PathStep::Name(format!("{}::{}", td.module, td.name))];
12017 steps.extend(path.steps.iter().cloned());
12018 let rooted = ast::Path { steps, partial: false };
12019 let synthetic = ast::SelectStmt {
12020 result: Expr::Path(rooted.clone()),
12021 filter: None,
12022 order_by: vec![],
12023 offset: None,
12024 limit: None,
12025 lock: None,
12026 };
12027 let mut ps = self.compile_path_select(&synthetic, &rooted, elements, false)?;
12028 Self::correlate_path_select(&mut ps, alias);
12029 let guards: Vec<IrExpr> = branches
12030 .iter()
12031 .map(|earlier| {
12032 IrExpr::UnaryOp(Box::new(IrUnaryOp {
12033 op: ast::UnaryOpKind::Not,
12034 operand: IrExpr::UnaryOp(Box::new(IrUnaryOp {
12035 op: ast::UnaryOpKind::Exists,
12036 operand: IrExpr::PathSubquery(Box::new(earlier.clone())),
12037 })),
12038 }))
12039 })
12040 .collect();
12041 if !guards.is_empty() {
12042 ps.filter = and_conditions(ps.filter, guards);
12043 }
12044 branches.push(ps);
12045 }
12046 Ok(branches)
12047 }
12048
12049 fn path_lands_on_objects(&self, td: &TypeDescriptor, path: &ast::Path) -> bool {
12054 let Some(last) = path
12055 .steps
12056 .iter()
12057 .rposition(|step| matches!(step, ast::PathStep::Name(_) | ast::PathStep::Backlink(_)))
12058 else {
12059 return false;
12060 };
12061 match &path.steps[last] {
12062 ast::PathStep::Backlink(_) => true,
12063 ast::PathStep::Name(name) => self
12064 .walk_path_types(td, &path.steps[..last], MAX_COMPUTED_SPLICES)
12065 .1
12066 .is_some_and(|owner| {
12067 Self::resolve_multilink(owner, name).is_some() || Self::resolve_link(owner, name).is_some()
12068 }),
12069 _ => false,
12070 }
12071 }
12072
12073 fn relative_paths_reach_many(&self, td: &TypeDescriptor, operands: &[ast::Path]) -> bool {
12078 operands.iter().any(|path| {
12079 let (root_td, rest) = match path.steps.first() {
12080 Some(ast::PathStep::TypeIntersection(tr)) => {
12081 let name = match &tr.module {
12082 Some(module) => format!("{}::{}", module, tr.name),
12083 None => tr.name.clone(),
12084 };
12085 match self.resolve_type(&name) {
12086 Ok(resolved) => (resolved, &path.steps[1..]),
12087 Err(_) => return true,
12088 }
12089 }
12090 _ => (td, &path.steps[..]),
12091 };
12092 self.path_crosses_multi(root_td, rest)
12093 })
12094 }
12095
12096 fn walk_path_types(
12105 &self,
12106 td: &'a TypeDescriptor,
12107 steps: &[ast::PathStep],
12108 depth: usize,
12109 ) -> (bool, Option<&'a TypeDescriptor>) {
12110 let mut current = td;
12111 let mut multi = false;
12112 for (i, step) in steps.iter().enumerate() {
12113 match step {
12114 ast::PathStep::Backlink(backlink_name) => {
12118 let Some(ast::PathStep::TypeIntersection(tr)) = steps.get(i + 1) else {
12119 return (true, None);
12120 };
12121 let owner_name = match &tr.module {
12122 Some(m) => format!("{}::{}", m, tr.name),
12123 None => tr.name.clone(),
12124 };
12125 let current_qname = format!("{}::{}", current.module, current.name);
12126 let single = self
12127 .resolve_type(&owner_name)
12128 .is_ok_and(|owner| self.backlink_is_single(owner, backlink_name, ¤t_qname));
12129 multi |= !single;
12130 continue;
12131 }
12132 ast::PathStep::TypeIntersection(tr) => {
12133 let name = match &tr.module {
12134 Some(m) => format!("{}::{}", m, tr.name),
12135 None => tr.name.clone(),
12136 };
12137 match self.resolve_type(&name) {
12138 Ok(t) => current = t,
12139 Err(_) => return (multi, None),
12140 }
12141 }
12142 ast::PathStep::Name(n) => {
12143 if let Some(ml) = Self::resolve_multilink(current, n) {
12144 multi = true;
12145 match self.resolve_type(&ml.target) {
12146 Ok(t) => current = t,
12147 Err(_) => return (multi, None),
12148 }
12149 continue;
12150 }
12151 if let Some(target) = Self::resolve_link(current, n).map(|l| l.target.clone()) {
12152 match self.resolve_type(&target) {
12153 Ok(t) => current = t,
12154 Err(_) => return (multi, None),
12155 }
12156 continue;
12157 }
12158 if depth > 0
12161 && let Some(cd) = self.resolve_computed(current, n)
12162 && let Ok(expr) = crate::parse::parse_pointer_expr(&cd.expression)
12163 {
12164 if let Some((p, _, modifiers)) = Self::pointer_subject(&expr)
12165 && p.partial
12166 {
12167 let (m, t) = self.walk_path_types(current, &p.steps, depth - 1);
12168 let capped = modifiers
12171 .is_some_and(|m| matches!(&m.limit, Some(Expr::Literal(ast::Literal::Int(1)))));
12172 multi = multi || (m && !capped);
12173 match t {
12174 Some(t) => current = t,
12175 None => return (multi, None),
12176 }
12177 continue;
12178 }
12179 if let Some((fc, _)) = Self::function_subject(&expr)
12180 && let Some(fd) = self.resolve_object_fn(fc)
12181 {
12182 multi = multi || fd.return_is_set;
12183 match self.resolve_type(&fd.return_pg_type) {
12184 Ok(t) => current = t,
12185 Err(_) => return (multi, None),
12186 }
12187 continue;
12188 }
12189 }
12190 return (multi, None);
12192 }
12193 _ => return (multi, None),
12194 }
12195 }
12196 (multi, Some(current))
12197 }
12198
12199 fn compile_partial_path_as_subquery(
12214 &mut self,
12215 p: &ast::Path,
12216 td: &TypeDescriptor,
12217 alias: &str,
12218 ) -> Result<IrExpr, PyQLError> {
12219 let (root_name, rest) = match p.steps.first() {
12220 Some(ast::PathStep::TypeIntersection(tr)) => {
12221 let name = match &tr.module {
12222 Some(m) => format!("{}::{}", m, tr.name),
12223 None => tr.name.clone(),
12224 };
12225 (name, &p.steps[1..])
12226 }
12227 _ => (format!("{}::{}", td.module, td.name), &p.steps[..]),
12228 };
12229 let root_td = self.resolve_type(&root_name)?;
12230 let multi = self.path_crosses_multi(root_td, rest);
12231
12232 let mut steps = vec![ast::PathStep::Name(root_name)];
12233 steps.extend(rest.iter().cloned());
12234 let full_path = ast::Path { steps, partial: false };
12235 let synthetic = ast::SelectStmt {
12236 result: Expr::Path(full_path.clone()),
12237 filter: None,
12238 order_by: vec![],
12239 offset: None,
12240 limit: None,
12241 lock: None,
12242 };
12243 let mut ps = self.compile_path_select(&synthetic, &full_path, &[], false)?;
12244 Self::correlate_path_select(&mut ps, alias);
12245 if multi && matches!(ps.result, IrPathResult::Scalar(..)) {
12246 Ok(IrExpr::ArrayFromSelect(Box::new(IrArraySource::PathSelect(Box::new(
12247 ps,
12248 )))))
12249 } else {
12250 Ok(IrExpr::PathSubquery(Box::new(ps)))
12251 }
12252 }
12253
12254 fn relative_subselect(
12263 &mut self,
12264 stmt: &Stmt,
12265 ctx: Option<(&TypeDescriptor, &str)>,
12266 ) -> Result<Option<IrPathSelect>, PyQLError> {
12267 let (Some((td, alias)), Stmt::Select(sel)) = (ctx, stmt) else {
12268 return Ok(None);
12269 };
12270 let (path, shape): (&ast::Path, &[ShapeElement]) = match &sel.result {
12271 Expr::Path(p) if p.partial => (p, &[]),
12272 Expr::Shape(sh) => match sh.expr.as_ref() {
12273 Some(Expr::Path(p)) if p.partial => (p, sh.elements.as_slice()),
12274 _ => return Ok(None),
12275 },
12276 _ => return Ok(None),
12277 };
12278 let mut steps = vec![ast::PathStep::Name(format!("{}::{}", td.module, td.name))];
12279 steps.extend(path.steps.iter().cloned());
12280 let rooted = ast::Path { steps, partial: false };
12281 let synthetic = ast::SelectStmt {
12282 result: Expr::Path(rooted.clone()),
12283 filter: sel.filter.clone(),
12284 order_by: sel.order_by.clone(),
12285 offset: sel.offset.clone(),
12286 limit: sel.limit.clone(),
12287 lock: None,
12288 };
12289 let mut ps = self.compile_path_select(&synthetic, &rooted, shape, false)?;
12290 Self::correlate_path_select(&mut ps, alias);
12291 Ok(Some(ps))
12292 }
12293
12294 fn correlate_path_select(ps: &mut IrPathSelect, outer_alias: &str) {
12298 if outer_alias == "NEW" || outer_alias == "OLD" {
12301 ps.root.table = format!("@row:{outer_alias}");
12302 ps.root.poly = None;
12303 }
12304 let correlation = IrExpr::BinOp(Box::new(IrBinOp {
12305 left: IrExpr::ColumnRef {
12306 alias: ps.root.alias.clone(),
12307 column: "id".to_string(),
12308 pg_type: "uuid".to_string(),
12309 },
12310 op: ast::BinOpKind::Eq,
12311 right: IrExpr::ColumnRef {
12312 alias: outer_alias.to_string(),
12313 column: "id".to_string(),
12314 pg_type: "uuid".to_string(),
12315 },
12316 }));
12317 ps.filter = Some(match ps.filter.take() {
12318 Some(existing) => IrExpr::BinOp(Box::new(IrBinOp {
12319 left: correlation,
12320 op: ast::BinOpKind::And,
12321 right: existing,
12322 })),
12323 None => correlation,
12324 });
12325 }
12326
12327 fn subquery_expr_err(&self, stmt: &Stmt) -> PyQLError {
12331 let what = match stmt {
12332 Stmt::Insert(_) => "an insert",
12333 Stmt::Update(_) => "an update",
12334 Stmt::Delete(_) => "a delete",
12335 Stmt::With(_) => "a `with` block",
12336 Stmt::For(_) => "a `for` loop",
12337 Stmt::Group(_) => "a `group`",
12338 _ => "this sub-statement",
12339 };
12340 self.type_err(&format!(
12341 "{what} cannot stand in for a value — a sub-statement is only valid in expression \
12342 position as a select over a path, e.g. '(select .emails filter .primary limit 1).address'"
12343 ))
12344 }
12345
12346 fn set_operands(expr: &Expr) -> Option<Vec<&Expr>> {
12354 match expr {
12355 Expr::Set(elems) if elems.is_empty() => None,
12359 Expr::Set(elems) => Some(elems.iter().collect()),
12360 Expr::Union(left, right) => {
12361 let mut operands = Self::set_operands(left).unwrap_or_else(|| vec![left.as_ref()]);
12362 operands.extend(Self::set_operands(right).unwrap_or_else(|| vec![right.as_ref()]));
12363 Some(operands)
12364 }
12365 _ => None,
12366 }
12367 }
12368
12369 fn in_value_position<R>(&mut self, f: impl FnOnce(&mut Self) -> R) -> R {
12373 let previous = std::mem::replace(&mut self.value_position, true);
12374 let result = f(self);
12375 self.value_position = previous;
12376 result
12377 }
12378
12379 fn as_condition<R>(&mut self, f: impl FnOnce(&mut Self) -> R) -> R {
12384 let previous = std::mem::replace(&mut self.value_position, false);
12385 let result = f(self);
12386 self.value_position = previous;
12387 result
12388 }
12389
12390 fn compile_expr_ctx(&mut self, expr: &Expr, ctx: Option<(&TypeDescriptor, &str)>) -> Result<IrExpr, PyQLError> {
12400 match expr {
12401 Expr::Path(p) => match ctx {
12408 Some((td, alias)) => self.compile_path(p, td, alias),
12409 None => self.compile_free_path(p),
12410 },
12411
12412 Expr::PathStepOn { .. } => match self.try_compile_walk_off_subquery(expr, ctx)? {
12415 Some(ir) => Ok(ir),
12416 None => Err(self.type_err(
12417 "a type intersection, link property or backlink needs a path, a binding \
12418 or a sub-select to walk off",
12419 )),
12420 },
12421
12422 Expr::Literal(lit) => Ok(IrExpr::Literal(match lit {
12423 Literal::Str(s) => IrLiteral::Str(s.clone()),
12424 Literal::Int(n) => IrLiteral::Int(*n),
12425 Literal::Float(f) => IrLiteral::Float(*f),
12426 Literal::Bool(b) => IrLiteral::Bool(*b),
12427 })),
12428
12429 Expr::Parameter(name) => {
12430 let index = self.param_index(name);
12431 Ok(IrExpr::Param { index })
12432 }
12433
12434 Expr::Global(name) => self.compile_global(name),
12435
12436 Expr::Index { expr: e, index: i } => {
12437 let ir_expr = self.compile_expr_ctx(e, ctx)?;
12438 if self.is_json_expr(e, &ir_expr) {
12439 return match &**i {
12440 Expr::Literal(Literal::Str(key)) => Ok(IrExpr::JsonbField {
12441 expr: Box::new(ir_expr),
12442 field: key.clone(),
12443 }),
12444 Expr::Literal(Literal::Int(n)) if *n >= 0 => Ok(IrExpr::JsonbIndex {
12445 expr: Box::new(ir_expr),
12446 index: *n as usize,
12447 }),
12448 _ => Err(self.type_err("indexing json needs a literal key or a literal position")),
12449 };
12450 }
12451 let ir_index = self.compile_expr_ctx(i, ctx)?;
12452 let is_array = is_array_expr(&ir_expr);
12453 Ok(IrExpr::Subscript {
12454 expr: Box::new(ir_expr),
12455 index: Box::new(ir_index),
12456 is_array,
12457 })
12458 }
12459
12460 Expr::Slice {
12461 expr: e,
12462 lower: lo,
12463 upper: hi,
12464 } => {
12465 let ir_expr = self.compile_expr_ctx(e, ctx)?;
12466 let is_array = is_array_expr(&ir_expr);
12467 let ir_lower = lo.as_ref().map(|x| self.compile_expr_ctx(x, ctx)).transpose()?;
12468 let ir_upper = hi.as_ref().map(|x| self.compile_expr_ctx(x, ctx)).transpose()?;
12469 Ok(IrExpr::Slice {
12470 expr: Box::new(ir_expr),
12471 lower: ir_lower.map(Box::new),
12472 upper: ir_upper.map(Box::new),
12473 is_array,
12474 })
12475 }
12476
12477 Expr::TypeCast(tc) => {
12478 if matches!(&tc.expr, Expr::Set(elems) if elems.is_empty()) {
12487 return Ok(IrExpr::Null);
12488 }
12489 if let ast::TypeExpr::Tuple { elements } = &tc.ty
12490 && let Some(ir) = self.try_compile_tuple_literal_cast_ctx(elements, &tc.expr, ctx)?
12491 {
12492 let pg_type = self.resolve_cast_pg_type(&tc.ty)?;
12493 let tuple_shape = self.resolve_tuple_cast_shape(&tc.ty);
12494 return Ok(IrExpr::TypeCast(Box::new(IrTypeCast {
12495 expr: ir,
12496 pg_type,
12497 tuple_shape,
12498 })));
12499 }
12500 if let ast::TypeExpr::Array { element } = &tc.ty
12501 && let Some(ir) = self.try_compile_array_literal_cast_ctx(element, &tc.expr, ctx)?
12502 {
12503 let pg_type = self.resolve_cast_pg_type(&tc.ty)?;
12504 return Ok(IrExpr::TypeCast(Box::new(IrTypeCast {
12505 expr: ir,
12506 pg_type,
12507 tuple_shape: None,
12508 })));
12509 }
12510 const STDLIB_MODULES: &[&str] = &["std", "cal", "math", "sys", "pgvector", "crypto", "postgis"];
12517 if let Some((module, name)) = tc.ty.as_named()
12518 && module.map(|m| !STDLIB_MODULES.contains(&m)).unwrap_or(false)
12519 {
12520 let qname = match module {
12521 Some(m) => format!("{m}::{name}"),
12522 None => name.to_string(),
12523 };
12524 if self.resolve_enum(&qname).is_none()
12525 && self.resolve_scalar(&qname).is_none()
12526 && self.resolve_named_tuple(&qname).is_none()
12527 && self.resolve_type(&qname).is_ok()
12528 {
12529 return self.compile_expr_ctx(&tc.expr, ctx);
12530 }
12531 }
12532 let inner = if casts_to_json(&tc.ty) {
12536 self.without_implicit_id(|this| this.compile_expr_ctx(&tc.expr, ctx))?
12537 } else {
12538 self.compile_expr_ctx(&tc.expr, ctx)?
12539 };
12540 let pg_type = self.resolve_cast_pg_type(&tc.ty)?;
12541
12542 if infer_ir_type(&inner) == Some("jsonb") {
12552 if let ast::TypeExpr::Array { element } = &tc.ty {
12553 let elem_pg = self.resolve_cast_pg_type(element)?;
12554 let sql_template =
12555 format!("ARRAY(SELECT (elem #>> '{{}}')::{elem_pg} FROM jsonb_array_elements($1) AS elem)");
12556 return Ok(IrExpr::FunctionCall(super::IrFunctionCall {
12557 return_pg_type: None,
12558 schema: None,
12559 name: "jsonb_array_cast".to_string(),
12560 args: vec![inner],
12561 sql_template: Some(sql_template),
12562 }));
12563 }
12564 if matches!(
12565 pg_type.as_str(),
12566 "uuid" | "timestamptz" | "timestamp" | "date" | "time" | "interval"
12567 ) {
12568 let sql_template = format!("(($1 #>> '{{}}'))::{pg_type}");
12569 return Ok(IrExpr::FunctionCall(super::IrFunctionCall {
12570 return_pg_type: None,
12571 schema: None,
12572 name: "jsonb_scalar_cast".to_string(),
12573 args: vec![inner],
12574 sql_template: Some(sql_template),
12575 }));
12576 }
12577 }
12578
12579 if infer_ir_type(&inner) == Some("text")
12588 && let Some((module, name)) = tc.ty.as_named()
12589 {
12590 let parser = match (module, name) {
12591 (None | Some("std"), "datetime") => Some(("std", "to_datetime")),
12592 (Some("cal"), "local_datetime") => Some(("cal", "to_local_datetime")),
12593 (Some("cal"), "local_date") => Some(("cal", "to_local_date")),
12594 (Some("cal"), "local_time") => Some(("cal", "to_local_time")),
12595 _ => None,
12596 };
12597 if let Some((ns, fn_name)) = parser {
12598 return self.resolve_fn_call(Some(ns), fn_name, vec![inner]);
12599 }
12600 let helper = match (module, name) {
12605 (None | Some("std"), "duration") => Some("duration_in"),
12606 (Some("cal"), "date_duration") => Some("date_duration_in"),
12607 _ => None,
12608 };
12609 if let Some(helper) = helper {
12610 return Ok(IrExpr::FunctionCall(super::IrFunctionCall {
12611 return_pg_type: Some("interval".to_string()),
12612 schema: Some("_pylon".to_string()),
12613 name: helper.to_string(),
12614 args: vec![inner],
12615 sql_template: None,
12616 }));
12617 }
12618 }
12619
12620 if pg_type == "text"
12629 && matches!(
12630 infer_ir_type(&inner),
12631 Some("timestamptz" | "timestamp" | "date" | "time" | "interval")
12632 )
12633 {
12634 return self.resolve_fn_call(Some("std"), "to_str", vec![inner]);
12635 }
12636
12637 let inner = match (pg_type.as_str(), infer_ir_type(&inner)) {
12640 ("int2" | "int8", Some("boolean")) => IrExpr::TypeCast(Box::new(IrTypeCast {
12641 expr: inner,
12642 pg_type: "int4".to_string(),
12643 tuple_shape: None,
12644 })),
12645 _ => inner,
12646 };
12647
12648 let tuple_shape = self.resolve_tuple_cast_shape(&tc.ty);
12649 Ok(self.tuple_cast(inner, pg_type, tuple_shape))
12650 }
12651
12652 Expr::BinOp(b)
12657 if b.op == ast::BinOpKind::Coalesce
12658 && ctx.is_some()
12659 && Self::coalesce_of_relative_paths(expr).is_some_and(|operands| {
12660 let td = ctx.expect("checked").0;
12661 self.relative_paths_reach_many(td, &operands)
12662 && operands.iter().all(|path| self.path_lands_on_objects(td, path))
12663 }) =>
12664 {
12665 let (td, alias) = ctx.expect("checked by the guard");
12666 let operands = Self::coalesce_of_relative_paths(expr).expect("checked by the guard");
12667 let branches = self.coalesce_path_branches(td, alias, &operands, &[])?;
12668 Ok(IrExpr::ObjectPathUnion {
12669 branches,
12670 limit: None,
12671 multi: true,
12672 })
12673 }
12674
12675 Expr::BinOp(b) => {
12676 let yields_values = matches!(
12679 b.op,
12680 ast::BinOpKind::Add
12681 | ast::BinOpKind::Sub
12682 | ast::BinOpKind::Mul
12683 | ast::BinOpKind::Div
12684 | ast::BinOpKind::FloorDiv
12685 | ast::BinOpKind::Mod
12686 | ast::BinOpKind::Pow
12687 | ast::BinOpKind::Coalesce
12688 | ast::BinOpKind::Concat
12689 );
12690 if let Some(elementwise) = self.try_elementwise_in(b, ctx)? {
12697 return Ok(elementwise);
12698 }
12699 if let Some((td, alias)) = ctx
12700 && !yields_values
12701 {
12702 if let Some(exists) = self.try_backlink_exists(b, td, alias)? {
12703 return Ok(exists);
12704 }
12705 if let Some(exists) = self.try_multilink_exists(b, td, alias)? {
12706 return Ok(exists);
12707 }
12708 }
12709 if matches!(b.op, ast::BinOpKind::In | ast::BinOpKind::NotIn)
12719 && let Expr::Set(elems) = &b.right
12720 {
12721 let left = self.compile_expr_ctx(&b.left, ctx)?;
12722 let items = elems
12723 .iter()
12724 .map(|e| self.compile_expr_ctx(e, ctx))
12725 .collect::<Result<Vec<_>, _>>()?;
12726 let right = IrExpr::Array(items);
12727 return Ok(IrExpr::BinOp(Box::new(IrBinOp {
12728 left,
12729 op: b.op.clone(),
12730 right,
12731 })));
12732 }
12733 if matches!(b.op, ast::BinOpKind::Eq | ast::BinOpKind::Ne) {
12739 let id = ["id".to_string()];
12740 let left_id = match &b.left {
12741 Expr::FunctionCall(fc) => self.try_compile_fn_scalar_subquery(fc, &id, None, ctx)?,
12742 _ => None,
12743 };
12744 let right_id = match &b.right {
12745 Expr::FunctionCall(fc) => self.try_compile_fn_scalar_subquery(fc, &id, None, ctx)?,
12746 _ => None,
12747 };
12748 if left_id.is_some() || right_id.is_some() {
12749 let left = match left_id {
12750 Some(e) => e,
12751 None => self.compile_expr_ctx(&b.left, ctx)?,
12752 };
12753 let right = match right_id {
12754 Some(e) => e,
12755 None => self.compile_expr_ctx(&b.right, ctx)?,
12756 };
12757 return Ok(IrExpr::BinOp(Box::new(IrBinOp {
12758 left,
12759 op: b.op.clone(),
12760 right,
12761 })));
12762 }
12763 }
12764 let mut left = self.compile_expr_ctx(&b.left, ctx)?;
12765 let mut right = self.compile_expr_ctx(&b.right, ctx)?;
12766 if matches!(
12770 b.op,
12771 ast::BinOpKind::Lt | ast::BinOpKind::Le | ast::BinOpKind::Gt | ast::BinOpKind::Ge
12772 ) && yields_array(&left) != yields_array(&right)
12773 {
12774 left = set_walk_as_scalar(left);
12775 right = set_walk_as_scalar(right);
12776 }
12777 if b.op == ast::BinOpKind::Coalesce && (yields_array(&left) || yields_array(&right)) {
12781 let as_set = |value: IrExpr| {
12782 if yields_array(&value) {
12783 value
12784 } else {
12785 IrExpr::FunctionCall(IrFunctionCall {
12786 return_pg_type: None,
12787 schema: None,
12788 name: "array_remove".to_string(),
12789 args: vec![value],
12790 sql_template: Some("array_remove(ARRAY[$1], NULL)".to_string()),
12791 })
12792 }
12793 };
12794 let left = as_set(left);
12795 let condition = IrExpr::FunctionCall(IrFunctionCall {
12796 return_pg_type: None,
12797 schema: None,
12798 name: "cardinality".to_string(),
12799 args: vec![left.clone()],
12800 sql_template: Some("(cardinality($1) > 0)".to_string()),
12801 });
12802 return Ok(IrExpr::IfElse(Box::new(IrIfElse {
12803 condition,
12804 if_: left,
12805 else_: as_set(right),
12806 })));
12807 }
12808 if matches!(b.op, ast::BinOpKind::Eq | ast::BinOpKind::Ne) {
12813 let is_set = |e: &IrExpr| match e {
12816 IrExpr::ArrayFromSelect(_) => true,
12817 IrExpr::CteRef { name, .. } => self.multi_row_ctes.contains(name),
12818 _ => false,
12819 };
12820 let flipped = is_set(&left) && !is_set(&right) && !is_array_expr(&right);
12821 let straight = is_set(&right) && !is_set(&left) && !is_array_expr(&left);
12822 if flipped || straight {
12823 let (value, set) = if straight { (left, right) } else { (right, left) };
12824 let membership = IrExpr::BinOp(Box::new(IrBinOp {
12825 left: value,
12826 op: ast::BinOpKind::In,
12827 right: set,
12828 }));
12829 return Ok(if matches!(b.op, ast::BinOpKind::Ne) {
12830 IrExpr::UnaryOp(Box::new(IrUnaryOp {
12831 op: ast::UnaryOpKind::Not,
12832 operand: membership,
12833 }))
12834 } else {
12835 membership
12836 });
12837 }
12838 }
12839 if let (Some(lt), Some(rt)) = (infer_ir_type(&left), infer_ir_type(&right))
12840 && !types_compatible(lt, rt)
12841 && !datetime_arithmetic_compatible(&b.op, lt, rt)
12842 {
12843 return Err(PyQLError::Type(PyQLTypeError {
12844 message: format!(
12845 "operator '{op}' cannot be applied to operands of type \
12846 '{lq}' and '{rq}'",
12847 op = b.op,
12848 lq = pg_type_to_pyql(lt),
12849 rq = pg_type_to_pyql(rt),
12850 ),
12851 position: Position { line: 0, col: 0 },
12852 }));
12853 }
12854 Ok(IrExpr::BinOp(Box::new(IrBinOp {
12855 left: true_division_operand(&b.op, left, &right),
12856 op: b.op.clone(),
12857 right,
12858 })))
12859 }
12860
12861 Expr::FunctionCall(f) => {
12862 if (f.module.is_none() || f.module.as_deref() == Some("std")) && f.name == "notify" {
12867 return self.compile_notify(f, ctx);
12868 }
12869 if (f.module.is_none() || f.module.as_deref() == Some("std")) && f.name == "notify_raw" {
12870 return self.compile_notify_raw(f, ctx);
12871 }
12872
12873 if ctx.is_none()
12876 && (f.module.is_none() || f.module.as_deref() == Some("std"))
12877 && (f.name == "sequence_next" || f.name == "sequence_reset")
12878 {
12879 return self.compile_sequence_fn(f);
12880 }
12881
12882 let assert_names: &[&str] = if ctx.is_some() {
12887 &["assert_single", "assert_exists", "assert_distinct"]
12888 } else {
12889 &["assert_single"]
12890 };
12891 if (f.module.is_none() || f.module.as_deref() == Some("std"))
12892 && assert_names.contains(&f.name.as_str())
12893 && !f.args.is_empty()
12894 && let Expr::SubQuery(inner_stmt) = &f.args[0]
12895 {
12896 let inner = match self.relative_subselect(inner_stmt, ctx)? {
12897 Some(ps) => IrArraySource::PathSelect(Box::new(ps)),
12898 None => self.compile_subquery_to_array_source(inner_stmt)?,
12899 };
12900 let fn_pg = match f.name.as_str() {
12901 "assert_single" => "assert_single",
12902 "assert_exists" => "assert_exists",
12903 _ => "assert_distinct",
12904 };
12905 let mut args = vec![IrExpr::ArrayFromSelect(Box::new(inner))];
12906 args.extend(self.assert_message(f, ctx)?);
12907 return Ok(IrExpr::FunctionCall(IrFunctionCall {
12908 return_pg_type: None,
12909 schema: Some("_pylon".to_string()),
12910 name: fn_pg.to_string(),
12911 args,
12912 sql_template: None,
12913 }));
12914 }
12915
12916 if let Some((td, alias)) = ctx
12920 && (f.module.is_none() || f.module.as_deref() == Some("std"))
12921 && f.name == "contains"
12922 && f.args.len() == 2
12923 && let Expr::Path(p) = &f.args[0]
12924 && p.partial
12925 && p.steps.len() >= 2
12926 && let ast::PathStep::Name(ln) = &p.steps[0]
12927 && Self::resolve_multilink(td, ln).is_some()
12928 {
12929 let synthetic = ast::BinOp {
12930 left: f.args[0].clone(),
12931 op: ast::BinOpKind::Eq,
12932 right: f.args[1].clone(),
12933 };
12934 if let Some(exists) = self.try_multilink_exists(&synthetic, td, alias)? {
12935 return Ok(exists);
12936 }
12937 }
12938
12939 if f.args.len() == 1
12945 && f.kwargs.is_empty()
12946 && let Some(mut ps) = self.compile_shape_field_select(&f.args[0], ctx)?
12947 {
12948 let IrPathResult::Scalar(column, _) = ps.result else {
12949 return Err(self.type_err("a shape's pointer read off a walk is a value"));
12950 };
12951 let ns = f.module.as_deref().unwrap_or("std");
12952 let aggregate =
12953 crate::stdlib::lookup(ns, &f.name)
12954 .into_iter()
12955 .find_map(|d| match &d.impl_strategy {
12956 crate::stdlib::ImplStrategy::SqlBuiltin(sql_name) if d.is_aggregate() => {
12957 Some(sql_name.to_string())
12958 }
12959 _ => None,
12960 });
12961 let Some(sql_name) = aggregate else {
12962 return Err(self.type_err(&format!(
12963 "'{}' over a shape's pointer read off a walk needs an aggregate",
12964 f.name
12965 )));
12966 };
12967 let over_nothing = aggregate_over_nothing_sql(&f.name);
12970 let subquery = if matches!(&column, IrExpr::FunctionCall(f) if f.name == "unnest" && f.sql_template.is_none())
12973 {
12974 ps.result = IrPathResult::Scalar(column, None);
12975 IrExpr::FunctionCall(IrFunctionCall {
12976 return_pg_type: None,
12977 schema: None,
12978 name: sql_name.clone(),
12979 args: vec![IrExpr::ArrayFromSelect(Box::new(IrArraySource::PathSelect(Box::new(
12980 ps,
12981 ))))],
12982 sql_template: Some(format!(
12983 "(SELECT {sql_name}(\"_s\".\"v\") FROM unnest($1) AS \"_s\"(\"v\"))"
12984 )),
12985 })
12986 } else {
12987 let aggregate = IrExpr::FunctionCall(IrFunctionCall {
12988 return_pg_type: None,
12989 schema: None,
12990 name: sql_name,
12991 args: vec![column],
12992 sql_template: None,
12993 });
12994 ps.result = IrPathResult::Scalar(aggregate, None);
12995 IrExpr::PathSubquery(Box::new(ps))
12996 };
12997 return Ok(match over_nothing {
12998 Some(value) => IrExpr::FunctionCall(IrFunctionCall {
12999 return_pg_type: None,
13000 schema: None,
13001 name: "coalesce".to_string(),
13002 args: vec![subquery, IrExpr::RawSql(value.to_string())],
13003 sql_template: None,
13004 }),
13005 None => subquery,
13006 });
13007 }
13008 if f.args.len() == 1 {
13009 let arg = &f.args[0];
13010 if matches!(arg, Expr::Intersect(_, _) | Expr::Except(_, _)) {
13013 use crate::stdlib::{ImplStrategy, lookup};
13014 let ns = f.module.as_deref().unwrap_or("std");
13015 let best = lookup(ns, &f.name)
13016 .iter()
13017 .find(|d| d.params.len() == 1)
13018 .map(|d| d.impl_strategy.clone());
13019 if let Some(ImplStrategy::SqlBuiltin(sql_name)) = best
13020 && let IrExpr::SetOp { op, left, right, .. } = self.compile_expr_ctx(arg, ctx)?
13021 {
13022 return Ok(IrExpr::SetOp {
13023 op,
13024 left,
13025 right,
13026 mode: super::SetOpMode::Aggregate(sql_name.to_string()),
13027 });
13028 }
13029 }
13030 if let Expr::FunctionCall(unpack) = arg
13035 && unpack.module.as_deref().unwrap_or("std") == "std"
13036 && unpack.name == "array_unpack"
13037 && unpack.kwargs.is_empty()
13038 && let [array] = unpack.args.as_slice()
13039 && let Some(sql_name) = crate::stdlib::lookup(f.module.as_deref().unwrap_or("std"), &f.name)
13040 .into_iter()
13041 .find_map(|d| match &d.impl_strategy {
13042 crate::stdlib::ImplStrategy::SqlBuiltin(sql_name) if d.is_aggregate() => {
13043 Some(sql_name.to_string())
13044 }
13045 _ => None,
13046 })
13047 {
13048 let values = self.compile_expr_ctx(array, ctx)?;
13049 if !yields_array(&values) {
13053 let over_nothing = aggregate_over_nothing_sql(&f.name).unwrap_or("NULL");
13054 return Ok(IrExpr::FunctionCall(IrFunctionCall {
13055 return_pg_type: None,
13056 schema: None,
13057 name: sql_name.clone(),
13058 args: vec![values],
13059 sql_template: Some(format!(
13060 "(SELECT coalesce({sql_name}(\"_s\".\"v\"), {over_nothing}) FROM unnest($1) AS \"_s\"(\"v\"))"
13061 )),
13062 }));
13063 }
13064 }
13065 if matches!(arg, Expr::FieldAccess { .. })
13069 && matches!(Self::peel_field_access_chain(arg).0, Expr::SubQuery(_))
13070 {
13071 let ns = f.module.as_deref().unwrap_or("std");
13072 let aggregate =
13073 crate::stdlib::lookup(ns, &f.name)
13074 .into_iter()
13075 .find_map(|d| match &d.impl_strategy {
13076 crate::stdlib::ImplStrategy::SqlBuiltin(sql_name) if d.is_aggregate() => {
13077 Some(sql_name.to_string())
13078 }
13079 _ => None,
13080 });
13081 if let Some(sql_name) = aggregate {
13082 let values = self.compile_expr_ctx(arg, ctx)?;
13083 if matches!(values, IrExpr::ArrayFromSelect(_)) {
13084 let over_nothing = aggregate_over_nothing_sql(&f.name).unwrap_or("NULL");
13085 return Ok(IrExpr::FunctionCall(IrFunctionCall {
13086 return_pg_type: None,
13087 schema: None,
13088 name: sql_name.clone(),
13089 args: vec![values],
13090 sql_template: Some(format!(
13091 "(SELECT coalesce({sql_name}(\"_s\".\"v\"), {over_nothing}) FROM unnest($1) AS \"_s\"(\"v\"))"
13092 )),
13093 }));
13094 }
13095 return Ok(IrExpr::FunctionCall(IrFunctionCall {
13096 return_pg_type: None,
13097 schema: None,
13098 name: sql_name,
13099 args: vec![values],
13100 sql_template: None,
13101 }));
13102 }
13103 }
13104 if let Expr::BinOp(b) = arg
13108 && b.op == ast::BinOpKind::Coalesce
13109 {
13110 let values = self.compile_expr_ctx(arg, ctx)?;
13111 let ns = f.module.as_deref().unwrap_or("std");
13112 let aggregate =
13113 crate::stdlib::lookup(ns, &f.name)
13114 .into_iter()
13115 .find_map(|d| match &d.impl_strategy {
13116 crate::stdlib::ImplStrategy::SqlBuiltin(sql_name) if d.is_aggregate() => {
13117 Some(sql_name.to_string())
13118 }
13119 _ => None,
13120 });
13121 if let Some(sql_name) = aggregate
13122 && yields_array(&values)
13123 {
13124 let over_nothing = aggregate_over_nothing_sql(&f.name).unwrap_or("NULL");
13125 return Ok(IrExpr::FunctionCall(IrFunctionCall {
13126 return_pg_type: None,
13127 schema: None,
13128 name: sql_name.clone(),
13129 args: vec![values],
13130 sql_template: Some(format!(
13131 "(SELECT coalesce({sql_name}(\"_s\".\"v\"), {over_nothing}) FROM unnest($1) AS \"_s\"(\"v\"))"
13132 )),
13133 }));
13134 }
13135 return self.resolve_fn_call(f.module.as_deref(), &f.name, vec![values]);
13136 }
13137 if let Some(name) = self.resolve_cte_name(arg) {
13142 use crate::stdlib::{ImplStrategy, lookup};
13143 let ns = f.module.as_deref().unwrap_or("std");
13144 let overloads = lookup(ns, &f.name);
13145 let best = overloads
13146 .iter()
13147 .find(|d| d.params.len() == 1)
13148 .or_else(|| overloads.first());
13149 if let Some(ImplStrategy::SqlBuiltin(sql_name)) = best.map(|d| &d.impl_strategy)
13150 && best.is_some_and(|d| d.params.first().is_some_and(|p| p.ty.is_set()))
13151 {
13152 let object = self.cte_types.get(name).is_some_and(|bound| bound.contains("::"));
13153 return Ok(IrExpr::AggOverCte {
13154 fn_name: sql_name.to_string(),
13155 cte: name.to_string(),
13156 column: (!object).then(|| "v".to_string()),
13157 });
13158 }
13159 }
13160 if let Some((td, alias)) = ctx {
13161 if let Expr::SubQuery(stmt) = arg
13165 && f.name != "array_agg"
13166 && let Some(sql_name) = crate::stdlib::lookup(f.module.as_deref().unwrap_or("std"), &f.name)
13167 .into_iter()
13168 .find_map(|d| match &d.impl_strategy {
13169 crate::stdlib::ImplStrategy::SqlBuiltin(sql_name) if d.is_aggregate() => {
13170 Some(sql_name.to_string())
13171 }
13172 _ => None,
13173 })
13174 && let Some(mut ps) = self.relative_subselect(stmt, ctx)?
13175 {
13176 if let IrPathResult::Object { alias, .. } = &ps.result {
13178 let id = IrExpr::ColumnRef {
13179 alias: alias.clone(),
13180 column: "id".to_string(),
13181 pg_type: "uuid".to_string(),
13182 };
13183 ps.result = IrPathResult::Scalar(id, None);
13184 }
13185 let aggregate = IrExpr::FunctionCall(IrFunctionCall {
13186 return_pg_type: None,
13187 schema: None,
13188 name: sql_name.clone(),
13189 args: vec![IrExpr::ArrayFromSelect(Box::new(IrArraySource::PathSelect(Box::new(
13190 ps,
13191 ))))],
13192 sql_template: Some(format!(
13193 "(SELECT {sql_name}(\"_s\".\"v\") FROM unnest($1) AS \"_s\"(\"v\"))"
13194 )),
13195 });
13196 return Ok(aggregate_over_nothing(&f.name, aggregate));
13197 }
13198 if let Expr::Path(p) = arg
13204 && !p.partial
13205 && p.steps.len() > 1
13206 && let Some(root) = self.find_path_root_in_expr(arg)
13207 {
13208 let synthetic = ast::SelectStmt {
13209 result: Expr::FunctionCall(f.clone()),
13210 filter: None,
13211 order_by: vec![],
13212 offset: None,
13213 limit: None,
13214 lock: None,
13215 };
13216 let ps = self.compile_expr_as_path_select(&synthetic, &synthetic.result, &root, false)?;
13217 return Ok(aggregate_over_nothing(&f.name, IrExpr::PathSubquery(Box::new(ps))));
13218 }
13219 if let Expr::Path(p) = arg
13232 && p.partial
13233 && (p.steps.len() > 1 && self.path_crosses_multi(td, &p.steps)
13234 || matches!(p.steps.first(), Some(ast::PathStep::Backlink(_)))
13235 || matches!(p.steps.as_slice(), [ast::PathStep::Name(name)]
13236 if Self::resolve_multilink(td, name).is_none()
13237 && self.path_crosses_multi(td, &p.steps)))
13238 {
13239 let mut steps = vec![ast::PathStep::Name(format!("{}::{}", td.module, td.name))];
13240 steps.extend(p.steps.iter().cloned());
13241 let rooted = ast::Path { steps, partial: false };
13242 let mut call = f.clone();
13243 call.args[0] = Expr::Path(rooted);
13244 let synthetic = ast::SelectStmt {
13245 result: Expr::FunctionCall(call),
13246 filter: None,
13247 order_by: vec![],
13248 offset: None,
13249 limit: None,
13250 lock: None,
13251 };
13252 let root = format!("{}::{}", td.module, td.name);
13253 let mut ps =
13254 self.compile_expr_as_path_select(&synthetic, &synthetic.result, &root, false)?;
13255 Self::correlate_path_select(&mut ps, alias);
13256 return Ok(aggregate_over_nothing(&f.name, IrExpr::PathSubquery(Box::new(ps))));
13257 }
13258 if let Expr::Path(p) = arg
13259 && p.partial
13260 && p.steps.len() == 1
13261 && let ast::PathStep::Name(ml_name) = &p.steps[0]
13262 && Self::resolve_multilink(td, ml_name).is_some()
13263 {
13264 use crate::stdlib::{ImplStrategy, lookup};
13265 let ns = f.module.as_deref().unwrap_or("std");
13266 let overloads = lookup(ns, &f.name);
13267 let best = overloads
13268 .iter()
13269 .find(|d| d.params.len() == 1)
13270 .or_else(|| overloads.first());
13271 if let Some(ImplStrategy::SqlBuiltin(sql_name)) = best.map(|d| &d.impl_strategy) {
13272 let fn_name = sql_name.to_string();
13273 let inner = self.multilink_correlation_select(ml_name, td, alias)?;
13274 return Ok(IrExpr::AggOverQuery {
13275 fn_name,
13276 inner: Box::new(inner),
13277 });
13278 }
13279 }
13280 } else {
13281 if let Expr::Path(p) = arg
13289 && !p.partial
13290 && p.steps.len() > 1
13291 && let Some(root) = self.find_path_root_in_expr(arg)
13292 {
13293 let synthetic = ast::SelectStmt {
13294 result: Expr::FunctionCall(f.clone()),
13295 filter: None,
13296 order_by: vec![],
13297 offset: None,
13298 limit: None,
13299 lock: None,
13300 };
13301 let ps = self.compile_expr_as_path_select(&synthetic, &synthetic.result, &root, false)?;
13302 return Ok(aggregate_over_nothing(&f.name, IrExpr::PathSubquery(Box::new(ps))));
13303 }
13304 if let Expr::SubQuery(stmt) = arg
13309 && matches!(stmt.as_ref(), Stmt::Insert(_) | Stmt::Update(_) | Stmt::Delete(_))
13310 {
13311 use crate::stdlib::{ImplStrategy, lookup};
13312 let ns = f.module.as_deref().unwrap_or("std");
13313 let overloads = lookup(ns, &f.name);
13314 let best = overloads
13315 .iter()
13316 .find(|d| d.params.len() == 1)
13317 .or_else(|| overloads.first())
13318 .cloned();
13319 if let Some(d) = best
13320 && let ImplStrategy::SqlBuiltin(sql_name) = &d.impl_strategy
13321 {
13322 let fn_name = sql_name.to_string();
13323 let (cte_name, type_name) = self.hoist_dml_as_cte(stmt.as_ref())?;
13324 let td = self.resolve_type(&type_name)?;
13325 let source = IrSource {
13326 poly: None,
13327 type_name: format!("{}::{}", td.module, td.name),
13328 table: self.cte_source_table(&cte_name),
13329 alias: self.fresh_alias(),
13330 };
13331 let inner = IrSelect::schema_bound(source, Self::pk_returning(td), None);
13332 return Ok(IrExpr::AggOverQuery {
13333 fn_name,
13334 inner: Box::new(inner),
13335 });
13336 }
13337 }
13338 let inner_sel: Option<ast::SelectStmt> = match arg {
13339 Expr::Path(p) if !p.partial => {
13340 let qname = p
13342 .steps
13343 .iter()
13344 .filter_map(|s| {
13345 if let ast::PathStep::Name(n) = s {
13346 Some(n.as_str())
13347 } else {
13348 None
13349 }
13350 })
13351 .collect::<Vec<_>>()
13352 .join("::");
13353 let is_schema_type = self
13354 .schema
13355 .types
13356 .iter()
13357 .any(|t| format!("{}::{}", t.module, t.name) == qname || t.name == qname);
13358 if is_schema_type {
13359 Some(ast::SelectStmt {
13360 result: arg.clone(),
13361 filter: None,
13362 order_by: vec![],
13363 offset: None,
13364 limit: None,
13365 lock: None,
13366 })
13367 } else {
13368 None
13369 }
13370 }
13371 Expr::SubQuery(stmt) => {
13372 if let ast::Stmt::Select(inner) = stmt.as_ref() {
13373 Some(inner.clone())
13374 } else {
13375 None
13376 }
13377 }
13378 _ => None,
13379 };
13380 if let Some(sel) = inner_sel {
13381 use crate::stdlib::{ImplStrategy, lookup};
13382 let ns = f.module.as_deref().unwrap_or("std");
13383 let overloads = lookup(ns, &f.name);
13384 let best = overloads
13385 .iter()
13386 .find(|d| d.params.len() == 1)
13387 .or_else(|| overloads.first());
13388 let (inner_result, inner_distinct) = match &sel.result {
13398 Expr::UnaryOp(u) if matches!(u.op, ast::UnaryOpKind::Distinct) => (&u.operand, true),
13399 other => (other, false),
13400 };
13401 if let Expr::Path(inner_path) = inner_result
13402 && !inner_path.partial
13403 && inner_path.steps.len() > 1
13404 && let Some(ast::PathStep::Name(root)) = inner_path.steps.first()
13405 && let Ok(root_td) = self.resolve_path_root(root)
13406 && self
13407 .walk_path_types(root_td, &inner_path.steps[1..], MAX_COMPUTED_SPLICES)
13408 .1
13409 .is_none()
13410 {
13411 let root = root.clone();
13412 let mut inner_call = f.clone();
13413 inner_call.args = vec![if inner_distinct {
13414 Expr::UnaryOp(Box::new(ast::UnaryOp {
13415 op: ast::UnaryOpKind::Distinct,
13416 operand: inner_result.clone(),
13417 }))
13418 } else {
13419 inner_result.clone()
13420 }];
13421 let call = ast::SelectStmt {
13422 result: Expr::FunctionCall(inner_call),
13423 ..sel.clone()
13424 };
13425 let ps = self.compile_expr_as_path_select(&call, &call.result, &root, false)?;
13426 return Ok(IrExpr::PathSubquery(Box::new(ps)));
13427 }
13428 if let Some(d) = best
13429 && let ImplStrategy::SqlBuiltin(sql_name) = &d.impl_strategy
13430 {
13431 let fn_name = sql_name.to_string();
13432 let inner_ir = self.compile_select(&sel, &sel.result, false)?;
13433 if fn_name == "array_agg"
13438 && matches!(inner_ir.rows.as_slice(), [IrRowSource::Bound { .. }])
13439 {
13440 return Ok(IrExpr::ArrayFromSelect(Box::new(IrArraySource::ObjectSelect(
13441 Box::new(inner_ir),
13442 ))));
13443 }
13444 return Ok(IrExpr::AggOverQuery {
13445 fn_name,
13446 inner: Box::new(inner_ir),
13447 });
13448 }
13449 }
13450 }
13451 }
13452
13453 if let Some(operands) = f.args.iter().find_map(Self::set_operands) {
13461 use crate::stdlib::{ImplStrategy, lookup};
13462 let ns = f.module.as_deref().unwrap_or("std");
13463 let overloads = lookup(ns, &f.name);
13464 let best = overloads
13465 .iter()
13466 .find(|d| d.params.len() == f.args.len())
13467 .or_else(|| overloads.first());
13468 let (schema, fn_name) = match best.map(|d| &d.impl_strategy) {
13469 Some(ImplStrategy::SqlBuiltin(sql_name)) => (None, sql_name.to_string()),
13470 Some(_) => {
13471 return Err(self.type_err(&format!(
13472 "function '{}::{}' cannot be called with a set literal in this context",
13473 ns, f.name
13474 )));
13475 }
13476 None => return Err(self.type_err(&format!("function '{}::{}' does not exist", ns, f.name))),
13477 };
13478 let elems = operands
13479 .iter()
13480 .map(|e| self.compile_expr_ctx(e, ctx))
13481 .collect::<Result<Vec<_>, _>>()?;
13482 return Ok(IrExpr::AggOverSet { fn_name, schema, elems });
13483 }
13484
13485 if let Some((td, _)) = ctx
13494 && f.module.as_deref().unwrap_or("std") == "std"
13495 && matches!(f.name.as_str(), "any" | "all")
13496 && f.kwargs.is_empty()
13497 && let [condition] = f.args.as_slice()
13498 && let Some(per_element) = per_element_of_one_multilink(condition, td)
13499 {
13500 let (link, element_condition) = per_element;
13501 let condition = if f.name == "all" {
13502 Expr::UnaryOp(Box::new(ast::UnaryOp {
13503 op: ast::UnaryOpKind::Not,
13504 operand: element_condition,
13505 }))
13506 } else {
13507 element_condition
13508 };
13509 let exists = Expr::UnaryOp(Box::new(ast::UnaryOp {
13510 op: ast::UnaryOpKind::Exists,
13511 operand: Expr::SubQuery(Box::new(Stmt::Select(ast::SelectStmt {
13512 result: Expr::Path(ast::Path {
13513 steps: link,
13514 partial: true,
13515 }),
13516 filter: Some(condition),
13517 order_by: vec![],
13518 offset: None,
13519 limit: None,
13520 lock: None,
13521 }))),
13522 }));
13523 let exists = if f.name == "all" {
13524 Expr::UnaryOp(Box::new(ast::UnaryOp {
13525 op: ast::UnaryOpKind::Not,
13526 operand: exists,
13527 }))
13528 } else {
13529 exists
13530 };
13531 return self.compile_expr_ctx(&exists, ctx);
13532 }
13533 if f.module.as_deref().unwrap_or("std") == "std"
13536 && matches!(f.name.as_str(), "any" | "all")
13537 && f.kwargs.is_empty()
13538 && let [Expr::TypeIs { expr: subject, ty }] = f.args.as_slice()
13539 && let answers @ IrExpr::ArrayFromSelect(_) = self.compile_type_is_set(subject, ty, ctx)?
13540 {
13541 let (aggregate, over_nothing) = if f.name == "all" {
13542 ("bool_and", "true")
13543 } else {
13544 ("bool_or", "false")
13545 };
13546 return Ok(IrExpr::FunctionCall(IrFunctionCall {
13547 return_pg_type: None,
13548 schema: None,
13549 name: aggregate.to_string(),
13550 args: vec![answers],
13551 sql_template: Some(format!(
13552 "(SELECT coalesce({aggregate}(\"_unnested\".\"v\"), {over_nothing}) FROM unnest($1) AS \"_unnested\"(\"v\"))"
13553 )),
13554 }));
13555 }
13556 if f.module.as_deref().unwrap_or("std") == "std"
13561 && matches!(f.name.as_str(), "any" | "all")
13562 && f.kwargs.is_empty()
13563 && let [Expr::BinOp(comparison)] = f.args.as_slice()
13564 && let Expr::FunctionCall(unpack) = &comparison.left
13565 && unpack.module.as_deref().unwrap_or("std") == "std"
13566 && unpack.name == "array_unpack"
13567 && let [array] = unpack.args.as_slice()
13568 {
13569 let array = self.compile_expr_ctx(array, ctx)?;
13570 let element = "<unnested element>".to_string();
13571 self.inline_bindings
13572 .insert(element.clone(), IrExpr::RawSql("\"_unnested\".\"v\"".to_string()));
13573 let per_element = self.compile_expr_ctx(
13574 &Expr::BinOp(Box::new(ast::BinOp {
13575 left: Expr::Path(ast::Path::absolute(element.clone())),
13576 op: comparison.op.clone(),
13577 right: comparison.right.clone(),
13578 })),
13579 ctx,
13580 );
13581 self.inline_bindings.remove(&element);
13582 let (aggregate, over_nothing) = if f.name == "all" {
13583 ("bool_and", "true")
13584 } else {
13585 ("bool_or", "false")
13586 };
13587 return Ok(IrExpr::FunctionCall(IrFunctionCall {
13588 return_pg_type: None,
13589 schema: None,
13590 name: aggregate.to_string(),
13591 args: vec![per_element?, array],
13592 sql_template: Some(format!(
13593 "(SELECT coalesce({aggregate}($1), {over_nothing}) FROM unnest($2) AS \"_unnested\"(\"v\"))"
13594 )),
13595 }));
13596 }
13597 if let Some(args) = self.compile_named_call_args(f, ctx)? {
13598 return self.resolve_fn_call(f.module.as_deref(), &f.name, args);
13599 }
13600 let is_explicit_set = f.module.as_deref().unwrap_or("std") == "std"
13601 && matches!(f.name.as_str(), "any" | "all")
13602 && f.args.len() == 1;
13603 let is_universal = is_explicit_set && f.name == "all";
13604 if is_explicit_set {
13605 self.explicit_set_depth += 1;
13606 }
13607 if is_universal {
13608 self.universal_set_depth += 1;
13609 }
13610 let args = f
13611 .args
13612 .iter()
13613 .map(|a| self.compile_expr_ctx(a, ctx))
13614 .collect::<Result<Vec<_>, _>>();
13615 if is_universal {
13616 self.universal_set_depth -= 1;
13617 }
13618 if is_explicit_set {
13619 self.explicit_set_depth -= 1;
13620 }
13621 let args = args?;
13622 if is_explicit_set
13628 && let [values] = args.as_slice()
13629 && yields_array(values)
13630 {
13631 let (aggregate, over_nothing) = if f.name == "all" {
13632 ("bool_and", "true")
13633 } else {
13634 ("bool_or", "false")
13635 };
13636 return Ok(IrExpr::FunctionCall(IrFunctionCall {
13637 return_pg_type: None,
13638 schema: None,
13639 name: aggregate.to_string(),
13640 args: vec![values.clone()],
13641 sql_template: Some(format!(
13642 "(SELECT coalesce({aggregate}(\"_s\".\"v\"), {over_nothing}) FROM unnest($1) AS \"_s\"(\"v\"))"
13643 )),
13644 }));
13645 }
13646 self.resolve_fn_call(f.module.as_deref(), &f.name, args)
13647 }
13648
13649 Expr::UnaryOp(u) if u.op == ast::UnaryOpKind::Exists => self.compile_exists_ctx(&u.operand, ctx),
13650
13651 Expr::UnaryOp(u) if u.op == ast::UnaryOpKind::Distinct => {
13657 let operand = self.compile_expr_ctx(&u.operand, ctx)?;
13658 Ok(match operand {
13659 IrExpr::PathSubquery(mut ps) => {
13660 ps.distinct = true;
13661 IrExpr::PathSubquery(ps)
13662 }
13663 IrExpr::ArrayFromSelect(src) => IrExpr::ArrayFromSelect(Box::new(match *src {
13664 IrArraySource::Select(mut sel) => {
13665 sel.distinct = true;
13666 IrArraySource::Select(sel)
13667 }
13668 IrArraySource::PathSelect(mut ps) => {
13669 ps.distinct = true;
13670 IrArraySource::PathSelect(ps)
13671 }
13672 other => other,
13673 })),
13674 other => other,
13675 })
13676 }
13677
13678 Expr::UnaryOp(u) => {
13679 let operand = self.compile_expr_ctx(&u.operand, ctx)?;
13680 Ok(IrExpr::UnaryOp(Box::new(IrUnaryOp {
13681 op: u.op.clone(),
13682 operand,
13683 })))
13684 }
13685
13686 Expr::IfElse(ie) => {
13687 let condition = self.compile_expr_ctx(&ie.condition, ctx)?;
13688 let if_ = self.compile_expr_ctx(&ie.if_expr, ctx)?;
13689 let else_ = self.compile_expr_ctx(&ie.else_expr, ctx)?;
13690 Ok(IrExpr::IfElse(Box::new(IrIfElse { condition, if_, else_ })))
13691 }
13692
13693 Expr::Array(elems) => {
13694 let items = elems
13698 .iter()
13699 .map(|e| self.compile_expr_ctx(e, ctx).map(set_walk_as_scalar))
13700 .collect::<Result<Vec<_>, _>>()?;
13701 Ok(IrExpr::Array(items))
13702 }
13703
13704 Expr::NamedTuple(fields) => {
13705 let ir = fields
13706 .iter()
13707 .map(|(name, e)| Ok((name.clone(), self.compile_expr_ctx(e, ctx)?)))
13708 .collect::<Result<Vec<_>, PyQLError>>()?;
13709 Ok(IrExpr::NamedTuple {
13710 fields: ir,
13711 is_free_object: false,
13712 })
13713 }
13714
13715 Expr::Tuple(elems) => {
13716 let ir = elems
13717 .iter()
13718 .map(|e| self.compile_expr_ctx(e, ctx))
13719 .collect::<Result<Vec<_>, PyQLError>>()?;
13720 Ok(IrExpr::Tuple(ir))
13721 }
13722
13723 Expr::FieldAccess { expr: inner, field } => {
13724 if let Some(ir) = self.try_compile_walk_off_subquery(expr, ctx)? {
13727 return Ok(ir);
13728 }
13729 if let Expr::NamedTuple(fields) = inner.as_ref() {
13730 let (_, val) = fields.iter().find(|(k, _)| k == field).ok_or_else(|| {
13731 self.type_err(&format!("{field} is not a member of {}", named_tuple_type_str(fields)))
13732 })?;
13733 return self.compile_expr_ctx(val, ctx);
13734 }
13735 if let Some(ps) = self.compile_shape_field_select(expr, ctx)? {
13736 return Ok(IrExpr::ArrayFromSelect(Box::new(IrArraySource::PathSelect(Box::new(
13737 ps,
13738 )))));
13739 }
13740 let (base, fields) = Self::peel_field_access_chain(expr);
13745 if let Expr::SubQuery(stmt) = base {
13746 let stmt = stmt.as_ref().clone();
13747 return self.compile_subquery_expr(&stmt, &fields, ctx, &[]);
13748 }
13749 if let Expr::FunctionCall(fc) = base {
13752 let fc = fc.clone();
13753 if let Some(ir) = self.try_compile_fn_scalar_subquery(&fc, &fields, None, ctx)? {
13754 return Ok(ir);
13755 }
13756 }
13757 let ir = self.compile_expr_ctx(inner, ctx)?;
13758 Ok(Self::project_free_object_field(ir, field))
13759 }
13760
13761 Expr::TupleIndex { expr: inner, index } => {
13762 match inner.as_ref() {
13763 Expr::Tuple(elems) => {
13764 let elem = elems.get(*index).ok_or_else(|| {
13765 self.type_err(&format!(
13766 "{index} is not a member of {}",
13767 positional_tuple_type_str(elems)
13768 ))
13769 })?;
13770 self.compile_expr_ctx(elem, ctx)
13771 }
13772 Expr::NamedTuple(fields) => {
13773 let (_, val) = fields.get(*index).ok_or_else(|| {
13774 self.type_err(&format!("{index} is not a member of {}", named_tuple_type_str(fields)))
13775 })?;
13776 self.compile_expr_ctx(val, ctx)
13777 }
13778 _ => {
13784 if let Expr::TypeCast(tc) = inner.as_ref()
13785 && let Some(shape) = self.resolve_tuple_cast_shape(&tc.ty)
13786 && *index >= shape.members.len()
13787 {
13788 return Err(self.type_err(&format!(
13789 "{index} is not a member of {}",
13790 self.type_expr_to_display_str(&tc.ty)
13791 )));
13792 }
13793 let ir = self.compile_expr_ctx(inner, ctx)?;
13794 Ok(IrExpr::JsonbIndex {
13795 expr: Box::new(ir),
13796 index: *index,
13797 })
13798 }
13799 }
13800 }
13801
13802 Expr::Detached(inner) => {
13812 if ctx.is_some()
13813 && let Some(root) = self.find_path_root_in_expr(inner)
13814 {
13815 let synthetic = ast::SelectStmt {
13816 result: (**inner).clone(),
13817 filter: None,
13818 order_by: vec![],
13819 offset: None,
13820 limit: None,
13821 lock: None,
13822 };
13823 let ps = self.compile_expr_as_path_select(&synthetic, inner, &root, false)?;
13824 return Ok(IrExpr::PathSubquery(Box::new(ps)));
13825 }
13826 self.compile_expr_ctx(inner, None)
13827 }
13828
13829 Expr::Set(elems) if ctx.is_none() && elems.is_empty() => Ok(IrExpr::Null),
13835
13836 Expr::Set(elems) if ctx.is_none() => {
13837 let compiled: Result<Vec<_>, _> = elems.iter().map(|e| self.compile_expr_ctx(e, ctx)).collect();
13838 let mut compiled = compiled?;
13839 if compiled.len() == 1 {
13840 Ok(compiled.remove(0))
13841 } else {
13842 Err(self.type_err("multi-element set literal is not supported in free SELECT context"))
13843 }
13844 }
13845
13846 Expr::Shape(s) if s.expr.is_none() => {
13860 let fields = s
13861 .elements
13862 .iter()
13863 .map(|el| -> Result<(String, IrExpr), PyQLError> {
13864 let name = path_leaf(&el.path)?.to_string();
13865 let expr = el.compexpr.as_ref().ok_or_else(|| {
13866 self.type_err("free object field must have a value expression (':= expr')")
13867 })?;
13868 let compiled = match expr {
13869 Expr::SubQuery(stmt)
13870 if matches!(stmt.as_ref(), Stmt::Insert(_) | Stmt::Update(_) | Stmt::Delete(_)) =>
13871 {
13872 self.dml_as_value(stmt.as_ref())?
13873 }
13874 other => match self.free_object_link_field(other)? {
13875 Some(object) => object,
13876 None => self.compile_expr_ctx(other, ctx)?,
13877 },
13878 };
13879 Ok((name, compiled))
13880 })
13881 .collect::<Result<Vec<_>, _>>()?;
13882 Ok(IrExpr::NamedTuple {
13883 fields,
13884 is_free_object: true,
13885 })
13886 }
13887
13888 Expr::Shape(s) if matches!(&s.expr, Some(inner) if self.is_free_cte_ref(inner)) => {
13896 let Some(Expr::Path(root_path)) = &s.expr else {
13897 unreachable!()
13898 };
13899 let ast::PathStep::Name(root) = &root_path.steps[0] else {
13900 unreachable!()
13901 };
13902 let fields = s
13903 .elements
13904 .iter()
13905 .map(|el| -> Result<(String, IrExpr), PyQLError> {
13906 let name = path_leaf(&el.path)?.to_string();
13907 let expr = match &el.compexpr {
13908 Some(over) => self.compile_expr_ctx(over, ctx)?,
13909 None => match self.resolve_cte_field_chain(root, &[name.as_str()]) {
13910 Some(result) => result?,
13911 None => {
13912 return Err(self.type_err(&format!("free object '{root}' has no field '{name}'")));
13913 }
13914 },
13915 };
13916 Ok((name, expr))
13917 })
13918 .collect::<Result<Vec<_>, _>>()?;
13919 Ok(IrExpr::NamedTuple {
13920 fields,
13921 is_free_object: true,
13922 })
13923 }
13924
13925 Expr::Shape(sh)
13929 if ctx.is_some()
13930 && matches!(sh.expr.as_ref(), Some(Expr::Union(_, _)))
13931 && Self::union_of_relative_paths(sh.expr.as_ref().expect("checked")).is_some() =>
13932 {
13933 let (td, alias) = ctx.expect("checked by the guard");
13934 let operands =
13935 Self::union_of_relative_paths(sh.expr.as_ref().expect("checked")).expect("checked by the guard");
13936 let elements = sh.elements.clone();
13937 let multi = self.relative_paths_reach_many(td, &operands);
13938 let mut branches = Vec::with_capacity(operands.len());
13939 for path in operands {
13940 let mut steps = vec![ast::PathStep::Name(format!("{}::{}", td.module, td.name))];
13941 steps.extend(path.steps.iter().cloned());
13942 let rooted = ast::Path { steps, partial: false };
13943 let synthetic = ast::SelectStmt {
13944 result: Expr::Path(rooted.clone()),
13945 filter: None,
13946 order_by: vec![],
13947 offset: None,
13948 limit: None,
13949 lock: None,
13950 };
13951 let mut ps = self.compile_path_select(&synthetic, &rooted, &elements, false)?;
13952 Self::correlate_path_select(&mut ps, alias);
13953 branches.push(ps);
13954 }
13955 Ok(IrExpr::ObjectPathUnion {
13956 branches,
13957 limit: None,
13958 multi,
13959 })
13960 }
13961
13962 Expr::Shape(sh)
13966 if ctx.is_some() && sh.expr.as_ref().and_then(Self::coalesce_of_relative_paths).is_some() =>
13967 {
13968 let (td, alias) = ctx.expect("checked by the guard");
13969 let operands =
13970 Self::coalesce_of_relative_paths(sh.expr.as_ref().expect("checked")).expect("checked by the guard");
13971 let multi = self.relative_paths_reach_many(td, &operands);
13972 let branches = self.coalesce_path_branches(td, alias, &operands, &sh.elements)?;
13973 Ok(IrExpr::ObjectPathUnion {
13974 branches,
13975 limit: None,
13976 multi,
13977 })
13978 }
13979
13980 Expr::Shape(sh) if matches!(sh.expr.as_ref(), Some(Expr::SubQuery(_))) => {
13981 let sh = sh.clone();
13982 match self.shape_over_subquery(&sh)? {
13983 Some(ir) => Ok(ir),
13984 None => Err(PyQLError::Type(PyQLTypeError {
13985 message: "shapes and set literals are not valid in expression context".into(),
13986 position: Position { line: 0, col: 0 },
13987 })),
13988 }
13989 }
13990
13991 Expr::Shape(sh) if Self::shape_over_subquery_projection(sh).is_some() => {
13992 let (stmt, fields) = Self::shape_over_subquery_projection(sh).expect("checked by the guard");
13993 let stmt = stmt.clone();
13994 let elements = sh.elements.clone();
13995 self.compile_subquery_expr(&stmt, &fields, ctx, &elements)
13996 }
13997
13998 Expr::Set(items) if items.is_empty() => Ok(IrExpr::Null),
14003
14004 Expr::Shape(sh)
14005 if matches!(sh.expr.as_ref(), Some(Expr::Path(p))
14006 if !p.partial
14007 && matches!(p.steps.first(), Some(ast::PathStep::Name(n))
14008 if self.cte_object_type(n).is_some() || self.for_var_types.contains_key(n))) =>
14009 {
14010 let expr = expr.clone();
14011 match self.shape_over_binding(&expr)? {
14012 Some(ir) => Ok(ir),
14013 None => Err(PyQLError::Type(PyQLTypeError {
14014 message: "shapes and set literals are not valid in expression context".into(),
14015 position: Position { line: 0, col: 0 },
14016 })),
14017 }
14018 }
14019
14020 Expr::Shape(_) | Expr::Set(_) => Err(PyQLError::Type(PyQLTypeError {
14026 message: "shapes and set literals are not valid in expression context".into(),
14027 position: Position { line: 0, col: 0 },
14028 })),
14029
14030 Expr::SubQuery(stmt) => {
14038 let stmt = stmt.as_ref().clone();
14039 self.compile_subquery_expr(&stmt, &[], ctx, &[])
14040 }
14041
14042 Expr::Union(_, _) => Err(PyQLError::Type(PyQLTypeError {
14043 message: "union is not valid in expression context".into(),
14044 position: Position { line: 0, col: 0 },
14045 })),
14046
14047 Expr::Except(left, right) | Expr::Intersect(left, right) => {
14051 let op = if matches!(expr, Expr::Intersect(_, _)) {
14052 super::SetOpKind::Intersect
14053 } else {
14054 super::SetOpKind::Except
14055 };
14056 let left = self.compile_expr_ctx(left, ctx)?;
14057 let right = self.compile_expr_ctx(right, ctx)?;
14058 Ok(IrExpr::SetOp {
14059 op,
14060 left: Box::new(left),
14061 right: Box::new(right),
14062 mode: super::SetOpMode::Array,
14063 })
14064 }
14065
14066 Expr::TypeIs { expr, ty } => match ctx {
14072 Some((td, alias)) => self.compile_type_is(expr, ty, td, alias),
14073 None => match expr.as_ref() {
14074 Expr::Path(p) => self.compile_path_type_is(p, ty, None),
14075 _ => Err(self.type_err("'is' type check is not valid in free SELECT context")),
14076 },
14077 },
14078 }
14079 }
14080
14081 fn compile_expr(&mut self, expr: &Expr, td: &TypeDescriptor, alias: &str) -> Result<IrExpr, PyQLError> {
14082 self.compile_expr_ctx(expr, Some((td, alias)))
14083 }
14084
14085 fn compile_free_expr(&mut self, expr: &Expr) -> Result<IrExpr, PyQLError> {
14086 self.compile_expr_ctx(expr, None)
14087 }
14088
14089 fn compile_path_type_is(
14092 &mut self,
14093 path: &ast::Path,
14094 ty: &ast::TypeExpr,
14095 ctx: Option<(&TypeDescriptor, &str)>,
14096 ) -> Result<IrExpr, PyQLError> {
14097 let (ty_module, ty_name) = ty
14098 .as_named()
14099 .ok_or_else(|| self.type_err("cannot use IS with a tuple or array type"))?;
14100 let check_qname = match (ty_module, ctx) {
14101 (Some(module), _) => format!("{module}::{ty_name}"),
14102 (None, Some((td, _))) => format!("{}::{}", td.module, ty_name),
14103 (None, None) => ty_name.to_string(),
14104 };
14105 let check_td = self.resolve_type(&check_qname)?;
14106 let check_qname = format!("{}::{}", check_td.module, check_td.name);
14107 let mut type_path = path.clone();
14108 type_path.steps.push(ast::PathStep::Name("__type__".to_string()));
14109 let own_type = self.compile_expr_ctx(&Expr::Path(type_path), ctx)?;
14110 let implementors: Vec<String> = self
14111 .find_poly_implementors(&check_qname)
14112 .into_iter()
14113 .map(|implementor| implementor.type_name)
14114 .collect();
14115 let is_one_of = |own_type: IrExpr| {
14116 implementors
14117 .iter()
14118 .map(|implementor| {
14119 IrExpr::BinOp(Box::new(IrBinOp {
14120 left: own_type.clone(),
14121 op: ast::BinOpKind::Eq,
14122 right: IrExpr::Literal(IrLiteral::Str(implementor.clone())),
14123 }))
14124 })
14125 .reduce(|left, right| {
14126 IrExpr::BinOp(Box::new(IrBinOp {
14127 left,
14128 op: ast::BinOpKind::Or,
14129 right,
14130 }))
14131 })
14132 .unwrap_or(IrExpr::Literal(IrLiteral::Bool(false)))
14133 };
14134 Ok(match own_type {
14136 IrExpr::ArrayFromSelect(source) => match *source {
14137 IrArraySource::PathSelect(mut path_select) => {
14138 let IrPathResult::Scalar(element, cast) = path_select.result else {
14139 return Err(self.type_err("'__type__' is read as a value"));
14140 };
14141 path_select.result = IrPathResult::Scalar(is_one_of(element), cast);
14142 IrExpr::ArrayFromSelect(Box::new(IrArraySource::PathSelect(path_select)))
14143 }
14144 other => is_one_of(IrExpr::ArrayFromSelect(Box::new(other))),
14145 },
14146 single => is_one_of(single),
14147 })
14148 }
14149
14150 fn compile_type_is(
14151 &mut self,
14152 expr: &Expr,
14153 ty: &ast::TypeExpr,
14154 td: &TypeDescriptor,
14155 alias: &str,
14156 ) -> Result<IrExpr, PyQLError> {
14157 self.compile_type_is_inner(expr, ty, td, alias, false)
14158 }
14159
14160 fn compile_type_is_set(
14166 &mut self,
14167 expr: &Expr,
14168 ty: &ast::TypeExpr,
14169 ctx: Option<(&TypeDescriptor, &str)>,
14170 ) -> Result<IrExpr, PyQLError> {
14171 match ctx {
14172 Some((td, alias)) => self.compile_type_is_inner(expr, ty, td, alias, true),
14173 None => match expr {
14174 Expr::Path(p) => self.compile_path_type_is(p, ty, None),
14175 _ => Err(self.type_err("'is' type check is not valid in free SELECT context")),
14176 },
14177 }
14178 }
14179
14180 fn compile_type_is_inner(
14181 &mut self,
14182 expr: &Expr,
14183 ty: &ast::TypeExpr,
14184 td: &TypeDescriptor,
14185 alias: &str,
14186 set_valued: bool,
14187 ) -> Result<IrExpr, PyQLError> {
14188 let self_qname = format!("{}::{}", td.module, td.name);
14189
14190 let reaches_other_objects = match expr {
14193 Expr::Path(p) if p.partial || p.steps.len() > 1 => {
14194 !matches!(p.steps.last(), Some(ast::PathStep::Name(n)) if n == "__type__")
14195 }
14196 Expr::Path(p) => matches!(
14197 p.steps.as_slice(),
14198 [ast::PathStep::Name(n)] if n != &td.name && n != &self_qname && self.resolve_type(n).is_err()
14199 && self.resolve_name_ref(n, true).is_some()
14200 ),
14201 _ => false,
14202 };
14203 if reaches_other_objects && let Expr::Path(p) = expr {
14204 return self.compile_path_type_is(p, ty, Some((td, alias)));
14205 }
14206
14207 let (source_qname, cross_scope) = match expr {
14210 Expr::Path(p) if !p.partial && p.steps.len() == 1 => {
14211 if let ast::PathStep::Name(n) = &p.steps[0] {
14212 if n == &td.name || n == &self_qname || self.scope_answers_to(n, &self_qname) {
14213 (self_qname.clone(), false)
14214 } else {
14215 match self.resolve_type(n) {
14217 Ok(other) => (format!("{}::{}", other.module, other.name), true),
14218 Err(_) => (self_qname.clone(), false),
14219 }
14220 }
14221 } else {
14222 (self_qname.clone(), false)
14223 }
14224 }
14225 _ => (self_qname.clone(), false),
14226 };
14227
14228 let (ty_module, ty_name) = ty
14229 .as_named()
14230 .ok_or_else(|| self.type_err("cannot use IS with a tuple or array type"))?;
14231 let check_module = ty_module.unwrap_or(td.module.as_str());
14232 let check_qname = format!("{}::{}", check_module, ty_name);
14233 self.resolve_type(&check_qname)?;
14234
14235 if !cross_scope {
14236 return Ok(self.type_check_bool_expr(&source_qname, &check_qname, td, alias));
14238 }
14239
14240 let src_alias = self.fresh_alias();
14243 let src_td = self.resolve_type(&source_qname)?;
14244 let source_table = src_td.table.clone();
14245 let (poly_implementors, poly_columns) = if self.is_polymorphic(src_td) {
14246 (
14247 self.find_poly_implementors(&source_qname),
14248 Self::poly_dml_columns(src_td),
14249 )
14250 } else {
14251 (vec![], vec![])
14252 };
14253 let bool_expr = self.type_check_bool_expr(&source_qname, &check_qname, src_td, &src_alias);
14254
14255 if !set_valued && matches!(bool_expr, IrExpr::Literal(IrLiteral::Bool(_))) {
14262 return Ok(bool_expr);
14263 }
14264
14265 let source = IrSource {
14266 poly: None,
14267 type_name: source_qname.clone(),
14268 table: source_table,
14269 alias: src_alias,
14270 };
14271
14272 let answers = IrExpr::ArrayFromSelect(Box::new(IrArraySource::RawExpr {
14273 source,
14274 poly_implementors,
14275 poly_columns,
14276 expr: bool_expr,
14277 }));
14278 if set_valued {
14279 return Ok(answers);
14280 }
14281 Ok(IrExpr::FunctionCall(IrFunctionCall {
14288 return_pg_type: None,
14289 schema: Some("_pylon".to_string()),
14290 name: "assert_single".to_string(),
14291 args: vec![
14292 answers,
14293 IrExpr::Literal(IrLiteral::Str(format!(
14294 "'{source_qname} is {check_qname}' is asked once for every {source_qname} object, \
14295 so it is not the single value a filter or an 'if' condition needs — narrow it to \
14296 one object, or name the type being selected"
14297 ))),
14298 ],
14299 sql_template: None,
14300 }))
14301 }
14302
14303 fn type_check_bool_expr(&self, source_qname: &str, check_qname: &str, td: &TypeDescriptor, alias: &str) -> IrExpr {
14305 if check_qname == source_qname || Self::is_or_implements(td, check_qname) {
14306 return IrExpr::Literal(IrLiteral::Bool(true));
14307 }
14308 if !self.is_polymorphic(td) {
14309 return IrExpr::Literal(IrLiteral::Bool(false));
14310 }
14311 let reachable: Vec<String> = self
14323 .find_poly_implementors(source_qname)
14324 .into_iter()
14325 .map(|implementor| implementor.type_name)
14326 .collect();
14327 let matching: Vec<String> = self
14328 .find_poly_implementors(check_qname)
14329 .into_iter()
14330 .map(|implementor| implementor.type_name)
14331 .filter(|type_name| reachable.contains(type_name))
14332 .collect();
14333 if matching.is_empty() {
14334 return IrExpr::Literal(IrLiteral::Bool(false));
14335 }
14336 if matching.len() == reachable.len() {
14337 return IrExpr::Literal(IrLiteral::Bool(true));
14338 }
14339 matching
14340 .into_iter()
14341 .map(|type_name| {
14342 IrExpr::BinOp(Box::new(IrBinOp {
14343 left: IrExpr::ColumnRef {
14344 alias: alias.to_string(),
14345 column: "__type__".into(),
14346 pg_type: "text".into(),
14347 },
14348 op: crate::parse::ast::BinOpKind::Eq,
14349 right: IrExpr::Literal(IrLiteral::Str(type_name)),
14350 }))
14351 })
14352 .reduce(|left, right| {
14353 IrExpr::BinOp(Box::new(IrBinOp {
14354 left,
14355 op: crate::parse::ast::BinOpKind::Or,
14356 right,
14357 }))
14358 })
14359 .unwrap_or(IrExpr::Literal(IrLiteral::Bool(false)))
14360 }
14361
14362 fn resolve_name_ref(&mut self, name: &str, allow_fn_param: bool) -> Option<IrExpr> {
14376 if self.for_vars.contains_key(name) {
14377 return Some(self.for_var_ref(name));
14378 }
14379 if let Some(IrFreeExpr::FreeObject(_)) = self.cte_free_items.get(name) {
14390 return Some(IrExpr::NamedTuple {
14391 fields: vec![],
14392 is_free_object: true,
14393 });
14394 }
14395 if let Some(ir) = self.inline_bindings.get(name) {
14396 return Some(ir.clone());
14397 }
14398 if let Some(t) = self.cte_types.get(name) {
14399 let scalar = !t.contains("::");
14403 return Some(IrExpr::CteRef {
14404 name: self.cte_sql_name(name),
14405 scalar,
14406 pg_type: (scalar && !t.is_empty()).then(|| literal_sentinel_to_pg(t).to_string()),
14407 });
14408 }
14409 if allow_fn_param && let Some(pg_type) = self.fn_params.get(name) {
14410 return Some(IrExpr::FnParam {
14411 name: name.to_string(),
14412 pg_type: pg_type.clone(),
14413 });
14414 }
14415 None
14416 }
14417
14418 fn enclosing_anchor(&self, root: &str) -> Option<(String, String)> {
14425 let innermost = self.anchors.last()?;
14426 if !innermost.detached || !innermost.answers_to(root) {
14427 return None;
14428 }
14429 self.anchors
14430 .iter()
14431 .rev()
14432 .skip(1)
14433 .find(|a| a.answers_to(root))
14434 .map(|a| (a.qualified.clone(), a.alias.clone()))
14435 }
14436
14437 fn scope_answers_to(&self, root: &str, self_qname: &str) -> bool {
14441 self.anchors
14442 .last()
14443 .is_some_and(|anchor| anchor.qualified == self_qname && anchor.answers_to(root))
14444 }
14445
14446 fn outer_anchor(&self, root: &str) -> Option<(String, String)> {
14448 self.anchors
14449 .iter()
14450 .rev()
14451 .find(|a| a.answers_to(root))
14452 .map(|a| (a.qualified.clone(), a.alias.clone()))
14453 }
14454
14455 fn compile_path(&mut self, p: &ast::Path, td: &TypeDescriptor, alias: &str) -> Result<IrExpr, PyQLError> {
14456 if !p.partial {
14457 if p.steps.len() == 1
14458 && let ast::PathStep::Name(n) = &p.steps[0]
14459 {
14460 if let Some(ir) = self.resolve_name_ref(n, true) {
14461 return Ok(ir);
14462 }
14463 if n == "__type__" {
14465 return Ok(if self.is_polymorphic(td) {
14466 IrExpr::ColumnRef {
14467 alias: alias.to_string(),
14468 column: "__type__".to_string(),
14469 pg_type: "text".to_string(),
14470 }
14471 } else {
14472 IrExpr::Literal(IrLiteral::Str(format!("{}::{}", td.module, td.name)))
14473 });
14474 }
14475 }
14476 if p.steps.len() == 2
14478 && let [ast::PathStep::Name(type_ref), ast::PathStep::Name(variant)] = p.steps.as_slice()
14479 && self.resolve_enum(type_ref).is_some()
14480 {
14481 return self.compile_enum_access(type_ref, variant);
14482 }
14483 if let Some(resolved) = self.resolve_cte_path(p) {
14485 return resolved;
14486 }
14487 if p.steps.len() > 1
14503 && let ast::PathStep::Name(root) = &p.steps[0]
14504 && self
14505 .cte_types
14506 .get(root.as_str())
14507 .map(|t| t.contains("::"))
14508 .unwrap_or(false)
14509 {
14510 let full_path = ast::Path {
14511 steps: p.steps.clone(),
14512 partial: false,
14513 };
14514 let synthetic = ast::SelectStmt {
14515 result: Expr::Path(full_path.clone()),
14516 filter: None,
14517 order_by: vec![],
14518 offset: None,
14519 limit: None,
14520 lock: None,
14521 };
14522 let ps = self.compile_path_select(&synthetic, &full_path, &[], false)?;
14523 return Ok(IrExpr::PathSubquery(Box::new(ps)));
14524 }
14525 if p.steps.len() > 1
14539 && let ast::PathStep::Name(root) = &p.steps[0]
14540 && (root == "__new__" || root == "__old__")
14541 {
14542 if let Some((anchor_td, anchor_alias)) = self.special_anchors.get(root).cloned() {
14543 let relative = ast::Path {
14544 steps: p.steps[1..].to_vec(),
14545 partial: true,
14546 };
14547 return self.compile_path(&relative, anchor_td, &anchor_alias);
14548 }
14549 return Err(PyQLError::Resolution(PyQLResolutionError::UnknownField(
14550 PyQLUnknownFieldError {
14551 message: format!("{root} cannot be used in this expression"),
14552 position: Position { line: 0, col: 0 },
14553 },
14554 )));
14555 }
14556 if p.steps.len() > 1 && matches!(&p.steps[0], ast::PathStep::Name(root) if root == "__subject__") {
14559 let relative = ast::Path {
14560 steps: p.steps[1..].to_vec(),
14561 partial: true,
14562 };
14563 return self.compile_path(&relative, td, alias);
14564 }
14565 if p.steps.len() > 1
14569 && let ast::PathStep::Name(root) = &p.steps[0]
14570 {
14571 let qualified = format!("{}::{}", td.module, td.name);
14572 if *root == td.name || *root == qualified {
14573 let relative = ast::Path {
14574 steps: p.steps[1..].to_vec(),
14575 partial: true,
14576 };
14577 if let Some((outer_qualified, outer_alias)) = self.enclosing_anchor(root) {
14581 let outer_td = self.resolve_type(&outer_qualified)?.clone();
14582 return self.compile_path(&relative, &outer_td, &outer_alias);
14583 }
14584 return self.compile_path(&relative, td, alias);
14585 }
14586 if let Some((outer_qualified, outer_alias)) = self.outer_anchor(root) {
14591 let relative = ast::Path {
14592 steps: p.steps[1..].to_vec(),
14593 partial: true,
14594 };
14595 let outer_td = self.resolve_type(&outer_qualified)?.clone();
14596 return self.compile_path(&relative, &outer_td, &outer_alias);
14597 }
14598 }
14599 if let Some(ast::PathStep::Name(var)) = p.steps.first()
14604 && p.steps.len() > 1
14605 && self.for_var_types.contains_key(var)
14606 && !matches!(p.steps[1], ast::PathStep::TypeIntersection(_))
14607 {
14608 let synthetic = ast::SelectStmt {
14609 result: Expr::Path(p.clone()),
14610 filter: None,
14611 order_by: vec![],
14612 offset: None,
14613 limit: None,
14614 lock: None,
14615 };
14616 let ps = self.compile_path_select(&synthetic, p, &[], false)?;
14617 return Ok(IrExpr::PathSubquery(Box::new(ps)));
14618 }
14619 if let [
14627 ast::PathStep::Name(var),
14628 ast::PathStep::TypeIntersection(type_ref),
14629 rest @ ..,
14630 ] = p.steps.as_slice()
14631 && !rest.is_empty()
14632 && self.for_var_types.contains_key(var)
14633 {
14634 let type_name = match &type_ref.module {
14635 Some(m) => format!("{}::{}", m, type_ref.name),
14636 None => type_ref.name.clone(),
14637 };
14638 let narrowed = self.resolve_type(&type_name)?;
14639 let mut steps = vec![ast::PathStep::Name(format!("{}::{}", narrowed.module, narrowed.name))];
14640 steps.extend(rest.iter().cloned());
14641 let rooted = ast::Path { steps, partial: false };
14642 let synthetic = ast::SelectStmt {
14643 result: Expr::Path(rooted.clone()),
14644 filter: None,
14645 order_by: vec![],
14646 offset: None,
14647 limit: None,
14648 lock: None,
14649 };
14650 let mut ps = self.compile_path_select(&synthetic, &rooted, &[], false)?;
14651 let correlation = IrExpr::BinOp(Box::new(IrBinOp {
14652 left: IrExpr::ColumnRef {
14653 alias: ps.root.alias.clone(),
14654 column: "id".to_string(),
14655 pg_type: "uuid".to_string(),
14656 },
14657 op: ast::BinOpKind::Eq,
14658 right: self.for_var_ref(var),
14659 }));
14660 ps.filter = and_conditions(ps.filter, vec![correlation]);
14661 return Ok(IrExpr::PathSubquery(Box::new(ps)));
14662 }
14663 if let [ast::PathStep::Name(var), ast::PathStep::TypeIntersection(type_ref)] = p.steps.as_slice()
14664 && self.for_var_types.contains_key(var)
14665 {
14666 let type_name = match &type_ref.module {
14667 Some(m) => format!("{}::{}", m, type_ref.name),
14668 None => type_ref.name.clone(),
14669 };
14670 let narrowed = self.resolve_type(&type_name)?;
14671 let narrowed_alias = self.fresh_alias();
14672 let source = IrSource {
14673 poly: self.poly_fanout_for(&format!("{}::{}", narrowed.module, narrowed.name)),
14674 type_name: format!("{}::{}", narrowed.module, narrowed.name),
14675 table: narrowed.table.clone(),
14676 alias: narrowed_alias.clone(),
14677 };
14678 let filter = IrExpr::BinOp(Box::new(IrBinOp {
14679 left: IrExpr::ColumnRef {
14680 alias: narrowed_alias,
14681 column: "id".to_string(),
14682 pg_type: "uuid".to_string(),
14683 },
14684 op: ast::BinOpKind::Eq,
14685 right: self.for_var_ref(var),
14686 }));
14687 return Ok(IrExpr::Subquery(Box::new(IrSelect::schema_bound(
14688 source,
14689 Self::pk_returning(narrowed),
14690 Some(filter),
14691 ))));
14692 }
14693 return Err(PyQLError::Type(PyQLTypeError {
14694 message: "absolute paths are not valid in expression context; use .name".into(),
14695 position: Position { line: 0, col: 0 },
14696 }));
14697 }
14698
14699 if p.partial
14704 && let [ast::PathStep::LinkProp(prop_name)] = p.steps.as_slice()
14705 {
14706 return self.compile_link_prop_ref(prop_name);
14707 }
14708
14709 if p.partial && matches!(p.steps.first(), Some(ast::PathStep::TypeIntersection(_))) {
14711 return self.compile_type_intersection_expr(&p.steps, td, alias);
14712 }
14713
14714 if p.partial && matches!(p.steps.first(), Some(ast::PathStep::Backlink(_))) {
14719 return self.compile_partial_path_as_subquery(p, td, alias);
14720 }
14721 if p.partial
14726 && p.steps.len() > 1
14727 && let Some(ast::PathStep::Name(first)) = p.steps.first()
14728 && let Some(Expr::IfElse(ie)) = self
14729 .active_declared_pointers
14730 .iter()
14731 .find(|d| path_leaf(&d.path).is_ok_and(|name| name == first))
14732 .and_then(|d| d.compexpr.clone())
14733 {
14734 let extend = |branch: &Expr| match branch {
14735 Expr::Path(bp) => {
14736 let mut steps = bp.steps.clone();
14737 steps.extend(p.steps[1..].iter().cloned());
14738 Some(Expr::Path(ast::Path {
14739 steps,
14740 partial: bp.partial,
14741 }))
14742 }
14743 Expr::SubQuery(_) => p.steps[1..].iter().try_fold(branch.clone(), |expr, step| match step {
14746 ast::PathStep::Name(field) => Some(Expr::FieldAccess {
14747 expr: Box::new(expr),
14748 field: field.clone(),
14749 }),
14750 _ => None,
14751 }),
14752 Expr::Set(items) if items.is_empty() => Some(Expr::Set(vec![])),
14754 Expr::TypeCast(cast) if matches!(&cast.expr, Expr::Set(items) if items.is_empty()) => {
14755 Some(Expr::Set(vec![]))
14756 }
14757 _ => None,
14758 };
14759 if let (Some(if_expr), Some(else_expr)) = (extend(&ie.if_expr), extend(&ie.else_expr)) {
14760 let distributed = Expr::IfElse(Box::new(ast::IfElse {
14761 condition: ie.condition.clone(),
14762 if_expr,
14763 else_expr,
14764 }));
14765 return self.compile_expr(&distributed, td, alias);
14766 }
14767 }
14768
14769 let narrows_last = matches!(p.steps.last(), Some(ast::PathStep::TypeIntersection(_)));
14774
14775 if p.steps.len() == 2 && !narrows_last {
14776 return self.compile_path_2step(p, td, alias);
14777 }
14778
14779 if p.steps.len() != 1 {
14782 return self.compile_partial_path_as_subquery(p, td, alias);
14783 }
14784
14785 let pointer_name = match &p.steps[0] {
14786 ast::PathStep::Name(n) => n.as_str(),
14787 _ => {
14788 return Err(PyQLError::Type(PyQLTypeError {
14789 message: "type intersections are not valid in expression context".into(),
14790 position: Position { line: 0, col: 0 },
14791 }));
14792 }
14793 };
14794
14795 if pointer_name == "__type__" {
14798 return Ok(if self.is_polymorphic(td) {
14799 IrExpr::ColumnRef {
14800 alias: alias.to_string(),
14801 column: "__type__".to_string(),
14802 pg_type: "text".to_string(),
14803 }
14804 } else {
14805 IrExpr::Literal(IrLiteral::Str(format!("{}::{}", td.module, td.name)))
14806 });
14807 }
14808
14809 if let Some(prop) = Self::resolve_property(td, pointer_name) {
14810 return Ok(IrExpr::ColumnRef {
14811 alias: alias.to_string(),
14812 column: prop.name.clone(),
14813 pg_type: prop.pg_type.clone(),
14814 });
14815 }
14816
14817 if let Some(link) = Self::resolve_link(td, pointer_name) {
14818 if link.is_junction_backed() {
14819 return self.junction_target_id_expr(td, link, alias);
14820 }
14821 return Ok(IrExpr::ColumnRef {
14823 alias: alias.to_string(),
14824 column: format!("{}_id", link.name),
14825 pg_type: "uuid".to_string(),
14826 });
14827 }
14828
14829 if let Some(cd) = self.resolve_computed(td, pointer_name) {
14831 let expr_ast = crate::parse::parse_pointer_expr(&cd.expression).map_err(PyQLError::Syntax)?;
14832 return self.compile_expr(&expr_ast, td, alias);
14833 }
14834
14835 if Self::resolve_multilink(td, pointer_name).is_some() {
14841 return self.compile_partial_path_as_subquery(p, td, alias);
14842 }
14843
14844 if let Some(expr) = self
14849 .active_declared_pointers
14850 .iter()
14851 .find(|d| path_leaf(&d.path).is_ok_and(|n| n == pointer_name))
14852 .and_then(|d| d.compexpr.clone())
14853 {
14854 return self.compile_expr(&expr, td, alias);
14855 }
14856
14857 Err(self.field_err(pointer_name, &format!("{}::{}", td.module, td.name)))
14858 }
14859
14860 fn compile_free_path(&mut self, p: &ast::Path) -> Result<IrExpr, PyQLError> {
14865 if p.partial {
14866 if let Some((qualified, alias)) = self.anchors.last().map(|a| (a.qualified.clone(), a.alias.clone())) {
14870 let td = self.resolve_type(&qualified)?;
14871 return self.compile_path(p, td, &alias);
14872 }
14873 return Err(self.type_err(
14874 "property reference (.name) is not valid in free SELECT; \
14875 use a schema-bound SELECT instead",
14876 ));
14877 }
14878 if p.steps.len() == 2
14879 && let [ast::PathStep::Name(type_ref), ast::PathStep::Name(variant)] = p.steps.as_slice()
14880 && self.resolve_enum(type_ref).is_some()
14881 {
14882 return self.compile_enum_access(type_ref, variant);
14883 }
14884 if let Some(resolved) = self.resolve_cte_path(p) {
14887 return resolved;
14888 }
14889 if p.steps.len() == 1
14890 && let ast::PathStep::Name(n) = &p.steps[0]
14891 && let Some(ir) = self.resolve_name_ref(n, true)
14892 {
14893 return Ok(ir);
14894 }
14895 if !p.partial
14901 && p.steps.len() > 1
14902 && let Some(ast::PathStep::Name(root)) = p.steps.first()
14903 && self.cte_object_type(root).is_some()
14904 {
14905 let synthetic = ast::SelectStmt {
14906 result: Expr::Path(p.clone()),
14907 filter: None,
14908 order_by: vec![],
14909 offset: None,
14910 limit: None,
14911 lock: None,
14912 };
14913 let ps = self.compile_path_select(&synthetic, p, &[], false)?;
14914 let widens = ps.joins.iter().any(|join| !matches!(join, IrPathJoin::Single { .. }));
14919 let root_is_multi = self.multi_row_ctes.contains(root.as_str());
14920 let multi = matches!(ps.result, IrPathResult::Scalar(..)) && (widens || root_is_multi);
14921 return Ok(if multi {
14922 IrExpr::ArrayFromSelect(Box::new(IrArraySource::PathSelect(Box::new(ps))))
14923 } else {
14924 IrExpr::PathSubquery(Box::new(ps))
14925 });
14926 }
14927 Err(self.type_err("expression is not valid in free SELECT context"))
14928 }
14929
14930 fn compile_path_2step(&mut self, p: &ast::Path, td: &TypeDescriptor, alias: &str) -> Result<IrExpr, PyQLError> {
14931 let link_name = match &p.steps[0] {
14932 ast::PathStep::Name(n) => n.as_str(),
14933 _ => {
14934 return Err(PyQLError::Type(PyQLTypeError {
14935 message: "type intersections are not valid in expression context".into(),
14936 position: Position { line: 0, col: 0 },
14937 }));
14938 }
14939 };
14940 let pointer_name = match &p.steps[1] {
14941 ast::PathStep::Name(n) => n.as_str(),
14942 _ => {
14943 return Err(PyQLError::Type(PyQLTypeError {
14944 message: "type intersections are not valid in expression context".into(),
14945 position: Position { line: 0, col: 0 },
14946 }));
14947 }
14948 };
14949
14950 if let Some(link) = Self::resolve_link(td, link_name) {
14951 if pointer_name == "id" {
14952 if link.is_junction_backed() {
14953 return self.junction_target_id_expr(td, link, alias);
14954 }
14955 return Ok(IrExpr::ColumnRef {
14956 alias: alias.to_string(),
14957 column: format!("{}_id", link_name),
14958 pg_type: "uuid".to_string(),
14959 });
14960 }
14961 let target_td = self.resolve_type(&link.target)?;
14962 if let Some(prop) = Self::resolve_property(target_td, pointer_name) {
14963 let ft_alias = self.fresh_alias();
14964 let target_id_expr = if link.is_junction_backed() {
14965 self.junction_target_id_expr(td, link, alias)?
14966 } else {
14967 IrExpr::ColumnRef {
14968 alias: alias.to_string(),
14969 column: format!("{}_id", link_name),
14970 pg_type: "uuid".to_string(),
14971 }
14972 };
14973 return Ok(IrExpr::Subquery(Box::new(IrSelect::schema_bound(
14974 IrSource {
14975 poly: None,
14976 type_name: format!("{}::{}", target_td.module, target_td.name),
14977 table: target_td.table.clone(),
14978 alias: ft_alias.clone(),
14979 },
14980 vec![IrShapePointer::Scalar(IrScalarPointer {
14981 implicit_id: false,
14982 marker_offset: None,
14983 alias: prop.name.clone(),
14984 column: prop.name.clone(),
14985 pg_type: prop.pg_type.clone(),
14986 tuple_shape: self.resolve_property_tuple_shape(prop),
14987 })],
14988 Some(IrExpr::BinOp(Box::new(IrBinOp {
14989 left: IrExpr::ColumnRef {
14990 alias: ft_alias.clone(),
14991 column: "id".to_string(),
14992 pg_type: "uuid".to_string(),
14993 },
14994 op: ast::BinOpKind::Eq,
14995 right: target_id_expr,
14996 }))),
14997 ))));
14998 }
14999 return self.compile_partial_path_as_subquery(p, td, alias);
15002 }
15003
15004 if Self::resolve_multilink(td, link_name).is_some() {
15008 return self.compile_partial_path_as_subquery(p, td, alias);
15009 }
15010
15011 self.compile_partial_path_as_subquery(p, td, alias)
15018 }
15019
15020 fn try_backlink_exists(
15024 &mut self,
15025 b: &ast::BinOp,
15026 td: &TypeDescriptor,
15027 alias: &str,
15028 ) -> Result<Option<IrExpr>, PyQLError> {
15029 use ast::PathStep;
15030 fn is_backlink(p: &ast::Path) -> bool {
15031 p.partial && matches!(p.steps.first(), Some(PathStep::Backlink(_)))
15032 }
15033 let (path_steps, value_ast, flip) = if let Expr::Path(p) = &b.left {
15034 if is_backlink(p) {
15035 (p.steps.as_slice(), &b.right, false)
15036 } else {
15037 return Ok(None);
15038 }
15039 } else if let Expr::Path(p) = &b.right {
15040 if is_backlink(p) {
15041 (p.steps.as_slice(), &b.left, true)
15042 } else {
15043 return Ok(None);
15044 }
15045 } else {
15046 return Ok(None);
15047 };
15048 let value_expr = self.compile_comparison_value(value_ast, &b.op, td, alias)?;
15049 let current_qname = format!("{}::{}", td.module, td.name);
15050 let exists = self.compile_backlink_as_exists(
15051 path_steps,
15052 Some((b.op.clone(), value_expr, flip)),
15053 ¤t_qname,
15054 alias,
15055 )?;
15056 Ok(Some(exists))
15057 }
15058
15059 fn compile_backlink_as_exists(
15063 &mut self,
15064 steps: &[ast::PathStep],
15065 comparison: Option<(ast::BinOpKind, IrExpr, bool)>,
15066 current_qname: &str,
15067 alias: &str,
15068 ) -> Result<IrExpr, PyQLError> {
15069 use ast::PathStep;
15070
15071 let backlink_name = match steps.first() {
15072 Some(PathStep::Backlink(n)) => n.clone(),
15073 _ => return Err(self.type_err("internal: expected backlink step")),
15074 };
15075 let Some(PathStep::TypeIntersection(type_ref)) = steps.get(1) else {
15079 return self.backlink_exists_over_owners(
15082 None,
15083 &backlink_name,
15084 &steps[1..],
15085 comparison,
15086 current_qname,
15087 alias,
15088 );
15089 };
15090
15091 let type_name = match &type_ref.module {
15092 Some(m) => format!("{}::{}", m, type_ref.name),
15093 None => type_ref.name.clone(),
15094 };
15095 let target_td = self.resolve_type(&type_name)?;
15096 if self.declares_backlink(target_td, &backlink_name, current_qname) {
15097 return self.backlink_exists_for_owner(
15098 target_td,
15099 &backlink_name,
15100 &steps[2..],
15101 comparison,
15102 current_qname,
15103 alias,
15104 );
15105 }
15106 let narrow_to = format!("{}::{}", target_td.module, target_td.name);
15111 self.backlink_exists_over_owners(
15112 Some(&narrow_to),
15113 &backlink_name,
15114 &steps[2..],
15115 comparison,
15116 current_qname,
15117 alias,
15118 )
15119 }
15120
15121 fn backlink_owner(
15124 &self,
15125 owner_td: &'a TypeDescriptor,
15126 backlink_name: &str,
15127 current_qname: &str,
15128 ) -> Result<&'a TypeDescriptor, PyQLError> {
15129 Ok(if self.declares_backlink(owner_td, backlink_name, current_qname) {
15135 owner_td
15136 } else {
15137 let narrowed = format!("{}::{}", owner_td.module, owner_td.name);
15140 let declaring: Vec<&'a TypeDescriptor> = self
15141 .schema
15142 .types
15143 .iter()
15144 .filter(|t| !t.abstract_ && Self::is_or_implements(t, &narrowed))
15145 .filter(|t| self.declares_backlink(t, backlink_name, current_qname))
15146 .collect();
15147 let declaring = Self::without_inherited_owners(declaring);
15148 match declaring.as_slice() {
15149 [only] => only,
15150 [] => owner_td,
15151 several => {
15152 return Err(self.type_err(&format!(
15153 "'{backlink_name}' pointing to {current_qname} is declared by {} types under \
15154 {narrowed} ({}), so a backlink narrowed to it has no single source to read \
15155 — narrow to one of them instead",
15156 several.len(),
15157 several
15158 .iter()
15159 .map(|t| format!("{}::{}", t.module, t.name))
15160 .collect::<Vec<_>>()
15161 .join(", "),
15162 )));
15163 }
15164 }
15165 })
15166 }
15167
15168 fn backlink_is_single(&self, owner_td: &'a TypeDescriptor, backlink_name: &str, current_qname: &str) -> bool {
15171 let Ok(owner) = self.backlink_owner(owner_td, backlink_name, current_qname) else {
15172 return false;
15173 };
15174 owner
15175 .links
15176 .iter()
15177 .any(|l| l.name == backlink_name && l.is_exclusive && self.link_target_reaches(&l.target, current_qname))
15178 || owner.multilinks.iter().any(|ml| {
15179 ml.name == backlink_name && ml.is_exclusive && self.link_target_reaches(&ml.target, current_qname)
15180 })
15181 }
15182
15183 fn declares_backlink(&self, td: &TypeDescriptor, backlink_name: &str, current_qname: &str) -> bool {
15186 td.links
15187 .iter()
15188 .any(|l| l.name == backlink_name && self.link_target_reaches(&l.target, current_qname))
15189 || td
15190 .multilinks
15191 .iter()
15192 .any(|ml| ml.name == backlink_name && self.link_target_reaches(&ml.target, current_qname))
15193 }
15194
15195 #[allow(clippy::too_many_arguments)]
15199 fn without_inherited_owners(owners: Vec<&'a TypeDescriptor>) -> Vec<&'a TypeDescriptor> {
15202 let qnames: Vec<String> = owners.iter().map(|t| format!("{}::{}", t.module, t.name)).collect();
15203 owners
15204 .into_iter()
15205 .filter(|t| !t.bases.iter().any(|base| qnames.contains(base)))
15206 .collect()
15207 }
15208
15209 fn backlink_exists_over_owners(
15210 &mut self,
15211 narrow_to: Option<&str>,
15212 backlink_name: &str,
15213 rest: &[ast::PathStep],
15214 comparison: Option<(ast::BinOpKind, IrExpr, bool)>,
15215 current_qname: &str,
15216 alias: &str,
15217 ) -> Result<IrExpr, PyQLError> {
15218 let schema = self.schema;
15219 let owners: Vec<&'a TypeDescriptor> = schema
15220 .types
15221 .iter()
15222 .filter(|t| !t.abstract_)
15223 .filter(|t| narrow_to.is_none_or(|q| Self::is_or_implements(t, q)))
15224 .filter(|t| self.declares_backlink(t, backlink_name, current_qname))
15225 .collect();
15226 let owners = Self::without_inherited_owners(owners);
15227 if owners.is_empty() {
15228 return Err(self.type_err(&match narrow_to {
15229 Some(q) => format!("type {q} has no link or multi-link '{backlink_name}' pointing to {current_qname}"),
15230 None => format!("no type has a link or multi-link '{backlink_name}' pointing to {current_qname}"),
15231 }));
15232 }
15233 let mut combined: Option<IrExpr> = None;
15234 for owner_td in owners {
15235 let one = self.backlink_exists_for_owner(
15236 owner_td,
15237 backlink_name,
15238 rest,
15239 comparison.clone(),
15240 current_qname,
15241 alias,
15242 )?;
15243 combined = Some(match combined {
15244 None => one,
15245 Some(previous) => IrExpr::BinOp(Box::new(IrBinOp {
15246 left: previous,
15247 op: ast::BinOpKind::Or,
15248 right: one,
15249 })),
15250 });
15251 }
15252 Ok(combined.expect("owners is non-empty"))
15253 }
15254
15255 fn is_or_implements(td: &TypeDescriptor, qname: &str) -> bool {
15257 format!("{}::{}", td.module, td.name) == qname
15258 || td.interfaces.iter().any(|i| i == qname)
15259 || td.parents.iter().any(|p| p == qname)
15260 || td.bases.iter().any(|b| b == qname)
15261 }
15262
15263 fn has_subtypes(&self, td: &TypeDescriptor) -> bool {
15267 let qname = format!("{}::{}", td.module, td.name);
15268 !td.abstract_ && self.schema.types.iter().any(|t| t.bases.contains(&qname))
15269 }
15270
15271 fn backs_no_relation(td: &TypeDescriptor) -> bool {
15275 td.abstract_ && !td.materialized
15276 }
15277
15278 fn subtype_fanouts(&self) -> HashMap<(String, String), IrPolyFanout> {
15280 self.schema
15281 .types
15282 .iter()
15283 .filter(|t| self.has_subtypes(t) || Self::backs_no_relation(t))
15284 .filter_map(|t| {
15285 let fanout = self.poly_fanout_for(&format!("{}::{}", t.module, t.name))?;
15286 Some(((t.module.clone(), t.table.clone()), fanout))
15287 })
15288 .collect()
15289 }
15290
15291 fn is_polymorphic(&self, td: &TypeDescriptor) -> bool {
15296 td.abstract_ || self.has_subtypes(td)
15297 }
15298
15299 #[allow(clippy::too_many_arguments)]
15303 fn backlink_exists_for_owner(
15304 &mut self,
15305 target_td: &'a TypeDescriptor,
15306 backlink_name: &str,
15307 rest: &[ast::PathStep],
15308 comparison: Option<(ast::BinOpKind, IrExpr, bool)>,
15309 current_qname: &str,
15310 alias: &str,
15311 ) -> Result<IrExpr, PyQLError> {
15312 let backlink_name = backlink_name.to_string();
15313 let target_qname = format!("{}::{}", target_td.module, target_td.name);
15314 let target_table = target_td.table.clone();
15315 let t_alias = self.fresh_alias();
15316
15317 let join_cond = if let Some(l) = target_td
15326 .links
15327 .iter()
15328 .find(|l| l.name == backlink_name && self.link_target_reaches(&l.target, current_qname))
15329 {
15330 if l.is_junction_backed() {
15331 let (jt_table, jt_module, jt_owner_col, jt_current_col, _) = self.link_junction_info(target_td, l)?;
15335 let jt_alias = self.fresh_alias();
15336 IrExpr::UnaryOp(Box::new(IrUnaryOp {
15337 op: ast::UnaryOpKind::Exists,
15338 operand: IrExpr::Subquery(Box::new(IrSelect::schema_bound(
15339 IrSource {
15340 poly: None,
15341 type_name: format!("{}::__jt__", jt_module),
15342 table: jt_table,
15343 alias: jt_alias.clone(),
15344 },
15345 vec![],
15346 Some(IrExpr::BinOp(Box::new(IrBinOp {
15347 left: IrExpr::BinOp(Box::new(IrBinOp {
15348 left: IrExpr::ColumnRef {
15349 alias: jt_alias.clone(),
15350 column: jt_owner_col,
15351 pg_type: "uuid".to_string(),
15352 },
15353 op: ast::BinOpKind::Eq,
15354 right: IrExpr::ColumnRef {
15355 alias: t_alias.clone(),
15356 column: "id".to_string(),
15357 pg_type: "uuid".to_string(),
15358 },
15359 })),
15360 op: ast::BinOpKind::And,
15361 right: IrExpr::BinOp(Box::new(IrBinOp {
15362 left: IrExpr::ColumnRef {
15363 alias: jt_alias,
15364 column: jt_current_col,
15365 pg_type: "uuid".to_string(),
15366 },
15367 op: ast::BinOpKind::Eq,
15368 right: IrExpr::ColumnRef {
15369 alias: alias.to_string(),
15370 column: "id".to_string(),
15371 pg_type: "uuid".to_string(),
15372 },
15373 })),
15374 }))),
15375 ))),
15376 }))
15377 } else {
15378 let fk_col = format!("{}_id", backlink_name);
15379 IrExpr::BinOp(Box::new(IrBinOp {
15381 left: IrExpr::ColumnRef {
15382 alias: t_alias.clone(),
15383 column: fk_col,
15384 pg_type: "uuid".to_string(),
15385 },
15386 op: ast::BinOpKind::Eq,
15387 right: IrExpr::ColumnRef {
15388 alias: alias.to_string(),
15389 column: "id".to_string(),
15390 pg_type: "uuid".to_string(),
15391 },
15392 }))
15393 }
15394 } else if let Some(ml) = target_td
15395 .multilinks
15396 .iter()
15397 .find(|ml| ml.name == backlink_name && self.link_target_reaches(&ml.target, current_qname))
15398 .cloned()
15399 {
15400 let (jt_table, jt_module, _, _, _) = self.multilink_junction_info(target_td, &ml)?;
15405 let jt_alias = self.fresh_alias();
15406 IrExpr::UnaryOp(Box::new(IrUnaryOp {
15407 op: ast::UnaryOpKind::Exists,
15408 operand: IrExpr::Subquery(Box::new(IrSelect::schema_bound(
15409 IrSource {
15410 poly: None,
15411 type_name: format!("{}::__jt__", jt_module),
15412 table: jt_table,
15413 alias: jt_alias.clone(),
15414 },
15415 vec![],
15416 Some(IrExpr::BinOp(Box::new(IrBinOp {
15417 left: IrExpr::BinOp(Box::new(IrBinOp {
15418 left: IrExpr::ColumnRef {
15419 alias: jt_alias.clone(),
15420 column: "source".to_string(),
15421 pg_type: "uuid".to_string(),
15422 },
15423 op: ast::BinOpKind::Eq,
15424 right: IrExpr::ColumnRef {
15425 alias: t_alias.clone(),
15426 column: "id".to_string(),
15427 pg_type: "uuid".to_string(),
15428 },
15429 })),
15430 op: ast::BinOpKind::And,
15431 right: IrExpr::BinOp(Box::new(IrBinOp {
15432 left: IrExpr::ColumnRef {
15433 alias: jt_alias,
15434 column: "target".to_string(),
15435 pg_type: "uuid".to_string(),
15436 },
15437 op: ast::BinOpKind::Eq,
15438 right: IrExpr::ColumnRef {
15439 alias: alias.to_string(),
15440 column: "id".to_string(),
15441 pg_type: "uuid".to_string(),
15442 },
15443 })),
15444 }))),
15445 ))),
15446 }))
15447 } else {
15448 return Err(PyQLError::Type(PyQLTypeError {
15449 message: format!(
15450 "type {} has no link or multi-link '{}' pointing to {}",
15451 target_qname, backlink_name, current_qname,
15452 ),
15453 position: Position { line: 0, col: 0 },
15454 }));
15455 };
15456
15457 let tail_cond = self.compile_backlink_tail(rest, comparison, &target_qname, &t_alias)?;
15458
15459 let filter = match tail_cond {
15460 Some(tc) => IrExpr::BinOp(Box::new(IrBinOp {
15461 left: join_cond,
15462 op: ast::BinOpKind::And,
15463 right: tc,
15464 })),
15465 None => join_cond,
15466 };
15467
15468 Ok(IrExpr::UnaryOp(Box::new(IrUnaryOp {
15469 op: ast::UnaryOpKind::Exists,
15470 operand: IrExpr::Subquery(Box::new(IrSelect::schema_bound(
15471 IrSource {
15472 poly: None,
15473 type_name: target_qname,
15474 table: target_table,
15475 alias: t_alias,
15476 },
15477 vec![],
15478 Some(filter),
15479 ))),
15480 })))
15481 }
15482
15483 fn compile_backlink_tail(
15485 &mut self,
15486 steps: &[ast::PathStep],
15487 comparison: Option<(ast::BinOpKind, IrExpr, bool)>,
15488 target_qname: &str,
15489 t_alias: &str,
15490 ) -> Result<Option<IrExpr>, PyQLError> {
15491 use ast::PathStep;
15492
15493 if steps.is_empty() {
15498 return Ok(comparison.map(|(op, value, flip)| {
15499 let key = IrExpr::ColumnRef {
15500 alias: t_alias.to_string(),
15501 column: "id".to_string(),
15502 pg_type: "uuid".to_string(),
15503 };
15504 let (left, right) = if flip { (value, key) } else { (key, value) };
15505 IrExpr::BinOp(Box::new(IrBinOp { left, op, right }))
15506 }));
15507 }
15508
15509 if matches!(steps.first(), Some(PathStep::Backlink(_))) {
15511 let inner = self.compile_backlink_as_exists(steps, comparison, target_qname, t_alias)?;
15512 return Ok(Some(inner));
15513 }
15514
15515 let target_td = self.resolve_type(target_qname)?;
15516
15517 if let [PathStep::Name(pointer_name)] = steps {
15519 if let Some(prop) = Self::resolve_property(target_td, pointer_name) {
15520 let col = IrExpr::ColumnRef {
15521 alias: t_alias.to_string(),
15522 column: prop.name.clone(),
15523 pg_type: prop.pg_type.clone(),
15524 };
15525 return Ok(Some(self.apply_comparison(col, comparison)));
15526 }
15527 if let Some(link) = Self::resolve_link(target_td, pointer_name) {
15528 let col = if link.is_junction_backed() {
15529 self.junction_target_id_expr(target_td, link, t_alias)?
15530 } else {
15531 IrExpr::ColumnRef {
15532 alias: t_alias.to_string(),
15533 column: format!("{}_id", link.name),
15534 pg_type: "uuid".to_string(),
15535 }
15536 };
15537 return Ok(Some(self.apply_comparison(col, comparison)));
15538 }
15539 }
15543
15544 if let [PathStep::Name(link_name), PathStep::Name(prop_name)] = steps
15546 && let Some(link) = Self::resolve_link(target_td, link_name)
15547 {
15548 let link_target = link.target.clone();
15549 let target_id_expr = if link.is_junction_backed() {
15550 self.junction_target_id_expr(target_td, link, t_alias)?
15551 } else {
15552 IrExpr::ColumnRef {
15553 alias: t_alias.to_string(),
15554 column: format!("{}_id", link_name),
15555 pg_type: "uuid".to_string(),
15556 }
15557 };
15558 let link_target_td = self.resolve_type(&link_target)?;
15559 let link_target_qname = format!("{}::{}", link_target_td.module, link_target_td.name);
15560 let link_target_table = link_target_td.table.clone();
15561 if let Some(prop) = Self::resolve_property(link_target_td, prop_name) {
15562 let l_alias = self.fresh_alias();
15563 let id_cond = IrExpr::BinOp(Box::new(IrBinOp {
15564 left: IrExpr::ColumnRef {
15565 alias: l_alias.clone(),
15566 column: "id".to_string(),
15567 pg_type: "uuid".to_string(),
15568 },
15569 op: ast::BinOpKind::Eq,
15570 right: target_id_expr,
15571 }));
15572 let col = IrExpr::ColumnRef {
15573 alias: l_alias.clone(),
15574 column: prop.name.clone(),
15575 pg_type: prop.pg_type.clone(),
15576 };
15577 let prop_cond = self.apply_comparison(col, comparison);
15578 let full = IrExpr::BinOp(Box::new(IrBinOp {
15579 left: id_cond,
15580 op: ast::BinOpKind::And,
15581 right: prop_cond,
15582 }));
15583 return Ok(Some(IrExpr::UnaryOp(Box::new(IrUnaryOp {
15584 op: ast::UnaryOpKind::Exists,
15585 operand: IrExpr::Subquery(Box::new(IrSelect::schema_bound(
15586 IrSource {
15587 poly: None,
15588 type_name: link_target_qname,
15589 table: link_target_table,
15590 alias: l_alias,
15591 },
15592 vec![],
15593 Some(full),
15594 ))),
15595 }))));
15596 }
15597 }
15598
15599 let tail = ast::Path {
15604 steps: steps.to_vec(),
15605 partial: true,
15606 };
15607 let walked = self.compile_path(&tail, target_td, t_alias)?;
15608 Ok(Some(self.apply_comparison(walked, comparison)))
15609 }
15610
15611 fn is_set_expr(&self, e: &IrExpr) -> bool {
15617 match e {
15618 IrExpr::ArrayFromSelect(_) => true,
15619 IrExpr::CteRef { name, .. } => self.multi_row_ctes.contains(name),
15620 _ => false,
15621 }
15622 }
15623
15624 fn apply_comparison(&self, col: IrExpr, comparison: Option<(ast::BinOpKind, IrExpr, bool)>) -> IrExpr {
15626 let is_set = |e: &IrExpr| self.is_set_expr(e);
15627 match comparison {
15628 Some((op, val, flip)) => {
15629 let (l, r) = if flip { (val, col) } else { (col, val) };
15630 if matches!(op, ast::BinOpKind::Eq | ast::BinOpKind::Ne) && is_set(&l) != is_set(&r) {
15634 let (value, set) = if is_set(&r) { (l, r) } else { (r, l) };
15635 let membership = IrExpr::BinOp(Box::new(IrBinOp {
15636 left: value,
15637 op: ast::BinOpKind::In,
15638 right: set,
15639 }));
15640 return if matches!(op, ast::BinOpKind::Ne) {
15641 IrExpr::UnaryOp(Box::new(IrUnaryOp {
15642 op: ast::UnaryOpKind::Not,
15643 operand: membership,
15644 }))
15645 } else {
15646 membership
15647 };
15648 }
15649 IrExpr::BinOp(Box::new(IrBinOp { left: l, op, right: r }))
15650 }
15651 None => ir_is_not_null(col),
15652 }
15653 }
15654
15655 fn compile_comparison_value(
15665 &mut self,
15666 value: &Expr,
15667 op: &ast::BinOpKind,
15668 td: &TypeDescriptor,
15669 alias: &str,
15670 ) -> Result<IrExpr, PyQLError> {
15671 match value {
15672 Expr::Set(elems) if matches!(op, ast::BinOpKind::In | ast::BinOpKind::NotIn) => Ok(IrExpr::Array(
15673 elems
15674 .iter()
15675 .map(|e| self.compile_expr(e, td, alias))
15676 .collect::<Result<Vec<_>, _>>()?,
15677 )),
15678 other => self.compile_expr(other, td, alias),
15679 }
15680 }
15681
15682 fn try_elementwise_in(
15693 &mut self,
15694 b: &ast::BinOp,
15695 ctx: Option<(&TypeDescriptor, &str)>,
15696 ) -> Result<Option<IrExpr>, PyQLError> {
15697 let membership = matches!(b.op, ast::BinOpKind::In | ast::BinOpKind::NotIn);
15703 if !membership && !matches!(b.op, ast::BinOpKind::Eq | ast::BinOpKind::Ne) {
15704 return Ok(None);
15705 }
15706 if membership
15718 && (self.value_position || self.explicit_set_depth > 0)
15719 && let Some(root) = self.object_set_name(&b.left)
15720 {
15721 let rewritten = Expr::BinOp(Box::new(ast::BinOp {
15722 left: Expr::Path(ast::Path {
15723 steps: vec![ast::PathStep::Name(root.clone()), ast::PathStep::Name("id".into())],
15724 partial: false,
15725 }),
15726 op: b.op.clone(),
15727 right: b.right.clone(),
15728 }));
15729 let over_the_set = ast::SelectStmt {
15730 result: rewritten.clone(),
15731 filter: None,
15732 order_by: vec![],
15733 offset: None,
15734 limit: None,
15735 lock: None,
15736 };
15737 let ps = self.compile_expr_as_path_select(&over_the_set, &rewritten, &root, false)?;
15738 return Ok(Some(IrExpr::ArrayFromSelect(Box::new(IrArraySource::PathSelect(
15739 Box::new(ps),
15740 )))));
15741 }
15742 if !self.value_position && self.universal_set_depth == 0 {
15747 return Ok(None);
15748 }
15749 let Some((td, _)) = ctx else { return Ok(None) };
15750 let Expr::Path(p) = &b.left else { return Ok(None) };
15754 if !p.partial || !self.path_crosses_multi(td, &p.steps) {
15755 return Ok(None);
15756 }
15757 let hoisted = self.hoisted_ctes.len();
15761 let left = self.compile_expr_ctx(&b.left, ctx)?;
15762 let mut ps = match left {
15767 IrExpr::PathSubquery(ps) | IrExpr::ObjectPathSubquery(ps) => ps,
15768 IrExpr::ArrayFromSelect(source) => match *source {
15769 IrArraySource::PathSelect(ps) => ps,
15770 _ => {
15771 self.hoisted_ctes.truncate(hoisted);
15772 return Ok(None);
15773 }
15774 },
15775 _ => {
15776 self.hoisted_ctes.truncate(hoisted);
15777 return Ok(None);
15778 }
15779 };
15780 let right = match &b.right {
15784 Expr::Set(elems) => IrExpr::Array(
15785 elems
15786 .iter()
15787 .map(|e| self.compile_expr_ctx(e, ctx))
15788 .collect::<Result<Vec<_>, _>>()?,
15789 ),
15790 other => self.compile_expr_ctx(other, ctx)?,
15791 };
15792 if !membership && !self.is_set_expr(&right) {
15796 self.hoisted_ctes.truncate(hoisted);
15797 return Ok(None);
15798 }
15799 let element = match &ps.result {
15802 IrPathResult::Scalar(expr, _) => expr.clone(),
15803 IrPathResult::Object { alias, .. } => IrExpr::ColumnRef {
15804 alias: alias.clone(),
15805 column: "id".to_string(),
15806 pg_type: "uuid".to_string(),
15807 },
15808 };
15809 let per_element = self.apply_comparison(element, Some((b.op.clone(), right, false)));
15812 ps.result = IrPathResult::Scalar(per_element, None);
15813 Ok(Some(IrExpr::ArrayFromSelect(Box::new(IrArraySource::PathSelect(ps)))))
15814 }
15815
15816 fn try_multilink_exists(
15818 &mut self,
15819 b: &ast::BinOp,
15820 td: &TypeDescriptor,
15821 alias: &str,
15822 ) -> Result<Option<IrExpr>, PyQLError> {
15823 fn ml_first_name(steps: &[ast::PathStep]) -> Option<&str> {
15827 match steps.first()? {
15828 ast::PathStep::Name(n) => Some(n.as_str()),
15829 _ => None,
15830 }
15831 }
15832
15833 let (path_steps, value_ast, flip) = if let Expr::Path(p) = &b.left {
15834 if p.partial {
15835 if let Some(ln) = ml_first_name(&p.steps) {
15836 if Self::resolve_multilink(td, ln).is_some() {
15837 (p.steps.as_slice(), &b.right, false)
15838 } else {
15839 return Ok(None);
15840 }
15841 } else {
15842 return Ok(None);
15843 }
15844 } else {
15845 return Ok(None);
15846 }
15847 } else if let Expr::Path(p) = &b.right {
15848 if p.partial {
15849 if let Some(ln) = ml_first_name(&p.steps) {
15850 if Self::resolve_multilink(td, ln).is_some() {
15851 (p.steps.as_slice(), &b.left, true)
15852 } else {
15853 return Ok(None);
15854 }
15855 } else {
15856 return Ok(None);
15857 }
15858 } else {
15859 return Ok(None);
15860 }
15861 } else {
15862 return Ok(None);
15863 };
15864
15865 let value_expr = self.compile_comparison_value(value_ast, &b.op, td, alias)?;
15866 let ml_name = match &path_steps[0] {
15867 ast::PathStep::Name(n) => n.clone(),
15868 _ => return Ok(None),
15869 };
15870 let ml = Self::resolve_multilink(td, &ml_name).unwrap();
15871
15872 if self.explicit_set_depth == 0 {
15875 let mut pointer_path = String::new();
15878 for step in path_steps {
15879 match step {
15880 ast::PathStep::Name(n) => {
15881 pointer_path.push('.');
15882 pointer_path.push_str(n);
15883 }
15884 ast::PathStep::Backlink(n) => {
15885 pointer_path.push_str(".<");
15886 pointer_path.push_str(n);
15887 }
15888 ast::PathStep::LinkProp(n) => {
15889 pointer_path.push('@');
15890 pointer_path.push_str(n);
15891 }
15892 ast::PathStep::TypeIntersection(t) => {
15893 pointer_path.push_str("[is ");
15894 pointer_path.push_str(&t.qualified_name());
15895 pointer_path.push(']');
15896 }
15897 }
15898 }
15899 self.warnings.push(format!(
15900 "possibly more than one element returned by an expression in a FILTER clause \
15901 (multi-link '{pointer_path}'); wrap with any() to make intent explicit",
15902 ));
15903 }
15904
15905 let ml_target = ml.target.clone();
15907 let ml_through = ml.through.clone();
15908 let td_module = td.module.clone();
15909 let td_name = td.name.clone();
15910 let td_table = td.table.clone();
15911
15912 let tail_steps: Vec<ast::PathStep> = path_steps[1..].to_vec();
15914
15915 let jt_alias = self.fresh_alias();
15916
15917 let (jt_table, jt_module, jt_src_col, jt_tgt_col) = if let Some(through_qname) = &ml_through {
15919 let through_td = self.resolve_type(through_qname)?;
15920 if through_td.junction {
15921 (
15924 format!("{}.{}", td_table, ml_name),
15925 td_module.clone(),
15926 "source".to_string(),
15927 "target".to_string(),
15928 )
15929 } else {
15930 let source_qname = format!("{}::{}", td_module, td_name);
15931 let src_col = through_td
15932 .links
15933 .iter()
15934 .find(|l| l.target == source_qname)
15935 .ok_or_else(|| {
15936 PyQLError::Type(PyQLTypeError {
15937 message: format!("through type {through_qname} has no link to {source_qname}"),
15938 position: Position { line: 0, col: 0 },
15939 })
15940 })?
15941 .name
15942 .clone();
15943 let tgt_col = through_td
15944 .links
15945 .iter()
15946 .find(|l| l.target == ml_target && l.name != src_col)
15947 .or_else(|| through_td.links.iter().find(|l| l.target == ml_target))
15948 .ok_or_else(|| {
15949 PyQLError::Type(PyQLTypeError {
15950 message: format!("through type {through_qname} has no link to {ml_target}"),
15951 position: Position { line: 0, col: 0 },
15952 })
15953 })?
15954 .name
15955 .clone();
15956 (
15957 through_td.table.clone(),
15958 through_td.module.clone(),
15959 format!("{}_id", src_col),
15960 format!("{}_id", tgt_col),
15961 )
15962 }
15963 } else {
15964 (
15965 format!("{}.{}", td_table, ml_name),
15966 td_module.clone(),
15967 "source".to_string(),
15968 "target".to_string(),
15969 )
15970 };
15971
15972 let src_filter = IrExpr::BinOp(Box::new(IrBinOp {
15974 left: IrExpr::ColumnRef {
15975 alias: jt_alias.clone(),
15976 column: jt_src_col,
15977 pg_type: "uuid".to_string(),
15978 },
15979 op: ast::BinOpKind::Eq,
15980 right: IrExpr::ColumnRef {
15981 alias: alias.to_string(),
15982 column: "id".to_string(),
15983 pg_type: "uuid".to_string(),
15984 },
15985 }));
15986
15987 let tail_filter = self.compile_path_tail_filter(
15989 &tail_steps,
15990 b.op.clone(),
15991 value_expr,
15992 flip,
15993 &ml_target,
15994 &jt_alias,
15995 &jt_tgt_col,
15996 )?;
15997
15998 let full_filter = IrExpr::BinOp(Box::new(IrBinOp {
15999 left: src_filter,
16000 op: ast::BinOpKind::And,
16001 right: tail_filter,
16002 }));
16003
16004 let jt_source = IrSource {
16006 poly: None,
16007 type_name: format!("{}::__jt__", jt_module),
16008 table: jt_table,
16009 alias: jt_alias,
16010 };
16011 let inner = IrExpr::Subquery(Box::new(IrSelect::schema_bound(jt_source, vec![], Some(full_filter))));
16012
16013 Ok(Some(IrExpr::UnaryOp(Box::new(IrUnaryOp {
16014 op: ast::UnaryOpKind::Exists,
16015 operand: inner,
16016 }))))
16017 }
16018
16019 #[allow(clippy::too_many_arguments)]
16026 fn compile_path_tail_filter(
16027 &mut self,
16028 steps: &[ast::PathStep],
16029 op: ast::BinOpKind,
16030 value_expr: IrExpr,
16031 flip: bool,
16032 target_type: &str,
16033 jt_alias: &str,
16034 jt_tgt_col: &str,
16035 ) -> Result<IrExpr, PyQLError> {
16036 if steps.is_empty() {
16039 let col_ref = IrExpr::ColumnRef {
16040 alias: jt_alias.to_string(),
16041 column: jt_tgt_col.to_string(),
16042 pg_type: "uuid".to_string(),
16043 };
16044 return Ok(self.apply_comparison(col_ref, Some((op, value_expr, flip))));
16049 }
16050
16051 if let Some(ast::PathStep::TypeIntersection(type_ref)) = steps.first() {
16057 let narrowed = self.resolve_type(&type_ref.qualified_name())?;
16058 let narrowed_qname = format!("{}::{}", narrowed.module, narrowed.name);
16059 return self.compile_path_tail_filter(
16060 &steps[1..],
16061 op,
16062 value_expr,
16063 flip,
16064 &narrowed_qname,
16065 jt_alias,
16066 jt_tgt_col,
16067 );
16068 }
16069
16070 let first_name = match steps.first() {
16071 Some(ast::PathStep::Name(n)) => n.clone(),
16072 _ => return Err(self.type_err("expected a property or link name in path")),
16073 };
16074
16075 let target_td = self.resolve_type(target_type)?;
16076 let target_table = target_td.table.clone();
16077
16078 if steps.len() == 1 {
16079 if first_name == "id" {
16081 let col_ref = IrExpr::ColumnRef {
16083 alias: jt_alias.to_string(),
16084 column: jt_tgt_col.to_string(),
16085 pg_type: "uuid".to_string(),
16086 };
16087 let (l, r) = if flip {
16088 (value_expr, col_ref)
16089 } else {
16090 (col_ref, value_expr)
16091 };
16092 return Ok(IrExpr::BinOp(Box::new(IrBinOp { left: l, op, right: r })));
16093 }
16094 let single_link = target_td
16100 .links
16101 .iter()
16102 .find(|l| l.name == first_name && !l.is_junction_backed());
16103 if single_link.is_none() && target_td.multilinks.iter().any(|l| l.name == first_name) {
16104 let target_display = target_type.replace("::", ".");
16105 return Err(PyQLError::Type(PyQLTypeError {
16106 message: format!(
16107 "operator '{op}' cannot be applied to operands of type '{target_display}' and the value type",
16108 op = op,
16109 ),
16110 position: Position { line: 0, col: 0 },
16111 }));
16112 }
16113 let (prop_name, prop_pg) = match single_link {
16114 Some(link) => (format!("{}_id", link.name), "uuid".to_string()),
16115 None => {
16116 let prop = target_td
16117 .properties
16118 .iter()
16119 .find(|p| p.name == first_name)
16120 .ok_or_else(|| self.field_err(&first_name, target_type))?;
16121 (prop.name.clone(), prop.pg_type.clone())
16122 }
16123 };
16124 let tgt_alias = self.fresh_alias();
16125 let id_filter = IrExpr::BinOp(Box::new(IrBinOp {
16127 left: IrExpr::ColumnRef {
16128 alias: tgt_alias.clone(),
16129 column: "id".to_string(),
16130 pg_type: "uuid".to_string(),
16131 },
16132 op: ast::BinOpKind::Eq,
16133 right: IrExpr::ColumnRef {
16134 alias: jt_alias.to_string(),
16135 column: jt_tgt_col.to_string(),
16136 pg_type: "uuid".to_string(),
16137 },
16138 }));
16139 let prop_col = IrExpr::ColumnRef {
16140 alias: tgt_alias.clone(),
16141 column: prop_name,
16142 pg_type: prop_pg,
16143 };
16144 let (pl, pr) = if flip {
16145 (value_expr, prop_col)
16146 } else {
16147 (prop_col, value_expr)
16148 };
16149 let prop_filter = IrExpr::BinOp(Box::new(IrBinOp {
16150 left: pl,
16151 op,
16152 right: pr,
16153 }));
16154 let full = IrExpr::BinOp(Box::new(IrBinOp {
16155 left: id_filter,
16156 op: ast::BinOpKind::And,
16157 right: prop_filter,
16158 }));
16159 let inner = IrExpr::Subquery(Box::new(IrSelect::schema_bound(
16160 IrSource {
16161 poly: None,
16162 type_name: target_type.to_string(),
16163 table: target_table,
16164 alias: tgt_alias,
16165 },
16166 vec![],
16167 Some(full),
16168 )));
16169 return Ok(IrExpr::UnaryOp(Box::new(IrUnaryOp {
16170 op: ast::UnaryOpKind::Exists,
16171 operand: inner,
16172 })));
16173 }
16174
16175 if target_td.multilinks.iter().any(|l| l.name == first_name) {
16177 return Err(self.type_err("nested multi-link traversal in comparison is not yet supported"));
16178 }
16179 let link = target_td
16180 .links
16181 .iter()
16182 .find(|l| l.name == first_name)
16183 .ok_or_else(|| self.field_err(&first_name, target_type))?;
16184 if link.is_junction_backed() {
16185 return Err(self.type_err(&format!(
16192 "filtering through a junction-backed single link ('{first_name}') nested inside \
16193 a multi-link path comparison is not yet supported — filter on '.{first_name}' \
16194 directly instead"
16195 )));
16196 }
16197 let next_target = link.target.clone();
16198 let fk_col = format!("{}_id", first_name);
16199 let tgt_alias = self.fresh_alias();
16200
16201 if steps.len() == 2
16203 && let Some(ast::PathStep::Name(n)) = steps.get(1)
16204 && n == "id"
16205 {
16206 let id_filter = IrExpr::BinOp(Box::new(IrBinOp {
16210 left: IrExpr::ColumnRef {
16211 alias: tgt_alias.clone(),
16212 column: "id".to_string(),
16213 pg_type: "uuid".to_string(),
16214 },
16215 op: ast::BinOpKind::Eq,
16216 right: IrExpr::ColumnRef {
16217 alias: jt_alias.to_string(),
16218 column: jt_tgt_col.to_string(),
16219 pg_type: "uuid".to_string(),
16220 },
16221 }));
16222 let fk_ref = IrExpr::ColumnRef {
16223 alias: tgt_alias.clone(),
16224 column: fk_col,
16225 pg_type: "uuid".to_string(),
16226 };
16227 let (fl, fr) = if flip {
16228 (value_expr, fk_ref)
16229 } else {
16230 (fk_ref, value_expr)
16231 };
16232 let fk_filter = IrExpr::BinOp(Box::new(IrBinOp {
16233 left: fl,
16234 op,
16235 right: fr,
16236 }));
16237 let full = IrExpr::BinOp(Box::new(IrBinOp {
16238 left: id_filter,
16239 op: ast::BinOpKind::And,
16240 right: fk_filter,
16241 }));
16242 let inner = IrExpr::Subquery(Box::new(IrSelect::schema_bound(
16243 IrSource {
16244 poly: None,
16245 type_name: target_type.to_string(),
16246 table: target_table,
16247 alias: tgt_alias,
16248 },
16249 vec![],
16250 Some(full),
16251 )));
16252 return Ok(IrExpr::UnaryOp(Box::new(IrUnaryOp {
16253 op: ast::UnaryOpKind::Exists,
16254 operand: inner,
16255 })));
16256 }
16257
16258 let id_filter = IrExpr::BinOp(Box::new(IrBinOp {
16261 left: IrExpr::ColumnRef {
16262 alias: tgt_alias.clone(),
16263 column: "id".to_string(),
16264 pg_type: "uuid".to_string(),
16265 },
16266 op: ast::BinOpKind::Eq,
16267 right: IrExpr::ColumnRef {
16268 alias: jt_alias.to_string(),
16269 column: jt_tgt_col.to_string(),
16270 pg_type: "uuid".to_string(),
16271 },
16272 }));
16273 let nested_filter =
16274 self.compile_path_tail_filter(&steps[1..], op, value_expr, flip, &next_target, &tgt_alias, &fk_col)?;
16275 let full = IrExpr::BinOp(Box::new(IrBinOp {
16276 left: id_filter,
16277 op: ast::BinOpKind::And,
16278 right: nested_filter,
16279 }));
16280 let inner = IrExpr::Subquery(Box::new(IrSelect::schema_bound(
16281 IrSource {
16282 poly: None,
16283 type_name: target_type.to_string(),
16284 table: target_table,
16285 alias: tgt_alias,
16286 },
16287 vec![],
16288 Some(full),
16289 )));
16290 Ok(IrExpr::UnaryOp(Box::new(IrUnaryOp {
16291 op: ast::UnaryOpKind::Exists,
16292 operand: inner,
16293 })))
16294 }
16295}
16296
16297#[derive(Clone)]
16301struct JunctionReadOverride {
16302 junction: String,
16303 targets: Option<String>,
16304}
16305
16306struct ConflictElse {
16310 sets: Vec<(String, IrExpr)>,
16311 predicate: Option<IrExpr>,
16312}
16313
16314impl<'a> Compiler<'a> {
16315 fn compile_conflict(
16317 &mut self,
16318 uc: &ast::UnlessConflict,
16319 td: &TypeDescriptor,
16320 ) -> Result<(IrConflict, Vec<IrMultiLinkMutation>), PyQLError> {
16321 let on = uc.on.as_ref().map(|e| self.compile_expr(e, td, "")).transpose()?;
16324 let mut appends = vec![];
16325 let mut do_update_where = None;
16326 let do_update = match uc.else_.as_ref() {
16327 Some(e) => {
16328 let resolved = self.compile_conflict_else(e, &mut appends)?;
16329 do_update_where = resolved.predicate;
16330 Some(resolved.sets)
16331 }
16332 None => None,
16333 };
16334 Ok((
16335 IrConflict {
16336 on,
16337 do_update,
16338 do_update_where,
16339 },
16340 appends,
16341 ))
16342 }
16343
16344 fn compile_conflict_else(
16359 &mut self,
16360 expr: &Expr,
16361 appends: &mut Vec<IrMultiLinkMutation>,
16362 ) -> Result<ConflictElse, PyQLError> {
16363 let Expr::SubQuery(stmt) = expr else {
16364 return Err(self.type_err("UNLESS CONFLICT ELSE must be an UPDATE expression, e.g. ELSE (UPDATE …)"));
16365 };
16366 if let Stmt::Select(sel) = stmt.as_ref()
16371 && sel.filter.is_none()
16372 && sel.limit.is_none()
16373 && sel.offset.is_none()
16374 && let Ok(type_name) = self.expr_as_type_name(&sel.result)
16375 && let Ok(sel_td) = self.resolve_type(&type_name)
16376 {
16377 let table = sel_td.table.clone();
16378 let pk = sel_td
16379 .properties
16380 .iter()
16381 .find(|p| p.is_pk)
16382 .ok_or_else(|| self.type_err(&format!("type '{type_name}' has no primary key to read back")))?;
16383 return Ok(ConflictElse {
16384 sets: vec![(
16385 pk.name.clone(),
16386 IrExpr::ColumnRef {
16387 alias: table,
16388 column: pk.name.clone(),
16389 pg_type: pk.pg_type.clone(),
16390 },
16391 )],
16392 predicate: None,
16393 });
16394 }
16395 let Stmt::Update(upd) = stmt.as_ref() else {
16396 return Err(self.type_err(
16397 "UNLESS CONFLICT ELSE must be an UPDATE that changes the conflicting row, or a \
16398 SELECT of its type to read it back unchanged",
16399 ));
16400 };
16401 let type_name = self.expr_as_type_name(&upd.subject)?;
16402 let upd_td = self.resolve_type(&type_name)?;
16403 let table = upd_td.table.clone();
16404 let do_update_where = upd
16412 .filter
16413 .as_ref()
16414 .map(|f| self.as_condition(|c| c.compile_expr(f, upd_td, &table)))
16415 .transpose()?;
16416 let mut scalar_shape = vec![];
16423 for el in &upd.shape {
16424 let pointer_name = path_leaf(&el.path)?;
16425 let Some(ml) = Self::resolve_multilink(upd_td, pointer_name) else {
16426 scalar_shape.push(el.clone());
16427 continue;
16428 };
16429 if el.op == ShapeOp::Remove {
16430 return Err(self.type_err(&format!(
16431 "cannot use `-=` for multi-link '{pointer_name}' inside an UNLESS CONFLICT \
16432 ELSE clause; the rows to remove are not known until the conflict resolves"
16433 )));
16434 }
16435 let Some(value) = &el.compexpr else { continue };
16436 let (jt, module, src_col, tgt_col, through_td) = self.own_multilink_junction_info(upd_td, ml)?;
16437 let values = self.compile_multilink_values(value, upd_td, &table, through_td)?;
16438 appends.push(IrMultiLinkMutation {
16439 junction_table: jt,
16440 module,
16441 source_col: src_col,
16442 target_col: tgt_col,
16443 values,
16444 single: false,
16445 });
16446 }
16447 let sets = self.compile_assignments_for_update(&scalar_shape, upd_td, &table)?;
16448 Ok(ConflictElse {
16449 sets,
16450 predicate: do_update_where,
16451 })
16452 }
16453
16454 fn hoist_dml_as_cte(&mut self, stmt: &Stmt) -> Result<(String, String), PyQLError> {
16464 let type_name = self.dml_subject_type(stmt)?;
16465 let before = std::mem::take(&mut self.for_vars_read);
16466 let inner = self.compile_stmt(stmt);
16467 let read = std::mem::replace(&mut self.for_vars_read, before);
16468 let correlated_to = self
16469 .for_scope
16470 .iter()
16471 .rev()
16472 .find(|slot| read.contains(*slot))
16473 .map(|slot| format!("_for_{slot}"));
16474 self.for_vars_read.extend(read);
16475 let cte_name = self.fresh_nested_cte_name();
16476 if let Some(slot) = correlated_to.as_deref().and_then(|i| i.strip_prefix("_for_")) {
16477 self.correlated_bindings.insert(cte_name.clone(), slot.to_string());
16478 }
16479 self.hoisted_ctes.push(IrCteDef {
16480 name: cte_name.clone(),
16481 stmt: inner?,
16482 type_name: type_name.clone(),
16483 correlated_to,
16484 });
16485 Ok((cte_name, type_name))
16486 }
16487
16488 fn for_body_mutates(stmt: &Stmt) -> bool {
16491 match stmt {
16492 Stmt::Insert(_) | Stmt::Update(_) | Stmt::Delete(_) => true,
16493 Stmt::For(inner) => Self::for_body_mutates(&inner.body),
16494 _ => false,
16495 }
16496 }
16497
16498 fn bind_mutating_loop_source(&mut self, result_expr: &Expr) -> Result<Option<Expr>, PyQLError> {
16509 let (subject, shape) = match result_expr {
16510 Expr::SubQuery(stmt) => (stmt.as_ref(), None),
16511 Expr::Shape(sh) => match sh.expr.as_ref() {
16512 Some(Expr::SubQuery(stmt)) => (stmt.as_ref(), Some(sh.as_ref())),
16513 _ => return Ok(None),
16514 },
16515 _ => return Ok(None),
16516 };
16517 if !matches!(subject, Stmt::For(f) if Self::for_body_mutates(&f.body)) {
16518 return Ok(None);
16519 }
16520 let before = std::mem::take(&mut self.for_vars_read);
16524 let inner = self.compile_stmt(subject);
16525 let read = std::mem::replace(&mut self.for_vars_read, before);
16526 let correlated_to = self
16527 .for_scope
16528 .iter()
16529 .rev()
16530 .find(|slot| read.contains(*slot))
16531 .map(|slot| format!("_for_{slot}"));
16532 self.for_vars_read.extend(read);
16533 let inner = inner?;
16534 let cte_name = self.fresh_nested_cte_name();
16535 if let Some(slot) = correlated_to.as_deref().and_then(|i| i.strip_prefix("_for_")) {
16536 self.correlated_bindings.insert(cte_name.clone(), slot.to_string());
16537 }
16538 let type_name = self.register_cte(&cte_name, &inner);
16539 self.hoisted_ctes.push(IrCteDef {
16540 name: cte_name.clone(),
16541 stmt: inner,
16542 type_name,
16543 correlated_to,
16544 });
16545 let bound = Expr::Path(ast::Path {
16546 steps: vec![ast::PathStep::Name(cte_name)],
16547 partial: false,
16548 });
16549 Ok(Some(match shape {
16550 Some(sh) => Expr::Shape(Box::new(ast::ShapeExpr {
16551 expr: Some(bound),
16552 elements: sh.elements.clone(),
16553 marker_offset: sh.marker_offset,
16554 })),
16555 None => bound,
16556 }))
16557 }
16558
16559 fn hoist_nested_dml(&mut self, stmt: &Stmt) -> Result<String, PyQLError> {
16560 let type_name = self.dml_subject_type(stmt)?;
16561 let inner = self.compile_stmt(stmt)?;
16562 let cte_name = self.fresh_nested_cte_name();
16563 self.pending_nested_ctes.push(IrCteDef {
16564 name: cte_name.clone(),
16565 stmt: inner,
16566 type_name,
16567 correlated_to: None,
16568 });
16569 Ok(cte_name)
16570 }
16571
16572 fn compile_link_subquery(&mut self, stmt: &Stmt) -> Result<IrExpr, PyQLError> {
16577 if matches!(stmt, Stmt::Insert(_) | Stmt::Update(_) | Stmt::Delete(_)) {
16581 let cte_name = self.hoist_nested_dml(stmt)?;
16582 return Ok(IrExpr::ColumnRef {
16583 alias: cte_name,
16584 column: "id".to_string(),
16585 pg_type: "uuid".to_string(),
16586 });
16587 }
16588 let Stmt::Select(sel) = stmt else {
16589 return Err(self.type_err(
16590 "only SELECT is valid as a link assignment value; \
16591 use SELECT (INSERT …) { id } to assign from a DML result",
16592 ));
16593 };
16594
16595 let nested_dml = match &sel.result {
16617 Expr::SubQuery(inner) if matches!(inner.as_ref(), Stmt::Insert(_) | Stmt::Update(_) | Stmt::Delete(_)) => {
16618 Some(inner.as_ref())
16619 }
16620 Expr::Shape(s) => match s.expr.as_ref() {
16621 Some(Expr::SubQuery(inner))
16622 if matches!(inner.as_ref(), Stmt::Insert(_) | Stmt::Update(_) | Stmt::Delete(_)) =>
16623 {
16624 Some(inner.as_ref())
16625 }
16626 _ => None,
16627 },
16628 _ => None,
16629 };
16630 if let Some(inner_stmt) = nested_dml {
16631 let cte_name = self.hoist_nested_dml(inner_stmt)?;
16632 return Ok(IrExpr::ColumnRef {
16633 alias: cte_name,
16634 column: "id".to_string(),
16635 pg_type: "uuid".to_string(),
16636 });
16637 }
16638
16639 let type_name = self.expr_as_type_name(&sel.result)?;
16640 let td = self.resolve_type(&type_name)?;
16641 let alias = self.fresh_alias();
16642
16643 let (filter, order_by, offset, limit) = self.compile_path_modifiers(sel, td, &alias)?;
16646 let mut select = IrSelect::schema_bound(
16647 IrSource {
16648 poly: None,
16649 type_name: format!("{}::{}", td.module, td.name),
16650 table: td.table.clone(),
16651 alias,
16652 },
16653 Self::pk_returning(td),
16654 filter,
16655 );
16656 select.order_by = order_by;
16657 select.offset = offset;
16658 select.limit = limit;
16659 Ok(IrExpr::Subquery(Box::new(select)))
16660 }
16661
16662 fn compile_sort_ctx(
16666 &mut self,
16667 s: &ast::SortExpr,
16668 ctx: Option<(&TypeDescriptor, &str)>,
16669 ) -> Result<IrSort, PyQLError> {
16670 Ok(IrSort {
16671 expr: self.compile_expr_ctx(&s.expr, ctx)?,
16672 direction: match s.direction {
16673 SortDirection::Asc => IrSortDir::Asc,
16674 SortDirection::Desc => IrSortDir::Desc,
16675 },
16676 nulls: match s.nones {
16677 NonesOrder::First => IrNulls::First,
16678 NonesOrder::Last => IrNulls::Last,
16679 },
16680 })
16681 }
16682
16683 fn compile_sort(&mut self, s: &ast::SortExpr, td: &TypeDescriptor, alias: &str) -> Result<IrSort, PyQLError> {
16684 self.compile_sort_ctx(s, Some((td, alias)))
16685 }
16686
16687 fn assert_message(
16695 &mut self,
16696 f: &ast::FunctionCall,
16697 ctx: Option<(&TypeDescriptor, &str)>,
16698 ) -> Result<Option<IrExpr>, PyQLError> {
16699 let mut message = None;
16700 for (name, value) in &f.kwargs {
16701 if name != "message" {
16702 return Err(self.type_err(&format!("function 'std::{}' has no parameter '{name}'", f.name)));
16703 }
16704 message = Some(self.compile_expr_ctx(value, ctx)?);
16705 }
16706 Ok(message)
16707 }
16708
16709 fn compile_named_call_args(
16715 &mut self,
16716 f: &ast::FunctionCall,
16717 ctx: Option<(&TypeDescriptor, &str)>,
16718 ) -> Result<Option<Vec<IrExpr>>, PyQLError> {
16719 use crate::stdlib::NamedDefault;
16720
16721 let ns = f.module.as_deref().unwrap_or("std");
16722 let overloads = crate::stdlib::lookup(ns, &f.name);
16723 let positional = |d: &crate::stdlib::FnDescriptor| d.params.iter().filter(|p| p.named_only.is_none()).count();
16724 let named = |d: &crate::stdlib::FnDescriptor| d.params.iter().filter(|p| p.named_only.is_some()).count();
16725 let takes_positionally = |d: &crate::stdlib::FnDescriptor| {
16728 if d.is_variadic() {
16729 f.args.len() + 1 >= positional(d)
16730 } else {
16731 positional(d) == f.args.len()
16732 }
16733 };
16734 if f.kwargs.is_empty() {
16735 if overloads.iter().any(|d| takes_positionally(d) && named(d) == 0) {
16736 return Ok(None);
16737 }
16738 let wrote_a_named_one_positionally =
16743 |d: &crate::stdlib::FnDescriptor| positional(d) < f.args.len() && f.args.len() <= d.params.len();
16744 if !overloads.iter().any(|d| takes_positionally(d))
16745 && let Some(param) = overloads
16746 .iter()
16747 .filter(|d| wrote_a_named_one_positionally(d))
16748 .flat_map(|d| d.params.iter())
16749 .find(|p| p.named_only.is_some())
16750 .map(|p| p.keyword())
16751 {
16752 return Err(self.type_err(&format!(
16753 "function '{ns}::{}' takes '{param}' as a named argument only — e.g. '{param} := …'",
16754 f.name
16755 )));
16756 }
16757 }
16758 let fits = |d: &&&crate::stdlib::FnDescriptor| {
16759 takes_positionally(d)
16760 && named(d) > 0
16761 && f.kwargs
16762 .iter()
16763 .all(|(name, _)| d.params.iter().any(|p| p.keyword() == name && p.named_only.is_some()))
16764 };
16765 let Some(desc) = overloads.iter().find(fits) else {
16766 if let Some((name, _)) = f.kwargs.first() {
16767 let message = if overloads.iter().all(|d| named(d) == 0) {
16768 format!(
16769 "function '{ns}::{}' does not take named arguments, got '{name}'",
16770 f.name
16771 )
16772 } else {
16773 format!("function '{ns}::{}' has no parameter '{name}'", f.name)
16774 };
16775 return Err(self.type_err(&message));
16776 }
16777 return Ok(None);
16778 };
16779 let mut args = f
16780 .args
16781 .iter()
16782 .map(|a| self.compile_expr_ctx(a, ctx))
16783 .collect::<Result<Vec<_>, _>>()?;
16784 for param in desc.params.iter().filter(|p| p.named_only.is_some()) {
16785 let arg = match (
16786 f.kwargs.iter().find(|(name, _)| name == param.keyword()),
16787 param.named_only,
16788 ) {
16789 (Some((_, value)), _) => self.compile_expr_ctx(value, ctx)?,
16790 (None, Some(NamedDefault::Required)) => {
16791 let keyword = param.keyword();
16792 return Err(self.type_err(&format!(
16793 "function '{ns}::{}' requires the named argument '{keyword}' — e.g. '{keyword} := …'",
16794 f.name
16795 )));
16796 }
16797 (None, Some(NamedDefault::Int(n))) => IrExpr::Literal(IrLiteral::Int(n)),
16798 (None, Some(NamedDefault::Bool(b))) => IrExpr::Literal(IrLiteral::Bool(b)),
16799 (None, Some(NamedDefault::Str(s))) => IrExpr::Literal(IrLiteral::Str(s.to_string())),
16800 (None, _) => IrExpr::Null,
16801 };
16802 args.push(arg);
16803 }
16804 Ok(Some(args))
16805 }
16806
16807 fn resolve_fn_call(&mut self, module: Option<&str>, name: &str, args: Vec<IrExpr>) -> Result<IrExpr, PyQLError> {
16808 use crate::stdlib::{ImplStrategy, lookup};
16809
16810 let ns = module.unwrap_or("std");
16811 if ns == "std" && matches!(name, "any" | "all") && args.len() == 1 && !is_array_expr(&args[0]) {
16816 return Ok(args.into_iter().next().expect("checked by the guard"));
16817 }
16818 let overloads = lookup(ns, name);
16819
16820 let by_arity: Vec<&crate::stdlib::FnDescriptor> = overloads
16828 .iter()
16829 .copied()
16830 .filter(|d| {
16831 if d.is_variadic() {
16832 args.len() + 1 >= d.params.len()
16833 } else {
16834 d.params.len() == args.len()
16835 }
16836 })
16837 .collect();
16838
16839 let exact = by_arity.iter().copied().find(|d| {
16845 params_for_args(d, args.len())
16846 .zip(&args)
16847 .any(|(p, a)| p.ty.scalar_pg_type().is_some() && infer_ir_type(a).is_some())
16848 && params_for_args(d, args.len()).zip(&args).all(|(p, a)| {
16849 match (p.ty.scalar_pg_type(), infer_ir_type(a)) {
16850 (Some(declared), Some(known)) => declared == known,
16851 _ => pylon_type_matches(a, &p.ty),
16852 }
16853 })
16854 });
16855 let best = exact.or_else(|| {
16856 by_arity.iter().copied().find(|d| {
16857 params_for_args(d, args.len())
16858 .zip(&args)
16859 .all(|(p, a)| pylon_type_matches(a, &p.ty))
16860 })
16861 });
16862
16863 let args_len = args.len();
16868 let args: Vec<IrExpr> = match best {
16869 Some(descriptor) => args
16870 .into_iter()
16871 .enumerate()
16872 .map(
16873 |(i, arg)| match params_for_args(descriptor, args_len).nth(i).map(|p| &p.ty) {
16874 Some(crate::stdlib::PylonType::Set(_)) | Some(crate::stdlib::PylonType::Array(_)) | None => arg,
16875 Some(_) => set_walk_as_scalar(arg),
16876 },
16877 )
16878 .collect(),
16879 None => args,
16880 };
16881 let args = match best {
16882 Some(descriptor) => pack_variadic_args(descriptor, args),
16883 None => args,
16884 };
16885
16886 let stdlib_return = best
16890 .map(|d| match &d.return_type {
16891 crate::stdlib::PylonType::Optional(inner) => inner.as_ref(),
16892 other => other,
16893 })
16894 .and_then(|ty| ty.scalar_pg_type())
16895 .map(str::to_string);
16896 let (schema, resolved_name, sql_template) = if let Some(desc) = best {
16897 match &desc.impl_strategy {
16898 ImplStrategy::SqlBuiltin(sql_name) => (None, sql_name.to_string(), None),
16899 ImplStrategy::SqlExpression(tmpl) => (None, name.to_string(), Some(tmpl.to_string())),
16900 ImplStrategy::PylonFunction(def) => (Some("_pylon".to_string()), def.name.to_string(), None),
16901 ImplStrategy::SqlOperator(op) if args.len() == 2 => {
16908 (None, name.to_string(), Some(format!("($1 {op} $2)")))
16909 }
16910 ImplStrategy::TranspilerIntrinsic(intrinsic) => {
16911 return self.compile_range_intrinsic(intrinsic, name, args);
16912 }
16913 _ => (module.map(str::to_string), name.to_string(), None),
16915 }
16916 } else {
16917 let candidates: Vec<&FunctionDescriptor> = self
16933 .schema
16934 .functions
16935 .iter()
16936 .filter(|f| {
16937 let module_matches = module.map(|m| m == f.module.as_str()).unwrap_or(true);
16938 module_matches && f.name == name && !f.return_is_object
16939 })
16940 .collect();
16941 let user_fn = candidates
16942 .iter()
16943 .find(|f| f.params.len() == args.len())
16944 .copied()
16945 .or_else(|| candidates.first().copied());
16946 if let Some(fd) = user_fn {
16947 if fd.params.len() != args.len() {
16948 return Err(self.type_err(&format!(
16949 "function '{}::{}' expects {} argument(s), got {}",
16950 fd.module,
16951 fd.name,
16952 fd.params.len(),
16953 args.len()
16954 )));
16955 }
16956 let cast_args = fd
16957 .params
16958 .iter()
16959 .zip(args)
16960 .map(|(p, a)| {
16961 IrExpr::TypeCast(Box::new(super::IrTypeCast {
16962 expr: a,
16963 pg_type: p.pg_type.clone(),
16964 tuple_shape: None,
16965 }))
16966 })
16967 .collect();
16968 let qualified = format!("{}::{}", fd.module, fd.name);
16969 let fn_module = fd.module.clone();
16970 let fn_name = fd.name.clone();
16971 let mut call_args: Vec<IrExpr> = cast_args;
16972 if let Some(globals) = self.globals_arg_for_call(&qualified)? {
16973 call_args.insert(0, globals);
16974 }
16975 let return_pg_type = (!fd.return_is_set).then(|| fd.return_pg_type.clone());
16980 return Ok(IrExpr::FunctionCall(super::IrFunctionCall {
16981 return_pg_type,
16982 schema: Some(fn_module),
16983 name: fn_name,
16984 args: call_args,
16985 sql_template: None,
16986 }));
16987 }
16988 if let Some(fd) = self.schema.functions.iter().find(|f| {
16995 let module_matches = module.map(|m| m == f.module.as_str()).unwrap_or(true);
16996 module_matches && f.name == name && f.return_is_object
16997 }) {
16998 return Err(self.type_err(&format!(
16999 "function '{}::{}' returns objects, so it can only be the subject of a \
17000 select (`select {}::{}(…) {{ … }}`), not part of a larger expression",
17001 fd.module, fd.name, fd.module, fd.name
17002 )));
17003 }
17004 let qualified = match module {
17005 Some(m) => format!("{m}::{name}"),
17006 None => name.to_string(),
17007 };
17008 if !overloads.is_empty() {
17013 return Err(self.type_err(&if by_arity.is_empty() {
17014 format!(
17015 "function '{qualified}' takes {}, got {}",
17016 describe_arities(&overloads),
17017 args.len()
17018 )
17019 } else {
17020 format!(
17021 "function '{qualified}' has no overload accepting ({}) — it accepts {}",
17022 describe_args(&args),
17023 describe_signatures(&by_arity),
17024 )
17025 }));
17026 }
17027 return Err(self.type_err(&format!(
17028 "function '{qualified}' does not exist{}",
17029 self.suggest_function_name(ns, name)
17030 )));
17031 };
17032
17033 Ok(IrExpr::FunctionCall(super::IrFunctionCall {
17034 return_pg_type: stdlib_return,
17035 schema,
17036 name: resolved_name,
17037 args,
17038 sql_template,
17039 }))
17040 }
17041
17042 fn compile_range_intrinsic(&self, intrinsic: &str, name: &str, args: Vec<IrExpr>) -> Result<IrExpr, PyQLError> {
17051 match intrinsic {
17052 "range" => {
17053 let bounds = args.len().saturating_sub(3);
17056 let point_ty = args[..bounds].iter().find_map(infer_ir_type).ok_or_else(|| {
17057 self.type_err(
17058 "range(): cannot infer the element type from either bound — \
17059 use an explicit cast, e.g. range(<int64>$lower, <int64>$upper)",
17060 )
17061 })?;
17062 let ctor = range_ctor_for_pg_type(point_ty).ok_or_else(|| {
17063 self.type_err(&format!(
17064 "range(): unsupported element type '{point_ty}' — PostgreSQL only has native \
17065 ranges over int64, decimal, datetime, cal::local_datetime, and cal::local_date"
17066 ))
17067 })?;
17068 let upper = match args.len() {
17072 4 => "NULL",
17073 5 => "$2",
17074 n => return Err(self.type_err(&format!("range(): unexpected argument count {n}"))),
17075 };
17076 let flags: Vec<Option<bool>> = args[args.len() - 3..].iter().map(bool_literal).collect();
17077 let sql_template = match flags[..] {
17081 [Some(_), Some(_), Some(true)] => format!("'empty'::{ctor}"),
17082 [Some(true), Some(false), Some(false)] => format!("{ctor}($1, {upper})"),
17083 [Some(inc_lower), Some(inc_upper), Some(false)] => format!(
17084 "{ctor}($1, {upper}, '{}{}')",
17085 if inc_lower { '[' } else { '(' },
17086 if inc_upper { ']' } else { ')' },
17087 ),
17088 _ => {
17089 let (lower_inc, upper_inc, empty) = match args.len() {
17090 4 => ("$2", "$3", "$4"),
17091 _ => ("$3", "$4", "$5"),
17092 };
17093 format!(
17094 "(CASE WHEN {empty} THEN 'empty'::{ctor} ELSE {ctor}($1, {upper}, \
17095 (CASE WHEN {lower_inc} THEN '[' ELSE '(' END) \
17096 || (CASE WHEN {upper_inc} THEN ']' ELSE ')' END)) END)"
17097 )
17098 }
17099 };
17100 Ok(IrExpr::FunctionCall(super::IrFunctionCall {
17111 return_pg_type: Some(ctor.to_string()),
17112 schema: None,
17113 name: ctor.to_string(),
17114 args,
17115 sql_template: Some(sql_template),
17116 }))
17117 }
17118 "multirange" => {
17119 let Some(IrExpr::Array(elems)) = args.first() else {
17120 return Err(self.type_err("multirange(): argument must be an array literal of ranges"));
17121 };
17122 let first_ctor = elems
17123 .first()
17124 .and_then(|e| match e {
17125 IrExpr::FunctionCall(fc) => Some(fc.name.as_str()),
17126 _ => None,
17127 })
17128 .ok_or_else(|| {
17129 self.type_err(
17130 "multirange(): cannot infer the element range type from an empty or non-range \
17131 array — pass at least one range(...) call, e.g. multirange([range(1, 3)])",
17132 )
17133 })?;
17134 let ctor = multirange_ctor_for_range_ctor(first_ctor).ok_or_else(|| {
17135 self.type_err(&format!("multirange(): unrecognized range constructor '{first_ctor}'"))
17136 })?;
17137 Ok(IrExpr::FunctionCall(super::IrFunctionCall {
17141 return_pg_type: Some(ctor.to_string()),
17142 schema: None,
17143 name: name.to_string(),
17144 args,
17145 sql_template: Some(format!("{ctor}(VARIADIC $1)")),
17146 }))
17147 }
17148 other => Err(self.type_err(&format!("internal error: unhandled TranspilerIntrinsic '{other}'"))),
17149 }
17150 }
17151
17152 fn compile_sequence_fn(&mut self, fc: &ast::FunctionCall) -> Result<IrExpr, PyQLError> {
17157 let (module, scalar_name) = self.resolve_sequence_scalar_arg(fc)?;
17158
17159 if fc.name == "sequence_next" {
17160 if fc.args.len() != 1 {
17161 return Err(self.type_err("sequence_next takes exactly 1 argument"));
17162 }
17163 let sql = format!("nextval('\"{}\".\"{}_seq\"')", module, scalar_name);
17164 return Ok(IrExpr::FunctionCall(super::IrFunctionCall {
17165 return_pg_type: None,
17166 schema: None,
17167 name: "nextval".into(),
17168 args: vec![],
17169 sql_template: Some(sql),
17170 }));
17171 }
17172
17173 match fc.args.len() {
17175 1 => {
17176 let sql = format!("setval('\"{}\".\"{}_seq\"', 1, false)", module, scalar_name);
17177 Ok(IrExpr::FunctionCall(super::IrFunctionCall {
17178 return_pg_type: None,
17179 schema: None,
17180 name: "setval".into(),
17181 args: vec![],
17182 sql_template: Some(sql),
17183 }))
17184 }
17185 2 => {
17186 let val = self.compile_free_expr(&fc.args[1])?;
17187 let sql = format!("setval('\"{}\".\"{}_seq\"', $1, true)", module, scalar_name);
17188 Ok(IrExpr::FunctionCall(super::IrFunctionCall {
17189 return_pg_type: None,
17190 schema: None,
17191 name: "setval".into(),
17192 args: vec![val],
17193 sql_template: Some(sql),
17194 }))
17195 }
17196 _ => Err(self.type_err("sequence_reset takes 1 or 2 arguments")),
17197 }
17198 }
17199
17200 fn resolve_sequence_scalar_arg(&self, fc: &ast::FunctionCall) -> Result<(String, String), PyQLError> {
17203 use crate::parse::ast::{Expr, Path, PathStep};
17204
17205 let arg = fc.args.first().ok_or_else(|| {
17206 self.type_err(&format!(
17207 "{}() requires a sequence scalar type as its first argument",
17208 fc.name
17209 ))
17210 })?;
17211
17212 let (arg_module, arg_name): (Option<&str>, &str) = match arg {
17215 Expr::Path(Path { steps, partial: false }) => match steps.as_slice() {
17216 [PathStep::Name(s)] => {
17217 if let Some((m, n)) = s.split_once("::") {
17218 (Some(m), n)
17219 } else {
17220 (None, s.as_str())
17221 }
17222 }
17223 _ => return Err(self.type_err(&format!(
17224 "{}(): first argument must be a sequence scalar type name (e.g. OrderNumber or default::OrderNumber)",
17225 fc.name
17226 ))),
17227 },
17228 _ => return Err(self.type_err(&format!(
17229 "{}(): first argument must be a sequence scalar type name (e.g. OrderNumber or default::OrderNumber)",
17230 fc.name
17231 ))),
17232 };
17233
17234 let scalar = self.schema.scalars.iter().find(|s| {
17235 s.is_sequence && s.name == arg_name && arg_module.map(|m| m == s.module.as_str()).unwrap_or(true)
17236 });
17237
17238 match scalar {
17239 Some(s) => Ok((s.module.clone(), s.name.clone())),
17240 None => Err(self.type_err(&format!(
17241 "{}(): '{}' is not a known sequence scalar type",
17242 fc.name, arg_name
17243 ))),
17244 }
17245 }
17246
17247 const NOTIFY_PAYLOAD_MAX_BYTES: usize = 8000;
17256
17257 fn notify_type_payload_err(&self, channel: &str, qname: &str) -> PyQLError {
17259 self.type_err(&format!(
17260 "notify(): payload for Channel '{channel}' (a '{qname}' object channel) must name an object of that \
17261 type — either a with-block binding, or __new__/__old__ inside a trigger handler"
17262 ))
17263 }
17264
17265 fn compile_notify(
17275 &mut self,
17276 fc: &ast::FunctionCall,
17277 ctx: Option<(&TypeDescriptor, &str)>,
17278 ) -> Result<IrExpr, PyQLError> {
17279 use crate::parse::ast::{Expr, Path, PathStep};
17280
17281 if fc.args.len() != 2 {
17282 return Err(self.type_err("notify() takes exactly 2 arguments: (Channel, payload)"));
17283 }
17284
17285 let (channel_module, channel_name): (Option<&str>, &str) = match &fc.args[0] {
17286 Expr::Path(Path { steps, partial: false }) => match steps.as_slice() {
17287 [PathStep::Name(s)] => {
17288 if let Some((m, n)) = s.split_once("::") {
17289 (Some(m), n)
17290 } else {
17291 (None, s.as_str())
17292 }
17293 }
17294 _ => {
17295 return Err(self.type_err(
17296 "notify(): first argument must be a Channel name (e.g. OrderEvents or orders::OrderEvents)",
17297 ));
17298 }
17299 },
17300 _ => {
17301 return Err(self.type_err(
17302 "notify(): first argument must be a Channel name (e.g. OrderEvents or orders::OrderEvents)",
17303 ));
17304 }
17305 };
17306 let full_channel_name = match channel_module {
17307 Some(m) => format!("{m}::{channel_name}"),
17308 None => channel_name.to_string(),
17309 };
17310 let channel = self
17311 .resolve_channel(&full_channel_name)
17312 .ok_or_else(|| self.type_err(&format!("notify(): '{channel_name}' is not a known Channel")))?;
17313 let wire_name = channel.wire_name.clone();
17314 let payload_arg = &fc.args[1];
17315
17316 let anchor_fallback: Option<(&'a TypeDescriptor, String)> = self.special_anchors.values().next().cloned();
17330 let ctx = ctx.or_else(|| anchor_fallback.as_ref().map(|(td, alias)| (*td, alias.as_str())));
17331
17332 let payload_ir = match &channel.payload {
17333 crate::schema::ChannelPayload::Type(qname) => {
17334 let Expr::Path(Path { steps, partial: false }) = payload_arg else {
17343 return Err(self.notify_type_payload_err(&full_channel_name, qname));
17344 };
17345 let [PathStep::Name(name)] = steps.as_slice() else {
17346 return Err(self.notify_type_payload_err(&full_channel_name, qname));
17347 };
17348
17349 if name == "__new__" || name == "__old__" {
17350 let (anchor_td, alias) = self.special_anchors.get(name.as_str()).cloned().ok_or_else(|| {
17351 self.type_err(&format!(
17352 "notify(): '{name}' cannot be used here — it's only bound inside a trigger handler"
17353 ))
17354 })?;
17355 let anchor_qname = format!("{}::{}", anchor_td.module, anchor_td.name);
17356 if &anchor_qname != qname {
17357 return Err(self.type_err(&format!(
17358 "notify(): Channel '{full_channel_name}' expects a payload of type '{qname}', got '{anchor_qname}'"
17359 )));
17360 }
17361 IrExpr::ColumnRef {
17362 alias,
17363 column: "id".to_string(),
17364 pg_type: "uuid".to_string(),
17365 }
17366 } else if let Some(cte_type) = self.cte_types.get(name.as_str()).cloned() {
17367 if &cte_type != qname {
17368 return Err(self.type_err(&format!(
17369 "notify(): Channel '{full_channel_name}' expects a payload of type '{qname}', got '{cte_type}'"
17370 )));
17371 }
17372 IrExpr::CteRef {
17375 name: name.clone(),
17376 scalar: false,
17377 pg_type: None,
17378 }
17379 } else {
17380 return Err(self.notify_type_payload_err(&full_channel_name, qname));
17381 }
17382 }
17383 crate::schema::ChannelPayload::Scalar(pg_type) => {
17384 let ir = self.compile_expr_ctx(payload_arg, ctx)?;
17385 if let Some(actual) = infer_ir_type(&ir)
17386 && !types_compatible(actual, pg_type)
17387 {
17388 return Err(self.type_err(&format!(
17389 "notify(): Channel '{full_channel_name}' expects a payload of type '{expected}', got '{actual_pyql}'",
17390 expected = pg_type_to_pyql(pg_type),
17391 actual_pyql = pg_type_to_pyql(actual),
17392 )));
17393 }
17394 ir
17395 }
17396 crate::schema::ChannelPayload::Object(declared_fields) => {
17397 let Expr::Shape(sh) = payload_arg else {
17398 return Err(self.type_err(&format!(
17399 "notify(): payload for Channel '{full_channel_name}' (an Object channel) must be a free \
17400 object literal, e.g. {{ {} }}",
17401 declared_fields
17402 .iter()
17403 .map(|(n, _)| format!("{n} := .."))
17404 .collect::<Vec<_>>()
17405 .join(", ")
17406 )));
17407 };
17408 if sh.expr.is_some() {
17409 return Err(self.type_err(&format!(
17410 "notify(): payload for Channel '{full_channel_name}' (an Object channel) must be a free \
17411 object literal, not a shape over a type"
17412 )));
17413 }
17414 let ir = self.compile_expr_ctx(payload_arg, ctx)?;
17415 let IrExpr::NamedTuple {
17416 fields,
17417 is_free_object: true,
17418 } = &ir
17419 else {
17420 return Err(self.type_err(&format!(
17421 "notify(): payload for Channel '{full_channel_name}' (an Object channel) must be a free object literal"
17422 )));
17423 };
17424 let declared_names: std::collections::HashSet<&str> =
17425 declared_fields.iter().map(|(n, _)| n.as_str()).collect();
17426 let actual_names: std::collections::HashSet<&str> = fields.iter().map(|(n, _)| n.as_str()).collect();
17427 if declared_names != actual_names {
17428 let mut expected: Vec<&str> = declared_names.iter().copied().collect();
17429 expected.sort();
17430 let mut actual: Vec<&str> = actual_names.iter().copied().collect();
17431 actual.sort();
17432 return Err(self.type_err(&format!(
17433 "notify(): payload fields for Channel '{full_channel_name}' don't match — expected {{{}}}, got {{{}}}",
17434 expected.join(", "), actual.join(", ")
17435 )));
17436 }
17437 for (name, expr) in fields {
17438 let Some((_, declared_pg_type)) = declared_fields.iter().find(|(n, _)| n == name) else {
17439 continue;
17440 };
17441 if let Some(actual) = infer_ir_type(expr)
17442 && !types_compatible(actual, declared_pg_type)
17443 {
17444 return Err(self.type_err(&format!(
17445 "notify(): field '{name}' of Channel '{full_channel_name}' expects type '{expected}', got '{actual_pyql}'",
17446 expected = pg_type_to_pyql(declared_pg_type),
17447 actual_pyql = pg_type_to_pyql(actual),
17448 )));
17449 }
17450 }
17451 ir
17452 }
17453 };
17454
17455 let floor = static_min_payload_bytes(payload_arg);
17456 if floor >= Self::NOTIFY_PAYLOAD_MAX_BYTES {
17457 return Err(self.type_err(&format!(
17458 "notify(): this payload is at least {floor} bytes, which is at or over PostgreSQL's {}-byte NOTIFY \
17459 payload limit — the notification would fail at runtime, aborting the transaction that sent it",
17460 Self::NOTIFY_PAYLOAD_MAX_BYTES
17461 )));
17462 }
17463
17464 let sql = format!("pg_notify('{}', ($1)::text)", wire_name.replace('\'', "''"));
17465 Ok(IrExpr::FunctionCall(super::IrFunctionCall {
17466 return_pg_type: None,
17467 schema: None,
17468 name: "pg_notify".to_string(),
17469 args: vec![payload_ir],
17470 sql_template: Some(sql),
17471 }))
17472 }
17473
17474 fn compile_notify_raw(
17478 &mut self,
17479 fc: &ast::FunctionCall,
17480 ctx: Option<(&TypeDescriptor, &str)>,
17481 ) -> Result<IrExpr, PyQLError> {
17482 if fc.args.len() != 2 {
17483 return Err(self.type_err("notify_raw() takes exactly 2 arguments: (channel_name, payload)"));
17484 }
17485 let channel_ir = self.compile_expr_ctx(&fc.args[0], ctx)?;
17486 let payload_ir = self.compile_expr_ctx(&fc.args[1], ctx)?;
17487
17488 let floor = static_min_payload_bytes(&fc.args[1]);
17489 if floor >= Self::NOTIFY_PAYLOAD_MAX_BYTES {
17490 return Err(self.type_err(&format!(
17491 "notify_raw(): this payload is at least {floor} bytes, which is at or over PostgreSQL's {}-byte \
17492 NOTIFY payload limit",
17493 Self::NOTIFY_PAYLOAD_MAX_BYTES
17494 )));
17495 }
17496
17497 Ok(IrExpr::FunctionCall(super::IrFunctionCall {
17498 return_pg_type: None,
17499 schema: None,
17500 name: "pg_notify".to_string(),
17501 args: vec![channel_ir, payload_ir],
17502 sql_template: None,
17503 }))
17504 }
17505
17506 fn globals_arg_for_call(&mut self, qualified: &str) -> Result<Option<IrExpr>, PyQLError> {
17515 if self.fns_needing_globals.is_none() {
17516 self.fns_needing_globals = Some(functions_needing_globals(self.schema).clone());
17517 }
17518 if !self.fns_needing_globals.as_ref().is_some_and(|s| s.contains(qualified)) {
17519 return Ok(None);
17520 }
17521 if self.in_fn_body {
17522 self.used_globals_arg = true;
17523 return Ok(Some(IrExpr::RawSql(GLOBALS_ARG.to_string())));
17524 }
17525 let session_globals: Vec<String> = self
17526 .schema
17527 .globals
17528 .iter()
17529 .filter(|g| g.computed_expr.is_none())
17530 .map(|g| format!("{}::{}", g.module, g.name))
17531 .collect();
17532 let mut args = Vec::with_capacity(session_globals.len() * 2);
17533 for name in session_globals {
17534 let value = self.compile_global(&name)?;
17535 args.push(IrExpr::Literal(IrLiteral::Str(name)));
17536 args.push(value);
17537 }
17538 Ok(Some(IrExpr::FunctionCall(super::IrFunctionCall {
17539 return_pg_type: None,
17540 schema: None,
17541 name: "jsonb_build_object".to_string(),
17542 args,
17543 sql_template: None,
17544 })))
17545 }
17546
17547 fn try_compile_fn_object_select(
17553 &mut self,
17554 fc: &ast::FunctionCall,
17555 elements: &[ast::ShapeElement],
17556 s: &ast::SelectStmt,
17557 distinct: bool,
17558 ) -> Result<Option<IrFunctionSelect>, PyQLError> {
17559 use crate::schema::FunctionDescriptor;
17560
17561 let fd: Option<&FunctionDescriptor> = self.schema.functions.iter().find(|f| {
17562 let module_matches = fc.module.as_deref().map(|m| m == f.module.as_str()).unwrap_or(true);
17563 module_matches && f.name == fc.name && f.return_is_object
17564 });
17565 let fd = match fd {
17566 Some(f) => f,
17567 None => return Ok(None),
17568 };
17569 if fd.params.len() != fc.args.len() {
17570 return Err(self.type_err(&format!(
17571 "function '{}::{}' expects {} argument(s), got {}",
17572 fd.module,
17573 fd.name,
17574 fd.params.len(),
17575 fc.args.len()
17576 )));
17577 }
17578
17579 let fn_module = fd.module.clone();
17580 let fn_name = fd.name.clone();
17581 let return_type_name = fd.return_pg_type.clone(); let polymorphic = fd.return_is_polymorphic;
17583
17584 let mut fn_args = fc
17585 .args
17586 .iter()
17587 .map(|a| self.compile_free_expr(a))
17588 .collect::<Result<Vec<_>, _>>()?;
17589 let qualified = format!("{}::{}", fn_module, fn_name);
17590 if let Some(globals) = self.globals_arg_for_call(&qualified)? {
17591 fn_args.insert(0, globals);
17592 }
17593
17594 let alias = self.fresh_alias();
17595
17596 let td = self.resolve_type(&return_type_name)?;
17598 let td = td.clone();
17599
17600 let (poly_implementors, poly_columns) = if polymorphic {
17601 self.collect_poly_info(&return_type_name)
17602 } else {
17603 (vec![], vec![])
17604 };
17605
17606 let td_module = td.module.clone();
17607 let shape = self.compile_shape(elements, &td, &alias, &td_module)?;
17609 let outer_declared = self.active_declared_pointers.clone();
17613 self.active_declared_pointers
17614 .extend(elements.iter().filter(|el| el.compexpr.is_some()).cloned());
17615 let modifiers = self.compile_path_modifiers(s, &td, &alias);
17616 self.active_declared_pointers = outer_declared;
17617 let (filter, order_by, offset, limit) = modifiers?;
17618
17619 Ok(Some(IrFunctionSelect {
17620 fn_module,
17621 fn_name,
17622 fn_args,
17623 alias,
17624 type_name: return_type_name,
17625 polymorphic,
17626 poly_implementors,
17627 poly_columns,
17628 shape,
17629 filter,
17630 order_by,
17631 offset,
17632 limit,
17633 distinct,
17634 }))
17635 }
17636
17637 fn read_nested_links_from_their_ctes(dml: &IrStmt, dml_cte: Option<&str>, shape: &mut [IrShapePointer]) {
17654 let (assignments, nested_ctes, appends) = match dml {
17655 IrStmt::Insert(ins) => (&ins.assignments, &ins.nested_ctes, &ins.multi_link_appends),
17656 IrStmt::Update(upd) => (&upd.assignments, &upd.nested_ctes, &upd.multi_link_appends),
17657 _ => return,
17658 };
17659 if let Some(dml_cte) = dml_cte {
17665 for pointer in shape.iter_mut() {
17666 let IrShapePointer::MultiLink(link) = pointer else {
17667 continue;
17668 };
17669 let IrMultiLinkJoin::Standard { junction_table, .. } = &mut link.join else {
17670 continue;
17671 };
17672 let Some(index) = appends.iter().position(|a| a.junction_table == *junction_table) else {
17673 continue;
17674 };
17675 *junction_table = format!("@cte:{dml_cte}__ml_add_{index}");
17676 if let IrMultiLinkValueSource::CteRef(target_cte) = &appends[index].values.source {
17677 for row in &mut link.subquery.rows {
17678 if let IrRowSource::Bound { source, .. } = row {
17679 source.table = format!("@cte:{target_cte}");
17680 source.poly = None;
17681 }
17682 }
17683 }
17684 }
17685 }
17686 if nested_ctes.is_empty() {
17687 return;
17688 }
17689 let from_cte: HashMap<&str, &str> = assignments
17690 .iter()
17691 .filter_map(|(column, expr)| match expr {
17692 IrExpr::ColumnRef { alias, column: c, .. }
17693 if c == "id" && nested_ctes.iter().any(|cte| cte.name == *alias) =>
17694 {
17695 Some((column.as_str(), alias.as_str()))
17696 }
17697 _ => None,
17698 })
17699 .collect();
17700 if from_cte.is_empty() {
17701 return;
17702 }
17703 for pointer in shape {
17704 let IrShapePointer::SingleLink(link) = pointer else {
17705 continue;
17706 };
17707 let IrSingleLinkCorrelation::Fk { fk_column, .. } = &link.correlation else {
17708 continue;
17709 };
17710 let Some(cte_name) = from_cte.get(fk_column.as_str()) else {
17711 continue;
17712 };
17713 for row in &mut link.subquery.rows {
17714 if let IrRowSource::Bound { source, .. } = row {
17715 source.table = format!("@cte:{cte_name}");
17716 source.poly = None;
17717 }
17718 }
17719 }
17720 }
17721
17722 fn poly_dml_columns(td: &TypeDescriptor) -> Vec<String> {
17723 td.properties
17724 .iter()
17725 .map(|p| p.name.clone())
17726 .chain(
17727 td.links
17728 .iter()
17729 .filter(|l| !l.is_junction_backed())
17730 .map(|l| format!("{}_id", l.name)),
17731 )
17732 .collect()
17733 }
17734
17735 fn link_target_select(
17745 &self,
17746 target_td: &TypeDescriptor,
17747 mut source: IrSource,
17748 shape: Vec<IrShapePointer>,
17749 ) -> IrSelect {
17750 source.poly = self.link_target_fanout(target_td);
17751 IrSelect::schema_bound(source, shape, None)
17752 }
17753
17754 fn link_target_fanout(&self, target_td: &TypeDescriptor) -> Option<IrPolyFanout> {
17757 self.poly_fanout_for(&format!("{}::{}", target_td.module, target_td.name))
17758 }
17759
17760 fn resolve_join_fanouts(&self, path_select: &mut IrPathSelect) {
17767 for join in &mut path_select.joins {
17768 let target = match join {
17769 IrPathJoin::Single { target, .. }
17770 | IrPathJoin::Multi { target, .. }
17771 | IrPathJoin::BacklinkSingle { target, .. }
17772 | IrPathJoin::BacklinkMulti { target, .. }
17773 | IrPathJoin::Function { target, .. }
17774 | IrPathJoin::Lateral { target, .. } => target,
17775 };
17776 if target.poly.is_none() && !target.table.starts_with("@cte:") {
17777 target.poly = self.poly_fanout_for(&target.type_name.clone());
17778 }
17779 }
17780 }
17781
17782 fn poly_fanout_for(&self, type_name: &str) -> Option<IrPolyFanout> {
17785 let td = self
17786 .schema
17787 .types
17788 .iter()
17789 .find(|t| format!("{}::{}", t.module, t.name) == type_name)?;
17790 if !td.abstract_ && !self.has_subtypes(td) {
17794 return None;
17795 }
17796 let (implementors, columns) = self.collect_poly_info(type_name);
17797 Some(IrPolyFanout { implementors, columns })
17798 }
17799
17800 fn collect_poly_info(&self, type_name: &str) -> (Vec<IrPolyImplementor>, Vec<String>) {
17802 let implementors = self.find_poly_implementors(type_name);
17803 let columns = if let Some(td) = self
17804 .schema
17805 .types
17806 .iter()
17807 .find(|t| format!("{}::{}", t.module, t.name) == type_name)
17808 {
17809 Self::poly_dml_columns(td)
17814 } else {
17815 vec![]
17816 };
17817 (implementors, columns)
17818 }
17819
17820 fn try_compile_vector_search(
17831 &mut self,
17832 fc: &ast::FunctionCall,
17833 elements: &[ast::ShapeElement],
17834 s: &ast::SelectStmt,
17835 ) -> Result<Option<IrVectorSearch>, PyQLError> {
17836 if fc.module.as_deref() != Some("vector") || fc.name != "search" {
17837 return Ok(None);
17838 }
17839
17840 let text_query_arg = fc.kwargs.iter().find(|(k, _)| k == "query").map(|(_, v)| v);
17842 let is_text_overload = text_query_arg.is_some();
17843
17844 if !is_text_overload && fc.args.len() < 2 {
17845 return Err(
17846 self.type_err("vector::search requires either a positional vector argument or `query := $text`")
17847 );
17848 }
17849
17850 let (type_qname, inner_filter_ast): (String, Option<ast::Expr>) = match &fc.args[0] {
17857 ast::Expr::Path(p) if !p.partial => {
17858 let name = p.steps.iter()
17859 .filter_map(|s| if let ast::PathStep::Name(n) = s { Some(n.as_str()) } else { None })
17860 .collect::<Vec<_>>().join("::");
17861 let td = self.resolve_type(&name)
17862 .map_err(|_| self.type_err(&format!("vector::search: '{}' is not a known type", name)))?;
17863 (format!("{}::{}", td.module, td.name), None)
17864 }
17865 ast::Expr::SubQuery(stmt) => {
17866 if let ast::Stmt::Select(inner_sel) = stmt.as_ref() {
17867 let inner_type_name = match &inner_sel.result {
17868 ast::Expr::Path(p) if !p.partial => {
17869 p.steps.iter()
17870 .filter_map(|s| if let ast::PathStep::Name(n) = s { Some(n.as_str()) } else { None })
17871 .collect::<Vec<_>>().join("::")
17872 }
17873 _ => return Err(self.type_err(
17874 "vector::search: subquery first argument must select a single type (e.g. select Product filter …)"
17875 )),
17876 };
17877 let td = self.resolve_type(&inner_type_name)
17878 .map_err(|_| self.type_err(&format!("vector::search: '{}' is not a known type", inner_type_name)))?;
17879 let qname = format!("{}::{}", td.module, td.name);
17880 (qname, inner_sel.filter.clone())
17881 } else {
17882 return Err(self.type_err("vector::search: subquery first argument must be a SELECT"));
17883 }
17884 }
17885 _ => return Err(self.type_err(
17886 "vector::search: first argument must be a type name or a filtered subquery (e.g. select Product filter …)"
17887 )),
17888 };
17889
17890 let index_name: Option<String> = fc.kwargs.iter().find(|(k, _)| k == "index_name").and_then(|(_, v)| {
17892 if let ast::Expr::Literal(ast::Literal::Str(s)) = v {
17893 Some(s.clone())
17894 } else {
17895 None
17896 }
17897 });
17898
17899 let td = self.resolve_type(&type_qname)?.clone();
17901 let vi = td
17902 .vector_indexes
17903 .iter()
17904 .find(|vi| vi.index_name.as_deref() == index_name.as_deref())
17905 .ok_or_else(|| {
17906 let key = index_name.as_deref().unwrap_or("<default>");
17907 self.type_err(&format!("type '{}' has no vector index '{}'", type_qname, key))
17908 })?;
17909
17910 let vector_col = vi.column_name();
17911 let distance_op = match vi.metric.as_str() {
17912 "euclidean" => "<->",
17913 "inner_product" => "<#>",
17914 _ => "<=>", };
17916
17917 let (
17919 query_expr,
17920 inference_query_param_name,
17921 inference_query_literal,
17922 inference_model,
17923 inference_type_name,
17924 inference_index_name,
17925 );
17926
17927 if is_text_overload {
17928 let vec_idx = self.param_index("__deferred_vec__");
17931 let vec_param = IrExpr::Param { index: vec_idx };
17932 let inner_cast = IrExpr::TypeCast(Box::new(IrTypeCast {
17934 expr: vec_param,
17935 pg_type: "float8[]".to_string(),
17936 tuple_shape: None,
17937 }));
17938 query_expr = IrExpr::TypeCast(Box::new(IrTypeCast {
17939 expr: inner_cast,
17940 pg_type: "vector".to_string(),
17941 tuple_shape: None,
17942 }));
17943 let query_arg = text_query_arg.unwrap();
17944 inference_query_param_name = Some(match query_arg {
17945 ast::Expr::Parameter(name) => name.clone(),
17946 _ => String::new(),
17947 });
17948 inference_query_literal = match query_arg {
17949 ast::Expr::Literal(ast::Literal::Str(s)) => Some(s.clone()),
17950 _ => None,
17951 };
17952 inference_model = Some(vi.model.clone());
17953 inference_type_name = Some(type_qname.clone());
17954 inference_index_name = Some(index_name.clone());
17955 } else {
17956 let raw_query_expr = self.compile_free_expr(&fc.args[1])?;
17958 query_expr = IrExpr::TypeCast(Box::new(IrTypeCast {
17959 expr: raw_query_expr,
17960 pg_type: "vector".to_string(),
17961 tuple_shape: None,
17962 }));
17963 inference_query_param_name = None;
17964 inference_query_literal = None;
17965 inference_model = None;
17966 inference_type_name = None;
17967 inference_index_name = None;
17968 }
17969
17970 let alias = self.fresh_alias();
17971 let source = IrSource {
17972 poly: None,
17973 type_name: type_qname.clone(),
17974 table: td.table.clone(),
17975 alias: alias.clone(),
17976 };
17977
17978 let pre_filter: Option<IrExpr> = match inner_filter_ast {
17981 Some(ref f) => Some(self.as_condition(|c| c.compile_expr(f, &td, &alias))?),
17982 None => None,
17983 };
17984
17985 let mut object_shape: Vec<IrShapePointer> = vec![];
17990 for el in elements {
17991 if el.splat.is_some() {
17992 continue;
17993 } let pointer_name = match el.path.steps.first() {
17995 Some(ast::PathStep::Name(n)) => n.as_str(),
17996 _ => continue,
17997 };
17998 match pointer_name {
17999 "distance" => { }
18000 "object" => {
18001 let sub_els = el.nested.as_deref().unwrap_or(&[]);
18002 object_shape = self.compile_shape(sub_els, &td, &alias, &td.module)?;
18003 }
18004 other => {
18005 return Err(self.type_err(&format!(
18006 "vector::search result has no pointer '{}'; valid pointers are 'object' and 'distance'",
18007 other
18008 )));
18009 }
18010 }
18011 }
18012
18013 let (outer_filter, order_by_distance, offset, limit) = self.compile_vs_modifiers(s)?;
18014
18015 let filter = match (pre_filter, outer_filter) {
18017 (Some(a), Some(b)) => Some(IrExpr::BinOp(Box::new(IrBinOp {
18018 left: a,
18019 op: ast::BinOpKind::And,
18020 right: b,
18021 }))),
18022 (Some(f), None) | (None, Some(f)) => Some(f),
18023 (None, None) => None,
18024 };
18025
18026 Ok(Some(IrVectorSearch {
18027 source,
18028 vector_col,
18029 distance_op,
18030 query_expr,
18031 object_shape,
18032 filter,
18033 order_by_distance,
18034 offset,
18035 limit,
18036 inference_query_param_name,
18037 inference_query_literal,
18038 inference_model,
18039 inference_type_name,
18040 inference_index_name,
18041 }))
18042 }
18043
18044 fn compile_vs_modifiers(&mut self, s: &ast::SelectStmt) -> Result<SearchModifiers, PyQLError> {
18047 let mut order_by_distance: Option<IrSortDir> = None;
18048 for sort in &s.order_by {
18049 let is_distance = matches!(&sort.expr,
18050 ast::Expr::Path(p) if p.partial && p.steps.len() == 1
18051 && matches!(&p.steps[0], ast::PathStep::Name(n) if n == "distance")
18052 );
18053 if is_distance {
18054 let dir = match sort.direction {
18055 ast::SortDirection::Desc => IrSortDir::Desc,
18056 ast::SortDirection::Asc => IrSortDir::Asc,
18057 };
18058 order_by_distance = Some(dir);
18059 } else {
18060 return Err(self.type_err("vector::search: only 'order by .distance' is supported as a sort key"));
18061 }
18062 }
18063
18064 let filter = match &s.filter {
18065 Some(f) => Some(self.compile_free_expr(f)?),
18066 None => None,
18067 };
18068 let offset = match &s.offset {
18069 Some(o) => Some(self.compile_free_expr(o)?),
18070 None => None,
18071 };
18072 let limit = match &s.limit {
18073 Some(l) => Some(self.compile_free_expr(l)?),
18074 None => None,
18075 };
18076
18077 Ok((filter, order_by_distance, offset, limit))
18078 }
18079
18080 fn try_compile_fts_search(
18085 &mut self,
18086 fc: &ast::FunctionCall,
18087 elements: &[ast::ShapeElement],
18088 s: &ast::SelectStmt,
18089 ) -> Result<Option<IrFtsSearch>, PyQLError> {
18090 if fc.module.as_deref() != Some("fts") || fc.name != "search" {
18091 return Ok(None);
18092 }
18093 if fc.args.len() < 2 {
18094 return Err(self.type_err("fts::search requires at least 2 arguments: (TypeName, $query)"));
18095 }
18096
18097 let type_qname = match &fc.args[0] {
18099 ast::Expr::Path(p) if !p.partial && p.steps.len() == 1 => {
18100 if let ast::PathStep::Name(n) = &p.steps[0] {
18101 let td = self
18102 .resolve_type(n)
18103 .map_err(|_| self.type_err(&format!("fts::search: '{}' is not a known type", n)))?;
18104 format!("{}::{}", td.module, td.name)
18105 } else {
18106 return Err(self.type_err("fts::search: first argument must be a type name"));
18107 }
18108 }
18109 _ => return Err(self.type_err("fts::search: first argument must be a bare type name")),
18110 };
18111
18112 let index_name: Option<String> = fc.kwargs.iter().find(|(k, _)| k == "index_name").and_then(|(_, v)| {
18114 if let ast::Expr::Literal(ast::Literal::Str(s)) = v {
18115 Some(s.clone())
18116 } else {
18117 None
18118 }
18119 });
18120
18121 let mode_str = fc
18122 .kwargs
18123 .iter()
18124 .find(|(k, _)| k == "mode")
18125 .and_then(|(_, v)| {
18126 if let ast::Expr::Literal(ast::Literal::Str(s)) = v {
18127 Some(s.as_str())
18128 } else {
18129 None
18130 }
18131 })
18132 .unwrap_or("BestFields");
18133
18134 let tsquery_fn: &'static str = match mode_str {
18135 "Phrase" => "phraseto_tsquery",
18136 _ => "websearch_to_tsquery", };
18138
18139 let td = self.resolve_type(&type_qname)?.clone();
18141 let si = td
18142 .search_indexes
18143 .iter()
18144 .find(|si| si.index_name.as_deref() == index_name.as_deref())
18145 .ok_or_else(|| {
18146 let key = index_name.as_deref().unwrap_or("<default>");
18147 self.type_err(&format!("type '{}' has no search index '{}'", type_qname, key))
18148 })?;
18149
18150 let backend = si.backend.clone();
18151 let search_col = si.column_name();
18152 let is_deferred = backend != SearchBackend::Postgres;
18153 let deferred_index_name = if is_deferred {
18154 Some(si.deferred_index_name(&td.module, &td.name))
18155 } else {
18156 None
18157 };
18158
18159 let query_expr;
18164 let deferred_query_param_name;
18165 let deferred_query_literal;
18166 let deferred_ids_param;
18167 let deferred_scores_param;
18168
18169 if is_deferred {
18170 let ids_idx = self.param_index("__deferred_ids__");
18171 let scores_idx = self.param_index("__deferred_scores__");
18172 deferred_ids_param = Some(ids_idx);
18173 deferred_scores_param = Some(scores_idx);
18174 deferred_query_param_name = match &fc.args[1] {
18175 ast::Expr::Parameter(name) => Some(name.clone()),
18176 _ => None,
18177 };
18178 deferred_query_literal = match &fc.args[1] {
18179 ast::Expr::Literal(ast::Literal::Str(s)) => Some(s.clone()),
18180 _ => None,
18181 };
18182 query_expr = IrExpr::Literal(crate::ir::IrLiteral::Str(String::new()));
18184 } else {
18185 query_expr = self.compile_free_expr(&fc.args[1])?;
18186 deferred_query_param_name = None;
18187 deferred_query_literal = None;
18188 deferred_ids_param = None;
18189 deferred_scores_param = None;
18190 }
18191
18192 let alias = self.fresh_alias();
18193 let source = IrSource {
18194 poly: None,
18195 type_name: type_qname.clone(),
18196 table: td.table.clone(),
18197 alias: alias.clone(),
18198 };
18199
18200 let mut object_shape: Vec<IrShapePointer> = vec![];
18202 for el in elements {
18203 if el.splat.is_some() {
18204 continue;
18205 }
18206 let pointer_name = match el.path.steps.first() {
18207 Some(ast::PathStep::Name(n)) => n.as_str(),
18208 _ => continue,
18209 };
18210 match pointer_name {
18211 "score" => { }
18212 "object" => {
18213 let sub_els = el.nested.as_deref().unwrap_or(&[]);
18214 object_shape = self.compile_shape(sub_els, &td, &alias, &td.module)?;
18215 }
18216 other => {
18217 return Err(self.type_err(&format!(
18218 "fts::search result has no pointer '{}'; valid pointers are 'object' and 'score'",
18219 other
18220 )));
18221 }
18222 }
18223 }
18224
18225 let (filter, order_by_rank, offset, limit) = self.compile_fts_modifiers(s)?;
18226
18227 Ok(Some(IrFtsSearch {
18228 source,
18229 backend,
18230 search_col,
18231 tsquery_fn,
18232 query_expr,
18233 object_shape,
18234 filter,
18235 order_by_rank,
18236 offset,
18237 limit,
18238 deferred_index_name,
18239 deferred_query_param_name,
18240 deferred_query_literal,
18241 deferred_ids_param,
18242 deferred_scores_param,
18243 }))
18244 }
18245
18246 fn compile_fts_modifiers(&mut self, s: &ast::SelectStmt) -> Result<SearchModifiers, PyQLError> {
18249 let mut order_by_rank: Option<IrSortDir> = None;
18250 for sort in &s.order_by {
18251 let is_rank = matches!(&sort.expr,
18252 ast::Expr::Path(p) if p.partial && p.steps.len() == 1
18253 && matches!(&p.steps[0], ast::PathStep::Name(n) if n == "score")
18254 );
18255 if is_rank {
18256 let dir = match sort.direction {
18257 ast::SortDirection::Desc => IrSortDir::Desc,
18258 ast::SortDirection::Asc => IrSortDir::Asc,
18259 };
18260 order_by_rank = Some(dir);
18261 } else {
18262 return Err(self.type_err("fts::search: only 'order by .score' is supported as a sort key"));
18263 }
18264 }
18265
18266 let filter = match &s.filter {
18267 Some(f) => Some(self.compile_free_expr(f)?),
18268 None => None,
18269 };
18270 let offset = match &s.offset {
18271 Some(o) => Some(self.compile_free_expr(o)?),
18272 None => None,
18273 };
18274 let limit = match &s.limit {
18275 Some(l) => Some(self.compile_free_expr(l)?),
18276 None => None,
18277 };
18278
18279 Ok((filter, order_by_rank, offset, limit))
18280 }
18281
18282 fn type_err(&self, msg: &str) -> PyQLError {
18285 PyQLError::Type(PyQLTypeError {
18286 message: msg.to_string(),
18287 position: Position { line: 0, col: 0 },
18288 })
18289 }
18290
18291 fn field_err(&self, field: &str, type_name: &str) -> PyQLError {
18292 if std::env::var("PYLON_DBG_FIELD_ERR").is_ok() {
18293 eprintln!(
18294 "DBG field_err {field} on {type_name}
18295{}",
18296 std::backtrace::Backtrace::force_capture()
18297 );
18298 }
18299 let suggestion = self
18300 .schema
18301 .types
18302 .iter()
18303 .find(|t| format!("{}::{}", t.module, t.name) == type_name)
18304 .and_then(|td| Self::suggest_pointer_name(td, field));
18305 let message = match suggestion {
18306 Some(s) => format!("object type '{type_name}' has no link or property '{field}'. Did you mean '{s}'?"),
18307 None => format!("object type '{type_name}' has no link or property '{field}'"),
18308 };
18309 PyQLError::Resolution(PyQLResolutionError::UnknownField(PyQLUnknownFieldError {
18310 message,
18311 position: Position { line: 0, col: 0 },
18312 }))
18313 }
18314
18315 fn suggest_function_name(&self, ns: &str, name: &str) -> String {
18331 const MIN_SIMILARITY: f64 = 0.7;
18332 if let Some(other) = ["std", "math", "cal", "sys"]
18333 .into_iter()
18334 .find(|o| *o != ns && !crate::stdlib::lookup(o, name).is_empty())
18335 {
18336 return format!(" — it lives in {other}, use {other}::{name}()");
18337 }
18338 crate::stdlib::registry()
18339 .iter()
18340 .filter(|d| d.namespace == ns)
18341 .map(|d| (format!("{ns}::{}", d.name), d.name))
18342 .chain(
18343 self.schema
18344 .functions
18345 .iter()
18346 .map(|f| (format!("{}::{}", f.module, f.name), f.name.as_str())),
18347 )
18348 .map(|(qualified, candidate)| (qualified, strsim::jaro_winkler(name, candidate)))
18349 .filter(|(_, score)| *score >= MIN_SIMILARITY)
18350 .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
18351 .map(|(qualified, _)| format!(" — did you mean {qualified}()?"))
18352 .unwrap_or_default()
18353 }
18354
18355 fn suggest_pointer_name(td: &TypeDescriptor, name: &str) -> Option<String> {
18366 const MIN_SIMILARITY: f64 = 0.7;
18367 td.properties
18368 .iter()
18369 .map(|p| p.name.as_str())
18370 .chain(td.links.iter().map(|l| l.name.as_str()))
18371 .chain(td.multilinks.iter().map(|m| m.name.as_str()))
18372 .chain(td.computed.iter().map(|c| c.name.as_str()))
18373 .map(|candidate| (candidate, strsim::jaro_winkler(name, candidate)))
18374 .filter(|(_, score)| *score >= MIN_SIMILARITY)
18375 .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
18376 .map(|(name, _)| name.to_string())
18377 }
18378
18379 fn pk_returning(td: &TypeDescriptor) -> Vec<IrShapePointer> {
18384 td.properties
18385 .iter()
18386 .filter(|p| p.is_pk)
18387 .map(|p| {
18388 IrShapePointer::Scalar(IrScalarPointer {
18389 implicit_id: false,
18390 marker_offset: None,
18391 alias: p.name.clone(),
18392 column: p.name.clone(),
18393 pg_type: p.pg_type.clone(),
18394 tuple_shape: None,
18395 })
18396 })
18397 .collect()
18398 }
18399}
18400
18401fn casts_to_json(ty: &ast::TypeExpr) -> bool {
18406 matches!(ty.as_named(), Some((module, "json")) if module.is_none_or(|m| m == "std"))
18407}
18408
18409fn has_any_link_props(vals: &IrMultiLinkValues) -> bool {
18412 if !vals.link_props.is_empty() {
18413 return true;
18414 }
18415 match &vals.source {
18416 IrMultiLinkValueSource::Union(a, b) => has_any_link_props(a) || has_any_link_props(b),
18417 _ => false,
18418 }
18419}
18420
18421fn is_empty_set_expr(expr: &Expr) -> bool {
18427 match expr {
18428 Expr::Set(elems) => elems.is_empty(),
18429 Expr::TypeCast(tc) => matches!(&tc.expr, Expr::Set(elems) if elems.is_empty()),
18430 _ => false,
18431 }
18432}
18433
18434fn path_leaf(p: &ast::Path) -> Result<&str, PyQLError> {
18436 match p.steps.as_slice() {
18437 [ast::PathStep::Name(n)] => Ok(n.as_str()),
18438 _ => Err(PyQLError::Type(PyQLTypeError {
18439 message: "expected a simple pointer name in shape element".into(),
18440 position: Position { line: 0, col: 0 },
18441 })),
18442 }
18443}
18444
18445fn type_expr_to_pg(ty: &ast::TypeExpr) -> Result<String, PyQLError> {
18447 let Some((module, bare_name)) = ty.as_named() else {
18448 return Err(PyQLError::Type(PyQLTypeError {
18452 message: "internal error: structural tuple/array type reached type_expr_to_pg".into(),
18453 position: Position { line: 0, col: 0 },
18454 }));
18455 };
18456
18457 if module == Some("pgvector") {
18459 return match bare_name {
18460 "vector" => Ok("vector".to_string()),
18461 other => Err(PyQLError::Type(PyQLTypeError {
18462 message: format!("unknown pgvector type '{other}'; valid types are: vector"),
18463 position: Position { line: 0, col: 0 },
18464 })),
18465 };
18466 }
18467
18468 if module == Some("postgis") {
18470 return match bare_name {
18471 "geometry" => Ok("geometry".to_string()),
18472 "geography" => Ok("geography".to_string()),
18473 "box2d" => Ok("box2d".to_string()),
18474 "box3d" => Ok("box3d".to_string()),
18475 other => Err(PyQLError::Type(PyQLTypeError {
18476 message: format!("unknown postgis type '{other}'; valid types are: geometry, geography, box2d, box3d"),
18477 position: Position { line: 0, col: 0 },
18478 })),
18479 };
18480 }
18481
18482 if module == Some("cal") {
18484 let pg = match bare_name {
18485 "local_datetime" => "timestamp",
18486 "local_date" => "date",
18487 "local_time" => "time",
18488 "relative_duration" | "date_duration" => "interval",
18489 other => {
18490 return Err(PyQLError::Type(PyQLTypeError {
18491 message: format!(
18492 "unknown cal type '{other}'; \
18493 valid types are: local_datetime, local_date, local_time, \
18494 relative_duration, date_duration"
18495 ),
18496 position: Position { line: 0, col: 0 },
18497 }));
18498 }
18499 };
18500 return Ok(pg.to_string());
18501 }
18502
18503 let name = match module {
18504 Some("std") | None => bare_name,
18505 Some(m) => {
18506 return Err(PyQLError::Type(PyQLTypeError {
18507 message: format!("unknown type '{}::{}'", m, bare_name),
18508 position: Position { line: 0, col: 0 },
18509 }));
18510 }
18511 };
18512 Ok(match name {
18513 "str" => "text",
18514 "int16" => "int2",
18515 "int32" => "int4",
18516 "int64" => "int8",
18517 "float32" => "float4",
18518 "float64" => "float8",
18519 "bool" => "boolean",
18520 "uuid" => "uuid",
18521 "bytes" => "bytea",
18522 "json" => "jsonb",
18523 "decimal" => "numeric",
18524 "bigint" => "numeric",
18527 "datetime" => "timestamptz",
18528 "date" => "date",
18529 "time" => "time",
18530 "duration" => "interval",
18531 other => {
18532 return Err(PyQLError::Type(PyQLTypeError {
18533 message: format!("unknown type '{other}'"),
18534 position: Position { line: 0, col: 0 },
18535 }));
18536 }
18537 }
18538 .to_string())
18539}
18540
18541fn ir_is_not_null(expr: IrExpr) -> IrExpr {
18545 IrExpr::FunctionCall(IrFunctionCall {
18546 return_pg_type: None,
18547 schema: None,
18548 name: String::new(),
18549 args: vec![expr],
18550 sql_template: Some("($1 IS NOT NULL)".to_string()),
18551 })
18552}
18553
18554fn describe_arities(overloads: &[&crate::stdlib::FnDescriptor]) -> String {
18558 let mut arities: Vec<usize> = overloads
18561 .iter()
18562 .map(|d| d.params.iter().filter(|p| p.named_only.is_none()).count())
18563 .collect();
18564 arities.sort_unstable();
18565 arities.dedup();
18566 if overloads.iter().any(|d| d.is_variadic()) {
18567 return format!("at least {} argument(s)", arities[0].saturating_sub(1));
18568 }
18569 let list = arities.iter().map(usize::to_string).collect::<Vec<_>>().join(" or ");
18570 format!("{list} argument(s)")
18571}
18572
18573fn describe_args(args: &[IrExpr]) -> String {
18576 args.iter()
18577 .map(|a| infer_ir_type(a).map_or("?", literal_sentinel_to_pg))
18578 .collect::<Vec<_>>()
18579 .join(", ")
18580}
18581
18582fn describe_signatures(overloads: &[&crate::stdlib::FnDescriptor]) -> String {
18585 overloads
18586 .iter()
18587 .map(|d| {
18588 let params = d
18589 .params
18590 .iter()
18591 .map(|p| match p.named_only {
18592 Some(_) => format!("{} := {}", p.keyword(), p.ty.pyql_name()),
18595 None => p.ty.pyql_name(),
18596 })
18597 .collect::<Vec<_>>()
18598 .join(", ");
18599 format!("({params})")
18600 })
18601 .collect::<Vec<_>>()
18602 .join(" or ")
18603}
18604
18605fn bool_literal(expr: &IrExpr) -> Option<bool> {
18608 match expr {
18609 IrExpr::Literal(IrLiteral::Bool(b)) => Some(*b),
18610 _ => None,
18611 }
18612}
18613
18614fn pack_variadic_args(d: &crate::stdlib::FnDescriptor, args: Vec<IrExpr>) -> Vec<IrExpr> {
18623 let named = d.named_count();
18624 let Some(variadic_at) = d.variadic_index().filter(|v| v + 1 < d.params.len()) else {
18625 return args;
18626 };
18627 let Some(absorbed) = args.len().checked_sub(variadic_at + named) else {
18628 return args;
18629 };
18630 let mut packed: Vec<IrExpr> = Vec::with_capacity(variadic_at + 1 + named);
18631 let mut rest = args.into_iter();
18632 packed.extend(rest.by_ref().take(variadic_at));
18633 packed.push(IrExpr::Array(rest.by_ref().take(absorbed).collect()));
18634 packed.extend(rest);
18635 packed
18636}
18637
18638fn params_for_args(d: &crate::stdlib::FnDescriptor, argc: usize) -> impl Iterator<Item = &crate::stdlib::Param> {
18651 let named = d.named_count();
18652 let variadic_at = d.is_variadic().then(|| d.variadic_index()).flatten();
18653 let absorbed = variadic_at.map_or(0, |v| argc.saturating_sub(v + named));
18654 (0..argc).filter_map(move |i| match variadic_at {
18655 Some(v) if i >= v && i < v + absorbed => d.params.get(v),
18656 Some(v) if i >= v => d.params.get(i + 1 - absorbed),
18657 _ => d.params.get(i),
18658 })
18659}
18660
18661fn pylon_type_matches(expr: &IrExpr, ty: &crate::stdlib::PylonType) -> bool {
18662 use crate::stdlib::PylonType as PT;
18663 match ty {
18664 PT::Any | PT::AnyOrderable | PT::AnyPoint => true,
18666 PT::Array(_) => is_array_expr(expr),
18667 PT::Range(_) => matches!(infer_ir_type(expr), Some(t) if t.ends_with("range") && !t.contains("multirange")),
18672 PT::Multirange(_) => matches!(infer_ir_type(expr), Some(t) if t.contains("multirange")),
18673 _ => match (ty.scalar_pg_type(), infer_ir_type(expr)) {
18687 (Some(declared), Some(known)) => types_compatible(known, declared),
18688 (Some(_), None) => false,
18689 (None, _) => true,
18690 },
18691 }
18692}
18693
18694fn expr_returns_set(expr: &Expr) -> bool {
18696 let Expr::FunctionCall(call) = expr else {
18697 return false;
18698 };
18699 crate::stdlib::lookup(call.module.as_deref().unwrap_or("std"), &call.name)
18700 .iter()
18701 .any(|d| d.returns_set())
18702}
18703
18704fn literal_sentinel_to_pg(t: &str) -> &str {
18705 match t {
18706 "__int_literal" => "int8",
18707 "__float_literal" => "float8",
18708 other => other,
18709 }
18710}
18711
18712fn is_array_expr(expr: &IrExpr) -> bool {
18713 match through_coalesce(expr) {
18714 IrExpr::Array(_) | IrExpr::ArrayFromSelect(_) => true,
18715 IrExpr::FunctionCall(f) if f.schema.is_none() => {
18718 let mut overloads = crate::stdlib::registry().iter().filter(|d| d.name == f.name).peekable();
18719 overloads.peek().is_some() && overloads.all(|d| matches!(d.return_type, crate::stdlib::PylonType::Array(_)))
18720 }
18721 other => matches!(infer_ir_type(other), Some(t) if t.ends_with("[]")),
18725 }
18726}
18727
18728fn static_min_payload_bytes(expr: &ast::Expr) -> usize {
18744 use crate::parse::ast::{BinOpKind, Expr, Literal};
18745 match expr {
18746 Expr::Literal(Literal::Str(s)) => s.len(),
18747 Expr::BinOp(op) if matches!(op.op, BinOpKind::Concat) => {
18748 static_min_payload_bytes(&op.left) + static_min_payload_bytes(&op.right)
18749 }
18750 Expr::Shape(sh) if sh.expr.is_none() => {
18751 let mut total = 2;
18755 for (i, el) in sh.elements.iter().enumerate() {
18756 if i > 0 {
18757 total += 1;
18758 }
18759 let name_len = match el.path.steps.last() {
18760 Some(crate::parse::ast::PathStep::Name(n)) => n.len(),
18761 _ => 0,
18762 };
18763 total += name_len + 3;
18764 if let Some(value) = &el.compexpr {
18765 total += static_min_payload_bytes(value);
18766 }
18767 }
18768 total
18769 }
18770 _ => 0,
18771 }
18772}
18773
18774fn expr_to_std_type(expr: &ast::Expr) -> &'static str {
18775 match expr {
18776 ast::Expr::Literal(ast::Literal::Str(_)) => "std::str",
18777 ast::Expr::Literal(ast::Literal::Int(_)) => "std::int64",
18778 ast::Expr::Literal(ast::Literal::Float(_)) => "std::float64",
18779 ast::Expr::Literal(ast::Literal::Bool(_)) => "std::bool",
18780 _ => "anytype",
18781 }
18782}
18783
18784fn named_tuple_type_str(fields: &[(String, ast::Expr)]) -> String {
18785 let inner = fields
18786 .iter()
18787 .map(|(k, v)| format!("{}: {}", k, expr_to_std_type(v)))
18788 .collect::<Vec<_>>()
18789 .join(", ");
18790 format!("tuple<{}>", inner)
18791}
18792
18793fn positional_tuple_type_str(elems: &[ast::Expr]) -> String {
18794 let inner = elems.iter().map(expr_to_std_type).collect::<Vec<_>>().join(", ");
18795 format!("tuple<{}>", inner)
18796}
18797
18798pub(crate) fn infer_ir_type(expr: &IrExpr) -> Option<&str> {
18799 match expr {
18800 IrExpr::ColumnRef { pg_type, .. } => Some(pg_type.as_str()),
18801 IrExpr::TypeCast(tc) => Some(tc.pg_type.as_str()),
18802 IrExpr::FnParam { pg_type, .. } => Some(pg_type.as_str()),
18803 IrExpr::Literal(lit) => Some(match lit {
18804 IrLiteral::Str(_) => "text",
18805 IrLiteral::Int(_) => "__int_literal",
18806 IrLiteral::Float(_) => "__float_literal",
18807 IrLiteral::Bool(_) => "boolean",
18808 }),
18809 IrExpr::EnumLiteral { pg_type, .. } => Some(pg_type.as_str()),
18810 IrExpr::NamedTuple { .. } => Some("jsonb"),
18811 IrExpr::JsonbField { .. } | IrExpr::JsonbIndex { .. } => Some("jsonb"),
18812 IrExpr::GlobalParam { pg_type, .. } => Some(pg_type.as_str()),
18813 IrExpr::CteRef { pg_type, .. } => pg_type.as_deref(),
18816 IrExpr::PathSubquery(ps) => match &ps.result {
18820 IrPathResult::Scalar(e, _) => infer_ir_type(e),
18821 IrPathResult::Object { .. } => None,
18822 },
18823 IrExpr::BinOp(b) if b.op == crate::parse::ast::BinOpKind::Concat => {
18826 infer_ir_type(&b.left).or_else(|| infer_ir_type(&b.right))
18827 }
18828 IrExpr::UnaryOp(u) if u.op == crate::parse::ast::UnaryOpKind::Distinct => infer_ir_type(&u.operand),
18829 IrExpr::UnaryOp(u) if u.op == crate::parse::ast::UnaryOpKind::Minus => infer_ir_type(&u.operand),
18832 IrExpr::UnaryOp(u)
18833 if matches!(
18834 u.op,
18835 crate::parse::ast::UnaryOpKind::Not | crate::parse::ast::UnaryOpKind::Exists
18836 ) =>
18837 {
18838 Some("boolean")
18839 }
18840 IrExpr::BinOp(b) if b.op == crate::parse::ast::BinOpKind::Coalesce => {
18846 infer_ir_type(&b.left).or_else(|| infer_ir_type(&b.right))
18847 }
18848 IrExpr::IfElse(ie) => infer_ir_type(&ie.if_).or_else(|| infer_ir_type(&ie.else_)),
18849 IrExpr::Slice { expr, .. } => infer_ir_type(expr),
18855 IrExpr::CteFieldRef { pg_type, .. } | IrExpr::ForVar { pg_type, .. } => pg_type.as_deref(),
18859 IrExpr::Subscript { expr, is_array, .. } => {
18860 let Some(base) = infer_ir_type(expr) else {
18864 return stdlib_array_element_type(expr);
18865 };
18866 if *is_array { base.strip_suffix("[]") } else { Some(base) }
18867 }
18868 IrExpr::BinOp(b) => arithmetic_result_type(&b.op, infer_ir_type(&b.left)?, infer_ir_type(&b.right)?),
18869 IrExpr::FunctionCall(f) if f.schema.is_none() && f.name == "coalesce" => f.args.iter().find_map(infer_ir_type),
18874 IrExpr::FunctionCall(f) if f.schema.is_none() && matches!(f.name.as_str(), "max" | "min" | "sum") => {
18875 aggregate_result_type(&f.name, infer_ir_type(f.args.first()?)?)
18876 }
18877 IrExpr::AggOverSet {
18878 fn_name, schema: None, ..
18879 } if fn_name == "count" => Some("int8"),
18880 IrExpr::AggOverSet {
18881 fn_name,
18882 schema: None,
18883 elems,
18884 } => aggregate_result_type(fn_name, infer_ir_type(elems.first()?)?),
18885 IrExpr::FunctionCall(f) => f.return_pg_type.as_deref(),
18890 _ => None,
18891 }
18892}
18893
18894fn stdlib_array_element_type(expr: &IrExpr) -> Option<&'static str> {
18899 let IrExpr::FunctionCall(f) = expr else {
18900 return None;
18901 };
18902 if f.schema.is_some() {
18903 return None;
18904 }
18905 let mut element: Option<&'static str> = None;
18906 for descriptor in crate::stdlib::registry().iter().filter(|d| d.name == f.name) {
18907 let crate::stdlib::PylonType::Array(inner) = &descriptor.return_type else {
18908 return None;
18909 };
18910 let scalar = inner.scalar_pg_type()?;
18911 if element.is_some_and(|seen| seen != scalar) {
18912 return None;
18913 }
18914 element = Some(scalar);
18915 }
18916 element
18917}
18918
18919fn temporal_result_type(op: &ast::BinOpKind, left: &str, right: &str) -> Option<&'static str> {
18925 use ast::BinOpKind::*;
18926 let instant = |t: &str| matches!(t, "timestamptz" | "timestamp" | "date" | "time");
18927 match (op, left, right) {
18928 (Sub, l, r) if instant(l) && instant(r) => Some("interval"),
18929 (Sub | Add, l, "interval") if instant(l) => Some(match l {
18930 "timestamptz" => "timestamptz",
18931 "timestamp" => "timestamp",
18932 "date" => "date",
18933 _ => "time",
18934 }),
18935 (Add, "interval", r) if instant(r) => Some(match r {
18936 "timestamptz" => "timestamptz",
18937 "timestamp" => "timestamp",
18938 "date" => "date",
18939 _ => "time",
18940 }),
18941 (Add | Sub, "interval", "interval") => Some("interval"),
18942 _ => None,
18943 }
18944}
18945
18946fn arithmetic_result_type(op: &ast::BinOpKind, left: &str, right: &str) -> Option<&'static str> {
18949 use ast::BinOpKind::*;
18950 if !matches!(op, Add | Sub | Mul | Div | FloorDiv | Mod | Pow) {
18951 return None;
18952 }
18953 let (left, right) = (literal_sentinel_to_pg(left), literal_sentinel_to_pg(right));
18954 if let Some(temporal) = temporal_result_type(op, left, right) {
18955 return Some(temporal);
18956 }
18957 let int = |t: &str| INT_TYPES.contains(&t);
18958 let float = |t: &str| FLOAT_TYPES.contains(&t);
18959 let numeric = |t: &str| NUMERIC_TYPES.contains(&t);
18960 if int(left) && int(right) {
18961 return Some(if matches!(op, Div | Pow) {
18962 "float8"
18963 } else if left == "int8" || right == "int8" {
18964 "int8"
18965 } else if left == "int4" || right == "int4" {
18966 "int4"
18967 } else {
18968 "int2"
18969 });
18970 }
18971 if (float(left) || int(left)) && (float(right) || int(right)) {
18972 return Some(if left == "float4" && right == "float4" {
18973 "float4"
18974 } else {
18975 "float8"
18976 });
18977 }
18978 if (numeric(left) || int(left)) && (numeric(right) || int(right)) {
18979 return Some("numeric");
18980 }
18981 None
18982}
18983
18984fn aggregate_result_type<'a>(name: &str, element: &'a str) -> Option<&'a str> {
18986 let element = literal_sentinel_to_pg(element);
18987 match name {
18988 "max" | "min" => Some(element),
18989 "sum" if INT_TYPES.contains(&element) => Some("int8"),
18990 "sum" if FLOAT_TYPES.contains(&element) || NUMERIC_TYPES.contains(&element) => Some(element),
18991 _ => None,
18992 }
18993}
18994
18995fn true_division_operand(op: &ast::BinOpKind, left: IrExpr, right: &IrExpr) -> IrExpr {
18998 let integer = |expr: &IrExpr| infer_ir_type(expr).is_some_and(|t| INT_TYPES.contains(&literal_sentinel_to_pg(t)));
18999 if *op != ast::BinOpKind::Div || !integer(&left) || !integer(right) {
19000 return left;
19001 }
19002 IrExpr::TypeCast(Box::new(IrTypeCast {
19003 expr: left,
19004 pg_type: "float8".to_string(),
19005 tuple_shape: None,
19006 }))
19007}
19008
19009fn range_ctor_for_pg_type(pg_type: &str) -> Option<&'static str> {
19016 match pg_type {
19017 "int2" | "int4" | "int8" | "__int_literal" => Some("int8range"),
19018 "numeric" | "__float_literal" => Some("numrange"),
19019 "timestamp" => Some("tsrange"),
19020 "timestamptz" => Some("tstzrange"),
19021 "date" => Some("daterange"),
19022 _ => None,
19023 }
19024}
19025
19026fn multirange_ctor_for_range_ctor(range_ctor: &str) -> Option<&'static str> {
19029 match range_ctor {
19030 "int8range" => Some("int8multirange"),
19031 "numrange" => Some("nummultirange"),
19032 "tsrange" => Some("tsmultirange"),
19033 "tstzrange" => Some("tstzmultirange"),
19034 "daterange" => Some("datemultirange"),
19035 _ => None,
19036 }
19037}
19038
19039const INT_TYPES: &[&str] = &["int2", "int4", "int8", "__int_literal"];
19040const FLOAT_TYPES: &[&str] = &["float4", "float8", "__float_literal"];
19041const NUMERIC_TYPES: &[&str] = &["numeric"];
19045
19046pub(crate) fn types_compatible(a: &str, b: &str) -> bool {
19056 if a == b {
19057 return true;
19058 }
19059 let a_int = INT_TYPES.contains(&a);
19060 let b_int = INT_TYPES.contains(&b);
19061 if a_int && b_int {
19062 return true;
19063 }
19064 let a_float = FLOAT_TYPES.contains(&a);
19065 let b_float = FLOAT_TYPES.contains(&b);
19066 if a_float && b_float {
19067 return true;
19068 }
19069 let a_numeric = NUMERIC_TYPES.contains(&a);
19070 let b_numeric = NUMERIC_TYPES.contains(&b);
19071 if a_numeric && b_numeric {
19072 return true;
19073 }
19074 (a_int && b_float) || (a_float && b_int) || (a_int && b_numeric) || (a_numeric && b_int)
19075}
19076
19077fn datetime_arithmetic_compatible(op: &ast::BinOpKind, a: &str, b: &str) -> bool {
19089 if !matches!(op, ast::BinOpKind::Add | ast::BinOpKind::Sub) {
19090 return false;
19091 }
19092 matches!(
19093 (a, b),
19094 ("timestamptz", "interval")
19095 | ("interval", "timestamptz")
19096 | ("timestamp", "interval")
19097 | ("interval", "timestamp")
19098 | ("date", "interval")
19099 | ("interval", "date")
19100 | ("time", "interval")
19101 | ("interval", "time")
19102 )
19103}
19104
19105fn collect_search_enqueue(td: &TypeDescriptor, type_name: &str, operation: &'static str) -> Vec<SearchEnqueueInfo> {
19110 td.search_indexes
19111 .iter()
19112 .filter(|si| si.backend == SearchBackend::OpenSearch || si.backend == SearchBackend::Meilisearch)
19113 .map(|si| SearchEnqueueInfo {
19114 type_name: type_name.to_string(),
19115 index_name: si.index_name.clone(),
19116 operation,
19117 backend: si.backend.clone(),
19118 })
19119 .collect()
19120}
19121
19122pub fn pg_type_to_pyql(pg: &str) -> &str {
19135 match pg {
19136 "text" | "varchar" => "std::str",
19137 "uuid" => "std::uuid",
19138 "int2" => "std::int16",
19139 "int4" => "std::int32",
19140 "int8" => "std::int64",
19141 "float4" => "std::float32",
19142 "float8" => "std::float64",
19143 "boolean" => "std::bool",
19144 "numeric" => "std::decimal",
19145 "timestamptz" => "std::datetime",
19146 "timestamp" => "cal::local_datetime",
19147 "date" => "cal::local_date",
19148 "time" => "cal::local_time",
19149 "interval" => "std::duration",
19150 "bytea" => "std::bytes",
19151 "jsonb" => "std::json",
19152 "__int_literal" => "std::int64",
19153 "__float_literal" => "std::float64",
19154 other => other,
19155 }
19156}