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 ctes: vec![],
92 global_ctes: c.global_ctes,
93 warnings: c.warnings,
94 uses_globals_arg: c.used_globals_arg,
95 });
96 }
97 if let Some(ir) = try_compile_fts_with_pattern(&mut c, w)? {
99 return Ok(IrOutput {
100 subtype_fanouts: c.subtype_fanouts(),
101 stmt: ir,
102 params: c.params,
103 ctes: vec![],
104 global_ctes: c.global_ctes,
105 warnings: c.warnings,
106 uses_globals_arg: c.used_globals_arg,
107 });
108 }
109
110 let mut cte_defs = vec![];
111 for alias in &w.aliases {
112 if let Some(declared) = declared_pointers_of(&alias.expr) {
113 c.cte_declared_pointers.insert(alias.name.clone(), declared);
114 }
115 if c.bind_group(&alias.name, &alias.expr)? {
116 continue;
117 }
118 let ir_stmt = compile_cte_binding(&mut c, &alias.expr)?;
119 let sql_name = c.claim_cte_sql_name(&alias.name);
120 let type_name = c.register_cte(&alias.name, &ir_stmt);
121 cte_defs.append(&mut c.hoisted_ctes);
124 cte_defs.push(IrCteDef {
125 name: sql_name,
126 stmt: ir_stmt,
127 type_name,
128 correlated_to: None,
129 });
130 }
131 let main = c.compile_stmt(&w.stmt)?;
132 (cte_defs, main)
133 } else {
134 (vec![], c.compile_stmt(stmt)?)
135 };
136
137 let mut ctes = ctes;
138 ctes.extend(std::mem::take(&mut c.hoisted_ctes));
139 Ok(IrOutput {
140 subtype_fanouts: c.subtype_fanouts(),
141 stmt: ir,
142 params: c.params,
143 ctes,
144 global_ctes: c.global_ctes,
145 warnings: c.warnings,
146 uses_globals_arg: c.used_globals_arg,
147 })
148}
149
150fn try_compile_vs_with_pattern(c: &mut Compiler<'_>, w: &ast::WithStmt) -> Result<Option<IrStmt>, PyQLError> {
153 if w.aliases.len() != 1 {
155 return Ok(None);
156 }
157 let alias_def = &w.aliases[0];
158 let fc = match &alias_def.expr {
159 Expr::FunctionCall(fc) => fc,
160 _ => return Ok(None),
161 };
162 if fc.module.as_deref() != Some("vector") || fc.name != "search" {
163 return Ok(None);
164 }
165
166 let select_stmt = match w.stmt.as_ref() {
168 Stmt::Select(s) => s,
169 _ => return Ok(None),
170 };
171 let (elements, result_inner): (&[ast::ShapeElement], &Expr) = match &select_stmt.result {
173 Expr::Shape(sh) => {
174 let inner = sh.expr.as_ref().unwrap_or(&select_stmt.result);
175 (sh.elements.as_slice(), inner)
176 }
177 other => (&[], other),
178 };
179 match result_inner {
181 Expr::Path(p) if !p.partial && p.steps.len() == 1 => {
182 if let ast::PathStep::Name(n) = &p.steps[0] {
183 if n != &alias_def.name {
184 return Ok(None);
185 }
186 } else {
187 return Ok(None);
188 }
189 }
190 _ => return Ok(None),
191 }
192
193 if let Some(ir) = c.try_compile_vector_search(fc, elements, select_stmt)? {
194 Ok(Some(IrStmt::VectorSearch(ir)))
195 } else {
196 Ok(None)
197 }
198}
199
200fn try_compile_fts_with_pattern(c: &mut Compiler<'_>, w: &ast::WithStmt) -> Result<Option<IrStmt>, PyQLError> {
201 if w.aliases.len() != 1 {
202 return Ok(None);
203 }
204 let alias_def = &w.aliases[0];
205 let fc = match &alias_def.expr {
206 Expr::FunctionCall(fc) => fc,
207 _ => return Ok(None),
208 };
209 if fc.module.as_deref() != Some("fts") || fc.name != "search" {
210 return Ok(None);
211 }
212
213 let select_stmt = match w.stmt.as_ref() {
214 Stmt::Select(s) => s,
215 _ => return Ok(None),
216 };
217 let (elements, result_inner): (&[ast::ShapeElement], &Expr) = match &select_stmt.result {
218 Expr::Shape(sh) => {
219 let inner = sh.expr.as_ref().unwrap_or(&select_stmt.result);
220 (sh.elements.as_slice(), inner)
221 }
222 other => (&[], other),
223 };
224 match result_inner {
225 Expr::Path(p) if !p.partial && p.steps.len() == 1 => {
226 if let ast::PathStep::Name(n) = &p.steps[0] {
227 if n != &alias_def.name {
228 return Ok(None);
229 }
230 } else {
231 return Ok(None);
232 }
233 }
234 _ => return Ok(None),
235 }
236
237 if let Some(ir) = c.try_compile_fts_search(fc, elements, select_stmt)? {
238 Ok(Some(IrStmt::FtsSearch(ir)))
239 } else {
240 Ok(None)
241 }
242}
243
244fn subject_default_insert(handler: &str, pointer: &str) -> Option<Expr> {
256 fn holds_insert(expr: &Expr) -> bool {
257 match expr {
258 Expr::SubQuery(stmt) => match stmt.as_ref() {
259 Stmt::Insert(_) => true,
260 Stmt::Select(sel) => holds_insert(&sel.result),
261 _ => false,
262 },
263 _ => false,
264 }
265 }
266 let Ok(Expr::BinOp(b)) = crate::parse::parse_pointer_expr(handler) else {
267 return None;
268 };
269 let reads_itself = matches!(&b.left, Expr::Path(p) if matches!(p.steps.as_slice(),
270 [ast::PathStep::Name(subject), ast::PathStep::Name(name)] if subject == "__subject__" && name == pointer));
271 (matches!(b.op, ast::BinOpKind::Coalesce) && reads_itself && holds_insert(&b.right)).then_some(b.right)
272}
273
274fn limits_to_one(sel: Option<&ast::SelectStmt>) -> bool {
276 sel.is_some_and(|sel| matches!(sel.limit, Some(Expr::Literal(ast::Literal::Int(1)))))
277}
278
279fn selects_at_most_one(sel: &ast::SelectStmt, td: &TypeDescriptor) -> bool {
282 fn pins_exclusive(expr: &Expr, td: &TypeDescriptor) -> bool {
283 let Expr::BinOp(b) = expr else { return false };
284 match b.op {
285 ast::BinOpKind::And => pins_exclusive(&b.left, td) || pins_exclusive(&b.right, td),
286 ast::BinOpKind::Eq => [&b.left, &b.right].into_iter().any(|side| {
287 matches!(side, Expr::Path(p) if p.partial
288 && matches!(p.steps.as_slice(), [ast::PathStep::Name(name)]
289 if td.properties.iter().any(|d| &d.name == name && (d.is_exclusive || d.is_pk))))
290 }),
291 _ => false,
292 }
293 }
294 matches!(sel.limit, Some(Expr::Literal(ast::Literal::Int(1))))
295 || sel.filter.as_ref().is_some_and(|f| pins_exclusive(f, td))
296}
297
298fn guarded_object_branch(expr: &Expr) -> Option<Expr> {
304 fn reads_relative(expr: &Expr) -> bool {
305 match expr {
306 Expr::Path(p) => p.partial,
307 Expr::Literal(_) | Expr::Parameter(_) | Expr::Global(_) => false,
308 Expr::BinOp(b) => reads_relative(&b.left) || reads_relative(&b.right),
309 Expr::UnaryOp(u) => reads_relative(&u.operand),
310 Expr::TypeCast(c) => reads_relative(&c.expr),
311 Expr::FunctionCall(f) => f.args.iter().chain(f.kwargs.iter().map(|(_, v)| v)).any(reads_relative),
312 Expr::IfElse(ie) => {
313 reads_relative(&ie.if_expr) || reads_relative(&ie.condition) || reads_relative(&ie.else_expr)
314 }
315 _ => true,
316 }
317 }
318 fn empty(expr: &Expr) -> bool {
319 match expr {
320 Expr::Set(items) => items.is_empty(),
321 Expr::TypeCast(c) => matches!(&c.expr, Expr::Set(items) if items.is_empty()),
322 _ => false,
323 }
324 }
325 let (ie, outer_shape) = match expr {
326 Expr::IfElse(ie) => (ie.as_ref(), None),
327 Expr::Shape(sh) => match sh.expr.as_ref()? {
328 Expr::IfElse(ie) => (ie.as_ref(), Some(sh.elements.clone())),
329 _ => return None,
330 },
331 _ => return None,
332 };
333 if reads_relative(&ie.condition) {
334 return None;
335 }
336 let (branch, condition) = match (empty(&ie.if_expr), empty(&ie.else_expr)) {
337 (false, true) => (&ie.if_expr, ie.condition.clone()),
338 (true, false) => (
339 &ie.else_expr,
340 Expr::UnaryOp(Box::new(ast::UnaryOp {
341 op: ast::UnaryOpKind::Not,
342 operand: ie.condition.clone(),
343 })),
344 ),
345 _ => return None,
346 };
347 let (branch, branch_shape) = match branch {
348 Expr::Shape(sh) => (sh.expr.as_ref()?, Some(sh.elements.clone())),
349 other => (other, None),
350 };
351 let mut select = match branch {
352 Expr::Path(p) if p.partial => ast::SelectStmt {
353 result: branch.clone(),
354 filter: None,
355 order_by: vec![],
356 offset: None,
357 limit: None,
358 lock: None,
359 },
360 Expr::SubQuery(stmt) => match stmt.as_ref() {
361 Stmt::Select(sel) if matches!(&sel.result, Expr::Path(p) if p.partial) => sel.clone(),
362 _ => return None,
363 },
364 _ => return None,
365 };
366 select.filter = Some(match select.filter.take() {
367 Some(filter) => Expr::BinOp(Box::new(ast::BinOp {
368 left: filter,
369 op: ast::BinOpKind::And,
370 right: condition,
371 })),
372 None => condition,
373 });
374 let guarded = Expr::SubQuery(Box::new(Stmt::Select(select)));
375 Some(match outer_shape.or(branch_shape) {
376 Some(elements) => Expr::Shape(Box::new(ast::ShapeExpr {
377 expr: Some(guarded),
378 elements,
379 marker_offset: None,
380 })),
381 None => guarded,
382 })
383}
384
385fn per_element_of_one_multilink(condition: &Expr, td: &TypeDescriptor) -> Option<(Vec<ast::PathStep>, Expr)> {
392 fn prefix_len(steps: &[ast::PathStep]) -> Option<usize> {
395 match steps {
396 [ast::PathStep::Name(_), ..] => Some(1),
397 [ast::PathStep::Backlink(_), ast::PathStep::TypeIntersection(_), ..] => Some(2),
398 _ => None,
399 }
400 }
401 fn rewrite(expr: &Expr, link: &mut Option<Vec<ast::PathStep>>) -> Option<Expr> {
402 Some(match expr {
403 Expr::Path(p) if p.partial => {
404 let split = prefix_len(&p.steps)?;
405 let prefix = p.steps[..split].to_vec();
406 if *link.get_or_insert_with(|| prefix.clone()) != prefix {
407 return None;
408 }
409 let rest = p.steps[split..].to_vec();
413 if rest.is_empty() {
414 return None;
415 }
416 Expr::Path(ast::Path {
417 steps: rest,
418 partial: true,
419 })
420 }
421 Expr::Path(_) | Expr::Literal(_) | Expr::Parameter(_) | Expr::Global(_) => expr.clone(),
422 Expr::BinOp(b) => Expr::BinOp(Box::new(ast::BinOp {
423 left: rewrite(&b.left, link)?,
424 op: b.op.clone(),
425 right: match (&b.op, &b.right) {
431 (ast::BinOpKind::In | ast::BinOpKind::NotIn, set @ Expr::Set(_)) => set.clone(),
432 (_, other) => rewrite(other, link)?,
433 },
434 })),
435 Expr::UnaryOp(u) => Expr::UnaryOp(Box::new(ast::UnaryOp {
436 op: u.op.clone(),
437 operand: rewrite(&u.operand, link)?,
438 })),
439 Expr::TypeCast(c) => Expr::TypeCast(Box::new(ast::TypeCast {
440 expr: rewrite(&c.expr, link)?,
441 ty: c.ty.clone(),
442 })),
443 Expr::TypeIs { expr, ty } => Expr::TypeIs {
444 expr: Box::new(rewrite(expr, link)?),
445 ty: ty.clone(),
446 },
447 Expr::FunctionCall(f)
451 if crate::stdlib::lookup(f.module.as_deref().unwrap_or("std"), &f.name)
452 .iter()
453 .any(|d| d.returns_set()) =>
454 {
455 return None;
456 }
457 Expr::FunctionCall(f) => Expr::FunctionCall(ast::FunctionCall {
458 module: f.module.clone(),
459 name: f.name.clone(),
460 args: f.args.iter().map(|a| rewrite(a, link)).collect::<Option<_>>()?,
461 kwargs: f
462 .kwargs
463 .iter()
464 .map(|(k, v)| Some((k.clone(), rewrite(v, link)?)))
465 .collect::<Option<_>>()?,
466 }),
467 _ => return None,
468 })
469 }
470 let mut link = None;
471 let element_condition = rewrite(condition, &mut link)?;
472 let link = link?;
473 let many = match link.first()? {
474 ast::PathStep::Name(name) => td.multilinks.iter().any(|m| &m.name == name),
475 ast::PathStep::Backlink(_) => true,
476 _ => false,
477 };
478 many.then_some((link, element_condition))
479}
480
481fn flatten_shape_subject(expr: &Expr) -> Option<Expr> {
491 let Expr::Shape(outer) = expr else { return None };
492 match outer.expr.as_ref()? {
493 Expr::SubQuery(stmt) => {
494 let Stmt::Select(sel) = stmt.as_ref() else { return None };
495 let bare = sel.filter.is_none()
496 && sel.order_by.is_empty()
497 && sel.offset.is_none()
498 && sel.limit.is_none()
499 && sel.lock.is_none();
500 if !bare || !matches!(sel.result, Expr::IfElse(_) | Expr::Union(_, _) | Expr::Shape(_)) {
501 return None;
502 }
503 let mut flat = outer.as_ref().clone();
504 flat.expr = Some(sel.result.clone());
505 Some(Expr::Shape(Box::new(flat)))
506 }
507 Expr::Shape(inner) => {
508 let elements = outer
509 .elements
510 .iter()
511 .map(|el| {
512 let declared = match (&el.compexpr, el.path.steps.as_slice()) {
513 (None, [ast::PathStep::Name(name)]) => inner.elements.iter().find(|d| {
514 d.compexpr.is_some()
515 && matches!(d.path.steps.as_slice(), [ast::PathStep::Name(n)] if n == name)
516 }),
517 _ => None,
518 };
519 match declared {
520 Some(d) => ShapeElement {
521 nested: el.nested.clone().or_else(|| d.nested.clone()),
522 filter: el.filter.clone().or_else(|| d.filter.clone()),
523 order_by: if el.order_by.is_empty() {
524 d.order_by.clone()
525 } else {
526 el.order_by.clone()
527 },
528 offset: el.offset.clone().or_else(|| d.offset.clone()),
529 limit: el.limit.clone().or_else(|| d.limit.clone()),
530 ..d.clone()
531 },
532 None => el.clone(),
533 }
534 })
535 .collect();
536 Some(Expr::Shape(Box::new(ast::ShapeExpr {
537 expr: inner.expr.clone(),
538 elements,
539 marker_offset: inner.marker_offset,
540 })))
541 }
542 _ => None,
543 }
544}
545
546fn group_key_binding(name: &str) -> String {
549 format!("<group key {name}>")
550}
551
552fn and_conditions(filter: Option<IrExpr>, extra: Vec<IrExpr>) -> Option<IrExpr> {
553 extra.into_iter().fold(filter, |acc, cond| match acc {
554 Some(existing) => Some(IrExpr::BinOp(Box::new(IrBinOp {
555 left: existing,
556 op: ast::BinOpKind::And,
557 right: cond,
558 }))),
559 None => Some(cond),
560 })
561}
562
563const MAX_COMPUTED_SPLICES: usize = 32;
564
565fn aggregate_over_nothing_sql(name: &str) -> Option<&'static str> {
570 match name {
571 "sum" => Some("0"),
572 "any" => Some("false"),
573 "all" => Some("true"),
574 "array_agg" => Some("'{}'"),
575 _ => None,
576 }
577}
578
579fn aggregate_over_nothing(name: &str, aggregate: IrExpr) -> IrExpr {
580 let Some(value) = aggregate_over_nothing_sql(name) else {
581 return aggregate;
582 };
583 IrExpr::FunctionCall(IrFunctionCall {
584 return_pg_type: None,
585 schema: None,
586 name: "coalesce".to_string(),
587 args: vec![aggregate, IrExpr::RawSql(value.to_string())],
588 sql_template: None,
589 })
590}
591
592fn set_walk_as_scalar(expr: IrExpr) -> IrExpr {
608 match expr {
609 IrExpr::ArrayFromSelect(source) => match *source {
610 IrArraySource::PathSelect(ps) => IrExpr::PathSubquery(ps),
611 other => IrExpr::ArrayFromSelect(Box::new(other)),
612 },
613 other => other,
614 }
615}
616
617fn yields_array(expr: &IrExpr) -> bool {
618 match expr {
619 IrExpr::ArrayFromSelect(_) => true,
620 IrExpr::IfElse(ie) => {
621 (yields_array(&ie.if_) || matches!(ie.if_, IrExpr::Null))
622 && (yields_array(&ie.else_) || matches!(ie.else_, IrExpr::Null))
623 && !(matches!(ie.if_, IrExpr::Null) && matches!(ie.else_, IrExpr::Null))
624 }
625 _ => false,
626 }
627}
628
629fn through_coalesce(expr: &IrExpr) -> &IrExpr {
633 match expr {
634 IrExpr::FunctionCall(f) if f.schema.is_none() && f.name == "coalesce" => {
635 f.args.first().map(through_coalesce).unwrap_or(expr)
636 }
637 other => other,
638 }
639}
640
641fn ir_value_type_name(expr: &IrExpr) -> String {
642 ir_value_type(expr).unwrap_or_default()
643}
644
645fn ir_value_type(expr: &IrExpr) -> Option<String> {
650 let expr = through_coalesce(expr);
651 match expr {
652 IrExpr::Array(elements) => {
653 let element = elements.first().and_then(infer_ir_type).unwrap_or("text");
654 Some(format!("{}[]", literal_sentinel_to_pg(element)))
655 }
656 IrExpr::FunctionCall(f) if f.schema.is_none() && f.name == "array_agg" => {
659 let element = f.args.first().and_then(infer_ir_type)?;
660 Some(format!("{}[]", literal_sentinel_to_pg(element)))
661 }
662 IrExpr::PathSubquery(ps) => match &ps.result {
665 IrPathResult::Scalar(inner, _) => ir_value_type(inner),
666 IrPathResult::Object { .. } => None,
667 },
668 IrExpr::ArrayFromSelect(source) => array_source_element_type(source).map(|element| format!("{element}[]")),
669 IrExpr::IfElse(ie) => ir_value_type(&ie.if_).or_else(|| ir_value_type(&ie.else_)),
672 other => infer_ir_type(other).map(|t| t.to_string()),
673 }
674}
675
676fn array_source_element_type(source: &IrArraySource) -> Option<String> {
679 match source {
680 IrArraySource::Select(sel) => match sel.rows.as_slice() {
681 [IrRowSource::Bound { shape, .. }] => match shape.first() {
682 Some(IrShapePointer::Scalar(scalar)) => Some(scalar.pg_type.clone()),
685 _ => Some("uuid".to_string()),
686 },
687 _ => None,
688 },
689 IrArraySource::PathSelect(ps) => match &ps.result {
690 IrPathResult::Scalar(expr, _) => infer_ir_type(expr).map(|t| literal_sentinel_to_pg(t).to_string()),
691 IrPathResult::Object { .. } => None,
692 },
693 _ => None,
694 }
695}
696
697fn declared_pointers_of(expr: &Expr) -> Option<Vec<ShapeElement>> {
699 if let Expr::IfElse(ie) = expr {
701 return declared_pointers_of(&ie.if_expr).or_else(|| declared_pointers_of(&ie.else_expr));
702 }
703 let Expr::SubQuery(stmt) = expr else {
704 return None;
705 };
706 let shape = match innermost_select(stmt)? {
707 ast::SelectStmt {
708 result: Expr::Shape(sh),
709 ..
710 } => sh,
711 _ => return None,
712 };
713 let declared: Vec<ShapeElement> = shape
714 .elements
715 .iter()
716 .filter(|el| el.compexpr.is_some())
717 .cloned()
718 .collect();
719 (!declared.is_empty()).then_some(declared)
720}
721
722fn innermost_select(stmt: &Stmt) -> Option<&ast::SelectStmt> {
724 match stmt {
725 Stmt::Select(sel) => Some(sel),
726 Stmt::With(w) => innermost_select(&w.stmt),
727 _ => None,
728 }
729}
730
731fn cte_stmt_type(stmt: &IrStmt) -> String {
732 match stmt {
733 IrStmt::Insert(ins) => ins.target.type_name.clone(),
734 IrStmt::Update(upd) => upd.target.type_name.clone(),
735 IrStmt::Delete(del) => del.target.type_name.clone(),
736 IrStmt::Select(sel) => match sel.rows.first() {
737 Some(IrRowSource::Bound { source, .. }) => source.type_name.clone(),
738 Some(IrRowSource::Free(IrFreeExpr::Scalar(expr))) => ir_value_type_name(expr),
742 _ => String::new(),
743 },
744 IrStmt::PathSelect(ps) => match &ps.result {
749 IrPathResult::Scalar(expr, _) => ir_value_type_name(expr),
750 IrPathResult::Object { type_name, .. } => type_name.clone(),
751 },
752 IrStmt::For(f) => cte_stmt_type(&f.body),
753 IrStmt::ScalarUnion(branches) => branches.first().map(cte_stmt_type).unwrap_or_default(),
754 IrStmt::Group(g) => g.source.type_name.clone(),
755 IrStmt::FunctionSelect(fs) => fs.type_name.clone(),
756 IrStmt::VectorSearch(vs) => format!("__vs__{}", vs.source.type_name),
757 IrStmt::FtsSearch(fs) => format!("__fts__{}", fs.source.type_name),
758 }
759}
760
761fn compile_cte_binding(c: &mut Compiler<'_>, expr: &Expr) -> Result<IrStmt, PyQLError> {
764 let stmt = compile_cte_binding_stmt(c, expr)?;
765 Ok(hoist_binding_dml(c, stmt))
766}
767
768fn hoist_binding_dml(c: &mut Compiler<'_>, stmt: IrStmt) -> IrStmt {
775 let IrStmt::Select(mut select) = stmt else {
776 return stmt;
777 };
778 let Some(dml) = select
779 .dml_source
780 .take_if(|dml| matches!(dml.as_ref(), IrStmt::Insert(_) | IrStmt::Update(_) | IrStmt::Delete(_)))
781 else {
782 return IrStmt::Select(select);
783 };
784 let cte_name = c.fresh_nested_cte_name();
785 for row in &mut select.rows {
786 if let IrRowSource::Bound { source, .. } = row {
787 source.table = format!("@cte:{cte_name}");
788 source.poly = None;
791 }
792 }
793 c.hoisted_ctes.push(IrCteDef {
794 name: cte_name,
795 type_name: cte_stmt_type(&dml),
796 stmt: *dml,
797 correlated_to: None,
798 });
799 IrStmt::Select(select)
800}
801
802fn compile_cte_binding_stmt(c: &mut Compiler<'_>, expr: &Expr) -> Result<IrStmt, PyQLError> {
803 if let Expr::SubQuery(s) = expr {
804 let stmt = c.compile_stmt(s)?;
805 if let IrStmt::Group(grp) = &stmt
806 && !matches!(grp.output, IrGroupOutput::Elements)
807 {
808 return Err(c.type_err("a `group` cannot be bound in a `with` except to iterate it with `for`"));
809 }
810 return Ok(stmt);
811 }
812 let expr = match expr {
816 Expr::BinOp(b)
817 if b.op == ast::BinOpKind::Coalesce
818 && matches!(&b.right, Expr::SubQuery(s) if matches!(s.as_ref(), Stmt::Insert(_) | Stmt::Update(_) | Stmt::Delete(_))) =>
819 {
820 &Expr::IfElse(Box::new(ast::IfElse {
821 if_expr: b.left.clone(),
822 condition: Expr::UnaryOp(Box::new(ast::UnaryOp {
823 op: ast::UnaryOpKind::Exists,
824 operand: b.left.clone(),
825 })),
826 else_expr: b.right.clone(),
827 }))
828 }
829 other => other,
830 };
831 let fake_sel = ast::SelectStmt {
834 result: expr.clone(),
835 filter: None,
836 order_by: vec![],
837 offset: None,
838 limit: None,
839 lock: None,
840 };
841 c.compile_stmt(&Stmt::Select(fake_sel))
842}
843
844pub fn functions_needing_globals(schema: &SchemaDescriptor) -> &std::collections::HashSet<String> {
852 schema
853 .functions_needing_globals
854 .get_or_init(|| find_functions_needing_globals(schema))
855}
856
857fn find_functions_needing_globals(schema: &SchemaDescriptor) -> std::collections::HashSet<String> {
858 let mut needs: std::collections::HashSet<String> = std::collections::HashSet::new();
859 loop {
860 let mut changed = false;
861 for fd in &schema.functions {
862 let qualified = format!("{}::{}", fd.module, fd.name);
863 if needs.contains(&qualified) {
864 continue;
865 }
866 if let Ok(out) = compile_fn_body_with(fd, schema, &needs)
867 && out.uses_globals_arg
868 {
869 needs.insert(qualified);
870 changed = true;
871 }
872 }
873 if !changed {
874 return needs;
875 }
876 }
877}
878
879pub fn compile_fn_body(
884 fn_desc: &crate::schema::FunctionDescriptor,
885 schema: &SchemaDescriptor,
886) -> Result<super::IrOutput, crate::error::PyQLError> {
887 compile_fn_body_with(fn_desc, schema, functions_needing_globals(schema))
888}
889
890pub fn compile_fn_body_with(
893 fn_desc: &crate::schema::FunctionDescriptor,
894 schema: &SchemaDescriptor,
895 fns_needing_globals: &std::collections::HashSet<String>,
896) -> Result<super::IrOutput, crate::error::PyQLError> {
897 use crate::parse;
898 use crate::parse::ast::Stmt;
899
900 let body = fn_desc.body.trim().to_string();
901 let starts_a_stmt = ["select", "with", "for", "group", "insert", "update", "delete"]
905 .iter()
906 .any(|keyword| {
907 body.len() > keyword.len()
908 && body[..keyword.len()].eq_ignore_ascii_case(keyword)
909 && !body.as_bytes()[keyword.len()].is_ascii_alphanumeric()
910 && body.as_bytes()[keyword.len()] != b'_'
911 });
912 let body = if starts_a_stmt {
913 body
914 } else {
915 format!("select {}", body)
916 };
917
918 let ast = parse::parse(&body)?;
919 let mut c = Compiler::new(schema);
920 c.in_fn_body = true;
921 c.fns_needing_globals = Some(fns_needing_globals.clone());
922 for p in &fn_desc.params {
923 c.fn_params.insert(p.name.clone(), p.pg_type.clone());
924 }
925
926 let (ctes, ir) = if let Stmt::With(w) = &ast {
927 let mut cte_defs = vec![];
928 for alias in &w.aliases {
929 let ir_stmt = compile_cte_binding(&mut c, &alias.expr)?;
930 let type_name = cte_stmt_type(&ir_stmt);
931 c.cte_types.insert(alias.name.clone(), type_name.clone());
932 c.note_cte_cardinality(&alias.name, &ir_stmt);
933 cte_defs.push(super::IrCteDef {
934 name: alias.name.clone(),
935 stmt: ir_stmt,
936 type_name,
937 correlated_to: None,
938 });
939 }
940 let main = c.compile_stmt(&w.stmt)?;
941 (cte_defs, main)
942 } else {
943 (vec![], c.compile_stmt(&ast)?)
944 };
945
946 let mut ctes = ctes;
947 ctes.extend(std::mem::take(&mut c.hoisted_ctes));
948 Ok(super::IrOutput {
949 subtype_fanouts: c.subtype_fanouts(),
950 stmt: ir,
951 params: c.params,
952 ctes,
953 global_ctes: c.global_ctes,
954 warnings: c.warnings,
955 uses_globals_arg: c.used_globals_arg,
956 })
957}
958
959pub struct RewriteAssignment {
974 pub pointer: String,
975 pub column: String,
976 pub ir: IrExpr,
977 pub sql: String,
978 pub reads_a_multi_link: bool,
983}
984
985pub fn compile_rewrite_assignments(
991 type_name: &str,
992 on_mask: u8,
993 schema: &SchemaDescriptor,
994) -> Result<Vec<RewriteAssignment>, crate::error::PyQLError> {
995 let mut c = Compiler::new(schema);
996 c.in_fn_body = true;
997 let owner_td = c.resolve_type(type_name)?;
998 c.special_anchors
999 .insert("__new__".to_string(), (owner_td, "NEW".to_string()));
1000 if on_mask & 1 == 0 {
1001 c.special_anchors
1002 .insert("__old__".to_string(), (owner_td, "OLD".to_string()));
1003 }
1004 let pointers = owner_td
1005 .properties
1006 .iter()
1007 .map(|p| (p.name.clone(), p.name.clone(), &p.rewrites))
1008 .chain(
1009 owner_td
1010 .links
1011 .iter()
1012 .filter(|l| !l.is_junction_backed())
1013 .map(|l| (l.name.clone(), format!("{}_id", l.name), &l.rewrites)),
1014 );
1015 let fanouts = c.subtype_fanouts();
1016 let mut out = Vec::new();
1017 for (name, column, rewrites) in pointers {
1018 for rw in rewrites.iter().filter(|rw| rw.on & on_mask != 0) {
1019 if on_mask == 1 && subject_default_insert(&rw.handler, &name).is_some() {
1021 continue;
1022 }
1023 let expr = crate::parse::parse_pointer_expr(&rw.handler)?;
1024 let ir = c.compile_expr(&expr, owner_td, "NEW")?;
1025 if !c.params.is_empty() || !c.hoisted_ctes.is_empty() {
1026 return Err(PyQLError::Type(PyQLTypeError {
1027 message: format!(
1028 "the rewrite of '{type_name}.{name}' has nowhere to bind a parameter or a binding"
1029 ),
1030 position: Position { line: 0, col: 0 },
1031 }));
1032 }
1033 let sql = crate::sql::emit_expr_with_fanouts(&ir, &fanouts);
1034 let junctions = c.junction_tables_of(owner_td)?;
1039 let reads_a_multi_link = junctions.iter().any(|table| sql.contains(table.as_str()));
1040 out.push(RewriteAssignment {
1041 pointer: name.clone(),
1042 column: column.clone(),
1043 ir,
1044 sql,
1045 reads_a_multi_link,
1046 });
1047 }
1048 }
1049 Ok(out)
1050}
1051
1052pub fn compile_trigger_handler(
1053 handler: &str,
1054 type_name: &str,
1055 on_mask: u8,
1056 schema: &SchemaDescriptor,
1057) -> Result<super::IrOutput, crate::error::PyQLError> {
1058 use crate::parse;
1059 use crate::parse::ast::Stmt;
1060
1061 let ast = parse::parse(handler.trim())?;
1062 let mut c = Compiler::new(schema);
1063 c.in_fn_body = true;
1066 let owner_td = c.resolve_type(type_name)?;
1071 if on_mask & 4 == 0 {
1072 c.special_anchors
1073 .insert("__new__".to_string(), (owner_td, "NEW".to_string()));
1074 }
1075 if on_mask & 1 == 0 {
1076 c.special_anchors
1077 .insert("__old__".to_string(), (owner_td, "OLD".to_string()));
1078 }
1079
1080 let (ctes, ir) = if let Stmt::With(w) = &ast {
1081 let mut cte_defs = vec![];
1082 for alias in &w.aliases {
1083 if !matches!(alias.expr, ast::Expr::SubQuery(_)) {
1087 let anchor = if on_mask & 4 == 0 { "NEW" } else { "OLD" };
1088 let value = c.compile_expr(&alias.expr, owner_td, anchor)?;
1089 c.inline_bindings.insert(alias.name.clone(), value);
1090 continue;
1091 }
1092 let ir_stmt = compile_cte_binding(&mut c, &alias.expr)?;
1093 let sql_name = c.claim_cte_sql_name(&alias.name);
1094 let cte_type_name = cte_stmt_type(&ir_stmt);
1095 c.cte_types.insert(alias.name.clone(), cte_type_name.clone());
1096 c.note_cte_cardinality(&alias.name, &ir_stmt);
1097 cte_defs.push(super::IrCteDef {
1098 name: sql_name,
1099 stmt: ir_stmt,
1100 type_name: cte_type_name,
1101 correlated_to: None,
1102 });
1103 }
1104 let main = c.compile_stmt(&w.stmt)?;
1105 (cte_defs, main)
1106 } else {
1107 (vec![], c.compile_stmt(&ast)?)
1108 };
1109
1110 let recursive_kind = recursive_dml_event(&ir, type_name)
1111 .or_else(|| ctes.iter().find_map(|cte| recursive_dml_event(&cte.stmt, type_name)));
1112 if let Some(kind) = recursive_kind
1113 && kind & on_mask != 0
1114 {
1115 return Err(crate::error::PyQLError::Fragment(crate::error::PyQLFragmentError {
1116 message: format!(
1117 "trigger on {type_name} is recursive: its handler {}s its own type, \
1118 which this trigger also fires on",
1119 dml_event_word(kind),
1120 ),
1121 context: type_name.to_string(),
1122 position: crate::error::Position { line: 0, col: 0 },
1123 }));
1124 }
1125
1126 let mut ctes = ctes;
1127 ctes.extend(std::mem::take(&mut c.hoisted_ctes));
1128 Ok(super::IrOutput {
1129 subtype_fanouts: c.subtype_fanouts(),
1130 stmt: ir,
1131 params: c.params,
1132 ctes,
1133 global_ctes: c.global_ctes,
1134 warnings: c.warnings,
1135 uses_globals_arg: c.used_globals_arg,
1136 })
1137}
1138
1139fn dml_event_word(kind: u8) -> &'static str {
1140 match kind {
1141 1 => "insert",
1142 2 => "update",
1143 4 => "delete",
1144 _ => "mutate",
1145 }
1146}
1147
1148fn recursive_dml_event(stmt: &IrStmt, owner_type: &str) -> Option<u8> {
1164 match stmt {
1165 IrStmt::Insert(ins) if ins.target.type_name == owner_type => Some(1),
1166 IrStmt::Update(upd) if upd.target.type_name == owner_type => Some(2),
1167 IrStmt::Delete(del) if del.target.type_name == owner_type => Some(4),
1168 IrStmt::Select(sel) => sel
1169 .dml_source
1170 .as_deref()
1171 .and_then(|inner| recursive_dml_event(inner, owner_type)),
1172 IrStmt::For(for_stmt) => recursive_dml_event(&for_stmt.body, owner_type),
1173 _ => None,
1174 }
1175}
1176
1177pub fn compile_expr_in_type(
1180 expr: &Expr,
1181 type_name: &str,
1182 schema: &SchemaDescriptor,
1183) -> Result<(IrExpr, Vec<String>), PyQLError> {
1184 let mut c = Compiler::new(schema);
1185 let td = c.resolve_type(type_name)?;
1186 let alias = c.fresh_alias();
1187 let ir = c.compile_expr(expr, td, &alias)?;
1188 Ok((ir, c.params))
1189}
1190
1191pub fn compile_computed_in_type(
1199 cd: &crate::schema::ComputedDescriptor,
1200 type_name: &str,
1201 schema: &SchemaDescriptor,
1202) -> Result<Option<IrExpr>, PyQLError> {
1203 let mut c = Compiler::new(schema);
1204 let td = c.resolve_type(type_name)?;
1205 let module = td.module.clone();
1206 let alias = c.fresh_alias();
1207 match c.compile_declared_computed(cd, td, &alias, &module, None, &[])? {
1208 IrShapePointer::Computed(p) => Ok(Some(p.expr)),
1209 _ => Ok(None),
1210 }
1211}
1212
1213pub fn compile_expr_unaliased(
1217 expr: &Expr,
1218 type_name: &str,
1219 schema: &SchemaDescriptor,
1220) -> Result<(IrExpr, Vec<String>), PyQLError> {
1221 let mut c = Compiler::new(schema);
1222 let td = c.resolve_type(type_name)?;
1223 let ir = c.compile_expr(expr, td, "")?;
1224 Ok((ir, c.params))
1225}
1226
1227pub fn compile_scalar_default(pyql: &str, schema: &SchemaDescriptor) -> Result<String, String> {
1232 compile_scalar_default_typed(pyql, schema).map(|(sql, _ir)| sql)
1233}
1234
1235pub fn compile_constraint_expr(
1244 pyql: &str,
1245 type_name: &str,
1246 schema: &SchemaDescriptor,
1247) -> Result<String, crate::error::PyQLError> {
1248 use crate::parse::ast::Stmt;
1249 let ast = crate::parse::parse(&format!("SELECT {pyql}"))?;
1250 let Stmt::Select(sel) = &ast else {
1251 return Err(PyQLError::Type(PyQLTypeError {
1252 message: "constraint expression must be an expression".into(),
1253 position: Position { line: 0, col: 0 },
1254 }));
1255 };
1256 let (ir, _params) = compile_expr_unaliased(&sel.result, type_name, schema)?;
1257 Ok(crate::sql::emit_expr(&ir))
1258}
1259
1260pub fn compile_scalar_default_typed(pyql: &str, schema: &SchemaDescriptor) -> Result<(String, IrExpr), String> {
1261 use crate::parse::ast::Stmt;
1262 let full = format!("SELECT {}", pyql);
1263 let ast = crate::parse::parse(&full).map_err(|e| e.message)?;
1264 let Stmt::Select(sel) = &ast else {
1265 return Err("default expression must be a select statement".into());
1266 };
1267 let mut c = Compiler::new(schema);
1268 let ir = c.compile_free_expr(&sel.result).map_err(|e| e.to_string())?;
1269 let sql = crate::sql::emit_expr(&ir);
1270 Ok((sql, ir))
1271}
1272
1273pub fn default_blocker(expr: &IrExpr) -> Option<&'static str> {
1279 use IrExpr as E;
1280 match expr {
1281 E::Subquery(_)
1282 | E::ObjectSubquery(_)
1283 | E::ObjectPathSubquery(_)
1284 | E::ObjectPathUnion { .. }
1285 | E::PathSubquery(_)
1286 | E::FnSubquery(_)
1287 | E::ArrayFromSelect(_)
1288 | E::ScalarSubquery(_)
1289 | E::AggOverQuery { .. }
1290 | E::AggOverCte { .. }
1291 | E::ExistsOverCte { .. }
1292 | E::SetOp { .. }
1293 | E::AggOverSet { .. }
1294 | E::CteRef { .. }
1295 | E::CteFieldRef { .. }
1296 | E::GlobalRef { .. } => Some("a sub-select"),
1297 E::ColumnRef { .. } => Some("a reference to another pointer"),
1298 E::Param { .. } | E::GlobalParam { .. } => Some("a query parameter"),
1299 E::ForVar { .. } => Some("a for-loop variable"),
1300 E::FnParam { .. } => Some("a function parameter"),
1301 E::BinOp(b) => default_blocker(&b.left).or_else(|| default_blocker(&b.right)),
1302 E::UnaryOp(u) => default_blocker(&u.operand),
1303 E::TypeCast(c) => default_blocker(&c.expr),
1304 E::IfElse(i) => default_blocker(&i.condition)
1305 .or_else(|| default_blocker(&i.if_))
1306 .or_else(|| default_blocker(&i.else_)),
1307 E::FunctionCall(f) => f.args.iter().find_map(default_blocker),
1308 E::Array(items) | E::Tuple(items) => items.iter().find_map(default_blocker),
1309 E::NamedTuple { fields, .. } => fields.iter().find_map(|(_, e)| default_blocker(e)),
1310 E::Subscript { expr, index, .. } => default_blocker(expr).or_else(|| default_blocker(index)),
1311 E::JsonbField { expr, .. } | E::JsonbIndex { expr, .. } => default_blocker(expr),
1312 E::Slice { expr, lower, upper, .. } => default_blocker(expr)
1313 .or_else(|| lower.as_deref().and_then(default_blocker))
1314 .or_else(|| upper.as_deref().and_then(default_blocker)),
1315 E::Literal(_) | E::Null | E::EnumLiteral { .. } | E::RawSql(_) => None,
1316 }
1317}
1318
1319pub fn column_default_sql(pyql: &str, schema: &SchemaDescriptor) -> Option<String> {
1324 let (sql, ir) = compile_scalar_default_typed(pyql, schema).ok()?;
1325 default_blocker(&ir).is_none().then_some(sql)
1326}
1327
1328pub fn inlined_pointer_defaults(td: &TypeDescriptor, schema: &SchemaDescriptor) -> Vec<(String, String)> {
1337 let properties = td
1338 .properties
1339 .iter()
1340 .filter(|p| !p.is_pk)
1341 .filter_map(|p| p.default_pyql.as_ref().map(|d| (p.name.clone(), d.clone())));
1342 let links = td
1343 .links
1344 .iter()
1345 .filter(|l| !l.is_junction_backed())
1346 .filter_map(|l| l.default_pyql.as_ref().map(|d| (l.name.clone(), d.clone())));
1347 properties
1348 .chain(links)
1349 .filter(|(_, pyql)| column_default_sql(pyql, schema).is_none())
1350 .collect()
1351}
1352
1353fn default_shape_element(pointer: &str, value: Expr) -> ShapeElement {
1355 ShapeElement {
1356 path: ast::Path::relative(pointer),
1357 splat: None,
1358 nested: None,
1359 compexpr: Some(value),
1360 op: ShapeOp::Assign,
1361 filter: None,
1362 order_by: vec![],
1363 offset: None,
1364 limit: None,
1365 marker_offset: None,
1366 }
1367}
1368
1369pub fn compile_inlined_default(
1376 type_name: &str,
1377 pointer: &str,
1378 pyql: &str,
1379 schema: &SchemaDescriptor,
1380) -> Result<(String, IrExpr), PyQLError> {
1381 let mut c = Compiler::new(schema);
1382 let td = c.resolve_type(type_name)?;
1383 let alias = c.fresh_alias();
1384 let element = default_shape_element(pointer, crate::parse::parse_pointer_expr(pyql)?);
1385 let mut assignments = c.compile_assignments(&[element], td, &alias)?;
1386 Ok(assignments.remove(0))
1387}
1388
1389struct Compiler<'a> {
1392 schema: &'a SchemaDescriptor,
1393 params: Vec<String>,
1395 alias_counter: usize,
1396 cte_types: HashMap<String, String>,
1398 group_bindings: HashMap<String, IrGroup>,
1402 cte_free_items: HashMap<String, IrFreeExpr>,
1407 multi_row_ctes: HashSet<String>,
1412 for_vars: HashMap<String, String>,
1414 for_var_types: HashMap<String, String>,
1418 for_var_ctes: HashMap<String, String>,
1423 for_var_slots: HashMap<String, String>,
1427 pending_insert_guard: Option<Expr>,
1430 pending_update_guard: Option<Expr>,
1434 pending_delete_guard: Option<Expr>,
1436 cte_declared_pointers: HashMap<String, Vec<ShapeElement>>,
1440 active_declared_pointers: Vec<ShapeElement>,
1442 expanding_computeds: Vec<String>,
1446 anchors: Vec<SelectAnchor>,
1452 hoisted_ctes: Vec<IrCteDef>,
1456 hoisted_binding_sources: HashMap<String, Expr>,
1460 cte_sql_names: HashMap<String, String>,
1465 cte_namespace: std::collections::HashSet<String>,
1469 for_vars_read: std::collections::HashSet<String>,
1472 correlated_bindings: HashMap<String, String>,
1475 correlated_reads: std::cell::RefCell<std::collections::HashSet<String>>,
1479 for_scope: Vec<String>,
1481 link_prop_scope: Vec<Option<(String, String)>>,
1486 pending_detached: bool,
1489 modifier_anchor: Option<(String, String)>,
1494 tail_sorts: Vec<ast::SortExpr>,
1499 inline_bindings: std::collections::HashMap<String, IrExpr>,
1503 in_fn_body: bool,
1508 fns_needing_globals: Option<std::collections::HashSet<String>>,
1517 used_globals_arg: bool,
1520 fn_params: HashMap<String, String>,
1522 special_anchors: HashMap<String, (&'a TypeDescriptor, String)>,
1535 global_ctes: Vec<IrGlobalCte>,
1537 pending_nested_ctes: Vec<IrCteDef>,
1545 junction_read_overrides: HashMap<String, JunctionReadOverride>,
1551 nested_cte_counter: usize,
1552 warnings: Vec<String>,
1554 explicit_set_depth: usize,
1558 universal_set_depth: usize,
1563 config: crate::ir::SessionConfig,
1566 implicit_id_in_shapes: bool,
1573 value_position: bool,
1580}
1581
1582struct SelectAnchor {
1584 type_name: String,
1585 qualified: String,
1586 alias: String,
1587 detached: bool,
1588 declared_on: Option<(String, String)>,
1602}
1603
1604impl SelectAnchor {
1605 fn answers_to(&self, root: &str) -> bool {
1607 self.type_name == root
1608 || self.qualified == root
1609 || self
1610 .declared_on
1611 .as_ref()
1612 .is_some_and(|(name, qualified)| name == root || qualified == root)
1613 }
1614}
1615
1616pub const GLOBALS_ARG: &str = "__pylon_json_globals__";
1626
1627fn globals_arg_read(qualified: &str, pg_type: &str) -> String {
1629 let key = qualified.replace('\'', "''");
1630 if let Some(element) = pg_type.strip_suffix("[]") {
1631 format!(
1637 "(case when jsonb_typeof({GLOBALS_ARG} -> '{key}') = 'array' \
1638 then coalesce((select array_agg(value::{element}) \
1639 from jsonb_array_elements_text({GLOBALS_ARG} -> '{key}') as value), '{{}}'::{pg_type}) \
1640 else null end)"
1641 )
1642 } else {
1643 format!("(({GLOBALS_ARG} ->> '{key}')::{pg_type})")
1644 }
1645}
1646
1647impl<'a> Compiler<'a> {
1648 fn new(schema: &'a SchemaDescriptor) -> Self {
1649 Self::with_config(schema, crate::ir::SessionConfig::default())
1650 }
1651
1652 fn with_config(schema: &'a SchemaDescriptor, config: crate::ir::SessionConfig) -> Self {
1653 Compiler {
1654 schema,
1655 params: vec![],
1656 alias_counter: 0,
1657 cte_types: HashMap::new(),
1658 group_bindings: HashMap::new(),
1659 cte_free_items: HashMap::new(),
1660 multi_row_ctes: HashSet::new(),
1661 for_vars: HashMap::new(),
1662 for_var_types: HashMap::new(),
1663 for_var_ctes: HashMap::new(),
1664 for_var_slots: HashMap::new(),
1665 pending_insert_guard: None,
1666 pending_update_guard: None,
1667 pending_delete_guard: None,
1668 cte_declared_pointers: HashMap::new(),
1669 active_declared_pointers: vec![],
1670 expanding_computeds: vec![],
1671 fn_params: HashMap::new(),
1672 special_anchors: HashMap::new(),
1673 global_ctes: vec![],
1674 pending_nested_ctes: vec![],
1675 junction_read_overrides: HashMap::new(),
1676 nested_cte_counter: 0,
1677 warnings: vec![],
1678 explicit_set_depth: 0,
1679 universal_set_depth: 0,
1680 config,
1681 implicit_id_in_shapes: true,
1682 value_position: false,
1683 anchors: Vec::new(),
1684 link_prop_scope: Vec::new(),
1685 hoisted_ctes: Vec::new(),
1686 hoisted_binding_sources: HashMap::new(),
1687 cte_sql_names: HashMap::new(),
1688 cte_namespace: std::collections::HashSet::new(),
1689 for_vars_read: std::collections::HashSet::new(),
1690 correlated_bindings: HashMap::new(),
1691 correlated_reads: std::cell::RefCell::new(std::collections::HashSet::new()),
1692 for_scope: Vec::new(),
1693 pending_detached: false,
1694 modifier_anchor: None,
1695 tail_sorts: vec![],
1696 inline_bindings: std::collections::HashMap::new(),
1697 in_fn_body: false,
1698 fns_needing_globals: None,
1699 used_globals_arg: false,
1700 }
1701 }
1702
1703 fn is_value_binding(&self, name: &str) -> bool {
1710 self.inline_bindings.contains_key(name) || self.cte_types.get(name).is_some_and(|t| !t.contains("::"))
1711 }
1712
1713 fn bind_inline_if_correlated(
1720 &mut self,
1721 name: &str,
1722 expr: &Expr,
1723 ctx: Option<(&TypeDescriptor, &str)>,
1724 ) -> Result<bool, PyQLError> {
1725 if (self.anchors.is_empty() && ctx.is_none()) || !matches!(expr, Expr::Path(p) if p.partial) {
1726 return Ok(false);
1727 }
1728 let ir = self.compile_expr_ctx(expr, ctx)?;
1729 self.inline_bindings.insert(name.to_string(), ir);
1730 Ok(true)
1731 }
1732
1733 fn cte_sql_name(&self, name: &str) -> String {
1735 let sql_name = self
1736 .cte_sql_names
1737 .get(name)
1738 .cloned()
1739 .unwrap_or_else(|| name.to_string());
1740 if let Some(slot) = self.correlated_bindings.get(&sql_name) {
1741 self.correlated_reads.borrow_mut().insert(slot.clone());
1742 }
1743 sql_name
1744 }
1745
1746 fn cte_source_table(&self, cte: &str) -> String {
1749 if let Some(slot) = self.correlated_bindings.get(cte) {
1750 self.correlated_reads.borrow_mut().insert(slot.clone());
1751 }
1752 format!("@cte:{cte}")
1753 }
1754
1755 fn compile_binding_in_scope(&mut self, expr: &Expr) -> Result<(IrStmt, Option<String>), PyQLError> {
1759 let before = std::mem::take(&mut self.for_vars_read);
1760 let before_correlated = std::mem::take(&mut *self.correlated_reads.borrow_mut());
1761 let ir_stmt = compile_cte_binding(self, expr);
1762 let mut read = std::mem::replace(&mut self.for_vars_read, before);
1763 let correlated_read = std::mem::replace(&mut *self.correlated_reads.borrow_mut(), before_correlated);
1764 read.extend(correlated_read.iter().cloned());
1768 let correlated_to = self
1771 .for_scope
1772 .iter()
1773 .rev()
1774 .find(|slot| read.contains(*slot))
1775 .map(|slot| format!("_for_{slot}"));
1776 self.for_vars_read.extend(read);
1777 self.correlated_reads.borrow_mut().extend(correlated_read);
1778 Ok((ir_stmt?, correlated_to))
1779 }
1780
1781 fn claim_in_cte_namespace(&mut self, base: &str) -> String {
1786 let taken = |namespace: &std::collections::HashSet<String>, candidate: &String| {
1789 namespace.contains(candidate)
1790 || namespace
1791 .iter()
1792 .any(|claimed| claimed != base && candidate.starts_with(&format!("{claimed}__")))
1793 };
1794 let mut candidate = base.to_string();
1795 let mut suffix = 1;
1796 while taken(&self.cte_namespace, &candidate) {
1797 candidate = format!("{base}__{suffix}");
1798 suffix += 1;
1799 }
1800 self.cte_namespace.insert(candidate.clone());
1801 candidate
1802 }
1803
1804 fn claim_generated_cte_name(&mut self, preferred: &str) -> String {
1808 self.claim_in_cte_namespace(preferred)
1809 }
1810
1811 fn claim_cte_sql_name(&mut self, name: &str) -> String {
1814 let base = match name.starts_with('_') {
1818 true => format!("w{name}"),
1819 false => name.to_string(),
1820 };
1821 let claimed = self.claim_in_cte_namespace(&base);
1822 if claimed == name {
1823 self.cte_sql_names.remove(name);
1824 } else {
1825 self.cte_sql_names.insert(name.to_string(), claimed.clone());
1826 }
1827 claimed
1828 }
1829
1830 fn register_cte(&mut self, name: &str, ir_stmt: &IrStmt) -> String {
1831 let type_name = cte_stmt_type(ir_stmt);
1832 self.cte_types.insert(name.to_string(), type_name.clone());
1833 if let IrStmt::Select(sel) = ir_stmt
1834 && let [IrRowSource::Free(item)] = sel.rows.as_slice()
1835 {
1836 self.cte_free_items.insert(name.to_string(), item.clone());
1837 }
1838 self.note_cte_cardinality(name, ir_stmt);
1839 type_name
1840 }
1841
1842 fn pins_an_exclusive_property(&self, sel: &IrSelect) -> bool {
1847 let [IrRowSource::Bound { source, .. }] = sel.rows.as_slice() else {
1848 return false;
1849 };
1850 let Some(filter) = &sel.filter else { return false };
1851 let Ok(td) = self.resolve_type(&source.type_name) else {
1852 return false;
1853 };
1854 Self::pins_exclusive(td, filter)
1855 }
1856
1857 fn pins_exclusive(td: &TypeDescriptor, filter: &IrExpr) -> bool {
1861 let IrExpr::BinOp(binop) = filter else { return false };
1862 match binop.op {
1863 ast::BinOpKind::And => Self::pins_exclusive(td, &binop.left) || Self::pins_exclusive(td, &binop.right),
1864 ast::BinOpKind::Eq => {
1865 let column = match (&binop.left, &binop.right) {
1866 (IrExpr::ColumnRef { column, .. }, _) => column,
1867 (_, IrExpr::ColumnRef { column, .. }) => column,
1868 _ => return false,
1869 };
1870 td.properties
1871 .iter()
1872 .any(|p| &p.name == column && (p.is_exclusive || p.is_pk))
1873 }
1874 _ => false,
1875 }
1876 }
1877
1878 fn note_cte_cardinality(&mut self, name: &str, ir_stmt: &IrStmt) {
1884 let single = match ir_stmt {
1885 IrStmt::Insert(_) => true,
1886 IrStmt::Select(sel) => {
1887 matches!(sel.limit, Some(IrExpr::Literal(IrLiteral::Int(1))))
1888 || matches!(
1889 sel.rows.as_slice(),
1890 [IrRowSource::Free(
1891 IrFreeExpr::Scalar(_)
1892 | IrFreeExpr::FreeObject(_)
1893 | IrFreeExpr::NamedTupleRow(_)
1894 | IrFreeExpr::Tuple(_)
1895 )]
1896 )
1897 || self.pins_an_exclusive_property(sel)
1898 }
1899 _ => false,
1900 };
1901 if !single {
1902 self.multi_row_ctes.insert(name.to_string());
1903 }
1904 }
1905
1906 fn resolve_cte_field_chain(&self, root: &str, steps: &[&str]) -> Option<Result<IrExpr, PyQLError>> {
1917 let (first, rest) = steps.split_first()?;
1918 let fields = match self.cte_free_items.get(root)? {
1919 IrFreeExpr::FreeObject(fields) => fields,
1920 _ => return None,
1921 };
1922 let mut current: &IrExpr = match fields.iter().find(|(n, _)| n == first) {
1923 Some((_, e)) => e,
1924 None => {
1925 return Some(Err(
1926 self.type_err(&format!("free object '{root}' has no field '{first}'"))
1927 ));
1928 }
1929 };
1930 let mut expr = IrExpr::CteFieldRef {
1931 name: root.to_string(),
1932 field: first.to_string(),
1933 pg_type: infer_ir_type(current).map(str::to_string),
1934 };
1935 for step in rest {
1936 if let IrExpr::NamedTuple {
1937 fields: nested,
1938 is_free_object: true,
1939 } = current
1940 {
1941 match nested.iter().find(|(n, _)| n == step) {
1942 Some((_, next)) => current = next,
1943 None => {
1944 return Some(Err(
1945 self.type_err(&format!("{step} is not a member of the nested free object"))
1946 ));
1947 }
1948 }
1949 }
1950 expr = IrExpr::JsonbField {
1951 expr: Box::new(expr),
1952 field: step.to_string(),
1953 };
1954 }
1955 Some(Ok(expr))
1956 }
1957
1958 fn resolve_cte_path(&self, p: &ast::Path) -> Option<Result<IrExpr, PyQLError>> {
1963 if p.partial || p.steps.len() < 2 {
1964 return None;
1965 }
1966 let ast::PathStep::Name(root) = &p.steps[0] else {
1967 return None;
1968 };
1969 let mut steps = Vec::with_capacity(p.steps.len() - 1);
1970 for step in &p.steps[1..] {
1971 match step {
1972 ast::PathStep::Name(n) => steps.push(n.as_str()),
1973 _ => return None,
1974 }
1975 }
1976 self.resolve_cte_field_chain(root, &steps)
1977 }
1978
1979 fn resolve_cte_name<'e>(&self, expr: &'e Expr) -> Option<&'e str> {
1981 if let Expr::Path(p) = expr
1982 && !p.partial
1983 && p.steps.len() == 1
1984 && let ast::PathStep::Name(n) = &p.steps[0]
1985 && self.cte_types.contains_key(n.as_str())
1986 {
1987 return Some(n.as_str());
1988 }
1989 None
1990 }
1991
1992 fn for_var_ref(&mut self, name: &str) -> IrExpr {
1997 let slot = self
1998 .for_var_slots
1999 .get(name)
2000 .cloned()
2001 .unwrap_or_else(|| name.to_string());
2002 self.for_vars_read.insert(slot.clone());
2003 let pg_type = self.for_vars.get(name).cloned();
2004 IrExpr::ForVar { name: slot, pg_type }
2005 }
2006
2007 fn fresh_alias(&mut self) -> String {
2008 let a = format!("t{}", self.alias_counter);
2009 self.alias_counter += 1;
2010 a
2011 }
2012
2013 fn fresh_nested_cte_name(&mut self) -> String {
2017 let n = self.nested_cte_counter;
2018 self.nested_cte_counter += 1;
2019 self.claim_generated_cte_name(&format!("_nested_dml_{n}"))
2020 }
2021
2022 fn param_index(&mut self, name: &str) -> usize {
2024 if let Some(i) = self.params.iter().position(|n| n == name) {
2025 return i;
2026 }
2027 let i = self.params.len();
2028 self.params.push(name.to_string());
2029 i
2030 }
2031
2032 fn resolve_global_pg_type(&self, scalar_type: &str) -> String {
2040 let builtin = match scalar_type.strip_prefix("std::").unwrap_or(scalar_type) {
2042 "str" => Some("text"),
2043 "int16" => Some("int2"),
2044 "int32" => Some("int4"),
2045 "int64" => Some("int8"),
2046 "float32" => Some("float4"),
2047 "float64" => Some("float8"),
2048 "decimal" => Some("numeric"),
2049 "bool" => Some("boolean"),
2050 "datetime" => Some("timestamptz"),
2051 "cal::local_datetime" => Some("timestamp"),
2052 "cal::local_date" => Some("date"),
2053 "cal::local_time" => Some("time"),
2054 "uuid" => Some("uuid"),
2055 "bytes" => Some("bytea"),
2056 "json" => Some("jsonb"),
2057 "duration" => Some("interval"),
2058 _ => None,
2059 };
2060 if let Some(t) = builtin {
2061 return t.to_string();
2062 }
2063 if let Some(inner) = scalar_type.strip_prefix("array<").and_then(|s| s.strip_suffix('>')) {
2064 return format!("{}[]", self.resolve_global_pg_type(inner));
2065 }
2066 if scalar_type.starts_with("tuple<") {
2067 return "jsonb".to_string();
2068 }
2069 if let Some(ed) = self.resolve_enum(scalar_type) {
2070 return format!("{}.\"{}\"", crate::sql::pg_schema_str(&ed.module), ed.name);
2071 }
2072 if self.resolve_named_tuple(scalar_type).is_some() {
2073 return "jsonb".to_string();
2074 }
2075 self.schema
2077 .scalars
2078 .iter()
2079 .find(|s| s.name == scalar_type || format!("{}::{}", s.module, s.name) == scalar_type)
2080 .map(|s| s.pg_type.clone())
2081 .unwrap_or_else(|| "text".to_string())
2082 }
2083
2084 fn try_compile_global_select(
2088 &mut self,
2089 outer: &ast::SelectStmt,
2090 result: &Expr,
2091 _distinct: bool,
2092 ) -> Result<Option<IrStmt>, PyQLError> {
2093 let (global_name, shape_elements): (&str, &[ast::ShapeElement]) = match result {
2094 Expr::Global(name) => (name.as_str(), &[]),
2095 Expr::Shape(sh) => match sh.expr.as_ref() {
2096 Some(Expr::Global(name)) => (name.as_str(), sh.elements.as_slice()),
2097 _ => return Ok(None),
2098 },
2099 _ => return Ok(None),
2100 };
2101
2102 let global = self
2103 .schema
2104 .globals
2105 .iter()
2106 .find(|g| g.name == global_name || format!("{}::{}", g.module, g.name) == global_name);
2107 let global = match global {
2108 Some(g) => g.clone(),
2109 None => return Ok(None),
2110 };
2111 let computed_expr = match global.computed_expr {
2112 Some(e) => e,
2113 None => return Ok(None), };
2115
2116 let inner_ast = crate::parse::parse(&computed_expr)?;
2117 let inner_sel = match inner_ast {
2118 Stmt::Select(sel) => sel,
2119 _ => return Ok(None),
2120 };
2121
2122 let merged_result = if shape_elements.is_empty() {
2123 inner_sel.result.clone()
2124 } else {
2125 Expr::Shape(Box::new(ast::ShapeExpr {
2126 expr: Some(inner_sel.result.clone()),
2127 elements: shape_elements.to_vec(),
2128 marker_offset: None,
2129 }))
2130 };
2131
2132 let merged_filter = match (&inner_sel.filter, &outer.filter) {
2133 (Some(a), Some(b)) => Some(Expr::BinOp(Box::new(ast::BinOp {
2134 left: a.clone(),
2135 op: ast::BinOpKind::And,
2136 right: b.clone(),
2137 }))),
2138 (Some(a), None) => Some(a.clone()),
2139 (None, b) => b.clone(),
2140 };
2141
2142 let merged = ast::SelectStmt {
2143 result: merged_result,
2144 filter: merged_filter,
2145 order_by: if outer.order_by.is_empty() {
2146 inner_sel.order_by.clone()
2147 } else {
2148 outer.order_by.clone()
2149 },
2150 offset: outer.offset.clone().or(inner_sel.offset.clone()),
2151 limit: outer.limit.clone().or(inner_sel.limit.clone()),
2152 lock: outer.lock.clone().or(inner_sel.lock.clone()),
2153 };
2154
2155 let ir = self.compile_stmt(&Stmt::Select(merged))?;
2156 Ok(Some(ir))
2157 }
2158
2159 fn project_free_object_field(expr: IrExpr, field: &str) -> IrExpr {
2167 if let IrExpr::NamedTuple {
2168 fields,
2169 is_free_object: true,
2170 } = &expr
2171 && let Some((_, value)) = fields.iter().find(|(name, _)| name == field)
2172 {
2173 return value.clone();
2174 }
2175 IrExpr::JsonbField {
2176 expr: Box::new(expr),
2177 field: field.to_string(),
2178 }
2179 }
2180
2181 fn union_of_relative_paths(expr: &Expr) -> Option<Vec<ast::Path>> {
2184 fn walk(expr: &Expr, out: &mut Vec<ast::Path>) -> bool {
2185 match expr {
2186 Expr::Union(a, b) => walk(a, out) && walk(b, out),
2187 Expr::Path(p) if p.partial => {
2188 out.push(p.clone());
2189 true
2190 }
2191 _ => false,
2192 }
2193 }
2194 if !matches!(expr, Expr::Union(_, _)) {
2195 return None;
2196 }
2197 let mut out = vec![];
2198 walk(expr, &mut out).then_some(out)
2199 }
2200
2201 fn coalesce_of_relative_paths(expr: &Expr) -> Option<Vec<ast::Path>> {
2206 fn walk(expr: &Expr, out: &mut Vec<ast::Path>) -> bool {
2207 match expr {
2208 Expr::BinOp(b) if b.op == ast::BinOpKind::Coalesce => walk(&b.left, out) && walk(&b.right, out),
2209 Expr::Path(p) if p.partial => {
2210 out.push(p.clone());
2211 true
2212 }
2213 _ => false,
2214 }
2215 }
2216 if !matches!(expr, Expr::BinOp(b) if b.op == ast::BinOpKind::Coalesce) {
2217 return None;
2218 }
2219 let mut out = vec![];
2220 walk(expr, &mut out).then_some(out)
2221 }
2222
2223 fn shape_over_subquery(&mut self, sh: &ast::ShapeExpr) -> Result<Option<IrExpr>, PyQLError> {
2230 fn push_shape(stmt: &Stmt, elements: &[ShapeElement]) -> Option<Stmt> {
2231 match stmt {
2232 Stmt::Select(sel) => {
2233 if matches!(sel.result, Expr::Shape(_)) {
2234 return None;
2235 }
2236 let mut shaped = sel.clone();
2237 shaped.result = Expr::Shape(Box::new(ast::ShapeExpr {
2238 expr: Some(sel.result.clone()),
2239 elements: elements.to_vec(),
2240 marker_offset: None,
2241 }));
2242 Some(Stmt::Select(shaped))
2243 }
2244 Stmt::With(w) => {
2245 let inner = push_shape(&w.stmt, elements)?;
2246 let mut carried = w.clone();
2247 carried.stmt = Box::new(inner);
2248 Some(Stmt::With(carried))
2249 }
2250 _ => None,
2251 }
2252 }
2253 let Some(Expr::SubQuery(stmt)) = sh.expr.as_ref() else {
2254 return Ok(None);
2255 };
2256 let Some(shaped) = push_shape(stmt.as_ref(), &sh.elements) else {
2257 return Ok(None);
2258 };
2259 let single = matches!(
2260 innermost_select(&shaped).and_then(|s| s.limit.as_ref()),
2261 Some(Expr::Literal(ast::Literal::Int(1)))
2262 );
2263 let IrStmt::Select(select) = self.compile_stmt(&shaped)? else {
2264 return Ok(None);
2265 };
2266 if !matches!(select.rows.as_slice(), [IrRowSource::Bound { .. }]) {
2267 return Ok(None);
2268 }
2269 Ok(Some(if single {
2270 IrExpr::ObjectSubquery(Box::new(select))
2271 } else {
2272 IrExpr::ArrayFromSelect(Box::new(IrArraySource::ObjectSelect(Box::new(select))))
2273 }))
2274 }
2275
2276 fn shape_over_subquery_projection(sh: &ast::ShapeExpr) -> Option<(&Stmt, Vec<String>)> {
2281 let inner = sh.expr.as_ref()?;
2282 if !matches!(inner, Expr::FieldAccess { .. }) {
2283 return None;
2284 }
2285 let (base, fields) = Self::peel_field_access_chain(inner);
2286 match base {
2287 Expr::SubQuery(stmt) => Some((stmt.as_ref(), fields)),
2288 _ => None,
2289 }
2290 }
2291
2292 fn junction_overrides_for(dml: &IrStmt, cte_name: &str) -> HashMap<String, JunctionReadOverride> {
2302 let appends = match dml {
2303 IrStmt::Insert(ins) => &ins.multi_link_appends,
2304 IrStmt::Update(upd) => &upd.multi_link_appends,
2305 _ => return HashMap::new(),
2306 };
2307 appends
2308 .iter()
2309 .enumerate()
2310 .filter(|(_, append)| append.values.link_props.is_empty())
2311 .map(|(i, append)| {
2312 let targets = match &append.values.source {
2315 IrMultiLinkValueSource::CteRef(target_cte) => Some(format!("@cte:{target_cte}")),
2316 _ => None,
2317 };
2318 (
2319 append.junction_table.clone(),
2320 JunctionReadOverride {
2321 junction: format!("@cte:{cte_name}__ml_add_{i}"),
2322 targets,
2323 },
2324 )
2325 })
2326 .collect()
2327 }
2328
2329 fn try_compile_walk_off_subquery(
2340 &mut self,
2341 expr: &Expr,
2342 ctx: Option<(&TypeDescriptor, &str)>,
2343 ) -> Result<Option<IrExpr>, PyQLError> {
2344 if !matches!(expr, Expr::PathStepOn { .. } | Expr::FieldAccess { .. }) {
2345 return Ok(None);
2346 }
2347 let (base, steps) = Self::peel_path_step_chain(expr);
2348 if !steps.iter().any(|step| !matches!(step, ast::PathStep::Name(_))) {
2349 return Ok(None);
2350 }
2351 let Expr::SubQuery(inner_stmt) = base else {
2352 return Ok(None);
2353 };
2354 let inner = compile_cte_binding(self, &Expr::SubQuery(inner_stmt.clone()))?;
2356 let cte_name = self.fresh_nested_cte_name();
2357 let overrides = Self::junction_overrides_for(&inner, &cte_name);
2358 let type_name = self.register_cte(&cte_name, &inner);
2359 self.hoisted_ctes.push(IrCteDef {
2360 name: cte_name.clone(),
2361 stmt: inner,
2362 type_name,
2363 correlated_to: None,
2364 });
2365 let mut path_steps = vec![ast::PathStep::Name(cte_name)];
2366 path_steps.extend(steps);
2367 let path = ast::Path {
2368 steps: path_steps,
2369 partial: false,
2370 };
2371 let previous = std::mem::replace(&mut self.junction_read_overrides, overrides);
2372 let ir = match ctx {
2373 Some((td, alias)) => self.compile_path(&path, td, alias),
2374 None => self.compile_free_path(&path),
2375 };
2376 self.junction_read_overrides = previous;
2377 Ok(Some(ir?))
2378 }
2379
2380 fn peel_path_step_chain(expr: &Expr) -> (&Expr, Vec<ast::PathStep>) {
2385 let mut steps = Vec::new();
2386 let mut current = expr;
2387 loop {
2388 match current {
2389 Expr::FieldAccess { expr: inner, field } => {
2390 steps.push(ast::PathStep::Name(field.clone()));
2391 current = inner;
2392 }
2393 Expr::PathStepOn { expr: inner, step } => {
2394 steps.push((**step).clone());
2395 current = inner;
2396 }
2397 _ => break,
2398 }
2399 }
2400 steps.reverse();
2401 (current, steps)
2402 }
2403
2404 fn peel_field_access_chain(expr: &Expr) -> (&Expr, Vec<String>) {
2405 let mut fields = Vec::new();
2406 let mut current = expr;
2407 while let Expr::FieldAccess { expr: inner, field } = current {
2408 fields.push(field.clone());
2409 current = inner;
2410 }
2411 fields.reverse();
2412 (current, fields)
2413 }
2414
2415 fn resolve_field_owner_select(&self, expr: &Expr) -> Option<ast::SelectStmt> {
2423 let sel = match expr {
2424 Expr::SubQuery(stmt) => match stmt.as_ref() {
2425 Stmt::Select(sel) => sel.clone(),
2426 _ => return None,
2427 },
2428 Expr::Global(name) => {
2429 let global = self
2430 .schema
2431 .globals
2432 .iter()
2433 .find(|g| g.name == *name || format!("{}::{}", g.module, g.name) == *name)?;
2434 let computed_expr = global.computed_expr.as_ref()?;
2435 match crate::parse::parse(computed_expr).ok()? {
2436 Stmt::Select(sel) => sel,
2437 _ => return None,
2438 }
2439 }
2440 _ => return None,
2441 };
2442 let mut sel = sel;
2446 if let Expr::Detached(inner) = &sel.result {
2447 sel.result = inner.as_ref().clone();
2448 }
2449 match &sel.result {
2450 Expr::Path(p) if !p.partial => Some(sel),
2451 _ => None,
2452 }
2453 }
2454
2455 fn compile_walk_off_bound_head(
2469 &mut self,
2470 outer: &ast::SelectStmt,
2471 inner_sel: &ast::SelectStmt,
2472 fields: &[String],
2473 trailing_shape: &[ShapeElement],
2474 ) -> Result<Option<IrStmt>, PyQLError> {
2475 let head = Expr::SubQuery(Box::new(Stmt::Select(inner_sel.clone())));
2476 let inner = compile_cte_binding(self, &head)?;
2477 let cte_name = self.fresh_nested_cte_name();
2478 let overrides = Self::junction_overrides_for(&inner, &cte_name);
2479 let type_name = self.register_cte(&cte_name, &inner);
2480 self.hoisted_ctes.push(IrCteDef {
2481 name: cte_name.clone(),
2482 stmt: inner,
2483 type_name,
2484 correlated_to: None,
2485 });
2486 let mut steps = vec![ast::PathStep::Name(cte_name)];
2487 steps.extend(fields.iter().cloned().map(ast::PathStep::Name));
2488 let walk = Expr::Path(ast::Path { steps, partial: false });
2489 let result = if trailing_shape.is_empty() {
2490 walk
2491 } else {
2492 Expr::Shape(Box::new(ast::ShapeExpr {
2493 expr: Some(walk),
2494 elements: trailing_shape.to_vec(),
2495 marker_offset: None,
2496 }))
2497 };
2498 let rerooted = ast::SelectStmt {
2499 result,
2500 filter: outer.filter.clone(),
2501 order_by: outer.order_by.clone(),
2502 offset: outer.offset.clone(),
2503 limit: outer.limit.clone(),
2504 lock: outer.lock.clone(),
2505 };
2506 let previous = std::mem::replace(&mut self.junction_read_overrides, overrides);
2507 let ir = self.compile_stmt(&Stmt::Select(rerooted));
2508 self.junction_read_overrides = previous;
2509 Ok(Some(ir?))
2510 }
2511
2512 fn try_compile_field_access_select(
2513 &mut self,
2514 outer: &ast::SelectStmt,
2515 result: &Expr,
2516 ) -> Result<Option<IrStmt>, PyQLError> {
2517 let (result, trailing_shape): (&Expr, &[ShapeElement]) = match result {
2521 Expr::Shape(sh) => match sh.expr.as_ref() {
2522 Some(inner @ Expr::FieldAccess { .. }) => (inner, sh.elements.as_slice()),
2523 _ => (result, &[]),
2524 },
2525 other => (other, &[]),
2526 };
2527 let (root, fields) = Self::peel_field_access_chain(result);
2528 if fields.is_empty() {
2529 return Ok(None);
2530 }
2531 let inner_sel = match self.resolve_field_owner_select(root) {
2532 Some(sel) => sel,
2533 None => return Ok(None),
2534 };
2535 let Expr::Path(type_path) = &inner_sel.result else {
2536 return Ok(None);
2537 };
2538 if inner_sel.limit.is_some() || inner_sel.offset.is_some() {
2544 return self.compile_walk_off_bound_head(outer, &inner_sel, &fields, trailing_shape);
2545 }
2546 let mut steps = type_path.steps.clone();
2547 let field_count = fields.len();
2548 steps.extend(fields.into_iter().map(ast::PathStep::Name));
2549 let merged_result = Expr::Path(ast::Path { steps, partial: false });
2550
2551 let merged_filter = match (&inner_sel.filter, &outer.filter) {
2552 (Some(a), Some(b)) => Some(Expr::BinOp(Box::new(ast::BinOp {
2553 left: a.clone(),
2554 op: ast::BinOpKind::And,
2555 right: b.clone(),
2556 }))),
2557 (Some(a), None) => Some(a.clone()),
2558 (None, b) => b.clone(),
2559 };
2560
2561 let merged = ast::SelectStmt {
2562 result: merged_result,
2563 filter: merged_filter,
2564 order_by: if outer.order_by.is_empty() {
2565 inner_sel.order_by.clone()
2566 } else {
2567 outer.order_by.clone()
2568 },
2569 offset: outer.offset.clone().or(inner_sel.offset.clone()),
2570 limit: outer.limit.clone().or(inner_sel.limit.clone()),
2571 lock: outer.lock.clone().or(inner_sel.lock.clone()),
2572 };
2573
2574 if outer.filter.is_none() {
2583 let merged = ast::SelectStmt {
2584 order_by: inner_sel.order_by.clone(),
2585 ..merged
2586 };
2587 let Expr::Path(merged_path) = &merged.result else {
2588 unreachable!("built as a path just above")
2589 };
2590 let ps = self.compile_path_select_with_tail(
2591 &merged,
2592 merged_path,
2593 trailing_shape,
2594 false,
2595 field_count,
2596 &outer.order_by,
2597 )?;
2598 return Ok(Some(IrStmt::PathSelect(ps)));
2599 }
2600
2601 let merged = if trailing_shape.is_empty() {
2602 merged
2603 } else {
2604 ast::SelectStmt {
2605 result: Expr::Shape(Box::new(ast::ShapeExpr {
2606 expr: Some(merged.result.clone()),
2607 elements: trailing_shape.to_vec(),
2608 marker_offset: None,
2609 })),
2610 ..merged
2611 }
2612 };
2613 let ir = self.compile_stmt(&Stmt::Select(merged))?;
2614 Ok(Some(ir))
2615 }
2616
2617 fn try_compile_alias_select(
2618 &mut self,
2619 outer: &ast::SelectStmt,
2620 result: &Expr,
2621 distinct: bool,
2622 ) -> Result<Option<IrStmt>, PyQLError> {
2623 let (path_name, shape_elements): (&str, &[ast::ShapeElement]) = match result {
2625 Expr::Path(p) if !p.partial && p.steps.len() == 1 => {
2626 if let ast::PathStep::Name(n) = &p.steps[0] {
2627 (n.as_str(), &[])
2628 } else {
2629 return Ok(None);
2630 }
2631 }
2632 Expr::Shape(sh) => match sh.expr.as_ref() {
2633 Some(Expr::Path(p)) if !p.partial && p.steps.len() == 1 => {
2634 if let ast::PathStep::Name(n) = &p.steps[0] {
2635 (n.as_str(), sh.elements.as_slice())
2636 } else {
2637 return Ok(None);
2638 }
2639 }
2640 _ => return Ok(None),
2641 },
2642 _ => return Ok(None),
2643 };
2644
2645 let alias = self
2647 .schema
2648 .aliases
2649 .iter()
2650 .find(|a| a.name == path_name || format!("{}::{}", a.module, a.name) == path_name);
2651 let alias = match alias {
2652 Some(a) => a.clone(),
2653 None => return Ok(None),
2654 };
2655
2656 let inner_ast = crate::parse::parse(&alias.expr)?;
2657 let inner_sel = match inner_ast {
2658 Stmt::Select(sel) => sel,
2659 _ => return Err(self.type_err(&format!("alias '{}' expression must be a select statement", alias.name))),
2660 };
2661
2662 let inner_base = match &inner_sel.result {
2674 Expr::Shape(sh) => sh.expr.clone(),
2675 other => Some(other.clone()),
2676 };
2677 let merged_result = if shape_elements.is_empty() {
2678 inner_sel.result.clone()
2679 } else {
2680 Expr::Shape(Box::new(ast::ShapeExpr {
2681 expr: inner_base,
2682 elements: shape_elements.to_vec(),
2683 marker_offset: None,
2684 }))
2685 };
2686
2687 let merged_filter = match (&inner_sel.filter, &outer.filter) {
2688 (Some(a), Some(b)) => Some(Expr::BinOp(Box::new(ast::BinOp {
2689 left: a.clone(),
2690 op: ast::BinOpKind::And,
2691 right: b.clone(),
2692 }))),
2693 (Some(a), None) => Some(a.clone()),
2694 (None, b) => b.clone(),
2695 };
2696
2697 let merged = ast::SelectStmt {
2698 result: merged_result,
2699 filter: merged_filter,
2700 order_by: if outer.order_by.is_empty() {
2701 inner_sel.order_by.clone()
2702 } else {
2703 outer.order_by.clone()
2704 },
2705 offset: outer.offset.clone().or(inner_sel.offset.clone()),
2706 limit: outer.limit.clone().or(inner_sel.limit.clone()),
2707 lock: outer.lock.clone().or(inner_sel.lock.clone()),
2708 };
2709
2710 let _ = distinct; let ir = self.compile_stmt(&Stmt::Select(merged))?;
2712 Ok(Some(ir))
2713 }
2714
2715 fn compile_global(&mut self, raw_name: &str) -> Result<IrExpr, PyQLError> {
2716 let global = self
2717 .schema
2718 .globals
2719 .iter()
2720 .find(|g| g.name == raw_name || format!("{}::{}", g.module, g.name) == raw_name);
2721 let global = global
2722 .ok_or_else(|| {
2723 PyQLError::Resolution(PyQLResolutionError::UnknownField(PyQLUnknownFieldError {
2724 message: format!("unknown global: {:?}", raw_name),
2725 position: Position { line: 0, col: 0 },
2726 }))
2727 })?
2728 .clone();
2729
2730 let qualified = format!("{}::{}", global.module, global.name);
2731 let cte_name = format!("__global__{}", qualified);
2732
2733 if let Some(computed_expr) = global.computed_expr {
2734 if self.global_ctes.iter().any(|g| g.cte_name() == cte_name) {
2736 return Ok(IrExpr::GlobalRef { cte_name });
2737 }
2738 let inner_ast = crate::parse::parse(&computed_expr)?;
2740 let inner_stmt = self.compile_stmt(&inner_ast)?;
2741 self.global_ctes
2742 .push(IrGlobalCte::Computed(Box::new(IrComputedGlobalCte {
2743 cte_name: cte_name.clone(),
2744 qualified_name: qualified,
2745 stmt: inner_stmt,
2746 })));
2747 Ok(IrExpr::GlobalRef { cte_name })
2748 } else if self.in_fn_body {
2749 let pg_type = self.resolve_global_pg_type(&global.scalar_type);
2753 self.used_globals_arg = true;
2754 Ok(IrExpr::TypeCast(Box::new(super::IrTypeCast {
2759 expr: IrExpr::RawSql(globals_arg_read(&qualified, &pg_type)),
2760 pg_type,
2761 tuple_shape: None,
2762 })))
2763 } else {
2764 let pg_type = self.resolve_global_pg_type(&global.scalar_type);
2766 let param_name = format!("__global__{}", qualified);
2767 let index = self.param_index(¶m_name);
2768 if !self.global_ctes.iter().any(|g| g.cte_name() == cte_name) {
2770 self.global_ctes.push(IrGlobalCte::Session(IrSessionGlobalCte {
2771 cte_name,
2772 qualified_name: qualified,
2773 param_index: index,
2774 pg_type: pg_type.clone(),
2775 }));
2776 }
2777 Ok(IrExpr::GlobalParam { index, pg_type })
2778 }
2779 }
2780
2781 fn cte_object_type(&self, name: &str) -> Option<String> {
2786 self.cte_types.get(name).filter(|t| t.contains("::")).cloned()
2787 }
2788
2789 fn resolve_type(&self, name: &str) -> Result<&'a TypeDescriptor, PyQLError> {
2790 self.schema
2792 .types
2793 .iter()
2794 .find(|t| t.name == name || format!("{}::{}", t.module, t.name) == name)
2795 .ok_or_else(|| {
2796 PyQLError::Resolution(PyQLResolutionError::UnknownType(PyQLUnknownTypeError {
2797 message: format!("unknown type '{name}'"),
2798 position: Position { line: 0, col: 0 },
2799 }))
2800 })
2801 }
2802
2803 fn resolve_enum(&self, name: &str) -> Option<&'a crate::schema::EnumDescriptor> {
2808 self.schema
2809 .enums
2810 .iter()
2811 .find(|e| e.name == name || format!("{}::{}", e.module, e.name) == name)
2812 .or_else(|| crate::stdlib::lookup_enum(name))
2813 }
2814
2815 fn resolve_channel(&self, name: &str) -> Option<&'a crate::schema::ChannelDescriptor> {
2819 self.schema.find_channel(name)
2820 }
2821
2822 fn resolve_scalar(&self, name: &str) -> Option<&'a crate::schema::ScalarDescriptor> {
2828 self.schema
2829 .scalars
2830 .iter()
2831 .find(|s| s.name == name || format!("{}::{}", s.module, s.name) == name)
2832 }
2833
2834 fn resolve_named_tuple(&self, name: &str) -> Option<&'a crate::schema::NamedTupleDescriptor> {
2838 self.schema
2839 .named_tuples
2840 .iter()
2841 .find(|nt| nt.name == name || format!("{}::{}", nt.module, nt.name) == name)
2842 }
2843
2844 fn tuple_member_to_json_member(&self, m: &crate::schema::TupleMemberDescriptor) -> crate::query::JsonMember {
2848 use crate::schema::TupleMemberKind;
2849 let kind = match &m.kind {
2850 TupleMemberKind::Scalar { .. } => crate::query::JsonMemberKind::Scalar,
2851 TupleMemberKind::Enum { module, name } => crate::query::JsonMemberKind::Enum {
2852 enum_type: format!("{}::{}", module, name),
2853 },
2854 TupleMemberKind::NamedTuple { module, name } => {
2855 let qname = format!("{}::{}", module, name);
2856 let nested_members = self
2857 .resolve_named_tuple(&qname)
2858 .map(|nt| {
2859 nt.members
2860 .iter()
2861 .map(|mm| self.tuple_member_to_json_member(mm))
2862 .collect()
2863 })
2864 .unwrap_or_default();
2865 crate::query::JsonMemberKind::Tuple {
2866 type_name: Some(qname),
2867 members: nested_members,
2868 }
2869 }
2870 TupleMemberKind::Tuple { members } => crate::query::JsonMemberKind::Tuple {
2871 type_name: None,
2872 members: members.iter().map(|mm| self.tuple_member_to_json_member(mm)).collect(),
2873 },
2874 };
2875 crate::query::JsonMember {
2876 key: m.name.clone(),
2877 kind,
2878 }
2879 }
2880
2881 fn ast_tuple_element_to_json_member(&self, elem: &ast::TupleTypeElement) -> crate::query::JsonMember {
2884 crate::query::JsonMember {
2885 key: elem.name.clone(),
2886 kind: self.ast_type_expr_to_json_member_kind(&elem.ty),
2887 }
2888 }
2889
2890 fn ast_type_expr_to_json_member_kind(&self, ty: &ast::TypeExpr) -> crate::query::JsonMemberKind {
2891 if let ast::TypeExpr::Tuple { elements } = ty {
2892 return crate::query::JsonMemberKind::Tuple {
2893 type_name: None,
2894 members: elements
2895 .iter()
2896 .map(|e| self.ast_tuple_element_to_json_member(e))
2897 .collect(),
2898 };
2899 }
2900 let Some((module, name)) = ty.as_named() else {
2901 return crate::query::JsonMemberKind::Scalar;
2902 };
2903 let qname = match module {
2904 Some(m) => format!("{}::{}", m, name),
2905 None => name.to_string(),
2906 };
2907 if let Some(ed) = self.resolve_enum(&qname) {
2908 return crate::query::JsonMemberKind::Enum {
2909 enum_type: format!("{}::{}", ed.module, ed.name),
2910 };
2911 }
2912 if let Some(nt) = self.resolve_named_tuple(&qname) {
2913 return crate::query::JsonMemberKind::Tuple {
2914 type_name: Some(format!("{}::{}", nt.module, nt.name)),
2915 members: nt.members.iter().map(|m| self.tuple_member_to_json_member(m)).collect(),
2916 };
2917 }
2918 crate::query::JsonMemberKind::Scalar
2919 }
2920
2921 fn resolve_tuple_cast_shape(&self, ty: &ast::TypeExpr) -> Option<TupleCastShape> {
2927 match ty {
2928 ast::TypeExpr::Tuple { elements } => Some(TupleCastShape {
2929 type_name: None,
2930 members: elements
2931 .iter()
2932 .map(|e| self.ast_tuple_element_to_json_member(e))
2933 .collect(),
2934 }),
2935 _ => {
2936 let (module, name) = ty.as_named()?;
2937 let qname = match module {
2938 Some(m) => format!("{}::{}", m, name),
2939 None => name.to_string(),
2940 };
2941 let nt = self.resolve_named_tuple(&qname)?;
2942 Some(TupleCastShape {
2943 type_name: Some(format!("{}::{}", nt.module, nt.name)),
2944 members: nt.members.iter().map(|m| self.tuple_member_to_json_member(m)).collect(),
2945 })
2946 }
2947 }
2948 }
2949
2950 fn type_expr_to_display_str(&self, ty: &ast::TypeExpr) -> String {
2954 match ty {
2955 ast::TypeExpr::Tuple { elements } => {
2956 let inner = elements
2957 .iter()
2958 .map(|e| match &e.name {
2959 Some(n) => format!("{}: {}", n, self.type_expr_to_display_str(&e.ty)),
2960 None => self.type_expr_to_display_str(&e.ty),
2961 })
2962 .collect::<Vec<_>>()
2963 .join(", ");
2964 format!("tuple<{}>", inner)
2965 }
2966 ast::TypeExpr::Array { element } => format!("array<{}>", self.type_expr_to_display_str(element)),
2967 ast::TypeExpr::Named { module, name } => match module {
2968 Some(m) => format!("{}::{}", m, name),
2969 None => type_expr_to_pg(ty)
2970 .map(|pg| pg_type_to_pyql(&pg).to_string())
2971 .unwrap_or_else(|_| name.clone()),
2972 },
2973 }
2974 }
2975
2976 fn resolve_property_tuple_shape(&self, prop: &PropertyDescriptor) -> Option<TupleCastShape> {
2981 if let Some(qname) = prop.pg_type.strip_prefix("__nt__:") {
2982 let nt = self.resolve_named_tuple(qname)?;
2983 return Some(TupleCastShape {
2984 type_name: Some(qname.to_string()),
2985 members: nt.members.iter().map(|m| self.tuple_member_to_json_member(m)).collect(),
2986 });
2987 }
2988 prop.tuple_members.as_ref().map(|members| TupleCastShape {
2989 type_name: None,
2990 members: members.iter().map(|m| self.tuple_member_to_json_member(m)).collect(),
2991 })
2992 }
2993
2994 fn scalar_cast_free_select(
3001 &mut self,
3002 tc: &ast::TypeCast,
3003 pg_type: String,
3004 distinct: bool,
3005 ) -> Result<IrStmt, PyQLError> {
3006 let cast_expr = match &tc.ty {
3007 ast::TypeExpr::Tuple { elements } => {
3008 let tuple_shape = self.resolve_tuple_cast_shape(&tc.ty);
3009 match self.try_compile_tuple_literal_cast_ctx(elements, &tc.expr, None)? {
3010 Some(ir) => IrExpr::TypeCast(Box::new(IrTypeCast {
3015 expr: ir,
3016 pg_type,
3017 tuple_shape,
3018 })),
3019 None => {
3020 let inner = self.compile_expr_ctx(&tc.expr, None)?;
3021 IrExpr::TypeCast(Box::new(IrTypeCast {
3022 expr: inner,
3023 pg_type,
3024 tuple_shape,
3025 }))
3026 }
3027 }
3028 }
3029 _ => {
3030 let inner = self.compile_expr_ctx(&tc.expr, None)?;
3031 let tuple_shape = self.resolve_tuple_cast_shape(&tc.ty);
3032 IrExpr::TypeCast(Box::new(IrTypeCast {
3033 expr: inner,
3034 pg_type,
3035 tuple_shape,
3036 }))
3037 }
3038 };
3039 Ok(IrStmt::Select(IrSelect {
3040 rows: vec![IrRowSource::Free(IrFreeExpr::Scalar(cast_expr))],
3041 filter: None,
3042 order_by: vec![],
3043 offset: None,
3044 limit: None,
3045 distinct,
3046 dml_source: None,
3047 polymorphic: false,
3048 poly_implementors: vec![],
3049 poly_columns: vec![],
3050 lock: None,
3051 }))
3052 }
3053
3054 fn resolve_cast_pg_type(&self, ty: &ast::TypeExpr) -> Result<String, PyQLError> {
3063 if matches!(ty, ast::TypeExpr::Tuple { .. }) {
3064 return Ok("jsonb".to_string());
3065 }
3066 if let ast::TypeExpr::Array { element } = ty {
3067 let element_pg = self.resolve_cast_pg_type(element)?;
3068 return Ok(format!("{}[]", element_pg));
3069 }
3070 let (module, name) = ty.as_named().expect("checked above: not Tuple/Array");
3071 let qname = match module {
3072 Some(m) => format!("{}::{}", m, name),
3073 None => name.to_string(),
3074 };
3075 if let Some(ed) = self.resolve_enum(&qname) {
3076 return Ok(format!("{}.\"{}\"", crate::sql::pg_schema_str(&ed.module), ed.name));
3077 }
3078 if self.resolve_named_tuple(&qname).is_some() {
3079 return Ok("jsonb".to_string());
3080 }
3081 if let Some(sd) = self.resolve_scalar(&qname) {
3087 return Ok(format!("{}.\"{}\"", crate::sql::pg_schema_str(&sd.module), sd.name));
3088 }
3089 type_expr_to_pg(ty)
3090 }
3091
3092 fn compile_tuple_element_cast_ctx(
3098 &mut self,
3099 target_ty: &ast::TypeExpr,
3100 value: &Expr,
3101 ctx: Option<(&TypeDescriptor, &str)>,
3102 ) -> Result<IrExpr, PyQLError> {
3103 if let ast::TypeExpr::Tuple { elements } = target_ty
3104 && let Some(ir) = self.try_compile_tuple_literal_cast_ctx(elements, value, ctx)?
3105 {
3106 return Ok(ir);
3107 }
3108 let inner = self.compile_expr_ctx(value, ctx)?;
3109 let pg_type = self.resolve_cast_pg_type(target_ty)?;
3110 Ok(IrExpr::TypeCast(Box::new(IrTypeCast {
3111 expr: inner,
3112 pg_type,
3113 tuple_shape: None,
3114 })))
3115 }
3116
3117 fn try_compile_tuple_literal_cast_ctx(
3126 &mut self,
3127 target_elements: &[ast::TupleTypeElement],
3128 source: &Expr,
3129 ctx: Option<(&TypeDescriptor, &str)>,
3130 ) -> Result<Option<IrExpr>, PyQLError> {
3131 let source_values: Vec<&Expr> = match source {
3132 Expr::Tuple(vals) if vals.len() == target_elements.len() => vals.iter().collect(),
3133 Expr::NamedTuple(fields) if fields.len() == target_elements.len() => {
3134 fields.iter().map(|(_, v)| v).collect()
3135 }
3136 _ => return Ok(None),
3137 };
3138 let named = target_elements.iter().all(|e| e.name.is_some());
3139 let mut casted = Vec::with_capacity(target_elements.len());
3140 for (elem, value) in target_elements.iter().zip(source_values) {
3141 casted.push(self.compile_tuple_element_cast_ctx(&elem.ty, value, ctx)?);
3142 }
3143 if named {
3144 let fields = target_elements
3145 .iter()
3146 .zip(casted)
3147 .map(|(e, v)| (e.name.clone().unwrap(), v))
3148 .collect();
3149 Ok(Some(IrExpr::NamedTuple {
3150 fields,
3151 is_free_object: false,
3152 }))
3153 } else {
3154 Ok(Some(IrExpr::Tuple(casted)))
3155 }
3156 }
3157
3158 fn try_compile_array_literal_cast_ctx(
3170 &mut self,
3171 element_ty: &ast::TypeExpr,
3172 source: &Expr,
3173 ctx: Option<(&TypeDescriptor, &str)>,
3174 ) -> Result<Option<IrExpr>, PyQLError> {
3175 let Expr::Array(elems) = source else { return Ok(None) };
3176 let casted = elems
3177 .iter()
3178 .map(|e| self.compile_tuple_element_cast_ctx(element_ty, e, ctx))
3179 .collect::<Result<Vec<_>, _>>()?;
3180 Ok(Some(IrExpr::Array(casted)))
3181 }
3182
3183 fn compile_enum_access(&self, type_ref: &str, variant: &str) -> Result<IrExpr, PyQLError> {
3184 let ed = self
3185 .resolve_enum(type_ref)
3186 .ok_or_else(|| self.type_err(&format!("unknown type '{}'", type_ref)))?;
3187 if !ed.members.iter().any(|m| m == variant) {
3188 return Err(self.type_err(&format!(
3189 "enum '{}::{}' has no member '{}'",
3190 ed.module, ed.name, variant
3191 )));
3192 }
3193 let pg_type = if crate::stdlib::lookup_enum(&format!("{}::{}", ed.module, ed.name)).is_some() {
3196 "text".to_string()
3197 } else {
3198 format!("{}.\"{}\"", crate::sql::pg_schema_str(&ed.module), ed.name)
3199 };
3200 Ok(IrExpr::EnumLiteral {
3201 pg_type,
3202 variant: variant.to_string(),
3203 })
3204 }
3205
3206 fn row_source_table(&self, name: &str, td: &TypeDescriptor) -> String {
3209 if let Some(cte) = self.for_var_ctes.get(name) {
3210 return self.cte_source_table(cte);
3211 }
3212 if self.cte_object_type(name).is_some() {
3213 return self.cte_source_table(&self.cte_sql_name(name));
3214 }
3215 td.table.clone()
3216 }
3217
3218 fn resolve_path_root(&self, root_name: &str) -> Result<&'a TypeDescriptor, PyQLError> {
3226 if let Some(qualified) = self.for_var_types.get(root_name) {
3227 return self.resolve_type(qualified);
3228 }
3229 match self.cte_types.get(root_name).filter(|t| t.contains("::")).cloned() {
3230 Some(bound) => self.resolve_type(&bound),
3231 None => self.resolve_type(root_name),
3232 }
3233 }
3234
3235 fn find_poly_implementors(&self, iface_qname: &str) -> Vec<IrPolyImplementor> {
3236 self.schema
3237 .types
3238 .iter()
3239 .filter(|t| !t.abstract_ && Self::is_or_implements(t, iface_qname))
3240 .map(|t| IrPolyImplementor {
3241 type_name: format!("{}::{}", t.module, t.name),
3242 table: t.table.clone(),
3243 module: t.module.clone(),
3244 })
3245 .collect()
3246 }
3247
3248 fn resolve_property<'t>(td: &'t TypeDescriptor, name: &str) -> Option<&'t PropertyDescriptor> {
3249 td.properties.iter().find(|p| p.name == name)
3250 }
3251
3252 fn resolve_link<'t>(td: &'t TypeDescriptor, name: &str) -> Option<&'t LinkDescriptor> {
3253 td.links.iter().find(|l| l.name == name)
3254 }
3255
3256 fn resolve_multilink<'t>(td: &'t TypeDescriptor, name: &str) -> Option<&'t MultiLinkDescriptor> {
3257 td.multilinks.iter().find(|m| m.name == name)
3258 }
3259
3260 fn compile_link_prop_ref(&mut self, prop_name: &str) -> Result<IrExpr, PyQLError> {
3264 let Some(scope) = self.link_prop_scope.last().cloned() else {
3265 return Err(self.type_err(&format!(
3266 "'@{prop_name}' is a link property, so it is only valid in the modifiers or shape of the \
3267 link it belongs to, e.g. 'locators: {{ … }} filter @{prop_name}'"
3268 )));
3269 };
3270 let Some((through_qname, junction_alias)) = scope else {
3271 return Err(self.type_err(&format!(
3272 "this link has no link properties, so '@{prop_name}' cannot be read — \
3273 declare the link with a `Through[...]` type to give it some"
3274 )));
3275 };
3276 let through_td = self.resolve_type(&through_qname)?;
3277 let Some(prop) = Self::resolve_property(through_td, prop_name) else {
3278 return Err(self.field_err(prop_name, &through_qname));
3279 };
3280 Ok(IrExpr::ColumnRef {
3281 alias: junction_alias,
3282 column: prop.name.clone(),
3283 pg_type: prop.pg_type.clone(),
3284 })
3285 }
3286
3287 fn link_target_reaches(&self, target: &str, current_qname: &str) -> bool {
3293 if target == current_qname {
3294 return true;
3295 }
3296 self.resolve_type(current_qname)
3297 .is_ok_and(|td| td.interfaces.iter().any(|i| i == target))
3298 }
3299
3300 fn resolve_computed(&self, td: &TypeDescriptor, name: &str) -> Option<crate::schema::ComputedDescriptor> {
3306 if let Some(cd) = td.computed.iter().find(|c| c.name == name) {
3307 return Some(cd.clone());
3308 }
3309 td.interfaces
3310 .iter()
3311 .filter_map(|iface| self.resolve_type(iface).ok())
3312 .find_map(|itd| itd.computed.iter().find(|c| c.name == name))
3313 .cloned()
3314 }
3315
3316 fn compile_stmt(&mut self, stmt: &Stmt) -> Result<IrStmt, PyQLError> {
3319 match stmt {
3320 Stmt::Select(s) => {
3321 let (distinct, result) = match &s.result {
3322 Expr::UnaryOp(u) if u.op == ast::UnaryOpKind::Distinct => (true, &u.operand),
3323 Expr::Detached(inner) => {
3332 self.pending_detached = true;
3333 (false, inner.as_ref())
3334 }
3335 other => (false, other),
3336 };
3337
3338 if let Expr::Shape(sh) = result
3342 && let Some(Expr::FunctionCall(assert)) = &sh.expr
3343 && assert.module.as_deref().is_none_or(|m| m == "std")
3344 && matches!(assert.name.as_str(), "assert_exists" | "assert_distinct")
3345 && let [Expr::FunctionCall(inner)] = assert.args.as_slice()
3346 && let Some(mut fs) = self.try_compile_fn_object_select(inner, &sh.elements, s, distinct)?
3347 {
3348 let message = self.assert_message(assert, None)?;
3349 let unchecked = ast::SelectStmt {
3350 result: Expr::FunctionCall(inner.clone()),
3351 filter: None,
3352 order_by: vec![],
3353 offset: None,
3354 limit: None,
3355 lock: None,
3356 };
3357 let every_row = self
3358 .try_compile_fn_object_select(inner, &[], &unchecked, false)?
3359 .ok_or_else(|| self.type_err("assert subject is not an object-returning function"))?;
3360 let checked = IrExpr::FunctionCall(IrFunctionCall {
3361 return_pg_type: None,
3362 schema: Some("_pylon".to_string()),
3363 name: assert.name.clone(),
3364 args: std::iter::once(IrExpr::ArrayFromSelect(Box::new(IrArraySource::StmtColumn {
3365 stmt: Box::new(IrStmt::FunctionSelect(every_row)),
3366 column: "id".to_string(),
3367 })))
3368 .chain(message)
3369 .collect(),
3370 sql_template: None,
3371 });
3372 let check = IrExpr::BinOp(Box::new(IrBinOp {
3373 left: IrExpr::FunctionCall(IrFunctionCall {
3374 return_pg_type: None,
3375 schema: None,
3376 name: "cardinality".to_string(),
3377 args: vec![checked],
3378 sql_template: None,
3379 }),
3380 op: ast::BinOpKind::Ge,
3381 right: IrExpr::Literal(IrLiteral::Int(0)),
3382 }));
3383 fs.filter = and_conditions(fs.filter, vec![check]);
3384 return Ok(IrStmt::FunctionSelect(fs));
3385 }
3386 if let Some(flattened) = flatten_shape_subject(result) {
3387 let mut flat = s.clone();
3388 flat.result = match distinct {
3389 true => Expr::UnaryOp(Box::new(ast::UnaryOp {
3390 op: ast::UnaryOpKind::Distinct,
3391 operand: flattened,
3392 })),
3393 false => flattened,
3394 };
3395 return self.compile_stmt(&Stmt::Select(flat));
3396 }
3397 if let Expr::Shape(sh) = result
3398 && let Some(Expr::SubQuery(inner)) = &sh.expr
3399 && let Stmt::Group(g) = inner.as_ref()
3400 {
3401 return self.compile_group_projection(s, g, &sh.elements).map(IrStmt::Group);
3402 }
3403
3404 if let Some(ir) = self.try_compile_alias_select(s, result, distinct)? {
3406 return Ok(ir);
3407 }
3408
3409 if let Some(ir) = self.try_compile_global_select(s, result, distinct)? {
3411 return Ok(ir);
3412 }
3413
3414 if let Some(ir) = self.try_compile_field_access_select(s, result)? {
3417 return Ok(ir);
3418 }
3419
3420 if let Expr::Shape(sh) = result
3422 && let Some(Expr::FunctionCall(fc)) = sh.expr.as_ref()
3423 && let Some(ir) = self.try_compile_fn_object_select(fc, &sh.elements, s, distinct)?
3424 {
3425 return Ok(IrStmt::FunctionSelect(ir));
3426 }
3427
3428 if let Expr::FunctionCall(fc) = result
3430 && let Some(ir) = self.try_compile_fn_object_select(fc, &[], s, distinct)?
3431 {
3432 return Ok(IrStmt::FunctionSelect(ir));
3433 }
3434
3435 if let Expr::Shape(sh) = result
3437 && let Some(Expr::FunctionCall(fc)) = sh.expr.as_ref()
3438 {
3439 if let Some(ir) = self.try_compile_vector_search(fc, &sh.elements, s)? {
3440 return Ok(IrStmt::VectorSearch(ir));
3441 }
3442 if let Some(ir) = self.try_compile_fts_search(fc, &sh.elements, s)? {
3443 return Ok(IrStmt::FtsSearch(ir));
3444 }
3445 }
3446 if let Expr::FunctionCall(fc) = result {
3448 if let Some(ir) = self.try_compile_vector_search(fc, &[], s)? {
3449 return Ok(IrStmt::VectorSearch(ir));
3450 }
3451 if let Some(ir) = self.try_compile_fts_search(fc, &[], s)? {
3452 return Ok(IrStmt::FtsSearch(ir));
3453 }
3454 }
3455
3456 if let Expr::Shape(sh) = result
3461 && let Some(Expr::TypeCast(tc)) = sh.expr.as_ref()
3462 && let Some((module, name)) = tc.ty.as_named()
3463 && module
3464 .map(|m| !["std", "cal", "math", "sys", "pgvector", "crypto", "postgis"].contains(&m))
3465 .unwrap_or(false)
3466 {
3467 let id_filter = Expr::BinOp(Box::new(ast::BinOp {
3468 left: Expr::Path(ast::Path::relative("id")),
3469 op: ast::BinOpKind::Eq,
3470 right: tc.expr.clone(),
3471 }));
3472 let merged_filter = match &s.filter {
3473 None => Some(id_filter),
3474 Some(existing) => Some(Expr::BinOp(Box::new(ast::BinOp {
3475 left: id_filter,
3476 op: ast::BinOpKind::And,
3477 right: existing.clone(),
3478 }))),
3479 };
3480 let qname = match module {
3483 Some(m) => format!("{}::{}", m, name),
3484 None => name.to_string(),
3485 };
3486 let synthetic = ast::SelectStmt {
3487 result: Expr::Shape(Box::new(ast::ShapeExpr {
3488 expr: Some(Expr::Path(ast::Path::absolute(&qname))),
3489 elements: sh.elements.clone(),
3490 marker_offset: None,
3491 })),
3492 filter: merged_filter,
3493 order_by: s.order_by.clone(),
3494 offset: s.offset.clone(),
3495 limit: s.limit.clone(),
3496 lock: s.lock.clone(),
3497 };
3498 return self
3499 .compile_select(&synthetic, &synthetic.result, distinct)
3500 .map(IrStmt::Select);
3501 }
3502 const STDLIB_MODULES: &[&str] = &["std", "cal", "math", "sys", "pgvector", "crypto", "postgis"];
3507 if let Expr::TypeCast(tc) = result {
3508 if matches!(&tc.ty, ast::TypeExpr::Tuple { .. }) {
3512 return self.scalar_cast_free_select(tc, "jsonb".to_string(), distinct);
3513 }
3514 if let Some((module, name)) = tc.ty.as_named() {
3515 let qname = match module {
3516 Some(m) => format!("{}::{}", m, name),
3517 None => name.to_string(),
3518 };
3519 if let Some(ed) = self.resolve_enum(&qname) {
3520 let pg_type = format!("{}.\"{}\"", crate::sql::pg_schema_str(&ed.module), ed.name);
3521 return self.scalar_cast_free_select(tc, pg_type, distinct);
3522 }
3523 if self.resolve_named_tuple(&qname).is_some() {
3524 return self.scalar_cast_free_select(tc, "jsonb".to_string(), distinct);
3525 }
3526 if let Some(sd) = self.resolve_scalar(&qname) {
3527 let pg_type = format!("{}.\"{}\"", crate::sql::pg_schema_str(&sd.module), sd.name);
3528 return self.scalar_cast_free_select(tc, pg_type, distinct);
3529 }
3530 if module.map(|m| !STDLIB_MODULES.contains(&m)).unwrap_or(false) {
3531 return self.compile_schema_cast_select(s, tc).map(IrStmt::Select);
3532 }
3533 }
3534 }
3535 if let Expr::Path(p) = result {
3537 if !p.partial
3538 && p.steps.len() == 2
3539 && let [ast::PathStep::Name(type_ref), ast::PathStep::Name(variant)] = p.steps.as_slice()
3540 && self.resolve_enum(type_ref).is_some()
3541 {
3542 let expr = self.compile_enum_access(type_ref, variant)?;
3543 return Ok(IrStmt::Select(IrSelect {
3544 rows: vec![IrRowSource::Free(IrFreeExpr::Scalar(expr))],
3545 filter: None,
3546 order_by: vec![],
3547 offset: None,
3548 limit: None,
3549 distinct,
3550 dml_source: None,
3551 polymorphic: false,
3552 poly_implementors: vec![],
3553 poly_columns: vec![],
3554 lock: None,
3555 }));
3556 }
3557 if let Some(resolved) = self.resolve_cte_path(p) {
3563 let expr = resolved?;
3564 return Ok(IrStmt::Select(IrSelect {
3565 rows: vec![IrRowSource::Free(IrFreeExpr::Scalar(expr))],
3566 filter: None,
3567 order_by: vec![],
3568 offset: None,
3569 limit: None,
3570 distinct,
3571 dml_source: None,
3572 polymorphic: false,
3573 poly_implementors: vec![],
3574 poly_columns: vec![],
3575 lock: None,
3576 }));
3577 }
3578 if !p.partial && p.steps.len() > 1 {
3579 return self.compile_path_select(s, p, &[], distinct).map(IrStmt::PathSelect);
3580 }
3581 }
3582 if let Expr::Shape(sh) = result
3583 && let Some(Expr::Path(p)) = sh.expr.as_ref()
3584 && !p.partial
3585 && p.steps.len() > 1
3586 {
3587 return self
3588 .compile_path_select(s, p, &sh.elements, distinct)
3589 .map(IrStmt::PathSelect);
3590 }
3591 if let Expr::FunctionCall(f) = result
3593 && (f.module.is_none() || f.module.as_deref() == Some("std"))
3594 && matches!(f.name.as_str(), "assert_exists" | "assert_distinct")
3595 && !f.args.is_empty()
3596 && let Expr::SubQuery(inner_stmt) = &f.args[0]
3597 {
3598 let offset = s.offset.as_ref().map(|e| self.compile_free_expr(e)).transpose()?;
3599 let limit = s.limit.as_ref().map(|e| self.compile_free_expr(e)).transpose()?;
3600 let message = self.assert_message(f, None)?;
3601 if let Ok(type_name) = self.dml_subject_type(inner_stmt)
3605 && let Ok(td) = self.resolve_type(&type_name)
3606 {
3607 let td_module = td.module.clone();
3608 let td_name = td.name.clone();
3609 let td_table = td.table.clone();
3610 let pk = td
3611 .properties
3612 .iter()
3613 .find(|p| p.is_pk)
3614 .map(|p| (p.name.clone(), p.pg_type.clone()))
3615 .unwrap_or_else(|| ("id".to_string(), "uuid".to_string()));
3616 let inner_ir = self.compile_stmt(inner_stmt)?;
3617 let alias = self.fresh_alias();
3618 let vetted = IrExpr::FunctionCall(IrFunctionCall {
3619 return_pg_type: None,
3620 schema: Some("_pylon".to_string()),
3621 name: f.name.clone(),
3622 args: std::iter::once(IrExpr::ArrayFromSelect(Box::new(IrArraySource::StmtColumn {
3623 stmt: Box::new(inner_ir),
3624 column: pk.0.clone(),
3625 })))
3626 .chain(message)
3627 .collect(),
3628 sql_template: None,
3629 });
3630 let filter = IrExpr::BinOp(Box::new(IrBinOp {
3631 left: IrExpr::ColumnRef {
3632 alias: alias.clone(),
3633 column: pk.0.clone(),
3634 pg_type: pk.1.clone(),
3635 },
3636 op: ast::BinOpKind::In,
3637 right: vetted,
3638 }));
3639 let shape = vec![IrShapePointer::Scalar(IrScalarPointer {
3640 implicit_id: false,
3641 marker_offset: None,
3642 alias: "id".to_string(),
3643 column: pk.0,
3644 pg_type: pk.1,
3645 tuple_shape: None,
3646 })];
3647 let source = IrSource {
3648 poly: None,
3649 type_name: format!("{td_module}::{td_name}"),
3650 table: td_table,
3651 alias,
3652 };
3653 let mut select = IrSelect::schema_bound(source, shape, Some(filter));
3654 select.offset = offset;
3655 select.limit = limit;
3656 select.distinct = distinct;
3657 return Ok(IrStmt::Select(select));
3658 }
3659 let inner = self.compile_subquery_to_array_source(inner_stmt)?;
3660 return Ok(IrStmt::Select(IrSelect {
3661 rows: vec![IrRowSource::Free(IrFreeExpr::AssertSet {
3662 fn_name: f.name.clone(),
3663 inner: Box::new(inner),
3664 message,
3665 })],
3666 filter: None,
3667 order_by: vec![],
3668 offset,
3669 limit,
3670 distinct,
3671 dml_source: None,
3672 polymorphic: false,
3673 poly_implementors: vec![],
3674 poly_columns: vec![],
3675 lock: None,
3676 }));
3677 }
3678 if let Some(expr) = self.aggregate_over_unpacked(s, result, distinct)? {
3682 return Ok(IrStmt::Select(IrSelect {
3683 rows: vec![IrRowSource::Free(IrFreeExpr::Scalar(expr))],
3684 filter: None,
3685 order_by: vec![],
3686 offset: None,
3687 limit: None,
3688 distinct: false,
3689 dml_source: None,
3690 polymorphic: false,
3691 poly_implementors: vec![],
3692 poly_columns: vec![],
3693 lock: None,
3694 }));
3695 }
3696 if let Expr::BinOp(b) = result
3704 && matches!(b.op, ast::BinOpKind::In | ast::BinOpKind::NotIn)
3705 && let Some(root) = self.object_set_name(&b.left)
3706 {
3707 let rewritten = Expr::BinOp(Box::new(ast::BinOp {
3708 left: Expr::Path(ast::Path {
3709 steps: vec![ast::PathStep::Name(root.clone()), ast::PathStep::Name("id".into())],
3710 partial: false,
3711 }),
3712 op: b.op.clone(),
3713 right: b.right.clone(),
3714 }));
3715 return self
3716 .compile_expr_as_path_select(s, &rewritten, &root, distinct)
3717 .map(IrStmt::PathSelect);
3718 }
3719 if let Some(root) = self.find_path_root_in_expr(result) {
3721 return self
3722 .compile_expr_as_path_select(s, result, &root, distinct)
3723 .map(IrStmt::PathSelect);
3724 }
3725 if let Expr::TypeIs { expr, ty } = result
3727 && let Expr::Path(p) = expr.as_ref()
3728 && !p.partial
3729 && p.steps.len() == 1
3730 && let ast::PathStep::Name(src_name) = &p.steps[0]
3731 && self.resolve_name_ref(src_name, true).is_none()
3732 {
3733 let src_td = self.resolve_type(src_name)?;
3734 {
3735 let src_qname = format!("{}::{}", src_td.module, src_td.name);
3736 let (ty_module, ty_name) = ty
3737 .as_named()
3738 .ok_or_else(|| self.type_err("cannot use IS with a tuple or array type"))?;
3739 let check_module = ty_module.unwrap_or(&src_td.module);
3740 let check_name = format!("{}::{}", check_module, ty_name);
3741 self.resolve_type(&check_name)?;
3742 let check_qname = check_name;
3743 let src_table = src_td.table.clone();
3744 let src_alias = self.fresh_alias();
3745 let src_td = self.resolve_type(&src_qname)?;
3746 let poly_implementors = if self.is_polymorphic(src_td) {
3747 self.find_poly_implementors(&src_qname)
3748 } else {
3749 vec![]
3750 };
3751 let bool_expr = self.type_check_bool_expr(&src_qname, &check_qname, src_td, &src_alias);
3752 let ps = IrPathSelect {
3753 root: IrSource {
3754 poly: None,
3755 type_name: src_qname,
3756 table: src_table,
3757 alias: src_alias,
3758 },
3759 joins: vec![],
3760 result: IrPathResult::Scalar(bool_expr, None),
3761 filter: s
3762 .filter
3763 .as_ref()
3764 .map(|_| Err(self.type_err("FILTER is not supported on type-is SELECT")))
3765 .transpose()?,
3766 order_by: vec![],
3767 offset: None,
3768 limit: None,
3769 distinct,
3770 poly_implementors,
3771 };
3772 return Ok(IrStmt::PathSelect(ps));
3773 }
3774 }
3775 if let Expr::Union(a, b) = result
3776 && !distinct
3777 && s.filter.is_none()
3778 && s.order_by.is_empty()
3779 && s.offset.is_none()
3780 && s.limit.is_none()
3781 && (self.is_scalar_walk(a) || self.is_scalar_walk(b))
3782 {
3783 return self.compile_scalar_union(result);
3784 }
3785 if !distinct
3787 && s.filter.is_none()
3788 && s.order_by.is_empty()
3789 && s.offset.is_none()
3790 && s.limit.is_none()
3791 && let Some(ps) = self.compile_shape_field_select(result, None)?
3792 {
3793 return Ok(IrStmt::PathSelect(ps));
3794 }
3795 if let Expr::Shape(sh) = result
3802 && let Some(subject) = sh.expr.as_ref()
3803 && !matches!(subject, Expr::Path(_))
3804 {
3805 let (base, fields) = Self::peel_field_access_chain(subject);
3806 if let Expr::SubQuery(inner_stmt) = base
3807 && !fields.is_empty()
3808 && matches!(inner_stmt.as_ref(), Stmt::Insert(_) | Stmt::Update(_) | Stmt::Delete(_))
3809 {
3810 let inner = self.compile_stmt(inner_stmt)?;
3811 let cte_name = self.fresh_nested_cte_name();
3812 let overrides = Self::junction_overrides_for(&inner, &cte_name);
3815 let type_name = self.register_cte(&cte_name, &inner);
3816 self.hoisted_ctes.push(IrCteDef {
3817 name: cte_name.clone(),
3818 stmt: inner,
3819 type_name,
3820 correlated_to: None,
3821 });
3822 let mut steps = vec![ast::PathStep::Name(cte_name)];
3823 steps.extend(fields.into_iter().map(ast::PathStep::Name));
3824 let rerooted = ast::SelectStmt {
3825 result: Expr::Shape(Box::new(ast::ShapeExpr {
3826 expr: Some(Expr::Path(ast::Path { steps, partial: false })),
3827 elements: sh.elements.clone(),
3828 marker_offset: None,
3829 })),
3830 filter: s.filter.clone(),
3831 order_by: s.order_by.clone(),
3832 offset: s.offset.clone(),
3833 limit: s.limit.clone(),
3834 lock: s.lock.clone(),
3835 };
3836 let previous = std::mem::replace(&mut self.junction_read_overrides, overrides);
3837 let compiled = self.compile_stmt(&Stmt::Select(rerooted));
3838 self.junction_read_overrides = previous;
3839 return compiled;
3840 }
3841 }
3842 if let Expr::Shape(sh) = result
3848 && let Some(Expr::SubQuery(inner_stmt)) = sh.expr.as_ref()
3849 && matches!(inner_stmt.as_ref(), Stmt::Select(_))
3850 && self.dml_subject_type(inner_stmt).is_err()
3851 {
3852 let inner = self.compile_stmt(inner_stmt)?;
3853 let cte_name = self.fresh_nested_cte_name();
3854 let type_name = self.register_cte(&cte_name, &inner);
3855 self.hoisted_ctes.push(IrCteDef {
3856 name: cte_name.clone(),
3857 stmt: inner,
3858 type_name,
3859 correlated_to: None,
3860 });
3861 let rerooted = ast::SelectStmt {
3862 result: Expr::Shape(Box::new(ast::ShapeExpr {
3863 expr: Some(Expr::Path(ast::Path {
3864 steps: vec![ast::PathStep::Name(cte_name)],
3865 partial: false,
3866 })),
3867 elements: sh.elements.clone(),
3868 marker_offset: None,
3869 })),
3870 filter: s.filter.clone(),
3871 order_by: s.order_by.clone(),
3872 offset: s.offset.clone(),
3873 limit: s.limit.clone(),
3874 lock: s.lock.clone(),
3875 };
3876 return self.compile_stmt(&Stmt::Select(rerooted));
3877 }
3878 if let Expr::SubQuery(inner) = result
3884 && matches!(inner.as_ref(), Stmt::For(_))
3885 && !distinct
3886 && s.filter.is_none()
3887 && s.order_by.is_empty()
3888 && s.offset.is_none()
3889 && s.limit.is_none()
3890 {
3891 return self.compile_stmt(inner);
3892 }
3893 self.check_union_type_compat(result)?;
3895 if self.is_free_result(result) {
3896 self.compile_free_select(s, result, distinct).map(IrStmt::Select)
3897 } else {
3898 self.compile_select(s, result, distinct).map(IrStmt::Select)
3899 }
3900 }
3901 Stmt::Insert(s) => self.compile_insert(s).map(IrStmt::Insert),
3902 Stmt::Update(s) => self.compile_update(s).map(IrStmt::Update),
3903 Stmt::Delete(s) => self.compile_delete(s).map(IrStmt::Delete),
3904 Stmt::Group(s) => self.compile_group(s).map(IrStmt::Group),
3905 Stmt::With(w) => {
3909 for alias in &w.aliases {
3910 if self.bind_inline_if_correlated(&alias.name, &alias.expr, None)?
3911 || self.bind_group(&alias.name, &alias.expr)?
3912 {
3913 continue;
3914 }
3915 let (ir_inner, correlated_to) = self.compile_binding_in_scope(&alias.expr)?;
3916 let sql_name = self.claim_cte_sql_name(&alias.name);
3917 if let Some(slot) = correlated_to.as_deref().and_then(|i| i.strip_prefix("_for_")) {
3918 self.correlated_bindings.insert(sql_name.clone(), slot.to_string());
3919 }
3920 let type_name = self.register_cte(&alias.name, &ir_inner);
3921 self.hoisted_binding_sources
3922 .insert(sql_name.clone(), alias.expr.clone());
3923 self.hoisted_ctes.push(IrCteDef {
3924 name: sql_name,
3925 stmt: ir_inner,
3926 type_name,
3927 correlated_to,
3928 });
3929 }
3930 self.compile_stmt(&w.stmt)
3931 }
3932 Stmt::For(f) => match self.compile_group_elements(f)? {
3933 Some(grp) => Ok(IrStmt::Group(grp)),
3934 None => self.compile_for(f).map(IrStmt::For),
3935 },
3936 Stmt::Analyze(inner) => self.compile_stmt(inner),
3940 }
3941 }
3942
3943 fn compile_schema_cast_select(&mut self, sel: &ast::SelectStmt, tc: &ast::TypeCast) -> Result<IrSelect, PyQLError> {
3947 let id_filter = Expr::BinOp(Box::new(ast::BinOp {
3948 left: Expr::Path(ast::Path::relative("id")),
3949 op: ast::BinOpKind::Eq,
3950 right: tc.expr.clone(),
3951 }));
3952 let merged_filter = match &sel.filter {
3953 None => Some(id_filter),
3954 Some(existing) => Some(Expr::BinOp(Box::new(ast::BinOp {
3955 left: id_filter,
3956 op: ast::BinOpKind::And,
3957 right: existing.clone(),
3958 }))),
3959 };
3960 let (module, name) = tc
3963 .ty
3964 .as_named()
3965 .ok_or_else(|| self.type_err("cannot use a tuple or array type as a schema object cast"))?;
3966 let qname = match module {
3971 Some(m) => format!("{}::{}", m, name),
3972 None => name.to_string(),
3973 };
3974 let synthetic = ast::SelectStmt {
3975 result: Expr::Path(ast::Path::absolute(&qname)),
3976 filter: merged_filter,
3977 order_by: sel.order_by.clone(),
3978 offset: sel.offset.clone(),
3979 limit: sel.limit.clone(),
3980 lock: sel.lock.clone(),
3981 };
3982 self.compile_select(&synthetic, &synthetic.result, false)
3983 }
3984
3985 fn compile_shape_anchored(
3994 &mut self,
3995 elements: &[ShapeElement],
3996 td: &'a TypeDescriptor,
3997 alias: &str,
3998 module: &str,
3999 ) -> Result<Vec<IrShapePointer>, PyQLError> {
4000 self.anchors.push(SelectAnchor {
4001 type_name: td.name.clone(),
4002 qualified: format!("{}::{}", td.module, td.name),
4003 alias: alias.to_string(),
4004 detached: false,
4005 declared_on: None,
4006 });
4007 let result = self.compile_shape(elements, td, alias, module);
4008 self.anchors.pop();
4009 result
4010 }
4011
4012 fn compile_path_select(
4013 &mut self,
4014 sel: &ast::SelectStmt,
4015 path: &ast::Path,
4016 shape_elements: &[ShapeElement],
4017 distinct: bool,
4018 ) -> Result<IrPathSelect, PyQLError> {
4019 self.compile_path_select_with_tail(sel, path, shape_elements, distinct, 0, &[])
4020 }
4021
4022 fn compile_path_select_with_tail(
4027 &mut self,
4028 sel: &ast::SelectStmt,
4029 path: &ast::Path,
4030 shape_elements: &[ShapeElement],
4031 distinct: bool,
4032 tail: usize,
4033 tail_sorts: &[ast::SortExpr],
4034 ) -> Result<IrPathSelect, PyQLError> {
4035 let outer_anchor = self.modifier_anchor.take();
4036 let outer_sorts = std::mem::replace(&mut self.tail_sorts, tail_sorts.to_vec());
4039 let mut result = self.compile_path_select_inner(sel, path, shape_elements, distinct, tail);
4040 self.tail_sorts = outer_sorts;
4041 self.modifier_anchor = outer_anchor;
4042 if let Ok(path_select) = &mut result {
4043 self.resolve_join_fanouts(path_select);
4044 }
4045 result
4046 }
4047
4048 fn compile_path_select_inner(
4049 &mut self,
4050 sel: &ast::SelectStmt,
4051 path: &ast::Path,
4052 shape_elements: &[ShapeElement],
4053 distinct: bool,
4054 mut tail: usize,
4055 ) -> Result<IrPathSelect, PyQLError> {
4056 use ast::PathStep;
4057
4058 let root_name = match &path.steps[0] {
4059 PathStep::Name(n) => n.as_str(),
4060 _ => return Err(self.type_err("path traversal must start with a type name")),
4061 };
4062 let root_td = self.resolve_path_root(root_name)?;
4069 let root_alias = self.fresh_alias();
4070 let root = IrSource {
4071 poly: None,
4072 type_name: format!("{}::{}", root_td.module, root_td.name),
4073 table: self.row_source_table(root_name, root_td),
4074 alias: root_alias.clone(),
4075 };
4076 let for_var_root = self.for_var_types.contains_key(root_name).then(|| {
4080 IrExpr::BinOp(Box::new(IrBinOp {
4081 left: IrExpr::ColumnRef {
4082 alias: root_alias.clone(),
4083 column: "id".to_string(),
4084 pg_type: "uuid".to_string(),
4085 },
4086 op: ast::BinOpKind::Eq,
4087 right: self.for_var_ref(root_name),
4088 }))
4089 });
4090
4091 let mut joins: Vec<IrPathJoin> = vec![];
4092 let mut current_td = root_td;
4093 let mut current_alias = root_alias;
4094
4095 let mut steps: Vec<PathStep> = path.steps[1..].to_vec();
4099 let mut pending_filters: Vec<(usize, Expr)> = vec![];
4102 let mut extra_conditions: Vec<IrExpr> = for_var_root.into_iter().collect();
4103 let mut junction_scope: Option<(String, String)> = None;
4106 let mut splices = 0usize;
4107 let mut idx = 0;
4108 if tail > 0 && tail >= steps.len() {
4109 self.modifier_anchor = Some((
4110 format!("{}::{}", current_td.module, current_td.name),
4111 current_alias.clone(),
4112 ));
4113 }
4114 while idx < steps.len() {
4115 let owned_step = steps[idx].clone();
4116 let step = &owned_step;
4117 let n_steps = steps.len();
4118 let is_last = |extra: usize| idx + extra == n_steps - 1;
4119 if tail > 0 && idx == n_steps - tail {
4120 self.modifier_anchor = Some((
4121 format!("{}::{}", current_td.module, current_td.name),
4122 current_alias.clone(),
4123 ));
4124 }
4125
4126 while let Some(pos) = pending_filters.iter().position(|(i, _)| *i + 1 == idx) {
4129 let (_, f) = pending_filters.remove(pos);
4130 let cond = self.as_condition(|c| c.compile_expr(&f, current_td, ¤t_alias))?;
4131 extra_conditions.push(cond);
4132 }
4133
4134 if let PathStep::TypeIntersection(type_ref) = step {
4136 let type_name = match &type_ref.module {
4137 Some(m) => format!("{}::{}", m, type_ref.name),
4138 None => type_ref.name.clone(),
4139 };
4140 let narrowed = self.resolve_type(&type_name)?;
4141 let narrowed_has_relation = !Self::backs_no_relation(narrowed);
4148 if narrowed_has_relation && narrowed.table != current_td.table {
4149 let target_alias = self.fresh_alias();
4150 let target = IrSource {
4151 poly: None,
4152 type_name: format!("{}::{}", narrowed.module, narrowed.name),
4153 table: narrowed.table.clone(),
4154 alias: target_alias.clone(),
4155 };
4156 joins.push(IrPathJoin::Single {
4157 source_alias: current_alias.clone(),
4158 fk_col: "id".to_string(),
4159 target,
4160 });
4161 current_alias = target_alias;
4162 }
4163 current_td = narrowed;
4164 idx += 1;
4165 continue;
4166 }
4167
4168 if let PathStep::Backlink(link_name) = step {
4170 let owner_hint = steps.get(idx + 1).and_then(|s| {
4172 if let PathStep::TypeIntersection(tr) = s {
4173 Some(tr.clone())
4174 } else {
4175 None
4176 }
4177 });
4178 let consumed_extra = if owner_hint.is_some() { 1 } else { 0 };
4179
4180 let owner_td: &TypeDescriptor = if let Some(ref tr) = owner_hint {
4181 let type_name = match &tr.module {
4182 Some(m) => format!("{}::{}", m, tr.name),
4183 None => tr.name.clone(),
4184 };
4185 let td = self.resolve_type(&type_name)?;
4186 let current_qname = format!("{}::{}", current_td.module, current_td.name);
4187 let link_targets_current =
4188 td.links
4189 .iter()
4190 .any(|l| l.name == *link_name && self.link_target_reaches(&l.target, ¤t_qname))
4191 || td.multilinks.iter().any(|ml| {
4192 ml.name == *link_name && self.link_target_reaches(&ml.target, ¤t_qname)
4193 });
4194 if link_targets_current {
4195 td
4196 } else {
4197 let narrowed = format!("{}::{}", td.module, td.name);
4200 let declaring: Vec<&'a TypeDescriptor> = self
4201 .schema
4202 .types
4203 .iter()
4204 .filter(|t| !t.abstract_ && Self::is_or_implements(t, &narrowed))
4205 .filter(|t| self.declares_backlink(t, link_name, ¤t_qname))
4206 .collect();
4207 match Self::without_inherited_owners(declaring).as_slice() {
4208 [only] => only,
4209 _ => {
4210 return Err(self.type_err(&format!(
4211 "link '{}::{}' does not target '{}'; backlink is not valid here",
4212 type_name, link_name, current_qname,
4213 )));
4214 }
4215 }
4216 }
4217 } else {
4218 let current_qname = format!("{}::{}", current_td.module, current_td.name);
4220 self.schema
4221 .types
4222 .iter()
4223 .filter(|t| {
4225 t.bases.is_empty()
4226 || !t.bases.iter().any(|base| {
4227 self.resolve_type(base).is_ok_and(|b| {
4228 b.links.iter().any(|l| l.name == *link_name)
4229 || b.multilinks.iter().any(|ml| ml.name == *link_name)
4230 })
4231 })
4232 })
4233 .find(|t| {
4234 t.links
4235 .iter()
4236 .any(|l| l.name == *link_name && self.link_target_reaches(&l.target, ¤t_qname))
4237 || t.multilinks.iter().any(|ml| {
4238 ml.name == *link_name && self.link_target_reaches(&ml.target, ¤t_qname)
4239 })
4240 })
4241 .ok_or_else(|| {
4242 self.type_err(&format!(
4243 "no type has a link '{}' targeting '{}'",
4244 link_name, current_qname,
4245 ))
4246 })?
4247 };
4248
4249 let target_alias = self.fresh_alias();
4250 let target = IrSource {
4251 poly: None,
4252 type_name: format!("{}::{}", owner_td.module, owner_td.name),
4253 table: owner_td.table.clone(),
4254 alias: target_alias.clone(),
4255 };
4256
4257 if let Some(l) = owner_td.links.iter().find(|l| l.name == *link_name) {
4261 if l.is_junction_backed() {
4262 let junction_alias = self.fresh_alias();
4263 let (junction_table, module, owner_col, current_col, _) =
4264 self.link_junction_info(owner_td, l)?;
4265 joins.push(IrPathJoin::BacklinkMulti {
4266 source_alias: current_alias.clone(),
4267 junction_alias,
4268 junction_table,
4269 module,
4270 owner_col,
4271 current_col,
4272 target,
4273 });
4274 } else {
4275 joins.push(IrPathJoin::BacklinkSingle {
4276 source_alias: current_alias.clone(),
4277 fk_col: format!("{}_id", link_name),
4278 target,
4279 });
4280 }
4281 } else {
4282 let ml = owner_td
4283 .multilinks
4284 .iter()
4285 .find(|ml| ml.name == *link_name)
4286 .unwrap()
4287 .clone();
4288 let junction_alias = self.fresh_alias();
4289 let (junction_table, module, _, _, _) = self.multilink_junction_info(owner_td, &ml)?;
4290 joins.push(IrPathJoin::BacklinkMulti {
4291 source_alias: current_alias.clone(),
4292 junction_alias,
4293 junction_table,
4294 module,
4295 owner_col: "source".to_string(),
4296 current_col: "target".to_string(),
4297 target,
4298 });
4299 }
4300
4301 if is_last(consumed_extra) {
4302 let shape =
4303 self.compile_shape_anchored(shape_elements, owner_td, &target_alias, &owner_td.module.clone())?;
4304 let result = IrPathResult::Object {
4305 alias: target_alias.clone(),
4306 type_name: format!("{}::{}", owner_td.module, owner_td.name),
4307 shape,
4308 };
4309 let (filter, order_by, offset, limit) = self.compile_path_modifiers_scoped(
4310 sel,
4311 owner_td,
4312 &target_alias,
4313 junction_scope.clone(),
4314 shape_elements,
4315 )?;
4316 return Ok(IrPathSelect {
4317 root,
4318 joins,
4319 result,
4320 filter: and_conditions(filter, extra_conditions),
4321 order_by,
4322 offset,
4323 limit,
4324 distinct,
4325 poly_implementors: vec![],
4326 });
4327 }
4328 current_td = owner_td;
4329 current_alias = target_alias;
4330 idx += 1 + consumed_extra;
4331 continue;
4332 }
4333
4334 let step_name = match step {
4335 PathStep::Name(n) => n.as_str(),
4336 _ => return Err(self.type_err("only name steps are supported in path traversal")),
4337 };
4338
4339 if step_name == "__type__" {
4343 if !is_last(0) {
4344 return Err(
4345 self.type_err("'__type__' is the type's name, not an object — it cannot be traversed further")
4346 );
4347 }
4348 let polymorphic = self.is_polymorphic(current_td);
4349 let expr = if polymorphic {
4350 IrExpr::ColumnRef {
4351 alias: current_alias.clone(),
4352 column: "__type__".to_string(),
4353 pg_type: "text".to_string(),
4354 }
4355 } else {
4356 IrExpr::Literal(IrLiteral::Str(format!("{}::{}", current_td.module, current_td.name)))
4357 };
4358 let poly_implementors = if polymorphic && joins.is_empty() && !root.table.starts_with("@cte:") {
4363 self.find_poly_implementors(&root.type_name.clone())
4364 } else {
4365 vec![]
4366 };
4367 let (filter, order_by, offset, limit) = self.compile_path_modifiers_scoped(
4368 sel,
4369 current_td,
4370 ¤t_alias,
4371 junction_scope.clone(),
4372 shape_elements,
4373 )?;
4374 return Ok(IrPathSelect {
4375 root,
4376 joins,
4377 result: IrPathResult::Scalar(expr, None),
4378 filter: and_conditions(filter, extra_conditions),
4379 order_by,
4380 offset,
4381 limit,
4382 distinct,
4383 poly_implementors,
4384 });
4385 }
4386
4387 if let Some(p) = current_td.properties.iter().find(|p| p.name == step_name) {
4389 if !is_last(0) {
4390 if p.pg_type.starts_with("__nt__:") || p.tuple_members.is_some() {
4394 let base = IrExpr::ColumnRef {
4395 alias: current_alias.clone(),
4396 column: p.name.clone(),
4397 pg_type: p.pg_type.clone(),
4398 };
4399 let remaining = &steps[idx + 1..];
4400 let mut ir: IrExpr = base;
4401 for step in remaining {
4402 let field = match step {
4403 ast::PathStep::Name(n) => n.clone(),
4404 _ => return Err(self.type_err("only field name steps are valid inside a named tuple")),
4405 };
4406 ir = IrExpr::JsonbField {
4407 expr: Box::new(ir),
4408 field,
4409 };
4410 }
4411 let (filter, order_by, offset, limit) = self.compile_path_modifiers_scoped(
4412 sel,
4413 current_td,
4414 ¤t_alias,
4415 junction_scope.clone(),
4416 shape_elements,
4417 )?;
4418 return Ok(IrPathSelect {
4419 root,
4420 joins,
4421 result: IrPathResult::Scalar(ir, None),
4422 filter: and_conditions(filter, extra_conditions),
4423 order_by,
4424 offset,
4425 limit,
4426 distinct,
4427 poly_implementors: vec![],
4428 });
4429 }
4430 return Err(self.type_err(&format!(
4431 "'{step_name}' is a scalar property, not a link — cannot traverse further"
4432 )));
4433 }
4434 let tuple_shape = self.resolve_property_tuple_shape(p);
4435 let result = IrPathResult::Scalar(
4436 IrExpr::ColumnRef {
4437 alias: current_alias.clone(),
4438 column: p.name.clone(),
4439 pg_type: p.pg_type.clone(),
4440 },
4441 tuple_shape,
4442 );
4443 let (filter, order_by, offset, limit) = self.compile_path_modifiers_scoped(
4444 sel,
4445 current_td,
4446 ¤t_alias,
4447 junction_scope.clone(),
4448 shape_elements,
4449 )?;
4450 return Ok(IrPathSelect {
4451 root,
4452 joins,
4453 result,
4454 filter: and_conditions(filter, extra_conditions),
4455 order_by,
4456 offset,
4457 limit,
4458 distinct,
4459 poly_implementors: vec![],
4460 });
4461 }
4462
4463 if let Some(l) = Self::resolve_link(current_td, step_name) {
4465 let target_td = self.resolve_type(&l.target)?;
4466 let target_alias = self.fresh_alias();
4467 let target = IrSource {
4468 poly: None,
4469 type_name: format!("{}::{}", target_td.module, target_td.name),
4470 table: target_td.table.clone(),
4471 alias: target_alias.clone(),
4472 };
4473 if l.is_junction_backed() {
4474 let join = self.build_multilink_join(current_td, &l.name, &l.target, &l.through)?;
4479 let junction_alias = self.fresh_alias();
4480 junction_scope = l.through.clone().map(|t| (t, junction_alias.clone()));
4481 joins.push(IrPathJoin::Multi {
4482 source_alias: current_alias.clone(),
4483 junction_alias,
4484 join,
4485 target,
4486 });
4487 } else {
4488 joins.push(IrPathJoin::Single {
4489 source_alias: current_alias.clone(),
4490 fk_col: format!("{}_id", l.name),
4491 target,
4492 });
4493 }
4494 if is_last(0) {
4495 let shape = self.compile_shape_anchored(
4496 shape_elements,
4497 target_td,
4498 &target_alias,
4499 &target_td.module.clone(),
4500 )?;
4501 let result = IrPathResult::Object {
4502 alias: target_alias.clone(),
4503 type_name: format!("{}::{}", target_td.module, target_td.name),
4504 shape,
4505 };
4506 let (filter, order_by, offset, limit) = self.compile_path_modifiers_scoped(
4507 sel,
4508 target_td,
4509 &target_alias,
4510 junction_scope.clone(),
4511 shape_elements,
4512 )?;
4513 return Ok(IrPathSelect {
4514 root,
4515 joins,
4516 result,
4517 filter: and_conditions(filter, extra_conditions),
4518 order_by,
4519 offset,
4520 limit,
4521 distinct,
4522 poly_implementors: vec![],
4523 });
4524 }
4525 current_td = target_td;
4526 current_alias = target_alias;
4527 idx += 1;
4528 continue;
4529 }
4530
4531 if let Some(ml) = Self::resolve_multilink(current_td, step_name) {
4533 let target_td = self.resolve_type(&ml.target)?;
4534 let target_alias = self.fresh_alias();
4535 let junction_alias = self.fresh_alias();
4536 let override_for_step = self
4540 .junction_read_overrides
4541 .get(&self.owner_junction(current_td, &ml.name, None))
4542 .cloned();
4543 let target = IrSource {
4544 poly: None,
4545 type_name: format!("{}::{}", target_td.module, target_td.name),
4546 table: match override_for_step.as_ref().and_then(|o| o.targets.clone()) {
4547 Some(cte) => cte,
4548 None => target_td.table.clone(),
4549 },
4550 alias: target_alias.clone(),
4551 };
4552 let join_info = if let Some(through_qname) = &ml.through {
4553 let through_td = self.resolve_type(through_qname)?;
4554 if through_td.junction {
4555 IrMultiLinkJoin::Standard {
4558 junction_table: self.owner_junction(current_td, &ml.name, Some(through_td)),
4559 module: current_td.module.clone(),
4560 }
4561 } else {
4562 let source_qname = format!("{}::{}", current_td.module, current_td.name);
4563 let source_col = through_td
4564 .links
4565 .iter()
4566 .find(|l| l.target == source_qname)
4567 .ok_or_else(|| {
4568 PyQLError::Type(PyQLTypeError {
4569 message: format!("through type {through_qname} has no link to {source_qname}"),
4570 position: Position { line: 0, col: 0 },
4571 })
4572 })?
4573 .name
4574 .clone();
4575 let target_col = through_td
4576 .links
4577 .iter()
4578 .find(|l| l.target == ml.target && l.name != source_col)
4579 .or_else(|| through_td.links.iter().find(|l| l.target == ml.target))
4580 .ok_or_else(|| {
4581 PyQLError::Type(PyQLTypeError {
4582 message: format!(
4583 "through type {through_qname} has no link to target {}",
4584 ml.target
4585 ),
4586 position: Position { line: 0, col: 0 },
4587 })
4588 })?
4589 .name
4590 .clone();
4591 IrMultiLinkJoin::Through {
4592 junction_table: through_td.table.clone(),
4593 module: through_td.module.clone(),
4594 source_col,
4595 target_col,
4596 }
4597 }
4598 } else {
4599 let junction_table = self.owner_junction(current_td, &ml.name, None);
4600 IrMultiLinkJoin::Standard {
4601 junction_table: match &override_for_step {
4602 Some(o) => o.junction.clone(),
4603 None => junction_table,
4604 },
4605 module: current_td.module.clone(),
4606 }
4607 };
4608 junction_scope = ml.through.clone().map(|t| (t, junction_alias.clone()));
4609 joins.push(IrPathJoin::Multi {
4610 source_alias: current_alias.clone(),
4611 junction_alias,
4612 join: join_info,
4613 target,
4614 });
4615 if is_last(0) {
4616 let shape = self.compile_shape_anchored(
4617 shape_elements,
4618 target_td,
4619 &target_alias,
4620 &target_td.module.clone(),
4621 )?;
4622 let result = IrPathResult::Object {
4623 alias: target_alias.clone(),
4624 type_name: format!("{}::{}", target_td.module, target_td.name),
4625 shape,
4626 };
4627 let (filter, order_by, offset, limit) = self.compile_path_modifiers_scoped(
4628 sel,
4629 target_td,
4630 &target_alias,
4631 junction_scope.clone(),
4632 shape_elements,
4633 )?;
4634 return Ok(IrPathSelect {
4635 root,
4636 joins,
4637 result,
4638 filter: and_conditions(filter, extra_conditions),
4639 order_by,
4640 offset,
4641 limit,
4642 distinct,
4643 poly_implementors: vec![],
4644 });
4645 }
4646 current_td = target_td;
4647 current_alias = target_alias;
4648 idx += 1;
4649 continue;
4650 }
4651
4652 let declared = match self.resolve_computed(current_td, step_name) {
4657 Some(cd) => Some(crate::parse::parse_pointer_expr(&cd.expression).map_err(PyQLError::Syntax)?),
4658 None if idx == 0 => self
4659 .active_declared_pointers
4660 .iter()
4661 .find(|d| path_leaf(&d.path).is_ok_and(|n| n == step_name))
4662 .and_then(|d| d.compexpr.clone()),
4663 None => None,
4664 };
4665 if let Some(parsed) = declared {
4666 let rewritten = Self::field_access_over_select(&parsed);
4669 let expr_ast = match &rewritten {
4670 Some((sel, _)) => Expr::SubQuery(Box::new(Stmt::Select(sel.clone()))),
4671 None => parsed,
4672 };
4673 let field_steps = rewritten.as_ref().map(|(_, n)| *n).unwrap_or(0);
4674
4675 if let Some((p, _, modifiers)) = Self::pointer_subject(&expr_ast)
4687 && p.partial
4688 && !p.steps.is_empty()
4689 && (!is_last(0)
4690 || self
4691 .walk_path_types(current_td, &p.steps, MAX_COMPUTED_SPLICES)
4692 .1
4693 .is_some())
4694 {
4695 if let Some(m) = modifiers
4700 && (m.limit.is_some() || m.offset.is_some() || !m.order_by.is_empty())
4701 {
4702 let mut inner_steps =
4703 vec![PathStep::Name(format!("{}::{}", current_td.module, current_td.name))];
4704 inner_steps.extend(p.steps.iter().cloned());
4705 let inner_path = ast::Path {
4706 steps: inner_steps,
4707 partial: false,
4708 };
4709 let inner_sel = ast::SelectStmt {
4710 result: Expr::Path(inner_path.clone()),
4711 filter: m.filter.clone(),
4712 order_by: m.order_by.clone(),
4713 offset: m.offset.clone(),
4714 limit: m.limit.clone(),
4715 lock: None,
4716 };
4717 let mut inner =
4718 self.compile_path_select_with_tail(&inner_sel, &inner_path, &[], false, field_steps, &[])?;
4719 Self::correlate_path_select(&mut inner, ¤t_alias);
4720 let IrPathResult::Object { type_name, .. } = &inner.result else {
4721 return Err(self.type_err(&format!(
4722 "computed pointer '{step_name}' is a scalar — a path cannot continue through it"
4723 )));
4724 };
4725 let target_td = self.resolve_type(&type_name.clone())?;
4726 let target_alias = self.fresh_alias();
4727 let target = IrSource {
4728 poly: None,
4729 type_name: format!("{}::{}", target_td.module, target_td.name),
4730 table: target_td.table.clone(),
4731 alias: target_alias.clone(),
4732 };
4733 joins.push(IrPathJoin::Lateral {
4734 inner: Box::new(inner),
4735 target,
4736 });
4737 if is_last(0) {
4738 let module = target_td.module.clone();
4739 let shape =
4740 self.compile_shape_anchored(shape_elements, target_td, &target_alias, &module)?;
4741 let result = IrPathResult::Object {
4742 alias: target_alias.clone(),
4743 type_name: format!("{}::{}", target_td.module, target_td.name),
4744 shape,
4745 };
4746 let (filter, order_by, offset, limit) = self.compile_path_modifiers_scoped(
4747 sel,
4748 target_td,
4749 &target_alias,
4750 junction_scope,
4751 shape_elements,
4752 )?;
4753 return Ok(IrPathSelect {
4754 root,
4755 joins,
4756 result,
4757 filter: and_conditions(filter, extra_conditions),
4758 order_by,
4759 offset,
4760 limit,
4761 distinct,
4762 poly_implementors: vec![],
4763 });
4764 }
4765 current_td = target_td;
4766 current_alias = target_alias;
4767 idx += 1;
4768 continue;
4769 }
4770 splices += 1;
4771 if splices > MAX_COMPUTED_SPLICES {
4772 return Err(self.type_err(&format!(
4773 "computed pointer '{step_name}' expands into itself — \
4774 a path cannot be resolved through a cycle of computed pointers"
4775 )));
4776 }
4777 let filter = modifiers.and_then(|m| m.filter.clone());
4778 let spliced = p.steps.clone();
4779 if tail > 0 && idx >= steps.len() - tail {
4780 tail += spliced.len() - 1;
4781 }
4782 let landing = idx + spliced.len() - 1;
4783 steps.splice(idx..idx + 1, spliced);
4784 let shift = landing - idx;
4787 for (i, _) in pending_filters.iter_mut() {
4788 if *i > idx {
4789 *i += shift;
4790 }
4791 }
4792 if let Some(f) = filter {
4793 pending_filters.push((landing, f));
4794 }
4795 continue;
4796 }
4797
4798 if let Some((fc, modifiers)) = Self::function_subject(&expr_ast)
4804 && let Some(fd) = self.resolve_object_fn(fc)
4805 {
4806 if fd.params.len() != fc.args.len() {
4807 return Err(self.type_err(&format!(
4808 "function '{}::{}' expects {} argument(s), got {}",
4809 fd.module,
4810 fd.name,
4811 fd.params.len(),
4812 fc.args.len()
4813 )));
4814 }
4815 let (fn_module, fn_name, return_type_name) =
4816 (fd.module.clone(), fd.name.clone(), fd.return_pg_type.clone());
4817 let mut args = fc
4818 .args
4819 .iter()
4820 .map(|a| self.compile_expr(a, current_td, ¤t_alias))
4821 .collect::<Result<Vec<_>, _>>()?;
4822 let qualified = format!("{fn_module}::{fn_name}");
4823 if let Some(globals) = self.globals_arg_for_call(&qualified)? {
4824 args.insert(0, globals);
4825 }
4826 let target_td = self.resolve_type(&return_type_name)?;
4827 let target_alias = self.fresh_alias();
4828 let target = IrSource {
4829 poly: None,
4830 type_name: format!("{}::{}", target_td.module, target_td.name),
4831 table: target_td.table.clone(),
4832 alias: target_alias.clone(),
4833 };
4834 joins.push(IrPathJoin::Function {
4835 fn_module,
4836 fn_name,
4837 args,
4838 target,
4839 });
4840 if let Some(f) = modifiers.and_then(|m| m.filter.clone()) {
4841 let cond = self.as_condition(|c| c.compile_expr(&f, target_td, &target_alias))?;
4842 extra_conditions.push(cond);
4843 }
4844 if is_last(0) {
4845 let shape = self.compile_shape_anchored(
4846 shape_elements,
4847 target_td,
4848 &target_alias,
4849 &target_td.module.clone(),
4850 )?;
4851 let result = IrPathResult::Object {
4852 alias: target_alias.clone(),
4853 type_name: format!("{}::{}", target_td.module, target_td.name),
4854 shape,
4855 };
4856 let (filter, order_by, offset, limit) = self.compile_path_modifiers_scoped(
4857 sel,
4858 target_td,
4859 &target_alias,
4860 junction_scope.clone(),
4861 shape_elements,
4862 )?;
4863 return Ok(IrPathSelect {
4864 root,
4865 joins,
4866 result,
4867 filter: and_conditions(filter, extra_conditions),
4868 order_by,
4869 offset,
4870 limit,
4871 distinct,
4872 poly_implementors: vec![],
4873 });
4874 }
4875 current_td = target_td;
4876 current_alias = target_alias;
4877 idx += 1;
4878 continue;
4879 }
4880
4881 if !is_last(0) {
4882 return Err(self.type_err(&format!(
4883 "'{step_name}' is a computed pointer — it has no stored column to traverse further through"
4884 )));
4885 }
4886 let expanding = format!("{}::{}.{step_name}", current_td.module, current_td.name);
4887 if self.expanding_computeds.contains(&expanding) {
4888 return Err(self.type_err(&format!(
4889 "computed pointer '{step_name}' expands into itself — \
4890 a path cannot be resolved through a cycle of computed pointers"
4891 )));
4892 }
4893 self.expanding_computeds.push(expanding);
4894 let expr = self.compile_expr(&expr_ast, current_td, ¤t_alias);
4895 self.expanding_computeds.pop();
4896 let expr = expr?;
4897 let (filter, order_by, offset, limit) = self.compile_path_modifiers_scoped(
4898 sel,
4899 current_td,
4900 ¤t_alias,
4901 junction_scope.clone(),
4902 shape_elements,
4903 )?;
4904 return Ok(IrPathSelect {
4905 root,
4906 joins,
4907 result: IrPathResult::Scalar(expr, None),
4908 filter: and_conditions(filter, extra_conditions),
4909 order_by,
4910 offset,
4911 limit,
4912 distinct,
4913 poly_implementors: vec![],
4914 });
4915 }
4916
4917 return Err(self.field_err(step_name, &format!("{}::{}", current_td.module, current_td.name)));
4918 }
4919
4920 if matches!(steps.last(), Some(PathStep::TypeIntersection(_))) {
4926 let module = current_td.module.clone();
4927 let type_name = format!("{}::{}", current_td.module, current_td.name);
4928 let shape = self.compile_shape_anchored(shape_elements, current_td, ¤t_alias, &module)?;
4929 let (filter, order_by, offset, limit) = self.compile_path_modifiers_scoped(
4930 sel,
4931 current_td,
4932 ¤t_alias,
4933 junction_scope.clone(),
4934 shape_elements,
4935 )?;
4936 return Ok(IrPathSelect {
4937 root,
4938 joins,
4939 result: IrPathResult::Object {
4940 alias: current_alias,
4941 type_name,
4942 shape,
4943 },
4944 filter: and_conditions(filter, extra_conditions),
4945 order_by,
4946 offset,
4947 limit,
4948 distinct,
4949 poly_implementors: vec![],
4950 });
4951 }
4952
4953 Err(self.type_err("empty path traversal"))
4955 }
4956
4957 fn compile_path_modifiers_scoped(
4962 &mut self,
4963 sel: &ast::SelectStmt,
4964 td: &TypeDescriptor,
4965 alias: &str,
4966 junction: Option<(String, String)>,
4967 shape: &[ShapeElement],
4968 ) -> Result<SelectModifiers, PyQLError> {
4969 let pushed = junction.is_some();
4970 if pushed {
4971 self.link_prop_scope.push(junction);
4972 }
4973 let outer_declared = self.active_declared_pointers.clone();
4978 self.active_declared_pointers
4979 .extend(shape.iter().filter(|el| el.compexpr.is_some()).cloned());
4980 let anchored = self.modifier_anchor.take();
4981 let tail_sorts = std::mem::take(&mut self.tail_sorts);
4982 let result = match &anchored {
4983 Some((qualified, anchor_alias)) => {
4984 let anchor_td = self.resolve_type(qualified)?;
4985 let anchor_alias = anchor_alias.clone();
4986 self.compile_path_modifiers(sel, anchor_td, &anchor_alias)
4987 }
4988 None => self.compile_path_modifiers(sel, td, alias),
4989 };
4990 let result = result.and_then(|(filter, mut order_by, offset, limit)| {
4991 if !tail_sorts.is_empty() {
4992 let sorting = ast::SelectStmt {
4993 result: sel.result.clone(),
4994 filter: None,
4995 order_by: tail_sorts,
4996 offset: None,
4997 limit: None,
4998 lock: None,
4999 };
5000 let (_, sorts, _, _) = self.compile_path_modifiers(&sorting, td, alias)?;
5001 order_by = sorts;
5002 }
5003 Ok((filter, order_by, offset, limit))
5004 });
5005 if pushed {
5006 self.link_prop_scope.pop();
5007 }
5008 self.active_declared_pointers = outer_declared;
5009 result
5010 }
5011
5012 fn compile_path_modifiers(
5013 &mut self,
5014 sel: &ast::SelectStmt,
5015 td: &TypeDescriptor,
5016 alias: &str,
5017 ) -> Result<SelectModifiers, PyQLError> {
5018 self.anchors.push(SelectAnchor {
5019 type_name: td.name.clone(),
5020 qualified: format!("{}::{}", td.module, td.name),
5021 alias: alias.to_string(),
5022 detached: std::mem::take(&mut self.pending_detached),
5023 declared_on: None,
5024 });
5025 let result = self.compile_path_modifiers_inner(sel, td, alias);
5026 self.anchors.pop();
5027 result
5028 }
5029
5030 fn compile_path_modifiers_inner(
5031 &mut self,
5032 sel: &ast::SelectStmt,
5033 td: &TypeDescriptor,
5034 alias: &str,
5035 ) -> Result<SelectModifiers, PyQLError> {
5036 let filter = sel
5037 .filter
5038 .as_ref()
5039 .map(|f| self.as_condition(|c| c.compile_expr(f, td, alias)))
5040 .transpose()?;
5041 let order_by = sel
5042 .order_by
5043 .iter()
5044 .map(|s| self.compile_sort(s, td, alias))
5045 .collect::<Result<Vec<_>, _>>()?;
5046 let offset = sel
5047 .offset
5048 .as_ref()
5049 .map(|e| self.compile_expr(e, td, alias))
5050 .transpose()?;
5051 let limit = sel
5052 .limit
5053 .as_ref()
5054 .map(|e| self.compile_expr(e, td, alias))
5055 .transpose()?;
5056 Ok((filter, order_by, offset, limit))
5057 }
5058
5059 fn object_set_name(&self, expr: &Expr) -> Option<String> {
5066 let Expr::Path(p) = expr else { return None };
5067 if p.partial || p.steps.len() != 1 {
5068 return None;
5069 }
5070 let ast::PathStep::Name(name) = &p.steps[0] else {
5071 return None;
5072 };
5073 if self.cte_object_type(name).is_some() {
5074 return Some(name.clone());
5075 }
5076 let bound = self.for_vars.contains_key(name)
5080 || self.cte_free_items.contains_key(name)
5081 || self.inline_bindings.contains_key(name)
5082 || self.cte_types.contains_key(name)
5083 || self.fn_params.contains_key(name);
5084 if !bound && self.resolve_type(name).is_ok() {
5085 return Some(name.clone());
5086 }
5087 None
5088 }
5089
5090 fn find_path_root_in_expr(&self, expr: &Expr) -> Option<String> {
5093 match expr {
5094 Expr::Path(p) if !p.partial && p.steps.len() > 1 => {
5095 if let ast::PathStep::Name(root) = &p.steps[0]
5096 && (self.resolve_type(root).is_ok() || self.cte_object_type(root).is_some())
5097 {
5098 return Some(root.clone());
5099 }
5100 None
5101 }
5102 Expr::FunctionCall(f) => f.args.iter().find_map(|a| self.find_path_root_in_expr(a)),
5103 Expr::BinOp(b) => self
5104 .find_path_root_in_expr(&b.left)
5105 .or_else(|| self.find_path_root_in_expr(&b.right)),
5106 Expr::UnaryOp(u) => self.find_path_root_in_expr(&u.operand),
5107 _ => None,
5108 }
5109 }
5110
5111 fn rewrite_abs_to_partial(expr: Expr, root_name: &str) -> Expr {
5113 match expr {
5114 Expr::Path(ref p) if !p.partial => {
5115 if let ast::PathStep::Name(first) = &p.steps[0]
5116 && first == root_name
5117 && p.steps.len() > 1
5118 {
5119 return Expr::Path(ast::Path {
5120 steps: p.steps[1..].to_vec(),
5121 partial: true,
5122 });
5123 }
5124 expr
5125 }
5126 Expr::FunctionCall(f) => Expr::FunctionCall(ast::FunctionCall {
5127 module: f.module,
5128 name: f.name,
5129 args: f
5130 .args
5131 .into_iter()
5132 .map(|a| Self::rewrite_abs_to_partial(a, root_name))
5133 .collect(),
5134 kwargs: f.kwargs,
5135 }),
5136 Expr::BinOp(b) => Expr::BinOp(Box::new(ast::BinOp {
5137 left: Self::rewrite_abs_to_partial(b.left, root_name),
5138 op: b.op,
5139 right: Self::rewrite_abs_to_partial(b.right, root_name),
5140 })),
5141 Expr::UnaryOp(u) => Expr::UnaryOp(Box::new(ast::UnaryOp {
5142 op: u.op,
5143 operand: Self::rewrite_abs_to_partial(u.operand, root_name),
5144 })),
5145 other => other,
5146 }
5147 }
5148
5149 fn shape_alias_paths(elements: &[ShapeElement]) -> Option<Vec<(String, ast::Path)>> {
5155 let mut defs = Vec::with_capacity(elements.len());
5156 for element in elements {
5157 let [ast::PathStep::Name(name)] = element.path.steps.as_slice() else {
5158 return None;
5159 };
5160 match &element.compexpr {
5161 Some(Expr::Path(p)) if p.partial => defs.push((name.clone(), p.clone())),
5162 _ => return None,
5163 }
5164 }
5165 Some(defs)
5166 }
5167
5168 fn substitute_shape_aliases(expr: Expr, defs: &[(String, ast::Path)]) -> Expr {
5172 match expr {
5173 Expr::Path(ref p) if p.partial => {
5174 let Some(ast::PathStep::Name(first)) = p.steps.first() else {
5175 return expr;
5176 };
5177 let Some((_, definition)) = defs.iter().find(|(name, _)| name == first) else {
5178 return expr;
5179 };
5180 let mut steps = definition.steps.clone();
5181 steps.extend(p.steps[1..].iter().cloned());
5182 Expr::Path(ast::Path { steps, partial: true })
5183 }
5184 Expr::FunctionCall(f) => Expr::FunctionCall(ast::FunctionCall {
5185 module: f.module,
5186 name: f.name,
5187 args: f
5188 .args
5189 .into_iter()
5190 .map(|a| Self::substitute_shape_aliases(a, defs))
5191 .collect(),
5192 kwargs: f.kwargs,
5193 }),
5194 Expr::BinOp(b) => Expr::BinOp(Box::new(ast::BinOp {
5195 left: Self::substitute_shape_aliases(b.left, defs),
5196 op: b.op,
5197 right: Self::substitute_shape_aliases(b.right, defs),
5198 })),
5199 Expr::UnaryOp(u) => Expr::UnaryOp(Box::new(ast::UnaryOp {
5200 op: u.op,
5201 operand: Self::substitute_shape_aliases(u.operand, defs),
5202 })),
5203 other => other,
5204 }
5205 }
5206
5207 fn aggregate_over_unpacked(
5214 &mut self,
5215 sel: &ast::SelectStmt,
5216 result: &Expr,
5217 distinct: bool,
5218 ) -> Result<Option<IrExpr>, PyQLError> {
5219 let Expr::FunctionCall(f) = result else {
5220 return Ok(None);
5221 };
5222 let [Expr::FunctionCall(unpack)] = f.args.as_slice() else {
5223 return Ok(None);
5224 };
5225 if !f.kwargs.is_empty()
5226 || !unpack.kwargs.is_empty()
5227 || unpack.module.as_deref().unwrap_or("std") != "std"
5228 || unpack.name != "array_unpack"
5229 {
5230 return Ok(None);
5231 }
5232 let [array] = unpack.args.as_slice() else {
5233 return Ok(None);
5234 };
5235 let namespace = f.module.as_deref().unwrap_or("std");
5236 let Some(sql_name) =
5237 crate::stdlib::lookup(namespace, &f.name)
5238 .into_iter()
5239 .find_map(|d| match &d.impl_strategy {
5240 crate::stdlib::ImplStrategy::SqlBuiltin(sql_name) if d.is_aggregate() => Some(sql_name.to_string()),
5241 _ => None,
5242 })
5243 else {
5244 return Ok(None);
5245 };
5246
5247 let elements = match array {
5251 Expr::Path(path) if !path.partial && path.steps.len() > 1 => {
5252 let walk = ast::Path {
5253 partial: false,
5254 steps: path.steps.clone(),
5255 };
5256 let mut path_select = self.compile_path_select(sel, &walk, &[], distinct)?;
5257 let IrPathResult::Scalar(column, _) = path_select.result else {
5258 return Ok(None);
5259 };
5260 path_select.result = IrPathResult::Scalar(
5261 IrExpr::FunctionCall(IrFunctionCall {
5262 return_pg_type: None,
5263 schema: None,
5264 name: "unnest".to_string(),
5265 args: vec![column],
5266 sql_template: None,
5267 }),
5268 None,
5269 );
5270 IrExpr::ArrayFromSelect(Box::new(IrArraySource::PathSelect(Box::new(path_select))))
5271 }
5272 _ if sel.filter.is_none()
5273 && sel.order_by.is_empty()
5274 && sel.offset.is_none()
5275 && sel.limit.is_none()
5276 && !distinct =>
5277 {
5278 self.compile_expr_ctx(array, None)?
5279 }
5280 _ => return Ok(None),
5281 };
5282
5283 let over_nothing = aggregate_over_nothing_sql(&f.name).unwrap_or("NULL");
5286 Ok(Some(IrExpr::FunctionCall(IrFunctionCall {
5287 return_pg_type: None,
5288 schema: None,
5289 name: sql_name.clone(),
5290 args: vec![elements],
5291 sql_template: Some(format!(
5292 "(SELECT coalesce({sql_name}(\"_s\".\"v\"), {over_nothing}) FROM unnest($1) AS \"_s\"(\"v\"))"
5293 )),
5294 })))
5295 }
5296
5297 fn compile_expr_as_path_select(
5301 &mut self,
5302 sel: &ast::SelectStmt,
5303 result: &Expr,
5304 root_type_name: &str,
5305 distinct: bool,
5306 ) -> Result<IrPathSelect, PyQLError> {
5307 let (agg_arg, agg_distinct) = match result {
5318 Expr::FunctionCall(f) if f.args.len() == 1 => match &f.args[0] {
5319 Expr::UnaryOp(u) if matches!(u.op, ast::UnaryOpKind::Distinct) => (Some(&u.operand), true),
5320 other => (Some(other), false),
5321 },
5322 _ => (None, false),
5323 };
5324 if let Expr::FunctionCall(f) = result
5325 && f.args.len() == 1
5326 && let Some(Expr::Path(p)) = agg_arg
5327 && !p.partial
5328 && p.steps.len() > 1
5329 {
5330 let arg_path = ast::Path {
5331 partial: false,
5332 steps: p.steps.clone(),
5333 };
5334 let mut ps = self.compile_path_select(sel, &arg_path, &[], distinct)?;
5335 let aggregated = match ps.result.clone() {
5339 IrPathResult::Scalar(column, _) => Some(column),
5340 IrPathResult::Object { alias, .. } => Some(IrExpr::ColumnRef {
5341 alias,
5342 column: "id".to_string(),
5343 pg_type: "uuid".to_string(),
5344 }),
5345 };
5346 if let Some(column) = aggregated {
5347 let mut args = vec![column];
5348 if !f.kwargs.is_empty() {
5349 if !matches!(f.name.as_str(), "assert_single" | "assert_exists" | "assert_distinct") {
5350 return Err(self.type_err(&format!(
5351 "function '{}' does not take named arguments, got '{}'",
5352 f.name, f.kwargs[0].0
5353 )));
5354 }
5355 args.extend(self.assert_message(f, None)?);
5356 }
5357 let mut call = self.resolve_fn_call(f.module.as_deref(), &f.name, args)?;
5358 if agg_distinct {
5359 let IrExpr::FunctionCall(fc) = &mut call else {
5360 return Err(self.type_err(&format!(
5361 "'{}' does not take a distinct argument — it does not resolve to an aggregate",
5362 f.name
5363 )));
5364 };
5365 if fc.schema.is_some() || fc.sql_template.is_some() {
5366 return Err(self.type_err(&format!(
5367 "'distinct' inside '{}' is not supported — only a plain SQL aggregate can take it",
5368 f.name
5369 )));
5370 }
5371 fc.sql_template = Some(format!("{}(DISTINCT $1)", fc.name));
5372 }
5373 ps.result = IrPathResult::Scalar(aggregate_over_nothing(&f.name, call), None);
5374 return Ok(ps);
5375 }
5376 }
5377
5378 if let Expr::BinOp(b) = result {
5382 let (path_expr, value_expr, flip) = match (&b.left, &b.right) {
5383 (Expr::Path(p), v) if !p.partial => (p, v, false),
5384 (v, Expr::Path(p)) if !p.partial => (p, v, true),
5385 _ => return self.compile_expr_as_path_select_fallback(sel, result, root_type_name, distinct),
5386 };
5387 let root_td = match self.cte_object_type(root_type_name) {
5389 Some(t) => self.resolve_type(&t)?,
5390 None => self.resolve_type(root_type_name)?,
5391 };
5392 let ast_path = ast::Path {
5394 partial: false,
5395 steps: path_expr.steps.clone(),
5396 };
5397 let mut ps = self.compile_path_select(sel, &ast_path, &[], distinct)?;
5398 let val_ir = self.compile_expr(value_expr, root_td, &ps.root.alias)?;
5399 let scalar_col = match ps.result {
5401 IrPathResult::Scalar(e, _) => e,
5402 IrPathResult::Object { type_name, .. } => {
5403 let val_type = infer_ir_type(&val_ir).map(pg_type_to_pyql).unwrap_or("unknown");
5404 return Err(PyQLError::Type(PyQLTypeError {
5405 message: format!(
5406 "operator '{}' cannot be applied to operands of type '{}' and '{}'",
5407 b.op, type_name, val_type,
5408 ),
5409 position: Position { line: 0, col: 0 },
5410 }));
5411 }
5412 };
5413 let (l, r) = if flip {
5414 (val_ir, scalar_col)
5415 } else {
5416 (scalar_col, val_ir)
5417 };
5418 ps.result = IrPathResult::Scalar(
5419 IrExpr::BinOp(Box::new(IrBinOp {
5420 left: true_division_operand(&b.op, l, &r),
5421 op: b.op.clone(),
5422 right: r,
5423 })),
5424 None,
5425 );
5426 return Ok(ps);
5427 }
5428 self.compile_expr_as_path_select_fallback(sel, result, root_type_name, distinct)
5429 }
5430
5431 fn compile_expr_as_path_select_fallback(
5432 &mut self,
5433 sel: &ast::SelectStmt,
5434 result: &Expr,
5435 root_type_name: &str,
5436 distinct: bool,
5437 ) -> Result<IrPathSelect, PyQLError> {
5438 let cte_object_type = self.cte_object_type(root_type_name);
5439 let td = match &cte_object_type {
5440 Some(t) => self.resolve_type(t)?,
5441 None => self.resolve_type(root_type_name)?,
5442 };
5443 let alias = self.fresh_alias();
5444 let root = IrSource {
5445 poly: None,
5446 type_name: format!("{}::{}", td.module, td.name),
5447 table: match &cte_object_type {
5448 Some(_) => self.cte_source_table(root_type_name),
5449 None => td.table.clone(),
5450 },
5451 alias: alias.clone(),
5452 };
5453 let rewritten = Self::rewrite_abs_to_partial(result.clone(), root_type_name);
5454 let expr = self.compile_expr(&rewritten, td, &alias)?;
5455 let (filter, order_by, offset, limit) = self.compile_path_modifiers(sel, td, &alias)?;
5456 Ok(IrPathSelect {
5457 root,
5458 joins: vec![],
5459 result: IrPathResult::Scalar(expr, None),
5460 filter,
5461 order_by,
5462 offset,
5463 limit,
5464 distinct,
5465 poly_implementors: vec![],
5466 })
5467 }
5468
5469 fn compile_subquery_to_array_source(&mut self, stmt: &Stmt) -> Result<IrArraySource, PyQLError> {
5472 match self.compile_stmt(stmt)? {
5473 IrStmt::Select(s) if matches!(s.rows.as_slice(), [IrRowSource::Bound { .. }]) => {
5474 Ok(IrArraySource::Select(s))
5475 }
5476 IrStmt::PathSelect(ps) => Ok(IrArraySource::PathSelect(Box::new(ps))),
5477 _ => Err(self.type_err("assert functions require a schema-bound SELECT as argument")),
5478 }
5479 }
5480
5481 fn compile_exists_ctx(
5489 &mut self,
5490 operand: &Expr,
5491 ctx: Option<(&TypeDescriptor, &str)>,
5492 ) -> Result<IrExpr, PyQLError> {
5493 match operand {
5494 Expr::Parameter(name) => {
5496 let idx = self.param_index(name);
5497 Ok(ir_is_not_null(IrExpr::Param { index: idx }))
5498 }
5499 Expr::TypeCast(_) => {
5501 let inner = self.compile_expr_ctx(operand, ctx)?;
5502 Ok(ir_is_not_null(inner))
5503 }
5504
5505 Expr::Path(p)
5507 if ctx.is_some() && p.partial && matches!(p.steps.first(), Some(ast::PathStep::Backlink(_))) =>
5508 {
5509 let (td, alias) = ctx.unwrap();
5510 let current_qname = format!("{}::{}", td.module, td.name);
5511 let exists = self.compile_backlink_as_exists(&p.steps, None, ¤t_qname, alias)?;
5512 Ok(exists)
5513 }
5514
5515 Expr::Path(p) if ctx.is_some() && p.partial && p.steps.len() == 1 => {
5519 let (td, alias) = ctx.unwrap();
5520 if let ast::PathStep::LinkProp(prop_name) = &p.steps[0] {
5525 let prop = self.compile_link_prop_ref(prop_name)?;
5526 return Ok(ir_is_not_null(prop));
5527 }
5528 if let ast::PathStep::Name(n) = &p.steps[0]
5532 && Self::resolve_property(td, n).is_none()
5533 && Self::resolve_link(td, n).is_none()
5534 && Self::resolve_multilink(td, n).is_none()
5535 && self.resolve_computed(td, n).is_none()
5536 && let Some(expr) = self
5537 .active_declared_pointers
5538 .iter()
5539 .find(|d| path_leaf(&d.path).is_ok_and(|name| name == n))
5540 .and_then(|d| d.compexpr.clone())
5541 {
5542 let inner = self.compile_expr(&expr, td, alias)?;
5543 return Ok(ir_is_not_null(inner));
5544 }
5545 if let ast::PathStep::TypeIntersection(type_ref) = &p.steps[0] {
5549 let narrowed = self.resolve_type(&type_ref.qualified_name())?;
5550 let check = format!("{}::{}", narrowed.module, narrowed.name);
5551 let source = format!("{}::{}", td.module, td.name);
5552 return Ok(self.type_check_bool_expr(&source, &check, td, alias));
5553 }
5554 let pointer_name = match &p.steps[0] {
5555 ast::PathStep::Name(n) => n.as_str(),
5556 _ => return Err(self.type_err("exists: invalid path step")),
5557 };
5558 if let Some(prop) = Self::resolve_property(td, pointer_name) {
5559 return Ok(ir_is_not_null(IrExpr::ColumnRef {
5560 alias: alias.to_string(),
5561 column: prop.name.clone(),
5562 pg_type: prop.pg_type.clone(),
5563 }));
5564 }
5565 if let Some(link) = Self::resolve_link(td, pointer_name) {
5566 if link.is_junction_backed() {
5567 return self.compile_junction_link_exists_check(link, td, alias);
5568 }
5569 return Ok(ir_is_not_null(IrExpr::ColumnRef {
5570 alias: alias.to_string(),
5571 column: format!("{}_id", link.name),
5572 pg_type: "uuid".to_string(),
5573 }));
5574 }
5575 if Self::resolve_multilink(td, pointer_name).is_some() {
5576 return self.compile_multilink_exists_check(pointer_name, td, alias);
5577 }
5578 if let Some(cd) = self.resolve_computed(td, pointer_name) {
5584 let expr_ast = crate::parse::parse_pointer_expr(&cd.expression).map_err(PyQLError::Syntax)?;
5585 return self.compile_exists_ctx(&expr_ast, ctx);
5586 }
5587 Err(self.field_err(pointer_name, &format!("{}::{}", td.module, td.name)))
5588 }
5589
5590 Expr::SubQuery(stmt)
5596 if ctx.is_some()
5597 && matches!(
5598 stmt.as_ref(),
5599 Stmt::Select(inner) if matches!(&inner.result, Expr::Path(p) if p.partial)
5600 ) =>
5601 {
5602 let Stmt::Select(inner) = stmt.as_ref() else {
5603 unreachable!("checked by the guard")
5604 };
5605 let Expr::Path(path) = &inner.result else {
5606 unreachable!("checked by the guard")
5607 };
5608 let (td, alias) = ctx.expect("checked by the guard");
5609 let mut steps = vec![ast::PathStep::Name(format!("{}::{}", td.module, td.name))];
5610 steps.extend(path.steps.iter().cloned());
5611 let full_path = ast::Path { steps, partial: false };
5612 let rooted = ast::SelectStmt {
5613 result: Expr::Path(full_path.clone()),
5614 filter: inner.filter.clone(),
5615 order_by: inner.order_by.clone(),
5616 offset: inner.offset.clone(),
5617 limit: inner.limit.clone(),
5618 lock: None,
5619 };
5620 let mut ps = self.compile_path_select(&rooted, &full_path, &[], false)?;
5621 Self::correlate_path_select(&mut ps, alias);
5622 Ok(IrExpr::UnaryOp(Box::new(IrUnaryOp {
5623 op: ast::UnaryOpKind::Exists,
5624 operand: IrExpr::PathSubquery(Box::new(ps)),
5625 })))
5626 }
5627
5628 Expr::SubQuery(stmt) => self.compile_subquery_exists(stmt),
5630
5631 operand if let Some(name) = self.resolve_cte_name(operand) => {
5634 let object = self.cte_types.get(name).is_some_and(|bound| bound.contains("::"));
5635 Ok(IrExpr::ExistsOverCte {
5636 cte: name.to_string(),
5637 column: (!object).then(|| "v".to_string()),
5638 })
5639 }
5640
5641 other => {
5647 let inner = self.compile_expr_ctx(other, ctx)?;
5648 if let IrExpr::SetOp { op, left, right, .. } = inner {
5651 return Ok(IrExpr::SetOp {
5652 op,
5653 left,
5654 right,
5655 mode: super::SetOpMode::Exists,
5656 });
5657 }
5658 Ok(ir_is_not_null(inner))
5659 }
5660 }
5661 }
5662
5663 fn compile_subquery_exists(&mut self, stmt: &Stmt) -> Result<IrExpr, PyQLError> {
5665 match self.compile_stmt(stmt)? {
5666 IrStmt::Select(s) => {
5667 let mut rows = s.rows;
5668 if rows.len() != 1 {
5669 return Err(self.type_err("exists requires a schema-bound SELECT expression"));
5670 }
5671 let IrRowSource::Bound { source, .. } = rows.remove(0) else {
5672 return Err(self.type_err("exists requires a schema-bound SELECT expression"));
5673 };
5674 let inner = IrExpr::Subquery(Box::new(IrSelect::schema_bound(source, vec![], s.filter)));
5675 Ok(IrExpr::UnaryOp(Box::new(IrUnaryOp {
5676 op: ast::UnaryOpKind::Exists,
5677 operand: inner,
5678 })))
5679 }
5680 IrStmt::PathSelect(ps) => {
5681 Ok(IrExpr::UnaryOp(Box::new(IrUnaryOp {
5684 op: ast::UnaryOpKind::Exists,
5685 operand: IrExpr::Subquery(Box::new(IrSelect::schema_bound(ps.root, vec![], ps.filter))),
5686 })))
5687 }
5688 _ => Err(self.type_err("exists requires a SELECT expression")),
5689 }
5690 }
5691
5692 fn junction_correlation_select(
5701 &mut self,
5702 td: &TypeDescriptor,
5703 alias: &str,
5704 name: &str,
5705 target: &str,
5706 through: &Option<String>,
5707 ) -> Result<IrSelect, PyQLError> {
5708 let jt_alias = self.fresh_alias();
5709 let (jt_table, jt_module, jt_src_col, _, _) = self.junction_info_for(td, name, target, through)?;
5710
5711 let filter = IrExpr::BinOp(Box::new(IrBinOp {
5712 left: IrExpr::ColumnRef {
5713 alias: jt_alias.clone(),
5714 column: jt_src_col,
5715 pg_type: "uuid".to_string(),
5716 },
5717 op: ast::BinOpKind::Eq,
5718 right: IrExpr::ColumnRef {
5719 alias: alias.to_string(),
5720 column: "id".to_string(),
5721 pg_type: "uuid".to_string(),
5722 },
5723 }));
5724 Ok(IrSelect::schema_bound(
5725 IrSource {
5726 poly: None,
5727 type_name: format!("{}::__jt__", jt_module),
5728 table: jt_table,
5729 alias: jt_alias,
5730 },
5731 vec![],
5732 Some(filter),
5733 ))
5734 }
5735
5736 fn multilink_correlation_select(
5737 &mut self,
5738 ml_name: &str,
5739 td: &TypeDescriptor,
5740 alias: &str,
5741 ) -> Result<IrSelect, PyQLError> {
5742 let ml = Self::resolve_multilink(td, ml_name).unwrap();
5743 let (name, target, through) = (ml.name.clone(), ml.target.clone(), ml.through.clone());
5744 self.junction_correlation_select(td, alias, &name, &target, &through)
5745 }
5746
5747 fn compile_multilink_exists_check(
5748 &mut self,
5749 ml_name: &str,
5750 td: &TypeDescriptor,
5751 alias: &str,
5752 ) -> Result<IrExpr, PyQLError> {
5753 let inner = self.multilink_correlation_select(ml_name, td, alias)?;
5754 Ok(IrExpr::UnaryOp(Box::new(IrUnaryOp {
5755 op: ast::UnaryOpKind::Exists,
5756 operand: IrExpr::Subquery(Box::new(inner)),
5757 })))
5758 }
5759
5760 fn compile_junction_link_exists_check(
5763 &mut self,
5764 l: &LinkDescriptor,
5765 td: &TypeDescriptor,
5766 alias: &str,
5767 ) -> Result<IrExpr, PyQLError> {
5768 let (name, target, through) = (l.name.clone(), l.target.clone(), l.through.clone());
5769 let inner = self.junction_correlation_select(td, alias, &name, &target, &through)?;
5770 Ok(IrExpr::UnaryOp(Box::new(IrUnaryOp {
5771 op: ast::UnaryOpKind::Exists,
5772 operand: IrExpr::Subquery(Box::new(inner)),
5773 })))
5774 }
5775
5776 fn junction_target_id_expr(
5783 &mut self,
5784 td: &TypeDescriptor,
5785 l: &LinkDescriptor,
5786 alias: &str,
5787 ) -> Result<IrExpr, PyQLError> {
5788 let (name, target, through) = (l.name.clone(), l.target.clone(), l.through.clone());
5789 let (jt_table, jt_module, jt_src_col, jt_tgt_col, _) = self.junction_info_for(td, &name, &target, &through)?;
5790 let jt_alias = self.fresh_alias();
5791 let filter = IrExpr::BinOp(Box::new(IrBinOp {
5792 left: IrExpr::ColumnRef {
5793 alias: jt_alias.clone(),
5794 column: jt_src_col,
5795 pg_type: "uuid".to_string(),
5796 },
5797 op: ast::BinOpKind::Eq,
5798 right: IrExpr::ColumnRef {
5799 alias: alias.to_string(),
5800 column: "id".to_string(),
5801 pg_type: "uuid".to_string(),
5802 },
5803 }));
5804 let select = IrSelect::schema_bound(
5805 IrSource {
5806 poly: None,
5807 type_name: format!("{}::__jt__", jt_module),
5808 table: jt_table,
5809 alias: jt_alias,
5810 },
5811 vec![IrShapePointer::Scalar(IrScalarPointer {
5812 implicit_id: false,
5813 marker_offset: None,
5814 alias: "target".to_string(),
5815 column: jt_tgt_col,
5816 pg_type: "uuid".to_string(),
5817 tuple_shape: None,
5818 })],
5819 Some(filter),
5820 );
5821 Ok(IrExpr::Subquery(Box::new(select)))
5822 }
5823
5824 fn check_union_type_compat(&self, expr: &Expr) -> Result<(), PyQLError> {
5828 let Expr::Union(a, b) = expr else { return Ok(()) };
5829 let a_free = self.is_free_result(a);
5830 let b_free = self.is_free_result(b);
5831 if a_free != b_free {
5832 let left = self.union_operand_type_display(a);
5833 let right = self.union_operand_type_display(b);
5834 return Err(PyQLError::Type(PyQLTypeError {
5835 message: format!(
5836 "operator 'UNION' cannot be applied to operands of type '{}' and '{}'",
5837 left, right,
5838 ),
5839 position: Position { line: 0, col: 0 },
5840 }));
5841 }
5842 Ok(())
5843 }
5844
5845 fn union_operand_type_display(&self, expr: &Expr) -> String {
5846 match expr {
5847 Expr::Path(p) if !p.partial && p.steps.len() == 1 => {
5848 if let ast::PathStep::Name(n) = &p.steps[0]
5849 && let Some(t) = self.cte_types.get(n.as_str())
5850 {
5851 if t.contains("::") {
5852 return t.clone(); }
5854 if !t.is_empty() {
5855 return pg_type_to_pyql(t).to_string(); }
5857 }
5858 if let Ok(td) = self.resolve_type(
5860 p.steps
5861 .first()
5862 .and_then(|s| {
5863 if let ast::PathStep::Name(n) = s {
5864 Some(n.as_str())
5865 } else {
5866 None
5867 }
5868 })
5869 .unwrap_or(""),
5870 ) {
5871 return format!("{}::{}", td.module, td.name);
5872 }
5873 }
5874 Expr::Literal(Literal::Int(_)) => return "std::int64".to_string(),
5875 Expr::Literal(Literal::Str(_)) => return "std::str".to_string(),
5876 Expr::Literal(Literal::Float(_)) => return "std::float64".to_string(),
5877 Expr::Literal(Literal::Bool(_)) => return "std::bool".to_string(),
5878 _ => {}
5879 }
5880 "unknown".to_string()
5881 }
5882
5883 fn is_free_result(&self, expr: &Expr) -> bool {
5884 let expr = match expr {
5885 Expr::UnaryOp(u) if u.op == ast::UnaryOpKind::Distinct => &u.operand,
5886 Expr::Detached(inner) => inner.as_ref(),
5887 other => other,
5888 };
5889 match expr {
5890 Expr::Path(p) if !p.partial => {
5891 if p.steps.len() == 1
5892 && let ast::PathStep::Name(n) = &p.steps[0]
5893 {
5894 if self.for_vars.contains_key(n.as_str()) {
5898 return !self.for_var_types.contains_key(n.as_str());
5899 }
5900 if self.is_value_binding(n.as_str()) {
5902 return true;
5903 }
5904 if self.fn_params.contains_key(n.as_str()) {
5909 return true;
5910 }
5911 }
5912 false
5913 }
5914 Expr::Shape(s) if s.expr.is_some() => false,
5915 Expr::SubQuery(_) => false,
5916 Expr::Union(a, b) => self.is_free_result(a) && self.is_free_result(b),
5917 Expr::IfElse(ie) => {
5922 let empty = |e: &Expr| matches!(e, Expr::Set(items) if items.is_empty());
5923 match (empty(&ie.if_expr), empty(&ie.else_expr)) {
5924 (true, true) => true,
5925 (true, false) => self.is_free_result(&ie.else_expr),
5926 (false, true) => self.is_free_result(&ie.if_expr),
5927 (false, false) => self.is_free_result(&ie.if_expr) || self.is_free_result(&ie.else_expr),
5928 }
5929 }
5930 _ => true,
5931 }
5932 }
5933
5934 fn is_free_cte_ref(&self, expr: &Expr) -> bool {
5939 let Expr::Path(p) = expr else { return false };
5940 if p.partial || p.steps.len() != 1 {
5941 return false;
5942 }
5943 let ast::PathStep::Name(n) = &p.steps[0] else {
5944 return false;
5945 };
5946 self.is_value_binding(n.as_str())
5947 }
5948
5949 fn dml_as_value(&mut self, stmt: &Stmt) -> Result<IrExpr, PyQLError> {
5957 let (cte_name, type_name) = self.hoist_dml_as_cte(stmt)?;
5958 let source = IrSource {
5962 poly: None,
5963 type_name,
5964 table: self.cte_source_table(&cte_name),
5965 alias: self.fresh_alias(),
5966 };
5967 Ok(IrExpr::Subquery(Box::new(IrSelect::schema_bound(
5968 source,
5969 vec![IrShapePointer::Scalar(IrScalarPointer {
5972 implicit_id: false,
5973 marker_offset: None,
5974 alias: "id".to_string(),
5975 column: "id".to_string(),
5976 pg_type: "uuid".to_string(),
5977 tuple_shape: None,
5978 })],
5979 None,
5980 ))))
5981 }
5982
5983 fn collect_union_items(&mut self, expr: &Expr, items: &mut Vec<IrFreeExpr>) -> Result<(), PyQLError> {
5984 match expr {
5985 Expr::Union(a, b) => {
5986 self.collect_union_items(a, items)?;
5987 self.collect_union_items(b, items)?;
5988 }
5989 Expr::Set(exprs) => {
5990 for e in exprs {
5991 self.collect_union_items(e, items)?;
5992 }
5993 }
5994 Expr::SubQuery(stmt) if matches!(stmt.as_ref(), Stmt::Insert(_) | Stmt::Update(_) | Stmt::Delete(_)) => {
6001 items.push(IrFreeExpr::Scalar(self.dml_as_value(stmt.as_ref())?));
6002 }
6003 other => {
6004 items.push(IrFreeExpr::Scalar(self.compile_free_expr(other)?));
6005 }
6006 }
6007 Ok(())
6008 }
6009
6010 fn shape_over_binding(&mut self, expr: &Expr) -> Result<Option<IrExpr>, PyQLError> {
6016 let Expr::Shape(sh) = expr else {
6017 return Ok(None);
6018 };
6019 let Some(Expr::Path(p)) = sh.expr.as_ref() else {
6020 return Ok(None);
6021 };
6022 let Some(ast::PathStep::Name(root)) = p.steps.first() else {
6023 return Ok(None);
6024 };
6025 if p.partial || (self.cte_object_type(root).is_none() && !self.for_var_types.contains_key(root)) {
6026 return Ok(None);
6027 }
6028 self.free_object_link_field(expr)
6029 }
6030
6031 fn free_object_link_field(&mut self, expr: &Expr) -> Result<Option<IrExpr>, PyQLError> {
6037 let Expr::Shape(sh) = expr else {
6038 return Ok(None);
6039 };
6040 let Some(Expr::Path(p)) = sh.expr.as_ref() else {
6041 return Ok(None);
6042 };
6043 if p.partial {
6044 return Ok(None);
6045 }
6046 let rest_steps = p.steps.as_slice();
6047 let Some(ast::PathStep::Name(name)) = rest_steps.first() else {
6048 return Ok(None);
6049 };
6050 let cte_name = self.cte_object_type(name).map(|_| name.clone());
6051 let Ok(td) = self.resolve_path_root(name) else {
6052 return Ok(None);
6053 };
6054 let table = self.row_source_table(name, td);
6055 if rest_steps.len() > 1 {
6059 let synthetic = ast::SelectStmt {
6060 result: Expr::Path(p.clone()),
6061 filter: None,
6062 order_by: vec![],
6063 offset: None,
6064 limit: None,
6065 lock: None,
6066 };
6067 let path_select = self.compile_path_select(&synthetic, p, &sh.elements, false)?;
6068 return Ok(Some(IrExpr::ObjectPathSubquery(Box::new(path_select))));
6069 }
6070 let alias = self.fresh_alias();
6071 let source = IrSource {
6072 poly: self.poly_fanout_for(&format!("{}::{}", td.module, td.name)),
6073 type_name: format!("{}::{}", td.module, td.name),
6074 table,
6075 alias: alias.clone(),
6076 };
6077 let outer_declared = std::mem::replace(
6080 &mut self.active_declared_pointers,
6081 cte_name
6082 .as_deref()
6083 .and_then(|n| self.cte_declared_pointers.get(n).cloned())
6084 .unwrap_or_default(),
6085 );
6086 let shape = self.compile_shape(&sh.elements, td, &alias, &td.module);
6087 self.active_declared_pointers = outer_declared;
6088 let filter = self.for_var_types.contains_key(name).then(|| {
6091 IrExpr::BinOp(Box::new(IrBinOp {
6092 left: IrExpr::ColumnRef {
6093 alias: alias.clone(),
6094 column: "id".to_string(),
6095 pg_type: "uuid".to_string(),
6096 },
6097 op: ast::BinOpKind::Eq,
6098 right: self.for_var_ref(name),
6099 }))
6100 });
6101 Ok(Some(IrExpr::ObjectSubquery(Box::new(IrSelect::schema_bound(
6102 source, shape?, filter,
6103 )))))
6104 }
6105
6106 fn free_object_field(&mut self, expr: &Expr) -> Result<IrExpr, PyQLError> {
6110 if let Some(object) = self.free_object_link_field(expr)? {
6111 return Ok(object);
6112 }
6113 match expr {
6114 Expr::SubQuery(stmt) if matches!(stmt.as_ref(), Stmt::Insert(_) | Stmt::Update(_) | Stmt::Delete(_)) => {
6115 self.dml_as_value(stmt.as_ref())
6116 }
6117 other => self.compile_free_expr(other),
6118 }
6119 }
6120
6121 fn compile_free_select(
6122 &mut self,
6123 sel: &ast::SelectStmt,
6124 result_expr: &Expr,
6125 distinct: bool,
6126 ) -> Result<IrSelect, PyQLError> {
6127 if let Expr::IfElse(ie) = result_expr {
6131 let is_empty_set = |expr: &Expr| matches!(expr, Expr::Set(items) if items.is_empty());
6132 let is_free_object = |expr: &Expr| matches!(expr, Expr::Shape(sh) if sh.expr.is_none());
6133 let guarded = if is_free_object(&ie.if_expr) && is_empty_set(&ie.else_expr) {
6134 Some((&ie.if_expr, false))
6135 } else if is_empty_set(&ie.if_expr) && is_free_object(&ie.else_expr) {
6136 Some((&ie.else_expr, true))
6137 } else {
6138 None
6139 };
6140 if let Some((object, negated)) = guarded {
6141 let condition = self.compile_free_expr(&ie.condition)?;
6142 let condition = if negated {
6143 IrExpr::UnaryOp(Box::new(IrUnaryOp {
6144 op: ast::UnaryOpKind::Not,
6145 operand: condition,
6146 }))
6147 } else {
6148 condition
6149 };
6150 let mut select = self.compile_free_select(sel, object, distinct)?;
6151 select.filter = and_conditions(select.filter, vec![condition]);
6152 return Ok(select);
6153 }
6154 }
6155 let items: Vec<IrFreeExpr> = match result_expr {
6156 Expr::Union(_, _) | Expr::Set(_) => {
6157 let mut union_items = vec![];
6158 self.collect_union_items(result_expr, &mut union_items)?;
6159 let mut first: Option<(String, String)> = None; for item in &union_items {
6162 if let IrFreeExpr::Scalar(expr) = item
6163 && let Some(t) = infer_ir_type(expr)
6164 {
6165 let t = t.to_string();
6166 if let Some((ft, fq)) = &first {
6167 if !types_compatible(ft, &t) {
6168 return Err(PyQLError::Type(PyQLTypeError {
6169 message: format!(
6170 "operator 'UNION' cannot be applied to operands of type '{}' and '{}'",
6171 fq,
6172 pg_type_to_pyql(&t),
6173 ),
6174 position: Position { line: 0, col: 0 },
6175 }));
6176 }
6177 } else {
6178 first = Some((t.clone(), pg_type_to_pyql(&t).to_string()));
6179 }
6180 }
6181 }
6182 union_items
6183 }
6184 Expr::Path(p) if !p.partial && p.steps.len() == 1 => {
6185 if let ast::PathStep::Name(n) = &p.steps[0] {
6186 if self
6187 .cte_types
6188 .get(n.as_str())
6189 .map(|t| !t.contains("::"))
6190 .unwrap_or(false)
6191 {
6192 vec![IrFreeExpr::CtePassthrough(n.clone())]
6193 } else {
6194 vec![IrFreeExpr::Scalar(self.compile_free_expr(result_expr)?)]
6195 }
6196 } else {
6197 vec![IrFreeExpr::Scalar(self.compile_free_expr(result_expr)?)]
6198 }
6199 }
6200 Expr::Shape(s) if s.expr.is_none() => {
6201 let fields = s
6202 .elements
6203 .iter()
6204 .map(|el| -> Result<(String, IrExpr), PyQLError> {
6205 let name = path_leaf(&el.path)?.to_string();
6206 let expr = el.compexpr.as_ref().ok_or_else(|| {
6207 self.type_err("free object field must have a value expression (':= expr')")
6208 })?;
6209 Ok((name, self.free_object_field(expr)?))
6210 })
6211 .collect::<Result<Vec<_>, _>>()?;
6212 vec![IrFreeExpr::FreeObject(fields)]
6213 }
6214 Expr::Tuple(exprs) => {
6215 let ir = exprs
6219 .iter()
6220 .map(|e| self.free_object_field(e))
6221 .collect::<Result<_, _>>()?;
6222 vec![IrFreeExpr::Tuple(ir)]
6223 }
6224 Expr::NamedTuple(fields) => {
6225 let ir = fields
6226 .iter()
6227 .map(|(name, e)| Ok((name.clone(), self.free_object_field(e)?)))
6228 .collect::<Result<Vec<_>, PyQLError>>()?;
6229 let holds_an_object = ir.iter().any(|(_, e)| match e {
6235 IrExpr::ObjectSubquery(_) | IrExpr::ObjectPathSubquery(_) => true,
6236 IrExpr::ArrayFromSelect(source) => match source.as_ref() {
6237 IrArraySource::ObjectSelect(_) | IrArraySource::ObjectFunction(_) | IrArraySource::Group(_) => {
6238 true
6239 }
6240 IrArraySource::PathSelect(ps) => matches!(ps.result, IrPathResult::Object { .. }),
6241 _ => false,
6242 },
6243 _ => false,
6244 });
6245 if holds_an_object {
6246 vec![IrFreeExpr::NamedTupleRow(ir)]
6247 } else {
6248 vec![IrFreeExpr::Scalar(IrExpr::NamedTuple {
6249 fields: ir,
6250 is_free_object: false,
6251 })]
6252 }
6253 }
6254 other => vec![IrFreeExpr::Scalar(self.compile_free_expr(other)?)],
6255 };
6256
6257 let order_by = sel
6258 .order_by
6259 .iter()
6260 .map(|s| self.compile_sort_ctx(s, None))
6261 .collect::<Result<Vec<_>, _>>()?;
6262
6263 let offset = sel.offset.as_ref().map(|e| self.compile_free_expr(e)).transpose()?;
6264 let limit = sel.limit.as_ref().map(|e| self.compile_free_expr(e)).transpose()?;
6265 let filter = sel.filter.as_ref().map(|f| self.compile_free_filter(f)).transpose()?;
6266
6267 Ok(IrSelect {
6268 rows: items.into_iter().map(IrRowSource::Free).collect(),
6269 filter,
6270 order_by,
6271 offset,
6272 limit,
6273 distinct,
6274 dml_source: None,
6275 polymorphic: false,
6276 poly_implementors: vec![],
6277 poly_columns: vec![],
6278 lock: None,
6279 })
6280 }
6281
6282 fn compile_free_filter(&mut self, filter: &Expr) -> Result<IrExpr, PyQLError> {
6290 self.as_condition(|c| c.compile_free_filter_inner(filter))
6291 }
6292
6293 fn compile_free_filter_inner(&mut self, filter: &Expr) -> Result<IrExpr, PyQLError> {
6294 let Some(root) = self.find_path_root_in_expr(filter) else {
6295 return self.compile_free_expr(filter);
6296 };
6297 let td = self.resolve_path_root(&root)?;
6300 let table = self.row_source_table(&root, td);
6301 let alias = self.fresh_alias();
6302 let source = IrSource {
6303 poly: None,
6304 type_name: format!("{}::{}", td.module, td.name),
6305 table,
6306 alias: alias.clone(),
6307 };
6308 let condition = self.compile_expr(&Self::rewrite_abs_to_partial(filter.clone(), &root), td, &alias)?;
6309 self.warnings.push(format!(
6310 "possibly more than one element returned by an expression in a FILTER clause \
6311 (every '{root}'); wrap with any() to make intent explicit",
6312 ));
6313 Ok(IrExpr::UnaryOp(Box::new(IrUnaryOp {
6314 op: ast::UnaryOpKind::Exists,
6315 operand: IrExpr::Subquery(Box::new(IrSelect::schema_bound(source, vec![], Some(condition)))),
6316 })))
6317 }
6318
6319 fn common_union_type(&self, branches: &[(String, String)]) -> Option<String> {
6329 let covers_all = |candidate: &String| {
6330 branches.iter().all(|(t, _)| {
6331 t == candidate
6332 || self
6333 .resolve_type(t)
6334 .map(|td| Self::is_or_implements(td, candidate))
6335 .unwrap_or(false)
6336 })
6337 };
6338 if let Some(branch) = branches.iter().map(|(t, _)| t.clone()).find(&covers_all) {
6339 return Some(branch);
6340 }
6341 let first = self.resolve_type(&branches.first()?.0).ok()?;
6346 first
6347 .interfaces
6348 .iter()
6349 .chain(first.parents.iter())
6350 .find(|ancestor| covers_all(ancestor))
6351 .cloned()
6352 }
6353
6354 fn object_union_branches(&self, expr: &Expr) -> Option<Vec<(String, String)>> {
6355 fn flatten<'e>(expr: &'e Expr, out: &mut Vec<&'e Expr>) {
6356 match expr {
6357 Expr::Union(a, b) => {
6358 flatten(a, out);
6359 flatten(b, out);
6360 }
6361 other => out.push(other),
6362 }
6363 }
6364 if !matches!(expr, Expr::Union(_, _)) {
6365 return None;
6366 }
6367 let mut operands: Vec<&Expr> = vec![];
6368 flatten(expr, &mut operands);
6369
6370 let mut branches: Vec<(String, String)> = vec![];
6371 for operand in operands {
6372 let Expr::Path(path) = operand else { return None };
6373 if path.partial || path.steps.len() != 1 {
6374 return None;
6375 }
6376 let ast::PathStep::Name(name) = &path.steps[0] else {
6377 return None;
6378 };
6379 match self.cte_object_type(name) {
6380 Some(qualified) => branches.push((qualified, self.cte_source_table(&self.cte_sql_name(name)))),
6381 None => match self.resolve_type(name) {
6382 Ok(td) => branches.push((format!("{}::{}", td.module, td.name), td.table.clone())),
6383 Err(_) => return None,
6384 },
6385 }
6386 }
6387 Some(branches)
6388 }
6389
6390 fn name_union_operands(&mut self, expr: &Expr) -> Result<Option<Expr>, PyQLError> {
6406 let Expr::Union(left, right) = expr else {
6407 return Ok(None);
6408 };
6409 let mut rewritten = false;
6410 let mut name_one = |compiler: &mut Self, operand: &Expr| -> Result<Expr, PyQLError> {
6411 match operand {
6412 Expr::Union(_, _) => match compiler.name_union_operands(operand)? {
6413 Some(inner) => {
6414 rewritten = true;
6415 Ok(inner)
6416 }
6417 None => Ok(operand.clone()),
6418 },
6419 Expr::SubQuery(stmt) => {
6420 let (cte_name, type_name) = compiler.hoist_dml_as_cte(stmt.as_ref())?;
6421 let Ok(td) = compiler.resolve_type(&type_name) else {
6425 return Ok(operand.clone());
6426 };
6427 compiler
6428 .cte_types
6429 .insert(cte_name.clone(), format!("{}::{}", td.module, td.name));
6430 rewritten = true;
6431 Ok(Expr::Path(ast::Path {
6432 steps: vec![ast::PathStep::Name(cte_name)],
6433 partial: false,
6434 }))
6435 }
6436 Expr::Path(path) if !path.partial && path.steps.len() > 1 => {
6440 let synthetic = Stmt::Select(ast::SelectStmt {
6441 result: operand.clone(),
6442 filter: None,
6443 order_by: vec![],
6444 offset: None,
6445 limit: None,
6446 lock: None,
6447 });
6448 let Ok((cte_name, type_name)) = compiler.hoist_dml_as_cte(&synthetic) else {
6449 return Ok(operand.clone());
6450 };
6451 let Ok(td) = compiler.resolve_type(&type_name) else {
6452 return Ok(operand.clone());
6453 };
6454 compiler
6455 .cte_types
6456 .insert(cte_name.clone(), format!("{}::{}", td.module, td.name));
6457 rewritten = true;
6458 Ok(Expr::Path(ast::Path {
6459 steps: vec![ast::PathStep::Name(cte_name)],
6460 partial: false,
6461 }))
6462 }
6463 other => Ok(other.clone()),
6464 }
6465 };
6466 let left = name_one(self, left)?;
6467 let right = name_one(self, right)?;
6468 if !rewritten {
6469 return Ok(None);
6470 }
6471 Ok(Some(Expr::Union(Box::new(left), Box::new(right))))
6472 }
6473
6474 fn set_mutation_guard(&mut self, branch: &Expr, condition: Expr) {
6484 let stmt = match branch {
6485 Expr::SubQuery(stmt) => Some(stmt.as_ref()),
6486 Expr::Shape(sh) => match sh.expr.as_ref() {
6487 Some(Expr::SubQuery(stmt)) => Some(stmt.as_ref()),
6488 _ => None,
6489 },
6490 _ => None,
6491 };
6492 match stmt {
6493 Some(Stmt::Update(_)) => self.pending_update_guard = Some(condition),
6494 Some(Stmt::Delete(_)) => self.pending_delete_guard = Some(condition),
6495 _ => self.pending_insert_guard = Some(condition),
6496 }
6497 }
6498
6499 fn object_if_else_as_union(&mut self, expr: &Expr) -> Option<Expr> {
6500 let Expr::IfElse(ie) = expr else {
6501 return None;
6502 };
6503 fn is_empty_set(expr: &Expr) -> bool {
6504 matches!(expr, Expr::Set(items) if items.is_empty())
6505 }
6506 let yields_objects = |branch: &Expr| match branch {
6507 Expr::Path(p) if !p.partial && p.steps.len() == 1 => match &p.steps[0] {
6508 ast::PathStep::Name(n) => self.cte_object_type(n).is_some() || self.resolve_type(n).is_ok(),
6509 _ => false,
6510 },
6511 Expr::SubQuery(stmt) => {
6512 if let Some(ast::SelectStmt {
6516 result: Expr::Path(path),
6517 ..
6518 }) = innermost_select(stmt.as_ref())
6519 && !path.partial
6520 && path.steps.len() > 1
6521 && let Some(ast::PathStep::Name(root)) = path.steps.first()
6522 && let Ok(root_td) = self.resolve_path_root(root)
6523 {
6524 return matches!(
6525 self.walk_path_types(root_td, &path.steps[1..], MAX_COMPUTED_SPLICES),
6526 (_, Some(_))
6527 );
6528 }
6529 matches!(
6530 self.dml_subject_type(stmt.as_ref()),
6531 Ok(name) if self.resolve_type(&name).is_ok()
6532 )
6533 }
6534 Expr::Shape(sh) => match sh.expr.as_ref() {
6536 Some(Expr::SubQuery(stmt)) => {
6537 matches!(self.dml_subject_type(stmt.as_ref()), Ok(name) if self.resolve_type(&name).is_ok())
6538 }
6539 Some(Expr::Path(p)) if !p.partial && p.steps.len() == 1 => match &p.steps[0] {
6540 ast::PathStep::Name(n) => self.cte_object_type(n).is_some() || self.resolve_type(n).is_ok(),
6541 _ => false,
6542 },
6543 _ => false,
6544 },
6545 _ => false,
6546 };
6547 fn mutating_stmt(branch: &Expr) -> Option<&Stmt> {
6558 match branch {
6559 Expr::SubQuery(stmt) => Some(stmt.as_ref()),
6560 Expr::Shape(sh) => match sh.expr.as_ref() {
6561 Some(Expr::SubQuery(stmt)) => Some(stmt.as_ref()),
6562 _ => None,
6563 },
6564 _ => None,
6565 }
6566 .filter(|stmt| matches!(stmt, Stmt::Insert(_) | Stmt::Update(_) | Stmt::Delete(_)))
6567 }
6568 let (if_yields_objects, else_yields_objects) = (yields_objects(&ie.if_expr), yields_objects(&ie.else_expr));
6573 let guard = |branch: &Expr, condition: Expr| {
6574 Expr::SubQuery(Box::new(Stmt::Select(ast::SelectStmt {
6575 result: branch.clone(),
6576 filter: Some(condition),
6577 order_by: vec![],
6578 offset: None,
6579 limit: None,
6580 lock: None,
6581 })))
6582 };
6583 let negated = Expr::UnaryOp(Box::new(ast::UnaryOp {
6584 op: ast::UnaryOpKind::Not,
6585 operand: ie.condition.clone(),
6586 }));
6587 let guarded_insert = |branch: &Expr, other: &Expr| {
6588 mutating_stmt(branch).is_some() && matches!(other, Expr::Set(items) if items.is_empty())
6589 };
6590 if guarded_insert(&ie.if_expr, &ie.else_expr) {
6591 self.set_mutation_guard(&ie.if_expr, ie.condition.clone());
6592 return Some(ie.if_expr.clone());
6593 }
6594 if guarded_insert(&ie.else_expr, &ie.if_expr) {
6595 let negated = Expr::UnaryOp(Box::new(ast::UnaryOp {
6596 op: ast::UnaryOpKind::Not,
6597 operand: ie.condition.clone(),
6598 }));
6599 self.set_mutation_guard(&ie.else_expr, negated);
6600 return Some(ie.else_expr.clone());
6601 }
6602 if let (Some(if_stmt), Some(else_stmt)) = (mutating_stmt(&ie.if_expr), mutating_stmt(&ie.else_expr))
6609 && std::mem::discriminant(if_stmt) != std::mem::discriminant(else_stmt)
6610 {
6611 self.set_mutation_guard(&ie.if_expr, ie.condition.clone());
6612 self.set_mutation_guard(&ie.else_expr, negated);
6613 return Some(Expr::Union(
6614 Box::new(ie.if_expr.clone()),
6615 Box::new(ie.else_expr.clone()),
6616 ));
6617 }
6618 let reads_objects = |branch: &Expr, yields: bool| mutating_stmt(branch).is_none() && yields;
6623 if mutating_stmt(&ie.if_expr).is_some() && reads_objects(&ie.else_expr, else_yields_objects) {
6624 self.set_mutation_guard(&ie.if_expr, ie.condition.clone());
6625 return Some(Expr::Union(
6626 Box::new(ie.if_expr.clone()),
6627 Box::new(guard(&ie.else_expr, negated)),
6628 ));
6629 }
6630 if mutating_stmt(&ie.else_expr).is_some() && reads_objects(&ie.if_expr, if_yields_objects) {
6631 self.set_mutation_guard(&ie.else_expr, negated);
6632 return Some(Expr::Union(
6633 Box::new(guard(&ie.if_expr, ie.condition.clone())),
6634 Box::new(ie.else_expr.clone()),
6635 ));
6636 }
6637 if mutating_stmt(&ie.if_expr).is_some() || mutating_stmt(&ie.else_expr).is_some() {
6638 return None;
6639 }
6640 let (if_empty, else_empty) = (is_empty_set(&ie.if_expr), is_empty_set(&ie.else_expr));
6641 if if_empty && else_empty {
6642 return None;
6643 }
6644 if !(if_empty || if_yields_objects) || !(else_empty || else_yields_objects) {
6645 return None;
6646 }
6647 if else_empty {
6648 return Some(guard(&ie.if_expr, ie.condition.clone()));
6649 }
6650 if if_empty {
6651 return Some(guard(&ie.else_expr, negated));
6652 }
6653 Some(Expr::Union(
6654 Box::new(guard(&ie.if_expr, ie.condition.clone())),
6655 Box::new(guard(&ie.else_expr, negated)),
6656 ))
6657 }
6658
6659 fn object_coalesce_as_if_else(expr: &Expr) -> Option<Expr> {
6664 fn as_if_else(b: &ast::BinOp) -> Option<Expr> {
6665 (b.op == ast::BinOpKind::Coalesce).then(|| {
6666 Expr::IfElse(Box::new(ast::IfElse {
6667 condition: Expr::UnaryOp(Box::new(ast::UnaryOp {
6668 op: ast::UnaryOpKind::Exists,
6669 operand: b.left.clone(),
6670 })),
6671 if_expr: b.left.clone(),
6672 else_expr: b.right.clone(),
6673 }))
6674 })
6675 }
6676 match expr {
6677 Expr::BinOp(b) => as_if_else(b),
6678 Expr::Shape(sh) => match sh.expr.as_ref() {
6679 Some(Expr::BinOp(b)) => Some(Expr::Shape(Box::new(ast::ShapeExpr {
6680 expr: Some(as_if_else(b)?),
6681 elements: sh.elements.clone(),
6682 marker_offset: sh.marker_offset,
6683 }))),
6684 _ => None,
6685 },
6686 _ => None,
6687 }
6688 }
6689
6690 fn try_compile_object_union_select(
6691 &mut self,
6692 sel: &ast::SelectStmt,
6693 result_expr: &Expr,
6694 distinct: bool,
6695 ) -> Result<Option<IrSelect>, PyQLError> {
6696 let coalesced;
6697 let result_expr = match Self::object_coalesce_as_if_else(result_expr) {
6698 Some(rewritten) => {
6699 coalesced = rewritten;
6700 &coalesced
6701 }
6702 None => result_expr,
6703 };
6704 let as_union;
6705 let (union_expr, shape_elements): (&Expr, &[ShapeElement]) = match result_expr {
6706 Expr::Union(_, _) => (result_expr, &[]),
6707 Expr::IfElse(_) => match self.object_if_else_as_union(result_expr) {
6708 Some(rewritten @ Expr::SubQuery(_)) => {
6711 return self.compile_select(sel, &rewritten, distinct).map(Some);
6712 }
6713 Some(rewritten) if !matches!(rewritten, Expr::Union(_, _)) => {
6714 return self.compile_select(sel, &rewritten, distinct).map(Some);
6715 }
6716 Some(rewritten) => {
6717 as_union = rewritten;
6718 (&as_union, &[] as &[ShapeElement])
6719 }
6720 None => return Ok(None),
6721 },
6722 Expr::Shape(shape) => match &shape.expr {
6723 Some(inner @ Expr::Union(_, _)) => (inner, shape.elements.as_slice()),
6724 Some(inner @ Expr::IfElse(_)) => match self.object_if_else_as_union(inner) {
6725 Some(Expr::SubQuery(stmt)) => {
6726 let shaped = Expr::Shape(Box::new(ast::ShapeExpr {
6727 expr: Some(Expr::SubQuery(stmt)),
6728 elements: shape.elements.clone(),
6729 marker_offset: None,
6730 }));
6731 return self.compile_select(sel, &shaped, distinct).map(Some);
6732 }
6733 Some(rewritten) if !matches!(rewritten, Expr::Union(_, _)) => {
6734 let shaped = match rewritten {
6735 Expr::Shape(_) => rewritten,
6736 other => Expr::Shape(Box::new(ast::ShapeExpr {
6737 expr: Some(other),
6738 elements: shape.elements.clone(),
6739 marker_offset: None,
6740 })),
6741 };
6742 return self.compile_select(sel, &shaped, distinct).map(Some);
6743 }
6744 Some(rewritten) => {
6745 as_union = rewritten;
6746 (&as_union, shape.elements.as_slice())
6747 }
6748 None => return Ok(None),
6749 },
6750 _ => return Ok(None),
6751 },
6752 _ => return Ok(None),
6753 };
6754
6755 let named;
6756 let union_expr = match self.name_union_operands(union_expr)? {
6757 Some(rewritten) => {
6758 named = rewritten;
6759 &named
6760 }
6761 None => union_expr,
6762 };
6763 let Some(branches) = self.object_union_branches(union_expr) else {
6764 return Ok(None);
6765 };
6766
6767 let Some(first_type) = self.common_union_type(&branches) else {
6768 let (first_type, _) = &branches[0];
6769 let (other, _) = branches
6770 .iter()
6771 .find(|(t, _)| t != first_type)
6772 .expect("no common type means at least two differ");
6773 return Err(self.type_err(&format!(
6774 "operator 'UNION' cannot be applied to operands of type '{first_type}' and '{other}'"
6775 )));
6776 };
6777 let first_type = &first_type;
6778 if sel.lock.is_some() {
6779 return Err(self.type_err(
6780 "FOR UPDATE/SHARE cannot be used on a UNION — its rows come from more than one \
6781 source, which a single locking clause can't target",
6782 ));
6783 }
6784 let td = self.resolve_type(first_type)?;
6785 let alias = self.fresh_alias();
6786
6787 self.anchors.push(SelectAnchor {
6788 type_name: td.name.clone(),
6789 qualified: format!("{}::{}", td.module, td.name),
6790 alias: alias.clone(),
6791 detached: std::mem::take(&mut self.pending_detached),
6792 declared_on: None,
6793 });
6794 let clauses = (|compiler: &mut Self| -> Result<_, PyQLError> {
6795 let shape = compiler.compile_shape(shape_elements, td, &alias, &td.module)?;
6796 let filter = sel
6797 .filter
6798 .as_ref()
6799 .map(|f| compiler.compile_expr(f, td, &alias))
6800 .transpose()?;
6801 let order_by = sel
6802 .order_by
6803 .iter()
6804 .map(|o| compiler.compile_sort(o, td, &alias))
6805 .collect::<Result<Vec<_>, _>>()?;
6806 let offset = sel
6807 .offset
6808 .as_ref()
6809 .map(|e| compiler.compile_expr(e, td, &alias))
6810 .transpose()?;
6811 let limit = sel
6812 .limit
6813 .as_ref()
6814 .map(|e| compiler.compile_expr(e, td, &alias))
6815 .transpose()?;
6816 Ok((shape, filter, order_by, offset, limit))
6817 })(self);
6818 self.anchors.pop();
6819 let (shape, mut filter, order_by, offset, limit) = clauses?;
6820 if let Some(var) = Self::subject_name(result_expr).filter(|n| self.for_var_types.contains_key(n)) {
6823 let narrowed = IrExpr::BinOp(Box::new(IrBinOp {
6824 left: IrExpr::ColumnRef {
6825 alias: alias.clone(),
6826 column: "id".to_string(),
6827 pg_type: "uuid".to_string(),
6828 },
6829 op: ast::BinOpKind::Eq,
6830 right: self.for_var_ref(&var),
6831 }));
6832 filter = and_conditions(filter, vec![narrowed]);
6833 }
6834
6835 let heterogeneous = branches.iter().any(|(t, _)| t != first_type);
6840 let common_columns = if heterogeneous {
6841 self.collect_poly_info(first_type).1
6842 } else {
6843 vec![]
6844 };
6845 let rows = branches
6846 .into_iter()
6847 .map(|(type_name, table)| IrRowSource::Bound {
6848 source: IrSource {
6849 poly: if heterogeneous {
6850 self.poly_fanout_for(&type_name)
6851 } else {
6852 None
6853 },
6854 type_name,
6855 table,
6856 alias: alias.clone(),
6857 },
6858 shape: shape.clone(),
6859 })
6860 .collect();
6861
6862 Ok(Some(IrSelect {
6863 rows,
6864 filter,
6865 order_by,
6866 offset,
6867 limit,
6868 distinct,
6869 dml_source: None,
6870 polymorphic: false,
6871 poly_implementors: vec![],
6872 poly_columns: common_columns,
6873 lock: None,
6874 }))
6875 }
6876
6877 fn subject_name(result_expr: &Expr) -> Option<String> {
6880 let path = match result_expr {
6881 Expr::Path(p) => p,
6882 Expr::Shape(sh) => match sh.expr.as_ref()? {
6883 Expr::Path(p) => p,
6884 _ => return None,
6885 },
6886 _ => return None,
6887 };
6888 if path.partial {
6889 return None;
6890 }
6891 match path.steps.as_slice() {
6892 [ast::PathStep::Name(n)] => Some(n.clone()),
6893 _ => None,
6894 }
6895 }
6896
6897 fn compile_select(
6898 &mut self,
6899 sel: &ast::SelectStmt,
6900 result_expr: &Expr,
6901 distinct: bool,
6902 ) -> Result<IrSelect, PyQLError> {
6903 if let Some(union_select) = self.try_compile_object_union_select(sel, result_expr, distinct)? {
6904 return Ok(union_select);
6905 }
6906 if let Some(rerooted) = self.bind_mutating_loop_source(result_expr)? {
6907 return self.compile_select(sel, &rerooted, distinct);
6908 }
6909 let (type_name, shape_elements, inner_stmt, cte_name) = self.extract_type_and_shape(result_expr)?;
6910 let td = self.resolve_type(&type_name)?;
6911 let alias = self.fresh_alias();
6912 let table = match cte_name {
6913 Some(ref cte) => self.cte_source_table(cte),
6914 None => Self::subject_name(result_expr)
6915 .map(|name| self.row_source_table(&name, td))
6916 .unwrap_or_else(|| td.table.clone()),
6917 };
6918 let source = IrSource {
6919 poly: self.poly_fanout_for(&format!("{}::{}", td.module, td.name)),
6920 type_name: format!("{}::{}", td.module, td.name),
6921 table,
6922 alias: alias.clone(),
6923 };
6924
6925 self.anchors.push(SelectAnchor {
6929 type_name: td.name.clone(),
6930 qualified: format!("{}::{}", td.module, td.name),
6931 alias: alias.clone(),
6932 detached: std::mem::take(&mut self.pending_detached),
6933 declared_on: None,
6934 });
6935 let declared = match cte_name.as_deref() {
6939 Some(name) => self.cte_declared_pointers.get(name).cloned().unwrap_or_default(),
6940 None => inner_stmt
6944 .and_then(innermost_select)
6945 .and_then(|inner| match &inner.result {
6946 Expr::Shape(sh) => Some(sh.elements.clone()),
6947 _ => None,
6948 })
6949 .unwrap_or_default(),
6950 };
6951 let splat_over_declared: Vec<ShapeElement>;
6953 let shape_elements = if shape_elements
6954 .iter()
6955 .any(|el| el.splat.is_some() && !matches!(el.path.steps.first(), Some(ast::PathStep::TypeIntersection(_))))
6956 {
6957 let named = |name: &str| {
6958 shape_elements
6959 .iter()
6960 .any(|el| el.splat.is_none() && path_leaf(&el.path).is_ok_and(|n| n == name))
6961 };
6962 let extra: Vec<ShapeElement> = declared
6963 .iter()
6964 .filter(|d| d.compexpr.is_some())
6965 .filter_map(|d| {
6966 let name = path_leaf(&d.path).ok()?;
6967 (!named(name)).then(|| ShapeElement {
6968 path: ast::Path::relative(name),
6969 splat: None,
6970 nested: None,
6971 compexpr: None,
6972 op: ast::ShapeOp::Assign,
6973 filter: None,
6974 order_by: vec![],
6975 offset: None,
6976 limit: None,
6977 marker_offset: None,
6978 })
6979 })
6980 .collect();
6981 splat_over_declared = shape_elements.iter().cloned().chain(extra).collect();
6982 &splat_over_declared[..]
6983 } else {
6984 shape_elements
6985 };
6986 let outer_declared = std::mem::replace(&mut self.active_declared_pointers, declared);
6987 let clauses = (|compiler: &mut Self| -> Result<_, PyQLError> {
6988 let shape = compiler.compile_shape(shape_elements, td, &alias, &td.module)?;
6989 compiler
6995 .active_declared_pointers
6996 .extend(shape_elements.iter().filter(|el| el.compexpr.is_some()).cloned());
6997 let filter = sel
6998 .filter
6999 .as_ref()
7000 .map(|f| compiler.as_condition(|c| c.compile_expr(f, td, &alias)))
7001 .transpose()?;
7002 let order_by = sel
7003 .order_by
7004 .iter()
7005 .map(|s| compiler.compile_sort(s, td, &alias))
7006 .collect::<Result<Vec<_>, _>>()?;
7007 let offset = sel
7008 .offset
7009 .as_ref()
7010 .map(|e| compiler.compile_expr(e, td, &alias))
7011 .transpose()?;
7012 let limit = sel
7013 .limit
7014 .as_ref()
7015 .map(|e| compiler.compile_expr(e, td, &alias))
7016 .transpose()?;
7017 Ok((shape, filter, order_by, offset, limit))
7018 })(self);
7019 self.anchors.pop();
7020 self.active_declared_pointers = outer_declared;
7021 let (shape, mut filter, order_by, offset, limit) = clauses?;
7022
7023 if let Some(var) = Self::subject_name(result_expr).filter(|n| self.for_var_types.contains_key(n)) {
7026 let narrowed = IrExpr::BinOp(Box::new(IrBinOp {
7027 left: IrExpr::ColumnRef {
7028 alias: alias.clone(),
7029 column: "id".to_string(),
7030 pg_type: "uuid".to_string(),
7031 },
7032 op: ast::BinOpKind::Eq,
7033 right: self.for_var_ref(&var),
7034 }));
7035 filter = and_conditions(filter, vec![narrowed]);
7036 }
7037
7038 let dml_source = inner_stmt.map(|s| self.compile_stmt(s).map(Box::new)).transpose()?;
7040 let mut shape = shape;
7041 if let Some(dml) = &dml_source {
7042 let dml_cte = cte_name.as_deref().unwrap_or(crate::sql::DML_CTE);
7046 Self::read_nested_links_from_their_ctes(dml, Some(dml_cte), &mut shape);
7047 }
7048
7049 let polymorphic = self.is_polymorphic(td);
7050 let (poly_implementors, poly_columns) = if polymorphic {
7051 let iface_qname = format!("{}::{}", td.module, td.name);
7052 (self.find_poly_implementors(&iface_qname), Self::poly_dml_columns(td))
7053 } else {
7054 (vec![], vec![])
7055 };
7056
7057 let lock = match &sel.lock {
7065 None => None,
7066 Some(lc) => {
7067 if distinct {
7068 return Err(self.type_err(
7069 "FOR UPDATE/SHARE cannot be combined with DISTINCT — Postgres can't \
7070 guarantee the result rows map 1:1 to physical table rows",
7071 ));
7072 }
7073 if polymorphic {
7074 return Err(self.type_err(
7075 "FOR UPDATE/SHARE cannot be used on an interface type — its rows span \
7076 multiple underlying tables, which a single locking clause can't target",
7077 ));
7078 }
7079 if dml_source.is_some() {
7080 return Err(self.type_err(
7081 "FOR UPDATE/SHARE cannot be used on SELECT (INSERT/UPDATE/DELETE …) — \
7082 there's nothing left to lock once the DML has already run",
7083 ));
7084 }
7085 Some(IrLockClause {
7086 strength: match lc.strength {
7087 ast::LockStrength::Update => IrLockStrength::Update,
7088 ast::LockStrength::NoKeyUpdate => IrLockStrength::NoKeyUpdate,
7089 ast::LockStrength::Share => IrLockStrength::Share,
7090 ast::LockStrength::KeyShare => IrLockStrength::KeyShare,
7091 },
7092 wait: match lc.wait {
7093 ast::LockWait::Block => IrLockWait::Block,
7094 ast::LockWait::NoWait => IrLockWait::NoWait,
7095 ast::LockWait::SkipLocked => IrLockWait::SkipLocked,
7096 },
7097 })
7098 }
7099 };
7100
7101 Ok(IrSelect {
7102 rows: vec![IrRowSource::Bound { source, shape }],
7103 filter,
7104 order_by,
7105 offset,
7106 limit,
7107 distinct,
7108 dml_source,
7109 polymorphic,
7110 poly_implementors,
7111 poly_columns,
7112 lock,
7113 })
7114 }
7115
7116 fn extract_type_and_shape<'e>(&self, expr: &'e Expr) -> Result<TypeAndShape<'e>, PyQLError> {
7121 match expr {
7122 Expr::Shape(s) => {
7123 let (type_name, cte_name, inner) = match s.expr.as_ref() {
7125 Some(Expr::SubQuery(stmt)) => (self.dml_subject_type(stmt)?, None, Some(stmt.as_ref())),
7126 Some(Expr::Path(p)) if !p.partial && p.steps.len() == 1 => {
7127 if let ast::PathStep::Name(n) = &p.steps[0] {
7128 if let Some(t) = self.for_var_types.get(n.as_str()) {
7129 (t.clone(), None, None)
7130 } else if let Some(t) = self.cte_types.get(n.as_str()) {
7131 if t.contains("::") {
7132 (t.clone(), Some(n.clone()), None)
7133 } else {
7134 (self.expr_as_type_name(s.expr.as_ref().unwrap())?, None, None)
7135 }
7136 } else {
7137 (self.expr_as_type_name(s.expr.as_ref().unwrap())?, None, None)
7138 }
7139 } else {
7140 (self.expr_as_type_name(s.expr.as_ref().unwrap())?, None, None)
7141 }
7142 }
7143 Some(inner) => (self.expr_as_type_name(inner)?, None, None),
7144 None => {
7145 return Err(PyQLError::Type(PyQLTypeError {
7146 message: "shape without subject expression".into(),
7147 position: Position { line: 0, col: 0 },
7148 }));
7149 }
7150 };
7151 Ok((type_name, &s.elements, inner, cte_name))
7152 }
7153 Expr::SubQuery(stmt) => Ok((self.dml_subject_type(stmt)?, &[], Some(stmt.as_ref()), None)),
7155 Expr::Path(p) if !p.partial && p.steps.len() == 1 => {
7157 if let ast::PathStep::Name(n) = &p.steps[0] {
7158 if let Some(t) = self.for_var_types.get(n.as_str()) {
7159 return Ok((t.clone(), &[], None, None));
7160 }
7161 if let Some(t) = self.cte_types.get(n.as_str())
7162 && t.contains("::")
7163 {
7164 return Ok((t.clone(), &[], None, Some(n.clone())));
7165 }
7166 }
7167 Ok((self.expr_as_type_name(expr)?, &[], None, None))
7168 }
7169 _ => Ok((self.expr_as_type_name(expr)?, &[], None, None)),
7170 }
7171 }
7172
7173 fn is_json_expr(&self, ast: &Expr, ir: &IrExpr) -> bool {
7177 if matches!(infer_ir_type(ir), Some("jsonb")) {
7178 return true;
7179 }
7180 let Expr::Path(path) = ast else {
7181 return false;
7182 };
7183 if path.partial || path.steps.len() != 1 {
7184 return false;
7185 }
7186 let ast::PathStep::Name(name) = &path.steps[0] else {
7187 return false;
7188 };
7189 self.for_vars.get(name.as_str()).is_some_and(|t| t == "jsonb")
7190 }
7191
7192 fn set_returning_element_type(&mut self, expr: &Expr) -> Result<Option<String>, PyQLError> {
7198 if !expr_returns_set(expr) {
7199 return Ok(None);
7200 }
7201 let Expr::FunctionCall(call) = expr else {
7202 return Ok(None);
7203 };
7204 let mut declared = crate::stdlib::lookup(call.module.as_deref().unwrap_or("std"), &call.name)
7207 .into_iter()
7208 .map(|descriptor| match &descriptor.return_type {
7209 crate::stdlib::PylonType::Set(element) => element.scalar_pg_type(),
7210 _ => None,
7211 });
7212 if let Some(element) = declared.next().flatten()
7213 && declared.all(|other| other == Some(element))
7214 {
7215 return Ok(Some(element.to_string()));
7216 }
7217 let [argument] = call.args.as_slice() else {
7218 return Ok(None);
7219 };
7220 let compiled = self.compile_free_expr(argument)?;
7221 let Some(array_type) = infer_ir_type(&compiled) else {
7222 return Ok(None);
7223 };
7224 Ok(array_type
7225 .strip_suffix("[]")
7226 .map(|element| literal_sentinel_to_pg(element).to_string()))
7227 }
7228
7229 fn compile_for(&mut self, f: &ast::ForStmt) -> Result<IrFor, PyQLError> {
7230 let iterated_binding = self.resolve_cte_name(&f.iterator).map(|name| self.cte_sql_name(name));
7233 let (iterator, pg_type, yielded_object_type) = match &f.iterator {
7235 Expr::Set(elems) => {
7236 let compiled: Result<Vec<_>, _> = elems.iter().map(|e| self.compile_free_expr(e)).collect();
7237 let exprs = compiled?;
7238 let raw = exprs.first().and_then(|e| infer_ir_type(e)).unwrap_or("text");
7239 let pg_type = literal_sentinel_to_pg(raw).to_string();
7240 (
7241 IrForIterator::Values {
7242 exprs,
7243 pg_type: pg_type.clone(),
7244 },
7245 pg_type,
7246 None,
7247 )
7248 }
7249 Expr::SubQuery(stmt) => {
7252 let inner = self.compile_stmt(stmt)?;
7253 if !matches!(inner, IrStmt::Select(_) | IrStmt::PathSelect(_)) {
7254 return Err(self.type_err(
7255 "for-loop iterator: only a select can be iterated over — \
7256 bind the statement in a `with` first",
7257 ));
7258 }
7259 let yielded = cte_stmt_type(&inner);
7260 let scalar = !yielded.contains("::");
7261 let pg_type = if scalar {
7262 let raw = if yielded.is_empty() { "text" } else { yielded.as_str() };
7263 literal_sentinel_to_pg(raw).to_string()
7264 } else {
7265 "uuid".to_string()
7266 };
7267 (
7268 IrForIterator::Query {
7269 stmt: Box::new(inner),
7270 scalar,
7271 },
7272 pg_type,
7273 (!scalar).then_some(yielded),
7274 )
7275 }
7276 Expr::Path(p)
7280 if !p.partial
7281 && p.steps.len() == 1
7282 && matches!(&p.steps[0], ast::PathStep::Name(n) if self.cte_types.contains_key(n.as_str())) =>
7283 {
7284 let ast::PathStep::Name(name) = &p.steps[0] else {
7285 unreachable!("checked by the guard")
7286 };
7287 let yielded = self.cte_types.get(name.as_str()).cloned().unwrap_or_default();
7288 let scalar = !yielded.contains("::");
7289 let pg_type = if scalar {
7290 let raw = if yielded.is_empty() { "text" } else { yielded.as_str() };
7291 literal_sentinel_to_pg(raw).to_string()
7292 } else {
7293 "uuid".to_string()
7294 };
7295 let source = IrSource {
7296 poly: None,
7297 type_name: yielded.clone(),
7298 table: self.cte_source_table(&self.cte_sql_name(name)),
7299 alias: self.fresh_alias(),
7300 };
7301 (
7302 IrForIterator::Query {
7303 stmt: Box::new(IrStmt::Select(IrSelect::schema_bound(source, vec![], None))),
7304 scalar,
7305 },
7306 pg_type,
7307 (!scalar).then_some(yielded),
7308 )
7309 }
7310 Expr::Path(p) if !p.partial && p.steps.len() > 1 => {
7314 let synthetic = ast::SelectStmt {
7315 result: Expr::Path(p.clone()),
7316 filter: None,
7317 order_by: vec![],
7318 offset: None,
7319 limit: None,
7320 lock: None,
7321 };
7322 let inner = self.compile_stmt(&Stmt::Select(synthetic))?;
7323 let yielded = cte_stmt_type(&inner);
7324 let scalar = !yielded.contains("::");
7325 let pg_type = if scalar {
7326 let raw = if yielded.is_empty() { "text" } else { yielded.as_str() };
7327 literal_sentinel_to_pg(raw).to_string()
7328 } else {
7329 "uuid".to_string()
7330 };
7331 (
7332 IrForIterator::Query {
7333 stmt: Box::new(inner),
7334 scalar,
7335 },
7336 pg_type,
7337 (!scalar).then_some(yielded),
7338 )
7339 }
7340 other => {
7341 let e = self.compile_free_expr(other)?;
7342 let raw = match self.set_returning_element_type(other)? {
7346 Some(element) => element,
7347 None => literal_sentinel_to_pg(infer_ir_type(&e).unwrap_or("text")).to_string(),
7348 };
7349 let pg_type = raw;
7350 let iterator = if expr_returns_set(other) {
7351 IrForIterator::SetReturning {
7352 expr: e,
7353 pg_type: pg_type.clone(),
7354 }
7355 } else {
7356 IrForIterator::Values {
7357 exprs: vec![e],
7358 pg_type: pg_type.clone(),
7359 }
7360 };
7361 (iterator, pg_type, None)
7362 }
7363 };
7364
7365 let slot = self
7369 .claim_generated_cte_name(&format!("_for_{}", f.var))
7370 .strip_prefix("_for_")
7371 .unwrap_or(&f.var)
7372 .to_string();
7373 let prev_slot = self.for_var_slots.insert(f.var.clone(), slot.clone());
7374 let prev = self.for_vars.insert(f.var.clone(), pg_type.clone());
7376 let prev_cte = match iterated_binding {
7377 Some(name) => self.for_var_ctes.insert(f.var.clone(), name),
7378 None => self.for_var_ctes.remove(&f.var),
7379 };
7380 let prev_type = match yielded_object_type {
7381 Some(qualified) => self.for_var_types.insert(f.var.clone(), qualified),
7382 None => self.for_var_types.remove(&f.var),
7383 };
7384 let hoisted_before = self.hoisted_ctes.len();
7385 self.for_scope.push(slot.clone());
7386 let body = self.compile_stmt(&f.body);
7387 self.for_scope.pop();
7388 let body = body?;
7389 let body_ctes: Vec<IrCteDef> = self.hoisted_ctes.split_off(hoisted_before);
7390 match prev {
7392 Some(old) => {
7393 self.for_vars.insert(f.var.clone(), old);
7394 }
7395 None => {
7396 self.for_vars.remove(&f.var);
7397 }
7398 }
7399 match prev_type {
7400 Some(old) => {
7401 self.for_var_types.insert(f.var.clone(), old);
7402 }
7403 None => {
7404 self.for_var_types.remove(&f.var);
7405 }
7406 }
7407 match prev_cte {
7408 Some(old) => {
7409 self.for_var_ctes.insert(f.var.clone(), old);
7410 }
7411 None => {
7412 self.for_var_ctes.remove(&f.var);
7413 }
7414 }
7415 match prev_slot {
7416 Some(old) => {
7417 self.for_var_slots.insert(f.var.clone(), old);
7418 }
7419 None => {
7420 self.for_var_slots.remove(&f.var);
7421 }
7422 }
7423
7424 let body_kind = match &body {
7429 IrStmt::Insert(_) | IrStmt::Select(_) | IrStmt::PathSelect(_) => None,
7430 IrStmt::Update(upd)
7438 if upd.assignments.is_empty()
7439 && upd.rewrites.is_empty()
7440 && !upd.multi_link_appends.is_empty()
7441 && upd
7442 .multi_link_appends
7443 .iter()
7444 .all(|a| crate::sql::append_value_is_the_loop_variable(&a.values, &f.var))
7445 && upd.multi_link_clears.is_empty()
7446 && upd.multi_link_replaces.is_empty()
7447 && upd.multi_link_removals.is_empty()
7448 && upd.poly_implementors.is_empty() =>
7449 {
7450 None
7451 }
7452 IrStmt::Update(upd)
7455 if upd.multi_link_appends.is_empty()
7456 && upd.multi_link_clears.is_empty()
7457 && upd.multi_link_replaces.is_empty()
7458 && upd.multi_link_removals.is_empty() =>
7459 {
7460 None
7461 }
7462 IrStmt::Update(_) if std::env::var("PYLON_DBG_FORUPD").is_ok() => None,
7463 IrStmt::Update(upd)
7468 if upd.assignments.is_empty()
7469 && upd.rewrites.is_empty()
7470 && !upd.multi_link_appends.is_empty()
7471 && upd.multi_link_clears.is_empty()
7472 && upd.multi_link_replaces.is_empty()
7473 && upd.multi_link_removals.is_empty()
7474 && upd.poly_implementors.is_empty()
7475 && upd
7476 .multi_link_appends
7477 .iter()
7478 .all(|a| crate::sql::per_iteration_insert(a, &upd.nested_ctes).is_some()) =>
7479 {
7480 None
7481 }
7482 IrStmt::Update(_) => Some("update"),
7483 IrStmt::Delete(_) => Some("delete"),
7484 IrStmt::For(inner) if matches!(inner.body.as_ref(), IrStmt::Insert(_)) => None,
7489 IrStmt::For(_) => Some("nested for"),
7490 IrStmt::Group(_) => Some("group"),
7491 _ => Some("this statement"),
7492 };
7493 if let Some(kind) = body_kind {
7494 return Err(self.type_err(&format!(
7495 "for-loop body: {kind} is not supported as a `for` body — use insert or select"
7496 )));
7497 }
7498
7499 Ok(IrFor {
7500 var_name: slot,
7501 iterator,
7502 body: Box::new(body),
7503 body_ctes,
7504 })
7505 }
7506
7507 fn compile_group(&mut self, g: &ast::GroupStmt) -> Result<IrGroup, PyQLError> {
7508 let mut walked: Option<&ast::Path> = None;
7515 let (type_name, cte_name) = match &g.subject {
7516 Expr::Path(p) if !p.partial => match p.steps.as_slice() {
7517 [ast::PathStep::Name(n)] => {
7518 if let Some(t) = self.cte_types.get(n.as_str()) {
7519 (t.clone(), Some(n.clone()))
7520 } else {
7521 (n.clone(), None)
7522 }
7523 }
7524 [ast::PathStep::Name(root), rest @ ..]
7525 if !rest.is_empty()
7526 && let Ok(root_td) = self.resolve_path_root(root)
7527 && let (_, Some(target)) = self.walk_path_types(root_td, rest, MAX_COMPUTED_SPLICES) =>
7528 {
7529 walked = Some(p);
7530 (format!("{}::{}", target.module, target.name), None)
7531 }
7532 _ => {
7533 return Err(PyQLError::Type(PyQLTypeError {
7534 message: format!("unsupported group subject: {:?}", g.subject),
7535 position: Position { line: 0, col: 0 },
7536 }));
7537 }
7538 },
7539 _ => {
7540 return Err(PyQLError::Type(PyQLTypeError {
7541 message: "group subject must be a type name".to_string(),
7542 position: Position { line: 0, col: 0 },
7543 }));
7544 }
7545 };
7546
7547 let td = self.resolve_type(&type_name)?;
7548 let alias = self.fresh_alias();
7549 let fq_type_name = format!("{}::{}", td.module, td.name);
7550 let table = match cte_name {
7551 Some(ref cte) => self.cte_source_table(cte),
7552 None => td.table.clone(),
7553 };
7554 let source = IrSource {
7555 poly: self.poly_fanout_for(&fq_type_name),
7556 type_name: fq_type_name.clone(),
7557 table,
7558 alias: alias.clone(),
7559 };
7560 let module = td.module.clone();
7561
7562 let shape = self.compile_shape(g.shape.as_deref().unwrap_or(&[]), td, &alias, &module)?;
7564
7565 let td = self.resolve_type(&type_name)?;
7567 let mut using_map: HashMap<String, IrExpr> = HashMap::new();
7568 for (alias_name, expr) in &g.using {
7569 let ir = self.compile_expr(expr, td, &alias)?;
7570 using_map.insert(alias_name.clone(), ir);
7571 }
7572
7573 let mut keys: Vec<(String, IrExpr)> = vec![];
7575 let td = self.resolve_type(&type_name)?;
7576 for by_expr in &g.by {
7577 match by_expr {
7578 Expr::Path(p) if p.partial && p.steps.len() == 1 => {
7579 if let ast::PathStep::Name(prop) = &p.steps[0] {
7580 let ir = self.compile_expr(by_expr, td, &alias)?;
7582 keys.push((prop.clone(), ir));
7583 } else {
7584 return Err(PyQLError::Type(PyQLTypeError {
7585 message: "group by path must be a simple property".to_string(),
7586 position: Position { line: 0, col: 0 },
7587 }));
7588 }
7589 }
7590 Expr::Path(p) if !p.partial && p.steps.len() == 1 => {
7591 if let ast::PathStep::Name(name) = &p.steps[0] {
7592 let ir = using_map.get(name).ok_or_else(|| {
7594 PyQLError::Type(PyQLTypeError {
7595 message: format!("group by references unknown alias '{}'", name),
7596 position: Position { line: 0, col: 0 },
7597 })
7598 })?;
7599 keys.push((name.clone(), ir.clone()));
7600 } else {
7601 return Err(PyQLError::Type(PyQLTypeError {
7602 message: "group by identifier must be a simple name".to_string(),
7603 position: Position { line: 0, col: 0 },
7604 }));
7605 }
7606 }
7607 _ => {
7608 return Err(PyQLError::Type(PyQLTypeError {
7609 message: format!("unsupported group by expression: {:?}", by_expr),
7610 position: Position { line: 0, col: 0 },
7611 }));
7612 }
7613 }
7614 }
7615
7616 let td = self.resolve_type(&type_name)?;
7620 let synthetic = ast::SelectStmt {
7621 result: g.subject.clone(),
7622 filter: g.filter.clone(),
7623 order_by: g.order_by.clone(),
7624 offset: g.offset.clone(),
7625 limit: g.limit.clone(),
7626 lock: None,
7627 };
7628 let (filter, order_by, offset, limit) = self.compile_path_modifiers(&synthetic, td, &alias)?;
7629 let filter = match walked {
7632 Some(subject) => {
7633 let subject = subject.clone();
7634 let rows = self.compile_subject_path_rows(&subject, &alias)?;
7635 and_conditions(filter, vec![rows])
7636 }
7637 None => filter,
7638 };
7639
7640 Ok(IrGroup {
7641 source,
7642 shape,
7643 keys,
7644 filter,
7645 order_by,
7646 offset,
7647 limit,
7648 output: IrGroupOutput::Groups,
7649 })
7650 }
7651
7652 fn bind_group(&mut self, name: &str, expr: &Expr) -> Result<bool, PyQLError> {
7653 let Expr::SubQuery(stmt) = expr else {
7654 return Ok(false);
7655 };
7656 let Stmt::Group(g) = stmt.as_ref() else {
7657 return Ok(false);
7658 };
7659 let grp = self.compile_group(g)?;
7660 self.group_bindings.insert(name.to_string(), grp);
7661 Ok(true)
7662 }
7663
7664 fn compile_group_projection(
7670 &mut self,
7671 sel: &ast::SelectStmt,
7672 g: &ast::GroupStmt,
7673 elements: &[ShapeElement],
7674 ) -> Result<IrGroup, PyQLError> {
7675 if sel.filter.is_some() {
7676 return Err(self.type_err("a shape over a `group` does not support filter"));
7677 }
7678 if !g.order_by.is_empty() || g.offset.is_some() || g.limit.is_some() {
7679 return Err(self.type_err(
7680 "a `group` read through a shape does not support order by, offset or limit on its elements",
7681 ));
7682 }
7683 let mut grp = self.compile_group(g)?;
7684 let keys: Vec<String> = grp.keys.iter().map(|(name, _)| name.clone()).collect();
7688 for (name, key) in &grp.keys {
7689 self.inline_bindings.insert(group_key_binding(name), key.clone());
7690 }
7691 let compiled = self.compile_group_projection_parts(sel, &grp, &keys, elements);
7692 for name in &keys {
7693 self.inline_bindings.remove(&group_key_binding(name));
7694 }
7695 let (pointers, order_by, offset, limit) = compiled?;
7696 grp.output = IrGroupOutput::Projection(Box::new(IrGroupProjection {
7697 pointers,
7698 order_by,
7699 offset,
7700 limit,
7701 }));
7702 Ok(grp)
7703 }
7704
7705 #[allow(clippy::type_complexity)]
7706 fn compile_group_projection_parts(
7707 &mut self,
7708 sel: &ast::SelectStmt,
7709 grp: &IrGroup,
7710 keys: &[String],
7711 elements: &[ShapeElement],
7712 ) -> Result<(Vec<IrShapePointer>, Vec<IrSort>, Option<IrExpr>, Option<IrExpr>), PyQLError> {
7713 let td = self.resolve_type(&grp.source.type_name)?;
7714 let mut rewritten = Vec::with_capacity(elements.len());
7715 for element in elements {
7716 let (Some(compexpr), [ast::PathStep::Name(_)]) = (&element.compexpr, element.path.steps.as_slice()) else {
7717 return Err(self.type_err(
7718 "only computed pointers (`name := …`) can be read off a `group` — e.g. `k := .key.name`",
7719 ));
7720 };
7721 let mut element = element.clone();
7722 element.compexpr = Some(self.rewrite_group_refs(compexpr, keys, td)?);
7723 rewritten.push(element);
7724 }
7725 let alias = grp.source.alias.clone();
7726 let module = td.module.clone();
7727 let pointers = self.without_implicit_id(|this| this.compile_shape(&rewritten, td, &alias, &module))?;
7731 let order_by = sel
7732 .order_by
7733 .iter()
7734 .map(|sort| {
7735 Ok(ast::SortExpr {
7736 expr: self.rewrite_group_refs(&sort.expr, keys, td)?,
7737 direction: sort.direction.clone(),
7738 nones: sort.nones.clone(),
7739 })
7740 })
7741 .collect::<Result<Vec<_>, PyQLError>>()?;
7742 let synthetic = ast::SelectStmt {
7743 result: Expr::Path(ast::Path::absolute(grp.source.type_name.clone())),
7744 filter: None,
7745 order_by,
7746 offset: sel.offset.clone(),
7747 limit: sel.limit.clone(),
7748 lock: None,
7749 };
7750 let (_, order_by, offset, limit) = self.compile_path_modifiers(&synthetic, td, &alias)?;
7751 Ok((pointers, order_by, offset, limit))
7752 }
7753
7754 fn rewrite_group_refs(&self, expr: &Expr, keys: &[String], td: &TypeDescriptor) -> Result<Expr, PyQLError> {
7755 const AGGREGATES: &[&str] = &["count", "sum", "min", "max", "avg", "array_agg", "all", "any"];
7756 let rewrite = |e: &Expr| self.rewrite_group_refs(e, keys, td);
7757 Ok(match expr {
7758 Expr::Path(p) if p.partial => {
7759 let name = match p.steps.first() {
7760 Some(ast::PathStep::Name(name)) => name.as_str(),
7761 _ => "",
7762 };
7763 match (name, p.steps.get(1..).unwrap_or_default()) {
7764 ("key", [ast::PathStep::Name(key)]) if keys.contains(key) => {
7765 Expr::Path(ast::Path::absolute(group_key_binding(key)))
7766 }
7767 ("key", [ast::PathStep::Name(key)]) => {
7768 return Err(self.type_err(&format!("'{key}' is not a key of this group")));
7769 }
7770 ("elements", _) => {
7771 return Err(self.type_err(
7772 "`.elements` of a group can only be read through an aggregate over one \
7773 of their properties — e.g. `count(.elements)`, `sum(.elements.amount)`",
7774 ));
7775 }
7776 _ => {
7777 return Err(self.type_err(&format!(
7778 "a group has no pointer '{name}' — read `.key.<name>` or aggregate over `.elements`"
7779 )));
7780 }
7781 }
7782 }
7783 Expr::FunctionCall(f)
7784 if f.args.len() == 1
7785 && f.kwargs.is_empty()
7786 && f.module.as_deref().is_none_or(|m| m == "std")
7787 && AGGREGATES.contains(&f.name.as_str())
7788 && let Expr::Path(p) = &f.args[0]
7789 && p.partial
7790 && matches!(p.steps.first(), Some(ast::PathStep::Name(n)) if n == "elements") =>
7791 {
7792 let property = match p.steps.get(1..).unwrap_or_default() {
7793 [] => "id",
7794 [ast::PathStep::Name(prop)] if td.properties.iter().any(|d| &d.name == prop) => prop.as_str(),
7795 _ => {
7796 return Err(self.type_err(&format!(
7797 "'{}' over a group's elements only reads one of their own properties",
7798 f.name
7799 )));
7800 }
7801 };
7802 Expr::FunctionCall(ast::FunctionCall {
7803 module: f.module.clone(),
7804 name: f.name.clone(),
7805 args: vec![Expr::Path(ast::Path::relative(property))],
7806 kwargs: vec![],
7807 })
7808 }
7809 Expr::FunctionCall(f) => Expr::FunctionCall(ast::FunctionCall {
7810 module: f.module.clone(),
7811 name: f.name.clone(),
7812 args: f.args.iter().map(rewrite).collect::<Result<_, _>>()?,
7813 kwargs: f
7814 .kwargs
7815 .iter()
7816 .map(|(k, v)| Ok((k.clone(), rewrite(v)?)))
7817 .collect::<Result<_, PyQLError>>()?,
7818 }),
7819 Expr::BinOp(b) => Expr::BinOp(Box::new(ast::BinOp {
7820 left: rewrite(&b.left)?,
7821 op: b.op.clone(),
7822 right: rewrite(&b.right)?,
7823 })),
7824 Expr::UnaryOp(u) => Expr::UnaryOp(Box::new(ast::UnaryOp {
7825 op: u.op.clone(),
7826 operand: rewrite(&u.operand)?,
7827 })),
7828 Expr::TypeCast(c) => Expr::TypeCast(Box::new(ast::TypeCast {
7829 expr: rewrite(&c.expr)?,
7830 ty: c.ty.clone(),
7831 })),
7832 Expr::IfElse(ie) => Expr::IfElse(Box::new(ast::IfElse {
7833 if_expr: rewrite(&ie.if_expr)?,
7834 condition: rewrite(&ie.condition)?,
7835 else_expr: rewrite(&ie.else_expr)?,
7836 })),
7837 other => other.clone(),
7838 })
7839 }
7840
7841 fn compile_group_elements(&mut self, f: &ast::ForStmt) -> Result<Option<IrGroup>, PyQLError> {
7844 let Stmt::Select(body) = f.body.as_ref() else {
7845 return Ok(None);
7846 };
7847 let unmodified =
7848 body.filter.is_none() && body.order_by.is_empty() && body.offset.is_none() && body.limit.is_none();
7849 let (inner, shape) = match &body.result {
7850 Expr::Shape(sh) if unmodified => match &sh.expr {
7851 Some(Expr::SubQuery(stmt)) => match stmt.as_ref() {
7852 Stmt::Select(inner) => (inner, Some(sh.elements.as_slice())),
7853 _ => return Ok(None),
7854 },
7855 _ => return Ok(None),
7856 },
7857 _ => (body, None),
7858 };
7859 let reads_elements = matches!(&inner.result, Expr::Path(p) if !p.partial
7860 && matches!(p.steps.as_slice(), [ast::PathStep::Name(var), ast::PathStep::Name(elements)]
7861 if var == &f.var && elements == "elements"));
7862 if !reads_elements {
7863 return Ok(None);
7864 }
7865 let mut grp = match &f.iterator {
7866 Expr::SubQuery(stmt) => match stmt.as_ref() {
7867 Stmt::Group(g) => self.compile_group(g)?,
7868 _ => return Ok(None),
7869 },
7870 Expr::Path(p) if !p.partial => match p.steps.as_slice() {
7871 [ast::PathStep::Name(name)] => match self.group_bindings.get(name) {
7872 Some(grp) => grp.clone(),
7873 None => return Ok(None),
7874 },
7875 _ => return Ok(None),
7876 },
7877 _ => return Ok(None),
7878 };
7879 let ordered = !inner.order_by.is_empty() || inner.offset.is_some() || inner.limit.is_some();
7880 if ordered && (!grp.order_by.is_empty() || grp.offset.is_some() || grp.limit.is_some()) {
7881 return Err(self.type_err(
7882 "a group that already orders or limits its elements cannot be ordered or limited again by a `for` over it",
7883 ));
7884 }
7885 let td = self.resolve_type(&grp.source.type_name)?;
7886 let alias = grp.source.alias.clone();
7887 let synthetic = ast::SelectStmt {
7888 result: Expr::Path(ast::Path::absolute(grp.source.type_name.clone())),
7889 ..inner.clone()
7890 };
7891 let (filter, order_by, offset, limit) = self.compile_path_modifiers(&synthetic, td, &alias)?;
7892 grp.filter = and_conditions(grp.filter, filter.into_iter().collect());
7893 if ordered {
7894 grp.order_by = order_by;
7895 grp.offset = offset;
7896 grp.limit = limit;
7897 }
7898 if let Some(shape) = shape {
7899 let module = td.module.clone();
7900 grp.shape = self.compile_shape(shape, td, &alias, &module)?;
7901 }
7902 grp.output = IrGroupOutput::Elements;
7903 Ok(Some(grp))
7904 }
7905
7906 fn compile_link_key(&mut self, expr: &Expr, td: &TypeDescriptor, alias: &str) -> Result<Option<IrExpr>, PyQLError> {
7910 let value = match expr {
7914 Expr::Set(elements) if elements.len() == 1 => &elements[0],
7915 other => other,
7916 };
7917 match value {
7918 Expr::SubQuery(inner_stmt)
7921 if matches!(inner_stmt.as_ref(), Stmt::Select(sel) if matches!(sel.result, Expr::Set(_))) =>
7922 {
7923 let IrStmt::Select(select) = self.compile_stmt(inner_stmt)? else {
7924 return Ok(None);
7925 };
7926 if !select.rows.iter().all(|r| matches!(r, IrRowSource::Free(IrFreeExpr::Scalar(_)))) {
7927 return Ok(None);
7928 }
7929 Ok(Some(IrExpr::ScalarSubquery(Box::new(select))))
7930 }
7931 Expr::SubQuery(inner_stmt) => self.compile_link_subquery(inner_stmt).map(Some),
7934 Expr::FunctionCall(fc) => self.try_compile_fn_scalar_subquery(fc, &["id".to_string()], None, None),
7939 Expr::IfElse(ie)
7943 if [&ie.if_expr, &ie.else_expr].iter().any(|b| {
7944 matches!(b, Expr::SubQuery(s) if matches!(s.as_ref(), Stmt::Insert(_) | Stmt::Update(_) | Stmt::Delete(_)))
7945 }) =>
7946 {
7947 let Some(rewritten) = self.object_if_else_as_union(value) else {
7948 return Ok(None);
7949 };
7950 self.compile_link_key(&rewritten, td, alias)
7951 }
7952 Expr::Union(a, b) => {
7953 let (Some(left), Some(right)) = (self.compile_link_key(a, td, alias)?, self.compile_link_key(b, td, alias)?)
7954 else {
7955 return Ok(None);
7956 };
7957 let hoisted = |key: IrExpr| match key {
7961 IrExpr::ColumnRef { alias, column, .. } if column == "id" => IrExpr::CteRef {
7962 name: alias,
7963 scalar: false,
7964 pg_type: None,
7965 },
7966 other => other,
7967 };
7968 Ok(Some(IrExpr::FunctionCall(IrFunctionCall {
7969 return_pg_type: None,
7970 schema: None,
7971 name: "coalesce".to_string(),
7972 args: vec![hoisted(left), hoisted(right)],
7973 sql_template: None,
7974 })))
7975 }
7976 Expr::IfElse(ie) => {
7979 if ![&ie.if_expr, &ie.else_expr].iter().any(|b| matches!(b, Expr::FunctionCall(_))) {
7980 return Ok(None);
7981 }
7982 Ok(Some(IrExpr::IfElse(Box::new(IrIfElse {
7983 condition: self.compile_expr(&ie.condition, td, alias)?,
7984 if_: self.compile_link_branch(&ie.if_expr, td, alias)?,
7985 else_: self.compile_link_branch(&ie.else_expr, td, alias)?,
7986 }))))
7987 }
7988 _ => Ok(None),
7989 }
7990 }
7991
7992 fn compile_link_branch(&mut self, branch: &Expr, td: &TypeDescriptor, alias: &str) -> Result<IrExpr, PyQLError> {
7993 if matches!(branch, Expr::Set(elements) if elements.is_empty()) {
7994 return Ok(IrExpr::Null);
7995 }
7996 match self.compile_link_key(branch, td, alias)? {
7997 Some(key) => Ok(key),
7998 None => self.compile_expr(branch, td, alias),
7999 }
8000 }
8001
8002 fn compile_shape_field_select(
8006 &mut self,
8007 expr: &Expr,
8008 ctx: Option<(&TypeDescriptor, &str)>,
8009 ) -> Result<Option<IrPathSelect>, PyQLError> {
8010 let Expr::FieldAccess { expr: inner, field } = expr else {
8011 return Ok(None);
8012 };
8013 let Expr::Shape(sh) = inner.as_ref() else {
8014 return Ok(None);
8015 };
8016 let Some(Expr::Path(base)) = sh.expr.as_ref() else {
8017 return Ok(None);
8018 };
8019 let Some(value) = sh
8020 .elements
8021 .iter()
8022 .find(|el| matches!(el.path.steps.as_slice(), [ast::PathStep::Name(n)] if n == field))
8023 .and_then(|el| el.compexpr.clone())
8024 else {
8025 return Ok(None);
8026 };
8027 let (rooted, correlate) = if base.partial {
8028 let Some((td, alias)) = ctx else {
8029 return Ok(None);
8030 };
8031 let mut steps = vec![ast::PathStep::Name(format!("{}::{}", td.module, td.name))];
8032 steps.extend(base.steps.iter().cloned());
8033 (ast::Path { steps, partial: false }, Some(alias.to_string()))
8034 } else if base.steps.len() > 1 {
8035 (base.clone(), None)
8036 } else if let [ast::PathStep::Name(root)] = base.steps.as_slice()
8037 && let Ok(root_td) = self.resolve_path_root(root)
8038 {
8039 let narrowing = ast::PathStep::TypeIntersection(ast::ObjectRef {
8041 module: Some(root_td.module.clone()),
8042 name: root_td.name.clone(),
8043 });
8044 (
8045 ast::Path {
8046 steps: vec![base.steps[0].clone(), narrowing],
8047 partial: false,
8048 },
8049 None,
8050 )
8051 } else {
8052 return Ok(None);
8053 };
8054 let synthetic = ast::SelectStmt {
8055 result: Expr::Path(rooted.clone()),
8056 filter: None,
8057 order_by: vec![],
8058 offset: None,
8059 limit: None,
8060 lock: None,
8061 };
8062 let mut ps = self.compile_path_select(&synthetic, &rooted, &[], false)?;
8063 if let Some(alias) = correlate {
8064 Self::correlate_path_select(&mut ps, &alias);
8065 }
8066 let IrPathResult::Object { alias, type_name, .. } = &ps.result else {
8067 return Err(self.type_err(&format!(
8068 "'{field}' is read off a walk that lands on a value, which has no shape"
8069 )));
8070 };
8071 let (alias, type_name) = (alias.clone(), type_name.clone());
8072 let td = self.resolve_type(&type_name)?;
8073 let value = self.compile_expr(&value, td, &alias)?;
8074 let value = if yields_array(&value) {
8077 IrExpr::FunctionCall(IrFunctionCall {
8078 return_pg_type: None,
8079 schema: None,
8080 name: "unnest".to_string(),
8081 args: vec![value],
8082 sql_template: None,
8083 })
8084 } else {
8085 value
8086 };
8087 ps.result = IrPathResult::Scalar(value, None);
8088 Ok(Some(ps))
8089 }
8090
8091 fn is_scalar_walk(&self, expr: &Expr) -> bool {
8094 let Expr::Path(p) = expr else { return false };
8095 let (Some(ast::PathStep::Name(root)), false) = (p.steps.first(), p.partial) else {
8096 return false;
8097 };
8098 let (Some(ast::PathStep::Name(leaf)), [_, middle @ .., _]) = (p.steps.last(), p.steps.as_slice()) else {
8099 return false;
8100 };
8101 let Ok(root_td) = self.resolve_path_root(root) else {
8102 return false;
8103 };
8104 let owner = if middle.is_empty() {
8105 Some(root_td)
8106 } else {
8107 self.walk_path_types(root_td, middle, MAX_COMPUTED_SPLICES).1
8108 };
8109 owner.is_some_and(|td| Self::resolve_property(td, leaf).is_some())
8110 }
8111
8112 fn compile_scalar_union(&mut self, expr: &Expr) -> Result<IrStmt, PyQLError> {
8115 fn operands<'e>(expr: &'e Expr, out: &mut Vec<&'e Expr>) {
8116 match expr {
8117 Expr::Union(a, b) => {
8118 operands(a, out);
8119 operands(b, out);
8120 }
8121 other => out.push(other),
8122 }
8123 }
8124 let mut exprs = vec![];
8125 operands(expr, &mut exprs);
8126 let mut branches = Vec::with_capacity(exprs.len());
8127 for operand in exprs {
8128 let stmt = match operand {
8129 Expr::SubQuery(stmt) => stmt.as_ref().clone(),
8130 other => Stmt::Select(ast::SelectStmt {
8131 result: other.clone(),
8132 filter: None,
8133 order_by: vec![],
8134 offset: None,
8135 limit: None,
8136 lock: None,
8137 }),
8138 };
8139 branches.push(self.compile_stmt(&stmt)?);
8140 }
8141 let types: Vec<String> = branches.iter().map(cte_stmt_type).collect();
8142 let first = types.iter().find(|t| !t.is_empty()).cloned().unwrap_or_default();
8143 if let Some(other) = types
8144 .iter()
8145 .find(|t| t.contains("::") || (!t.is_empty() && **t != first))
8146 {
8147 let display = |t: &str| {
8148 if t.contains("::") {
8149 t.to_string()
8150 } else {
8151 pg_type_to_pyql(t).to_string()
8152 }
8153 };
8154 return Err(self.type_err(&format!(
8155 "operator 'UNION' cannot be applied to operands of type '{}' and '{}'",
8156 display(&first),
8157 display(other)
8158 )));
8159 }
8160 Ok(IrStmt::ScalarUnion(branches))
8161 }
8162
8163 fn dml_subject_type(&self, stmt: &Stmt) -> Result<String, PyQLError> {
8165 match stmt {
8166 Stmt::Insert(ins) => Ok(ins.subject.qualified_name()),
8167 Stmt::Update(upd) => self.subject_type_name(&upd.subject),
8168 Stmt::Delete(del) => self.subject_type_name(&del.subject),
8169 Stmt::With(w) => self.dml_subject_type(&w.stmt),
8170 Stmt::For(f) => self.dml_subject_type(&f.body),
8171 Stmt::Analyze(inner) => self.dml_subject_type(inner),
8172 Stmt::Group(g) => self.expr_as_type_name(&g.subject),
8173 Stmt::Select(sel) => {
8174 if let Expr::TypeCast(tc) = &sel.result
8176 && let Some((module, name)) = tc.ty.as_named()
8177 && module.map(|m| m != "std").unwrap_or(false)
8178 {
8179 return Ok(name.to_string());
8180 }
8181 if let Expr::Path(path) = &sel.result
8184 && !path.partial
8185 && path.steps.len() > 1
8186 && let Some(ast::PathStep::Name(root)) = path.steps.first()
8187 && let Ok(root_td) = self.resolve_path_root(root)
8188 && let (_, Some(target)) = self.walk_path_types(root_td, &path.steps[1..], MAX_COMPUTED_SPLICES)
8189 {
8190 return Ok(format!("{}::{}", target.module, target.name));
8191 }
8192 let (type_name, _, _, _) = self.extract_type_and_shape(&sel.result)?;
8194 Ok(type_name)
8195 }
8196 }
8197 }
8198
8199 fn compile_subject_path_rows(&mut self, subject: &ast::Path, target_alias: &str) -> Result<IrExpr, PyQLError> {
8202 let mut steps = subject.steps.clone();
8206 steps.push(ast::PathStep::Name("id".to_string()));
8207 let ids = ast::Path { steps, partial: false };
8208 let synthetic = ast::SelectStmt {
8209 result: Expr::Path(ids.clone()),
8210 filter: None,
8211 order_by: vec![],
8212 offset: None,
8213 limit: None,
8214 lock: None,
8215 };
8216 let rows = self.compile_path_select(&synthetic, &ids, &[], false)?;
8217 Ok(IrExpr::BinOp(Box::new(IrBinOp {
8218 left: IrExpr::ColumnRef {
8219 alias: target_alias.to_string(),
8220 column: "id".to_string(),
8221 pg_type: "uuid".to_string(),
8222 },
8223 op: ast::BinOpKind::In,
8224 right: IrExpr::ArrayFromSelect(Box::new(IrArraySource::PathSelect(Box::new(rows)))),
8225 })))
8226 }
8227
8228 fn subject_type_name(&self, subject: &Expr) -> Result<String, PyQLError> {
8231 if let Expr::Path(p) = subject
8232 && p.steps.len() > 1
8233 && let Some(ast::PathStep::Name(root)) = p.steps.first()
8234 && let Ok(root_td) = self.resolve_path_root(root)
8235 && let (_, Some(target)) = self.walk_path_types(root_td, &p.steps[1..], MAX_COMPUTED_SPLICES)
8236 {
8237 return Ok(format!("{}::{}", target.module, target.name));
8238 }
8239 if let Expr::Path(p) = subject
8244 && !p.partial
8245 && let [ast::PathStep::Name(root)] = p.steps.as_slice()
8246 && let Some(bound) = self.cte_object_type(root)
8247 {
8248 return Ok(bound);
8249 }
8250 self.expr_as_type_name(subject)
8251 }
8252
8253 fn expr_as_type_name(&self, expr: &Expr) -> Result<String, PyQLError> {
8254 if std::env::var("PYLON_DBG_SUBJ").is_ok() {
8255 eprintln!(
8256 "DBG subj {:.90?}
8257{}",
8258 expr,
8259 std::backtrace::Backtrace::force_capture()
8260 );
8261 }
8262 match expr {
8263 Expr::Detached(inner) => self.expr_as_type_name(inner),
8266 Expr::Path(p) if !p.partial && p.steps.len() == 1 => {
8267 if let ast::PathStep::Name(n) = &p.steps[0] {
8268 return Ok(n.clone());
8269 }
8270 Err(self.type_err("expected a type name"))
8271 }
8272 Expr::Union(_, _) => {
8275 let branches = self
8276 .object_union_branches(expr)
8277 .ok_or_else(|| self.type_err("expected a type name as SELECT subject"))?;
8278 self.common_union_type(&branches)
8279 .ok_or_else(|| self.type_err("expected a type name as SELECT subject"))
8280 }
8281 Expr::Path(p)
8285 if !p.partial
8286 && p.steps.len() > 1
8287 && let Some(ast::PathStep::Name(root)) = p.steps.first()
8288 && let Ok(root_td) = self.resolve_path_root(root)
8289 && let (_, Some(target)) = self.walk_path_types(root_td, &p.steps[1..], MAX_COMPUTED_SPLICES) =>
8290 {
8291 Ok(format!("{}::{}", target.module, target.name))
8292 }
8293 _ => Err(self.type_err("expected a type name as SELECT subject")),
8294 }
8295 }
8296
8297 fn compile_insert(&mut self, ins: &ast::InsertStmt) -> Result<IrInsert, PyQLError> {
8300 let pending_guard = self.pending_insert_guard.take();
8304 let outer_pending_nested_ctes = std::mem::take(&mut self.pending_nested_ctes);
8310 let type_name = ins.subject.qualified_name();
8311 let td = self.resolve_type(&type_name)?;
8312 if td.abstract_ && td.materialized {
8313 return Err(self.type_err(&format!(
8314 "cannot insert into interface type '{}::{}'; insert into a concrete type instead",
8315 td.module, td.name
8316 )));
8317 }
8318 let alias = self.fresh_alias();
8319 let target = IrSource {
8320 poly: None,
8321 type_name: format!("{}::{}", td.module, td.name),
8322 table: td.table.clone(),
8323 alias: alias.clone(),
8324 };
8325
8326 let mut shape = ins.shape.clone();
8330 for link in &td.links {
8331 let unset = !shape.iter().any(|el| path_leaf(&el.path).is_ok_and(|n| n == link.name));
8332 for rw in link.rewrites.iter().filter(|rw| rw.on & 1 != 0) {
8333 if let Some(value) = subject_default_insert(&rw.handler, &link.name)
8334 && unset
8335 {
8336 shape.push(ShapeElement {
8337 path: ast::Path::relative(&link.name),
8338 splat: None,
8339 nested: None,
8340 compexpr: Some(value),
8341 op: ShapeOp::Assign,
8342 filter: None,
8343 order_by: vec![],
8344 offset: None,
8345 limit: None,
8346 marker_offset: None,
8347 });
8348 }
8349 }
8350 }
8351 for (pointer, pyql) in inlined_pointer_defaults(td, self.schema) {
8357 if shape.iter().any(|el| path_leaf(&el.path).is_ok_and(|n| n == pointer)) {
8358 continue;
8359 }
8360 let value = crate::parse::parse_pointer_expr(&pyql)?;
8361 shape.push(default_shape_element(&pointer, value));
8362 }
8363 let mut multi_link_appends = vec![];
8364 let mut scalar_elements: Vec<ShapeElement> = vec![];
8365 for el in &shape {
8366 let pointer_name = match path_leaf(&el.path) {
8367 Ok(n) => n,
8368 Err(_) => {
8369 scalar_elements.push(el.clone());
8370 continue;
8371 }
8372 };
8373 if let Some(ml) = Self::resolve_multilink(td, pointer_name) {
8374 match el.op {
8375 ShapeOp::Remove => {
8376 return Err(self.type_err(&format!(
8377 "cannot use `-=` for multi-link '{pointer_name}' in an insert; \
8378 there is nothing to remove from yet"
8379 )));
8380 }
8381 ShapeOp::Assign | ShapeOp::Append => {
8382 if let Some(expr) = &el.compexpr {
8383 let (jt, module, src_col, tgt_col, through_td) =
8384 self.own_multilink_junction_info(td, ml)?;
8385 let values = self.compile_multilink_values(expr, td, &alias, through_td)?;
8386 multi_link_appends.push(IrMultiLinkMutation {
8387 junction_table: jt,
8388 module,
8389 source_col: src_col,
8390 target_col: tgt_col,
8391 values,
8392 single: false,
8393 });
8394 }
8395 }
8396 }
8397 } else if let Some(l) = Self::resolve_link(td, pointer_name).filter(|l| l.is_junction_backed()) {
8398 if matches!(el.op, ShapeOp::Remove) {
8403 return Err(self.type_err(&format!(
8404 "cannot use `-=` for link '{pointer_name}' in an insert; \
8405 there is nothing to remove from yet"
8406 )));
8407 }
8408 if let Some(expr) = &el.compexpr {
8409 if !is_empty_set_expr(expr) {
8413 let (jt, module, src_col, tgt_col, through_td) = self.own_link_junction_info(td, l)?;
8414 let values = self.compile_multilink_values(expr, td, &alias, through_td)?;
8415 multi_link_appends.push(IrMultiLinkMutation {
8416 junction_table: jt,
8417 module,
8418 source_col: src_col,
8419 target_col: tgt_col,
8420 values,
8421 single: true,
8422 });
8423 }
8424 }
8425 } else {
8426 scalar_elements.push(el.clone());
8427 }
8428 }
8429
8430 let assignments = self.compile_assignments(&scalar_elements, td, &alias)?;
8431 let rewrites = vec![];
8434 let unless_conflict = match ins.unless_conflict.as_ref() {
8435 Some(uc) => {
8436 let (conflict, else_appends) = self.compile_conflict(uc, td)?;
8437 multi_link_appends.extend(else_appends);
8438 Some(conflict)
8439 }
8440 None => None,
8441 };
8442 let returning = Self::pk_returning(td);
8443 let type_name = format!("{}::{}", td.module, td.name);
8444 let enqueue_vector = td
8445 .vector_indexes
8446 .iter()
8447 .map(|vi| VectorEnqueueInfo {
8448 type_name: type_name.clone(),
8449 index_name: vi.index_name.clone(),
8450 })
8451 .collect();
8452 let enqueue_search = collect_search_enqueue(td, &type_name, "index");
8453 let mut nested_ctes = std::mem::replace(&mut self.pending_nested_ctes, outer_pending_nested_ctes);
8454
8455 let guard = match pending_guard {
8456 Some(condition) => Some(self.compile_expr(&condition, td, &alias)?),
8457 None => None,
8458 };
8459 if let Some(condition) = &guard {
8464 fn cte_refs(appends: &[IrMultiLinkMutation], into: &mut Vec<String>) {
8465 into.extend(appends.iter().filter_map(|append| match &append.values.source {
8466 IrMultiLinkValueSource::CteRef(name) => Some(name.clone()),
8467 _ => None,
8468 }));
8469 }
8470 fn carry_guard(stmt: &mut IrStmt, condition: &IrExpr, pending: &mut Vec<String>) {
8471 if let IrStmt::For(loop_) = stmt {
8474 for def in &mut loop_.body_ctes {
8475 carry_guard(&mut def.stmt, condition, pending);
8476 }
8477 }
8478 let nested = match stmt {
8479 IrStmt::Insert(nested) => Some(nested),
8480 IrStmt::For(loop_) => match loop_.body.as_mut() {
8481 IrStmt::Insert(nested) => Some(nested),
8482 _ => None,
8483 },
8484 _ => None,
8485 };
8486 let Some(nested) = nested else { return };
8487 if nested.guard.is_none() {
8488 nested.guard = Some(condition.clone());
8489 }
8490 cte_refs(&nested.multi_link_appends, pending);
8491 for def in &mut nested.nested_ctes {
8492 carry_guard(&mut def.stmt, condition, pending);
8493 }
8494 }
8495 let mut pending: Vec<String> = vec![];
8498 cte_refs(&multi_link_appends, &mut pending);
8499 for def in &mut nested_ctes {
8500 carry_guard(&mut def.stmt, condition, &mut pending);
8501 }
8502 let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
8503 while let Some(name) = pending.pop() {
8504 if !seen.insert(name.clone()) {
8505 continue;
8506 }
8507 if let Some(index) = self.hoisted_ctes.iter().position(|def| def.name == name) {
8508 carry_guard(&mut self.hoisted_ctes[index].stmt, condition, &mut pending);
8509 }
8510 }
8511 }
8512 let id_default_sql =
8516 Self::resolve_property(td, "id").map(|p| p.default_sql.clone().unwrap_or_else(|| "uuidv7()".to_string()));
8517 Ok(IrInsert {
8518 guard,
8519 target,
8520 assignments,
8521 unless_conflict,
8522 rewrites,
8523 id_default_sql,
8524 returning,
8525 enqueue_vector,
8526 enqueue_search,
8527 multi_link_appends,
8528 nested_ctes,
8529 })
8530 }
8531
8532 fn compile_assignments(
8533 &mut self,
8534 elements: &[ShapeElement],
8535 td: &TypeDescriptor,
8536 alias: &str,
8537 ) -> Result<Vec<(String, IrExpr)>, PyQLError> {
8538 self.compile_assignments_inner(elements, td, alias, false)
8539 }
8540
8541 fn compile_assignments_for_update(
8542 &mut self,
8543 elements: &[ShapeElement],
8544 td: &TypeDescriptor,
8545 alias: &str,
8546 ) -> Result<Vec<(String, IrExpr)>, PyQLError> {
8547 self.compile_assignments_inner(elements, td, alias, true)
8548 }
8549
8550 fn compile_assignments_inner(
8551 &mut self,
8552 elements: &[ShapeElement],
8553 td: &TypeDescriptor,
8554 alias: &str,
8555 deny_readonly: bool,
8556 ) -> Result<Vec<(String, IrExpr)>, PyQLError> {
8557 self.without_implicit_id(|this| this.compile_assignments_rows(elements, td, alias, deny_readonly))
8561 }
8562
8563 fn compile_assignments_rows(
8564 &mut self,
8565 elements: &[ShapeElement],
8566 td: &TypeDescriptor,
8567 alias: &str,
8568 deny_readonly: bool,
8569 ) -> Result<Vec<(String, IrExpr)>, PyQLError> {
8570 elements
8571 .iter()
8572 .map(|el| {
8573 let pointer_name = path_leaf(&el.path)?;
8574 let expr = el.compexpr.as_ref().ok_or_else(|| {
8575 PyQLError::Type(PyQLTypeError {
8576 message: format!("INSERT pointer '{pointer_name}' has no value expression"),
8577 position: Position { line: 0, col: 0 },
8578 })
8579 })?;
8580
8581 let column = if let Some(p) = Self::resolve_property(td, pointer_name) {
8583 if p.is_pk && (deny_readonly || !self.config.allow_user_specified_id) {
8588 return Err(PyQLError::Type(PyQLTypeError {
8589 message: "cannot assign to property 'id'".to_string(),
8590 position: Position { line: 0, col: 0 },
8591 }));
8592 }
8593 if deny_readonly && p.is_readonly {
8594 return Err(PyQLError::Type(PyQLTypeError {
8595 message: format!("cannot update property '{pointer_name}': it is declared as read-only"),
8596 position: Position { line: 0, col: 0 },
8597 }));
8598 }
8599 p.name.clone()
8600 } else if let Some(l) = Self::resolve_link(td, pointer_name) {
8601 if deny_readonly && l.is_readonly {
8602 return Err(PyQLError::Type(PyQLTypeError {
8603 message: format!("cannot update link '{pointer_name}': it is declared as read-only"),
8604 position: Position { line: 0, col: 0 },
8605 }));
8606 }
8607 if l.is_junction_backed() {
8608 return Err(PyQLError::Type(PyQLTypeError {
8615 message: format!(
8616 "'{pointer_name}' is a junction-backed link and cannot be \
8617 assigned inside an UNLESS CONFLICT ELSE clause"
8618 ),
8619 position: Position { line: 0, col: 0 },
8620 }));
8621 }
8622 let fk_col = format!("{}_id", l.name);
8628 if let Some(ir_expr) = self.compile_link_key(expr, td, alias)? {
8629 return Ok((fk_col, ir_expr));
8630 }
8631 fk_col
8632 } else if Self::resolve_multilink(td, pointer_name).is_some() {
8633 return Err(PyQLError::Type(PyQLTypeError {
8641 message: format!(
8642 "'{pointer_name}' is a multi-link and cannot be mutated inside an \
8643 UNLESS CONFLICT ELSE clause"
8644 ),
8645 position: Position { line: 0, col: 0 },
8646 }));
8647 } else if Self::resolve_multilink(td, pointer_name).is_some() {
8648 return Err(self.field_err(pointer_name, &format!("{}::{}", td.module, td.name)));
8649 } else if self.resolve_computed(td, pointer_name).is_some() {
8650 return Err(self.type_err(&format!(
8655 "cannot assign to '{pointer_name}': it is a computed pointer on \
8656 {}::{}, which has no stored column to write",
8657 td.module, td.name
8658 )));
8659 } else {
8660 return Err(self.field_err(pointer_name, &format!("{}::{}", td.module, td.name)));
8661 };
8662
8663 let ir_expr = if matches!(expr, Expr::Set(v) if v.is_empty()) {
8665 IrExpr::Null
8666 } else {
8667 self.compile_expr(expr, td, alias)?
8668 };
8669 Ok((column, ir_expr))
8670 })
8671 .collect()
8672 }
8673
8674 fn compile_update(&mut self, upd: &ast::UpdateStmt) -> Result<IrUpdate, PyQLError> {
8677 let pending_guard = self.pending_update_guard.take();
8680 let outer_pending_nested_ctes = std::mem::take(&mut self.pending_nested_ctes);
8682 if !matches!(&upd.subject, Expr::Path(_)) {
8688 let (base, fields) = Self::peel_field_access_chain(&upd.subject);
8689 if let Expr::SubQuery(inner_stmt) = base
8690 && !fields.is_empty()
8691 && matches!(inner_stmt.as_ref(), Stmt::Select(_))
8692 {
8693 let inner = self.compile_stmt(inner_stmt)?;
8694 let cte_name = self.fresh_nested_cte_name();
8695 let type_name = self.register_cte(&cte_name, &inner);
8696 self.hoisted_ctes.push(IrCteDef {
8697 name: cte_name.clone(),
8698 stmt: inner,
8699 type_name,
8700 correlated_to: None,
8701 });
8702 let mut steps = vec![ast::PathStep::Name(cte_name)];
8703 steps.extend(fields.into_iter().map(ast::PathStep::Name));
8704 let rerooted = ast::UpdateStmt {
8705 subject: Expr::Path(ast::Path { steps, partial: false }),
8706 filter: upd.filter.clone(),
8707 shape: upd.shape.clone(),
8708 };
8709 self.pending_nested_ctes = outer_pending_nested_ctes;
8710 self.pending_update_guard = pending_guard;
8711 return self.compile_update(&rerooted);
8712 }
8713 }
8714 if let Expr::Path(subject) = &upd.subject
8719 && subject.steps.len() > 1
8720 && let Some(ast::PathStep::Name(root)) = subject.steps.first()
8721 && let Ok(root_td) = self.resolve_path_root(root)
8722 && let (_, Some(target_td)) = self.walk_path_types(root_td, &subject.steps[1..], MAX_COMPUTED_SPLICES)
8723 {
8724 let narrowed = ast::UpdateStmt {
8725 subject: Expr::Path(ast::Path {
8726 steps: vec![ast::PathStep::Name(format!("{}::{}", target_td.module, target_td.name))],
8727 partial: false,
8728 }),
8729 filter: upd.filter.clone(),
8730 shape: upd.shape.clone(),
8731 };
8732 self.pending_nested_ctes = outer_pending_nested_ctes;
8733 self.pending_update_guard = pending_guard;
8734 let mut ir = self.compile_update(&narrowed)?;
8735 let rows = self.compile_subject_path_rows(subject, &ir.target.alias)?;
8739 ir.filter = Some(and_conditions(ir.filter.take(), vec![rows]).expect("row set is present"));
8740 return Ok(ir);
8741 }
8742 let type_name = self.expr_as_type_name(&upd.subject)?;
8743 let bound_rows = self.cte_object_type(&type_name);
8748 let iterated = self
8752 .for_var_types
8753 .contains_key(&type_name)
8754 .then(|| self.for_var_ref(&type_name));
8755 let td = self.resolve_path_root(&type_name)?;
8756 let alias = self.fresh_alias();
8757 let target = IrSource {
8758 poly: None,
8759 type_name: format!("{}::{}", td.module, td.name),
8760 table: td.table.clone(),
8761 alias: alias.clone(),
8762 };
8763
8764 let declared_filter = upd
8765 .filter
8766 .as_ref()
8767 .map(|f| self.as_condition(|c| c.compile_expr(f, td, &alias)))
8768 .transpose()?;
8769 let filter = match bound_rows {
8770 Some(_) => {
8771 let membership = IrExpr::BinOp(Box::new(IrBinOp {
8772 left: IrExpr::ColumnRef {
8773 alias: alias.clone(),
8774 column: "id".to_string(),
8775 pg_type: "uuid".to_string(),
8776 },
8777 op: ast::BinOpKind::In,
8778 right: IrExpr::ArrayFromSelect(Box::new(IrArraySource::Select(IrSelect::schema_bound(
8779 IrSource {
8780 poly: None,
8781 type_name: format!("{}::{}", td.module, td.name),
8782 table: format!("@cte:{type_name}"),
8783 alias: self.fresh_alias(),
8784 },
8785 vec![],
8786 None,
8787 )))),
8788 }));
8789 Some(and_conditions(declared_filter, vec![membership]).expect("membership is present"))
8790 }
8791 None => declared_filter,
8792 };
8793 let filter = match iterated {
8794 Some(value) => {
8795 let this_row = IrExpr::BinOp(Box::new(IrBinOp {
8796 left: IrExpr::ColumnRef {
8797 alias: alias.clone(),
8798 column: "id".to_string(),
8799 pg_type: "uuid".to_string(),
8800 },
8801 op: ast::BinOpKind::Eq,
8802 right: value,
8803 }));
8804 Some(and_conditions(filter, vec![this_row]).expect("the row condition is present"))
8805 }
8806 None => filter,
8807 };
8808 let filter = match pending_guard {
8812 Some(condition) => {
8813 let guard = self.compile_expr(&condition, td, &alias)?;
8814 and_conditions(filter, vec![guard])
8815 }
8816 None => filter,
8817 };
8818
8819 let mut multi_link_clears = vec![];
8821 let mut multi_link_replaces = vec![];
8822 let mut multi_link_appends = vec![];
8823 let mut multi_link_removals = vec![];
8824 let mut scalar_elements: Vec<ShapeElement> = vec![];
8825
8826 for el in &upd.shape {
8827 let pointer_name = match path_leaf(&el.path) {
8828 Ok(n) => n,
8829 Err(_) => {
8830 scalar_elements.push(el.clone());
8831 continue;
8832 }
8833 };
8834
8835 if let Some(ml) = Self::resolve_multilink(td, pointer_name) {
8836 let (jt, module, src_col, tgt_col, through_td) = self.own_multilink_junction_info(td, ml)?;
8837
8838 match el.op {
8839 ShapeOp::Assign => {
8840 let is_empty = el
8841 .compexpr
8842 .as_ref()
8843 .map(|e| matches!(e, Expr::Set(v) if v.is_empty()))
8844 .unwrap_or(false);
8845 if is_empty {
8846 multi_link_clears.push(IrMultiLinkClear {
8848 junction_table: jt,
8849 module,
8850 source_col: src_col,
8851 });
8852 } else if let Some(expr) = &el.compexpr {
8853 multi_link_clears.push(IrMultiLinkClear {
8855 junction_table: jt.clone(),
8856 module: module.clone(),
8857 source_col: src_col.clone(),
8858 });
8859 let values = self.compile_multilink_values(expr, td, &alias, through_td)?;
8860 multi_link_replaces.push(IrMultiLinkMutation {
8861 junction_table: jt,
8862 module,
8863 source_col: src_col,
8864 target_col: tgt_col,
8865 values,
8866 single: false,
8867 });
8868 }
8869 }
8870 ShapeOp::Append => {
8871 if let Some(expr) = &el.compexpr {
8872 let values = self.compile_multilink_values(expr, td, &alias, through_td)?;
8873 multi_link_appends.push(IrMultiLinkMutation {
8874 junction_table: jt,
8875 module,
8876 source_col: src_col,
8877 target_col: tgt_col,
8878 values,
8879 single: false,
8880 });
8881 }
8882 }
8883 ShapeOp::Remove => {
8884 if let Some(expr) = &el.compexpr {
8885 let values = self.compile_multilink_values(expr, td, &alias, through_td)?;
8886 if has_any_link_props(&values) {
8887 return Err(self.type_err(
8888 "link properties (`@prop := value`) cannot be assigned \
8889 when removing a link (`-=`)",
8890 ));
8891 }
8892 multi_link_removals.push(IrMultiLinkMutation {
8893 junction_table: jt,
8894 module,
8895 source_col: src_col,
8896 target_col: tgt_col,
8897 values,
8898 single: false,
8899 });
8900 }
8901 }
8902 }
8903 } else if let Some(l) = Self::resolve_link(td, pointer_name).filter(|l| l.is_junction_backed()) {
8904 if !matches!(el.op, ShapeOp::Assign) {
8908 return Err(self.type_err(&format!(
8909 "'{pointer_name}' is a single link; only `:=` is supported, not `+=`/`-=`"
8910 )));
8911 }
8912 let (jt, module, src_col, tgt_col, through_td) = self.own_link_junction_info(td, l)?;
8913 let is_empty = el.compexpr.as_ref().map(is_empty_set_expr).unwrap_or(false);
8914 if is_empty {
8915 multi_link_clears.push(IrMultiLinkClear {
8917 junction_table: jt,
8918 module,
8919 source_col: src_col,
8920 });
8921 } else if let Some(expr) = &el.compexpr {
8922 multi_link_clears.push(IrMultiLinkClear {
8923 junction_table: jt.clone(),
8924 module: module.clone(),
8925 source_col: src_col.clone(),
8926 });
8927 let values = self.compile_multilink_values(expr, td, &alias, through_td)?;
8928 multi_link_replaces.push(IrMultiLinkMutation {
8929 junction_table: jt,
8930 module,
8931 source_col: src_col,
8932 target_col: tgt_col,
8933 values,
8934 single: true,
8935 });
8936 }
8937 } else {
8938 scalar_elements.push(el.clone());
8939 }
8940 }
8941
8942 let assignments = self.compile_assignments_for_update(&scalar_elements, td, &alias)?;
8943 let rewrites = vec![];
8945 let returning = Self::pk_returning(td);
8946
8947 let (poly_implementors, poly_columns) = if self.is_polymorphic(td) {
8948 (
8949 self.find_poly_implementors(&format!("{}::{}", td.module, td.name)),
8950 Self::poly_dml_columns(td),
8951 )
8952 } else {
8953 (vec![], vec![])
8954 };
8955
8956 let written_cols: std::collections::HashSet<&str> = assignments.iter().map(|(c, _)| c.as_str()).collect();
8958 let type_name = format!("{}::{}", td.module, td.name);
8959 let enqueue_vector: Vec<VectorEnqueueInfo> = td
8960 .vector_indexes
8961 .iter()
8962 .filter(|vi| vi.pointers.iter().any(|f| written_cols.contains(f.as_str())))
8963 .map(|vi| VectorEnqueueInfo {
8964 type_name: type_name.clone(),
8965 index_name: vi.index_name.clone(),
8966 })
8967 .collect();
8968 let enqueue_search = collect_search_enqueue(td, &type_name, "index");
8969 let nested_ctes = std::mem::replace(&mut self.pending_nested_ctes, outer_pending_nested_ctes);
8975
8976 Ok(IrUpdate {
8977 target,
8978 filter,
8979 assignments,
8980 rewrites,
8981 returning,
8982 multi_link_clears,
8983 multi_link_replaces,
8984 multi_link_appends,
8985 multi_link_removals,
8986 poly_implementors,
8987 poly_columns,
8988 enqueue_vector,
8989 enqueue_search,
8990 nested_ctes,
8991 })
8992 }
8993
8994 fn junction_info_for(
9003 &mut self,
9004 td: &TypeDescriptor,
9005 name: &str,
9006 target: &str,
9007 through: &Option<String>,
9008 ) -> Result<(String, String, String, String, Option<&'a TypeDescriptor>), PyQLError> {
9009 let (junction_table, module, source_col, target_col, through_td) =
9010 self.own_junction_info_for(td, name, target, through)?;
9011 let owner_derived = through_td.is_none_or(|through_td| through_td.junction);
9012 let junction_table = if owner_derived {
9013 self.owner_junction(td, name, through_td)
9014 } else {
9015 junction_table
9016 };
9017 Ok((junction_table, module, source_col, target_col, through_td))
9018 }
9019
9020 fn junction_tables_of(&mut self, td: &TypeDescriptor) -> Result<Vec<String>, PyQLError> {
9025 let mut tables = vec![];
9026 for multilink in &td.multilinks {
9027 match &multilink.through {
9028 Some(through) => {
9029 let through_td = self.resolve_type(through)?;
9030 tables.push(format!("\"{}\"", through_td.table));
9031 }
9032 None => {
9033 tables.push(format!("\"{}.{}\"", td.table, multilink.name));
9034 for implementor in self.find_poly_implementors(&format!("{}::{}", td.module, td.name)) {
9035 tables.push(format!("\"{}.{}\"", implementor.table, multilink.name));
9036 }
9037 }
9038 }
9039 }
9040 Ok(tables)
9041 }
9042
9043 fn owner_junction(&self, td: &TypeDescriptor, name: &str, through_td: Option<&TypeDescriptor>) -> String {
9046 let own = format!("{}.{}", td.table, name);
9047 if !self.has_subtypes(td) {
9048 return own;
9049 }
9050 let tables = self
9051 .find_poly_implementors(&format!("{}::{}", td.module, td.name))
9052 .into_iter()
9053 .map(|implementor| (implementor.module, format!("{}.{}", implementor.table, name)))
9054 .collect::<Vec<_>>();
9055 let mut columns = vec!["source".to_string(), "target".to_string()];
9056 if let Some(through_td) = through_td {
9057 columns.extend(Self::poly_dml_columns(through_td).into_iter().filter(|c| c != "id"));
9058 }
9059 super::inherited_junction(&tables, &columns)
9060 }
9061
9062 fn own_junction_info_for(
9065 &mut self,
9066 td: &TypeDescriptor,
9067 name: &str,
9068 target: &str,
9069 through: &Option<String>,
9070 ) -> Result<(String, String, String, String, Option<&'a TypeDescriptor>), PyQLError> {
9071 match through {
9072 None => Ok((
9073 format!("{}.{}", td.table, name),
9074 td.module.clone(),
9075 "source".to_string(),
9076 "target".to_string(),
9077 None,
9078 )),
9079 Some(through_qname) => {
9080 let through_td = self.resolve_type(through_qname)?;
9081 let src_type = format!("{}::{}", td.module, td.name);
9082 let source_col = through_td
9083 .links
9084 .iter()
9085 .find(|l| l.target == src_type)
9086 .map(|l| l.name.clone())
9087 .unwrap_or_else(|| "source".to_string());
9088 let target_col = through_td
9095 .links
9096 .iter()
9097 .find(|l| l.target == target && l.name != source_col)
9098 .or_else(|| through_td.links.iter().find(|l| l.target == target))
9099 .map(|l| l.name.clone())
9100 .unwrap_or_else(|| "target".to_string());
9101 let (junction_table, junction_module) = if through_td.junction {
9116 (format!("{}.{}", td.table, name), td.module.clone())
9117 } else {
9118 (through_td.table.clone(), through_td.module.clone())
9119 };
9120 Ok((
9121 junction_table,
9122 junction_module,
9123 source_col,
9124 target_col,
9125 Some(through_td),
9126 ))
9127 }
9128 }
9129 }
9130
9131 fn multilink_junction_info(
9132 &mut self,
9133 td: &TypeDescriptor,
9134 ml: &MultiLinkDescriptor,
9135 ) -> Result<(String, String, String, String, Option<&'a TypeDescriptor>), PyQLError> {
9136 self.junction_info_for(td, &ml.name, &ml.target, &ml.through)
9137 }
9138
9139 fn own_multilink_junction_info(
9140 &mut self,
9141 td: &TypeDescriptor,
9142 ml: &MultiLinkDescriptor,
9143 ) -> Result<(String, String, String, String, Option<&'a TypeDescriptor>), PyQLError> {
9144 self.own_junction_info_for(td, &ml.name, &ml.target, &ml.through)
9145 }
9146
9147 fn own_link_junction_info(
9148 &mut self,
9149 td: &TypeDescriptor,
9150 l: &LinkDescriptor,
9151 ) -> Result<(String, String, String, String, Option<&'a TypeDescriptor>), PyQLError> {
9152 self.own_junction_info_for(td, &l.name, &l.target, &l.through)
9153 }
9154
9155 fn link_junction_info(
9157 &mut self,
9158 td: &TypeDescriptor,
9159 l: &LinkDescriptor,
9160 ) -> Result<(String, String, String, String, Option<&'a TypeDescriptor>), PyQLError> {
9161 self.junction_info_for(td, &l.name, &l.target, &l.through)
9162 }
9163
9164 fn build_multilink_join(
9169 &mut self,
9170 td: &TypeDescriptor,
9171 name: &str,
9172 target: &str,
9173 through: &Option<String>,
9174 ) -> Result<IrMultiLinkJoin, PyQLError> {
9175 if let Some(through_qname) = through {
9176 let through_td = self.resolve_type(through_qname)?;
9177 if through_td.junction {
9178 Ok(IrMultiLinkJoin::Standard {
9183 junction_table: self.owner_junction(td, name, Some(through_td)),
9184 module: td.module.clone(),
9185 })
9186 } else {
9187 let source_qname = format!("{}::{}", td.module, td.name);
9188 let source_col = through_td
9189 .links
9190 .iter()
9191 .find(|l| l.target == source_qname)
9192 .ok_or_else(|| {
9193 PyQLError::Type(PyQLTypeError {
9194 message: format!("through type {through_qname} has no link to {source_qname}"),
9195 position: Position { line: 0, col: 0 },
9196 })
9197 })?
9198 .name
9199 .clone();
9200 let target_col = through_td
9201 .links
9202 .iter()
9203 .find(|l| l.target == target && l.name != source_col)
9204 .or_else(|| through_td.links.iter().find(|l| l.target == target))
9205 .ok_or_else(|| {
9206 PyQLError::Type(PyQLTypeError {
9207 message: format!("through type {through_qname} has no link to target {target}"),
9208 position: Position { line: 0, col: 0 },
9209 })
9210 })?
9211 .name
9212 .clone();
9213 Ok(IrMultiLinkJoin::Through {
9214 junction_table: through_td.table.clone(),
9215 module: through_td.module.clone(),
9216 source_col,
9217 target_col,
9218 })
9219 }
9220 } else {
9221 let junction_table = self.owner_junction(td, name, None);
9222 Ok(IrMultiLinkJoin::Standard {
9223 junction_table: match self.junction_read_overrides.get(&junction_table) {
9224 Some(o) => o.junction.clone(),
9225 None => junction_table,
9226 },
9227 module: td.module.clone(),
9228 })
9229 }
9230 }
9231
9232 fn compile_multilink_values(
9241 &mut self,
9242 expr: &Expr,
9243 td: &'a TypeDescriptor,
9244 alias: &str,
9245 through_td: Option<&'a TypeDescriptor>,
9246 ) -> Result<IrMultiLinkValues, PyQLError> {
9247 self.without_implicit_id(|this| this.compile_multilink_values_inner(expr, td, alias, through_td))
9251 }
9252
9253 fn compile_multilink_values_inner(
9254 &mut self,
9255 expr: &Expr,
9256 td: &'a TypeDescriptor,
9257 alias: &str,
9258 through_td: Option<&'a TypeDescriptor>,
9259 ) -> Result<IrMultiLinkValues, PyQLError> {
9260 if let Expr::Union(a, b) = expr {
9263 let left = self.compile_multilink_values(a, td, alias, through_td)?;
9264 let right = self.compile_multilink_values(b, td, alias, through_td)?;
9265 return Ok(IrMultiLinkValues {
9266 source: IrMultiLinkValueSource::Union(Box::new(left), Box::new(right)),
9267 link_props: vec![],
9268 });
9269 }
9270
9271 if matches!(expr, Expr::IfElse(_))
9276 && let Some(rewritten) = self.object_if_else_as_union(expr)
9277 {
9278 return self.compile_multilink_values(&rewritten, td, alias, through_td);
9279 }
9280
9281 if let Expr::SubQuery(inner) = expr
9286 && let Stmt::Select(sel) = inner.as_ref()
9287 && let Expr::Shape(sh) = &sel.result
9288 && !sh.elements.is_empty()
9289 && sh
9290 .elements
9291 .iter()
9292 .all(|el| matches!(el.path.steps.as_slice(), [ast::PathStep::LinkProp(_)]))
9293 && let Some(base) = sh.expr.clone()
9294 {
9295 let lifted = Expr::Shape(Box::new(ast::ShapeExpr {
9296 expr: Some(Expr::SubQuery(Box::new(Stmt::Select(ast::SelectStmt {
9297 result: base,
9298 ..sel.clone()
9299 })))),
9300 elements: sh.elements.clone(),
9301 marker_offset: sh.marker_offset,
9302 }));
9303 return self.compile_multilink_values(&lifted, td, alias, through_td);
9304 }
9305
9306 if let Expr::Shape(shape) = expr {
9309 let inner_expr = shape
9310 .expr
9311 .as_ref()
9312 .ok_or_else(|| self.type_err("multilink value shape must have a base expression"))?;
9313 let mut inner = self.compile_multilink_values(inner_expr, td, alias, through_td)?;
9314 let walked_junction = match (&inner.source, inner_expr) {
9318 (IrMultiLinkValueSource::PathSelect(ps), Expr::SubQuery(stmt)) => {
9319 match (stmt.as_ref(), ps.joins.last()) {
9320 (Stmt::Select(sel), Some(IrPathJoin::Multi { junction_alias, .. })) => match &sel.result {
9321 Expr::Path(p) if p.partial => match p.steps.as_slice() {
9322 [ast::PathStep::Name(link)] => Self::resolve_multilink(td, link)
9323 .and_then(|m| m.through.clone())
9324 .map(|through| (through, junction_alias.clone())),
9325 _ => None,
9326 },
9327 _ => None,
9328 },
9329 _ => None,
9330 }
9331 }
9332 _ => None,
9333 };
9334
9335 let Some(through) = through_td else {
9336 return Err(self.type_err(
9337 "link properties (`@prop := value`) are only valid on a multi-link \
9338 declared with `Through[...]`",
9339 ));
9340 };
9341
9342 for el in &shape.elements {
9343 let prop_name = match el.path.steps.as_slice() {
9344 [ast::PathStep::LinkProp(name)] => name.clone(),
9345 _ => return Err(self.type_err("only `@prop := value` link-property assignments are valid here")),
9346 };
9347 let prop = Self::resolve_property(through, &prop_name)
9348 .ok_or_else(|| self.field_err(&prop_name, &format!("{}::{}", through.module, through.name)))?;
9349 if prop.is_readonly {
9350 return Err(self.type_err(&format!(
9351 "cannot set link property '{prop_name}': it is declared as read-only"
9352 )));
9353 }
9354 let value_expr = el
9355 .compexpr
9356 .as_ref()
9357 .ok_or_else(|| self.type_err(&format!("link property '{prop_name}' must be assigned a value")))?;
9358 let scoped = walked_junction.is_some();
9359 if scoped {
9360 self.link_prop_scope.push(walked_junction.clone());
9361 }
9362 let ir_expr = self.compile_expr(value_expr, td, alias);
9363 if scoped {
9364 self.link_prop_scope.pop();
9365 }
9366 inner.link_props.push((prop.name.clone(), ir_expr?));
9367 }
9368 return Ok(inner);
9369 }
9370
9371 if let Expr::FunctionCall(f) = expr
9374 && (f.module.is_none() || f.module.as_deref() == Some("std"))
9375 && matches!(f.name.as_str(), "assert_exists" | "assert_distinct")
9376 && let [arg] = f.args.as_slice()
9377 {
9378 let arg = arg.clone();
9379 let inner = self.compile_multilink_values(&arg, td, alias, through_td)?;
9380 let mut props = vec![];
9384 crate::sql::collect_link_prop_names(&inner, &mut props);
9385 if !props.is_empty() {
9386 return Err(self.type_err(&format!(
9387 "'{}' cannot be applied to a link that carries link properties ({}) — the check reads the targets alone",
9388 f.name,
9389 props.join(", "),
9390 )));
9391 }
9392 let message = self.assert_message(f, Some((td, alias)))?;
9393 return Ok(IrMultiLinkValues {
9394 source: IrMultiLinkValueSource::Asserted {
9395 fn_name: f.name.clone(),
9396 inner: Box::new(inner),
9397 message,
9398 },
9399 link_props: vec![],
9400 });
9401 }
9402
9403 if let Expr::SubQuery(inner) = expr
9409 && let Stmt::Select(sel) = inner.as_ref()
9410 && sel.filter.is_none()
9411 && sel.order_by.is_empty()
9412 && sel.offset.is_none()
9413 && sel.limit.is_none()
9414 && matches!(sel.result, Expr::Set(_) | Expr::Union(_, _))
9415 {
9416 let result = sel.result.clone();
9417 return self.compile_multilink_values(&result, td, alias, through_td);
9418 }
9419
9420 if let Some(name) = self.resolve_cte_name(expr) {
9422 return Ok(IrMultiLinkValues {
9423 source: IrMultiLinkValueSource::CteRef(name.to_string()),
9424 link_props: vec![],
9425 });
9426 }
9427
9428 if let Expr::Set(elements) = expr
9430 && !elements.is_empty()
9431 {
9432 let mut combined: Option<IrMultiLinkValues> = None;
9433 for element in elements {
9434 let one = self.compile_multilink_values(element, td, alias, through_td)?;
9435 combined = Some(match combined {
9436 None => one,
9437 Some(previous) => IrMultiLinkValues {
9438 source: IrMultiLinkValueSource::Union(Box::new(previous), Box::new(one)),
9439 link_props: vec![],
9440 },
9441 });
9442 }
9443 return Ok(combined.expect("elements is non-empty"));
9444 }
9445
9446 if let Expr::SubQuery(inner) = expr
9450 && let Stmt::For(_) = inner.as_ref()
9451 {
9452 let (cte_name, _) = self.hoist_dml_as_cte(inner.as_ref())?;
9453 return Ok(IrMultiLinkValues {
9454 source: IrMultiLinkValueSource::CteRef(cte_name),
9455 link_props: vec![],
9456 });
9457 }
9458
9459 if let Expr::SubQuery(inner) = expr
9462 && matches!(inner.as_ref(), Stmt::Insert(_) | Stmt::Update(_) | Stmt::Delete(_))
9463 {
9464 let cte_name = self.hoist_nested_dml(inner)?;
9465 return Ok(IrMultiLinkValues {
9466 source: IrMultiLinkValueSource::CteRef(cte_name),
9467 link_props: vec![],
9468 });
9469 }
9470
9471 if let Expr::SubQuery(inner) = expr
9476 && let Stmt::Select(sel) = inner.as_ref()
9477 && let Expr::Path(path) = &sel.result
9478 && path.partial
9479 {
9480 let mut steps = vec![ast::PathStep::Name(format!("{}::{}", td.module, td.name))];
9481 steps.extend(path.steps.iter().cloned());
9482 let rooted = ast::Path { steps, partial: false };
9483 let synthetic = ast::SelectStmt {
9484 result: Expr::Path(rooted.clone()),
9485 filter: sel.filter.clone(),
9486 order_by: sel.order_by.clone(),
9487 offset: sel.offset.clone(),
9488 limit: sel.limit.clone(),
9489 lock: None,
9490 };
9491 let mut ps = self.compile_path_select(&synthetic, &rooted, &[], false)?;
9492 Self::correlate_path_select(&mut ps, alias);
9493 return Ok(IrMultiLinkValues {
9494 source: IrMultiLinkValueSource::PathSelect(Box::new(ps)),
9495 link_props: vec![],
9496 });
9497 }
9498
9499 if let Expr::SubQuery(inner) = expr {
9501 return match self.compile_stmt(inner)? {
9502 IrStmt::Select(s) if matches!(s.rows.as_slice(), [IrRowSource::Bound { .. }]) => {
9503 Ok(IrMultiLinkValues {
9504 source: IrMultiLinkValueSource::Select(Box::new(s)),
9505 link_props: vec![],
9506 })
9507 }
9508 IrStmt::PathSelect(ps) => Ok(IrMultiLinkValues {
9509 source: IrMultiLinkValueSource::PathSelect(Box::new(ps)),
9510 link_props: vec![],
9511 }),
9512 _ => Err(self.type_err("multilink value must resolve to a SELECT or path query")),
9513 };
9514 }
9515
9516 if let Expr::FunctionCall(fc) = expr {
9517 let synthetic = ast::SelectStmt {
9518 result: expr.clone(),
9519 filter: None,
9520 order_by: vec![],
9521 offset: None,
9522 limit: None,
9523 lock: None,
9524 };
9525 if let Some(fs) = self.try_compile_fn_object_select(fc, &[], &synthetic, false)? {
9526 return Ok(IrMultiLinkValues {
9527 source: IrMultiLinkValueSource::Function(Box::new(fs)),
9528 link_props: vec![],
9529 });
9530 }
9531 }
9532
9533 if let Expr::Path(p) = expr
9535 && !p.partial
9536 {
9537 let fake_sel = ast::SelectStmt {
9538 result: expr.clone(),
9539 filter: None,
9540 order_by: vec![],
9541 offset: None,
9542 limit: None,
9543 lock: None,
9544 };
9545 return match self.compile_stmt(&Stmt::Select(fake_sel))? {
9546 IrStmt::PathSelect(ps) => Ok(IrMultiLinkValues {
9547 source: IrMultiLinkValueSource::PathSelect(Box::new(ps)),
9548 link_props: vec![],
9549 }),
9550 IrStmt::Select(s) if matches!(s.rows.as_slice(), [IrRowSource::Bound { .. }]) => {
9551 Ok(IrMultiLinkValues {
9552 source: IrMultiLinkValueSource::Select(Box::new(s)),
9553 link_props: vec![],
9554 })
9555 }
9556 _ => Err(self.type_err("expected a path expression for multilink value")),
9557 };
9558 }
9559
9560 Err(self.type_err("multilink value must be a CTE reference, parenthesised subquery, or type path"))
9561 }
9562
9563 fn compile_delete(&mut self, del: &ast::DeleteStmt) -> Result<IrDelete, PyQLError> {
9566 let pending_guard = self.pending_delete_guard.take();
9569 let type_name = self.expr_as_type_name(&del.subject)?;
9570 let bound_rows = self.cte_object_type(&type_name);
9575 let td = self.resolve_path_root(&type_name)?;
9576 let alias = self.fresh_alias();
9577 let target = IrSource {
9578 poly: None,
9579 type_name: format!("{}::{}", td.module, td.name),
9580 table: td.table.clone(),
9581 alias: alias.clone(),
9582 };
9583
9584 let declared_filter = del
9585 .filter
9586 .as_ref()
9587 .map(|f| self.as_condition(|c| c.compile_expr(f, td, &alias)))
9588 .transpose()?;
9589 let filter = match bound_rows {
9590 Some(_) => {
9591 let membership = IrExpr::BinOp(Box::new(IrBinOp {
9592 left: IrExpr::ColumnRef {
9593 alias: alias.clone(),
9594 column: "id".to_string(),
9595 pg_type: "uuid".to_string(),
9596 },
9597 op: ast::BinOpKind::In,
9598 right: IrExpr::ArrayFromSelect(Box::new(IrArraySource::Select(IrSelect::schema_bound(
9599 IrSource {
9600 poly: None,
9601 type_name: format!("{}::{}", td.module, td.name),
9602 table: format!("@cte:{type_name}"),
9603 alias: self.fresh_alias(),
9604 },
9605 vec![],
9606 None,
9607 )))),
9608 }));
9609 Some(and_conditions(declared_filter, vec![membership]).expect("membership is present"))
9610 }
9611 None => declared_filter,
9612 };
9613 let filter = match pending_guard {
9614 Some(condition) => {
9615 let guard = self.compile_expr(&condition, td, &alias)?;
9616 and_conditions(filter, vec![guard])
9617 }
9618 None => filter,
9619 };
9620
9621 let returning = Self::pk_returning(td);
9622
9623 let (poly_implementors, poly_columns) = if self.is_polymorphic(td) {
9624 (
9625 self.find_poly_implementors(&format!("{}::{}", td.module, td.name)),
9626 Self::poly_dml_columns(td),
9627 )
9628 } else {
9629 (vec![], vec![])
9630 };
9631 let qname = format!("{}::{}", td.module, td.name);
9632 let enqueue_search = collect_search_enqueue(td, &qname, "delete");
9633
9634 Ok(IrDelete {
9635 target,
9636 filter,
9637 returning,
9638 poly_implementors,
9639 poly_columns,
9640 enqueue_search,
9641 })
9642 }
9643
9644 fn compile_shape(
9647 &mut self,
9648 elements: &[ShapeElement],
9649 td: &TypeDescriptor,
9650 alias: &str,
9651 module: &str,
9652 ) -> Result<Vec<IrShapePointer>, PyQLError> {
9653 if elements.is_empty() {
9654 return Ok(Self::pk_returning(td));
9656 }
9657
9658 let mut pointers = vec![];
9659 for el in elements {
9660 if let Some(splat) = &el.splat {
9661 if let Some(ast::PathStep::TypeIntersection(type_ref)) = el.path.steps.first() {
9663 let type_ref = type_ref.clone();
9664 pointers.extend(self.compile_type_intersection_splat(&type_ref, splat, td, alias)?);
9665 } else {
9666 pointers.extend(self.compile_splat(splat, td, alias, module)?);
9667 }
9668 } else {
9669 pointers.push(self.compile_shape_element(el, td, alias, module)?);
9670 }
9671 }
9672 self.prepend_implicit_id(&mut pointers, td);
9673 Ok(pointers)
9674 }
9675
9676 fn prepend_implicit_id(&self, pointers: &mut Vec<IrShapePointer>, td: &TypeDescriptor) {
9685 if !self.implicit_id_in_shapes {
9686 return;
9687 }
9688 let Some(pk) = td.properties.iter().find(|p| p.is_pk) else {
9689 return;
9690 };
9691 if pointers.iter().any(|p| p.alias() == pk.name) {
9692 return;
9693 }
9694 pointers.insert(
9695 0,
9696 IrShapePointer::Scalar(IrScalarPointer {
9697 implicit_id: true,
9698 marker_offset: None,
9699 alias: pk.name.clone(),
9700 column: pk.name.clone(),
9701 pg_type: pk.pg_type.clone(),
9702 tuple_shape: None,
9703 }),
9704 );
9705 }
9706
9707 fn without_implicit_id<T>(&mut self, body: impl FnOnce(&mut Self) -> Result<T, PyQLError>) -> Result<T, PyQLError> {
9710 let saved = std::mem::replace(&mut self.implicit_id_in_shapes, false);
9711 let result = body(self);
9712 self.implicit_id_in_shapes = saved;
9713 result
9714 }
9715
9716 fn compile_splat(
9718 &mut self,
9719 splat: &ast::Splat,
9720 td: &TypeDescriptor,
9721 alias: &str,
9722 module: &str,
9723 ) -> Result<Vec<IrShapePointer>, PyQLError> {
9724 let mut pointers: Vec<IrShapePointer> = td
9725 .properties
9726 .iter()
9727 .map(|p| {
9728 IrShapePointer::Scalar(IrScalarPointer {
9729 implicit_id: false,
9730 marker_offset: None,
9731 alias: p.name.clone(),
9732 column: p.name.clone(),
9733 pg_type: p.pg_type.clone(),
9734 tuple_shape: self.resolve_property_tuple_shape(p),
9735 })
9736 })
9737 .collect();
9738
9739 for cd in &td.computed.clone() {
9740 if matches!(splat, ast::Splat::Shallow) && self.computed_is_object_valued(cd, td) {
9744 continue;
9745 }
9746 pointers.push(self.compile_declared_computed(cd, td, alias, module, None, &[])?);
9747 }
9748
9749 if matches!(splat, ast::Splat::Deep) {
9750 for l in &td.links {
9751 let target_td = self.resolve_type(&l.target)?;
9752 let sub_alias = self.fresh_alias();
9753 let target_module = target_td.module.clone();
9759 let sub_shape = self.compile_splat(&ast::Splat::Shallow, target_td, &sub_alias, &target_module)?;
9760 let subquery = IrSelect::schema_bound(
9761 IrSource {
9762 poly: self.link_target_fanout(target_td),
9763 type_name: format!("{}::{}", target_td.module, target_td.name),
9764 table: target_td.table.clone(),
9765 alias: sub_alias.clone(),
9766 },
9767 sub_shape,
9768 None,
9769 );
9770 let correlation = if l.is_junction_backed() {
9771 let join = self.build_multilink_join(td, &l.name, &l.target, &l.through)?;
9772 IrSingleLinkCorrelation::Junction {
9773 join,
9774 target_pk: "id".to_string(),
9775 }
9776 } else {
9777 IrSingleLinkCorrelation::Fk {
9778 fk_column: format!("{}_id", l.name),
9779 target_pk: "id".to_string(),
9780 }
9781 };
9782 pointers.push(IrShapePointer::SingleLink(IrSingleLinkPointer {
9783 marker_offset: None,
9784 alias: l.name.clone(),
9785 correlation,
9786 subquery,
9787 link_properties: vec![],
9788 }));
9789 }
9790
9791 for ml in &td.multilinks {
9792 let sub_alias = self.fresh_alias();
9793 let target_td = self.resolve_type(&ml.target)?;
9794 let target_module = target_td.module.clone();
9797 let sub_shape = self.compile_splat(&ast::Splat::Shallow, target_td, &sub_alias, &target_module)?;
9798
9799 let join = if let Some(through_qname) = &ml.through {
9800 let through_td = self.resolve_type(through_qname)?;
9801 if through_td.junction {
9802 IrMultiLinkJoin::Standard {
9804 junction_table: self.owner_junction(td, &ml.name, Some(through_td)),
9805 module: td.module.clone(),
9806 }
9807 } else {
9808 let source_qname = format!("{}::{}", td.module, td.name);
9809 let source_col = through_td
9810 .links
9811 .iter()
9812 .find(|l| l.target == source_qname)
9813 .ok_or_else(|| {
9814 PyQLError::Type(PyQLTypeError {
9815 message: format!(
9816 "through type {through_qname} has no link to source type {source_qname}"
9817 ),
9818 position: Position { line: 0, col: 0 },
9819 })
9820 })?
9821 .name
9822 .clone();
9823 let target_col = through_td
9824 .links
9825 .iter()
9826 .find(|l| l.target == ml.target && l.name != source_col)
9827 .or_else(|| through_td.links.iter().find(|l| l.target == ml.target))
9828 .ok_or_else(|| {
9829 PyQLError::Type(PyQLTypeError {
9830 message: format!(
9831 "through type {through_qname} has no link to target type {}",
9832 ml.target
9833 ),
9834 position: Position { line: 0, col: 0 },
9835 })
9836 })?
9837 .name
9838 .clone();
9839 IrMultiLinkJoin::Through {
9840 junction_table: through_td.table.clone(),
9841 module: through_td.module.clone(),
9842 source_col,
9843 target_col,
9844 }
9845 }
9846 } else {
9847 IrMultiLinkJoin::Standard {
9851 junction_table: self.owner_junction(td, &ml.name, None),
9852 module: td.module.clone(),
9853 }
9854 };
9855
9856 let subquery = self.link_target_select(
9857 target_td,
9858 IrSource {
9859 poly: None,
9860 type_name: format!("{}::{}", target_td.module, target_td.name),
9861 table: target_td.table.clone(),
9862 alias: sub_alias.clone(),
9863 },
9864 sub_shape,
9865 );
9866
9867 let link_properties = self.splat_link_properties(ml)?;
9868 pointers.push(IrShapePointer::MultiLink(IrMultiLinkPointer {
9869 marker_offset: None,
9870 alias: ml.name.clone(),
9871 join,
9872 subquery,
9873 link_properties,
9874 single: false,
9875 }));
9876 }
9877 }
9878
9879 Ok(pointers)
9880 }
9881
9882 fn splat_link_properties(&self, ml: &MultiLinkDescriptor) -> Result<Vec<IrLinkProp>, PyQLError> {
9885 let Some(through_qname) = &ml.through else {
9886 return Ok(vec![]);
9887 };
9888 let through_td = self.resolve_type(through_qname)?;
9889 if !through_td.junction {
9890 return Ok(vec![]);
9891 }
9892 Ok(through_td
9893 .properties
9894 .iter()
9895 .filter(|p| p.name != "id")
9896 .map(|p| IrLinkProp { name: p.name.clone() })
9897 .collect())
9898 }
9899
9900 fn compile_type_intersection_splat(
9904 &mut self,
9905 type_ref: &ast::ObjectRef,
9906 splat: &ast::Splat,
9907 parent_td: &TypeDescriptor,
9908 parent_alias: &str,
9909 ) -> Result<Vec<IrShapePointer>, PyQLError> {
9910 let type_name = match &type_ref.module {
9911 Some(m) => format!("{}::{}", m, type_ref.name),
9912 None => type_ref.name.clone(),
9913 };
9914 let concrete_td = self.resolve_type(&type_name)?;
9915 let concrete_qname = format!("{}::{}", concrete_td.module, concrete_td.name);
9916 let concrete_table = concrete_td.table.clone();
9917
9918 let interface_props: std::collections::HashSet<String> =
9920 parent_td.properties.iter().map(|p| p.name.clone()).collect();
9921
9922 let props: Vec<_> = concrete_td
9924 .properties
9925 .iter()
9926 .filter(|p| !interface_props.contains(&p.name))
9927 .cloned()
9928 .collect();
9929
9930 let interface_computed: std::collections::HashSet<String> =
9934 parent_td.computed.iter().map(|c| c.name.clone()).collect();
9935 let computed: Vec<_> = concrete_td
9936 .computed
9937 .iter()
9938 .filter(|c| !interface_computed.contains(&c.name))
9939 .filter(|c| !matches!(splat, ast::Splat::Shallow) || !self.computed_is_object_valued(c, concrete_td))
9940 .cloned()
9941 .collect();
9942
9943 let links: Vec<_> = if matches!(splat, ast::Splat::Deep) {
9945 concrete_td.links.to_vec()
9946 } else {
9947 vec![]
9948 };
9949
9950 let mut pointers = vec![];
9951 for prop in props {
9952 let sub_alias = self.fresh_alias();
9953 let filter = IrExpr::BinOp(Box::new(IrBinOp {
9954 left: IrExpr::ColumnRef {
9955 alias: sub_alias.clone(),
9956 column: "id".to_string(),
9957 pg_type: "uuid".to_string(),
9958 },
9959 op: ast::BinOpKind::Eq,
9960 right: IrExpr::ColumnRef {
9961 alias: parent_alias.to_string(),
9962 column: "id".to_string(),
9963 pg_type: "uuid".to_string(),
9964 },
9965 }));
9966 let subquery = IrSelect::schema_bound(
9967 IrSource {
9968 poly: None,
9969 type_name: concrete_qname.clone(),
9970 table: concrete_table.clone(),
9971 alias: sub_alias,
9972 },
9973 vec![IrShapePointer::Scalar(IrScalarPointer {
9974 implicit_id: false,
9975 marker_offset: None,
9976 alias: prop.name.clone(),
9977 column: prop.name.clone(),
9978 pg_type: prop.pg_type.clone(),
9979 tuple_shape: self.resolve_property_tuple_shape(&prop),
9980 })],
9981 Some(filter),
9982 );
9983 pointers.push(IrShapePointer::Computed(IrComputedPointer {
9984 marker_offset: None,
9985 alias: prop.name.clone(),
9986 expr: IrExpr::Subquery(Box::new(subquery)),
9987 }));
9988 }
9989
9990 for cd in computed {
9991 let sub_alias = self.fresh_alias();
9992 let filter = IrExpr::BinOp(Box::new(IrBinOp {
9993 left: IrExpr::ColumnRef {
9994 alias: sub_alias.clone(),
9995 column: "id".to_string(),
9996 pg_type: "uuid".to_string(),
9997 },
9998 op: ast::BinOpKind::Eq,
9999 right: IrExpr::ColumnRef {
10000 alias: parent_alias.to_string(),
10001 column: "id".to_string(),
10002 pg_type: "uuid".to_string(),
10003 },
10004 }));
10005 let expr_ast = crate::parse::parse_pointer_expr(&cd.expression).map_err(PyQLError::Syntax)?;
10006 let inner_ir = self.compile_expr(&expr_ast, concrete_td, &sub_alias)?;
10007 let subquery = IrSelect::schema_bound(
10008 IrSource {
10009 poly: None,
10010 type_name: concrete_qname.clone(),
10011 table: concrete_table.clone(),
10012 alias: sub_alias,
10013 },
10014 vec![IrShapePointer::Computed(IrComputedPointer {
10015 marker_offset: None,
10016 alias: cd.name.clone(),
10017 expr: inner_ir,
10018 })],
10019 Some(filter),
10020 );
10021 pointers.push(IrShapePointer::Computed(IrComputedPointer {
10022 marker_offset: None,
10023 alias: cd.name.clone(),
10024 expr: IrExpr::Subquery(Box::new(subquery)),
10025 }));
10026 }
10027
10028 for link in links {
10030 let target_td = self.resolve_type(&link.target)?;
10031 let sub_alias = self.fresh_alias();
10032 let filter = if link.is_junction_backed() {
10033 let (jt_table, jt_module, jt_src_col, jt_tgt_col, _) =
10034 self.junction_info_for(concrete_td, &link.name, &link.target, &link.through)?;
10035 let jt_alias = self.fresh_alias();
10036 let jt_filter = IrExpr::BinOp(Box::new(IrBinOp {
10037 left: IrExpr::BinOp(Box::new(IrBinOp {
10038 left: IrExpr::ColumnRef {
10039 alias: jt_alias.clone(),
10040 column: jt_src_col,
10041 pg_type: "uuid".to_string(),
10042 },
10043 op: ast::BinOpKind::Eq,
10044 right: IrExpr::ColumnRef {
10045 alias: parent_alias.to_string(),
10046 column: "id".to_string(),
10047 pg_type: "uuid".to_string(),
10048 },
10049 })),
10050 op: ast::BinOpKind::And,
10051 right: IrExpr::BinOp(Box::new(IrBinOp {
10052 left: IrExpr::ColumnRef {
10053 alias: jt_alias.clone(),
10054 column: jt_tgt_col,
10055 pg_type: "uuid".to_string(),
10056 },
10057 op: ast::BinOpKind::Eq,
10058 right: IrExpr::ColumnRef {
10059 alias: sub_alias.clone(),
10060 column: "id".to_string(),
10061 pg_type: "uuid".to_string(),
10062 },
10063 })),
10064 }));
10065 let exists_select = IrSelect::schema_bound(
10066 IrSource {
10067 poly: None,
10068 type_name: format!("{}::__jt__", jt_module),
10069 table: jt_table,
10070 alias: jt_alias,
10071 },
10072 vec![],
10073 Some(jt_filter),
10074 );
10075 IrExpr::UnaryOp(Box::new(IrUnaryOp {
10076 op: ast::UnaryOpKind::Exists,
10077 operand: IrExpr::Subquery(Box::new(exists_select)),
10078 }))
10079 } else {
10080 IrExpr::BinOp(Box::new(IrBinOp {
10081 left: IrExpr::ColumnRef {
10082 alias: sub_alias.clone(),
10083 column: "id".to_string(),
10084 pg_type: "uuid".to_string(),
10085 },
10086 op: ast::BinOpKind::Eq,
10087 right: IrExpr::ColumnRef {
10088 alias: parent_alias.to_string(),
10089 column: format!("{}_id", link.name),
10090 pg_type: "uuid".to_string(),
10091 },
10092 }))
10093 };
10094 let sub_shape = Self::pk_returning(target_td);
10095 let subquery = IrSelect::schema_bound(
10096 IrSource {
10097 poly: self.link_target_fanout(target_td),
10098 type_name: format!("{}::{}", target_td.module, target_td.name),
10099 table: target_td.table.clone(),
10100 alias: sub_alias,
10101 },
10102 sub_shape,
10103 Some(filter),
10104 );
10105 pointers.push(IrShapePointer::Computed(IrComputedPointer {
10106 marker_offset: None,
10107 alias: link.name.clone(),
10108 expr: IrExpr::Subquery(Box::new(subquery)),
10109 }));
10110 }
10111
10112 Ok(pointers)
10113 }
10114
10115 fn compile_type_intersection_pointer(
10117 &mut self,
10118 type_ref: &ast::ObjectRef,
10119 tail_steps: &[ast::PathStep],
10120 parent_alias: &str,
10121 marker_offset: Option<usize>,
10122 ) -> Result<IrShapePointer, PyQLError> {
10123 let expr = self.compile_type_intersection_expr_steps(type_ref, tail_steps, parent_alias)?;
10124 let alias = match tail_steps.last() {
10126 Some(ast::PathStep::Name(n)) => n.clone(),
10127 _ => return Err(self.type_err("type intersection must end with a pointer name")),
10128 };
10129 Ok(IrShapePointer::Computed(IrComputedPointer {
10130 alias,
10131 expr,
10132 marker_offset,
10133 }))
10134 }
10135
10136 fn compile_type_intersection_expr(
10138 &mut self,
10139 steps: &[ast::PathStep],
10140 td: &TypeDescriptor,
10141 parent_alias: &str,
10142 ) -> Result<IrExpr, PyQLError> {
10143 use ast::PathStep;
10144 let type_ref = match steps.first() {
10145 Some(PathStep::TypeIntersection(tr)) => tr.clone(),
10146 _ => return Err(self.type_err("expected type intersection")),
10147 };
10148 match self.compile_type_intersection_expr_steps(&type_ref, &steps[1..], parent_alias) {
10153 Ok(ir) => Ok(ir),
10154 Err(fast_path_err) => {
10155 let p = ast::Path {
10156 steps: steps.to_vec(),
10157 partial: true,
10158 };
10159 self.compile_partial_path_as_subquery(&p, td, parent_alias)
10160 .map_err(|_| fast_path_err)
10161 }
10162 }
10163 }
10164
10165 fn compile_type_intersection_expr_steps(
10167 &mut self,
10168 type_ref: &ast::ObjectRef,
10169 tail_steps: &[ast::PathStep],
10170 parent_alias: &str,
10171 ) -> Result<IrExpr, PyQLError> {
10172 use ast::PathStep;
10173 let type_name = match &type_ref.module {
10174 Some(m) => format!("{}::{}", m, type_ref.name),
10175 None => type_ref.name.clone(),
10176 };
10177 let concrete_td = self.resolve_type(&type_name)?;
10178 let concrete_qname = format!("{}::{}", concrete_td.module, concrete_td.name);
10179 let concrete_table = concrete_td.table.clone();
10180
10181 let pointer_name = match tail_steps.first() {
10182 Some(PathStep::Name(n)) => n.as_str(),
10183 _ => return Err(self.type_err("type intersection must be followed by a pointer name, e.g. [is Type].name")),
10184 };
10185
10186 let prop = concrete_td
10187 .properties
10188 .iter()
10189 .find(|p| p.name == pointer_name)
10190 .ok_or_else(|| self.field_err(pointer_name, &concrete_qname))?;
10191 let prop_name = prop.name.clone();
10192 let prop_type = prop.pg_type.clone();
10193
10194 let sub_alias = self.fresh_alias();
10195 let filter = IrExpr::BinOp(Box::new(IrBinOp {
10196 left: IrExpr::ColumnRef {
10197 alias: sub_alias.clone(),
10198 column: "id".to_string(),
10199 pg_type: "uuid".to_string(),
10200 },
10201 op: ast::BinOpKind::Eq,
10202 right: IrExpr::ColumnRef {
10203 alias: parent_alias.to_string(),
10204 column: "id".to_string(),
10205 pg_type: "uuid".to_string(),
10206 },
10207 }));
10208
10209 let poly = self.poly_fanout_for(&concrete_qname);
10210 Ok(IrExpr::Subquery(Box::new(IrSelect::schema_bound(
10211 IrSource {
10212 poly,
10213 type_name: concrete_qname,
10214 table: concrete_table,
10215 alias: sub_alias,
10216 },
10217 vec![IrShapePointer::Scalar(IrScalarPointer {
10218 implicit_id: false,
10219 marker_offset: None,
10220 alias: prop_name.clone(),
10221 column: prop_name,
10222 pg_type: prop_type,
10223 tuple_shape: self.resolve_property_tuple_shape(prop),
10224 })],
10225 Some(filter),
10226 ))))
10227 }
10228
10229 fn compile_shape_element(
10230 &mut self,
10231 el: &ShapeElement,
10232 td: &TypeDescriptor,
10233 alias: &str,
10234 module: &str,
10235 ) -> Result<IrShapePointer, PyQLError> {
10236 if let Some(ast::PathStep::TypeIntersection(type_ref)) = el.path.steps.first()
10238 && el.compexpr.is_none()
10239 && el.path.steps.len() >= 2
10240 {
10241 if let Some(nested) = el.nested.as_deref()
10245 && !nested.is_empty()
10246 && let Some(ast::PathStep::Name(leaf)) = el.path.steps.last()
10247 && let Some(ptr) = self.try_compile_pointer_expr(
10248 &leaf.clone(),
10249 &Expr::Path(el.path.clone()),
10250 td,
10251 alias,
10252 module,
10253 el.marker_offset,
10254 nested,
10255 )?
10256 {
10257 return Ok(ptr);
10258 }
10259 let type_ref = type_ref.clone();
10260 return self.compile_type_intersection_pointer(&type_ref, &el.path.steps[1..], alias, el.marker_offset);
10261 }
10262
10263 let pointer_name = path_leaf(&el.path)?;
10264
10265 if pointer_name == "__type__" && el.compexpr.is_none() {
10269 let expr = if self.is_polymorphic(td) {
10270 IrExpr::ColumnRef {
10271 alias: alias.to_string(),
10272 column: "__type__".to_string(),
10273 pg_type: "text".to_string(),
10274 }
10275 } else {
10276 IrExpr::Literal(IrLiteral::Str(format!("{}::{}", td.module, td.name)))
10277 };
10278 return Ok(IrShapePointer::Computed(IrComputedPointer {
10279 marker_offset: el.marker_offset,
10280 alias: "__type__".to_string(),
10281 expr,
10282 }));
10283 }
10284
10285 if let Some(compexpr) = &el.compexpr
10292 && let Some((call, value_expr, check_expr)) =
10293 Self::asserted_pointer_expr(compexpr, el.nested.as_deref().unwrap_or(&[]))
10294 {
10295 let element_for = |expr: Expr| ShapeElement {
10296 compexpr: Some(expr),
10297 nested: None,
10298 ..el.clone()
10299 };
10300 if let Ok(ptr) = self.compile_shape_element(&element_for(value_expr), td, alias, module)
10305 && ptr.is_object_pointer()
10306 {
10307 let check = match check_expr {
10308 Some(expr) => Some(self.compile_shape_element(&element_for(expr), td, alias, module)?),
10309 None => None,
10310 };
10311 let message = self.assert_message(&call, Some((td, alias)))?;
10312 return Ok(IrShapePointer::Asserted(Box::new(IrAssertedPointer {
10313 fn_name: call.name,
10314 inner: ptr,
10315 check,
10316 message,
10317 })));
10318 }
10319 }
10320
10321 if let Some(compexpr) = &el.compexpr
10325 && let Some(guarded) = guarded_object_branch(compexpr)
10326 {
10327 let element = ShapeElement {
10328 compexpr: Some(guarded),
10329 ..el.clone()
10330 };
10331 return self.compile_shape_element(&element, td, alias, module);
10332 }
10333
10334 if let Some(compexpr) = &el.compexpr {
10336 let inner_declared: Vec<ShapeElement> = Self::replaced_subject_shape(compexpr)
10344 .iter()
10345 .filter(|e| e.compexpr.is_some())
10346 .cloned()
10347 .collect();
10348 let restore = (!inner_declared.is_empty()).then(|| {
10349 let mut scope = self.active_declared_pointers.clone();
10350 scope.extend(inner_declared);
10351 std::mem::replace(&mut self.active_declared_pointers, scope)
10352 });
10353 let pointer = self.try_compile_pointer_expr(
10354 pointer_name,
10355 compexpr,
10356 td,
10357 alias,
10358 module,
10359 el.marker_offset,
10360 el.nested.as_deref().unwrap_or(&[]),
10361 );
10362 if let Some(previous) = restore {
10363 self.active_declared_pointers = previous;
10364 }
10365 if let Some(ptr) = pointer? {
10366 return Ok(ptr);
10367 }
10368 if let Some((fc, call_nested)) = Self::object_call_with_shape(compexpr, el.nested.as_deref())
10373 && let Some(fs) = self.compile_fn_object_source(fc, &call_nested)?
10374 {
10375 return Ok(IrShapePointer::Computed(IrComputedPointer {
10376 marker_offset: el.marker_offset,
10377 alias: pointer_name.to_string(),
10378 expr: IrExpr::ArrayFromSelect(Box::new(IrArraySource::ObjectFunction(Box::new(fs)))),
10379 }));
10380 }
10381 let ir = match compexpr {
10385 Expr::TypeIs { expr, ty } => self.compile_type_is_set(expr, ty, Some((td, alias)))?,
10386 _ => self.in_value_position(|c| c.compile_expr(compexpr, td, alias))?,
10387 };
10388 if let IrExpr::ArrayFromSelect(src) = ir {
10390 if let IrArraySource::RawExpr {
10391 source,
10392 poly_implementors,
10393 poly_columns,
10394 expr,
10395 } = *src
10396 {
10397 return Ok(IrShapePointer::ScalarSet(IrScalarSetPointer {
10398 alias: pointer_name.to_string(),
10399 source,
10400 poly_implementors,
10401 poly_columns,
10402 bool_expr: expr,
10403 }));
10404 }
10405 return Ok(IrShapePointer::Computed(IrComputedPointer {
10406 marker_offset: el.marker_offset,
10407 alias: pointer_name.to_string(),
10408 expr: IrExpr::ArrayFromSelect(src),
10409 }));
10410 }
10411 return Ok(IrShapePointer::Computed(IrComputedPointer {
10412 marker_offset: el.marker_offset,
10413 alias: pointer_name.to_string(),
10414 expr: ir,
10415 }));
10416 }
10417
10418 if let Some(p) = Self::resolve_property(td, pointer_name) {
10420 return Ok(IrShapePointer::Scalar(IrScalarPointer {
10421 implicit_id: false,
10422 marker_offset: el.marker_offset,
10423 alias: pointer_name.to_string(),
10424 column: p.name.clone(),
10425 pg_type: p.pg_type.clone(),
10426 tuple_shape: self.resolve_property_tuple_shape(p),
10427 }));
10428 }
10429
10430 if let Some(l) = Self::resolve_link(td, pointer_name) {
10432 let target_td = self.resolve_type(&l.target)?;
10433 let sub_alias = self.fresh_alias();
10434 let nested_elements = el.nested.as_deref().unwrap_or(&[]);
10435
10436 let (regular_els, link_properties): (Vec<ShapeElement>, Vec<IrLinkProp>) = if l.is_junction_backed() {
10441 let mut regular = Vec::new();
10442 let mut props = Vec::new();
10443 for nel in nested_elements {
10444 if let [ast::PathStep::LinkProp(name)] = nel.path.steps.as_slice() {
10445 props.push(IrLinkProp { name: name.clone() });
10446 } else {
10447 regular.push(nel.clone());
10448 }
10449 }
10450 (regular, props)
10451 } else {
10452 (nested_elements.to_vec(), vec![])
10453 };
10454
10455 let sub_shape = self.compile_shape(®ular_els, target_td, &sub_alias, &target_td.module.clone())?;
10456 let subquery = self.link_target_select(
10457 target_td,
10458 IrSource {
10459 poly: None,
10460 type_name: format!("{}::{}", target_td.module, target_td.name),
10461 table: target_td.table.clone(),
10462 alias: sub_alias,
10463 },
10464 sub_shape,
10465 );
10466 let correlation = if l.is_junction_backed() {
10467 let join = self.build_multilink_join(td, &l.name, &l.target, &l.through)?;
10468 IrSingleLinkCorrelation::Junction {
10469 join,
10470 target_pk: "id".to_string(),
10471 }
10472 } else {
10473 IrSingleLinkCorrelation::Fk {
10474 fk_column: format!("{}_id", l.name),
10475 target_pk: "id".to_string(),
10476 }
10477 };
10478 return Ok(IrShapePointer::SingleLink(IrSingleLinkPointer {
10479 marker_offset: el.marker_offset,
10480 alias: pointer_name.to_string(),
10481 correlation,
10482 subquery,
10483 link_properties,
10484 }));
10485 }
10486
10487 if Self::resolve_multilink(td, pointer_name).is_some() {
10489 return self.compile_multilink_pointer(pointer_name, pointer_name, td, alias, module, el);
10490 }
10491
10492 if let Some(cd) = self.resolve_computed(td, pointer_name) {
10494 return self.compile_declared_computed(
10495 &cd,
10496 td,
10497 alias,
10498 module,
10499 el.marker_offset,
10500 el.nested.as_deref().unwrap_or(&[]),
10501 );
10502 }
10503
10504 if let Some(declared) = self
10507 .active_declared_pointers
10508 .iter()
10509 .find(|d| path_leaf(&d.path).is_ok_and(|n| n == pointer_name))
10510 .cloned()
10511 && let Some(expr) = declared.compexpr.clone()
10512 {
10513 let nested = if el.nested.as_deref().unwrap_or(&[]).is_empty() {
10514 declared.nested.clone().unwrap_or_default()
10515 } else {
10516 el.nested.clone().unwrap_or_default()
10517 };
10518 return self.compile_computed_expr(pointer_name, &expr, td, alias, module, el.marker_offset, &nested, None);
10519 }
10520
10521 Err(self.field_err(pointer_name, &format!("{}::{}", td.module, td.name)))
10522 }
10523
10524 #[allow(clippy::too_many_arguments)]
10531 fn compile_chained_link_pointer(
10532 &mut self,
10533 pointer_name: &str,
10534 path: &ast::Path,
10535 td: &TypeDescriptor,
10536 alias: &str,
10537 nested: &[ShapeElement],
10538 modifiers: Option<&ast::SelectStmt>,
10539 multi: bool,
10540 tail: usize,
10541 ) -> Result<IrShapePointer, PyQLError> {
10542 let mut steps = vec![ast::PathStep::Name(format!("{}::{}", td.module, td.name))];
10543 steps.extend(path.steps.iter().cloned());
10544 let full_path = ast::Path { steps, partial: false };
10545 let synthetic = ast::SelectStmt {
10546 result: Expr::Path(full_path.clone()),
10547 filter: modifiers.and_then(|m| m.filter.clone()),
10548 order_by: modifiers.map(|m| m.order_by.clone()).unwrap_or_default(),
10549 offset: modifiers.and_then(|m| m.offset.clone()),
10550 limit: modifiers.and_then(|m| m.limit.clone()),
10551 lock: None,
10552 };
10553 let mut path_select = self.compile_path_select_with_tail(&synthetic, &full_path, nested, false, tail, &[])?;
10554 Self::correlate_path_select(&mut path_select, alias);
10555 let expr = if multi {
10559 IrExpr::ArrayFromSelect(Box::new(IrArraySource::PathSelect(Box::new(path_select))))
10560 } else {
10561 IrExpr::ObjectPathSubquery(Box::new(path_select))
10562 };
10563 Ok(IrShapePointer::Computed(IrComputedPointer {
10564 marker_offset: None,
10565 alias: pointer_name.to_string(),
10566 expr,
10567 }))
10568 }
10569
10570 fn compile_multilink_pointer(
10571 &mut self,
10572 output_alias: &str,
10573 ml_name: &str,
10574 td: &TypeDescriptor,
10575 _parent_alias: &str,
10576 module: &str,
10577 el: &ShapeElement,
10578 ) -> Result<IrShapePointer, PyQLError> {
10579 let ml = Self::resolve_multilink(td, ml_name)
10580 .expect("caller verified multilink exists")
10581 .clone();
10582 let target_td = self.resolve_type(&ml.target)?;
10583 let sub_alias = self.fresh_alias();
10584 let nested_elements = el.nested.as_deref().unwrap_or(&[]);
10585
10586 let mut link_properties: Vec<IrLinkProp> = Vec::new();
10588 let mut regular_els: Vec<ShapeElement> = Vec::new();
10589 for nel in nested_elements {
10590 if let [ast::PathStep::LinkProp(name)] = nel.path.steps.as_slice() {
10591 link_properties.push(IrLinkProp { name: name.clone() });
10592 } else {
10593 regular_els.push(nel.clone());
10594 }
10595 }
10596 let has_splat = nested_elements.iter().any(|nel| {
10597 nel.splat.is_some() && !matches!(nel.path.steps.first(), Some(ast::PathStep::TypeIntersection(_)))
10598 });
10599 if has_splat {
10600 for prop in self.splat_link_properties(&ml)? {
10601 if !link_properties.iter().any(|existing| existing.name == prop.name) {
10602 link_properties.push(prop);
10603 }
10604 }
10605 }
10606
10607 let sub_shape = self.compile_shape(®ular_els, target_td, &sub_alias, &target_td.module.clone())?;
10608
10609 let join = if let Some(through_qname) = &ml.through {
10610 let through_td = self.resolve_type(through_qname)?;
10611 if through_td.junction {
10612 IrMultiLinkJoin::Standard {
10616 junction_table: self.owner_junction(td, ml_name, Some(through_td)),
10617 module: td.module.clone(),
10618 }
10619 } else {
10620 let source_qname = format!("{}::{}", td.module, td.name);
10621 let source_col = through_td
10622 .links
10623 .iter()
10624 .find(|l| l.target == source_qname)
10625 .ok_or_else(|| {
10626 PyQLError::Type(PyQLTypeError {
10627 message: format!("through type {through_qname} has no link to source type {source_qname}"),
10628 position: Position { line: 0, col: 0 },
10629 })
10630 })?
10631 .name
10632 .clone();
10633 let target_col = through_td
10634 .links
10635 .iter()
10636 .find(|l| l.target == ml.target && l.name != source_col)
10637 .or_else(|| through_td.links.iter().find(|l| l.target == ml.target))
10638 .ok_or_else(|| {
10639 PyQLError::Type(PyQLTypeError {
10640 message: format!("through type {through_qname} has no link to target type {}", ml.target),
10641 position: Position { line: 0, col: 0 },
10642 })
10643 })?
10644 .name
10645 .clone();
10646 IrMultiLinkJoin::Through {
10647 junction_table: through_td.table.clone(),
10648 module: through_td.module.clone(),
10649 source_col,
10650 target_col,
10651 }
10652 }
10653 } else {
10654 IrMultiLinkJoin::Standard {
10655 junction_table: self.owner_junction(td, ml_name, None),
10656 module: module.to_string(),
10657 }
10658 };
10659
10660 self.link_prop_scope
10663 .push(ml.through.clone().map(|t| (t, "jt".to_string())));
10664 let modifiers = (|c: &mut Self| -> Result<SelectModifiers, PyQLError> {
10665 Ok((
10666 el.filter
10667 .as_ref()
10668 .map(|f| c.compile_expr(f, target_td, &sub_alias))
10669 .transpose()?,
10670 el.order_by
10671 .iter()
10672 .map(|s| c.compile_sort(s, target_td, &sub_alias))
10673 .collect::<Result<_, _>>()?,
10674 el.offset
10675 .as_ref()
10676 .map(|e| c.compile_expr(e, target_td, &sub_alias))
10677 .transpose()?,
10678 el.limit
10679 .as_ref()
10680 .map(|e| c.compile_expr(e, target_td, &sub_alias))
10681 .transpose()?,
10682 ))
10683 })(self);
10684 self.link_prop_scope.pop();
10685 let (filter, order_by, offset, limit) = modifiers?;
10686 let subquery = IrSelect {
10687 rows: vec![IrRowSource::Bound {
10688 source: IrSource {
10689 poly: self.link_target_fanout(target_td),
10690 type_name: format!("{}::{}", target_td.module, target_td.name),
10691 table: target_td.table.clone(),
10692 alias: sub_alias.clone(),
10693 },
10694 shape: sub_shape,
10695 }],
10696 filter,
10697 order_by,
10698 offset,
10699 limit,
10700 distinct: false,
10701 dml_source: None,
10702 polymorphic: false,
10703 poly_implementors: vec![],
10704 poly_columns: vec![],
10705 lock: None,
10706 };
10707
10708 Ok(IrShapePointer::MultiLink(IrMultiLinkPointer {
10709 marker_offset: el.marker_offset,
10710 alias: output_alias.to_string(),
10711 join,
10712 subquery,
10713 link_properties,
10714 single: false,
10715 }))
10716 }
10717
10718 fn compile_backlink_pointer(
10728 &mut self,
10729 output_alias: &str,
10730 path: &ast::Path,
10731 current_qname: &str,
10732 nested_elements: &[ShapeElement],
10733 marker_offset: Option<usize>,
10734 modifiers: Option<&ast::SelectStmt>,
10735 ) -> Result<IrShapePointer, PyQLError> {
10736 use ast::PathStep;
10737
10738 let backlink_name = match path.steps.first() {
10739 Some(PathStep::Backlink(n)) => n.clone(),
10740 _ => return Err(self.type_err("internal: expected backlink step")),
10741 };
10742 let type_ref = match path.steps.get(1) {
10743 Some(PathStep::TypeIntersection(tr)) => tr,
10744 _ => {
10745 return Err(PyQLError::Type(PyQLTypeError {
10746 message: format!(
10747 "backlink '.< {backlink_name}' requires a type intersection, \
10748 e.g.: .< {backlink_name}[is SomeType]"
10749 ),
10750 position: Position { line: 0, col: 0 },
10751 }));
10752 }
10753 };
10754 if path.steps.len() > 2 {
10755 return Err(self.type_err(
10756 "further path traversal after a backlink shape is not yet supported \
10757 (e.g. '.<link[is Type].property') — attach a nested shape instead: \
10758 '.<link[is Type] { property }'",
10759 ));
10760 }
10761
10762 let type_name = match &type_ref.module {
10763 Some(m) => format!("{}::{}", m, type_ref.name),
10764 None => type_ref.name.clone(),
10765 };
10766 let owner_td = self.backlink_owner(self.resolve_type(&type_name)?, &backlink_name, current_qname)?;
10767 let owner_qname = format!("{}::{}", owner_td.module, owner_td.name);
10768
10769 let join = if let Some(l) = owner_td
10770 .links
10771 .iter()
10772 .find(|l| l.name == backlink_name && self.link_target_reaches(&l.target, current_qname))
10773 {
10774 if l.is_junction_backed() {
10775 let (junction_table, module, owner_col, current_col, _) = self.link_junction_info(owner_td, l)?;
10776 IrMultiLinkJoin::BacklinkJunction {
10777 junction_table,
10778 module,
10779 owner_col,
10780 current_col,
10781 }
10782 } else {
10783 IrMultiLinkJoin::BacklinkFk {
10784 fk_col: format!("{}_id", backlink_name),
10785 }
10786 }
10787 } else if let Some(ml) = owner_td
10788 .multilinks
10789 .iter()
10790 .find(|ml| ml.name == backlink_name && self.link_target_reaches(&ml.target, current_qname))
10791 .cloned()
10792 {
10793 let (junction_table, module, _, _, _) = self.multilink_junction_info(owner_td, &ml)?;
10794 IrMultiLinkJoin::BacklinkJunction {
10795 junction_table,
10796 module,
10797 owner_col: "source".to_string(),
10798 current_col: "target".to_string(),
10799 }
10800 } else {
10801 return Err(PyQLError::Type(PyQLTypeError {
10802 message: format!(
10803 "type {} has no link or multi-link '{}' pointing to {}",
10804 owner_qname, backlink_name, current_qname,
10805 ),
10806 position: Position { line: 0, col: 0 },
10807 }));
10808 };
10809
10810 let sub_alias = self.fresh_alias();
10811 let sub_shape = self.compile_shape(nested_elements, owner_td, &sub_alias, &owner_td.module.clone())?;
10812
10813 let (filter, order_by, offset, limit) = match modifiers {
10822 Some(sel) => self.compile_path_modifiers(sel, owner_td, &sub_alias)?,
10823 None => (None, vec![], None, None),
10824 };
10825 let subquery = IrSelect {
10826 rows: vec![IrRowSource::Bound {
10827 source: IrSource {
10828 poly: self.poly_fanout_for(&owner_qname),
10829 type_name: owner_qname,
10830 table: owner_td.table.clone(),
10831 alias: sub_alias.clone(),
10832 },
10833 shape: sub_shape,
10834 }],
10835 filter,
10836 order_by,
10837 offset,
10838 limit,
10839 distinct: false,
10840 dml_source: None,
10841 polymorphic: false,
10842 poly_implementors: vec![],
10843 poly_columns: vec![],
10844 lock: None,
10845 };
10846
10847 Ok(IrShapePointer::MultiLink(IrMultiLinkPointer {
10848 alias: output_alias.to_string(),
10849 join,
10850 subquery,
10851 link_properties: vec![],
10852 marker_offset,
10853 single: self.backlink_is_single(owner_td, &backlink_name, current_qname) || limits_to_one(modifiers),
10854 }))
10855 }
10856
10857 fn compile_fn_object_source(
10865 &mut self,
10866 fc: &ast::FunctionCall,
10867 nested: &[ShapeElement],
10868 ) -> Result<Option<IrFunctionSelect>, PyQLError> {
10869 let Some(fd) = self.schema.functions.iter().find(|f| {
10870 let module_matches = fc.module.as_deref().map(|m| m == f.module.as_str()).unwrap_or(true);
10871 module_matches && f.name == fc.name && f.return_is_object
10872 }) else {
10873 return Ok(None);
10874 };
10875 let (fn_module, fn_name, return_type_name) = (fd.module.clone(), fd.name.clone(), fd.return_pg_type.clone());
10876 let return_td = self.resolve_type(&return_type_name)?;
10877 let alias = self.fresh_alias();
10878 let shape = if nested.is_empty() {
10879 Self::pk_returning(return_td)
10880 } else {
10881 self.compile_shape(nested, return_td, &alias, &return_td.module)?
10882 };
10883 let mut args = fc
10884 .args
10885 .iter()
10886 .map(|a| self.compile_free_expr(a))
10887 .collect::<Result<Vec<_>, _>>()?;
10888 if let Some(globals) = self.globals_arg_for_call(&format!("{fn_module}::{fn_name}"))? {
10891 args.insert(0, globals);
10892 }
10893 Ok(Some(IrFunctionSelect {
10894 fn_module,
10895 fn_name,
10896 fn_args: args,
10897 alias,
10898 type_name: format!("{}::{}", return_td.module, return_td.name),
10899 polymorphic: false,
10900 poly_implementors: vec![],
10901 poly_columns: vec![],
10902 shape,
10903 filter: None,
10904 order_by: vec![],
10905 offset: None,
10906 limit: None,
10907 distinct: false,
10908 }))
10909 }
10910
10911 fn asserted_pointer_expr(expr: &Expr, nested: &[ShapeElement]) -> Option<(ast::FunctionCall, Expr, Option<Expr>)> {
10919 if let Expr::Shape(sh) = expr
10927 && let Some(subquery @ Expr::SubQuery(_)) = sh.expr.as_ref()
10928 {
10929 return Self::asserted_pointer_expr(subquery, &sh.elements);
10930 }
10931 if let Expr::SubQuery(stmt) = expr
10932 && let Stmt::Select(sel) = stmt.as_ref()
10933 {
10934 let (call, inner, _) = Self::asserted_pointer_expr(&sel.result, nested)?;
10935 let rebuild = |offset: Option<Expr>, limit: Option<Expr>| {
10936 Expr::SubQuery(Box::new(Stmt::Select(ast::SelectStmt {
10937 result: inner.clone(),
10938 filter: sel.filter.clone(),
10939 order_by: sel.order_by.clone(),
10940 offset,
10941 limit,
10942 lock: sel.lock.clone(),
10943 })))
10944 };
10945 let value = rebuild(sel.offset.clone(), sel.limit.clone());
10946 let check = (sel.offset.is_some() || sel.limit.is_some()).then(|| rebuild(None, None));
10947 return Some((call, value, check));
10948 }
10949 let (call, nested) = match expr {
10953 Expr::Shape(sh) => match sh.expr.as_ref() {
10954 Some(Expr::FunctionCall(f)) => (f, sh.elements.clone()),
10955 _ => return None,
10956 },
10957 Expr::FunctionCall(f) => (f, nested.to_vec()),
10958 _ => return None,
10959 };
10960 if call.module.is_some() && call.module.as_deref() != Some("std") {
10961 return None;
10962 }
10963 if !matches!(call.name.as_str(), "assert_exists" | "assert_distinct") {
10964 return None;
10965 }
10966 let [arg] = call.args.as_slice() else {
10967 return None;
10968 };
10969 let inner = match (&arg, nested.is_empty()) {
10974 (Expr::Shape(_), _) | (_, true) => arg.clone(),
10975 _ => Expr::Shape(Box::new(ast::ShapeExpr {
10976 expr: Some(arg.clone()),
10977 elements: nested.clone(),
10978 marker_offset: None,
10979 })),
10980 };
10981 Some((call.clone(), inner, None))
10982 }
10983
10984 fn object_call_with_shape<'e>(
10988 expr: &'e Expr,
10989 nested: Option<&[ShapeElement]>,
10990 ) -> Option<(&'e ast::FunctionCall, Vec<ShapeElement>)> {
10991 match expr {
10992 Expr::Shape(sh) => match sh.expr.as_ref() {
10993 Some(Expr::FunctionCall(f)) => Some((f, sh.elements.clone())),
10994 _ => None,
10995 },
10996 Expr::FunctionCall(f) => Some((f, nested.unwrap_or(&[]).to_vec())),
10997 _ => None,
10998 }
10999 }
11000
11001 fn compile_declared_computed(
11002 &mut self,
11003 cd: &crate::schema::ComputedDescriptor,
11004 td: &TypeDescriptor,
11005 alias: &str,
11006 module: &str,
11007 marker_offset: Option<usize>,
11008 nested: &[ShapeElement],
11009 ) -> Result<IrShapePointer, PyQLError> {
11010 let expr_ast = crate::parse::parse_pointer_expr(&cd.expression).map_err(PyQLError::Syntax)?;
11011 let declared_on = self.computed_declared_on(td, &cd.name);
11012 self.compile_computed_expr(
11013 &cd.name,
11014 &expr_ast,
11015 td,
11016 alias,
11017 module,
11018 marker_offset,
11019 nested,
11020 declared_on,
11021 )
11022 }
11023
11024 fn computed_declared_on(&self, td: &TypeDescriptor, name: &str) -> Option<(String, String)> {
11027 td.bases
11028 .iter()
11029 .chain(td.parents.iter())
11030 .chain(td.interfaces.iter())
11031 .filter_map(|qualified| self.resolve_type(qualified).ok())
11032 .find(|ancestor| ancestor.computed.iter().any(|c| c.name == name))
11033 .map(|ancestor| (ancestor.name.clone(), format!("{}::{}", ancestor.module, ancestor.name)))
11034 }
11035
11036 #[allow(clippy::too_many_arguments)]
11039 fn compile_computed_expr(
11040 &mut self,
11041 name: &str,
11042 expr_ast: &Expr,
11043 td: &TypeDescriptor,
11044 alias: &str,
11045 module: &str,
11046 marker_offset: Option<usize>,
11047 nested: &[ShapeElement],
11048 declared_on: Option<(String, String)>,
11049 ) -> Result<IrShapePointer, PyQLError> {
11050 self.anchors.push(SelectAnchor {
11054 type_name: td.name.clone(),
11055 qualified: format!("{}::{}", td.module, td.name),
11056 alias: alias.to_string(),
11057 detached: false,
11058 declared_on,
11059 });
11060 let result = (|compiler: &mut Self| -> Result<IrShapePointer, PyQLError> {
11061 if let Some(ptr) =
11062 compiler.try_compile_pointer_expr(name, expr_ast, td, alias, module, marker_offset, nested)?
11063 {
11064 return Ok(ptr);
11065 }
11066 if let Expr::FunctionCall(fc) = expr_ast
11071 && let Some(fs) = compiler.compile_fn_object_source(fc, nested)?
11072 {
11073 return Ok(IrShapePointer::Computed(IrComputedPointer {
11074 marker_offset,
11075 alias: name.to_string(),
11076 expr: IrExpr::ArrayFromSelect(Box::new(IrArraySource::ObjectFunction(Box::new(fs)))),
11077 }));
11078 }
11079 let ir = compiler.in_value_position(|c| c.compile_expr(expr_ast, td, alias))?;
11080 Ok(IrShapePointer::Computed(IrComputedPointer {
11081 marker_offset,
11082 alias: name.to_string(),
11083 expr: ir,
11084 }))
11085 })(self);
11086 self.anchors.pop();
11087 result
11088 }
11089
11090 fn split_path_result(result: &Expr) -> Option<(&ast::Path, &[ShapeElement])> {
11094 match result {
11095 Expr::Path(p) => Some((p, &[])),
11096 Expr::Detached(inner) => Self::split_path_result(inner),
11100 Expr::Shape(sh) => match &sh.expr {
11101 Some(Expr::Path(p)) => Some((p, sh.elements.as_slice())),
11102 _ => None,
11103 },
11104 _ => None,
11105 }
11106 }
11107
11108 fn function_subject(e: &Expr) -> Option<(&ast::FunctionCall, Option<&ast::SelectStmt>)> {
11112 match e {
11113 Expr::FunctionCall(fc) => Some((fc, None)),
11114 Expr::SubQuery(stmt) => match stmt.as_ref() {
11115 Stmt::Select(sel) => match &sel.result {
11116 Expr::FunctionCall(fc) => Some((fc, Some(sel))),
11117 _ => None,
11118 },
11119 _ => None,
11120 },
11121 _ => None,
11122 }
11123 }
11124
11125 fn resolve_object_fn(&self, fc: &ast::FunctionCall) -> Option<&'a FunctionDescriptor> {
11127 self.schema.functions.iter().find(|f| {
11128 let module_matches = fc.module.as_deref().map(|m| m == f.module.as_str()).unwrap_or(true);
11129 module_matches && f.name == fc.name && f.return_is_object
11130 })
11131 }
11132
11133 fn replaced_subject_shape(e: &Expr) -> &[ShapeElement] {
11138 let Expr::Shape(sh) = e else { return &[] };
11139 if sh.elements.is_empty() {
11140 return &[];
11141 }
11142 match sh.expr.as_ref() {
11143 Some(inner) => Self::pointer_subject(inner).map(|(_, nested, _)| nested).unwrap_or(&[]),
11144 None => &[],
11145 }
11146 }
11147
11148 fn pointer_subject(e: &Expr) -> Option<(&ast::Path, &[ShapeElement], Option<&ast::SelectStmt>)> {
11153 match e {
11154 Expr::Path(p) => Some((p, &[], None)),
11155 Expr::SubQuery(stmt) => match stmt.as_ref() {
11156 Stmt::Select(sel) => {
11157 let (p, inner) = Self::split_path_result(&sel.result)?;
11158 Some((p, inner, Some(sel)))
11159 }
11160 _ => None,
11161 },
11162 Expr::Shape(sh) => {
11163 let (path, inner, modifiers) = Self::pointer_subject(sh.expr.as_ref()?)?;
11164 let nested = if sh.elements.is_empty() {
11165 inner
11166 } else {
11167 sh.elements.as_slice()
11168 };
11169 Some((path, nested, modifiers))
11170 }
11171 _ => None,
11172 }
11173 }
11174
11175 fn field_access_over_select(expr: &Expr) -> Option<(ast::SelectStmt, usize)> {
11181 let (root, fields) = Self::peel_field_access_chain(expr);
11182 if fields.is_empty() {
11183 return None;
11184 }
11185 let Expr::SubQuery(stmt) = root else { return None };
11186 let Stmt::Select(sel) = stmt.as_ref() else {
11187 return None;
11188 };
11189 let Expr::Path(path) = &sel.result else { return None };
11190 if !path.partial {
11191 return None;
11192 }
11193 let mut steps = path.steps.clone();
11194 let count = fields.len();
11195 steps.extend(fields.into_iter().map(ast::PathStep::Name));
11196 Some((
11197 ast::SelectStmt {
11198 result: Expr::Path(ast::Path { steps, partial: true }),
11199 ..sel.clone()
11200 },
11201 count,
11202 ))
11203 }
11204
11205 #[allow(clippy::too_many_arguments)]
11226 fn try_compile_pointer_expr(
11227 &mut self,
11228 pointer_name: &str,
11229 compexpr: &Expr,
11230 td: &TypeDescriptor,
11231 alias: &str,
11232 module: &str,
11233 marker_offset: Option<usize>,
11234 nested_override: &[ShapeElement],
11235 ) -> Result<Option<IrShapePointer>, PyQLError> {
11236 if let Some((sel, tail)) = Self::field_access_over_select(compexpr)
11246 && let Expr::Path(path) = &sel.result
11247 {
11248 let (head_steps, tail_steps) = path.steps.split_at(path.steps.len() - tail);
11249 let (head_multi, head_target) = self.walk_path_types(td, head_steps, MAX_COMPUTED_SPLICES);
11250 let Some(head_target) = head_target else {
11251 return Ok(None);
11252 };
11253 let (tail_multi, target) = self.walk_path_types(head_target, tail_steps, MAX_COMPUTED_SPLICES);
11254 if target.is_none() {
11255 return Ok(None);
11256 }
11257 let multi = tail_multi || (head_multi && !limits_to_one(Some(&sel)));
11258 if !multi && nested_override.is_empty() {
11259 return Ok(None);
11260 }
11261 let path = path.clone();
11262 return self
11263 .compile_chained_link_pointer(pointer_name, &path, td, alias, nested_override, Some(&sel), multi, tail)
11264 .map(Some);
11265 }
11266 let Some((path, declared_nested, modifiers)) = Self::pointer_subject(compexpr) else {
11267 return Ok(None);
11268 };
11269 if !path.partial {
11270 return Ok(None);
11271 }
11272 let nested = if nested_override.is_empty() {
11276 declared_nested
11277 } else {
11278 nested_override
11279 };
11280
11281 let ml_name = match path.steps.as_slice() {
11285 [ast::PathStep::Name(n)] if Self::resolve_multilink(td, n).is_some() => n.clone(),
11286 [ast::PathStep::Backlink(_)] | [ast::PathStep::Backlink(_), ast::PathStep::TypeIntersection(_)] => {
11291 let current_qname = format!("{}::{}", td.module, td.name);
11292 let path = path.clone();
11293 let nested = nested.to_vec();
11294 return self
11295 .compile_backlink_pointer(pointer_name, &path, ¤t_qname, &nested, marker_offset, modifiers)
11296 .map(Some);
11297 }
11298 steps
11308 if matches!(
11309 steps.first(),
11310 Some(ast::PathStep::Name(_) | ast::PathStep::TypeIntersection(_))
11311 ) =>
11312 {
11313 let (multi, target) = self.walk_path_types(td, steps, MAX_COMPUTED_SPLICES);
11314 if target.is_none() || (!multi && nested.is_empty()) {
11320 return Ok(None);
11321 }
11322 let path = path.clone();
11323 let nested = nested.to_vec();
11324 return self
11325 .compile_chained_link_pointer(
11326 pointer_name,
11327 &path,
11328 td,
11329 alias,
11330 &nested,
11331 modifiers,
11332 multi && !limits_to_one(modifiers),
11333 0,
11334 )
11335 .map(Some);
11336 }
11337 _ => return Ok(None),
11338 };
11339
11340 let synthetic = ShapeElement {
11341 path: path.clone(),
11342 splat: None,
11343 nested: Some(nested.to_vec()),
11344 compexpr: None,
11345 op: ast::ShapeOp::Assign,
11346 filter: modifiers.and_then(|s| s.filter.clone()),
11347 order_by: modifiers.map(|s| s.order_by.clone()).unwrap_or_default(),
11348 offset: modifiers.and_then(|s| s.offset.clone()),
11349 limit: modifiers.and_then(|s| s.limit.clone()),
11350 marker_offset,
11351 };
11352 let mut pointer = self.compile_multilink_pointer(pointer_name, &ml_name, td, alias, module, &synthetic)?;
11353 if let IrShapePointer::MultiLink(link) = &mut pointer {
11354 link.single = limits_to_one(modifiers);
11355 }
11356 Ok(Some(pointer))
11357 }
11358
11359 fn compile_subquery_expr(
11379 &mut self,
11380 stmt: &Stmt,
11381 extra_fields: &[String],
11382 ctx: Option<(&TypeDescriptor, &str)>,
11383 outer_shape: &[ShapeElement],
11384 ) -> Result<IrExpr, PyQLError> {
11385 if let Stmt::With(w) = stmt {
11389 for alias in &w.aliases {
11390 if self.bind_inline_if_correlated(&alias.name, &alias.expr, ctx)?
11391 || self.bind_group(&alias.name, &alias.expr)?
11392 {
11393 continue;
11394 }
11395 match self.hoisted_binding_sources.get(&alias.name) {
11403 Some(existing) if *existing == alias.expr => continue,
11404 Some(_) => {
11405 return Err(self.type_err(&format!(
11406 "two different `with` bindings named '{}' end up in one statement; \
11407 rename one of them",
11408 alias.name
11409 )));
11410 }
11411 None => {}
11412 }
11413 let (ir_stmt, correlated_to) = self.compile_binding_in_scope(&alias.expr)?;
11414 let sql_name = self.claim_cte_sql_name(&alias.name);
11415 if let Some(slot) = correlated_to.as_deref().and_then(|i| i.strip_prefix("_for_")) {
11416 self.correlated_bindings.insert(sql_name.clone(), slot.to_string());
11417 }
11418 let type_name = self.register_cte(&alias.name, &ir_stmt);
11419 self.hoisted_binding_sources
11420 .insert(alias.name.clone(), alias.expr.clone());
11421 self.hoisted_ctes.push(IrCteDef {
11422 name: sql_name,
11423 stmt: ir_stmt,
11424 type_name,
11425 correlated_to,
11426 });
11427 }
11428 let inner = (*w.stmt).clone();
11429 return self.compile_subquery_expr(&inner, extra_fields, ctx, outer_shape);
11430 }
11431
11432 if extra_fields.is_empty()
11433 && let Stmt::Select(sel) = stmt
11434 && let Expr::Shape(sh) = &sel.result
11435 && let Some(Expr::SubQuery(subject)) = &sh.expr
11436 && matches!(subject.as_ref(), Stmt::Group(_))
11437 && let IrStmt::Group(grp) = self.compile_stmt(stmt)?
11438 {
11439 return Ok(IrExpr::ArrayFromSelect(Box::new(IrArraySource::Group(Box::new(grp)))));
11440 }
11441
11442 if let Stmt::Select(inner) = stmt
11446 && let Expr::Shape(sh) = &inner.result
11447 && let Some(subject) = sh.expr.as_ref()
11448 && let Some(operands) = Self::union_of_relative_paths(subject)
11449 && let Some((td, alias)) = ctx
11450 {
11451 let elements = sh.elements.clone();
11452 let operands_reach_many = self.relative_paths_reach_many(td, &operands);
11453 let mut branches = Vec::with_capacity(operands.len());
11454 for path in operands {
11455 let mut steps = vec![ast::PathStep::Name(format!("{}::{}", td.module, td.name))];
11456 steps.extend(path.steps.iter().cloned());
11457 let rooted = ast::Path { steps, partial: false };
11458 let synthetic = ast::SelectStmt {
11459 result: Expr::Path(rooted.clone()),
11460 filter: inner.filter.clone(),
11461 order_by: vec![],
11462 offset: None,
11463 limit: None,
11464 lock: None,
11465 };
11466 let mut ps = self.compile_path_select(&synthetic, &rooted, &elements, false)?;
11467 Self::correlate_path_select(&mut ps, alias);
11468 branches.push(ps);
11469 }
11470 let limit = inner
11471 .limit
11472 .as_ref()
11473 .map(|l| self.compile_free_expr(l))
11474 .transpose()?
11475 .map(Box::new);
11476 let multi = !matches!(limit.as_deref(), Some(IrExpr::Literal(IrLiteral::Int(1)))) && operands_reach_many;
11477 return Ok(IrExpr::ObjectPathUnion { branches, limit, multi });
11478 }
11479 if matches!(stmt, Stmt::Insert(_) | Stmt::Update(_) | Stmt::Delete(_))
11490 && !extra_fields.is_empty()
11491 && ctx.is_none()
11492 {
11493 let type_name = self.dml_subject_type(stmt)?;
11494 let inner = self.compile_stmt(stmt)?;
11495 let cte_name = self.fresh_nested_cte_name();
11496 self.register_cte(&cte_name, &inner);
11502 self.hoisted_ctes.push(IrCteDef {
11503 name: cte_name.clone(),
11504 stmt: inner,
11505 type_name,
11506 correlated_to: None,
11507 });
11508 let mut steps = vec![ast::PathStep::Name(cte_name)];
11509 steps.extend(extra_fields.iter().cloned().map(ast::PathStep::Name));
11510 return self.compile_free_path(&ast::Path { steps, partial: false });
11511 }
11512
11513 let Stmt::Select(sel) = stmt else {
11514 return Err(self.subquery_expr_err(stmt));
11515 };
11516 let has_modifiers =
11517 sel.filter.is_some() || !sel.order_by.is_empty() || sel.offset.is_some() || sel.limit.is_some();
11518
11519 let Some((path, shape_els)) = Self::split_path_result(&sel.result) else {
11520 if let Expr::FunctionCall(fc) = &sel.result {
11523 let fc = fc.clone();
11524 if let Some(ir) = self.try_compile_fn_scalar_subquery(&fc, extra_fields, Some(sel), ctx)? {
11525 return Ok(ir);
11526 }
11527 }
11528 if has_modifiers {
11533 let inner = self.compile_stmt(stmt)?;
11534 let IrStmt::Select(select) = inner else {
11535 return Err(self.subquery_expr_err(stmt));
11536 };
11537 if !select
11538 .rows
11539 .iter()
11540 .all(|r| matches!(r, IrRowSource::Free(IrFreeExpr::Scalar(_))))
11541 {
11542 return Err(self.subquery_expr_err(stmt));
11543 }
11544 let mut ir = IrExpr::ScalarSubquery(Box::new(select));
11545 for field in extra_fields {
11546 ir = Self::project_free_object_field(ir, field);
11547 }
11548 return Ok(ir);
11549 }
11550 let mut ir = self.compile_expr_ctx(&sel.result, ctx)?;
11551 for field in extra_fields {
11552 ir = Self::project_free_object_field(ir, field);
11553 }
11554 return Ok(ir);
11555 };
11556 let rewritten_sel;
11563 let mut sel = sel;
11564 let mut shape_els = shape_els;
11565 let mut extra_steps: Vec<ast::PathStep> = extra_fields.iter().map(|f| ast::PathStep::Name(f.clone())).collect();
11566 if !shape_els.is_empty()
11567 && let Some(defs) = Self::shape_alias_paths(shape_els)
11568 {
11569 extra_steps = extra_fields
11570 .iter()
11571 .flat_map(|field| match defs.iter().find(|(name, _)| name == field) {
11572 Some((_, definition)) => definition.steps.clone(),
11573 None => vec![ast::PathStep::Name(field.clone())],
11574 })
11575 .collect();
11576 rewritten_sel = ast::SelectStmt {
11577 result: sel.result.clone(),
11578 filter: sel.filter.clone().map(|f| Self::substitute_shape_aliases(f, &defs)),
11579 order_by: sel
11580 .order_by
11581 .iter()
11582 .map(|o| ast::SortExpr {
11583 expr: Self::substitute_shape_aliases(o.expr.clone(), &defs),
11584 direction: o.direction.clone(),
11585 nones: o.nones.clone(),
11586 })
11587 .collect(),
11588 offset: sel.offset.clone(),
11589 limit: sel.limit.clone(),
11590 lock: sel.lock.clone(),
11591 };
11592 sel = &rewritten_sel;
11593 shape_els = &[];
11594 }
11595
11596 if !shape_els.is_empty()
11607 && extra_fields.is_empty()
11608 && !path.partial
11609 && let [ast::PathStep::Name(type_name)] = path.steps.as_slice()
11610 && let Some(root_td) = match self.cte_object_type(type_name) {
11611 Some(_) => self.resolve_path_root(type_name).ok(),
11612 None => self.resolve_type(type_name).ok(),
11613 }
11614 {
11615 let alias = self.fresh_alias();
11616 let binding = self.cte_object_type(type_name).map(|_| type_name.clone());
11617 let table = match &binding {
11618 Some(name) => self.row_source_table(name, root_td),
11619 None => root_td.table.clone(),
11620 };
11621 let outer_declared = binding.as_ref().map(|name| {
11622 let declared = self.cte_declared_pointers.get(name).cloned().unwrap_or_default();
11623 std::mem::replace(&mut self.active_declared_pointers, declared)
11624 });
11625 let modifiers = self.compile_path_modifiers(sel, root_td, &alias);
11626 let module = root_td.module.clone();
11627 let shape = self.compile_shape(shape_els, root_td, &alias, &module);
11628 if let Some(outer) = outer_declared {
11629 self.active_declared_pointers = outer;
11630 }
11631 let (filter, order_by, offset, limit) = modifiers?;
11632 let shape = shape?;
11633 let source = IrSource {
11634 poly: self.poly_fanout_for(&format!("{}::{}", root_td.module, root_td.name)),
11635 type_name: format!("{}::{}", root_td.module, root_td.name),
11636 table,
11637 alias,
11638 };
11639 let single = selects_at_most_one(sel, root_td);
11640 let mut select = IrSelect::schema_bound(source, shape, filter);
11641 select.order_by = order_by;
11642 select.offset = offset;
11643 select.limit = limit;
11644 return Ok(if single {
11647 IrExpr::ObjectSubquery(Box::new(select))
11648 } else {
11649 IrExpr::ArrayFromSelect(Box::new(IrArraySource::ObjectSelect(Box::new(select))))
11650 });
11651 }
11652
11653 if !shape_els.is_empty() && extra_fields.is_empty() {
11654 return Err(self.type_err(
11655 "a sub-select with a shape is not valid in expression context — \
11656 assign it to a computed pointer instead, or project a property \
11657 off it, e.g. '(select .emails limit 1).address'",
11658 ));
11659 }
11660
11661 if !path.partial
11664 && let [ast::PathStep::Name(name)] = path.steps.as_slice()
11665 && self.is_value_binding(name)
11666 {
11667 let mut ir = self.compile_expr_ctx(&sel.result, ctx)?;
11668 for field in extra_fields {
11669 ir = Self::project_free_object_field(ir, field);
11670 }
11671 return Ok(ir);
11672 }
11673
11674 if !path.partial
11678 && extra_fields.is_empty()
11679 && let [ast::PathStep::Name(type_name)] = path.steps.as_slice()
11680 && let Ok(root_td) = self.resolve_type(type_name)
11681 {
11682 let alias = self.fresh_alias();
11683 let (filter, order_by, offset, limit) = self.compile_path_modifiers(sel, root_td, &alias)?;
11684 let source = IrSource {
11685 poly: None,
11686 type_name: format!("{}::{}", root_td.module, root_td.name),
11687 table: root_td.table.clone(),
11688 alias,
11689 };
11690 let pk = root_td.properties.iter().find(|p| p.is_pk);
11694 let shape = vec![IrShapePointer::Scalar(IrScalarPointer {
11695 implicit_id: false,
11696 marker_offset: None,
11697 alias: "id".to_string(),
11698 column: pk.map(|p| p.name.clone()).unwrap_or_else(|| "id".to_string()),
11699 pg_type: pk.map(|p| p.pg_type.clone()).unwrap_or_else(|| "uuid".to_string()),
11700 tuple_shape: None,
11701 })];
11702 let mut select = IrSelect::schema_bound(source, shape, filter);
11703 select.order_by = order_by;
11704 select.offset = offset;
11705 select.limit = limit;
11706 return Ok(IrExpr::Subquery(Box::new(select)));
11707 }
11708
11709 let mut steps = path.steps.clone();
11710 let correlate = if path.partial {
11711 let Some((td, alias)) = ctx else {
11712 return Err(self.type_err(
11713 "a relative path in a sub-select needs an enclosing object — \
11714 write the type name explicitly, e.g. '(select Person.name)'",
11715 ));
11716 };
11717 steps.insert(0, ast::PathStep::Name(format!("{}::{}", td.module, td.name)));
11718 Some(alias.to_string())
11719 } else {
11720 None
11721 };
11722 steps.extend(extra_steps.iter().cloned());
11723 let full_path = ast::Path { steps, partial: false };
11724
11725 let mut ps = self.compile_path_select_with_tail(sel, &full_path, outer_shape, false, extra_steps.len(), &[])?;
11726 if let Some(outer_alias) = correlate {
11727 Self::correlate_path_select(&mut ps, &outer_alias);
11728 }
11729 if !outer_shape.is_empty() && matches!(ps.result, IrPathResult::Object { .. }) {
11734 return Ok(IrExpr::ObjectPathSubquery(Box::new(ps)));
11735 }
11736 let single = matches!(&sel.limit, Some(Expr::Literal(ast::Literal::Int(1))));
11742 let multi = match &full_path.steps[0] {
11743 ast::PathStep::Name(root) => {
11744 let root_td = self.resolve_path_root(root)?;
11745 let every_row_of_a_type = !path.partial
11748 && self.cte_object_type(root).is_none()
11749 && self.resolve_type(root).is_ok()
11750 && !selects_at_most_one(sel, root_td);
11751 every_row_of_a_type || self.path_crosses_multi(root_td, &full_path.steps[1..])
11752 }
11753 _ => false,
11754 };
11755 if multi && !single && matches!(ps.result, IrPathResult::Scalar(..)) {
11756 return Ok(IrExpr::ArrayFromSelect(Box::new(IrArraySource::PathSelect(Box::new(
11757 ps,
11758 )))));
11759 }
11760 Ok(IrExpr::PathSubquery(Box::new(ps)))
11761 }
11762
11763 fn try_compile_fn_scalar_subquery(
11778 &mut self,
11779 fc: &ast::FunctionCall,
11780 fields: &[String],
11781 modifiers: Option<&ast::SelectStmt>,
11782 ctx: Option<(&TypeDescriptor, &str)>,
11783 ) -> Result<Option<IrExpr>, PyQLError> {
11784 let fd = self.schema.functions.iter().find(|f| {
11785 let module_matches = fc.module.as_deref().map(|m| m == f.module.as_str()).unwrap_or(true);
11786 module_matches && f.name == fc.name && f.return_is_object
11787 });
11788 let Some(fd) = fd else { return Ok(None) };
11789 let (fn_module, fn_name, return_type_name, polymorphic, params) = (
11790 fd.module.clone(),
11791 fd.name.clone(),
11792 fd.return_pg_type.clone(),
11793 fd.return_is_polymorphic,
11794 fd.params.clone(),
11795 );
11796
11797 let qualified = format!("{fn_module}::{fn_name}");
11798 let [field] = fields else {
11799 return Err(self.type_err(&format!(
11800 "function '{qualified}' returns objects, so using it inside an expression needs \
11801 one of its properties, e.g. '{qualified}(…).name'"
11802 )));
11803 };
11804 if params.len() != fc.args.len() {
11805 return Err(self.type_err(&format!(
11806 "function '{qualified}' expects {} argument(s), got {}",
11807 params.len(),
11808 fc.args.len()
11809 )));
11810 }
11811
11812 let mut fn_args = fc
11813 .args
11814 .iter()
11815 .map(|a| self.compile_expr_ctx(a, ctx))
11816 .collect::<Result<Vec<_>, _>>()?;
11817 if let Some(globals) = self.globals_arg_for_call(&qualified)? {
11818 fn_args.insert(0, globals);
11819 }
11820
11821 let td = self.resolve_type(&return_type_name)?;
11822 let alias = self.fresh_alias();
11823 let Some(prop) = Self::resolve_property(td, field) else {
11824 return Err(self.field_err(field, &return_type_name));
11825 };
11826 let projected = IrShapePointer::Computed(IrComputedPointer {
11827 marker_offset: None,
11828 alias: field.clone(),
11829 expr: IrExpr::ColumnRef {
11830 alias: alias.clone(),
11831 column: prop.name.clone(),
11832 pg_type: prop.pg_type.clone(),
11833 },
11834 });
11835
11836 let (poly_implementors, poly_columns) = if polymorphic {
11837 self.collect_poly_info(&return_type_name)
11838 } else {
11839 (vec![], vec![])
11840 };
11841 let (filter, order_by, offset, limit) = match modifiers {
11842 Some(sel) => {
11843 let td = self.resolve_type(&return_type_name)?;
11844 self.compile_path_modifiers(sel, td, &alias)?
11845 }
11846 None => (None, vec![], None, None),
11847 };
11848
11849 Ok(Some(IrExpr::FnSubquery(Box::new(IrFunctionSelect {
11850 fn_module,
11851 fn_name,
11852 fn_args,
11853 alias,
11854 type_name: return_type_name,
11855 polymorphic,
11856 poly_implementors,
11857 poly_columns,
11858 shape: vec![projected],
11859 filter,
11860 order_by,
11861 offset,
11862 limit,
11863 distinct: false,
11864 }))))
11865 }
11866
11867 fn computed_is_object_valued(&self, cd: &crate::schema::ComputedDescriptor, td: &TypeDescriptor) -> bool {
11875 let Ok(expr) = crate::parse::parse_pointer_expr(&cd.expression) else {
11876 return false;
11877 };
11878 let expr = match Self::field_access_over_select(&expr) {
11879 Some((sel, _)) => Expr::SubQuery(Box::new(Stmt::Select(sel))),
11880 None => expr,
11881 };
11882 let Some((path, _, _)) = Self::pointer_subject(&expr) else {
11883 return false;
11884 };
11885 if !path.partial {
11886 return false;
11887 }
11888 match path.steps.as_slice() {
11889 [ast::PathStep::Backlink(_)] | [ast::PathStep::Backlink(_), ast::PathStep::TypeIntersection(_)] => true,
11893 steps => self.walk_path_types(td, steps, MAX_COMPUTED_SPLICES).1.is_some(),
11894 }
11895 }
11896
11897 fn path_crosses_multi(&self, td: &TypeDescriptor, steps: &[ast::PathStep]) -> bool {
11903 self.walk_path_types(td, steps, MAX_COMPUTED_SPLICES).0
11904 }
11905
11906 fn coalesce_path_branches(
11910 &mut self,
11911 td: &TypeDescriptor,
11912 alias: &str,
11913 operands: &[ast::Path],
11914 elements: &[ast::ShapeElement],
11915 ) -> Result<Vec<IrPathSelect>, PyQLError> {
11916 let mut branches: Vec<IrPathSelect> = Vec::with_capacity(operands.len());
11917 for path in operands {
11918 let mut steps = vec![ast::PathStep::Name(format!("{}::{}", td.module, td.name))];
11919 steps.extend(path.steps.iter().cloned());
11920 let rooted = ast::Path { steps, partial: false };
11921 let synthetic = ast::SelectStmt {
11922 result: Expr::Path(rooted.clone()),
11923 filter: None,
11924 order_by: vec![],
11925 offset: None,
11926 limit: None,
11927 lock: None,
11928 };
11929 let mut ps = self.compile_path_select(&synthetic, &rooted, elements, false)?;
11930 Self::correlate_path_select(&mut ps, alias);
11931 let guards: Vec<IrExpr> = branches
11932 .iter()
11933 .map(|earlier| {
11934 IrExpr::UnaryOp(Box::new(IrUnaryOp {
11935 op: ast::UnaryOpKind::Not,
11936 operand: IrExpr::UnaryOp(Box::new(IrUnaryOp {
11937 op: ast::UnaryOpKind::Exists,
11938 operand: IrExpr::PathSubquery(Box::new(earlier.clone())),
11939 })),
11940 }))
11941 })
11942 .collect();
11943 if !guards.is_empty() {
11944 ps.filter = and_conditions(ps.filter, guards);
11945 }
11946 branches.push(ps);
11947 }
11948 Ok(branches)
11949 }
11950
11951 fn path_lands_on_objects(&self, td: &TypeDescriptor, path: &ast::Path) -> bool {
11956 let Some(last) = path
11957 .steps
11958 .iter()
11959 .rposition(|step| matches!(step, ast::PathStep::Name(_) | ast::PathStep::Backlink(_)))
11960 else {
11961 return false;
11962 };
11963 match &path.steps[last] {
11964 ast::PathStep::Backlink(_) => true,
11965 ast::PathStep::Name(name) => self
11966 .walk_path_types(td, &path.steps[..last], MAX_COMPUTED_SPLICES)
11967 .1
11968 .is_some_and(|owner| {
11969 Self::resolve_multilink(owner, name).is_some() || Self::resolve_link(owner, name).is_some()
11970 }),
11971 _ => false,
11972 }
11973 }
11974
11975 fn relative_paths_reach_many(&self, td: &TypeDescriptor, operands: &[ast::Path]) -> bool {
11980 operands.iter().any(|path| {
11981 let (root_td, rest) = match path.steps.first() {
11982 Some(ast::PathStep::TypeIntersection(tr)) => {
11983 let name = match &tr.module {
11984 Some(module) => format!("{}::{}", module, tr.name),
11985 None => tr.name.clone(),
11986 };
11987 match self.resolve_type(&name) {
11988 Ok(resolved) => (resolved, &path.steps[1..]),
11989 Err(_) => return true,
11990 }
11991 }
11992 _ => (td, &path.steps[..]),
11993 };
11994 self.path_crosses_multi(root_td, rest)
11995 })
11996 }
11997
11998 fn walk_path_types(
12007 &self,
12008 td: &'a TypeDescriptor,
12009 steps: &[ast::PathStep],
12010 depth: usize,
12011 ) -> (bool, Option<&'a TypeDescriptor>) {
12012 let mut current = td;
12013 let mut multi = false;
12014 for (i, step) in steps.iter().enumerate() {
12015 match step {
12016 ast::PathStep::Backlink(backlink_name) => {
12020 let Some(ast::PathStep::TypeIntersection(tr)) = steps.get(i + 1) else {
12021 return (true, None);
12022 };
12023 let owner_name = match &tr.module {
12024 Some(m) => format!("{}::{}", m, tr.name),
12025 None => tr.name.clone(),
12026 };
12027 let current_qname = format!("{}::{}", current.module, current.name);
12028 let single = self
12029 .resolve_type(&owner_name)
12030 .is_ok_and(|owner| self.backlink_is_single(owner, backlink_name, ¤t_qname));
12031 multi |= !single;
12032 continue;
12033 }
12034 ast::PathStep::TypeIntersection(tr) => {
12035 let name = match &tr.module {
12036 Some(m) => format!("{}::{}", m, tr.name),
12037 None => tr.name.clone(),
12038 };
12039 match self.resolve_type(&name) {
12040 Ok(t) => current = t,
12041 Err(_) => return (multi, None),
12042 }
12043 }
12044 ast::PathStep::Name(n) => {
12045 if let Some(ml) = Self::resolve_multilink(current, n) {
12046 multi = true;
12047 match self.resolve_type(&ml.target) {
12048 Ok(t) => current = t,
12049 Err(_) => return (multi, None),
12050 }
12051 continue;
12052 }
12053 if let Some(target) = Self::resolve_link(current, n).map(|l| l.target.clone()) {
12054 match self.resolve_type(&target) {
12055 Ok(t) => current = t,
12056 Err(_) => return (multi, None),
12057 }
12058 continue;
12059 }
12060 if depth > 0
12063 && let Some(cd) = self.resolve_computed(current, n)
12064 && let Ok(expr) = crate::parse::parse_pointer_expr(&cd.expression)
12065 {
12066 if let Some((p, _, modifiers)) = Self::pointer_subject(&expr)
12067 && p.partial
12068 {
12069 let (m, t) = self.walk_path_types(current, &p.steps, depth - 1);
12070 let capped = modifiers
12073 .is_some_and(|m| matches!(&m.limit, Some(Expr::Literal(ast::Literal::Int(1)))));
12074 multi = multi || (m && !capped);
12075 match t {
12076 Some(t) => current = t,
12077 None => return (multi, None),
12078 }
12079 continue;
12080 }
12081 if let Some((fc, _)) = Self::function_subject(&expr)
12082 && let Some(fd) = self.resolve_object_fn(fc)
12083 {
12084 multi = multi || fd.return_is_set;
12085 match self.resolve_type(&fd.return_pg_type) {
12086 Ok(t) => current = t,
12087 Err(_) => return (multi, None),
12088 }
12089 continue;
12090 }
12091 }
12092 return (multi, None);
12094 }
12095 _ => return (multi, None),
12096 }
12097 }
12098 (multi, Some(current))
12099 }
12100
12101 fn compile_partial_path_as_subquery(
12116 &mut self,
12117 p: &ast::Path,
12118 td: &TypeDescriptor,
12119 alias: &str,
12120 ) -> Result<IrExpr, PyQLError> {
12121 let (root_name, rest) = match p.steps.first() {
12122 Some(ast::PathStep::TypeIntersection(tr)) => {
12123 let name = match &tr.module {
12124 Some(m) => format!("{}::{}", m, tr.name),
12125 None => tr.name.clone(),
12126 };
12127 (name, &p.steps[1..])
12128 }
12129 _ => (format!("{}::{}", td.module, td.name), &p.steps[..]),
12130 };
12131 let root_td = self.resolve_type(&root_name)?;
12132 let multi = self.path_crosses_multi(root_td, rest);
12133
12134 let mut steps = vec![ast::PathStep::Name(root_name)];
12135 steps.extend(rest.iter().cloned());
12136 let full_path = ast::Path { steps, partial: false };
12137 let synthetic = ast::SelectStmt {
12138 result: Expr::Path(full_path.clone()),
12139 filter: None,
12140 order_by: vec![],
12141 offset: None,
12142 limit: None,
12143 lock: None,
12144 };
12145 let mut ps = self.compile_path_select(&synthetic, &full_path, &[], false)?;
12146 Self::correlate_path_select(&mut ps, alias);
12147 if multi && matches!(ps.result, IrPathResult::Scalar(..)) {
12148 Ok(IrExpr::ArrayFromSelect(Box::new(IrArraySource::PathSelect(Box::new(
12149 ps,
12150 )))))
12151 } else {
12152 Ok(IrExpr::PathSubquery(Box::new(ps)))
12153 }
12154 }
12155
12156 fn relative_subselect(
12165 &mut self,
12166 stmt: &Stmt,
12167 ctx: Option<(&TypeDescriptor, &str)>,
12168 ) -> Result<Option<IrPathSelect>, PyQLError> {
12169 let (Some((td, alias)), Stmt::Select(sel)) = (ctx, stmt) else {
12170 return Ok(None);
12171 };
12172 let (path, shape): (&ast::Path, &[ShapeElement]) = match &sel.result {
12173 Expr::Path(p) if p.partial => (p, &[]),
12174 Expr::Shape(sh) => match sh.expr.as_ref() {
12175 Some(Expr::Path(p)) if p.partial => (p, sh.elements.as_slice()),
12176 _ => return Ok(None),
12177 },
12178 _ => return Ok(None),
12179 };
12180 let mut steps = vec![ast::PathStep::Name(format!("{}::{}", td.module, td.name))];
12181 steps.extend(path.steps.iter().cloned());
12182 let rooted = ast::Path { steps, partial: false };
12183 let synthetic = ast::SelectStmt {
12184 result: Expr::Path(rooted.clone()),
12185 filter: sel.filter.clone(),
12186 order_by: sel.order_by.clone(),
12187 offset: sel.offset.clone(),
12188 limit: sel.limit.clone(),
12189 lock: None,
12190 };
12191 let mut ps = self.compile_path_select(&synthetic, &rooted, shape, false)?;
12192 Self::correlate_path_select(&mut ps, alias);
12193 Ok(Some(ps))
12194 }
12195
12196 fn correlate_path_select(ps: &mut IrPathSelect, outer_alias: &str) {
12200 if outer_alias == "NEW" || outer_alias == "OLD" {
12203 ps.root.table = format!("@row:{outer_alias}");
12204 ps.root.poly = None;
12205 }
12206 let correlation = IrExpr::BinOp(Box::new(IrBinOp {
12207 left: IrExpr::ColumnRef {
12208 alias: ps.root.alias.clone(),
12209 column: "id".to_string(),
12210 pg_type: "uuid".to_string(),
12211 },
12212 op: ast::BinOpKind::Eq,
12213 right: IrExpr::ColumnRef {
12214 alias: outer_alias.to_string(),
12215 column: "id".to_string(),
12216 pg_type: "uuid".to_string(),
12217 },
12218 }));
12219 ps.filter = Some(match ps.filter.take() {
12220 Some(existing) => IrExpr::BinOp(Box::new(IrBinOp {
12221 left: correlation,
12222 op: ast::BinOpKind::And,
12223 right: existing,
12224 })),
12225 None => correlation,
12226 });
12227 }
12228
12229 fn subquery_expr_err(&self, stmt: &Stmt) -> PyQLError {
12233 let what = match stmt {
12234 Stmt::Insert(_) => "an insert",
12235 Stmt::Update(_) => "an update",
12236 Stmt::Delete(_) => "a delete",
12237 Stmt::With(_) => "a `with` block",
12238 Stmt::For(_) => "a `for` loop",
12239 Stmt::Group(_) => "a `group`",
12240 _ => "this sub-statement",
12241 };
12242 self.type_err(&format!(
12243 "{what} cannot stand in for a value — a sub-statement is only valid in expression \
12244 position as a select over a path, e.g. '(select .emails filter .primary limit 1).address'"
12245 ))
12246 }
12247
12248 fn set_operands(expr: &Expr) -> Option<Vec<&Expr>> {
12256 match expr {
12257 Expr::Set(elems) if elems.is_empty() => None,
12261 Expr::Set(elems) => Some(elems.iter().collect()),
12262 Expr::Union(left, right) => {
12263 let mut operands = Self::set_operands(left).unwrap_or_else(|| vec![left.as_ref()]);
12264 operands.extend(Self::set_operands(right).unwrap_or_else(|| vec![right.as_ref()]));
12265 Some(operands)
12266 }
12267 _ => None,
12268 }
12269 }
12270
12271 fn in_value_position<R>(&mut self, f: impl FnOnce(&mut Self) -> R) -> R {
12275 let previous = std::mem::replace(&mut self.value_position, true);
12276 let result = f(self);
12277 self.value_position = previous;
12278 result
12279 }
12280
12281 fn as_condition<R>(&mut self, f: impl FnOnce(&mut Self) -> R) -> R {
12286 let previous = std::mem::replace(&mut self.value_position, false);
12287 let result = f(self);
12288 self.value_position = previous;
12289 result
12290 }
12291
12292 fn compile_expr_ctx(&mut self, expr: &Expr, ctx: Option<(&TypeDescriptor, &str)>) -> Result<IrExpr, PyQLError> {
12302 match expr {
12303 Expr::Path(p) => match ctx {
12310 Some((td, alias)) => self.compile_path(p, td, alias),
12311 None => self.compile_free_path(p),
12312 },
12313
12314 Expr::PathStepOn { .. } => match self.try_compile_walk_off_subquery(expr, ctx)? {
12317 Some(ir) => Ok(ir),
12318 None => Err(self.type_err(
12319 "a type intersection, link property or backlink needs a path, a binding \
12320 or a sub-select to walk off",
12321 )),
12322 },
12323
12324 Expr::Literal(lit) => Ok(IrExpr::Literal(match lit {
12325 Literal::Str(s) => IrLiteral::Str(s.clone()),
12326 Literal::Int(n) => IrLiteral::Int(*n),
12327 Literal::Float(f) => IrLiteral::Float(*f),
12328 Literal::Bool(b) => IrLiteral::Bool(*b),
12329 })),
12330
12331 Expr::Parameter(name) => {
12332 let index = self.param_index(name);
12333 Ok(IrExpr::Param { index })
12334 }
12335
12336 Expr::Global(name) => self.compile_global(name),
12337
12338 Expr::Index { expr: e, index: i } => {
12339 let ir_expr = self.compile_expr_ctx(e, ctx)?;
12340 if self.is_json_expr(e, &ir_expr) {
12341 return match &**i {
12342 Expr::Literal(Literal::Str(key)) => Ok(IrExpr::JsonbField {
12343 expr: Box::new(ir_expr),
12344 field: key.clone(),
12345 }),
12346 Expr::Literal(Literal::Int(n)) if *n >= 0 => Ok(IrExpr::JsonbIndex {
12347 expr: Box::new(ir_expr),
12348 index: *n as usize,
12349 }),
12350 _ => Err(self.type_err("indexing json needs a literal key or a literal position")),
12351 };
12352 }
12353 let ir_index = self.compile_expr_ctx(i, ctx)?;
12354 let is_array = is_array_expr(&ir_expr);
12355 Ok(IrExpr::Subscript {
12356 expr: Box::new(ir_expr),
12357 index: Box::new(ir_index),
12358 is_array,
12359 })
12360 }
12361
12362 Expr::Slice {
12363 expr: e,
12364 lower: lo,
12365 upper: hi,
12366 } => {
12367 let ir_expr = self.compile_expr_ctx(e, ctx)?;
12368 let is_array = is_array_expr(&ir_expr);
12369 let ir_lower = lo.as_ref().map(|x| self.compile_expr_ctx(x, ctx)).transpose()?;
12370 let ir_upper = hi.as_ref().map(|x| self.compile_expr_ctx(x, ctx)).transpose()?;
12371 Ok(IrExpr::Slice {
12372 expr: Box::new(ir_expr),
12373 lower: ir_lower.map(Box::new),
12374 upper: ir_upper.map(Box::new),
12375 is_array,
12376 })
12377 }
12378
12379 Expr::TypeCast(tc) => {
12380 if matches!(&tc.expr, Expr::Set(elems) if elems.is_empty()) {
12389 return Ok(IrExpr::Null);
12390 }
12391 if let ast::TypeExpr::Tuple { elements } = &tc.ty
12392 && let Some(ir) = self.try_compile_tuple_literal_cast_ctx(elements, &tc.expr, ctx)?
12393 {
12394 let pg_type = self.resolve_cast_pg_type(&tc.ty)?;
12395 let tuple_shape = self.resolve_tuple_cast_shape(&tc.ty);
12396 return Ok(IrExpr::TypeCast(Box::new(IrTypeCast {
12397 expr: ir,
12398 pg_type,
12399 tuple_shape,
12400 })));
12401 }
12402 if let ast::TypeExpr::Array { element } = &tc.ty
12403 && let Some(ir) = self.try_compile_array_literal_cast_ctx(element, &tc.expr, ctx)?
12404 {
12405 let pg_type = self.resolve_cast_pg_type(&tc.ty)?;
12406 return Ok(IrExpr::TypeCast(Box::new(IrTypeCast {
12407 expr: ir,
12408 pg_type,
12409 tuple_shape: None,
12410 })));
12411 }
12412 const STDLIB_MODULES: &[&str] = &["std", "cal", "math", "sys", "pgvector", "crypto", "postgis"];
12419 if let Some((module, name)) = tc.ty.as_named()
12420 && module.map(|m| !STDLIB_MODULES.contains(&m)).unwrap_or(false)
12421 {
12422 let qname = match module {
12423 Some(m) => format!("{m}::{name}"),
12424 None => name.to_string(),
12425 };
12426 if self.resolve_enum(&qname).is_none()
12427 && self.resolve_scalar(&qname).is_none()
12428 && self.resolve_named_tuple(&qname).is_none()
12429 && self.resolve_type(&qname).is_ok()
12430 {
12431 return self.compile_expr_ctx(&tc.expr, ctx);
12432 }
12433 }
12434 let inner = if casts_to_json(&tc.ty) {
12438 self.without_implicit_id(|this| this.compile_expr_ctx(&tc.expr, ctx))?
12439 } else {
12440 self.compile_expr_ctx(&tc.expr, ctx)?
12441 };
12442 let pg_type = self.resolve_cast_pg_type(&tc.ty)?;
12443
12444 if infer_ir_type(&inner) == Some("jsonb") {
12454 if let ast::TypeExpr::Array { element } = &tc.ty {
12455 let elem_pg = self.resolve_cast_pg_type(element)?;
12456 let sql_template =
12457 format!("ARRAY(SELECT (elem #>> '{{}}')::{elem_pg} FROM jsonb_array_elements($1) AS elem)");
12458 return Ok(IrExpr::FunctionCall(super::IrFunctionCall {
12459 return_pg_type: None,
12460 schema: None,
12461 name: "jsonb_array_cast".to_string(),
12462 args: vec![inner],
12463 sql_template: Some(sql_template),
12464 }));
12465 }
12466 if matches!(
12467 pg_type.as_str(),
12468 "uuid" | "timestamptz" | "timestamp" | "date" | "time" | "interval"
12469 ) {
12470 let sql_template = format!("(($1 #>> '{{}}'))::{pg_type}");
12471 return Ok(IrExpr::FunctionCall(super::IrFunctionCall {
12472 return_pg_type: None,
12473 schema: None,
12474 name: "jsonb_scalar_cast".to_string(),
12475 args: vec![inner],
12476 sql_template: Some(sql_template),
12477 }));
12478 }
12479 }
12480
12481 if infer_ir_type(&inner) == Some("text")
12490 && let Some((module, name)) = tc.ty.as_named()
12491 {
12492 let parser = match (module, name) {
12493 (None | Some("std"), "datetime") => Some(("std", "to_datetime")),
12494 (Some("cal"), "local_datetime") => Some(("cal", "to_local_datetime")),
12495 (Some("cal"), "local_date") => Some(("cal", "to_local_date")),
12496 (Some("cal"), "local_time") => Some(("cal", "to_local_time")),
12497 _ => None,
12498 };
12499 if let Some((ns, fn_name)) = parser {
12500 return self.resolve_fn_call(Some(ns), fn_name, vec![inner]);
12501 }
12502 let helper = match (module, name) {
12507 (None | Some("std"), "duration") => Some("duration_in"),
12508 (Some("cal"), "date_duration") => Some("date_duration_in"),
12509 _ => None,
12510 };
12511 if let Some(helper) = helper {
12512 return Ok(IrExpr::FunctionCall(super::IrFunctionCall {
12513 return_pg_type: Some("interval".to_string()),
12514 schema: Some("_pylon".to_string()),
12515 name: helper.to_string(),
12516 args: vec![inner],
12517 sql_template: None,
12518 }));
12519 }
12520 }
12521
12522 if pg_type == "text"
12531 && matches!(
12532 infer_ir_type(&inner),
12533 Some("timestamptz" | "timestamp" | "date" | "time" | "interval")
12534 )
12535 {
12536 return self.resolve_fn_call(Some("std"), "to_str", vec![inner]);
12537 }
12538
12539 let inner = match (pg_type.as_str(), infer_ir_type(&inner)) {
12542 ("int2" | "int8", Some("boolean")) => IrExpr::TypeCast(Box::new(IrTypeCast {
12543 expr: inner,
12544 pg_type: "int4".to_string(),
12545 tuple_shape: None,
12546 })),
12547 _ => inner,
12548 };
12549
12550 let tuple_shape = self.resolve_tuple_cast_shape(&tc.ty);
12551 Ok(IrExpr::TypeCast(Box::new(IrTypeCast {
12552 expr: inner,
12553 pg_type,
12554 tuple_shape,
12555 })))
12556 }
12557
12558 Expr::BinOp(b)
12563 if b.op == ast::BinOpKind::Coalesce
12564 && ctx.is_some()
12565 && Self::coalesce_of_relative_paths(expr).is_some_and(|operands| {
12566 let td = ctx.expect("checked").0;
12567 self.relative_paths_reach_many(td, &operands)
12568 && operands.iter().all(|path| self.path_lands_on_objects(td, path))
12569 }) =>
12570 {
12571 let (td, alias) = ctx.expect("checked by the guard");
12572 let operands = Self::coalesce_of_relative_paths(expr).expect("checked by the guard");
12573 let branches = self.coalesce_path_branches(td, alias, &operands, &[])?;
12574 Ok(IrExpr::ObjectPathUnion {
12575 branches,
12576 limit: None,
12577 multi: true,
12578 })
12579 }
12580
12581 Expr::BinOp(b) => {
12582 let yields_values = matches!(
12585 b.op,
12586 ast::BinOpKind::Add
12587 | ast::BinOpKind::Sub
12588 | ast::BinOpKind::Mul
12589 | ast::BinOpKind::Div
12590 | ast::BinOpKind::FloorDiv
12591 | ast::BinOpKind::Mod
12592 | ast::BinOpKind::Pow
12593 | ast::BinOpKind::Coalesce
12594 | ast::BinOpKind::Concat
12595 );
12596 if let Some(elementwise) = self.try_elementwise_in(b, ctx)? {
12603 return Ok(elementwise);
12604 }
12605 if let Some((td, alias)) = ctx
12606 && !yields_values
12607 {
12608 if let Some(exists) = self.try_backlink_exists(b, td, alias)? {
12609 return Ok(exists);
12610 }
12611 if let Some(exists) = self.try_multilink_exists(b, td, alias)? {
12612 return Ok(exists);
12613 }
12614 }
12615 if matches!(b.op, ast::BinOpKind::In | ast::BinOpKind::NotIn)
12625 && let Expr::Set(elems) = &b.right
12626 {
12627 let left = self.compile_expr_ctx(&b.left, ctx)?;
12628 let items = elems
12629 .iter()
12630 .map(|e| self.compile_expr_ctx(e, ctx))
12631 .collect::<Result<Vec<_>, _>>()?;
12632 let right = IrExpr::Array(items);
12633 return Ok(IrExpr::BinOp(Box::new(IrBinOp {
12634 left,
12635 op: b.op.clone(),
12636 right,
12637 })));
12638 }
12639 if matches!(b.op, ast::BinOpKind::Eq | ast::BinOpKind::Ne) {
12645 let id = ["id".to_string()];
12646 let left_id = match &b.left {
12647 Expr::FunctionCall(fc) => self.try_compile_fn_scalar_subquery(fc, &id, None, ctx)?,
12648 _ => None,
12649 };
12650 let right_id = match &b.right {
12651 Expr::FunctionCall(fc) => self.try_compile_fn_scalar_subquery(fc, &id, None, ctx)?,
12652 _ => None,
12653 };
12654 if left_id.is_some() || right_id.is_some() {
12655 let left = match left_id {
12656 Some(e) => e,
12657 None => self.compile_expr_ctx(&b.left, ctx)?,
12658 };
12659 let right = match right_id {
12660 Some(e) => e,
12661 None => self.compile_expr_ctx(&b.right, ctx)?,
12662 };
12663 return Ok(IrExpr::BinOp(Box::new(IrBinOp {
12664 left,
12665 op: b.op.clone(),
12666 right,
12667 })));
12668 }
12669 }
12670 let mut left = self.compile_expr_ctx(&b.left, ctx)?;
12671 let mut right = self.compile_expr_ctx(&b.right, ctx)?;
12672 if matches!(
12676 b.op,
12677 ast::BinOpKind::Lt | ast::BinOpKind::Le | ast::BinOpKind::Gt | ast::BinOpKind::Ge
12678 ) && yields_array(&left) != yields_array(&right)
12679 {
12680 left = set_walk_as_scalar(left);
12681 right = set_walk_as_scalar(right);
12682 }
12683 if b.op == ast::BinOpKind::Coalesce && (yields_array(&left) || yields_array(&right)) {
12687 let as_set = |value: IrExpr| {
12688 if yields_array(&value) {
12689 value
12690 } else {
12691 IrExpr::FunctionCall(IrFunctionCall {
12692 return_pg_type: None,
12693 schema: None,
12694 name: "array_remove".to_string(),
12695 args: vec![value],
12696 sql_template: Some("array_remove(ARRAY[$1], NULL)".to_string()),
12697 })
12698 }
12699 };
12700 let left = as_set(left);
12701 let condition = IrExpr::FunctionCall(IrFunctionCall {
12702 return_pg_type: None,
12703 schema: None,
12704 name: "cardinality".to_string(),
12705 args: vec![left.clone()],
12706 sql_template: Some("(cardinality($1) > 0)".to_string()),
12707 });
12708 return Ok(IrExpr::IfElse(Box::new(IrIfElse {
12709 condition,
12710 if_: left,
12711 else_: as_set(right),
12712 })));
12713 }
12714 if matches!(b.op, ast::BinOpKind::Eq | ast::BinOpKind::Ne) {
12719 let is_set = |e: &IrExpr| match e {
12722 IrExpr::ArrayFromSelect(_) => true,
12723 IrExpr::CteRef { name, .. } => self.multi_row_ctes.contains(name),
12724 _ => false,
12725 };
12726 let flipped = is_set(&left) && !is_set(&right) && !is_array_expr(&right);
12727 let straight = is_set(&right) && !is_set(&left) && !is_array_expr(&left);
12728 if flipped || straight {
12729 let (value, set) = if straight { (left, right) } else { (right, left) };
12730 let membership = IrExpr::BinOp(Box::new(IrBinOp {
12731 left: value,
12732 op: ast::BinOpKind::In,
12733 right: set,
12734 }));
12735 return Ok(if matches!(b.op, ast::BinOpKind::Ne) {
12736 IrExpr::UnaryOp(Box::new(IrUnaryOp {
12737 op: ast::UnaryOpKind::Not,
12738 operand: membership,
12739 }))
12740 } else {
12741 membership
12742 });
12743 }
12744 }
12745 if let (Some(lt), Some(rt)) = (infer_ir_type(&left), infer_ir_type(&right))
12746 && !types_compatible(lt, rt)
12747 && !datetime_arithmetic_compatible(&b.op, lt, rt)
12748 {
12749 return Err(PyQLError::Type(PyQLTypeError {
12750 message: format!(
12751 "operator '{op}' cannot be applied to operands of type \
12752 '{lq}' and '{rq}'",
12753 op = b.op,
12754 lq = pg_type_to_pyql(lt),
12755 rq = pg_type_to_pyql(rt),
12756 ),
12757 position: Position { line: 0, col: 0 },
12758 }));
12759 }
12760 Ok(IrExpr::BinOp(Box::new(IrBinOp {
12761 left: true_division_operand(&b.op, left, &right),
12762 op: b.op.clone(),
12763 right,
12764 })))
12765 }
12766
12767 Expr::FunctionCall(f) => {
12768 if (f.module.is_none() || f.module.as_deref() == Some("std")) && f.name == "notify" {
12773 return self.compile_notify(f, ctx);
12774 }
12775 if (f.module.is_none() || f.module.as_deref() == Some("std")) && f.name == "notify_raw" {
12776 return self.compile_notify_raw(f, ctx);
12777 }
12778
12779 if ctx.is_none()
12782 && (f.module.is_none() || f.module.as_deref() == Some("std"))
12783 && (f.name == "sequence_next" || f.name == "sequence_reset")
12784 {
12785 return self.compile_sequence_fn(f);
12786 }
12787
12788 let assert_names: &[&str] = if ctx.is_some() {
12793 &["assert_single", "assert_exists", "assert_distinct"]
12794 } else {
12795 &["assert_single"]
12796 };
12797 if (f.module.is_none() || f.module.as_deref() == Some("std"))
12798 && assert_names.contains(&f.name.as_str())
12799 && !f.args.is_empty()
12800 && let Expr::SubQuery(inner_stmt) = &f.args[0]
12801 {
12802 let inner = match self.relative_subselect(inner_stmt, ctx)? {
12803 Some(ps) => IrArraySource::PathSelect(Box::new(ps)),
12804 None => self.compile_subquery_to_array_source(inner_stmt)?,
12805 };
12806 let fn_pg = match f.name.as_str() {
12807 "assert_single" => "assert_single",
12808 "assert_exists" => "assert_exists",
12809 _ => "assert_distinct",
12810 };
12811 let mut args = vec![IrExpr::ArrayFromSelect(Box::new(inner))];
12812 args.extend(self.assert_message(f, ctx)?);
12813 return Ok(IrExpr::FunctionCall(IrFunctionCall {
12814 return_pg_type: None,
12815 schema: Some("_pylon".to_string()),
12816 name: fn_pg.to_string(),
12817 args,
12818 sql_template: None,
12819 }));
12820 }
12821
12822 if let Some((td, alias)) = ctx
12826 && (f.module.is_none() || f.module.as_deref() == Some("std"))
12827 && f.name == "contains"
12828 && f.args.len() == 2
12829 && let Expr::Path(p) = &f.args[0]
12830 && p.partial
12831 && p.steps.len() >= 2
12832 && let ast::PathStep::Name(ln) = &p.steps[0]
12833 && Self::resolve_multilink(td, ln).is_some()
12834 {
12835 let synthetic = ast::BinOp {
12836 left: f.args[0].clone(),
12837 op: ast::BinOpKind::Eq,
12838 right: f.args[1].clone(),
12839 };
12840 if let Some(exists) = self.try_multilink_exists(&synthetic, td, alias)? {
12841 return Ok(exists);
12842 }
12843 }
12844
12845 if f.args.len() == 1
12851 && f.kwargs.is_empty()
12852 && let Some(mut ps) = self.compile_shape_field_select(&f.args[0], ctx)?
12853 {
12854 let IrPathResult::Scalar(column, _) = ps.result else {
12855 return Err(self.type_err("a shape's pointer read off a walk is a value"));
12856 };
12857 let ns = f.module.as_deref().unwrap_or("std");
12858 let aggregate =
12859 crate::stdlib::lookup(ns, &f.name)
12860 .into_iter()
12861 .find_map(|d| match &d.impl_strategy {
12862 crate::stdlib::ImplStrategy::SqlBuiltin(sql_name) if d.is_aggregate() => {
12863 Some(sql_name.to_string())
12864 }
12865 _ => None,
12866 });
12867 let Some(sql_name) = aggregate else {
12868 return Err(self.type_err(&format!(
12869 "'{}' over a shape's pointer read off a walk needs an aggregate",
12870 f.name
12871 )));
12872 };
12873 let over_nothing = aggregate_over_nothing_sql(&f.name);
12876 let subquery = if matches!(&column, IrExpr::FunctionCall(f) if f.name == "unnest" && f.sql_template.is_none())
12879 {
12880 ps.result = IrPathResult::Scalar(column, None);
12881 IrExpr::FunctionCall(IrFunctionCall {
12882 return_pg_type: None,
12883 schema: None,
12884 name: sql_name.clone(),
12885 args: vec![IrExpr::ArrayFromSelect(Box::new(IrArraySource::PathSelect(Box::new(
12886 ps,
12887 ))))],
12888 sql_template: Some(format!(
12889 "(SELECT {sql_name}(\"_s\".\"v\") FROM unnest($1) AS \"_s\"(\"v\"))"
12890 )),
12891 })
12892 } else {
12893 let aggregate = IrExpr::FunctionCall(IrFunctionCall {
12894 return_pg_type: None,
12895 schema: None,
12896 name: sql_name,
12897 args: vec![column],
12898 sql_template: None,
12899 });
12900 ps.result = IrPathResult::Scalar(aggregate, None);
12901 IrExpr::PathSubquery(Box::new(ps))
12902 };
12903 return Ok(match over_nothing {
12904 Some(value) => IrExpr::FunctionCall(IrFunctionCall {
12905 return_pg_type: None,
12906 schema: None,
12907 name: "coalesce".to_string(),
12908 args: vec![subquery, IrExpr::RawSql(value.to_string())],
12909 sql_template: None,
12910 }),
12911 None => subquery,
12912 });
12913 }
12914 if f.args.len() == 1 {
12915 let arg = &f.args[0];
12916 if matches!(arg, Expr::Intersect(_, _) | Expr::Except(_, _)) {
12919 use crate::stdlib::{ImplStrategy, lookup};
12920 let ns = f.module.as_deref().unwrap_or("std");
12921 let best = lookup(ns, &f.name)
12922 .iter()
12923 .find(|d| d.params.len() == 1)
12924 .map(|d| d.impl_strategy.clone());
12925 if let Some(ImplStrategy::SqlBuiltin(sql_name)) = best
12926 && let IrExpr::SetOp { op, left, right, .. } = self.compile_expr_ctx(arg, ctx)?
12927 {
12928 return Ok(IrExpr::SetOp {
12929 op,
12930 left,
12931 right,
12932 mode: super::SetOpMode::Aggregate(sql_name.to_string()),
12933 });
12934 }
12935 }
12936 if let Expr::FunctionCall(unpack) = arg
12941 && unpack.module.as_deref().unwrap_or("std") == "std"
12942 && unpack.name == "array_unpack"
12943 && unpack.kwargs.is_empty()
12944 && let [array] = unpack.args.as_slice()
12945 && let Some(sql_name) = crate::stdlib::lookup(f.module.as_deref().unwrap_or("std"), &f.name)
12946 .into_iter()
12947 .find_map(|d| match &d.impl_strategy {
12948 crate::stdlib::ImplStrategy::SqlBuiltin(sql_name) if d.is_aggregate() => {
12949 Some(sql_name.to_string())
12950 }
12951 _ => None,
12952 })
12953 {
12954 let values = self.compile_expr_ctx(array, ctx)?;
12955 if !yields_array(&values) {
12959 let over_nothing = aggregate_over_nothing_sql(&f.name).unwrap_or("NULL");
12960 return Ok(IrExpr::FunctionCall(IrFunctionCall {
12961 return_pg_type: None,
12962 schema: None,
12963 name: sql_name.clone(),
12964 args: vec![values],
12965 sql_template: Some(format!(
12966 "(SELECT coalesce({sql_name}(\"_s\".\"v\"), {over_nothing}) FROM unnest($1) AS \"_s\"(\"v\"))"
12967 )),
12968 }));
12969 }
12970 }
12971 if matches!(arg, Expr::FieldAccess { .. })
12975 && matches!(Self::peel_field_access_chain(arg).0, Expr::SubQuery(_))
12976 {
12977 let ns = f.module.as_deref().unwrap_or("std");
12978 let aggregate =
12979 crate::stdlib::lookup(ns, &f.name)
12980 .into_iter()
12981 .find_map(|d| match &d.impl_strategy {
12982 crate::stdlib::ImplStrategy::SqlBuiltin(sql_name) if d.is_aggregate() => {
12983 Some(sql_name.to_string())
12984 }
12985 _ => None,
12986 });
12987 if let Some(sql_name) = aggregate {
12988 let values = self.compile_expr_ctx(arg, ctx)?;
12989 if matches!(values, IrExpr::ArrayFromSelect(_)) {
12990 let over_nothing = aggregate_over_nothing_sql(&f.name).unwrap_or("NULL");
12991 return Ok(IrExpr::FunctionCall(IrFunctionCall {
12992 return_pg_type: None,
12993 schema: None,
12994 name: sql_name.clone(),
12995 args: vec![values],
12996 sql_template: Some(format!(
12997 "(SELECT coalesce({sql_name}(\"_s\".\"v\"), {over_nothing}) FROM unnest($1) AS \"_s\"(\"v\"))"
12998 )),
12999 }));
13000 }
13001 return Ok(IrExpr::FunctionCall(IrFunctionCall {
13002 return_pg_type: None,
13003 schema: None,
13004 name: sql_name,
13005 args: vec![values],
13006 sql_template: None,
13007 }));
13008 }
13009 }
13010 if let Expr::BinOp(b) = arg
13014 && b.op == ast::BinOpKind::Coalesce
13015 {
13016 let values = self.compile_expr_ctx(arg, ctx)?;
13017 let ns = f.module.as_deref().unwrap_or("std");
13018 let aggregate =
13019 crate::stdlib::lookup(ns, &f.name)
13020 .into_iter()
13021 .find_map(|d| match &d.impl_strategy {
13022 crate::stdlib::ImplStrategy::SqlBuiltin(sql_name) if d.is_aggregate() => {
13023 Some(sql_name.to_string())
13024 }
13025 _ => None,
13026 });
13027 if let Some(sql_name) = aggregate
13028 && yields_array(&values)
13029 {
13030 let over_nothing = aggregate_over_nothing_sql(&f.name).unwrap_or("NULL");
13031 return Ok(IrExpr::FunctionCall(IrFunctionCall {
13032 return_pg_type: None,
13033 schema: None,
13034 name: sql_name.clone(),
13035 args: vec![values],
13036 sql_template: Some(format!(
13037 "(SELECT coalesce({sql_name}(\"_s\".\"v\"), {over_nothing}) FROM unnest($1) AS \"_s\"(\"v\"))"
13038 )),
13039 }));
13040 }
13041 return self.resolve_fn_call(f.module.as_deref(), &f.name, vec![values]);
13042 }
13043 if let Some(name) = self.resolve_cte_name(arg) {
13048 use crate::stdlib::{ImplStrategy, lookup};
13049 let ns = f.module.as_deref().unwrap_or("std");
13050 let overloads = lookup(ns, &f.name);
13051 let best = overloads
13052 .iter()
13053 .find(|d| d.params.len() == 1)
13054 .or_else(|| overloads.first());
13055 if let Some(ImplStrategy::SqlBuiltin(sql_name)) = best.map(|d| &d.impl_strategy)
13056 && best.is_some_and(|d| d.params.first().is_some_and(|p| p.ty.is_set()))
13057 {
13058 let object = self.cte_types.get(name).is_some_and(|bound| bound.contains("::"));
13059 return Ok(IrExpr::AggOverCte {
13060 fn_name: sql_name.to_string(),
13061 cte: name.to_string(),
13062 column: (!object).then(|| "v".to_string()),
13063 });
13064 }
13065 }
13066 if let Some((td, alias)) = ctx {
13067 if let Expr::SubQuery(stmt) = arg
13071 && f.name != "array_agg"
13072 && let Some(sql_name) = crate::stdlib::lookup(f.module.as_deref().unwrap_or("std"), &f.name)
13073 .into_iter()
13074 .find_map(|d| match &d.impl_strategy {
13075 crate::stdlib::ImplStrategy::SqlBuiltin(sql_name) if d.is_aggregate() => {
13076 Some(sql_name.to_string())
13077 }
13078 _ => None,
13079 })
13080 && let Some(mut ps) = self.relative_subselect(stmt, ctx)?
13081 {
13082 if let IrPathResult::Object { alias, .. } = &ps.result {
13084 let id = IrExpr::ColumnRef {
13085 alias: alias.clone(),
13086 column: "id".to_string(),
13087 pg_type: "uuid".to_string(),
13088 };
13089 ps.result = IrPathResult::Scalar(id, None);
13090 }
13091 let aggregate = IrExpr::FunctionCall(IrFunctionCall {
13092 return_pg_type: None,
13093 schema: None,
13094 name: sql_name.clone(),
13095 args: vec![IrExpr::ArrayFromSelect(Box::new(IrArraySource::PathSelect(Box::new(
13096 ps,
13097 ))))],
13098 sql_template: Some(format!(
13099 "(SELECT {sql_name}(\"_s\".\"v\") FROM unnest($1) AS \"_s\"(\"v\"))"
13100 )),
13101 });
13102 return Ok(aggregate_over_nothing(&f.name, aggregate));
13103 }
13104 if let Expr::Path(p) = arg
13110 && !p.partial
13111 && p.steps.len() > 1
13112 && let Some(root) = self.find_path_root_in_expr(arg)
13113 {
13114 let synthetic = ast::SelectStmt {
13115 result: Expr::FunctionCall(f.clone()),
13116 filter: None,
13117 order_by: vec![],
13118 offset: None,
13119 limit: None,
13120 lock: None,
13121 };
13122 let ps = self.compile_expr_as_path_select(&synthetic, &synthetic.result, &root, false)?;
13123 return Ok(aggregate_over_nothing(&f.name, IrExpr::PathSubquery(Box::new(ps))));
13124 }
13125 if let Expr::Path(p) = arg
13138 && p.partial
13139 && (p.steps.len() > 1 && self.path_crosses_multi(td, &p.steps)
13140 || matches!(p.steps.first(), Some(ast::PathStep::Backlink(_)))
13141 || matches!(p.steps.as_slice(), [ast::PathStep::Name(name)]
13142 if Self::resolve_multilink(td, name).is_none()
13143 && self.path_crosses_multi(td, &p.steps)))
13144 {
13145 let mut steps = vec![ast::PathStep::Name(format!("{}::{}", td.module, td.name))];
13146 steps.extend(p.steps.iter().cloned());
13147 let rooted = ast::Path { steps, partial: false };
13148 let mut call = f.clone();
13149 call.args[0] = Expr::Path(rooted);
13150 let synthetic = ast::SelectStmt {
13151 result: Expr::FunctionCall(call),
13152 filter: None,
13153 order_by: vec![],
13154 offset: None,
13155 limit: None,
13156 lock: None,
13157 };
13158 let root = format!("{}::{}", td.module, td.name);
13159 let mut ps =
13160 self.compile_expr_as_path_select(&synthetic, &synthetic.result, &root, false)?;
13161 Self::correlate_path_select(&mut ps, alias);
13162 return Ok(aggregate_over_nothing(&f.name, IrExpr::PathSubquery(Box::new(ps))));
13163 }
13164 if let Expr::Path(p) = arg
13165 && p.partial
13166 && p.steps.len() == 1
13167 && let ast::PathStep::Name(ml_name) = &p.steps[0]
13168 && Self::resolve_multilink(td, ml_name).is_some()
13169 {
13170 use crate::stdlib::{ImplStrategy, lookup};
13171 let ns = f.module.as_deref().unwrap_or("std");
13172 let overloads = lookup(ns, &f.name);
13173 let best = overloads
13174 .iter()
13175 .find(|d| d.params.len() == 1)
13176 .or_else(|| overloads.first());
13177 if let Some(ImplStrategy::SqlBuiltin(sql_name)) = best.map(|d| &d.impl_strategy) {
13178 let fn_name = sql_name.to_string();
13179 let inner = self.multilink_correlation_select(ml_name, td, alias)?;
13180 return Ok(IrExpr::AggOverQuery {
13181 fn_name,
13182 inner: Box::new(inner),
13183 });
13184 }
13185 }
13186 } else {
13187 if let Expr::Path(p) = arg
13195 && !p.partial
13196 && p.steps.len() > 1
13197 && let Some(root) = self.find_path_root_in_expr(arg)
13198 {
13199 let synthetic = ast::SelectStmt {
13200 result: Expr::FunctionCall(f.clone()),
13201 filter: None,
13202 order_by: vec![],
13203 offset: None,
13204 limit: None,
13205 lock: None,
13206 };
13207 let ps = self.compile_expr_as_path_select(&synthetic, &synthetic.result, &root, false)?;
13208 return Ok(aggregate_over_nothing(&f.name, IrExpr::PathSubquery(Box::new(ps))));
13209 }
13210 if let Expr::SubQuery(stmt) = arg
13215 && matches!(stmt.as_ref(), Stmt::Insert(_) | Stmt::Update(_) | Stmt::Delete(_))
13216 {
13217 use crate::stdlib::{ImplStrategy, lookup};
13218 let ns = f.module.as_deref().unwrap_or("std");
13219 let overloads = lookup(ns, &f.name);
13220 let best = overloads
13221 .iter()
13222 .find(|d| d.params.len() == 1)
13223 .or_else(|| overloads.first())
13224 .cloned();
13225 if let Some(d) = best
13226 && let ImplStrategy::SqlBuiltin(sql_name) = &d.impl_strategy
13227 {
13228 let fn_name = sql_name.to_string();
13229 let (cte_name, type_name) = self.hoist_dml_as_cte(stmt.as_ref())?;
13230 let td = self.resolve_type(&type_name)?;
13231 let source = IrSource {
13232 poly: None,
13233 type_name: format!("{}::{}", td.module, td.name),
13234 table: self.cte_source_table(&cte_name),
13235 alias: self.fresh_alias(),
13236 };
13237 let inner = IrSelect::schema_bound(source, Self::pk_returning(td), None);
13238 return Ok(IrExpr::AggOverQuery {
13239 fn_name,
13240 inner: Box::new(inner),
13241 });
13242 }
13243 }
13244 let inner_sel: Option<ast::SelectStmt> = match arg {
13245 Expr::Path(p) if !p.partial => {
13246 let qname = p
13248 .steps
13249 .iter()
13250 .filter_map(|s| {
13251 if let ast::PathStep::Name(n) = s {
13252 Some(n.as_str())
13253 } else {
13254 None
13255 }
13256 })
13257 .collect::<Vec<_>>()
13258 .join("::");
13259 let is_schema_type = self
13260 .schema
13261 .types
13262 .iter()
13263 .any(|t| format!("{}::{}", t.module, t.name) == qname || t.name == qname);
13264 if is_schema_type {
13265 Some(ast::SelectStmt {
13266 result: arg.clone(),
13267 filter: None,
13268 order_by: vec![],
13269 offset: None,
13270 limit: None,
13271 lock: None,
13272 })
13273 } else {
13274 None
13275 }
13276 }
13277 Expr::SubQuery(stmt) => {
13278 if let ast::Stmt::Select(inner) = stmt.as_ref() {
13279 Some(inner.clone())
13280 } else {
13281 None
13282 }
13283 }
13284 _ => None,
13285 };
13286 if let Some(sel) = inner_sel {
13287 use crate::stdlib::{ImplStrategy, lookup};
13288 let ns = f.module.as_deref().unwrap_or("std");
13289 let overloads = lookup(ns, &f.name);
13290 let best = overloads
13291 .iter()
13292 .find(|d| d.params.len() == 1)
13293 .or_else(|| overloads.first());
13294 let (inner_result, inner_distinct) = match &sel.result {
13304 Expr::UnaryOp(u) if matches!(u.op, ast::UnaryOpKind::Distinct) => (&u.operand, true),
13305 other => (other, false),
13306 };
13307 if let Expr::Path(inner_path) = inner_result
13308 && !inner_path.partial
13309 && inner_path.steps.len() > 1
13310 && let Some(ast::PathStep::Name(root)) = inner_path.steps.first()
13311 && let Ok(root_td) = self.resolve_path_root(root)
13312 && self
13313 .walk_path_types(root_td, &inner_path.steps[1..], MAX_COMPUTED_SPLICES)
13314 .1
13315 .is_none()
13316 {
13317 let root = root.clone();
13318 let mut inner_call = f.clone();
13319 inner_call.args = vec![if inner_distinct {
13320 Expr::UnaryOp(Box::new(ast::UnaryOp {
13321 op: ast::UnaryOpKind::Distinct,
13322 operand: inner_result.clone(),
13323 }))
13324 } else {
13325 inner_result.clone()
13326 }];
13327 let call = ast::SelectStmt {
13328 result: Expr::FunctionCall(inner_call),
13329 ..sel.clone()
13330 };
13331 let ps = self.compile_expr_as_path_select(&call, &call.result, &root, false)?;
13332 return Ok(IrExpr::PathSubquery(Box::new(ps)));
13333 }
13334 if let Some(d) = best
13335 && let ImplStrategy::SqlBuiltin(sql_name) = &d.impl_strategy
13336 {
13337 let fn_name = sql_name.to_string();
13338 let inner_ir = self.compile_select(&sel, &sel.result, false)?;
13339 if fn_name == "array_agg"
13344 && matches!(inner_ir.rows.as_slice(), [IrRowSource::Bound { .. }])
13345 {
13346 return Ok(IrExpr::ArrayFromSelect(Box::new(IrArraySource::ObjectSelect(
13347 Box::new(inner_ir),
13348 ))));
13349 }
13350 return Ok(IrExpr::AggOverQuery {
13351 fn_name,
13352 inner: Box::new(inner_ir),
13353 });
13354 }
13355 }
13356 }
13357 }
13358
13359 if let Some(operands) = f.args.iter().find_map(Self::set_operands) {
13367 use crate::stdlib::{ImplStrategy, lookup};
13368 let ns = f.module.as_deref().unwrap_or("std");
13369 let overloads = lookup(ns, &f.name);
13370 let best = overloads
13371 .iter()
13372 .find(|d| d.params.len() == f.args.len())
13373 .or_else(|| overloads.first());
13374 let (schema, fn_name) = match best.map(|d| &d.impl_strategy) {
13375 Some(ImplStrategy::SqlBuiltin(sql_name)) => (None, sql_name.to_string()),
13376 Some(_) => {
13377 return Err(self.type_err(&format!(
13378 "function '{}::{}' cannot be called with a set literal in this context",
13379 ns, f.name
13380 )));
13381 }
13382 None => return Err(self.type_err(&format!("function '{}::{}' does not exist", ns, f.name))),
13383 };
13384 let elems = operands
13385 .iter()
13386 .map(|e| self.compile_expr_ctx(e, ctx))
13387 .collect::<Result<Vec<_>, _>>()?;
13388 return Ok(IrExpr::AggOverSet { fn_name, schema, elems });
13389 }
13390
13391 if let Some((td, _)) = ctx
13400 && f.module.as_deref().unwrap_or("std") == "std"
13401 && matches!(f.name.as_str(), "any" | "all")
13402 && f.kwargs.is_empty()
13403 && let [condition] = f.args.as_slice()
13404 && let Some(per_element) = per_element_of_one_multilink(condition, td)
13405 {
13406 let (link, element_condition) = per_element;
13407 let condition = if f.name == "all" {
13408 Expr::UnaryOp(Box::new(ast::UnaryOp {
13409 op: ast::UnaryOpKind::Not,
13410 operand: element_condition,
13411 }))
13412 } else {
13413 element_condition
13414 };
13415 let exists = Expr::UnaryOp(Box::new(ast::UnaryOp {
13416 op: ast::UnaryOpKind::Exists,
13417 operand: Expr::SubQuery(Box::new(Stmt::Select(ast::SelectStmt {
13418 result: Expr::Path(ast::Path {
13419 steps: link,
13420 partial: true,
13421 }),
13422 filter: Some(condition),
13423 order_by: vec![],
13424 offset: None,
13425 limit: None,
13426 lock: None,
13427 }))),
13428 }));
13429 let exists = if f.name == "all" {
13430 Expr::UnaryOp(Box::new(ast::UnaryOp {
13431 op: ast::UnaryOpKind::Not,
13432 operand: exists,
13433 }))
13434 } else {
13435 exists
13436 };
13437 return self.compile_expr_ctx(&exists, ctx);
13438 }
13439 if f.module.as_deref().unwrap_or("std") == "std"
13442 && matches!(f.name.as_str(), "any" | "all")
13443 && f.kwargs.is_empty()
13444 && let [Expr::TypeIs { expr: subject, ty }] = f.args.as_slice()
13445 && let answers @ IrExpr::ArrayFromSelect(_) = self.compile_type_is_set(subject, ty, ctx)?
13446 {
13447 let (aggregate, over_nothing) = if f.name == "all" {
13448 ("bool_and", "true")
13449 } else {
13450 ("bool_or", "false")
13451 };
13452 return Ok(IrExpr::FunctionCall(IrFunctionCall {
13453 return_pg_type: None,
13454 schema: None,
13455 name: aggregate.to_string(),
13456 args: vec![answers],
13457 sql_template: Some(format!(
13458 "(SELECT coalesce({aggregate}(\"_unnested\".\"v\"), {over_nothing}) FROM unnest($1) AS \"_unnested\"(\"v\"))"
13459 )),
13460 }));
13461 }
13462 if f.module.as_deref().unwrap_or("std") == "std"
13467 && matches!(f.name.as_str(), "any" | "all")
13468 && f.kwargs.is_empty()
13469 && let [Expr::BinOp(comparison)] = f.args.as_slice()
13470 && let Expr::FunctionCall(unpack) = &comparison.left
13471 && unpack.module.as_deref().unwrap_or("std") == "std"
13472 && unpack.name == "array_unpack"
13473 && let [array] = unpack.args.as_slice()
13474 {
13475 let array = self.compile_expr_ctx(array, ctx)?;
13476 let element = "<unnested element>".to_string();
13477 self.inline_bindings
13478 .insert(element.clone(), IrExpr::RawSql("\"_unnested\".\"v\"".to_string()));
13479 let per_element = self.compile_expr_ctx(
13480 &Expr::BinOp(Box::new(ast::BinOp {
13481 left: Expr::Path(ast::Path::absolute(element.clone())),
13482 op: comparison.op.clone(),
13483 right: comparison.right.clone(),
13484 })),
13485 ctx,
13486 );
13487 self.inline_bindings.remove(&element);
13488 let (aggregate, over_nothing) = if f.name == "all" {
13489 ("bool_and", "true")
13490 } else {
13491 ("bool_or", "false")
13492 };
13493 return Ok(IrExpr::FunctionCall(IrFunctionCall {
13494 return_pg_type: None,
13495 schema: None,
13496 name: aggregate.to_string(),
13497 args: vec![per_element?, array],
13498 sql_template: Some(format!(
13499 "(SELECT coalesce({aggregate}($1), {over_nothing}) FROM unnest($2) AS \"_unnested\"(\"v\"))"
13500 )),
13501 }));
13502 }
13503 if let Some(args) = self.compile_named_call_args(f, ctx)? {
13504 return self.resolve_fn_call(f.module.as_deref(), &f.name, args);
13505 }
13506 let is_explicit_set = f.module.as_deref().unwrap_or("std") == "std"
13507 && matches!(f.name.as_str(), "any" | "all")
13508 && f.args.len() == 1;
13509 let is_universal = is_explicit_set && f.name == "all";
13510 if is_explicit_set {
13511 self.explicit_set_depth += 1;
13512 }
13513 if is_universal {
13514 self.universal_set_depth += 1;
13515 }
13516 let args = f
13517 .args
13518 .iter()
13519 .map(|a| self.compile_expr_ctx(a, ctx))
13520 .collect::<Result<Vec<_>, _>>();
13521 if is_universal {
13522 self.universal_set_depth -= 1;
13523 }
13524 if is_explicit_set {
13525 self.explicit_set_depth -= 1;
13526 }
13527 let args = args?;
13528 if is_explicit_set
13534 && let [values] = args.as_slice()
13535 && yields_array(values)
13536 {
13537 let (aggregate, over_nothing) = if f.name == "all" {
13538 ("bool_and", "true")
13539 } else {
13540 ("bool_or", "false")
13541 };
13542 return Ok(IrExpr::FunctionCall(IrFunctionCall {
13543 return_pg_type: None,
13544 schema: None,
13545 name: aggregate.to_string(),
13546 args: vec![values.clone()],
13547 sql_template: Some(format!(
13548 "(SELECT coalesce({aggregate}(\"_s\".\"v\"), {over_nothing}) FROM unnest($1) AS \"_s\"(\"v\"))"
13549 )),
13550 }));
13551 }
13552 self.resolve_fn_call(f.module.as_deref(), &f.name, args)
13553 }
13554
13555 Expr::UnaryOp(u) if u.op == ast::UnaryOpKind::Exists => self.compile_exists_ctx(&u.operand, ctx),
13556
13557 Expr::UnaryOp(u) if u.op == ast::UnaryOpKind::Distinct => {
13563 let operand = self.compile_expr_ctx(&u.operand, ctx)?;
13564 Ok(match operand {
13565 IrExpr::PathSubquery(mut ps) => {
13566 ps.distinct = true;
13567 IrExpr::PathSubquery(ps)
13568 }
13569 IrExpr::ArrayFromSelect(src) => IrExpr::ArrayFromSelect(Box::new(match *src {
13570 IrArraySource::Select(mut sel) => {
13571 sel.distinct = true;
13572 IrArraySource::Select(sel)
13573 }
13574 IrArraySource::PathSelect(mut ps) => {
13575 ps.distinct = true;
13576 IrArraySource::PathSelect(ps)
13577 }
13578 other => other,
13579 })),
13580 other => other,
13581 })
13582 }
13583
13584 Expr::UnaryOp(u) => {
13585 let operand = self.compile_expr_ctx(&u.operand, ctx)?;
13586 Ok(IrExpr::UnaryOp(Box::new(IrUnaryOp {
13587 op: u.op.clone(),
13588 operand,
13589 })))
13590 }
13591
13592 Expr::IfElse(ie) => {
13593 let condition = self.compile_expr_ctx(&ie.condition, ctx)?;
13594 let if_ = self.compile_expr_ctx(&ie.if_expr, ctx)?;
13595 let else_ = self.compile_expr_ctx(&ie.else_expr, ctx)?;
13596 Ok(IrExpr::IfElse(Box::new(IrIfElse { condition, if_, else_ })))
13597 }
13598
13599 Expr::Array(elems) => {
13600 let items = elems
13604 .iter()
13605 .map(|e| self.compile_expr_ctx(e, ctx).map(set_walk_as_scalar))
13606 .collect::<Result<Vec<_>, _>>()?;
13607 Ok(IrExpr::Array(items))
13608 }
13609
13610 Expr::NamedTuple(fields) => {
13611 let ir = fields
13612 .iter()
13613 .map(|(name, e)| Ok((name.clone(), self.compile_expr_ctx(e, ctx)?)))
13614 .collect::<Result<Vec<_>, PyQLError>>()?;
13615 Ok(IrExpr::NamedTuple {
13616 fields: ir,
13617 is_free_object: false,
13618 })
13619 }
13620
13621 Expr::Tuple(elems) => {
13622 let ir = elems
13623 .iter()
13624 .map(|e| self.compile_expr_ctx(e, ctx))
13625 .collect::<Result<Vec<_>, PyQLError>>()?;
13626 Ok(IrExpr::Tuple(ir))
13627 }
13628
13629 Expr::FieldAccess { expr: inner, field } => {
13630 if let Some(ir) = self.try_compile_walk_off_subquery(expr, ctx)? {
13633 return Ok(ir);
13634 }
13635 if let Expr::NamedTuple(fields) = inner.as_ref() {
13636 let (_, val) = fields.iter().find(|(k, _)| k == field).ok_or_else(|| {
13637 self.type_err(&format!("{field} is not a member of {}", named_tuple_type_str(fields)))
13638 })?;
13639 return self.compile_expr_ctx(val, ctx);
13640 }
13641 if let Some(ps) = self.compile_shape_field_select(expr, ctx)? {
13642 return Ok(IrExpr::ArrayFromSelect(Box::new(IrArraySource::PathSelect(Box::new(
13643 ps,
13644 )))));
13645 }
13646 let (base, fields) = Self::peel_field_access_chain(expr);
13651 if let Expr::SubQuery(stmt) = base {
13652 let stmt = stmt.as_ref().clone();
13653 return self.compile_subquery_expr(&stmt, &fields, ctx, &[]);
13654 }
13655 if let Expr::FunctionCall(fc) = base {
13658 let fc = fc.clone();
13659 if let Some(ir) = self.try_compile_fn_scalar_subquery(&fc, &fields, None, ctx)? {
13660 return Ok(ir);
13661 }
13662 }
13663 let ir = self.compile_expr_ctx(inner, ctx)?;
13664 Ok(Self::project_free_object_field(ir, field))
13665 }
13666
13667 Expr::TupleIndex { expr: inner, index } => {
13668 match inner.as_ref() {
13669 Expr::Tuple(elems) => {
13670 let elem = elems.get(*index).ok_or_else(|| {
13671 self.type_err(&format!(
13672 "{index} is not a member of {}",
13673 positional_tuple_type_str(elems)
13674 ))
13675 })?;
13676 self.compile_expr_ctx(elem, ctx)
13677 }
13678 Expr::NamedTuple(fields) => {
13679 let (_, val) = fields.get(*index).ok_or_else(|| {
13680 self.type_err(&format!("{index} is not a member of {}", named_tuple_type_str(fields)))
13681 })?;
13682 self.compile_expr_ctx(val, ctx)
13683 }
13684 _ => {
13690 if let Expr::TypeCast(tc) = inner.as_ref()
13691 && let Some(shape) = self.resolve_tuple_cast_shape(&tc.ty)
13692 && *index >= shape.members.len()
13693 {
13694 return Err(self.type_err(&format!(
13695 "{index} is not a member of {}",
13696 self.type_expr_to_display_str(&tc.ty)
13697 )));
13698 }
13699 let ir = self.compile_expr_ctx(inner, ctx)?;
13700 Ok(IrExpr::JsonbIndex {
13701 expr: Box::new(ir),
13702 index: *index,
13703 })
13704 }
13705 }
13706 }
13707
13708 Expr::Detached(inner) => {
13718 if ctx.is_some()
13719 && let Some(root) = self.find_path_root_in_expr(inner)
13720 {
13721 let synthetic = ast::SelectStmt {
13722 result: (**inner).clone(),
13723 filter: None,
13724 order_by: vec![],
13725 offset: None,
13726 limit: None,
13727 lock: None,
13728 };
13729 let ps = self.compile_expr_as_path_select(&synthetic, inner, &root, false)?;
13730 return Ok(IrExpr::PathSubquery(Box::new(ps)));
13731 }
13732 self.compile_expr_ctx(inner, None)
13733 }
13734
13735 Expr::Set(elems) if ctx.is_none() && elems.is_empty() => Ok(IrExpr::Null),
13741
13742 Expr::Set(elems) if ctx.is_none() => {
13743 let compiled: Result<Vec<_>, _> = elems.iter().map(|e| self.compile_expr_ctx(e, ctx)).collect();
13744 let mut compiled = compiled?;
13745 if compiled.len() == 1 {
13746 Ok(compiled.remove(0))
13747 } else {
13748 Err(self.type_err("multi-element set literal is not supported in free SELECT context"))
13749 }
13750 }
13751
13752 Expr::Shape(s) if s.expr.is_none() => {
13766 let fields = s
13767 .elements
13768 .iter()
13769 .map(|el| -> Result<(String, IrExpr), PyQLError> {
13770 let name = path_leaf(&el.path)?.to_string();
13771 let expr = el.compexpr.as_ref().ok_or_else(|| {
13772 self.type_err("free object field must have a value expression (':= expr')")
13773 })?;
13774 let compiled = match expr {
13775 Expr::SubQuery(stmt)
13776 if matches!(stmt.as_ref(), Stmt::Insert(_) | Stmt::Update(_) | Stmt::Delete(_)) =>
13777 {
13778 self.dml_as_value(stmt.as_ref())?
13779 }
13780 other => match self.free_object_link_field(other)? {
13781 Some(object) => object,
13782 None => self.compile_expr_ctx(other, ctx)?,
13783 },
13784 };
13785 Ok((name, compiled))
13786 })
13787 .collect::<Result<Vec<_>, _>>()?;
13788 Ok(IrExpr::NamedTuple {
13789 fields,
13790 is_free_object: true,
13791 })
13792 }
13793
13794 Expr::Shape(s) if matches!(&s.expr, Some(inner) if self.is_free_cte_ref(inner)) => {
13802 let Some(Expr::Path(root_path)) = &s.expr else {
13803 unreachable!()
13804 };
13805 let ast::PathStep::Name(root) = &root_path.steps[0] else {
13806 unreachable!()
13807 };
13808 let fields = s
13809 .elements
13810 .iter()
13811 .map(|el| -> Result<(String, IrExpr), PyQLError> {
13812 let name = path_leaf(&el.path)?.to_string();
13813 let expr = match &el.compexpr {
13814 Some(over) => self.compile_expr_ctx(over, ctx)?,
13815 None => match self.resolve_cte_field_chain(root, &[name.as_str()]) {
13816 Some(result) => result?,
13817 None => {
13818 return Err(self.type_err(&format!("free object '{root}' has no field '{name}'")));
13819 }
13820 },
13821 };
13822 Ok((name, expr))
13823 })
13824 .collect::<Result<Vec<_>, _>>()?;
13825 Ok(IrExpr::NamedTuple {
13826 fields,
13827 is_free_object: true,
13828 })
13829 }
13830
13831 Expr::Shape(sh)
13835 if ctx.is_some()
13836 && matches!(sh.expr.as_ref(), Some(Expr::Union(_, _)))
13837 && Self::union_of_relative_paths(sh.expr.as_ref().expect("checked")).is_some() =>
13838 {
13839 let (td, alias) = ctx.expect("checked by the guard");
13840 let operands =
13841 Self::union_of_relative_paths(sh.expr.as_ref().expect("checked")).expect("checked by the guard");
13842 let elements = sh.elements.clone();
13843 let multi = self.relative_paths_reach_many(td, &operands);
13844 let mut branches = Vec::with_capacity(operands.len());
13845 for path in operands {
13846 let mut steps = vec![ast::PathStep::Name(format!("{}::{}", td.module, td.name))];
13847 steps.extend(path.steps.iter().cloned());
13848 let rooted = ast::Path { steps, partial: false };
13849 let synthetic = ast::SelectStmt {
13850 result: Expr::Path(rooted.clone()),
13851 filter: None,
13852 order_by: vec![],
13853 offset: None,
13854 limit: None,
13855 lock: None,
13856 };
13857 let mut ps = self.compile_path_select(&synthetic, &rooted, &elements, false)?;
13858 Self::correlate_path_select(&mut ps, alias);
13859 branches.push(ps);
13860 }
13861 Ok(IrExpr::ObjectPathUnion {
13862 branches,
13863 limit: None,
13864 multi,
13865 })
13866 }
13867
13868 Expr::Shape(sh)
13872 if ctx.is_some() && sh.expr.as_ref().and_then(Self::coalesce_of_relative_paths).is_some() =>
13873 {
13874 let (td, alias) = ctx.expect("checked by the guard");
13875 let operands =
13876 Self::coalesce_of_relative_paths(sh.expr.as_ref().expect("checked")).expect("checked by the guard");
13877 let multi = self.relative_paths_reach_many(td, &operands);
13878 let branches = self.coalesce_path_branches(td, alias, &operands, &sh.elements)?;
13879 Ok(IrExpr::ObjectPathUnion {
13880 branches,
13881 limit: None,
13882 multi,
13883 })
13884 }
13885
13886 Expr::Shape(sh) if matches!(sh.expr.as_ref(), Some(Expr::SubQuery(_))) => {
13887 let sh = sh.clone();
13888 match self.shape_over_subquery(&sh)? {
13889 Some(ir) => Ok(ir),
13890 None => Err(PyQLError::Type(PyQLTypeError {
13891 message: "shapes and set literals are not valid in expression context".into(),
13892 position: Position { line: 0, col: 0 },
13893 })),
13894 }
13895 }
13896
13897 Expr::Shape(sh) if Self::shape_over_subquery_projection(sh).is_some() => {
13898 let (stmt, fields) = Self::shape_over_subquery_projection(sh).expect("checked by the guard");
13899 let stmt = stmt.clone();
13900 let elements = sh.elements.clone();
13901 self.compile_subquery_expr(&stmt, &fields, ctx, &elements)
13902 }
13903
13904 Expr::Set(items) if items.is_empty() => Ok(IrExpr::Null),
13909
13910 Expr::Shape(sh)
13911 if matches!(sh.expr.as_ref(), Some(Expr::Path(p))
13912 if !p.partial
13913 && matches!(p.steps.first(), Some(ast::PathStep::Name(n))
13914 if self.cte_object_type(n).is_some() || self.for_var_types.contains_key(n))) =>
13915 {
13916 let expr = expr.clone();
13917 match self.shape_over_binding(&expr)? {
13918 Some(ir) => Ok(ir),
13919 None => Err(PyQLError::Type(PyQLTypeError {
13920 message: "shapes and set literals are not valid in expression context".into(),
13921 position: Position { line: 0, col: 0 },
13922 })),
13923 }
13924 }
13925
13926 Expr::Shape(_) | Expr::Set(_) => Err(PyQLError::Type(PyQLTypeError {
13932 message: "shapes and set literals are not valid in expression context".into(),
13933 position: Position { line: 0, col: 0 },
13934 })),
13935
13936 Expr::SubQuery(stmt) => {
13944 let stmt = stmt.as_ref().clone();
13945 self.compile_subquery_expr(&stmt, &[], ctx, &[])
13946 }
13947
13948 Expr::Union(_, _) => Err(PyQLError::Type(PyQLTypeError {
13949 message: "union is not valid in expression context".into(),
13950 position: Position { line: 0, col: 0 },
13951 })),
13952
13953 Expr::Except(left, right) | Expr::Intersect(left, right) => {
13957 let op = if matches!(expr, Expr::Intersect(_, _)) {
13958 super::SetOpKind::Intersect
13959 } else {
13960 super::SetOpKind::Except
13961 };
13962 let left = self.compile_expr_ctx(left, ctx)?;
13963 let right = self.compile_expr_ctx(right, ctx)?;
13964 Ok(IrExpr::SetOp {
13965 op,
13966 left: Box::new(left),
13967 right: Box::new(right),
13968 mode: super::SetOpMode::Array,
13969 })
13970 }
13971
13972 Expr::TypeIs { expr, ty } => match ctx {
13978 Some((td, alias)) => self.compile_type_is(expr, ty, td, alias),
13979 None => match expr.as_ref() {
13980 Expr::Path(p) => self.compile_path_type_is(p, ty, None),
13981 _ => Err(self.type_err("'is' type check is not valid in free SELECT context")),
13982 },
13983 },
13984 }
13985 }
13986
13987 fn compile_expr(&mut self, expr: &Expr, td: &TypeDescriptor, alias: &str) -> Result<IrExpr, PyQLError> {
13988 self.compile_expr_ctx(expr, Some((td, alias)))
13989 }
13990
13991 fn compile_free_expr(&mut self, expr: &Expr) -> Result<IrExpr, PyQLError> {
13992 self.compile_expr_ctx(expr, None)
13993 }
13994
13995 fn compile_path_type_is(
13998 &mut self,
13999 path: &ast::Path,
14000 ty: &ast::TypeExpr,
14001 ctx: Option<(&TypeDescriptor, &str)>,
14002 ) -> Result<IrExpr, PyQLError> {
14003 let (ty_module, ty_name) = ty
14004 .as_named()
14005 .ok_or_else(|| self.type_err("cannot use IS with a tuple or array type"))?;
14006 let check_qname = match (ty_module, ctx) {
14007 (Some(module), _) => format!("{module}::{ty_name}"),
14008 (None, Some((td, _))) => format!("{}::{}", td.module, ty_name),
14009 (None, None) => ty_name.to_string(),
14010 };
14011 let check_td = self.resolve_type(&check_qname)?;
14012 let check_qname = format!("{}::{}", check_td.module, check_td.name);
14013 let mut type_path = path.clone();
14014 type_path.steps.push(ast::PathStep::Name("__type__".to_string()));
14015 let own_type = self.compile_expr_ctx(&Expr::Path(type_path), ctx)?;
14016 let implementors: Vec<String> = self
14017 .find_poly_implementors(&check_qname)
14018 .into_iter()
14019 .map(|implementor| implementor.type_name)
14020 .collect();
14021 let is_one_of = |own_type: IrExpr| {
14022 implementors
14023 .iter()
14024 .map(|implementor| {
14025 IrExpr::BinOp(Box::new(IrBinOp {
14026 left: own_type.clone(),
14027 op: ast::BinOpKind::Eq,
14028 right: IrExpr::Literal(IrLiteral::Str(implementor.clone())),
14029 }))
14030 })
14031 .reduce(|left, right| {
14032 IrExpr::BinOp(Box::new(IrBinOp {
14033 left,
14034 op: ast::BinOpKind::Or,
14035 right,
14036 }))
14037 })
14038 .unwrap_or(IrExpr::Literal(IrLiteral::Bool(false)))
14039 };
14040 Ok(match own_type {
14042 IrExpr::ArrayFromSelect(source) => match *source {
14043 IrArraySource::PathSelect(mut path_select) => {
14044 let IrPathResult::Scalar(element, cast) = path_select.result else {
14045 return Err(self.type_err("'__type__' is read as a value"));
14046 };
14047 path_select.result = IrPathResult::Scalar(is_one_of(element), cast);
14048 IrExpr::ArrayFromSelect(Box::new(IrArraySource::PathSelect(path_select)))
14049 }
14050 other => is_one_of(IrExpr::ArrayFromSelect(Box::new(other))),
14051 },
14052 single => is_one_of(single),
14053 })
14054 }
14055
14056 fn compile_type_is(
14057 &mut self,
14058 expr: &Expr,
14059 ty: &ast::TypeExpr,
14060 td: &TypeDescriptor,
14061 alias: &str,
14062 ) -> Result<IrExpr, PyQLError> {
14063 self.compile_type_is_inner(expr, ty, td, alias, false)
14064 }
14065
14066 fn compile_type_is_set(
14072 &mut self,
14073 expr: &Expr,
14074 ty: &ast::TypeExpr,
14075 ctx: Option<(&TypeDescriptor, &str)>,
14076 ) -> Result<IrExpr, PyQLError> {
14077 match ctx {
14078 Some((td, alias)) => self.compile_type_is_inner(expr, ty, td, alias, true),
14079 None => match expr {
14080 Expr::Path(p) => self.compile_path_type_is(p, ty, None),
14081 _ => Err(self.type_err("'is' type check is not valid in free SELECT context")),
14082 },
14083 }
14084 }
14085
14086 fn compile_type_is_inner(
14087 &mut self,
14088 expr: &Expr,
14089 ty: &ast::TypeExpr,
14090 td: &TypeDescriptor,
14091 alias: &str,
14092 set_valued: bool,
14093 ) -> Result<IrExpr, PyQLError> {
14094 let self_qname = format!("{}::{}", td.module, td.name);
14095
14096 let reaches_other_objects = match expr {
14099 Expr::Path(p) if p.partial || p.steps.len() > 1 => {
14100 !matches!(p.steps.last(), Some(ast::PathStep::Name(n)) if n == "__type__")
14101 }
14102 Expr::Path(p) => matches!(
14103 p.steps.as_slice(),
14104 [ast::PathStep::Name(n)] if n != &td.name && n != &self_qname && self.resolve_type(n).is_err()
14105 && self.resolve_name_ref(n, true).is_some()
14106 ),
14107 _ => false,
14108 };
14109 if reaches_other_objects && let Expr::Path(p) = expr {
14110 return self.compile_path_type_is(p, ty, Some((td, alias)));
14111 }
14112
14113 let (source_qname, cross_scope) = match expr {
14116 Expr::Path(p) if !p.partial && p.steps.len() == 1 => {
14117 if let ast::PathStep::Name(n) = &p.steps[0] {
14118 if n == &td.name || n == &self_qname || self.scope_answers_to(n, &self_qname) {
14119 (self_qname.clone(), false)
14120 } else {
14121 match self.resolve_type(n) {
14123 Ok(other) => (format!("{}::{}", other.module, other.name), true),
14124 Err(_) => (self_qname.clone(), false),
14125 }
14126 }
14127 } else {
14128 (self_qname.clone(), false)
14129 }
14130 }
14131 _ => (self_qname.clone(), false),
14132 };
14133
14134 let (ty_module, ty_name) = ty
14135 .as_named()
14136 .ok_or_else(|| self.type_err("cannot use IS with a tuple or array type"))?;
14137 let check_module = ty_module.unwrap_or(td.module.as_str());
14138 let check_qname = format!("{}::{}", check_module, ty_name);
14139 self.resolve_type(&check_qname)?;
14140
14141 if !cross_scope {
14142 return Ok(self.type_check_bool_expr(&source_qname, &check_qname, td, alias));
14144 }
14145
14146 let src_alias = self.fresh_alias();
14149 let src_td = self.resolve_type(&source_qname)?;
14150 let source_table = src_td.table.clone();
14151 let (poly_implementors, poly_columns) = if self.is_polymorphic(src_td) {
14152 (
14153 self.find_poly_implementors(&source_qname),
14154 Self::poly_dml_columns(src_td),
14155 )
14156 } else {
14157 (vec![], vec![])
14158 };
14159 let bool_expr = self.type_check_bool_expr(&source_qname, &check_qname, src_td, &src_alias);
14160
14161 if !set_valued && matches!(bool_expr, IrExpr::Literal(IrLiteral::Bool(_))) {
14168 return Ok(bool_expr);
14169 }
14170
14171 let source = IrSource {
14172 poly: None,
14173 type_name: source_qname.clone(),
14174 table: source_table,
14175 alias: src_alias,
14176 };
14177
14178 let answers = IrExpr::ArrayFromSelect(Box::new(IrArraySource::RawExpr {
14179 source,
14180 poly_implementors,
14181 poly_columns,
14182 expr: bool_expr,
14183 }));
14184 if set_valued {
14185 return Ok(answers);
14186 }
14187 Ok(IrExpr::FunctionCall(IrFunctionCall {
14194 return_pg_type: None,
14195 schema: Some("_pylon".to_string()),
14196 name: "assert_single".to_string(),
14197 args: vec![
14198 answers,
14199 IrExpr::Literal(IrLiteral::Str(format!(
14200 "'{source_qname} is {check_qname}' is asked once for every {source_qname} object, \
14201 so it is not the single value a filter or an 'if' condition needs — narrow it to \
14202 one object, or name the type being selected"
14203 ))),
14204 ],
14205 sql_template: None,
14206 }))
14207 }
14208
14209 fn type_check_bool_expr(&self, source_qname: &str, check_qname: &str, td: &TypeDescriptor, alias: &str) -> IrExpr {
14211 if check_qname == source_qname || Self::is_or_implements(td, check_qname) {
14212 return IrExpr::Literal(IrLiteral::Bool(true));
14213 }
14214 if !self.is_polymorphic(td) {
14215 return IrExpr::Literal(IrLiteral::Bool(false));
14216 }
14217 let reachable: Vec<String> = self
14229 .find_poly_implementors(source_qname)
14230 .into_iter()
14231 .map(|implementor| implementor.type_name)
14232 .collect();
14233 let matching: Vec<String> = self
14234 .find_poly_implementors(check_qname)
14235 .into_iter()
14236 .map(|implementor| implementor.type_name)
14237 .filter(|type_name| reachable.contains(type_name))
14238 .collect();
14239 if matching.is_empty() {
14240 return IrExpr::Literal(IrLiteral::Bool(false));
14241 }
14242 if matching.len() == reachable.len() {
14243 return IrExpr::Literal(IrLiteral::Bool(true));
14244 }
14245 matching
14246 .into_iter()
14247 .map(|type_name| {
14248 IrExpr::BinOp(Box::new(IrBinOp {
14249 left: IrExpr::ColumnRef {
14250 alias: alias.to_string(),
14251 column: "__type__".into(),
14252 pg_type: "text".into(),
14253 },
14254 op: crate::parse::ast::BinOpKind::Eq,
14255 right: IrExpr::Literal(IrLiteral::Str(type_name)),
14256 }))
14257 })
14258 .reduce(|left, right| {
14259 IrExpr::BinOp(Box::new(IrBinOp {
14260 left,
14261 op: crate::parse::ast::BinOpKind::Or,
14262 right,
14263 }))
14264 })
14265 .unwrap_or(IrExpr::Literal(IrLiteral::Bool(false)))
14266 }
14267
14268 fn resolve_name_ref(&mut self, name: &str, allow_fn_param: bool) -> Option<IrExpr> {
14282 if self.for_vars.contains_key(name) {
14283 return Some(self.for_var_ref(name));
14284 }
14285 if let Some(IrFreeExpr::FreeObject(_)) = self.cte_free_items.get(name) {
14296 return Some(IrExpr::NamedTuple {
14297 fields: vec![],
14298 is_free_object: true,
14299 });
14300 }
14301 if let Some(ir) = self.inline_bindings.get(name) {
14302 return Some(ir.clone());
14303 }
14304 if let Some(t) = self.cte_types.get(name) {
14305 let scalar = !t.contains("::");
14309 return Some(IrExpr::CteRef {
14310 name: self.cte_sql_name(name),
14311 scalar,
14312 pg_type: (scalar && !t.is_empty()).then(|| literal_sentinel_to_pg(t).to_string()),
14313 });
14314 }
14315 if allow_fn_param && let Some(pg_type) = self.fn_params.get(name) {
14316 return Some(IrExpr::FnParam {
14317 name: name.to_string(),
14318 pg_type: pg_type.clone(),
14319 });
14320 }
14321 None
14322 }
14323
14324 fn enclosing_anchor(&self, root: &str) -> Option<(String, String)> {
14331 let innermost = self.anchors.last()?;
14332 if !innermost.detached || !innermost.answers_to(root) {
14333 return None;
14334 }
14335 self.anchors
14336 .iter()
14337 .rev()
14338 .skip(1)
14339 .find(|a| a.answers_to(root))
14340 .map(|a| (a.qualified.clone(), a.alias.clone()))
14341 }
14342
14343 fn scope_answers_to(&self, root: &str, self_qname: &str) -> bool {
14347 self.anchors
14348 .last()
14349 .is_some_and(|anchor| anchor.qualified == self_qname && anchor.answers_to(root))
14350 }
14351
14352 fn outer_anchor(&self, root: &str) -> Option<(String, String)> {
14354 self.anchors
14355 .iter()
14356 .rev()
14357 .find(|a| a.answers_to(root))
14358 .map(|a| (a.qualified.clone(), a.alias.clone()))
14359 }
14360
14361 fn compile_path(&mut self, p: &ast::Path, td: &TypeDescriptor, alias: &str) -> Result<IrExpr, PyQLError> {
14362 if !p.partial {
14363 if p.steps.len() == 1
14364 && let ast::PathStep::Name(n) = &p.steps[0]
14365 {
14366 if let Some(ir) = self.resolve_name_ref(n, true) {
14367 return Ok(ir);
14368 }
14369 if n == "__type__" {
14371 return Ok(if self.is_polymorphic(td) {
14372 IrExpr::ColumnRef {
14373 alias: alias.to_string(),
14374 column: "__type__".to_string(),
14375 pg_type: "text".to_string(),
14376 }
14377 } else {
14378 IrExpr::Literal(IrLiteral::Str(format!("{}::{}", td.module, td.name)))
14379 });
14380 }
14381 }
14382 if p.steps.len() == 2
14384 && let [ast::PathStep::Name(type_ref), ast::PathStep::Name(variant)] = p.steps.as_slice()
14385 && self.resolve_enum(type_ref).is_some()
14386 {
14387 return self.compile_enum_access(type_ref, variant);
14388 }
14389 if let Some(resolved) = self.resolve_cte_path(p) {
14391 return resolved;
14392 }
14393 if p.steps.len() > 1
14409 && let ast::PathStep::Name(root) = &p.steps[0]
14410 && self
14411 .cte_types
14412 .get(root.as_str())
14413 .map(|t| t.contains("::"))
14414 .unwrap_or(false)
14415 {
14416 let full_path = ast::Path {
14417 steps: p.steps.clone(),
14418 partial: false,
14419 };
14420 let synthetic = ast::SelectStmt {
14421 result: Expr::Path(full_path.clone()),
14422 filter: None,
14423 order_by: vec![],
14424 offset: None,
14425 limit: None,
14426 lock: None,
14427 };
14428 let ps = self.compile_path_select(&synthetic, &full_path, &[], false)?;
14429 return Ok(IrExpr::PathSubquery(Box::new(ps)));
14430 }
14431 if p.steps.len() > 1
14445 && let ast::PathStep::Name(root) = &p.steps[0]
14446 && (root == "__new__" || root == "__old__")
14447 {
14448 if let Some((anchor_td, anchor_alias)) = self.special_anchors.get(root).cloned() {
14449 let relative = ast::Path {
14450 steps: p.steps[1..].to_vec(),
14451 partial: true,
14452 };
14453 return self.compile_path(&relative, anchor_td, &anchor_alias);
14454 }
14455 return Err(PyQLError::Resolution(PyQLResolutionError::UnknownField(
14456 PyQLUnknownFieldError {
14457 message: format!("{root} cannot be used in this expression"),
14458 position: Position { line: 0, col: 0 },
14459 },
14460 )));
14461 }
14462 if p.steps.len() > 1 && matches!(&p.steps[0], ast::PathStep::Name(root) if root == "__subject__") {
14465 let relative = ast::Path {
14466 steps: p.steps[1..].to_vec(),
14467 partial: true,
14468 };
14469 return self.compile_path(&relative, td, alias);
14470 }
14471 if p.steps.len() > 1
14475 && let ast::PathStep::Name(root) = &p.steps[0]
14476 {
14477 let qualified = format!("{}::{}", td.module, td.name);
14478 if *root == td.name || *root == qualified {
14479 let relative = ast::Path {
14480 steps: p.steps[1..].to_vec(),
14481 partial: true,
14482 };
14483 if let Some((outer_qualified, outer_alias)) = self.enclosing_anchor(root) {
14487 let outer_td = self.resolve_type(&outer_qualified)?.clone();
14488 return self.compile_path(&relative, &outer_td, &outer_alias);
14489 }
14490 return self.compile_path(&relative, td, alias);
14491 }
14492 if let Some((outer_qualified, outer_alias)) = self.outer_anchor(root) {
14497 let relative = ast::Path {
14498 steps: p.steps[1..].to_vec(),
14499 partial: true,
14500 };
14501 let outer_td = self.resolve_type(&outer_qualified)?.clone();
14502 return self.compile_path(&relative, &outer_td, &outer_alias);
14503 }
14504 }
14505 if let Some(ast::PathStep::Name(var)) = p.steps.first()
14510 && p.steps.len() > 1
14511 && self.for_var_types.contains_key(var)
14512 && !matches!(p.steps[1], ast::PathStep::TypeIntersection(_))
14513 {
14514 let synthetic = ast::SelectStmt {
14515 result: Expr::Path(p.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, p, &[], false)?;
14523 return Ok(IrExpr::PathSubquery(Box::new(ps)));
14524 }
14525 if let [
14533 ast::PathStep::Name(var),
14534 ast::PathStep::TypeIntersection(type_ref),
14535 rest @ ..,
14536 ] = p.steps.as_slice()
14537 && !rest.is_empty()
14538 && self.for_var_types.contains_key(var)
14539 {
14540 let type_name = match &type_ref.module {
14541 Some(m) => format!("{}::{}", m, type_ref.name),
14542 None => type_ref.name.clone(),
14543 };
14544 let narrowed = self.resolve_type(&type_name)?;
14545 let mut steps = vec![ast::PathStep::Name(format!("{}::{}", narrowed.module, narrowed.name))];
14546 steps.extend(rest.iter().cloned());
14547 let rooted = ast::Path { steps, partial: false };
14548 let synthetic = ast::SelectStmt {
14549 result: Expr::Path(rooted.clone()),
14550 filter: None,
14551 order_by: vec![],
14552 offset: None,
14553 limit: None,
14554 lock: None,
14555 };
14556 let mut ps = self.compile_path_select(&synthetic, &rooted, &[], false)?;
14557 let correlation = IrExpr::BinOp(Box::new(IrBinOp {
14558 left: IrExpr::ColumnRef {
14559 alias: ps.root.alias.clone(),
14560 column: "id".to_string(),
14561 pg_type: "uuid".to_string(),
14562 },
14563 op: ast::BinOpKind::Eq,
14564 right: self.for_var_ref(var),
14565 }));
14566 ps.filter = and_conditions(ps.filter, vec![correlation]);
14567 return Ok(IrExpr::PathSubquery(Box::new(ps)));
14568 }
14569 if let [ast::PathStep::Name(var), ast::PathStep::TypeIntersection(type_ref)] = p.steps.as_slice()
14570 && self.for_var_types.contains_key(var)
14571 {
14572 let type_name = match &type_ref.module {
14573 Some(m) => format!("{}::{}", m, type_ref.name),
14574 None => type_ref.name.clone(),
14575 };
14576 let narrowed = self.resolve_type(&type_name)?;
14577 let narrowed_alias = self.fresh_alias();
14578 let source = IrSource {
14579 poly: self.poly_fanout_for(&format!("{}::{}", narrowed.module, narrowed.name)),
14580 type_name: format!("{}::{}", narrowed.module, narrowed.name),
14581 table: narrowed.table.clone(),
14582 alias: narrowed_alias.clone(),
14583 };
14584 let filter = IrExpr::BinOp(Box::new(IrBinOp {
14585 left: IrExpr::ColumnRef {
14586 alias: narrowed_alias,
14587 column: "id".to_string(),
14588 pg_type: "uuid".to_string(),
14589 },
14590 op: ast::BinOpKind::Eq,
14591 right: self.for_var_ref(var),
14592 }));
14593 return Ok(IrExpr::Subquery(Box::new(IrSelect::schema_bound(
14594 source,
14595 Self::pk_returning(narrowed),
14596 Some(filter),
14597 ))));
14598 }
14599 return Err(PyQLError::Type(PyQLTypeError {
14600 message: "absolute paths are not valid in expression context; use .name".into(),
14601 position: Position { line: 0, col: 0 },
14602 }));
14603 }
14604
14605 if p.partial
14610 && let [ast::PathStep::LinkProp(prop_name)] = p.steps.as_slice()
14611 {
14612 return self.compile_link_prop_ref(prop_name);
14613 }
14614
14615 if p.partial && matches!(p.steps.first(), Some(ast::PathStep::TypeIntersection(_))) {
14617 return self.compile_type_intersection_expr(&p.steps, td, alias);
14618 }
14619
14620 if p.partial && matches!(p.steps.first(), Some(ast::PathStep::Backlink(_))) {
14625 return self.compile_partial_path_as_subquery(p, td, alias);
14626 }
14627 if p.partial
14632 && p.steps.len() > 1
14633 && let Some(ast::PathStep::Name(first)) = p.steps.first()
14634 && let Some(Expr::IfElse(ie)) = self
14635 .active_declared_pointers
14636 .iter()
14637 .find(|d| path_leaf(&d.path).is_ok_and(|name| name == first))
14638 .and_then(|d| d.compexpr.clone())
14639 {
14640 let extend = |branch: &Expr| match branch {
14641 Expr::Path(bp) => {
14642 let mut steps = bp.steps.clone();
14643 steps.extend(p.steps[1..].iter().cloned());
14644 Some(Expr::Path(ast::Path {
14645 steps,
14646 partial: bp.partial,
14647 }))
14648 }
14649 Expr::SubQuery(_) => p.steps[1..].iter().try_fold(branch.clone(), |expr, step| match step {
14652 ast::PathStep::Name(field) => Some(Expr::FieldAccess {
14653 expr: Box::new(expr),
14654 field: field.clone(),
14655 }),
14656 _ => None,
14657 }),
14658 Expr::Set(items) if items.is_empty() => Some(Expr::Set(vec![])),
14660 Expr::TypeCast(cast) if matches!(&cast.expr, Expr::Set(items) if items.is_empty()) => {
14661 Some(Expr::Set(vec![]))
14662 }
14663 _ => None,
14664 };
14665 if let (Some(if_expr), Some(else_expr)) = (extend(&ie.if_expr), extend(&ie.else_expr)) {
14666 let distributed = Expr::IfElse(Box::new(ast::IfElse {
14667 condition: ie.condition.clone(),
14668 if_expr,
14669 else_expr,
14670 }));
14671 return self.compile_expr(&distributed, td, alias);
14672 }
14673 }
14674
14675 let narrows_last = matches!(p.steps.last(), Some(ast::PathStep::TypeIntersection(_)));
14680
14681 if p.steps.len() == 2 && !narrows_last {
14682 return self.compile_path_2step(p, td, alias);
14683 }
14684
14685 if p.steps.len() != 1 {
14688 return self.compile_partial_path_as_subquery(p, td, alias);
14689 }
14690
14691 let pointer_name = match &p.steps[0] {
14692 ast::PathStep::Name(n) => n.as_str(),
14693 _ => {
14694 return Err(PyQLError::Type(PyQLTypeError {
14695 message: "type intersections are not valid in expression context".into(),
14696 position: Position { line: 0, col: 0 },
14697 }));
14698 }
14699 };
14700
14701 if pointer_name == "__type__" {
14704 return Ok(if self.is_polymorphic(td) {
14705 IrExpr::ColumnRef {
14706 alias: alias.to_string(),
14707 column: "__type__".to_string(),
14708 pg_type: "text".to_string(),
14709 }
14710 } else {
14711 IrExpr::Literal(IrLiteral::Str(format!("{}::{}", td.module, td.name)))
14712 });
14713 }
14714
14715 if let Some(prop) = Self::resolve_property(td, pointer_name) {
14716 return Ok(IrExpr::ColumnRef {
14717 alias: alias.to_string(),
14718 column: prop.name.clone(),
14719 pg_type: prop.pg_type.clone(),
14720 });
14721 }
14722
14723 if let Some(link) = Self::resolve_link(td, pointer_name) {
14724 if link.is_junction_backed() {
14725 return self.junction_target_id_expr(td, link, alias);
14726 }
14727 return Ok(IrExpr::ColumnRef {
14729 alias: alias.to_string(),
14730 column: format!("{}_id", link.name),
14731 pg_type: "uuid".to_string(),
14732 });
14733 }
14734
14735 if let Some(cd) = self.resolve_computed(td, pointer_name) {
14737 let expr_ast = crate::parse::parse_pointer_expr(&cd.expression).map_err(PyQLError::Syntax)?;
14738 return self.compile_expr(&expr_ast, td, alias);
14739 }
14740
14741 if Self::resolve_multilink(td, pointer_name).is_some() {
14747 return self.compile_partial_path_as_subquery(p, td, alias);
14748 }
14749
14750 if let Some(expr) = self
14755 .active_declared_pointers
14756 .iter()
14757 .find(|d| path_leaf(&d.path).is_ok_and(|n| n == pointer_name))
14758 .and_then(|d| d.compexpr.clone())
14759 {
14760 return self.compile_expr(&expr, td, alias);
14761 }
14762
14763 Err(self.field_err(pointer_name, &format!("{}::{}", td.module, td.name)))
14764 }
14765
14766 fn compile_free_path(&mut self, p: &ast::Path) -> Result<IrExpr, PyQLError> {
14771 if p.partial {
14772 if let Some((qualified, alias)) = self.anchors.last().map(|a| (a.qualified.clone(), a.alias.clone())) {
14776 let td = self.resolve_type(&qualified)?;
14777 return self.compile_path(p, td, &alias);
14778 }
14779 return Err(self.type_err(
14780 "property reference (.name) is not valid in free SELECT; \
14781 use a schema-bound SELECT instead",
14782 ));
14783 }
14784 if p.steps.len() == 2
14785 && let [ast::PathStep::Name(type_ref), ast::PathStep::Name(variant)] = p.steps.as_slice()
14786 && self.resolve_enum(type_ref).is_some()
14787 {
14788 return self.compile_enum_access(type_ref, variant);
14789 }
14790 if let Some(resolved) = self.resolve_cte_path(p) {
14793 return resolved;
14794 }
14795 if p.steps.len() == 1
14796 && let ast::PathStep::Name(n) = &p.steps[0]
14797 && let Some(ir) = self.resolve_name_ref(n, true)
14798 {
14799 return Ok(ir);
14800 }
14801 if !p.partial
14807 && p.steps.len() > 1
14808 && let Some(ast::PathStep::Name(root)) = p.steps.first()
14809 && self.cte_object_type(root).is_some()
14810 {
14811 let synthetic = ast::SelectStmt {
14812 result: Expr::Path(p.clone()),
14813 filter: None,
14814 order_by: vec![],
14815 offset: None,
14816 limit: None,
14817 lock: None,
14818 };
14819 let ps = self.compile_path_select(&synthetic, p, &[], false)?;
14820 let widens = ps.joins.iter().any(|join| !matches!(join, IrPathJoin::Single { .. }));
14825 let root_is_multi = self.multi_row_ctes.contains(root.as_str());
14826 let multi = matches!(ps.result, IrPathResult::Scalar(..)) && (widens || root_is_multi);
14827 return Ok(if multi {
14828 IrExpr::ArrayFromSelect(Box::new(IrArraySource::PathSelect(Box::new(ps))))
14829 } else {
14830 IrExpr::PathSubquery(Box::new(ps))
14831 });
14832 }
14833 Err(self.type_err("expression is not valid in free SELECT context"))
14834 }
14835
14836 fn compile_path_2step(&mut self, p: &ast::Path, td: &TypeDescriptor, alias: &str) -> Result<IrExpr, PyQLError> {
14837 let link_name = match &p.steps[0] {
14838 ast::PathStep::Name(n) => n.as_str(),
14839 _ => {
14840 return Err(PyQLError::Type(PyQLTypeError {
14841 message: "type intersections are not valid in expression context".into(),
14842 position: Position { line: 0, col: 0 },
14843 }));
14844 }
14845 };
14846 let pointer_name = match &p.steps[1] {
14847 ast::PathStep::Name(n) => n.as_str(),
14848 _ => {
14849 return Err(PyQLError::Type(PyQLTypeError {
14850 message: "type intersections are not valid in expression context".into(),
14851 position: Position { line: 0, col: 0 },
14852 }));
14853 }
14854 };
14855
14856 if let Some(link) = Self::resolve_link(td, link_name) {
14857 if pointer_name == "id" {
14858 if link.is_junction_backed() {
14859 return self.junction_target_id_expr(td, link, alias);
14860 }
14861 return Ok(IrExpr::ColumnRef {
14862 alias: alias.to_string(),
14863 column: format!("{}_id", link_name),
14864 pg_type: "uuid".to_string(),
14865 });
14866 }
14867 let target_td = self.resolve_type(&link.target)?;
14868 if let Some(prop) = Self::resolve_property(target_td, pointer_name) {
14869 let ft_alias = self.fresh_alias();
14870 let target_id_expr = if link.is_junction_backed() {
14871 self.junction_target_id_expr(td, link, alias)?
14872 } else {
14873 IrExpr::ColumnRef {
14874 alias: alias.to_string(),
14875 column: format!("{}_id", link_name),
14876 pg_type: "uuid".to_string(),
14877 }
14878 };
14879 return Ok(IrExpr::Subquery(Box::new(IrSelect::schema_bound(
14880 IrSource {
14881 poly: None,
14882 type_name: format!("{}::{}", target_td.module, target_td.name),
14883 table: target_td.table.clone(),
14884 alias: ft_alias.clone(),
14885 },
14886 vec![IrShapePointer::Scalar(IrScalarPointer {
14887 implicit_id: false,
14888 marker_offset: None,
14889 alias: prop.name.clone(),
14890 column: prop.name.clone(),
14891 pg_type: prop.pg_type.clone(),
14892 tuple_shape: self.resolve_property_tuple_shape(prop),
14893 })],
14894 Some(IrExpr::BinOp(Box::new(IrBinOp {
14895 left: IrExpr::ColumnRef {
14896 alias: ft_alias.clone(),
14897 column: "id".to_string(),
14898 pg_type: "uuid".to_string(),
14899 },
14900 op: ast::BinOpKind::Eq,
14901 right: target_id_expr,
14902 }))),
14903 ))));
14904 }
14905 return self.compile_partial_path_as_subquery(p, td, alias);
14908 }
14909
14910 if Self::resolve_multilink(td, link_name).is_some() {
14914 return self.compile_partial_path_as_subquery(p, td, alias);
14915 }
14916
14917 self.compile_partial_path_as_subquery(p, td, alias)
14924 }
14925
14926 fn try_backlink_exists(
14930 &mut self,
14931 b: &ast::BinOp,
14932 td: &TypeDescriptor,
14933 alias: &str,
14934 ) -> Result<Option<IrExpr>, PyQLError> {
14935 use ast::PathStep;
14936 fn is_backlink(p: &ast::Path) -> bool {
14937 p.partial && matches!(p.steps.first(), Some(PathStep::Backlink(_)))
14938 }
14939 let (path_steps, value_ast, flip) = if let Expr::Path(p) = &b.left {
14940 if is_backlink(p) {
14941 (p.steps.as_slice(), &b.right, false)
14942 } else {
14943 return Ok(None);
14944 }
14945 } else if let Expr::Path(p) = &b.right {
14946 if is_backlink(p) {
14947 (p.steps.as_slice(), &b.left, true)
14948 } else {
14949 return Ok(None);
14950 }
14951 } else {
14952 return Ok(None);
14953 };
14954 let value_expr = self.compile_comparison_value(value_ast, &b.op, td, alias)?;
14955 let current_qname = format!("{}::{}", td.module, td.name);
14956 let exists = self.compile_backlink_as_exists(
14957 path_steps,
14958 Some((b.op.clone(), value_expr, flip)),
14959 ¤t_qname,
14960 alias,
14961 )?;
14962 Ok(Some(exists))
14963 }
14964
14965 fn compile_backlink_as_exists(
14969 &mut self,
14970 steps: &[ast::PathStep],
14971 comparison: Option<(ast::BinOpKind, IrExpr, bool)>,
14972 current_qname: &str,
14973 alias: &str,
14974 ) -> Result<IrExpr, PyQLError> {
14975 use ast::PathStep;
14976
14977 let backlink_name = match steps.first() {
14978 Some(PathStep::Backlink(n)) => n.clone(),
14979 _ => return Err(self.type_err("internal: expected backlink step")),
14980 };
14981 let Some(PathStep::TypeIntersection(type_ref)) = steps.get(1) else {
14985 return self.backlink_exists_over_owners(
14988 None,
14989 &backlink_name,
14990 &steps[1..],
14991 comparison,
14992 current_qname,
14993 alias,
14994 );
14995 };
14996
14997 let type_name = match &type_ref.module {
14998 Some(m) => format!("{}::{}", m, type_ref.name),
14999 None => type_ref.name.clone(),
15000 };
15001 let target_td = self.resolve_type(&type_name)?;
15002 if self.declares_backlink(target_td, &backlink_name, current_qname) {
15003 return self.backlink_exists_for_owner(
15004 target_td,
15005 &backlink_name,
15006 &steps[2..],
15007 comparison,
15008 current_qname,
15009 alias,
15010 );
15011 }
15012 let narrow_to = format!("{}::{}", target_td.module, target_td.name);
15017 self.backlink_exists_over_owners(
15018 Some(&narrow_to),
15019 &backlink_name,
15020 &steps[2..],
15021 comparison,
15022 current_qname,
15023 alias,
15024 )
15025 }
15026
15027 fn backlink_owner(
15030 &self,
15031 owner_td: &'a TypeDescriptor,
15032 backlink_name: &str,
15033 current_qname: &str,
15034 ) -> Result<&'a TypeDescriptor, PyQLError> {
15035 Ok(if self.declares_backlink(owner_td, backlink_name, current_qname) {
15041 owner_td
15042 } else {
15043 let narrowed = format!("{}::{}", owner_td.module, owner_td.name);
15046 let declaring: Vec<&'a TypeDescriptor> = self
15047 .schema
15048 .types
15049 .iter()
15050 .filter(|t| !t.abstract_ && Self::is_or_implements(t, &narrowed))
15051 .filter(|t| self.declares_backlink(t, backlink_name, current_qname))
15052 .collect();
15053 let declaring = Self::without_inherited_owners(declaring);
15054 match declaring.as_slice() {
15055 [only] => only,
15056 [] => owner_td,
15057 several => {
15058 return Err(self.type_err(&format!(
15059 "'{backlink_name}' pointing to {current_qname} is declared by {} types under \
15060 {narrowed} ({}), so a backlink narrowed to it has no single source to read \
15061 — narrow to one of them instead",
15062 several.len(),
15063 several
15064 .iter()
15065 .map(|t| format!("{}::{}", t.module, t.name))
15066 .collect::<Vec<_>>()
15067 .join(", "),
15068 )));
15069 }
15070 }
15071 })
15072 }
15073
15074 fn backlink_is_single(&self, owner_td: &'a TypeDescriptor, backlink_name: &str, current_qname: &str) -> bool {
15077 let Ok(owner) = self.backlink_owner(owner_td, backlink_name, current_qname) else {
15078 return false;
15079 };
15080 owner
15081 .links
15082 .iter()
15083 .any(|l| l.name == backlink_name && l.is_exclusive && self.link_target_reaches(&l.target, current_qname))
15084 || owner.multilinks.iter().any(|ml| {
15085 ml.name == backlink_name && ml.is_exclusive && self.link_target_reaches(&ml.target, current_qname)
15086 })
15087 }
15088
15089 fn declares_backlink(&self, td: &TypeDescriptor, backlink_name: &str, current_qname: &str) -> bool {
15092 td.links
15093 .iter()
15094 .any(|l| l.name == backlink_name && self.link_target_reaches(&l.target, current_qname))
15095 || td
15096 .multilinks
15097 .iter()
15098 .any(|ml| ml.name == backlink_name && self.link_target_reaches(&ml.target, current_qname))
15099 }
15100
15101 #[allow(clippy::too_many_arguments)]
15105 fn without_inherited_owners(owners: Vec<&'a TypeDescriptor>) -> Vec<&'a TypeDescriptor> {
15108 let qnames: Vec<String> = owners.iter().map(|t| format!("{}::{}", t.module, t.name)).collect();
15109 owners
15110 .into_iter()
15111 .filter(|t| !t.bases.iter().any(|base| qnames.contains(base)))
15112 .collect()
15113 }
15114
15115 fn backlink_exists_over_owners(
15116 &mut self,
15117 narrow_to: Option<&str>,
15118 backlink_name: &str,
15119 rest: &[ast::PathStep],
15120 comparison: Option<(ast::BinOpKind, IrExpr, bool)>,
15121 current_qname: &str,
15122 alias: &str,
15123 ) -> Result<IrExpr, PyQLError> {
15124 let schema = self.schema;
15125 let owners: Vec<&'a TypeDescriptor> = schema
15126 .types
15127 .iter()
15128 .filter(|t| !t.abstract_)
15129 .filter(|t| narrow_to.is_none_or(|q| Self::is_or_implements(t, q)))
15130 .filter(|t| self.declares_backlink(t, backlink_name, current_qname))
15131 .collect();
15132 let owners = Self::without_inherited_owners(owners);
15133 if owners.is_empty() {
15134 return Err(self.type_err(&match narrow_to {
15135 Some(q) => format!("type {q} has no link or multi-link '{backlink_name}' pointing to {current_qname}"),
15136 None => format!("no type has a link or multi-link '{backlink_name}' pointing to {current_qname}"),
15137 }));
15138 }
15139 let mut combined: Option<IrExpr> = None;
15140 for owner_td in owners {
15141 let one = self.backlink_exists_for_owner(
15142 owner_td,
15143 backlink_name,
15144 rest,
15145 comparison.clone(),
15146 current_qname,
15147 alias,
15148 )?;
15149 combined = Some(match combined {
15150 None => one,
15151 Some(previous) => IrExpr::BinOp(Box::new(IrBinOp {
15152 left: previous,
15153 op: ast::BinOpKind::Or,
15154 right: one,
15155 })),
15156 });
15157 }
15158 Ok(combined.expect("owners is non-empty"))
15159 }
15160
15161 fn is_or_implements(td: &TypeDescriptor, qname: &str) -> bool {
15163 format!("{}::{}", td.module, td.name) == qname
15164 || td.interfaces.iter().any(|i| i == qname)
15165 || td.parents.iter().any(|p| p == qname)
15166 || td.bases.iter().any(|b| b == qname)
15167 }
15168
15169 fn has_subtypes(&self, td: &TypeDescriptor) -> bool {
15173 let qname = format!("{}::{}", td.module, td.name);
15174 !td.abstract_ && self.schema.types.iter().any(|t| t.bases.contains(&qname))
15175 }
15176
15177 fn backs_no_relation(td: &TypeDescriptor) -> bool {
15181 td.abstract_ && !td.materialized
15182 }
15183
15184 fn subtype_fanouts(&self) -> HashMap<(String, String), IrPolyFanout> {
15186 self.schema
15187 .types
15188 .iter()
15189 .filter(|t| self.has_subtypes(t) || Self::backs_no_relation(t))
15190 .filter_map(|t| {
15191 let fanout = self.poly_fanout_for(&format!("{}::{}", t.module, t.name))?;
15192 Some(((t.module.clone(), t.table.clone()), fanout))
15193 })
15194 .collect()
15195 }
15196
15197 fn is_polymorphic(&self, td: &TypeDescriptor) -> bool {
15202 td.abstract_ || self.has_subtypes(td)
15203 }
15204
15205 #[allow(clippy::too_many_arguments)]
15209 fn backlink_exists_for_owner(
15210 &mut self,
15211 target_td: &'a TypeDescriptor,
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 backlink_name = backlink_name.to_string();
15219 let target_qname = format!("{}::{}", target_td.module, target_td.name);
15220 let target_table = target_td.table.clone();
15221 let t_alias = self.fresh_alias();
15222
15223 let join_cond = if let Some(l) = target_td
15232 .links
15233 .iter()
15234 .find(|l| l.name == backlink_name && self.link_target_reaches(&l.target, current_qname))
15235 {
15236 if l.is_junction_backed() {
15237 let (jt_table, jt_module, jt_owner_col, jt_current_col, _) = self.link_junction_info(target_td, l)?;
15241 let jt_alias = self.fresh_alias();
15242 IrExpr::UnaryOp(Box::new(IrUnaryOp {
15243 op: ast::UnaryOpKind::Exists,
15244 operand: IrExpr::Subquery(Box::new(IrSelect::schema_bound(
15245 IrSource {
15246 poly: None,
15247 type_name: format!("{}::__jt__", jt_module),
15248 table: jt_table,
15249 alias: jt_alias.clone(),
15250 },
15251 vec![],
15252 Some(IrExpr::BinOp(Box::new(IrBinOp {
15253 left: IrExpr::BinOp(Box::new(IrBinOp {
15254 left: IrExpr::ColumnRef {
15255 alias: jt_alias.clone(),
15256 column: jt_owner_col,
15257 pg_type: "uuid".to_string(),
15258 },
15259 op: ast::BinOpKind::Eq,
15260 right: IrExpr::ColumnRef {
15261 alias: t_alias.clone(),
15262 column: "id".to_string(),
15263 pg_type: "uuid".to_string(),
15264 },
15265 })),
15266 op: ast::BinOpKind::And,
15267 right: IrExpr::BinOp(Box::new(IrBinOp {
15268 left: IrExpr::ColumnRef {
15269 alias: jt_alias,
15270 column: jt_current_col,
15271 pg_type: "uuid".to_string(),
15272 },
15273 op: ast::BinOpKind::Eq,
15274 right: IrExpr::ColumnRef {
15275 alias: alias.to_string(),
15276 column: "id".to_string(),
15277 pg_type: "uuid".to_string(),
15278 },
15279 })),
15280 }))),
15281 ))),
15282 }))
15283 } else {
15284 let fk_col = format!("{}_id", backlink_name);
15285 IrExpr::BinOp(Box::new(IrBinOp {
15287 left: IrExpr::ColumnRef {
15288 alias: t_alias.clone(),
15289 column: fk_col,
15290 pg_type: "uuid".to_string(),
15291 },
15292 op: ast::BinOpKind::Eq,
15293 right: IrExpr::ColumnRef {
15294 alias: alias.to_string(),
15295 column: "id".to_string(),
15296 pg_type: "uuid".to_string(),
15297 },
15298 }))
15299 }
15300 } else if let Some(ml) = target_td
15301 .multilinks
15302 .iter()
15303 .find(|ml| ml.name == backlink_name && self.link_target_reaches(&ml.target, current_qname))
15304 .cloned()
15305 {
15306 let (jt_table, jt_module, _, _, _) = self.multilink_junction_info(target_td, &ml)?;
15311 let jt_alias = self.fresh_alias();
15312 IrExpr::UnaryOp(Box::new(IrUnaryOp {
15313 op: ast::UnaryOpKind::Exists,
15314 operand: IrExpr::Subquery(Box::new(IrSelect::schema_bound(
15315 IrSource {
15316 poly: None,
15317 type_name: format!("{}::__jt__", jt_module),
15318 table: jt_table,
15319 alias: jt_alias.clone(),
15320 },
15321 vec![],
15322 Some(IrExpr::BinOp(Box::new(IrBinOp {
15323 left: IrExpr::BinOp(Box::new(IrBinOp {
15324 left: IrExpr::ColumnRef {
15325 alias: jt_alias.clone(),
15326 column: "source".to_string(),
15327 pg_type: "uuid".to_string(),
15328 },
15329 op: ast::BinOpKind::Eq,
15330 right: IrExpr::ColumnRef {
15331 alias: t_alias.clone(),
15332 column: "id".to_string(),
15333 pg_type: "uuid".to_string(),
15334 },
15335 })),
15336 op: ast::BinOpKind::And,
15337 right: IrExpr::BinOp(Box::new(IrBinOp {
15338 left: IrExpr::ColumnRef {
15339 alias: jt_alias,
15340 column: "target".to_string(),
15341 pg_type: "uuid".to_string(),
15342 },
15343 op: ast::BinOpKind::Eq,
15344 right: IrExpr::ColumnRef {
15345 alias: alias.to_string(),
15346 column: "id".to_string(),
15347 pg_type: "uuid".to_string(),
15348 },
15349 })),
15350 }))),
15351 ))),
15352 }))
15353 } else {
15354 return Err(PyQLError::Type(PyQLTypeError {
15355 message: format!(
15356 "type {} has no link or multi-link '{}' pointing to {}",
15357 target_qname, backlink_name, current_qname,
15358 ),
15359 position: Position { line: 0, col: 0 },
15360 }));
15361 };
15362
15363 let tail_cond = self.compile_backlink_tail(rest, comparison, &target_qname, &t_alias)?;
15364
15365 let filter = match tail_cond {
15366 Some(tc) => IrExpr::BinOp(Box::new(IrBinOp {
15367 left: join_cond,
15368 op: ast::BinOpKind::And,
15369 right: tc,
15370 })),
15371 None => join_cond,
15372 };
15373
15374 Ok(IrExpr::UnaryOp(Box::new(IrUnaryOp {
15375 op: ast::UnaryOpKind::Exists,
15376 operand: IrExpr::Subquery(Box::new(IrSelect::schema_bound(
15377 IrSource {
15378 poly: None,
15379 type_name: target_qname,
15380 table: target_table,
15381 alias: t_alias,
15382 },
15383 vec![],
15384 Some(filter),
15385 ))),
15386 })))
15387 }
15388
15389 fn compile_backlink_tail(
15391 &mut self,
15392 steps: &[ast::PathStep],
15393 comparison: Option<(ast::BinOpKind, IrExpr, bool)>,
15394 target_qname: &str,
15395 t_alias: &str,
15396 ) -> Result<Option<IrExpr>, PyQLError> {
15397 use ast::PathStep;
15398
15399 if steps.is_empty() {
15404 return Ok(comparison.map(|(op, value, flip)| {
15405 let key = IrExpr::ColumnRef {
15406 alias: t_alias.to_string(),
15407 column: "id".to_string(),
15408 pg_type: "uuid".to_string(),
15409 };
15410 let (left, right) = if flip { (value, key) } else { (key, value) };
15411 IrExpr::BinOp(Box::new(IrBinOp { left, op, right }))
15412 }));
15413 }
15414
15415 if matches!(steps.first(), Some(PathStep::Backlink(_))) {
15417 let inner = self.compile_backlink_as_exists(steps, comparison, target_qname, t_alias)?;
15418 return Ok(Some(inner));
15419 }
15420
15421 let target_td = self.resolve_type(target_qname)?;
15422
15423 if let [PathStep::Name(pointer_name)] = steps {
15425 if let Some(prop) = Self::resolve_property(target_td, pointer_name) {
15426 let col = IrExpr::ColumnRef {
15427 alias: t_alias.to_string(),
15428 column: prop.name.clone(),
15429 pg_type: prop.pg_type.clone(),
15430 };
15431 return Ok(Some(self.apply_comparison(col, comparison)));
15432 }
15433 if let Some(link) = Self::resolve_link(target_td, pointer_name) {
15434 let col = if link.is_junction_backed() {
15435 self.junction_target_id_expr(target_td, link, t_alias)?
15436 } else {
15437 IrExpr::ColumnRef {
15438 alias: t_alias.to_string(),
15439 column: format!("{}_id", link.name),
15440 pg_type: "uuid".to_string(),
15441 }
15442 };
15443 return Ok(Some(self.apply_comparison(col, comparison)));
15444 }
15445 }
15449
15450 if let [PathStep::Name(link_name), PathStep::Name(prop_name)] = steps
15452 && let Some(link) = Self::resolve_link(target_td, link_name)
15453 {
15454 let link_target = link.target.clone();
15455 let target_id_expr = if link.is_junction_backed() {
15456 self.junction_target_id_expr(target_td, link, t_alias)?
15457 } else {
15458 IrExpr::ColumnRef {
15459 alias: t_alias.to_string(),
15460 column: format!("{}_id", link_name),
15461 pg_type: "uuid".to_string(),
15462 }
15463 };
15464 let link_target_td = self.resolve_type(&link_target)?;
15465 let link_target_qname = format!("{}::{}", link_target_td.module, link_target_td.name);
15466 let link_target_table = link_target_td.table.clone();
15467 if let Some(prop) = Self::resolve_property(link_target_td, prop_name) {
15468 let l_alias = self.fresh_alias();
15469 let id_cond = IrExpr::BinOp(Box::new(IrBinOp {
15470 left: IrExpr::ColumnRef {
15471 alias: l_alias.clone(),
15472 column: "id".to_string(),
15473 pg_type: "uuid".to_string(),
15474 },
15475 op: ast::BinOpKind::Eq,
15476 right: target_id_expr,
15477 }));
15478 let col = IrExpr::ColumnRef {
15479 alias: l_alias.clone(),
15480 column: prop.name.clone(),
15481 pg_type: prop.pg_type.clone(),
15482 };
15483 let prop_cond = self.apply_comparison(col, comparison);
15484 let full = IrExpr::BinOp(Box::new(IrBinOp {
15485 left: id_cond,
15486 op: ast::BinOpKind::And,
15487 right: prop_cond,
15488 }));
15489 return Ok(Some(IrExpr::UnaryOp(Box::new(IrUnaryOp {
15490 op: ast::UnaryOpKind::Exists,
15491 operand: IrExpr::Subquery(Box::new(IrSelect::schema_bound(
15492 IrSource {
15493 poly: None,
15494 type_name: link_target_qname,
15495 table: link_target_table,
15496 alias: l_alias,
15497 },
15498 vec![],
15499 Some(full),
15500 ))),
15501 }))));
15502 }
15503 }
15504
15505 let tail = ast::Path {
15510 steps: steps.to_vec(),
15511 partial: true,
15512 };
15513 let walked = self.compile_path(&tail, target_td, t_alias)?;
15514 Ok(Some(self.apply_comparison(walked, comparison)))
15515 }
15516
15517 fn is_set_expr(&self, e: &IrExpr) -> bool {
15523 match e {
15524 IrExpr::ArrayFromSelect(_) => true,
15525 IrExpr::CteRef { name, .. } => self.multi_row_ctes.contains(name),
15526 _ => false,
15527 }
15528 }
15529
15530 fn apply_comparison(&self, col: IrExpr, comparison: Option<(ast::BinOpKind, IrExpr, bool)>) -> IrExpr {
15532 let is_set = |e: &IrExpr| self.is_set_expr(e);
15533 match comparison {
15534 Some((op, val, flip)) => {
15535 let (l, r) = if flip { (val, col) } else { (col, val) };
15536 if matches!(op, ast::BinOpKind::Eq | ast::BinOpKind::Ne) && is_set(&l) != is_set(&r) {
15540 let (value, set) = if is_set(&r) { (l, r) } else { (r, l) };
15541 let membership = IrExpr::BinOp(Box::new(IrBinOp {
15542 left: value,
15543 op: ast::BinOpKind::In,
15544 right: set,
15545 }));
15546 return if matches!(op, ast::BinOpKind::Ne) {
15547 IrExpr::UnaryOp(Box::new(IrUnaryOp {
15548 op: ast::UnaryOpKind::Not,
15549 operand: membership,
15550 }))
15551 } else {
15552 membership
15553 };
15554 }
15555 IrExpr::BinOp(Box::new(IrBinOp { left: l, op, right: r }))
15556 }
15557 None => ir_is_not_null(col),
15558 }
15559 }
15560
15561 fn compile_comparison_value(
15571 &mut self,
15572 value: &Expr,
15573 op: &ast::BinOpKind,
15574 td: &TypeDescriptor,
15575 alias: &str,
15576 ) -> Result<IrExpr, PyQLError> {
15577 match value {
15578 Expr::Set(elems) if matches!(op, ast::BinOpKind::In | ast::BinOpKind::NotIn) => Ok(IrExpr::Array(
15579 elems
15580 .iter()
15581 .map(|e| self.compile_expr(e, td, alias))
15582 .collect::<Result<Vec<_>, _>>()?,
15583 )),
15584 other => self.compile_expr(other, td, alias),
15585 }
15586 }
15587
15588 fn try_elementwise_in(
15599 &mut self,
15600 b: &ast::BinOp,
15601 ctx: Option<(&TypeDescriptor, &str)>,
15602 ) -> Result<Option<IrExpr>, PyQLError> {
15603 let membership = matches!(b.op, ast::BinOpKind::In | ast::BinOpKind::NotIn);
15609 if !membership && !matches!(b.op, ast::BinOpKind::Eq | ast::BinOpKind::Ne) {
15610 return Ok(None);
15611 }
15612 if membership
15624 && (self.value_position || self.explicit_set_depth > 0)
15625 && let Some(root) = self.object_set_name(&b.left)
15626 {
15627 let rewritten = Expr::BinOp(Box::new(ast::BinOp {
15628 left: Expr::Path(ast::Path {
15629 steps: vec![ast::PathStep::Name(root.clone()), ast::PathStep::Name("id".into())],
15630 partial: false,
15631 }),
15632 op: b.op.clone(),
15633 right: b.right.clone(),
15634 }));
15635 let over_the_set = ast::SelectStmt {
15636 result: rewritten.clone(),
15637 filter: None,
15638 order_by: vec![],
15639 offset: None,
15640 limit: None,
15641 lock: None,
15642 };
15643 let ps = self.compile_expr_as_path_select(&over_the_set, &rewritten, &root, false)?;
15644 return Ok(Some(IrExpr::ArrayFromSelect(Box::new(IrArraySource::PathSelect(
15645 Box::new(ps),
15646 )))));
15647 }
15648 if !self.value_position && self.universal_set_depth == 0 {
15653 return Ok(None);
15654 }
15655 let Some((td, _)) = ctx else { return Ok(None) };
15656 let Expr::Path(p) = &b.left else { return Ok(None) };
15660 if !p.partial || !self.path_crosses_multi(td, &p.steps) {
15661 return Ok(None);
15662 }
15663 let hoisted = self.hoisted_ctes.len();
15667 let left = self.compile_expr_ctx(&b.left, ctx)?;
15668 let mut ps = match left {
15673 IrExpr::PathSubquery(ps) | IrExpr::ObjectPathSubquery(ps) => ps,
15674 IrExpr::ArrayFromSelect(source) => match *source {
15675 IrArraySource::PathSelect(ps) => ps,
15676 _ => {
15677 self.hoisted_ctes.truncate(hoisted);
15678 return Ok(None);
15679 }
15680 },
15681 _ => {
15682 self.hoisted_ctes.truncate(hoisted);
15683 return Ok(None);
15684 }
15685 };
15686 let right = match &b.right {
15690 Expr::Set(elems) => IrExpr::Array(
15691 elems
15692 .iter()
15693 .map(|e| self.compile_expr_ctx(e, ctx))
15694 .collect::<Result<Vec<_>, _>>()?,
15695 ),
15696 other => self.compile_expr_ctx(other, ctx)?,
15697 };
15698 if !membership && !self.is_set_expr(&right) {
15702 self.hoisted_ctes.truncate(hoisted);
15703 return Ok(None);
15704 }
15705 let element = match &ps.result {
15708 IrPathResult::Scalar(expr, _) => expr.clone(),
15709 IrPathResult::Object { alias, .. } => IrExpr::ColumnRef {
15710 alias: alias.clone(),
15711 column: "id".to_string(),
15712 pg_type: "uuid".to_string(),
15713 },
15714 };
15715 let per_element = self.apply_comparison(element, Some((b.op.clone(), right, false)));
15718 ps.result = IrPathResult::Scalar(per_element, None);
15719 Ok(Some(IrExpr::ArrayFromSelect(Box::new(IrArraySource::PathSelect(ps)))))
15720 }
15721
15722 fn try_multilink_exists(
15724 &mut self,
15725 b: &ast::BinOp,
15726 td: &TypeDescriptor,
15727 alias: &str,
15728 ) -> Result<Option<IrExpr>, PyQLError> {
15729 fn ml_first_name(steps: &[ast::PathStep]) -> Option<&str> {
15733 match steps.first()? {
15734 ast::PathStep::Name(n) => Some(n.as_str()),
15735 _ => None,
15736 }
15737 }
15738
15739 let (path_steps, value_ast, flip) = if let Expr::Path(p) = &b.left {
15740 if p.partial {
15741 if let Some(ln) = ml_first_name(&p.steps) {
15742 if Self::resolve_multilink(td, ln).is_some() {
15743 (p.steps.as_slice(), &b.right, false)
15744 } else {
15745 return Ok(None);
15746 }
15747 } else {
15748 return Ok(None);
15749 }
15750 } else {
15751 return Ok(None);
15752 }
15753 } else if let Expr::Path(p) = &b.right {
15754 if p.partial {
15755 if let Some(ln) = ml_first_name(&p.steps) {
15756 if Self::resolve_multilink(td, ln).is_some() {
15757 (p.steps.as_slice(), &b.left, true)
15758 } else {
15759 return Ok(None);
15760 }
15761 } else {
15762 return Ok(None);
15763 }
15764 } else {
15765 return Ok(None);
15766 }
15767 } else {
15768 return Ok(None);
15769 };
15770
15771 let value_expr = self.compile_comparison_value(value_ast, &b.op, td, alias)?;
15772 let ml_name = match &path_steps[0] {
15773 ast::PathStep::Name(n) => n.clone(),
15774 _ => return Ok(None),
15775 };
15776 let ml = Self::resolve_multilink(td, &ml_name).unwrap();
15777
15778 if self.explicit_set_depth == 0 {
15781 let mut pointer_path = String::new();
15784 for step in path_steps {
15785 match step {
15786 ast::PathStep::Name(n) => {
15787 pointer_path.push('.');
15788 pointer_path.push_str(n);
15789 }
15790 ast::PathStep::Backlink(n) => {
15791 pointer_path.push_str(".<");
15792 pointer_path.push_str(n);
15793 }
15794 ast::PathStep::LinkProp(n) => {
15795 pointer_path.push('@');
15796 pointer_path.push_str(n);
15797 }
15798 ast::PathStep::TypeIntersection(t) => {
15799 pointer_path.push_str("[is ");
15800 pointer_path.push_str(&t.qualified_name());
15801 pointer_path.push(']');
15802 }
15803 }
15804 }
15805 self.warnings.push(format!(
15806 "possibly more than one element returned by an expression in a FILTER clause \
15807 (multi-link '{pointer_path}'); wrap with any() to make intent explicit",
15808 ));
15809 }
15810
15811 let ml_target = ml.target.clone();
15813 let ml_through = ml.through.clone();
15814 let td_module = td.module.clone();
15815 let td_name = td.name.clone();
15816 let td_table = td.table.clone();
15817
15818 let tail_steps: Vec<ast::PathStep> = path_steps[1..].to_vec();
15820
15821 let jt_alias = self.fresh_alias();
15822
15823 let (jt_table, jt_module, jt_src_col, jt_tgt_col) = if let Some(through_qname) = &ml_through {
15825 let through_td = self.resolve_type(through_qname)?;
15826 if through_td.junction {
15827 (
15830 format!("{}.{}", td_table, ml_name),
15831 td_module.clone(),
15832 "source".to_string(),
15833 "target".to_string(),
15834 )
15835 } else {
15836 let source_qname = format!("{}::{}", td_module, td_name);
15837 let src_col = through_td
15838 .links
15839 .iter()
15840 .find(|l| l.target == source_qname)
15841 .ok_or_else(|| {
15842 PyQLError::Type(PyQLTypeError {
15843 message: format!("through type {through_qname} has no link to {source_qname}"),
15844 position: Position { line: 0, col: 0 },
15845 })
15846 })?
15847 .name
15848 .clone();
15849 let tgt_col = through_td
15850 .links
15851 .iter()
15852 .find(|l| l.target == ml_target && l.name != src_col)
15853 .or_else(|| through_td.links.iter().find(|l| l.target == ml_target))
15854 .ok_or_else(|| {
15855 PyQLError::Type(PyQLTypeError {
15856 message: format!("through type {through_qname} has no link to {ml_target}"),
15857 position: Position { line: 0, col: 0 },
15858 })
15859 })?
15860 .name
15861 .clone();
15862 (
15863 through_td.table.clone(),
15864 through_td.module.clone(),
15865 format!("{}_id", src_col),
15866 format!("{}_id", tgt_col),
15867 )
15868 }
15869 } else {
15870 (
15871 format!("{}.{}", td_table, ml_name),
15872 td_module.clone(),
15873 "source".to_string(),
15874 "target".to_string(),
15875 )
15876 };
15877
15878 let src_filter = IrExpr::BinOp(Box::new(IrBinOp {
15880 left: IrExpr::ColumnRef {
15881 alias: jt_alias.clone(),
15882 column: jt_src_col,
15883 pg_type: "uuid".to_string(),
15884 },
15885 op: ast::BinOpKind::Eq,
15886 right: IrExpr::ColumnRef {
15887 alias: alias.to_string(),
15888 column: "id".to_string(),
15889 pg_type: "uuid".to_string(),
15890 },
15891 }));
15892
15893 let tail_filter = self.compile_path_tail_filter(
15895 &tail_steps,
15896 b.op.clone(),
15897 value_expr,
15898 flip,
15899 &ml_target,
15900 &jt_alias,
15901 &jt_tgt_col,
15902 )?;
15903
15904 let full_filter = IrExpr::BinOp(Box::new(IrBinOp {
15905 left: src_filter,
15906 op: ast::BinOpKind::And,
15907 right: tail_filter,
15908 }));
15909
15910 let jt_source = IrSource {
15912 poly: None,
15913 type_name: format!("{}::__jt__", jt_module),
15914 table: jt_table,
15915 alias: jt_alias,
15916 };
15917 let inner = IrExpr::Subquery(Box::new(IrSelect::schema_bound(jt_source, vec![], Some(full_filter))));
15918
15919 Ok(Some(IrExpr::UnaryOp(Box::new(IrUnaryOp {
15920 op: ast::UnaryOpKind::Exists,
15921 operand: inner,
15922 }))))
15923 }
15924
15925 #[allow(clippy::too_many_arguments)]
15932 fn compile_path_tail_filter(
15933 &mut self,
15934 steps: &[ast::PathStep],
15935 op: ast::BinOpKind,
15936 value_expr: IrExpr,
15937 flip: bool,
15938 target_type: &str,
15939 jt_alias: &str,
15940 jt_tgt_col: &str,
15941 ) -> Result<IrExpr, PyQLError> {
15942 if steps.is_empty() {
15945 let col_ref = IrExpr::ColumnRef {
15946 alias: jt_alias.to_string(),
15947 column: jt_tgt_col.to_string(),
15948 pg_type: "uuid".to_string(),
15949 };
15950 return Ok(self.apply_comparison(col_ref, Some((op, value_expr, flip))));
15955 }
15956
15957 if let Some(ast::PathStep::TypeIntersection(type_ref)) = steps.first() {
15963 let narrowed = self.resolve_type(&type_ref.qualified_name())?;
15964 let narrowed_qname = format!("{}::{}", narrowed.module, narrowed.name);
15965 return self.compile_path_tail_filter(
15966 &steps[1..],
15967 op,
15968 value_expr,
15969 flip,
15970 &narrowed_qname,
15971 jt_alias,
15972 jt_tgt_col,
15973 );
15974 }
15975
15976 let first_name = match steps.first() {
15977 Some(ast::PathStep::Name(n)) => n.clone(),
15978 _ => return Err(self.type_err("expected a property or link name in path")),
15979 };
15980
15981 let target_td = self.resolve_type(target_type)?;
15982 let target_table = target_td.table.clone();
15983
15984 if steps.len() == 1 {
15985 if first_name == "id" {
15987 let col_ref = IrExpr::ColumnRef {
15989 alias: jt_alias.to_string(),
15990 column: jt_tgt_col.to_string(),
15991 pg_type: "uuid".to_string(),
15992 };
15993 let (l, r) = if flip {
15994 (value_expr, col_ref)
15995 } else {
15996 (col_ref, value_expr)
15997 };
15998 return Ok(IrExpr::BinOp(Box::new(IrBinOp { left: l, op, right: r })));
15999 }
16000 let single_link = target_td
16006 .links
16007 .iter()
16008 .find(|l| l.name == first_name && !l.is_junction_backed());
16009 if single_link.is_none() && target_td.multilinks.iter().any(|l| l.name == first_name) {
16010 let target_display = target_type.replace("::", ".");
16011 return Err(PyQLError::Type(PyQLTypeError {
16012 message: format!(
16013 "operator '{op}' cannot be applied to operands of type '{target_display}' and the value type",
16014 op = op,
16015 ),
16016 position: Position { line: 0, col: 0 },
16017 }));
16018 }
16019 let (prop_name, prop_pg) = match single_link {
16020 Some(link) => (format!("{}_id", link.name), "uuid".to_string()),
16021 None => {
16022 let prop = target_td
16023 .properties
16024 .iter()
16025 .find(|p| p.name == first_name)
16026 .ok_or_else(|| self.field_err(&first_name, target_type))?;
16027 (prop.name.clone(), prop.pg_type.clone())
16028 }
16029 };
16030 let tgt_alias = self.fresh_alias();
16031 let id_filter = IrExpr::BinOp(Box::new(IrBinOp {
16033 left: IrExpr::ColumnRef {
16034 alias: tgt_alias.clone(),
16035 column: "id".to_string(),
16036 pg_type: "uuid".to_string(),
16037 },
16038 op: ast::BinOpKind::Eq,
16039 right: IrExpr::ColumnRef {
16040 alias: jt_alias.to_string(),
16041 column: jt_tgt_col.to_string(),
16042 pg_type: "uuid".to_string(),
16043 },
16044 }));
16045 let prop_col = IrExpr::ColumnRef {
16046 alias: tgt_alias.clone(),
16047 column: prop_name,
16048 pg_type: prop_pg,
16049 };
16050 let (pl, pr) = if flip {
16051 (value_expr, prop_col)
16052 } else {
16053 (prop_col, value_expr)
16054 };
16055 let prop_filter = IrExpr::BinOp(Box::new(IrBinOp {
16056 left: pl,
16057 op,
16058 right: pr,
16059 }));
16060 let full = IrExpr::BinOp(Box::new(IrBinOp {
16061 left: id_filter,
16062 op: ast::BinOpKind::And,
16063 right: prop_filter,
16064 }));
16065 let inner = IrExpr::Subquery(Box::new(IrSelect::schema_bound(
16066 IrSource {
16067 poly: None,
16068 type_name: target_type.to_string(),
16069 table: target_table,
16070 alias: tgt_alias,
16071 },
16072 vec![],
16073 Some(full),
16074 )));
16075 return Ok(IrExpr::UnaryOp(Box::new(IrUnaryOp {
16076 op: ast::UnaryOpKind::Exists,
16077 operand: inner,
16078 })));
16079 }
16080
16081 if target_td.multilinks.iter().any(|l| l.name == first_name) {
16083 return Err(self.type_err("nested multi-link traversal in comparison is not yet supported"));
16084 }
16085 let link = target_td
16086 .links
16087 .iter()
16088 .find(|l| l.name == first_name)
16089 .ok_or_else(|| self.field_err(&first_name, target_type))?;
16090 if link.is_junction_backed() {
16091 return Err(self.type_err(&format!(
16098 "filtering through a junction-backed single link ('{first_name}') nested inside \
16099 a multi-link path comparison is not yet supported — filter on '.{first_name}' \
16100 directly instead"
16101 )));
16102 }
16103 let next_target = link.target.clone();
16104 let fk_col = format!("{}_id", first_name);
16105 let tgt_alias = self.fresh_alias();
16106
16107 if steps.len() == 2
16109 && let Some(ast::PathStep::Name(n)) = steps.get(1)
16110 && n == "id"
16111 {
16112 let id_filter = IrExpr::BinOp(Box::new(IrBinOp {
16116 left: IrExpr::ColumnRef {
16117 alias: tgt_alias.clone(),
16118 column: "id".to_string(),
16119 pg_type: "uuid".to_string(),
16120 },
16121 op: ast::BinOpKind::Eq,
16122 right: IrExpr::ColumnRef {
16123 alias: jt_alias.to_string(),
16124 column: jt_tgt_col.to_string(),
16125 pg_type: "uuid".to_string(),
16126 },
16127 }));
16128 let fk_ref = IrExpr::ColumnRef {
16129 alias: tgt_alias.clone(),
16130 column: fk_col,
16131 pg_type: "uuid".to_string(),
16132 };
16133 let (fl, fr) = if flip {
16134 (value_expr, fk_ref)
16135 } else {
16136 (fk_ref, value_expr)
16137 };
16138 let fk_filter = IrExpr::BinOp(Box::new(IrBinOp {
16139 left: fl,
16140 op,
16141 right: fr,
16142 }));
16143 let full = IrExpr::BinOp(Box::new(IrBinOp {
16144 left: id_filter,
16145 op: ast::BinOpKind::And,
16146 right: fk_filter,
16147 }));
16148 let inner = IrExpr::Subquery(Box::new(IrSelect::schema_bound(
16149 IrSource {
16150 poly: None,
16151 type_name: target_type.to_string(),
16152 table: target_table,
16153 alias: tgt_alias,
16154 },
16155 vec![],
16156 Some(full),
16157 )));
16158 return Ok(IrExpr::UnaryOp(Box::new(IrUnaryOp {
16159 op: ast::UnaryOpKind::Exists,
16160 operand: inner,
16161 })));
16162 }
16163
16164 let id_filter = IrExpr::BinOp(Box::new(IrBinOp {
16167 left: IrExpr::ColumnRef {
16168 alias: tgt_alias.clone(),
16169 column: "id".to_string(),
16170 pg_type: "uuid".to_string(),
16171 },
16172 op: ast::BinOpKind::Eq,
16173 right: IrExpr::ColumnRef {
16174 alias: jt_alias.to_string(),
16175 column: jt_tgt_col.to_string(),
16176 pg_type: "uuid".to_string(),
16177 },
16178 }));
16179 let nested_filter =
16180 self.compile_path_tail_filter(&steps[1..], op, value_expr, flip, &next_target, &tgt_alias, &fk_col)?;
16181 let full = IrExpr::BinOp(Box::new(IrBinOp {
16182 left: id_filter,
16183 op: ast::BinOpKind::And,
16184 right: nested_filter,
16185 }));
16186 let inner = IrExpr::Subquery(Box::new(IrSelect::schema_bound(
16187 IrSource {
16188 poly: None,
16189 type_name: target_type.to_string(),
16190 table: target_table,
16191 alias: tgt_alias,
16192 },
16193 vec![],
16194 Some(full),
16195 )));
16196 Ok(IrExpr::UnaryOp(Box::new(IrUnaryOp {
16197 op: ast::UnaryOpKind::Exists,
16198 operand: inner,
16199 })))
16200 }
16201}
16202
16203#[derive(Clone)]
16207struct JunctionReadOverride {
16208 junction: String,
16209 targets: Option<String>,
16210}
16211
16212struct ConflictElse {
16216 sets: Vec<(String, IrExpr)>,
16217 predicate: Option<IrExpr>,
16218}
16219
16220impl<'a> Compiler<'a> {
16221 fn compile_conflict(
16223 &mut self,
16224 uc: &ast::UnlessConflict,
16225 td: &TypeDescriptor,
16226 ) -> Result<(IrConflict, Vec<IrMultiLinkMutation>), PyQLError> {
16227 let on = uc.on.as_ref().map(|e| self.compile_expr(e, td, "")).transpose()?;
16230 let mut appends = vec![];
16231 let mut do_update_where = None;
16232 let do_update = match uc.else_.as_ref() {
16233 Some(e) => {
16234 let resolved = self.compile_conflict_else(e, &mut appends)?;
16235 do_update_where = resolved.predicate;
16236 Some(resolved.sets)
16237 }
16238 None => None,
16239 };
16240 Ok((
16241 IrConflict {
16242 on,
16243 do_update,
16244 do_update_where,
16245 },
16246 appends,
16247 ))
16248 }
16249
16250 fn compile_conflict_else(
16265 &mut self,
16266 expr: &Expr,
16267 appends: &mut Vec<IrMultiLinkMutation>,
16268 ) -> Result<ConflictElse, PyQLError> {
16269 let Expr::SubQuery(stmt) = expr else {
16270 return Err(self.type_err("UNLESS CONFLICT ELSE must be an UPDATE expression, e.g. ELSE (UPDATE …)"));
16271 };
16272 if let Stmt::Select(sel) = stmt.as_ref()
16277 && sel.filter.is_none()
16278 && sel.limit.is_none()
16279 && sel.offset.is_none()
16280 && let Ok(type_name) = self.expr_as_type_name(&sel.result)
16281 && let Ok(sel_td) = self.resolve_type(&type_name)
16282 {
16283 let table = sel_td.table.clone();
16284 let pk = sel_td
16285 .properties
16286 .iter()
16287 .find(|p| p.is_pk)
16288 .ok_or_else(|| self.type_err(&format!("type '{type_name}' has no primary key to read back")))?;
16289 return Ok(ConflictElse {
16290 sets: vec![(
16291 pk.name.clone(),
16292 IrExpr::ColumnRef {
16293 alias: table,
16294 column: pk.name.clone(),
16295 pg_type: pk.pg_type.clone(),
16296 },
16297 )],
16298 predicate: None,
16299 });
16300 }
16301 let Stmt::Update(upd) = stmt.as_ref() else {
16302 return Err(self.type_err(
16303 "UNLESS CONFLICT ELSE must be an UPDATE that changes the conflicting row, or a \
16304 SELECT of its type to read it back unchanged",
16305 ));
16306 };
16307 let type_name = self.expr_as_type_name(&upd.subject)?;
16308 let upd_td = self.resolve_type(&type_name)?;
16309 let table = upd_td.table.clone();
16310 let do_update_where = upd
16318 .filter
16319 .as_ref()
16320 .map(|f| self.as_condition(|c| c.compile_expr(f, upd_td, &table)))
16321 .transpose()?;
16322 let mut scalar_shape = vec![];
16329 for el in &upd.shape {
16330 let pointer_name = path_leaf(&el.path)?;
16331 let Some(ml) = Self::resolve_multilink(upd_td, pointer_name) else {
16332 scalar_shape.push(el.clone());
16333 continue;
16334 };
16335 if el.op == ShapeOp::Remove {
16336 return Err(self.type_err(&format!(
16337 "cannot use `-=` for multi-link '{pointer_name}' inside an UNLESS CONFLICT \
16338 ELSE clause; the rows to remove are not known until the conflict resolves"
16339 )));
16340 }
16341 let Some(value) = &el.compexpr else { continue };
16342 let (jt, module, src_col, tgt_col, through_td) = self.own_multilink_junction_info(upd_td, ml)?;
16343 let values = self.compile_multilink_values(value, upd_td, &table, through_td)?;
16344 appends.push(IrMultiLinkMutation {
16345 junction_table: jt,
16346 module,
16347 source_col: src_col,
16348 target_col: tgt_col,
16349 values,
16350 single: false,
16351 });
16352 }
16353 let sets = self.compile_assignments_for_update(&scalar_shape, upd_td, &table)?;
16354 Ok(ConflictElse {
16355 sets,
16356 predicate: do_update_where,
16357 })
16358 }
16359
16360 fn hoist_dml_as_cte(&mut self, stmt: &Stmt) -> Result<(String, String), PyQLError> {
16370 let type_name = self.dml_subject_type(stmt)?;
16371 let before = std::mem::take(&mut self.for_vars_read);
16372 let inner = self.compile_stmt(stmt);
16373 let read = std::mem::replace(&mut self.for_vars_read, before);
16374 let correlated_to = self
16375 .for_scope
16376 .iter()
16377 .rev()
16378 .find(|slot| read.contains(*slot))
16379 .map(|slot| format!("_for_{slot}"));
16380 self.for_vars_read.extend(read);
16381 let cte_name = self.fresh_nested_cte_name();
16382 if let Some(slot) = correlated_to.as_deref().and_then(|i| i.strip_prefix("_for_")) {
16383 self.correlated_bindings.insert(cte_name.clone(), slot.to_string());
16384 }
16385 self.hoisted_ctes.push(IrCteDef {
16386 name: cte_name.clone(),
16387 stmt: inner?,
16388 type_name: type_name.clone(),
16389 correlated_to,
16390 });
16391 Ok((cte_name, type_name))
16392 }
16393
16394 fn for_body_mutates(stmt: &Stmt) -> bool {
16397 match stmt {
16398 Stmt::Insert(_) | Stmt::Update(_) | Stmt::Delete(_) => true,
16399 Stmt::For(inner) => Self::for_body_mutates(&inner.body),
16400 _ => false,
16401 }
16402 }
16403
16404 fn bind_mutating_loop_source(&mut self, result_expr: &Expr) -> Result<Option<Expr>, PyQLError> {
16415 let (subject, shape) = match result_expr {
16416 Expr::SubQuery(stmt) => (stmt.as_ref(), None),
16417 Expr::Shape(sh) => match sh.expr.as_ref() {
16418 Some(Expr::SubQuery(stmt)) => (stmt.as_ref(), Some(sh.as_ref())),
16419 _ => return Ok(None),
16420 },
16421 _ => return Ok(None),
16422 };
16423 if !matches!(subject, Stmt::For(f) if Self::for_body_mutates(&f.body)) {
16424 return Ok(None);
16425 }
16426 let before = std::mem::take(&mut self.for_vars_read);
16430 let inner = self.compile_stmt(subject);
16431 let read = std::mem::replace(&mut self.for_vars_read, before);
16432 let correlated_to = self
16433 .for_scope
16434 .iter()
16435 .rev()
16436 .find(|slot| read.contains(*slot))
16437 .map(|slot| format!("_for_{slot}"));
16438 self.for_vars_read.extend(read);
16439 let inner = inner?;
16440 let cte_name = self.fresh_nested_cte_name();
16441 if let Some(slot) = correlated_to.as_deref().and_then(|i| i.strip_prefix("_for_")) {
16442 self.correlated_bindings.insert(cte_name.clone(), slot.to_string());
16443 }
16444 let type_name = self.register_cte(&cte_name, &inner);
16445 self.hoisted_ctes.push(IrCteDef {
16446 name: cte_name.clone(),
16447 stmt: inner,
16448 type_name,
16449 correlated_to,
16450 });
16451 let bound = Expr::Path(ast::Path {
16452 steps: vec![ast::PathStep::Name(cte_name)],
16453 partial: false,
16454 });
16455 Ok(Some(match shape {
16456 Some(sh) => Expr::Shape(Box::new(ast::ShapeExpr {
16457 expr: Some(bound),
16458 elements: sh.elements.clone(),
16459 marker_offset: sh.marker_offset,
16460 })),
16461 None => bound,
16462 }))
16463 }
16464
16465 fn hoist_nested_dml(&mut self, stmt: &Stmt) -> Result<String, PyQLError> {
16466 let type_name = self.dml_subject_type(stmt)?;
16467 let inner = self.compile_stmt(stmt)?;
16468 let cte_name = self.fresh_nested_cte_name();
16469 self.pending_nested_ctes.push(IrCteDef {
16470 name: cte_name.clone(),
16471 stmt: inner,
16472 type_name,
16473 correlated_to: None,
16474 });
16475 Ok(cte_name)
16476 }
16477
16478 fn compile_link_subquery(&mut self, stmt: &Stmt) -> Result<IrExpr, PyQLError> {
16483 if matches!(stmt, Stmt::Insert(_) | Stmt::Update(_) | Stmt::Delete(_)) {
16487 let cte_name = self.hoist_nested_dml(stmt)?;
16488 return Ok(IrExpr::ColumnRef {
16489 alias: cte_name,
16490 column: "id".to_string(),
16491 pg_type: "uuid".to_string(),
16492 });
16493 }
16494 let Stmt::Select(sel) = stmt else {
16495 return Err(self.type_err(
16496 "only SELECT is valid as a link assignment value; \
16497 use SELECT (INSERT …) { id } to assign from a DML result",
16498 ));
16499 };
16500
16501 let nested_dml = match &sel.result {
16523 Expr::SubQuery(inner) if matches!(inner.as_ref(), Stmt::Insert(_) | Stmt::Update(_) | Stmt::Delete(_)) => {
16524 Some(inner.as_ref())
16525 }
16526 Expr::Shape(s) => match s.expr.as_ref() {
16527 Some(Expr::SubQuery(inner))
16528 if matches!(inner.as_ref(), Stmt::Insert(_) | Stmt::Update(_) | Stmt::Delete(_)) =>
16529 {
16530 Some(inner.as_ref())
16531 }
16532 _ => None,
16533 },
16534 _ => None,
16535 };
16536 if let Some(inner_stmt) = nested_dml {
16537 let cte_name = self.hoist_nested_dml(inner_stmt)?;
16538 return Ok(IrExpr::ColumnRef {
16539 alias: cte_name,
16540 column: "id".to_string(),
16541 pg_type: "uuid".to_string(),
16542 });
16543 }
16544
16545 let type_name = self.expr_as_type_name(&sel.result)?;
16546 let td = self.resolve_type(&type_name)?;
16547 let alias = self.fresh_alias();
16548
16549 let (filter, order_by, offset, limit) = self.compile_path_modifiers(sel, td, &alias)?;
16552 let mut select = IrSelect::schema_bound(
16553 IrSource {
16554 poly: None,
16555 type_name: format!("{}::{}", td.module, td.name),
16556 table: td.table.clone(),
16557 alias,
16558 },
16559 Self::pk_returning(td),
16560 filter,
16561 );
16562 select.order_by = order_by;
16563 select.offset = offset;
16564 select.limit = limit;
16565 Ok(IrExpr::Subquery(Box::new(select)))
16566 }
16567
16568 fn compile_sort_ctx(
16572 &mut self,
16573 s: &ast::SortExpr,
16574 ctx: Option<(&TypeDescriptor, &str)>,
16575 ) -> Result<IrSort, PyQLError> {
16576 Ok(IrSort {
16577 expr: self.compile_expr_ctx(&s.expr, ctx)?,
16578 direction: match s.direction {
16579 SortDirection::Asc => IrSortDir::Asc,
16580 SortDirection::Desc => IrSortDir::Desc,
16581 },
16582 nulls: match s.nones {
16583 NonesOrder::First => IrNulls::First,
16584 NonesOrder::Last => IrNulls::Last,
16585 },
16586 })
16587 }
16588
16589 fn compile_sort(&mut self, s: &ast::SortExpr, td: &TypeDescriptor, alias: &str) -> Result<IrSort, PyQLError> {
16590 self.compile_sort_ctx(s, Some((td, alias)))
16591 }
16592
16593 fn assert_message(
16601 &mut self,
16602 f: &ast::FunctionCall,
16603 ctx: Option<(&TypeDescriptor, &str)>,
16604 ) -> Result<Option<IrExpr>, PyQLError> {
16605 let mut message = None;
16606 for (name, value) in &f.kwargs {
16607 if name != "message" {
16608 return Err(self.type_err(&format!("function 'std::{}' has no parameter '{name}'", f.name)));
16609 }
16610 message = Some(self.compile_expr_ctx(value, ctx)?);
16611 }
16612 Ok(message)
16613 }
16614
16615 fn compile_named_call_args(
16621 &mut self,
16622 f: &ast::FunctionCall,
16623 ctx: Option<(&TypeDescriptor, &str)>,
16624 ) -> Result<Option<Vec<IrExpr>>, PyQLError> {
16625 use crate::stdlib::NamedDefault;
16626
16627 let ns = f.module.as_deref().unwrap_or("std");
16628 let overloads = crate::stdlib::lookup(ns, &f.name);
16629 let positional = |d: &crate::stdlib::FnDescriptor| d.params.iter().filter(|p| p.named_only.is_none()).count();
16630 let named = |d: &crate::stdlib::FnDescriptor| d.params.iter().filter(|p| p.named_only.is_some()).count();
16631 let takes_positionally = |d: &crate::stdlib::FnDescriptor| {
16634 if d.is_variadic() {
16635 f.args.len() + 1 >= positional(d)
16636 } else {
16637 positional(d) == f.args.len()
16638 }
16639 };
16640 if f.kwargs.is_empty() {
16641 if overloads.iter().any(|d| takes_positionally(d) && named(d) == 0) {
16642 return Ok(None);
16643 }
16644 let wrote_a_named_one_positionally =
16649 |d: &crate::stdlib::FnDescriptor| positional(d) < f.args.len() && f.args.len() <= d.params.len();
16650 if !overloads.iter().any(|d| takes_positionally(d))
16651 && let Some(param) = overloads
16652 .iter()
16653 .filter(|d| wrote_a_named_one_positionally(d))
16654 .flat_map(|d| d.params.iter())
16655 .find(|p| p.named_only.is_some())
16656 .map(|p| p.keyword())
16657 {
16658 return Err(self.type_err(&format!(
16659 "function '{ns}::{}' takes '{param}' as a named argument only — e.g. '{param} := …'",
16660 f.name
16661 )));
16662 }
16663 }
16664 let fits = |d: &&&crate::stdlib::FnDescriptor| {
16665 takes_positionally(d)
16666 && named(d) > 0
16667 && f.kwargs
16668 .iter()
16669 .all(|(name, _)| d.params.iter().any(|p| p.keyword() == name && p.named_only.is_some()))
16670 };
16671 let Some(desc) = overloads.iter().find(fits) else {
16672 if let Some((name, _)) = f.kwargs.first() {
16673 let message = if overloads.iter().all(|d| named(d) == 0) {
16674 format!(
16675 "function '{ns}::{}' does not take named arguments, got '{name}'",
16676 f.name
16677 )
16678 } else {
16679 format!("function '{ns}::{}' has no parameter '{name}'", f.name)
16680 };
16681 return Err(self.type_err(&message));
16682 }
16683 return Ok(None);
16684 };
16685 let mut args = f
16686 .args
16687 .iter()
16688 .map(|a| self.compile_expr_ctx(a, ctx))
16689 .collect::<Result<Vec<_>, _>>()?;
16690 for param in desc.params.iter().filter(|p| p.named_only.is_some()) {
16691 let arg = match (
16692 f.kwargs.iter().find(|(name, _)| name == param.keyword()),
16693 param.named_only,
16694 ) {
16695 (Some((_, value)), _) => self.compile_expr_ctx(value, ctx)?,
16696 (None, Some(NamedDefault::Required)) => {
16697 let keyword = param.keyword();
16698 return Err(self.type_err(&format!(
16699 "function '{ns}::{}' requires the named argument '{keyword}' — e.g. '{keyword} := …'",
16700 f.name
16701 )));
16702 }
16703 (None, Some(NamedDefault::Int(n))) => IrExpr::Literal(IrLiteral::Int(n)),
16704 (None, Some(NamedDefault::Bool(b))) => IrExpr::Literal(IrLiteral::Bool(b)),
16705 (None, Some(NamedDefault::Str(s))) => IrExpr::Literal(IrLiteral::Str(s.to_string())),
16706 (None, _) => IrExpr::Null,
16707 };
16708 args.push(arg);
16709 }
16710 Ok(Some(args))
16711 }
16712
16713 fn resolve_fn_call(&mut self, module: Option<&str>, name: &str, args: Vec<IrExpr>) -> Result<IrExpr, PyQLError> {
16714 use crate::stdlib::{ImplStrategy, lookup};
16715
16716 let ns = module.unwrap_or("std");
16717 if ns == "std" && matches!(name, "any" | "all") && args.len() == 1 && !is_array_expr(&args[0]) {
16722 return Ok(args.into_iter().next().expect("checked by the guard"));
16723 }
16724 let overloads = lookup(ns, name);
16725
16726 let by_arity: Vec<&crate::stdlib::FnDescriptor> = overloads
16734 .iter()
16735 .copied()
16736 .filter(|d| {
16737 if d.is_variadic() {
16738 args.len() + 1 >= d.params.len()
16739 } else {
16740 d.params.len() == args.len()
16741 }
16742 })
16743 .collect();
16744
16745 let exact = by_arity.iter().copied().find(|d| {
16751 params_for_args(d, args.len())
16752 .zip(&args)
16753 .any(|(p, a)| p.ty.scalar_pg_type().is_some() && infer_ir_type(a).is_some())
16754 && params_for_args(d, args.len()).zip(&args).all(|(p, a)| {
16755 match (p.ty.scalar_pg_type(), infer_ir_type(a)) {
16756 (Some(declared), Some(known)) => declared == known,
16757 _ => pylon_type_matches(a, &p.ty),
16758 }
16759 })
16760 });
16761 let best = exact.or_else(|| {
16762 by_arity.iter().copied().find(|d| {
16763 params_for_args(d, args.len())
16764 .zip(&args)
16765 .all(|(p, a)| pylon_type_matches(a, &p.ty))
16766 })
16767 });
16768
16769 let args_len = args.len();
16774 let args: Vec<IrExpr> = match best {
16775 Some(descriptor) => args
16776 .into_iter()
16777 .enumerate()
16778 .map(
16779 |(i, arg)| match params_for_args(descriptor, args_len).nth(i).map(|p| &p.ty) {
16780 Some(crate::stdlib::PylonType::Set(_)) | Some(crate::stdlib::PylonType::Array(_)) | None => arg,
16781 Some(_) => set_walk_as_scalar(arg),
16782 },
16783 )
16784 .collect(),
16785 None => args,
16786 };
16787 let args = match best {
16788 Some(descriptor) => pack_variadic_args(descriptor, args),
16789 None => args,
16790 };
16791
16792 let stdlib_return = best
16796 .map(|d| match &d.return_type {
16797 crate::stdlib::PylonType::Optional(inner) => inner.as_ref(),
16798 other => other,
16799 })
16800 .and_then(|ty| ty.scalar_pg_type())
16801 .map(str::to_string);
16802 let (schema, resolved_name, sql_template) = if let Some(desc) = best {
16803 match &desc.impl_strategy {
16804 ImplStrategy::SqlBuiltin(sql_name) => (None, sql_name.to_string(), None),
16805 ImplStrategy::SqlExpression(tmpl) => (None, name.to_string(), Some(tmpl.to_string())),
16806 ImplStrategy::PylonFunction(def) => (Some("_pylon".to_string()), def.name.to_string(), None),
16807 ImplStrategy::SqlOperator(op) if args.len() == 2 => {
16814 (None, name.to_string(), Some(format!("($1 {op} $2)")))
16815 }
16816 ImplStrategy::TranspilerIntrinsic(intrinsic) => {
16817 return self.compile_range_intrinsic(intrinsic, name, args);
16818 }
16819 _ => (module.map(str::to_string), name.to_string(), None),
16821 }
16822 } else {
16823 let candidates: Vec<&FunctionDescriptor> = self
16839 .schema
16840 .functions
16841 .iter()
16842 .filter(|f| {
16843 let module_matches = module.map(|m| m == f.module.as_str()).unwrap_or(true);
16844 module_matches && f.name == name && !f.return_is_object
16845 })
16846 .collect();
16847 let user_fn = candidates
16848 .iter()
16849 .find(|f| f.params.len() == args.len())
16850 .copied()
16851 .or_else(|| candidates.first().copied());
16852 if let Some(fd) = user_fn {
16853 if fd.params.len() != args.len() {
16854 return Err(self.type_err(&format!(
16855 "function '{}::{}' expects {} argument(s), got {}",
16856 fd.module,
16857 fd.name,
16858 fd.params.len(),
16859 args.len()
16860 )));
16861 }
16862 let cast_args = fd
16863 .params
16864 .iter()
16865 .zip(args)
16866 .map(|(p, a)| {
16867 IrExpr::TypeCast(Box::new(super::IrTypeCast {
16868 expr: a,
16869 pg_type: p.pg_type.clone(),
16870 tuple_shape: None,
16871 }))
16872 })
16873 .collect();
16874 let qualified = format!("{}::{}", fd.module, fd.name);
16875 let fn_module = fd.module.clone();
16876 let fn_name = fd.name.clone();
16877 let mut call_args: Vec<IrExpr> = cast_args;
16878 if let Some(globals) = self.globals_arg_for_call(&qualified)? {
16879 call_args.insert(0, globals);
16880 }
16881 let return_pg_type = (!fd.return_is_set).then(|| fd.return_pg_type.clone());
16886 return Ok(IrExpr::FunctionCall(super::IrFunctionCall {
16887 return_pg_type,
16888 schema: Some(fn_module),
16889 name: fn_name,
16890 args: call_args,
16891 sql_template: None,
16892 }));
16893 }
16894 if let Some(fd) = self.schema.functions.iter().find(|f| {
16901 let module_matches = module.map(|m| m == f.module.as_str()).unwrap_or(true);
16902 module_matches && f.name == name && f.return_is_object
16903 }) {
16904 return Err(self.type_err(&format!(
16905 "function '{}::{}' returns objects, so it can only be the subject of a \
16906 select (`select {}::{}(…) {{ … }}`), not part of a larger expression",
16907 fd.module, fd.name, fd.module, fd.name
16908 )));
16909 }
16910 let qualified = match module {
16911 Some(m) => format!("{m}::{name}"),
16912 None => name.to_string(),
16913 };
16914 if !overloads.is_empty() {
16919 return Err(self.type_err(&if by_arity.is_empty() {
16920 format!(
16921 "function '{qualified}' takes {}, got {}",
16922 describe_arities(&overloads),
16923 args.len()
16924 )
16925 } else {
16926 format!(
16927 "function '{qualified}' has no overload accepting ({}) — it accepts {}",
16928 describe_args(&args),
16929 describe_signatures(&by_arity),
16930 )
16931 }));
16932 }
16933 return Err(self.type_err(&format!(
16934 "function '{qualified}' does not exist{}",
16935 self.suggest_function_name(ns, name)
16936 )));
16937 };
16938
16939 Ok(IrExpr::FunctionCall(super::IrFunctionCall {
16940 return_pg_type: stdlib_return,
16941 schema,
16942 name: resolved_name,
16943 args,
16944 sql_template,
16945 }))
16946 }
16947
16948 fn compile_range_intrinsic(&self, intrinsic: &str, name: &str, args: Vec<IrExpr>) -> Result<IrExpr, PyQLError> {
16957 match intrinsic {
16958 "range" => {
16959 let bounds = args.len().saturating_sub(3);
16962 let point_ty = args[..bounds].iter().find_map(infer_ir_type).ok_or_else(|| {
16963 self.type_err(
16964 "range(): cannot infer the element type from either bound — \
16965 use an explicit cast, e.g. range(<int64>$lower, <int64>$upper)",
16966 )
16967 })?;
16968 let ctor = range_ctor_for_pg_type(point_ty).ok_or_else(|| {
16969 self.type_err(&format!(
16970 "range(): unsupported element type '{point_ty}' — PostgreSQL only has native \
16971 ranges over int64, decimal, datetime, cal::local_datetime, and cal::local_date"
16972 ))
16973 })?;
16974 let upper = match args.len() {
16978 4 => "NULL",
16979 5 => "$2",
16980 n => return Err(self.type_err(&format!("range(): unexpected argument count {n}"))),
16981 };
16982 let flags: Vec<Option<bool>> = args[args.len() - 3..].iter().map(bool_literal).collect();
16983 let sql_template = match flags[..] {
16987 [Some(_), Some(_), Some(true)] => format!("'empty'::{ctor}"),
16988 [Some(true), Some(false), Some(false)] => format!("{ctor}($1, {upper})"),
16989 [Some(inc_lower), Some(inc_upper), Some(false)] => format!(
16990 "{ctor}($1, {upper}, '{}{}')",
16991 if inc_lower { '[' } else { '(' },
16992 if inc_upper { ']' } else { ')' },
16993 ),
16994 _ => {
16995 let (lower_inc, upper_inc, empty) = match args.len() {
16996 4 => ("$2", "$3", "$4"),
16997 _ => ("$3", "$4", "$5"),
16998 };
16999 format!(
17000 "(CASE WHEN {empty} THEN 'empty'::{ctor} ELSE {ctor}($1, {upper}, \
17001 (CASE WHEN {lower_inc} THEN '[' ELSE '(' END) \
17002 || (CASE WHEN {upper_inc} THEN ']' ELSE ')' END)) END)"
17003 )
17004 }
17005 };
17006 Ok(IrExpr::FunctionCall(super::IrFunctionCall {
17017 return_pg_type: Some(ctor.to_string()),
17018 schema: None,
17019 name: ctor.to_string(),
17020 args,
17021 sql_template: Some(sql_template),
17022 }))
17023 }
17024 "multirange" => {
17025 let Some(IrExpr::Array(elems)) = args.first() else {
17026 return Err(self.type_err("multirange(): argument must be an array literal of ranges"));
17027 };
17028 let first_ctor = elems
17029 .first()
17030 .and_then(|e| match e {
17031 IrExpr::FunctionCall(fc) => Some(fc.name.as_str()),
17032 _ => None,
17033 })
17034 .ok_or_else(|| {
17035 self.type_err(
17036 "multirange(): cannot infer the element range type from an empty or non-range \
17037 array — pass at least one range(...) call, e.g. multirange([range(1, 3)])",
17038 )
17039 })?;
17040 let ctor = multirange_ctor_for_range_ctor(first_ctor).ok_or_else(|| {
17041 self.type_err(&format!("multirange(): unrecognized range constructor '{first_ctor}'"))
17042 })?;
17043 Ok(IrExpr::FunctionCall(super::IrFunctionCall {
17047 return_pg_type: Some(ctor.to_string()),
17048 schema: None,
17049 name: name.to_string(),
17050 args,
17051 sql_template: Some(format!("{ctor}(VARIADIC $1)")),
17052 }))
17053 }
17054 other => Err(self.type_err(&format!("internal error: unhandled TranspilerIntrinsic '{other}'"))),
17055 }
17056 }
17057
17058 fn compile_sequence_fn(&mut self, fc: &ast::FunctionCall) -> Result<IrExpr, PyQLError> {
17063 let (module, scalar_name) = self.resolve_sequence_scalar_arg(fc)?;
17064
17065 if fc.name == "sequence_next" {
17066 if fc.args.len() != 1 {
17067 return Err(self.type_err("sequence_next takes exactly 1 argument"));
17068 }
17069 let sql = format!("nextval('\"{}\".\"{}_seq\"')", module, scalar_name);
17070 return Ok(IrExpr::FunctionCall(super::IrFunctionCall {
17071 return_pg_type: None,
17072 schema: None,
17073 name: "nextval".into(),
17074 args: vec![],
17075 sql_template: Some(sql),
17076 }));
17077 }
17078
17079 match fc.args.len() {
17081 1 => {
17082 let sql = format!("setval('\"{}\".\"{}_seq\"', 1, false)", module, scalar_name);
17083 Ok(IrExpr::FunctionCall(super::IrFunctionCall {
17084 return_pg_type: None,
17085 schema: None,
17086 name: "setval".into(),
17087 args: vec![],
17088 sql_template: Some(sql),
17089 }))
17090 }
17091 2 => {
17092 let val = self.compile_free_expr(&fc.args[1])?;
17093 let sql = format!("setval('\"{}\".\"{}_seq\"', $1, true)", module, scalar_name);
17094 Ok(IrExpr::FunctionCall(super::IrFunctionCall {
17095 return_pg_type: None,
17096 schema: None,
17097 name: "setval".into(),
17098 args: vec![val],
17099 sql_template: Some(sql),
17100 }))
17101 }
17102 _ => Err(self.type_err("sequence_reset takes 1 or 2 arguments")),
17103 }
17104 }
17105
17106 fn resolve_sequence_scalar_arg(&self, fc: &ast::FunctionCall) -> Result<(String, String), PyQLError> {
17109 use crate::parse::ast::{Expr, Path, PathStep};
17110
17111 let arg = fc.args.first().ok_or_else(|| {
17112 self.type_err(&format!(
17113 "{}() requires a sequence scalar type as its first argument",
17114 fc.name
17115 ))
17116 })?;
17117
17118 let (arg_module, arg_name): (Option<&str>, &str) = match arg {
17121 Expr::Path(Path { steps, partial: false }) => match steps.as_slice() {
17122 [PathStep::Name(s)] => {
17123 if let Some((m, n)) = s.split_once("::") {
17124 (Some(m), n)
17125 } else {
17126 (None, s.as_str())
17127 }
17128 }
17129 _ => return Err(self.type_err(&format!(
17130 "{}(): first argument must be a sequence scalar type name (e.g. OrderNumber or default::OrderNumber)",
17131 fc.name
17132 ))),
17133 },
17134 _ => return Err(self.type_err(&format!(
17135 "{}(): first argument must be a sequence scalar type name (e.g. OrderNumber or default::OrderNumber)",
17136 fc.name
17137 ))),
17138 };
17139
17140 let scalar = self.schema.scalars.iter().find(|s| {
17141 s.is_sequence && s.name == arg_name && arg_module.map(|m| m == s.module.as_str()).unwrap_or(true)
17142 });
17143
17144 match scalar {
17145 Some(s) => Ok((s.module.clone(), s.name.clone())),
17146 None => Err(self.type_err(&format!(
17147 "{}(): '{}' is not a known sequence scalar type",
17148 fc.name, arg_name
17149 ))),
17150 }
17151 }
17152
17153 const NOTIFY_PAYLOAD_MAX_BYTES: usize = 8000;
17162
17163 fn notify_type_payload_err(&self, channel: &str, qname: &str) -> PyQLError {
17165 self.type_err(&format!(
17166 "notify(): payload for Channel '{channel}' (a '{qname}' object channel) must name an object of that \
17167 type — either a with-block binding, or __new__/__old__ inside a trigger handler"
17168 ))
17169 }
17170
17171 fn compile_notify(
17181 &mut self,
17182 fc: &ast::FunctionCall,
17183 ctx: Option<(&TypeDescriptor, &str)>,
17184 ) -> Result<IrExpr, PyQLError> {
17185 use crate::parse::ast::{Expr, Path, PathStep};
17186
17187 if fc.args.len() != 2 {
17188 return Err(self.type_err("notify() takes exactly 2 arguments: (Channel, payload)"));
17189 }
17190
17191 let (channel_module, channel_name): (Option<&str>, &str) = match &fc.args[0] {
17192 Expr::Path(Path { steps, partial: false }) => match steps.as_slice() {
17193 [PathStep::Name(s)] => {
17194 if let Some((m, n)) = s.split_once("::") {
17195 (Some(m), n)
17196 } else {
17197 (None, s.as_str())
17198 }
17199 }
17200 _ => {
17201 return Err(self.type_err(
17202 "notify(): first argument must be a Channel name (e.g. OrderEvents or orders::OrderEvents)",
17203 ));
17204 }
17205 },
17206 _ => {
17207 return Err(self.type_err(
17208 "notify(): first argument must be a Channel name (e.g. OrderEvents or orders::OrderEvents)",
17209 ));
17210 }
17211 };
17212 let full_channel_name = match channel_module {
17213 Some(m) => format!("{m}::{channel_name}"),
17214 None => channel_name.to_string(),
17215 };
17216 let channel = self
17217 .resolve_channel(&full_channel_name)
17218 .ok_or_else(|| self.type_err(&format!("notify(): '{channel_name}' is not a known Channel")))?;
17219 let wire_name = channel.wire_name.clone();
17220 let payload_arg = &fc.args[1];
17221
17222 let anchor_fallback: Option<(&'a TypeDescriptor, String)> = self.special_anchors.values().next().cloned();
17236 let ctx = ctx.or_else(|| anchor_fallback.as_ref().map(|(td, alias)| (*td, alias.as_str())));
17237
17238 let payload_ir = match &channel.payload {
17239 crate::schema::ChannelPayload::Type(qname) => {
17240 let Expr::Path(Path { steps, partial: false }) = payload_arg else {
17249 return Err(self.notify_type_payload_err(&full_channel_name, qname));
17250 };
17251 let [PathStep::Name(name)] = steps.as_slice() else {
17252 return Err(self.notify_type_payload_err(&full_channel_name, qname));
17253 };
17254
17255 if name == "__new__" || name == "__old__" {
17256 let (anchor_td, alias) = self.special_anchors.get(name.as_str()).cloned().ok_or_else(|| {
17257 self.type_err(&format!(
17258 "notify(): '{name}' cannot be used here — it's only bound inside a trigger handler"
17259 ))
17260 })?;
17261 let anchor_qname = format!("{}::{}", anchor_td.module, anchor_td.name);
17262 if &anchor_qname != qname {
17263 return Err(self.type_err(&format!(
17264 "notify(): Channel '{full_channel_name}' expects a payload of type '{qname}', got '{anchor_qname}'"
17265 )));
17266 }
17267 IrExpr::ColumnRef {
17268 alias,
17269 column: "id".to_string(),
17270 pg_type: "uuid".to_string(),
17271 }
17272 } else if let Some(cte_type) = self.cte_types.get(name.as_str()).cloned() {
17273 if &cte_type != qname {
17274 return Err(self.type_err(&format!(
17275 "notify(): Channel '{full_channel_name}' expects a payload of type '{qname}', got '{cte_type}'"
17276 )));
17277 }
17278 IrExpr::CteRef {
17281 name: name.clone(),
17282 scalar: false,
17283 pg_type: None,
17284 }
17285 } else {
17286 return Err(self.notify_type_payload_err(&full_channel_name, qname));
17287 }
17288 }
17289 crate::schema::ChannelPayload::Scalar(pg_type) => {
17290 let ir = self.compile_expr_ctx(payload_arg, ctx)?;
17291 if let Some(actual) = infer_ir_type(&ir)
17292 && !types_compatible(actual, pg_type)
17293 {
17294 return Err(self.type_err(&format!(
17295 "notify(): Channel '{full_channel_name}' expects a payload of type '{expected}', got '{actual_pyql}'",
17296 expected = pg_type_to_pyql(pg_type),
17297 actual_pyql = pg_type_to_pyql(actual),
17298 )));
17299 }
17300 ir
17301 }
17302 crate::schema::ChannelPayload::Object(declared_fields) => {
17303 let Expr::Shape(sh) = payload_arg else {
17304 return Err(self.type_err(&format!(
17305 "notify(): payload for Channel '{full_channel_name}' (an Object channel) must be a free \
17306 object literal, e.g. {{ {} }}",
17307 declared_fields
17308 .iter()
17309 .map(|(n, _)| format!("{n} := .."))
17310 .collect::<Vec<_>>()
17311 .join(", ")
17312 )));
17313 };
17314 if sh.expr.is_some() {
17315 return Err(self.type_err(&format!(
17316 "notify(): payload for Channel '{full_channel_name}' (an Object channel) must be a free \
17317 object literal, not a shape over a type"
17318 )));
17319 }
17320 let ir = self.compile_expr_ctx(payload_arg, ctx)?;
17321 let IrExpr::NamedTuple {
17322 fields,
17323 is_free_object: true,
17324 } = &ir
17325 else {
17326 return Err(self.type_err(&format!(
17327 "notify(): payload for Channel '{full_channel_name}' (an Object channel) must be a free object literal"
17328 )));
17329 };
17330 let declared_names: std::collections::HashSet<&str> =
17331 declared_fields.iter().map(|(n, _)| n.as_str()).collect();
17332 let actual_names: std::collections::HashSet<&str> = fields.iter().map(|(n, _)| n.as_str()).collect();
17333 if declared_names != actual_names {
17334 let mut expected: Vec<&str> = declared_names.iter().copied().collect();
17335 expected.sort();
17336 let mut actual: Vec<&str> = actual_names.iter().copied().collect();
17337 actual.sort();
17338 return Err(self.type_err(&format!(
17339 "notify(): payload fields for Channel '{full_channel_name}' don't match — expected {{{}}}, got {{{}}}",
17340 expected.join(", "), actual.join(", ")
17341 )));
17342 }
17343 for (name, expr) in fields {
17344 let Some((_, declared_pg_type)) = declared_fields.iter().find(|(n, _)| n == name) else {
17345 continue;
17346 };
17347 if let Some(actual) = infer_ir_type(expr)
17348 && !types_compatible(actual, declared_pg_type)
17349 {
17350 return Err(self.type_err(&format!(
17351 "notify(): field '{name}' of Channel '{full_channel_name}' expects type '{expected}', got '{actual_pyql}'",
17352 expected = pg_type_to_pyql(declared_pg_type),
17353 actual_pyql = pg_type_to_pyql(actual),
17354 )));
17355 }
17356 }
17357 ir
17358 }
17359 };
17360
17361 let floor = static_min_payload_bytes(payload_arg);
17362 if floor >= Self::NOTIFY_PAYLOAD_MAX_BYTES {
17363 return Err(self.type_err(&format!(
17364 "notify(): this payload is at least {floor} bytes, which is at or over PostgreSQL's {}-byte NOTIFY \
17365 payload limit — the notification would fail at runtime, aborting the transaction that sent it",
17366 Self::NOTIFY_PAYLOAD_MAX_BYTES
17367 )));
17368 }
17369
17370 let sql = format!("pg_notify('{}', ($1)::text)", wire_name.replace('\'', "''"));
17371 Ok(IrExpr::FunctionCall(super::IrFunctionCall {
17372 return_pg_type: None,
17373 schema: None,
17374 name: "pg_notify".to_string(),
17375 args: vec![payload_ir],
17376 sql_template: Some(sql),
17377 }))
17378 }
17379
17380 fn compile_notify_raw(
17384 &mut self,
17385 fc: &ast::FunctionCall,
17386 ctx: Option<(&TypeDescriptor, &str)>,
17387 ) -> Result<IrExpr, PyQLError> {
17388 if fc.args.len() != 2 {
17389 return Err(self.type_err("notify_raw() takes exactly 2 arguments: (channel_name, payload)"));
17390 }
17391 let channel_ir = self.compile_expr_ctx(&fc.args[0], ctx)?;
17392 let payload_ir = self.compile_expr_ctx(&fc.args[1], ctx)?;
17393
17394 let floor = static_min_payload_bytes(&fc.args[1]);
17395 if floor >= Self::NOTIFY_PAYLOAD_MAX_BYTES {
17396 return Err(self.type_err(&format!(
17397 "notify_raw(): this payload is at least {floor} bytes, which is at or over PostgreSQL's {}-byte \
17398 NOTIFY payload limit",
17399 Self::NOTIFY_PAYLOAD_MAX_BYTES
17400 )));
17401 }
17402
17403 Ok(IrExpr::FunctionCall(super::IrFunctionCall {
17404 return_pg_type: None,
17405 schema: None,
17406 name: "pg_notify".to_string(),
17407 args: vec![channel_ir, payload_ir],
17408 sql_template: None,
17409 }))
17410 }
17411
17412 fn globals_arg_for_call(&mut self, qualified: &str) -> Result<Option<IrExpr>, PyQLError> {
17421 if self.fns_needing_globals.is_none() {
17422 self.fns_needing_globals = Some(functions_needing_globals(self.schema).clone());
17423 }
17424 if !self.fns_needing_globals.as_ref().is_some_and(|s| s.contains(qualified)) {
17425 return Ok(None);
17426 }
17427 if self.in_fn_body {
17428 self.used_globals_arg = true;
17429 return Ok(Some(IrExpr::RawSql(GLOBALS_ARG.to_string())));
17430 }
17431 let session_globals: Vec<String> = self
17432 .schema
17433 .globals
17434 .iter()
17435 .filter(|g| g.computed_expr.is_none())
17436 .map(|g| format!("{}::{}", g.module, g.name))
17437 .collect();
17438 let mut args = Vec::with_capacity(session_globals.len() * 2);
17439 for name in session_globals {
17440 let value = self.compile_global(&name)?;
17441 args.push(IrExpr::Literal(IrLiteral::Str(name)));
17442 args.push(value);
17443 }
17444 Ok(Some(IrExpr::FunctionCall(super::IrFunctionCall {
17445 return_pg_type: None,
17446 schema: None,
17447 name: "jsonb_build_object".to_string(),
17448 args,
17449 sql_template: None,
17450 })))
17451 }
17452
17453 fn try_compile_fn_object_select(
17459 &mut self,
17460 fc: &ast::FunctionCall,
17461 elements: &[ast::ShapeElement],
17462 s: &ast::SelectStmt,
17463 distinct: bool,
17464 ) -> Result<Option<IrFunctionSelect>, PyQLError> {
17465 use crate::schema::FunctionDescriptor;
17466
17467 let fd: Option<&FunctionDescriptor> = self.schema.functions.iter().find(|f| {
17468 let module_matches = fc.module.as_deref().map(|m| m == f.module.as_str()).unwrap_or(true);
17469 module_matches && f.name == fc.name && f.return_is_object
17470 });
17471 let fd = match fd {
17472 Some(f) => f,
17473 None => return Ok(None),
17474 };
17475 if fd.params.len() != fc.args.len() {
17476 return Err(self.type_err(&format!(
17477 "function '{}::{}' expects {} argument(s), got {}",
17478 fd.module,
17479 fd.name,
17480 fd.params.len(),
17481 fc.args.len()
17482 )));
17483 }
17484
17485 let fn_module = fd.module.clone();
17486 let fn_name = fd.name.clone();
17487 let return_type_name = fd.return_pg_type.clone(); let polymorphic = fd.return_is_polymorphic;
17489
17490 let mut fn_args = fc
17491 .args
17492 .iter()
17493 .map(|a| self.compile_free_expr(a))
17494 .collect::<Result<Vec<_>, _>>()?;
17495 let qualified = format!("{}::{}", fn_module, fn_name);
17496 if let Some(globals) = self.globals_arg_for_call(&qualified)? {
17497 fn_args.insert(0, globals);
17498 }
17499
17500 let alias = self.fresh_alias();
17501
17502 let td = self.resolve_type(&return_type_name)?;
17504 let td = td.clone();
17505
17506 let (poly_implementors, poly_columns) = if polymorphic {
17507 self.collect_poly_info(&return_type_name)
17508 } else {
17509 (vec![], vec![])
17510 };
17511
17512 let td_module = td.module.clone();
17513 let shape = self.compile_shape(elements, &td, &alias, &td_module)?;
17515 let outer_declared = self.active_declared_pointers.clone();
17519 self.active_declared_pointers
17520 .extend(elements.iter().filter(|el| el.compexpr.is_some()).cloned());
17521 let modifiers = self.compile_path_modifiers(s, &td, &alias);
17522 self.active_declared_pointers = outer_declared;
17523 let (filter, order_by, offset, limit) = modifiers?;
17524
17525 Ok(Some(IrFunctionSelect {
17526 fn_module,
17527 fn_name,
17528 fn_args,
17529 alias,
17530 type_name: return_type_name,
17531 polymorphic,
17532 poly_implementors,
17533 poly_columns,
17534 shape,
17535 filter,
17536 order_by,
17537 offset,
17538 limit,
17539 distinct,
17540 }))
17541 }
17542
17543 fn read_nested_links_from_their_ctes(dml: &IrStmt, dml_cte: Option<&str>, shape: &mut [IrShapePointer]) {
17560 let (assignments, nested_ctes, appends) = match dml {
17561 IrStmt::Insert(ins) => (&ins.assignments, &ins.nested_ctes, &ins.multi_link_appends),
17562 IrStmt::Update(upd) => (&upd.assignments, &upd.nested_ctes, &upd.multi_link_appends),
17563 _ => return,
17564 };
17565 if let Some(dml_cte) = dml_cte {
17571 for pointer in shape.iter_mut() {
17572 let IrShapePointer::MultiLink(link) = pointer else {
17573 continue;
17574 };
17575 let IrMultiLinkJoin::Standard { junction_table, .. } = &mut link.join else {
17576 continue;
17577 };
17578 let Some(index) = appends.iter().position(|a| a.junction_table == *junction_table) else {
17579 continue;
17580 };
17581 *junction_table = format!("@cte:{dml_cte}__ml_add_{index}");
17582 if let IrMultiLinkValueSource::CteRef(target_cte) = &appends[index].values.source {
17583 for row in &mut link.subquery.rows {
17584 if let IrRowSource::Bound { source, .. } = row {
17585 source.table = format!("@cte:{target_cte}");
17586 source.poly = None;
17587 }
17588 }
17589 }
17590 }
17591 }
17592 if nested_ctes.is_empty() {
17593 return;
17594 }
17595 let from_cte: HashMap<&str, &str> = assignments
17596 .iter()
17597 .filter_map(|(column, expr)| match expr {
17598 IrExpr::ColumnRef { alias, column: c, .. }
17599 if c == "id" && nested_ctes.iter().any(|cte| cte.name == *alias) =>
17600 {
17601 Some((column.as_str(), alias.as_str()))
17602 }
17603 _ => None,
17604 })
17605 .collect();
17606 if from_cte.is_empty() {
17607 return;
17608 }
17609 for pointer in shape {
17610 let IrShapePointer::SingleLink(link) = pointer else {
17611 continue;
17612 };
17613 let IrSingleLinkCorrelation::Fk { fk_column, .. } = &link.correlation else {
17614 continue;
17615 };
17616 let Some(cte_name) = from_cte.get(fk_column.as_str()) else {
17617 continue;
17618 };
17619 for row in &mut link.subquery.rows {
17620 if let IrRowSource::Bound { source, .. } = row {
17621 source.table = format!("@cte:{cte_name}");
17622 source.poly = None;
17623 }
17624 }
17625 }
17626 }
17627
17628 fn poly_dml_columns(td: &TypeDescriptor) -> Vec<String> {
17629 td.properties
17630 .iter()
17631 .map(|p| p.name.clone())
17632 .chain(
17633 td.links
17634 .iter()
17635 .filter(|l| !l.is_junction_backed())
17636 .map(|l| format!("{}_id", l.name)),
17637 )
17638 .collect()
17639 }
17640
17641 fn link_target_select(
17651 &self,
17652 target_td: &TypeDescriptor,
17653 mut source: IrSource,
17654 shape: Vec<IrShapePointer>,
17655 ) -> IrSelect {
17656 source.poly = self.link_target_fanout(target_td);
17657 IrSelect::schema_bound(source, shape, None)
17658 }
17659
17660 fn link_target_fanout(&self, target_td: &TypeDescriptor) -> Option<IrPolyFanout> {
17663 self.poly_fanout_for(&format!("{}::{}", target_td.module, target_td.name))
17664 }
17665
17666 fn resolve_join_fanouts(&self, path_select: &mut IrPathSelect) {
17673 for join in &mut path_select.joins {
17674 let target = match join {
17675 IrPathJoin::Single { target, .. }
17676 | IrPathJoin::Multi { target, .. }
17677 | IrPathJoin::BacklinkSingle { target, .. }
17678 | IrPathJoin::BacklinkMulti { target, .. }
17679 | IrPathJoin::Function { target, .. }
17680 | IrPathJoin::Lateral { target, .. } => target,
17681 };
17682 if target.poly.is_none() && !target.table.starts_with("@cte:") {
17683 target.poly = self.poly_fanout_for(&target.type_name.clone());
17684 }
17685 }
17686 }
17687
17688 fn poly_fanout_for(&self, type_name: &str) -> Option<IrPolyFanout> {
17691 let td = self
17692 .schema
17693 .types
17694 .iter()
17695 .find(|t| format!("{}::{}", t.module, t.name) == type_name)?;
17696 if !td.abstract_ && !self.has_subtypes(td) {
17700 return None;
17701 }
17702 let (implementors, columns) = self.collect_poly_info(type_name);
17703 Some(IrPolyFanout { implementors, columns })
17704 }
17705
17706 fn collect_poly_info(&self, type_name: &str) -> (Vec<IrPolyImplementor>, Vec<String>) {
17708 let implementors = self.find_poly_implementors(type_name);
17709 let columns = if let Some(td) = self
17710 .schema
17711 .types
17712 .iter()
17713 .find(|t| format!("{}::{}", t.module, t.name) == type_name)
17714 {
17715 Self::poly_dml_columns(td)
17720 } else {
17721 vec![]
17722 };
17723 (implementors, columns)
17724 }
17725
17726 fn try_compile_vector_search(
17737 &mut self,
17738 fc: &ast::FunctionCall,
17739 elements: &[ast::ShapeElement],
17740 s: &ast::SelectStmt,
17741 ) -> Result<Option<IrVectorSearch>, PyQLError> {
17742 if fc.module.as_deref() != Some("vector") || fc.name != "search" {
17743 return Ok(None);
17744 }
17745
17746 let text_query_arg = fc.kwargs.iter().find(|(k, _)| k == "query").map(|(_, v)| v);
17748 let is_text_overload = text_query_arg.is_some();
17749
17750 if !is_text_overload && fc.args.len() < 2 {
17751 return Err(
17752 self.type_err("vector::search requires either a positional vector argument or `query := $text`")
17753 );
17754 }
17755
17756 let (type_qname, inner_filter_ast): (String, Option<ast::Expr>) = match &fc.args[0] {
17763 ast::Expr::Path(p) if !p.partial => {
17764 let name = p.steps.iter()
17765 .filter_map(|s| if let ast::PathStep::Name(n) = s { Some(n.as_str()) } else { None })
17766 .collect::<Vec<_>>().join("::");
17767 let td = self.resolve_type(&name)
17768 .map_err(|_| self.type_err(&format!("vector::search: '{}' is not a known type", name)))?;
17769 (format!("{}::{}", td.module, td.name), None)
17770 }
17771 ast::Expr::SubQuery(stmt) => {
17772 if let ast::Stmt::Select(inner_sel) = stmt.as_ref() {
17773 let inner_type_name = match &inner_sel.result {
17774 ast::Expr::Path(p) if !p.partial => {
17775 p.steps.iter()
17776 .filter_map(|s| if let ast::PathStep::Name(n) = s { Some(n.as_str()) } else { None })
17777 .collect::<Vec<_>>().join("::")
17778 }
17779 _ => return Err(self.type_err(
17780 "vector::search: subquery first argument must select a single type (e.g. select Product filter …)"
17781 )),
17782 };
17783 let td = self.resolve_type(&inner_type_name)
17784 .map_err(|_| self.type_err(&format!("vector::search: '{}' is not a known type", inner_type_name)))?;
17785 let qname = format!("{}::{}", td.module, td.name);
17786 (qname, inner_sel.filter.clone())
17787 } else {
17788 return Err(self.type_err("vector::search: subquery first argument must be a SELECT"));
17789 }
17790 }
17791 _ => return Err(self.type_err(
17792 "vector::search: first argument must be a type name or a filtered subquery (e.g. select Product filter …)"
17793 )),
17794 };
17795
17796 let index_name: Option<String> = fc.kwargs.iter().find(|(k, _)| k == "index_name").and_then(|(_, v)| {
17798 if let ast::Expr::Literal(ast::Literal::Str(s)) = v {
17799 Some(s.clone())
17800 } else {
17801 None
17802 }
17803 });
17804
17805 let td = self.resolve_type(&type_qname)?.clone();
17807 let vi = td
17808 .vector_indexes
17809 .iter()
17810 .find(|vi| vi.index_name.as_deref() == index_name.as_deref())
17811 .ok_or_else(|| {
17812 let key = index_name.as_deref().unwrap_or("<default>");
17813 self.type_err(&format!("type '{}' has no vector index '{}'", type_qname, key))
17814 })?;
17815
17816 let vector_col = vi.column_name();
17817 let distance_op = match vi.metric.as_str() {
17818 "euclidean" => "<->",
17819 "inner_product" => "<#>",
17820 _ => "<=>", };
17822
17823 let (
17825 query_expr,
17826 inference_query_param_name,
17827 inference_query_literal,
17828 inference_model,
17829 inference_type_name,
17830 inference_index_name,
17831 );
17832
17833 if is_text_overload {
17834 let vec_idx = self.param_index("__deferred_vec__");
17837 let vec_param = IrExpr::Param { index: vec_idx };
17838 let inner_cast = IrExpr::TypeCast(Box::new(IrTypeCast {
17840 expr: vec_param,
17841 pg_type: "float8[]".to_string(),
17842 tuple_shape: None,
17843 }));
17844 query_expr = IrExpr::TypeCast(Box::new(IrTypeCast {
17845 expr: inner_cast,
17846 pg_type: "vector".to_string(),
17847 tuple_shape: None,
17848 }));
17849 let query_arg = text_query_arg.unwrap();
17850 inference_query_param_name = Some(match query_arg {
17851 ast::Expr::Parameter(name) => name.clone(),
17852 _ => String::new(),
17853 });
17854 inference_query_literal = match query_arg {
17855 ast::Expr::Literal(ast::Literal::Str(s)) => Some(s.clone()),
17856 _ => None,
17857 };
17858 inference_model = Some(vi.model.clone());
17859 inference_type_name = Some(type_qname.clone());
17860 inference_index_name = Some(index_name.clone());
17861 } else {
17862 let raw_query_expr = self.compile_free_expr(&fc.args[1])?;
17864 query_expr = IrExpr::TypeCast(Box::new(IrTypeCast {
17865 expr: raw_query_expr,
17866 pg_type: "vector".to_string(),
17867 tuple_shape: None,
17868 }));
17869 inference_query_param_name = None;
17870 inference_query_literal = None;
17871 inference_model = None;
17872 inference_type_name = None;
17873 inference_index_name = None;
17874 }
17875
17876 let alias = self.fresh_alias();
17877 let source = IrSource {
17878 poly: None,
17879 type_name: type_qname.clone(),
17880 table: td.table.clone(),
17881 alias: alias.clone(),
17882 };
17883
17884 let pre_filter: Option<IrExpr> = match inner_filter_ast {
17887 Some(ref f) => Some(self.as_condition(|c| c.compile_expr(f, &td, &alias))?),
17888 None => None,
17889 };
17890
17891 let mut object_shape: Vec<IrShapePointer> = vec![];
17896 for el in elements {
17897 if el.splat.is_some() {
17898 continue;
17899 } let pointer_name = match el.path.steps.first() {
17901 Some(ast::PathStep::Name(n)) => n.as_str(),
17902 _ => continue,
17903 };
17904 match pointer_name {
17905 "distance" => { }
17906 "object" => {
17907 let sub_els = el.nested.as_deref().unwrap_or(&[]);
17908 object_shape = self.compile_shape(sub_els, &td, &alias, &td.module)?;
17909 }
17910 other => {
17911 return Err(self.type_err(&format!(
17912 "vector::search result has no pointer '{}'; valid pointers are 'object' and 'distance'",
17913 other
17914 )));
17915 }
17916 }
17917 }
17918
17919 let (outer_filter, order_by_distance, offset, limit) = self.compile_vs_modifiers(s)?;
17920
17921 let filter = match (pre_filter, outer_filter) {
17923 (Some(a), Some(b)) => Some(IrExpr::BinOp(Box::new(IrBinOp {
17924 left: a,
17925 op: ast::BinOpKind::And,
17926 right: b,
17927 }))),
17928 (Some(f), None) | (None, Some(f)) => Some(f),
17929 (None, None) => None,
17930 };
17931
17932 Ok(Some(IrVectorSearch {
17933 source,
17934 vector_col,
17935 distance_op,
17936 query_expr,
17937 object_shape,
17938 filter,
17939 order_by_distance,
17940 offset,
17941 limit,
17942 inference_query_param_name,
17943 inference_query_literal,
17944 inference_model,
17945 inference_type_name,
17946 inference_index_name,
17947 }))
17948 }
17949
17950 fn compile_vs_modifiers(&mut self, s: &ast::SelectStmt) -> Result<SearchModifiers, PyQLError> {
17953 let mut order_by_distance: Option<IrSortDir> = None;
17954 for sort in &s.order_by {
17955 let is_distance = matches!(&sort.expr,
17956 ast::Expr::Path(p) if p.partial && p.steps.len() == 1
17957 && matches!(&p.steps[0], ast::PathStep::Name(n) if n == "distance")
17958 );
17959 if is_distance {
17960 let dir = match sort.direction {
17961 ast::SortDirection::Desc => IrSortDir::Desc,
17962 ast::SortDirection::Asc => IrSortDir::Asc,
17963 };
17964 order_by_distance = Some(dir);
17965 } else {
17966 return Err(self.type_err("vector::search: only 'order by .distance' is supported as a sort key"));
17967 }
17968 }
17969
17970 let filter = match &s.filter {
17971 Some(f) => Some(self.compile_free_expr(f)?),
17972 None => None,
17973 };
17974 let offset = match &s.offset {
17975 Some(o) => Some(self.compile_free_expr(o)?),
17976 None => None,
17977 };
17978 let limit = match &s.limit {
17979 Some(l) => Some(self.compile_free_expr(l)?),
17980 None => None,
17981 };
17982
17983 Ok((filter, order_by_distance, offset, limit))
17984 }
17985
17986 fn try_compile_fts_search(
17991 &mut self,
17992 fc: &ast::FunctionCall,
17993 elements: &[ast::ShapeElement],
17994 s: &ast::SelectStmt,
17995 ) -> Result<Option<IrFtsSearch>, PyQLError> {
17996 if fc.module.as_deref() != Some("fts") || fc.name != "search" {
17997 return Ok(None);
17998 }
17999 if fc.args.len() < 2 {
18000 return Err(self.type_err("fts::search requires at least 2 arguments: (TypeName, $query)"));
18001 }
18002
18003 let type_qname = match &fc.args[0] {
18005 ast::Expr::Path(p) if !p.partial && p.steps.len() == 1 => {
18006 if let ast::PathStep::Name(n) = &p.steps[0] {
18007 let td = self
18008 .resolve_type(n)
18009 .map_err(|_| self.type_err(&format!("fts::search: '{}' is not a known type", n)))?;
18010 format!("{}::{}", td.module, td.name)
18011 } else {
18012 return Err(self.type_err("fts::search: first argument must be a type name"));
18013 }
18014 }
18015 _ => return Err(self.type_err("fts::search: first argument must be a bare type name")),
18016 };
18017
18018 let index_name: Option<String> = fc.kwargs.iter().find(|(k, _)| k == "index_name").and_then(|(_, v)| {
18020 if let ast::Expr::Literal(ast::Literal::Str(s)) = v {
18021 Some(s.clone())
18022 } else {
18023 None
18024 }
18025 });
18026
18027 let mode_str = fc
18028 .kwargs
18029 .iter()
18030 .find(|(k, _)| k == "mode")
18031 .and_then(|(_, v)| {
18032 if let ast::Expr::Literal(ast::Literal::Str(s)) = v {
18033 Some(s.as_str())
18034 } else {
18035 None
18036 }
18037 })
18038 .unwrap_or("BestFields");
18039
18040 let tsquery_fn: &'static str = match mode_str {
18041 "Phrase" => "phraseto_tsquery",
18042 _ => "websearch_to_tsquery", };
18044
18045 let td = self.resolve_type(&type_qname)?.clone();
18047 let si = td
18048 .search_indexes
18049 .iter()
18050 .find(|si| si.index_name.as_deref() == index_name.as_deref())
18051 .ok_or_else(|| {
18052 let key = index_name.as_deref().unwrap_or("<default>");
18053 self.type_err(&format!("type '{}' has no search index '{}'", type_qname, key))
18054 })?;
18055
18056 let backend = si.backend.clone();
18057 let search_col = si.column_name();
18058 let is_deferred = backend != SearchBackend::Postgres;
18059 let deferred_index_name = if is_deferred {
18060 Some(si.deferred_index_name(&td.module, &td.name))
18061 } else {
18062 None
18063 };
18064
18065 let query_expr;
18070 let deferred_query_param_name;
18071 let deferred_query_literal;
18072 let deferred_ids_param;
18073 let deferred_scores_param;
18074
18075 if is_deferred {
18076 let ids_idx = self.param_index("__deferred_ids__");
18077 let scores_idx = self.param_index("__deferred_scores__");
18078 deferred_ids_param = Some(ids_idx);
18079 deferred_scores_param = Some(scores_idx);
18080 deferred_query_param_name = match &fc.args[1] {
18081 ast::Expr::Parameter(name) => Some(name.clone()),
18082 _ => None,
18083 };
18084 deferred_query_literal = match &fc.args[1] {
18085 ast::Expr::Literal(ast::Literal::Str(s)) => Some(s.clone()),
18086 _ => None,
18087 };
18088 query_expr = IrExpr::Literal(crate::ir::IrLiteral::Str(String::new()));
18090 } else {
18091 query_expr = self.compile_free_expr(&fc.args[1])?;
18092 deferred_query_param_name = None;
18093 deferred_query_literal = None;
18094 deferred_ids_param = None;
18095 deferred_scores_param = None;
18096 }
18097
18098 let alias = self.fresh_alias();
18099 let source = IrSource {
18100 poly: None,
18101 type_name: type_qname.clone(),
18102 table: td.table.clone(),
18103 alias: alias.clone(),
18104 };
18105
18106 let mut object_shape: Vec<IrShapePointer> = vec![];
18108 for el in elements {
18109 if el.splat.is_some() {
18110 continue;
18111 }
18112 let pointer_name = match el.path.steps.first() {
18113 Some(ast::PathStep::Name(n)) => n.as_str(),
18114 _ => continue,
18115 };
18116 match pointer_name {
18117 "score" => { }
18118 "object" => {
18119 let sub_els = el.nested.as_deref().unwrap_or(&[]);
18120 object_shape = self.compile_shape(sub_els, &td, &alias, &td.module)?;
18121 }
18122 other => {
18123 return Err(self.type_err(&format!(
18124 "fts::search result has no pointer '{}'; valid pointers are 'object' and 'score'",
18125 other
18126 )));
18127 }
18128 }
18129 }
18130
18131 let (filter, order_by_rank, offset, limit) = self.compile_fts_modifiers(s)?;
18132
18133 Ok(Some(IrFtsSearch {
18134 source,
18135 backend,
18136 search_col,
18137 tsquery_fn,
18138 query_expr,
18139 object_shape,
18140 filter,
18141 order_by_rank,
18142 offset,
18143 limit,
18144 deferred_index_name,
18145 deferred_query_param_name,
18146 deferred_query_literal,
18147 deferred_ids_param,
18148 deferred_scores_param,
18149 }))
18150 }
18151
18152 fn compile_fts_modifiers(&mut self, s: &ast::SelectStmt) -> Result<SearchModifiers, PyQLError> {
18155 let mut order_by_rank: Option<IrSortDir> = None;
18156 for sort in &s.order_by {
18157 let is_rank = matches!(&sort.expr,
18158 ast::Expr::Path(p) if p.partial && p.steps.len() == 1
18159 && matches!(&p.steps[0], ast::PathStep::Name(n) if n == "score")
18160 );
18161 if is_rank {
18162 let dir = match sort.direction {
18163 ast::SortDirection::Desc => IrSortDir::Desc,
18164 ast::SortDirection::Asc => IrSortDir::Asc,
18165 };
18166 order_by_rank = Some(dir);
18167 } else {
18168 return Err(self.type_err("fts::search: only 'order by .score' is supported as a sort key"));
18169 }
18170 }
18171
18172 let filter = match &s.filter {
18173 Some(f) => Some(self.compile_free_expr(f)?),
18174 None => None,
18175 };
18176 let offset = match &s.offset {
18177 Some(o) => Some(self.compile_free_expr(o)?),
18178 None => None,
18179 };
18180 let limit = match &s.limit {
18181 Some(l) => Some(self.compile_free_expr(l)?),
18182 None => None,
18183 };
18184
18185 Ok((filter, order_by_rank, offset, limit))
18186 }
18187
18188 fn type_err(&self, msg: &str) -> PyQLError {
18191 PyQLError::Type(PyQLTypeError {
18192 message: msg.to_string(),
18193 position: Position { line: 0, col: 0 },
18194 })
18195 }
18196
18197 fn field_err(&self, field: &str, type_name: &str) -> PyQLError {
18198 if std::env::var("PYLON_DBG_FIELD_ERR").is_ok() {
18199 eprintln!(
18200 "DBG field_err {field} on {type_name}
18201{}",
18202 std::backtrace::Backtrace::force_capture()
18203 );
18204 }
18205 let suggestion = self
18206 .schema
18207 .types
18208 .iter()
18209 .find(|t| format!("{}::{}", t.module, t.name) == type_name)
18210 .and_then(|td| Self::suggest_pointer_name(td, field));
18211 let message = match suggestion {
18212 Some(s) => format!("object type '{type_name}' has no link or property '{field}'. Did you mean '{s}'?"),
18213 None => format!("object type '{type_name}' has no link or property '{field}'"),
18214 };
18215 PyQLError::Resolution(PyQLResolutionError::UnknownField(PyQLUnknownFieldError {
18216 message,
18217 position: Position { line: 0, col: 0 },
18218 }))
18219 }
18220
18221 fn suggest_function_name(&self, ns: &str, name: &str) -> String {
18237 const MIN_SIMILARITY: f64 = 0.7;
18238 if let Some(other) = ["std", "math", "cal", "sys"]
18239 .into_iter()
18240 .find(|o| *o != ns && !crate::stdlib::lookup(o, name).is_empty())
18241 {
18242 return format!(" — it lives in {other}, use {other}::{name}()");
18243 }
18244 crate::stdlib::registry()
18245 .iter()
18246 .filter(|d| d.namespace == ns)
18247 .map(|d| (format!("{ns}::{}", d.name), d.name))
18248 .chain(
18249 self.schema
18250 .functions
18251 .iter()
18252 .map(|f| (format!("{}::{}", f.module, f.name), f.name.as_str())),
18253 )
18254 .map(|(qualified, candidate)| (qualified, strsim::jaro_winkler(name, candidate)))
18255 .filter(|(_, score)| *score >= MIN_SIMILARITY)
18256 .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
18257 .map(|(qualified, _)| format!(" — did you mean {qualified}()?"))
18258 .unwrap_or_default()
18259 }
18260
18261 fn suggest_pointer_name(td: &TypeDescriptor, name: &str) -> Option<String> {
18272 const MIN_SIMILARITY: f64 = 0.7;
18273 td.properties
18274 .iter()
18275 .map(|p| p.name.as_str())
18276 .chain(td.links.iter().map(|l| l.name.as_str()))
18277 .chain(td.multilinks.iter().map(|m| m.name.as_str()))
18278 .chain(td.computed.iter().map(|c| c.name.as_str()))
18279 .map(|candidate| (candidate, strsim::jaro_winkler(name, candidate)))
18280 .filter(|(_, score)| *score >= MIN_SIMILARITY)
18281 .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
18282 .map(|(name, _)| name.to_string())
18283 }
18284
18285 fn pk_returning(td: &TypeDescriptor) -> Vec<IrShapePointer> {
18290 td.properties
18291 .iter()
18292 .filter(|p| p.is_pk)
18293 .map(|p| {
18294 IrShapePointer::Scalar(IrScalarPointer {
18295 implicit_id: false,
18296 marker_offset: None,
18297 alias: p.name.clone(),
18298 column: p.name.clone(),
18299 pg_type: p.pg_type.clone(),
18300 tuple_shape: None,
18301 })
18302 })
18303 .collect()
18304 }
18305}
18306
18307fn casts_to_json(ty: &ast::TypeExpr) -> bool {
18312 matches!(ty.as_named(), Some((module, "json")) if module.is_none_or(|m| m == "std"))
18313}
18314
18315fn has_any_link_props(vals: &IrMultiLinkValues) -> bool {
18318 if !vals.link_props.is_empty() {
18319 return true;
18320 }
18321 match &vals.source {
18322 IrMultiLinkValueSource::Union(a, b) => has_any_link_props(a) || has_any_link_props(b),
18323 _ => false,
18324 }
18325}
18326
18327fn is_empty_set_expr(expr: &Expr) -> bool {
18333 match expr {
18334 Expr::Set(elems) => elems.is_empty(),
18335 Expr::TypeCast(tc) => matches!(&tc.expr, Expr::Set(elems) if elems.is_empty()),
18336 _ => false,
18337 }
18338}
18339
18340fn path_leaf(p: &ast::Path) -> Result<&str, PyQLError> {
18342 match p.steps.as_slice() {
18343 [ast::PathStep::Name(n)] => Ok(n.as_str()),
18344 _ => Err(PyQLError::Type(PyQLTypeError {
18345 message: "expected a simple pointer name in shape element".into(),
18346 position: Position { line: 0, col: 0 },
18347 })),
18348 }
18349}
18350
18351fn type_expr_to_pg(ty: &ast::TypeExpr) -> Result<String, PyQLError> {
18353 let Some((module, bare_name)) = ty.as_named() else {
18354 return Err(PyQLError::Type(PyQLTypeError {
18358 message: "internal error: structural tuple/array type reached type_expr_to_pg".into(),
18359 position: Position { line: 0, col: 0 },
18360 }));
18361 };
18362
18363 if module == Some("pgvector") {
18365 return match bare_name {
18366 "vector" => Ok("vector".to_string()),
18367 other => Err(PyQLError::Type(PyQLTypeError {
18368 message: format!("unknown pgvector type '{other}'; valid types are: vector"),
18369 position: Position { line: 0, col: 0 },
18370 })),
18371 };
18372 }
18373
18374 if module == Some("postgis") {
18376 return match bare_name {
18377 "geometry" => Ok("geometry".to_string()),
18378 "geography" => Ok("geography".to_string()),
18379 "box2d" => Ok("box2d".to_string()),
18380 "box3d" => Ok("box3d".to_string()),
18381 other => Err(PyQLError::Type(PyQLTypeError {
18382 message: format!("unknown postgis type '{other}'; valid types are: geometry, geography, box2d, box3d"),
18383 position: Position { line: 0, col: 0 },
18384 })),
18385 };
18386 }
18387
18388 if module == Some("cal") {
18390 let pg = match bare_name {
18391 "local_datetime" => "timestamp",
18392 "local_date" => "date",
18393 "local_time" => "time",
18394 "relative_duration" | "date_duration" => "interval",
18395 other => {
18396 return Err(PyQLError::Type(PyQLTypeError {
18397 message: format!(
18398 "unknown cal type '{other}'; \
18399 valid types are: local_datetime, local_date, local_time, \
18400 relative_duration, date_duration"
18401 ),
18402 position: Position { line: 0, col: 0 },
18403 }));
18404 }
18405 };
18406 return Ok(pg.to_string());
18407 }
18408
18409 let name = match module {
18410 Some("std") | None => bare_name,
18411 Some(m) => {
18412 return Err(PyQLError::Type(PyQLTypeError {
18413 message: format!("unknown type '{}::{}'", m, bare_name),
18414 position: Position { line: 0, col: 0 },
18415 }));
18416 }
18417 };
18418 Ok(match name {
18419 "str" => "text",
18420 "int16" => "int2",
18421 "int32" => "int4",
18422 "int64" => "int8",
18423 "float32" => "float4",
18424 "float64" => "float8",
18425 "bool" => "boolean",
18426 "uuid" => "uuid",
18427 "bytes" => "bytea",
18428 "json" => "jsonb",
18429 "decimal" => "numeric",
18430 "bigint" => "numeric",
18433 "datetime" => "timestamptz",
18434 "date" => "date",
18435 "time" => "time",
18436 "duration" => "interval",
18437 other => {
18438 return Err(PyQLError::Type(PyQLTypeError {
18439 message: format!("unknown type '{other}'"),
18440 position: Position { line: 0, col: 0 },
18441 }));
18442 }
18443 }
18444 .to_string())
18445}
18446
18447fn ir_is_not_null(expr: IrExpr) -> IrExpr {
18451 IrExpr::FunctionCall(IrFunctionCall {
18452 return_pg_type: None,
18453 schema: None,
18454 name: String::new(),
18455 args: vec![expr],
18456 sql_template: Some("($1 IS NOT NULL)".to_string()),
18457 })
18458}
18459
18460fn describe_arities(overloads: &[&crate::stdlib::FnDescriptor]) -> String {
18464 let mut arities: Vec<usize> = overloads
18467 .iter()
18468 .map(|d| d.params.iter().filter(|p| p.named_only.is_none()).count())
18469 .collect();
18470 arities.sort_unstable();
18471 arities.dedup();
18472 if overloads.iter().any(|d| d.is_variadic()) {
18473 return format!("at least {} argument(s)", arities[0].saturating_sub(1));
18474 }
18475 let list = arities.iter().map(usize::to_string).collect::<Vec<_>>().join(" or ");
18476 format!("{list} argument(s)")
18477}
18478
18479fn describe_args(args: &[IrExpr]) -> String {
18482 args.iter()
18483 .map(|a| infer_ir_type(a).map_or("?", literal_sentinel_to_pg))
18484 .collect::<Vec<_>>()
18485 .join(", ")
18486}
18487
18488fn describe_signatures(overloads: &[&crate::stdlib::FnDescriptor]) -> String {
18491 overloads
18492 .iter()
18493 .map(|d| {
18494 let params = d
18495 .params
18496 .iter()
18497 .map(|p| match p.named_only {
18498 Some(_) => format!("{} := {}", p.keyword(), p.ty.pyql_name()),
18501 None => p.ty.pyql_name(),
18502 })
18503 .collect::<Vec<_>>()
18504 .join(", ");
18505 format!("({params})")
18506 })
18507 .collect::<Vec<_>>()
18508 .join(" or ")
18509}
18510
18511fn bool_literal(expr: &IrExpr) -> Option<bool> {
18514 match expr {
18515 IrExpr::Literal(IrLiteral::Bool(b)) => Some(*b),
18516 _ => None,
18517 }
18518}
18519
18520fn pack_variadic_args(d: &crate::stdlib::FnDescriptor, args: Vec<IrExpr>) -> Vec<IrExpr> {
18529 let named = d.named_count();
18530 let Some(variadic_at) = d.variadic_index().filter(|v| v + 1 < d.params.len()) else {
18531 return args;
18532 };
18533 let Some(absorbed) = args.len().checked_sub(variadic_at + named) else {
18534 return args;
18535 };
18536 let mut packed: Vec<IrExpr> = Vec::with_capacity(variadic_at + 1 + named);
18537 let mut rest = args.into_iter();
18538 packed.extend(rest.by_ref().take(variadic_at));
18539 packed.push(IrExpr::Array(rest.by_ref().take(absorbed).collect()));
18540 packed.extend(rest);
18541 packed
18542}
18543
18544fn params_for_args(d: &crate::stdlib::FnDescriptor, argc: usize) -> impl Iterator<Item = &crate::stdlib::Param> {
18557 let named = d.named_count();
18558 let variadic_at = d.is_variadic().then(|| d.variadic_index()).flatten();
18559 let absorbed = variadic_at.map_or(0, |v| argc.saturating_sub(v + named));
18560 (0..argc).filter_map(move |i| match variadic_at {
18561 Some(v) if i >= v && i < v + absorbed => d.params.get(v),
18562 Some(v) if i >= v => d.params.get(i + 1 - absorbed),
18563 _ => d.params.get(i),
18564 })
18565}
18566
18567fn pylon_type_matches(expr: &IrExpr, ty: &crate::stdlib::PylonType) -> bool {
18568 use crate::stdlib::PylonType as PT;
18569 match ty {
18570 PT::Any | PT::AnyOrderable | PT::AnyPoint => true,
18572 PT::Array(_) => is_array_expr(expr),
18573 PT::Range(_) => matches!(infer_ir_type(expr), Some(t) if t.ends_with("range") && !t.contains("multirange")),
18578 PT::Multirange(_) => matches!(infer_ir_type(expr), Some(t) if t.contains("multirange")),
18579 _ => match (ty.scalar_pg_type(), infer_ir_type(expr)) {
18593 (Some(declared), Some(known)) => types_compatible(known, declared),
18594 (Some(_), None) => false,
18595 (None, _) => true,
18596 },
18597 }
18598}
18599
18600fn expr_returns_set(expr: &Expr) -> bool {
18602 let Expr::FunctionCall(call) = expr else {
18603 return false;
18604 };
18605 crate::stdlib::lookup(call.module.as_deref().unwrap_or("std"), &call.name)
18606 .iter()
18607 .any(|d| d.returns_set())
18608}
18609
18610fn literal_sentinel_to_pg(t: &str) -> &str {
18611 match t {
18612 "__int_literal" => "int8",
18613 "__float_literal" => "float8",
18614 other => other,
18615 }
18616}
18617
18618fn is_array_expr(expr: &IrExpr) -> bool {
18619 match through_coalesce(expr) {
18620 IrExpr::Array(_) | IrExpr::ArrayFromSelect(_) => true,
18621 IrExpr::FunctionCall(f) if f.schema.is_none() => {
18624 let mut overloads = crate::stdlib::registry().iter().filter(|d| d.name == f.name).peekable();
18625 overloads.peek().is_some() && overloads.all(|d| matches!(d.return_type, crate::stdlib::PylonType::Array(_)))
18626 }
18627 other => matches!(infer_ir_type(other), Some(t) if t.ends_with("[]")),
18631 }
18632}
18633
18634fn static_min_payload_bytes(expr: &ast::Expr) -> usize {
18650 use crate::parse::ast::{BinOpKind, Expr, Literal};
18651 match expr {
18652 Expr::Literal(Literal::Str(s)) => s.len(),
18653 Expr::BinOp(op) if matches!(op.op, BinOpKind::Concat) => {
18654 static_min_payload_bytes(&op.left) + static_min_payload_bytes(&op.right)
18655 }
18656 Expr::Shape(sh) if sh.expr.is_none() => {
18657 let mut total = 2;
18661 for (i, el) in sh.elements.iter().enumerate() {
18662 if i > 0 {
18663 total += 1;
18664 }
18665 let name_len = match el.path.steps.last() {
18666 Some(crate::parse::ast::PathStep::Name(n)) => n.len(),
18667 _ => 0,
18668 };
18669 total += name_len + 3;
18670 if let Some(value) = &el.compexpr {
18671 total += static_min_payload_bytes(value);
18672 }
18673 }
18674 total
18675 }
18676 _ => 0,
18677 }
18678}
18679
18680fn expr_to_std_type(expr: &ast::Expr) -> &'static str {
18681 match expr {
18682 ast::Expr::Literal(ast::Literal::Str(_)) => "std::str",
18683 ast::Expr::Literal(ast::Literal::Int(_)) => "std::int64",
18684 ast::Expr::Literal(ast::Literal::Float(_)) => "std::float64",
18685 ast::Expr::Literal(ast::Literal::Bool(_)) => "std::bool",
18686 _ => "anytype",
18687 }
18688}
18689
18690fn named_tuple_type_str(fields: &[(String, ast::Expr)]) -> String {
18691 let inner = fields
18692 .iter()
18693 .map(|(k, v)| format!("{}: {}", k, expr_to_std_type(v)))
18694 .collect::<Vec<_>>()
18695 .join(", ");
18696 format!("tuple<{}>", inner)
18697}
18698
18699fn positional_tuple_type_str(elems: &[ast::Expr]) -> String {
18700 let inner = elems.iter().map(expr_to_std_type).collect::<Vec<_>>().join(", ");
18701 format!("tuple<{}>", inner)
18702}
18703
18704pub(crate) fn infer_ir_type(expr: &IrExpr) -> Option<&str> {
18705 match expr {
18706 IrExpr::ColumnRef { pg_type, .. } => Some(pg_type.as_str()),
18707 IrExpr::TypeCast(tc) => Some(tc.pg_type.as_str()),
18708 IrExpr::FnParam { pg_type, .. } => Some(pg_type.as_str()),
18709 IrExpr::Literal(lit) => Some(match lit {
18710 IrLiteral::Str(_) => "text",
18711 IrLiteral::Int(_) => "__int_literal",
18712 IrLiteral::Float(_) => "__float_literal",
18713 IrLiteral::Bool(_) => "boolean",
18714 }),
18715 IrExpr::EnumLiteral { pg_type, .. } => Some(pg_type.as_str()),
18716 IrExpr::NamedTuple { .. } => Some("jsonb"),
18717 IrExpr::JsonbField { .. } | IrExpr::JsonbIndex { .. } => Some("jsonb"),
18718 IrExpr::GlobalParam { pg_type, .. } => Some(pg_type.as_str()),
18719 IrExpr::CteRef { pg_type, .. } => pg_type.as_deref(),
18722 IrExpr::PathSubquery(ps) => match &ps.result {
18726 IrPathResult::Scalar(e, _) => infer_ir_type(e),
18727 IrPathResult::Object { .. } => None,
18728 },
18729 IrExpr::BinOp(b) if b.op == crate::parse::ast::BinOpKind::Concat => {
18732 infer_ir_type(&b.left).or_else(|| infer_ir_type(&b.right))
18733 }
18734 IrExpr::UnaryOp(u) if u.op == crate::parse::ast::UnaryOpKind::Distinct => infer_ir_type(&u.operand),
18735 IrExpr::UnaryOp(u) if u.op == crate::parse::ast::UnaryOpKind::Minus => infer_ir_type(&u.operand),
18738 IrExpr::UnaryOp(u)
18739 if matches!(
18740 u.op,
18741 crate::parse::ast::UnaryOpKind::Not | crate::parse::ast::UnaryOpKind::Exists
18742 ) =>
18743 {
18744 Some("boolean")
18745 }
18746 IrExpr::BinOp(b) if b.op == crate::parse::ast::BinOpKind::Coalesce => {
18752 infer_ir_type(&b.left).or_else(|| infer_ir_type(&b.right))
18753 }
18754 IrExpr::IfElse(ie) => infer_ir_type(&ie.if_).or_else(|| infer_ir_type(&ie.else_)),
18755 IrExpr::Slice { expr, .. } => infer_ir_type(expr),
18761 IrExpr::CteFieldRef { pg_type, .. } | IrExpr::ForVar { pg_type, .. } => pg_type.as_deref(),
18765 IrExpr::Subscript { expr, is_array, .. } => {
18766 let Some(base) = infer_ir_type(expr) else {
18770 return stdlib_array_element_type(expr);
18771 };
18772 if *is_array { base.strip_suffix("[]") } else { Some(base) }
18773 }
18774 IrExpr::BinOp(b) => arithmetic_result_type(&b.op, infer_ir_type(&b.left)?, infer_ir_type(&b.right)?),
18775 IrExpr::FunctionCall(f) if f.schema.is_none() && f.name == "coalesce" => f.args.iter().find_map(infer_ir_type),
18780 IrExpr::FunctionCall(f) if f.schema.is_none() && matches!(f.name.as_str(), "max" | "min" | "sum") => {
18781 aggregate_result_type(&f.name, infer_ir_type(f.args.first()?)?)
18782 }
18783 IrExpr::AggOverSet {
18784 fn_name, schema: None, ..
18785 } if fn_name == "count" => Some("int8"),
18786 IrExpr::AggOverSet {
18787 fn_name,
18788 schema: None,
18789 elems,
18790 } => aggregate_result_type(fn_name, infer_ir_type(elems.first()?)?),
18791 IrExpr::FunctionCall(f) => f.return_pg_type.as_deref(),
18796 _ => None,
18797 }
18798}
18799
18800fn stdlib_array_element_type(expr: &IrExpr) -> Option<&'static str> {
18805 let IrExpr::FunctionCall(f) = expr else {
18806 return None;
18807 };
18808 if f.schema.is_some() {
18809 return None;
18810 }
18811 let mut element: Option<&'static str> = None;
18812 for descriptor in crate::stdlib::registry().iter().filter(|d| d.name == f.name) {
18813 let crate::stdlib::PylonType::Array(inner) = &descriptor.return_type else {
18814 return None;
18815 };
18816 let scalar = inner.scalar_pg_type()?;
18817 if element.is_some_and(|seen| seen != scalar) {
18818 return None;
18819 }
18820 element = Some(scalar);
18821 }
18822 element
18823}
18824
18825fn temporal_result_type(op: &ast::BinOpKind, left: &str, right: &str) -> Option<&'static str> {
18831 use ast::BinOpKind::*;
18832 let instant = |t: &str| matches!(t, "timestamptz" | "timestamp" | "date" | "time");
18833 match (op, left, right) {
18834 (Sub, l, r) if instant(l) && instant(r) => Some("interval"),
18835 (Sub | Add, l, "interval") if instant(l) => Some(match l {
18836 "timestamptz" => "timestamptz",
18837 "timestamp" => "timestamp",
18838 "date" => "date",
18839 _ => "time",
18840 }),
18841 (Add, "interval", r) if instant(r) => Some(match r {
18842 "timestamptz" => "timestamptz",
18843 "timestamp" => "timestamp",
18844 "date" => "date",
18845 _ => "time",
18846 }),
18847 (Add | Sub, "interval", "interval") => Some("interval"),
18848 _ => None,
18849 }
18850}
18851
18852fn arithmetic_result_type(op: &ast::BinOpKind, left: &str, right: &str) -> Option<&'static str> {
18855 use ast::BinOpKind::*;
18856 if !matches!(op, Add | Sub | Mul | Div | FloorDiv | Mod | Pow) {
18857 return None;
18858 }
18859 let (left, right) = (literal_sentinel_to_pg(left), literal_sentinel_to_pg(right));
18860 if let Some(temporal) = temporal_result_type(op, left, right) {
18861 return Some(temporal);
18862 }
18863 let int = |t: &str| INT_TYPES.contains(&t);
18864 let float = |t: &str| FLOAT_TYPES.contains(&t);
18865 let numeric = |t: &str| NUMERIC_TYPES.contains(&t);
18866 if int(left) && int(right) {
18867 return Some(if matches!(op, Div | Pow) {
18868 "float8"
18869 } else if left == "int8" || right == "int8" {
18870 "int8"
18871 } else if left == "int4" || right == "int4" {
18872 "int4"
18873 } else {
18874 "int2"
18875 });
18876 }
18877 if (float(left) || int(left)) && (float(right) || int(right)) {
18878 return Some(if left == "float4" && right == "float4" {
18879 "float4"
18880 } else {
18881 "float8"
18882 });
18883 }
18884 if (numeric(left) || int(left)) && (numeric(right) || int(right)) {
18885 return Some("numeric");
18886 }
18887 None
18888}
18889
18890fn aggregate_result_type<'a>(name: &str, element: &'a str) -> Option<&'a str> {
18892 let element = literal_sentinel_to_pg(element);
18893 match name {
18894 "max" | "min" => Some(element),
18895 "sum" if INT_TYPES.contains(&element) => Some("int8"),
18896 "sum" if FLOAT_TYPES.contains(&element) || NUMERIC_TYPES.contains(&element) => Some(element),
18897 _ => None,
18898 }
18899}
18900
18901fn true_division_operand(op: &ast::BinOpKind, left: IrExpr, right: &IrExpr) -> IrExpr {
18904 let integer = |expr: &IrExpr| infer_ir_type(expr).is_some_and(|t| INT_TYPES.contains(&literal_sentinel_to_pg(t)));
18905 if *op != ast::BinOpKind::Div || !integer(&left) || !integer(right) {
18906 return left;
18907 }
18908 IrExpr::TypeCast(Box::new(IrTypeCast {
18909 expr: left,
18910 pg_type: "float8".to_string(),
18911 tuple_shape: None,
18912 }))
18913}
18914
18915fn range_ctor_for_pg_type(pg_type: &str) -> Option<&'static str> {
18922 match pg_type {
18923 "int2" | "int4" | "int8" | "__int_literal" => Some("int8range"),
18924 "numeric" | "__float_literal" => Some("numrange"),
18925 "timestamp" => Some("tsrange"),
18926 "timestamptz" => Some("tstzrange"),
18927 "date" => Some("daterange"),
18928 _ => None,
18929 }
18930}
18931
18932fn multirange_ctor_for_range_ctor(range_ctor: &str) -> Option<&'static str> {
18935 match range_ctor {
18936 "int8range" => Some("int8multirange"),
18937 "numrange" => Some("nummultirange"),
18938 "tsrange" => Some("tsmultirange"),
18939 "tstzrange" => Some("tstzmultirange"),
18940 "daterange" => Some("datemultirange"),
18941 _ => None,
18942 }
18943}
18944
18945const INT_TYPES: &[&str] = &["int2", "int4", "int8", "__int_literal"];
18946const FLOAT_TYPES: &[&str] = &["float4", "float8", "__float_literal"];
18947const NUMERIC_TYPES: &[&str] = &["numeric"];
18951
18952pub(crate) fn types_compatible(a: &str, b: &str) -> bool {
18962 if a == b {
18963 return true;
18964 }
18965 let a_int = INT_TYPES.contains(&a);
18966 let b_int = INT_TYPES.contains(&b);
18967 if a_int && b_int {
18968 return true;
18969 }
18970 let a_float = FLOAT_TYPES.contains(&a);
18971 let b_float = FLOAT_TYPES.contains(&b);
18972 if a_float && b_float {
18973 return true;
18974 }
18975 let a_numeric = NUMERIC_TYPES.contains(&a);
18976 let b_numeric = NUMERIC_TYPES.contains(&b);
18977 if a_numeric && b_numeric {
18978 return true;
18979 }
18980 (a_int && b_float) || (a_float && b_int) || (a_int && b_numeric) || (a_numeric && b_int)
18981}
18982
18983fn datetime_arithmetic_compatible(op: &ast::BinOpKind, a: &str, b: &str) -> bool {
18995 if !matches!(op, ast::BinOpKind::Add | ast::BinOpKind::Sub) {
18996 return false;
18997 }
18998 matches!(
18999 (a, b),
19000 ("timestamptz", "interval")
19001 | ("interval", "timestamptz")
19002 | ("timestamp", "interval")
19003 | ("interval", "timestamp")
19004 | ("date", "interval")
19005 | ("interval", "date")
19006 | ("time", "interval")
19007 | ("interval", "time")
19008 )
19009}
19010
19011fn collect_search_enqueue(td: &TypeDescriptor, type_name: &str, operation: &'static str) -> Vec<SearchEnqueueInfo> {
19016 td.search_indexes
19017 .iter()
19018 .filter(|si| si.backend == SearchBackend::OpenSearch || si.backend == SearchBackend::Meilisearch)
19019 .map(|si| SearchEnqueueInfo {
19020 type_name: type_name.to_string(),
19021 index_name: si.index_name.clone(),
19022 operation,
19023 backend: si.backend.clone(),
19024 })
19025 .collect()
19026}
19027
19028pub fn pg_type_to_pyql(pg: &str) -> &str {
19041 match pg {
19042 "text" | "varchar" => "std::str",
19043 "uuid" => "std::uuid",
19044 "int2" => "std::int16",
19045 "int4" => "std::int32",
19046 "int8" => "std::int64",
19047 "float4" => "std::float32",
19048 "float8" => "std::float64",
19049 "boolean" => "std::bool",
19050 "numeric" => "std::decimal",
19051 "timestamptz" => "std::datetime",
19052 "timestamp" => "cal::local_datetime",
19053 "date" => "cal::local_date",
19054 "time" => "cal::local_time",
19055 "interval" => "std::duration",
19056 "bytea" => "std::bytes",
19057 "jsonb" => "std::json",
19058 "__int_literal" => "std::int64",
19059 "__float_literal" => "std::float64",
19060 other => other,
19061 }
19062}