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();
411 if rest.is_empty() {
412 return None;
413 }
414 Expr::Path(ast::Path {
415 steps: rest,
416 partial: true,
417 })
418 }
419 Expr::Path(_) | Expr::Literal(_) | Expr::Parameter(_) | Expr::Global(_) => expr.clone(),
420 Expr::BinOp(b) => Expr::BinOp(Box::new(ast::BinOp {
421 left: rewrite(&b.left, link)?,
422 op: b.op.clone(),
423 right: rewrite(&b.right, link)?,
424 })),
425 Expr::UnaryOp(u) => Expr::UnaryOp(Box::new(ast::UnaryOp {
426 op: u.op.clone(),
427 operand: rewrite(&u.operand, link)?,
428 })),
429 Expr::TypeCast(c) => Expr::TypeCast(Box::new(ast::TypeCast {
430 expr: rewrite(&c.expr, link)?,
431 ty: c.ty.clone(),
432 })),
433 Expr::TypeIs { expr, ty } => Expr::TypeIs {
434 expr: Box::new(rewrite(expr, link)?),
435 ty: ty.clone(),
436 },
437 Expr::FunctionCall(f)
441 if crate::stdlib::lookup(f.module.as_deref().unwrap_or("std"), &f.name)
442 .iter()
443 .any(|d| d.returns_set()) =>
444 {
445 return None;
446 }
447 Expr::FunctionCall(f) => Expr::FunctionCall(ast::FunctionCall {
448 module: f.module.clone(),
449 name: f.name.clone(),
450 args: f.args.iter().map(|a| rewrite(a, link)).collect::<Option<_>>()?,
451 kwargs: f
452 .kwargs
453 .iter()
454 .map(|(k, v)| Some((k.clone(), rewrite(v, link)?)))
455 .collect::<Option<_>>()?,
456 }),
457 _ => return None,
458 })
459 }
460 let mut link = None;
461 let element_condition = rewrite(condition, &mut link)?;
462 let link = link?;
463 let many = match link.first()? {
464 ast::PathStep::Name(name) => td.multilinks.iter().any(|m| &m.name == name),
465 ast::PathStep::Backlink(_) => true,
466 _ => false,
467 };
468 many.then_some((link, element_condition))
469}
470
471fn flatten_shape_subject(expr: &Expr) -> Option<Expr> {
481 let Expr::Shape(outer) = expr else { return None };
482 match outer.expr.as_ref()? {
483 Expr::SubQuery(stmt) => {
484 let Stmt::Select(sel) = stmt.as_ref() else { return None };
485 let bare = sel.filter.is_none()
486 && sel.order_by.is_empty()
487 && sel.offset.is_none()
488 && sel.limit.is_none()
489 && sel.lock.is_none();
490 if !bare || !matches!(sel.result, Expr::IfElse(_) | Expr::Union(_, _) | Expr::Shape(_)) {
491 return None;
492 }
493 let mut flat = outer.as_ref().clone();
494 flat.expr = Some(sel.result.clone());
495 Some(Expr::Shape(Box::new(flat)))
496 }
497 Expr::Shape(inner) => {
498 let elements = outer
499 .elements
500 .iter()
501 .map(|el| {
502 let declared = match (&el.compexpr, el.path.steps.as_slice()) {
503 (None, [ast::PathStep::Name(name)]) => inner.elements.iter().find(|d| {
504 d.compexpr.is_some()
505 && matches!(d.path.steps.as_slice(), [ast::PathStep::Name(n)] if n == name)
506 }),
507 _ => None,
508 };
509 match declared {
510 Some(d) => ShapeElement {
511 nested: el.nested.clone().or_else(|| d.nested.clone()),
512 filter: el.filter.clone().or_else(|| d.filter.clone()),
513 order_by: if el.order_by.is_empty() {
514 d.order_by.clone()
515 } else {
516 el.order_by.clone()
517 },
518 offset: el.offset.clone().or_else(|| d.offset.clone()),
519 limit: el.limit.clone().or_else(|| d.limit.clone()),
520 ..d.clone()
521 },
522 None => el.clone(),
523 }
524 })
525 .collect();
526 Some(Expr::Shape(Box::new(ast::ShapeExpr {
527 expr: inner.expr.clone(),
528 elements,
529 marker_offset: inner.marker_offset,
530 })))
531 }
532 _ => None,
533 }
534}
535
536fn group_key_binding(name: &str) -> String {
539 format!("<group key {name}>")
540}
541
542fn and_conditions(filter: Option<IrExpr>, extra: Vec<IrExpr>) -> Option<IrExpr> {
543 extra.into_iter().fold(filter, |acc, cond| match acc {
544 Some(existing) => Some(IrExpr::BinOp(Box::new(IrBinOp {
545 left: existing,
546 op: ast::BinOpKind::And,
547 right: cond,
548 }))),
549 None => Some(cond),
550 })
551}
552
553const MAX_COMPUTED_SPLICES: usize = 32;
554
555fn aggregate_over_nothing_sql(name: &str) -> Option<&'static str> {
560 match name {
561 "sum" => Some("0"),
562 "any" => Some("false"),
563 "all" => Some("true"),
564 "array_agg" => Some("'{}'"),
565 _ => None,
566 }
567}
568
569fn aggregate_over_nothing(name: &str, aggregate: IrExpr) -> IrExpr {
570 let Some(value) = aggregate_over_nothing_sql(name) else {
571 return aggregate;
572 };
573 IrExpr::FunctionCall(IrFunctionCall {
574 return_pg_type: None,
575 schema: None,
576 name: "coalesce".to_string(),
577 args: vec![aggregate, IrExpr::RawSql(value.to_string())],
578 sql_template: None,
579 })
580}
581
582fn set_walk_as_scalar(expr: IrExpr) -> IrExpr {
598 match expr {
599 IrExpr::ArrayFromSelect(source) => match *source {
600 IrArraySource::PathSelect(ps) => IrExpr::PathSubquery(ps),
601 other => IrExpr::ArrayFromSelect(Box::new(other)),
602 },
603 other => other,
604 }
605}
606
607fn yields_array(expr: &IrExpr) -> bool {
608 match expr {
609 IrExpr::ArrayFromSelect(_) => true,
610 IrExpr::IfElse(ie) => {
611 (yields_array(&ie.if_) || matches!(ie.if_, IrExpr::Null))
612 && (yields_array(&ie.else_) || matches!(ie.else_, IrExpr::Null))
613 && !(matches!(ie.if_, IrExpr::Null) && matches!(ie.else_, IrExpr::Null))
614 }
615 _ => false,
616 }
617}
618
619fn through_coalesce(expr: &IrExpr) -> &IrExpr {
623 match expr {
624 IrExpr::FunctionCall(f) if f.schema.is_none() && f.name == "coalesce" => {
625 f.args.first().map(through_coalesce).unwrap_or(expr)
626 }
627 other => other,
628 }
629}
630
631fn ir_value_type_name(expr: &IrExpr) -> String {
632 ir_value_type(expr).unwrap_or_default()
633}
634
635fn ir_value_type(expr: &IrExpr) -> Option<String> {
640 let expr = through_coalesce(expr);
641 match expr {
642 IrExpr::Array(elements) => {
643 let element = elements.first().and_then(infer_ir_type).unwrap_or("text");
644 Some(format!("{}[]", literal_sentinel_to_pg(element)))
645 }
646 IrExpr::FunctionCall(f) if f.schema.is_none() && f.name == "array_agg" => {
649 let element = f.args.first().and_then(infer_ir_type)?;
650 Some(format!("{}[]", literal_sentinel_to_pg(element)))
651 }
652 IrExpr::PathSubquery(ps) => match &ps.result {
655 IrPathResult::Scalar(inner, _) => ir_value_type(inner),
656 IrPathResult::Object { .. } => None,
657 },
658 IrExpr::ArrayFromSelect(source) => array_source_element_type(source).map(|element| format!("{element}[]")),
659 IrExpr::IfElse(ie) => ir_value_type(&ie.if_).or_else(|| ir_value_type(&ie.else_)),
662 other => infer_ir_type(other).map(|t| t.to_string()),
663 }
664}
665
666fn array_source_element_type(source: &IrArraySource) -> Option<String> {
669 match source {
670 IrArraySource::Select(sel) => match sel.rows.as_slice() {
671 [IrRowSource::Bound { shape, .. }] => match shape.first() {
672 Some(IrShapePointer::Scalar(scalar)) => Some(scalar.pg_type.clone()),
675 _ => Some("uuid".to_string()),
676 },
677 _ => None,
678 },
679 IrArraySource::PathSelect(ps) => match &ps.result {
680 IrPathResult::Scalar(expr, _) => infer_ir_type(expr).map(|t| literal_sentinel_to_pg(t).to_string()),
681 IrPathResult::Object { .. } => None,
682 },
683 _ => None,
684 }
685}
686
687fn declared_pointers_of(expr: &Expr) -> Option<Vec<ShapeElement>> {
689 if let Expr::IfElse(ie) = expr {
691 return declared_pointers_of(&ie.if_expr).or_else(|| declared_pointers_of(&ie.else_expr));
692 }
693 let Expr::SubQuery(stmt) = expr else {
694 return None;
695 };
696 let shape = match innermost_select(stmt)? {
697 ast::SelectStmt {
698 result: Expr::Shape(sh),
699 ..
700 } => sh,
701 _ => return None,
702 };
703 let declared: Vec<ShapeElement> = shape
704 .elements
705 .iter()
706 .filter(|el| el.compexpr.is_some())
707 .cloned()
708 .collect();
709 (!declared.is_empty()).then_some(declared)
710}
711
712fn innermost_select(stmt: &Stmt) -> Option<&ast::SelectStmt> {
714 match stmt {
715 Stmt::Select(sel) => Some(sel),
716 Stmt::With(w) => innermost_select(&w.stmt),
717 _ => None,
718 }
719}
720
721fn cte_stmt_type(stmt: &IrStmt) -> String {
722 match stmt {
723 IrStmt::Insert(ins) => ins.target.type_name.clone(),
724 IrStmt::Update(upd) => upd.target.type_name.clone(),
725 IrStmt::Delete(del) => del.target.type_name.clone(),
726 IrStmt::Select(sel) => match sel.rows.first() {
727 Some(IrRowSource::Bound { source, .. }) => source.type_name.clone(),
728 Some(IrRowSource::Free(IrFreeExpr::Scalar(expr))) => ir_value_type_name(expr),
732 _ => String::new(),
733 },
734 IrStmt::PathSelect(ps) => match &ps.result {
739 IrPathResult::Scalar(expr, _) => ir_value_type_name(expr),
740 IrPathResult::Object { type_name, .. } => type_name.clone(),
741 },
742 IrStmt::For(f) => cte_stmt_type(&f.body),
743 IrStmt::ScalarUnion(branches) => branches.first().map(cte_stmt_type).unwrap_or_default(),
744 IrStmt::Group(g) => g.source.type_name.clone(),
745 IrStmt::FunctionSelect(fs) => fs.type_name.clone(),
746 IrStmt::VectorSearch(vs) => format!("__vs__{}", vs.source.type_name),
747 IrStmt::FtsSearch(fs) => format!("__fts__{}", fs.source.type_name),
748 }
749}
750
751fn compile_cte_binding(c: &mut Compiler<'_>, expr: &Expr) -> Result<IrStmt, PyQLError> {
754 let stmt = compile_cte_binding_stmt(c, expr)?;
755 Ok(hoist_binding_dml(c, stmt))
756}
757
758fn hoist_binding_dml(c: &mut Compiler<'_>, stmt: IrStmt) -> IrStmt {
765 let IrStmt::Select(mut select) = stmt else {
766 return stmt;
767 };
768 let Some(dml) = select
769 .dml_source
770 .take_if(|dml| matches!(dml.as_ref(), IrStmt::Insert(_) | IrStmt::Update(_) | IrStmt::Delete(_)))
771 else {
772 return IrStmt::Select(select);
773 };
774 let cte_name = c.fresh_nested_cte_name();
775 for row in &mut select.rows {
776 if let IrRowSource::Bound { source, .. } = row {
777 source.table = format!("@cte:{cte_name}");
778 source.poly = None;
781 }
782 }
783 c.hoisted_ctes.push(IrCteDef {
784 name: cte_name,
785 type_name: cte_stmt_type(&dml),
786 stmt: *dml,
787 correlated_to: None,
788 });
789 IrStmt::Select(select)
790}
791
792fn compile_cte_binding_stmt(c: &mut Compiler<'_>, expr: &Expr) -> Result<IrStmt, PyQLError> {
793 if let Expr::SubQuery(s) = expr {
794 let stmt = c.compile_stmt(s)?;
795 if let IrStmt::Group(grp) = &stmt
796 && !matches!(grp.output, IrGroupOutput::Elements)
797 {
798 return Err(c.type_err("a `group` cannot be bound in a `with` except to iterate it with `for`"));
799 }
800 return Ok(stmt);
801 }
802 let expr = match expr {
806 Expr::BinOp(b)
807 if b.op == ast::BinOpKind::Coalesce
808 && matches!(&b.right, Expr::SubQuery(s) if matches!(s.as_ref(), Stmt::Insert(_) | Stmt::Update(_) | Stmt::Delete(_))) =>
809 {
810 &Expr::IfElse(Box::new(ast::IfElse {
811 if_expr: b.left.clone(),
812 condition: Expr::UnaryOp(Box::new(ast::UnaryOp {
813 op: ast::UnaryOpKind::Exists,
814 operand: b.left.clone(),
815 })),
816 else_expr: b.right.clone(),
817 }))
818 }
819 other => other,
820 };
821 let fake_sel = ast::SelectStmt {
824 result: expr.clone(),
825 filter: None,
826 order_by: vec![],
827 offset: None,
828 limit: None,
829 lock: None,
830 };
831 c.compile_stmt(&Stmt::Select(fake_sel))
832}
833
834pub fn functions_needing_globals(schema: &SchemaDescriptor) -> &std::collections::HashSet<String> {
842 schema
843 .functions_needing_globals
844 .get_or_init(|| find_functions_needing_globals(schema))
845}
846
847fn find_functions_needing_globals(schema: &SchemaDescriptor) -> std::collections::HashSet<String> {
848 let mut needs: std::collections::HashSet<String> = std::collections::HashSet::new();
849 loop {
850 let mut changed = false;
851 for fd in &schema.functions {
852 let qualified = format!("{}::{}", fd.module, fd.name);
853 if needs.contains(&qualified) {
854 continue;
855 }
856 if let Ok(out) = compile_fn_body_with(fd, schema, &needs)
857 && out.uses_globals_arg
858 {
859 needs.insert(qualified);
860 changed = true;
861 }
862 }
863 if !changed {
864 return needs;
865 }
866 }
867}
868
869pub fn compile_fn_body(
874 fn_desc: &crate::schema::FunctionDescriptor,
875 schema: &SchemaDescriptor,
876) -> Result<super::IrOutput, crate::error::PyQLError> {
877 compile_fn_body_with(fn_desc, schema, functions_needing_globals(schema))
878}
879
880pub fn compile_fn_body_with(
883 fn_desc: &crate::schema::FunctionDescriptor,
884 schema: &SchemaDescriptor,
885 fns_needing_globals: &std::collections::HashSet<String>,
886) -> Result<super::IrOutput, crate::error::PyQLError> {
887 use crate::parse;
888 use crate::parse::ast::Stmt;
889
890 let body = fn_desc.body.trim().to_string();
891 let starts_a_stmt = ["select", "with", "for", "group", "insert", "update", "delete"]
895 .iter()
896 .any(|keyword| {
897 body.len() > keyword.len()
898 && body[..keyword.len()].eq_ignore_ascii_case(keyword)
899 && !body.as_bytes()[keyword.len()].is_ascii_alphanumeric()
900 && body.as_bytes()[keyword.len()] != b'_'
901 });
902 let body = if starts_a_stmt {
903 body
904 } else {
905 format!("select {}", body)
906 };
907
908 let ast = parse::parse(&body)?;
909 let mut c = Compiler::new(schema);
910 c.in_fn_body = true;
911 c.fns_needing_globals = Some(fns_needing_globals.clone());
912 for p in &fn_desc.params {
913 c.fn_params.insert(p.name.clone(), p.pg_type.clone());
914 }
915
916 let (ctes, ir) = if let Stmt::With(w) = &ast {
917 let mut cte_defs = vec![];
918 for alias in &w.aliases {
919 let ir_stmt = compile_cte_binding(&mut c, &alias.expr)?;
920 let type_name = cte_stmt_type(&ir_stmt);
921 c.cte_types.insert(alias.name.clone(), type_name.clone());
922 c.note_cte_cardinality(&alias.name, &ir_stmt);
923 cte_defs.push(super::IrCteDef {
924 name: alias.name.clone(),
925 stmt: ir_stmt,
926 type_name,
927 correlated_to: None,
928 });
929 }
930 let main = c.compile_stmt(&w.stmt)?;
931 (cte_defs, main)
932 } else {
933 (vec![], c.compile_stmt(&ast)?)
934 };
935
936 let mut ctes = ctes;
937 ctes.extend(std::mem::take(&mut c.hoisted_ctes));
938 Ok(super::IrOutput {
939 subtype_fanouts: c.subtype_fanouts(),
940 stmt: ir,
941 params: c.params,
942 ctes,
943 global_ctes: c.global_ctes,
944 warnings: c.warnings,
945 uses_globals_arg: c.used_globals_arg,
946 })
947}
948
949pub struct RewriteAssignment {
964 pub pointer: String,
965 pub column: String,
966 pub ir: IrExpr,
967 pub sql: String,
968 pub reads_a_multi_link: bool,
973}
974
975pub fn compile_rewrite_assignments(
981 type_name: &str,
982 on_mask: u8,
983 schema: &SchemaDescriptor,
984) -> Result<Vec<RewriteAssignment>, crate::error::PyQLError> {
985 let mut c = Compiler::new(schema);
986 c.in_fn_body = true;
987 let owner_td = c.resolve_type(type_name)?;
988 c.special_anchors
989 .insert("__new__".to_string(), (owner_td, "NEW".to_string()));
990 if on_mask & 1 == 0 {
991 c.special_anchors
992 .insert("__old__".to_string(), (owner_td, "OLD".to_string()));
993 }
994 let pointers = owner_td
995 .properties
996 .iter()
997 .map(|p| (p.name.clone(), p.name.clone(), &p.rewrites))
998 .chain(
999 owner_td
1000 .links
1001 .iter()
1002 .filter(|l| !l.is_junction_backed())
1003 .map(|l| (l.name.clone(), format!("{}_id", l.name), &l.rewrites)),
1004 );
1005 let fanouts = c.subtype_fanouts();
1006 let mut out = Vec::new();
1007 for (name, column, rewrites) in pointers {
1008 for rw in rewrites.iter().filter(|rw| rw.on & on_mask != 0) {
1009 if on_mask == 1 && subject_default_insert(&rw.handler, &name).is_some() {
1011 continue;
1012 }
1013 let expr = crate::parse::parse_pointer_expr(&rw.handler)?;
1014 let ir = c.compile_expr(&expr, owner_td, "NEW")?;
1015 if !c.params.is_empty() || !c.hoisted_ctes.is_empty() {
1016 return Err(PyQLError::Type(PyQLTypeError {
1017 message: format!(
1018 "the rewrite of '{type_name}.{name}' has nowhere to bind a parameter or a binding"
1019 ),
1020 position: Position { line: 0, col: 0 },
1021 }));
1022 }
1023 let sql = crate::sql::emit_expr_with_fanouts(&ir, &fanouts);
1024 let junctions = c.junction_tables_of(owner_td)?;
1029 let reads_a_multi_link = junctions.iter().any(|table| sql.contains(table.as_str()));
1030 out.push(RewriteAssignment {
1031 pointer: name.clone(),
1032 column: column.clone(),
1033 ir,
1034 sql,
1035 reads_a_multi_link,
1036 });
1037 }
1038 }
1039 Ok(out)
1040}
1041
1042pub fn compile_trigger_handler(
1043 handler: &str,
1044 type_name: &str,
1045 on_mask: u8,
1046 schema: &SchemaDescriptor,
1047) -> Result<super::IrOutput, crate::error::PyQLError> {
1048 use crate::parse;
1049 use crate::parse::ast::Stmt;
1050
1051 let ast = parse::parse(handler.trim())?;
1052 let mut c = Compiler::new(schema);
1053 c.in_fn_body = true;
1056 let owner_td = c.resolve_type(type_name)?;
1061 if on_mask & 4 == 0 {
1062 c.special_anchors
1063 .insert("__new__".to_string(), (owner_td, "NEW".to_string()));
1064 }
1065 if on_mask & 1 == 0 {
1066 c.special_anchors
1067 .insert("__old__".to_string(), (owner_td, "OLD".to_string()));
1068 }
1069
1070 let (ctes, ir) = if let Stmt::With(w) = &ast {
1071 let mut cte_defs = vec![];
1072 for alias in &w.aliases {
1073 if !matches!(alias.expr, ast::Expr::SubQuery(_)) {
1077 let anchor = if on_mask & 4 == 0 { "NEW" } else { "OLD" };
1078 let value = c.compile_expr(&alias.expr, owner_td, anchor)?;
1079 c.inline_bindings.insert(alias.name.clone(), value);
1080 continue;
1081 }
1082 let ir_stmt = compile_cte_binding(&mut c, &alias.expr)?;
1083 let sql_name = c.claim_cte_sql_name(&alias.name);
1084 let cte_type_name = cte_stmt_type(&ir_stmt);
1085 c.cte_types.insert(alias.name.clone(), cte_type_name.clone());
1086 c.note_cte_cardinality(&alias.name, &ir_stmt);
1087 cte_defs.push(super::IrCteDef {
1088 name: sql_name,
1089 stmt: ir_stmt,
1090 type_name: cte_type_name,
1091 correlated_to: None,
1092 });
1093 }
1094 let main = c.compile_stmt(&w.stmt)?;
1095 (cte_defs, main)
1096 } else {
1097 (vec![], c.compile_stmt(&ast)?)
1098 };
1099
1100 let recursive_kind = recursive_dml_event(&ir, type_name)
1101 .or_else(|| ctes.iter().find_map(|cte| recursive_dml_event(&cte.stmt, type_name)));
1102 if let Some(kind) = recursive_kind
1103 && kind & on_mask != 0
1104 {
1105 return Err(crate::error::PyQLError::Fragment(crate::error::PyQLFragmentError {
1106 message: format!(
1107 "trigger on {type_name} is recursive: its handler {}s its own type, \
1108 which this trigger also fires on",
1109 dml_event_word(kind),
1110 ),
1111 context: type_name.to_string(),
1112 position: crate::error::Position { line: 0, col: 0 },
1113 }));
1114 }
1115
1116 let mut ctes = ctes;
1117 ctes.extend(std::mem::take(&mut c.hoisted_ctes));
1118 Ok(super::IrOutput {
1119 subtype_fanouts: c.subtype_fanouts(),
1120 stmt: ir,
1121 params: c.params,
1122 ctes,
1123 global_ctes: c.global_ctes,
1124 warnings: c.warnings,
1125 uses_globals_arg: c.used_globals_arg,
1126 })
1127}
1128
1129fn dml_event_word(kind: u8) -> &'static str {
1130 match kind {
1131 1 => "insert",
1132 2 => "update",
1133 4 => "delete",
1134 _ => "mutate",
1135 }
1136}
1137
1138fn recursive_dml_event(stmt: &IrStmt, owner_type: &str) -> Option<u8> {
1154 match stmt {
1155 IrStmt::Insert(ins) if ins.target.type_name == owner_type => Some(1),
1156 IrStmt::Update(upd) if upd.target.type_name == owner_type => Some(2),
1157 IrStmt::Delete(del) if del.target.type_name == owner_type => Some(4),
1158 IrStmt::Select(sel) => sel
1159 .dml_source
1160 .as_deref()
1161 .and_then(|inner| recursive_dml_event(inner, owner_type)),
1162 IrStmt::For(for_stmt) => recursive_dml_event(&for_stmt.body, owner_type),
1163 _ => None,
1164 }
1165}
1166
1167pub fn compile_expr_in_type(
1170 expr: &Expr,
1171 type_name: &str,
1172 schema: &SchemaDescriptor,
1173) -> Result<(IrExpr, Vec<String>), PyQLError> {
1174 let mut c = Compiler::new(schema);
1175 let td = c.resolve_type(type_name)?;
1176 let alias = c.fresh_alias();
1177 let ir = c.compile_expr(expr, td, &alias)?;
1178 Ok((ir, c.params))
1179}
1180
1181pub fn compile_computed_in_type(
1189 cd: &crate::schema::ComputedDescriptor,
1190 type_name: &str,
1191 schema: &SchemaDescriptor,
1192) -> Result<Option<IrExpr>, PyQLError> {
1193 let mut c = Compiler::new(schema);
1194 let td = c.resolve_type(type_name)?;
1195 let module = td.module.clone();
1196 let alias = c.fresh_alias();
1197 match c.compile_declared_computed(cd, td, &alias, &module, None, &[])? {
1198 IrShapePointer::Computed(p) => Ok(Some(p.expr)),
1199 _ => Ok(None),
1200 }
1201}
1202
1203pub fn compile_expr_unaliased(
1207 expr: &Expr,
1208 type_name: &str,
1209 schema: &SchemaDescriptor,
1210) -> Result<(IrExpr, Vec<String>), PyQLError> {
1211 let mut c = Compiler::new(schema);
1212 let td = c.resolve_type(type_name)?;
1213 let ir = c.compile_expr(expr, td, "")?;
1214 Ok((ir, c.params))
1215}
1216
1217pub fn compile_scalar_default(pyql: &str, schema: &SchemaDescriptor) -> Result<String, String> {
1222 compile_scalar_default_typed(pyql, schema).map(|(sql, _ir)| sql)
1223}
1224
1225pub fn compile_constraint_expr(
1234 pyql: &str,
1235 type_name: &str,
1236 schema: &SchemaDescriptor,
1237) -> Result<String, crate::error::PyQLError> {
1238 use crate::parse::ast::Stmt;
1239 let ast = crate::parse::parse(&format!("SELECT {pyql}"))?;
1240 let Stmt::Select(sel) = &ast else {
1241 return Err(PyQLError::Type(PyQLTypeError {
1242 message: "constraint expression must be an expression".into(),
1243 position: Position { line: 0, col: 0 },
1244 }));
1245 };
1246 let (ir, _params) = compile_expr_unaliased(&sel.result, type_name, schema)?;
1247 Ok(crate::sql::emit_expr(&ir))
1248}
1249
1250pub fn compile_scalar_default_typed(pyql: &str, schema: &SchemaDescriptor) -> Result<(String, IrExpr), String> {
1251 use crate::parse::ast::Stmt;
1252 let full = format!("SELECT {}", pyql);
1253 let ast = crate::parse::parse(&full).map_err(|e| e.message)?;
1254 let Stmt::Select(sel) = &ast else {
1255 return Err("default expression must be a select statement".into());
1256 };
1257 let mut c = Compiler::new(schema);
1258 let ir = c.compile_free_expr(&sel.result).map_err(|e| e.to_string())?;
1259 let sql = crate::sql::emit_expr(&ir);
1260 Ok((sql, ir))
1261}
1262
1263pub fn default_blocker(expr: &IrExpr) -> Option<&'static str> {
1269 use IrExpr as E;
1270 match expr {
1271 E::Subquery(_)
1272 | E::ObjectSubquery(_)
1273 | E::ObjectPathSubquery(_)
1274 | E::ObjectPathUnion { .. }
1275 | E::PathSubquery(_)
1276 | E::FnSubquery(_)
1277 | E::ArrayFromSelect(_)
1278 | E::ScalarSubquery(_)
1279 | E::AggOverQuery { .. }
1280 | E::AggOverCte { .. }
1281 | E::ExistsOverCte { .. }
1282 | E::SetOp { .. }
1283 | E::AggOverSet { .. }
1284 | E::CteRef { .. }
1285 | E::CteFieldRef { .. }
1286 | E::GlobalRef { .. } => Some("a sub-select"),
1287 E::ColumnRef { .. } => Some("a reference to another pointer"),
1288 E::Param { .. } | E::GlobalParam { .. } => Some("a query parameter"),
1289 E::ForVar { .. } => Some("a for-loop variable"),
1290 E::FnParam { .. } => Some("a function parameter"),
1291 E::BinOp(b) => default_blocker(&b.left).or_else(|| default_blocker(&b.right)),
1292 E::UnaryOp(u) => default_blocker(&u.operand),
1293 E::TypeCast(c) => default_blocker(&c.expr),
1294 E::IfElse(i) => default_blocker(&i.condition)
1295 .or_else(|| default_blocker(&i.if_))
1296 .or_else(|| default_blocker(&i.else_)),
1297 E::FunctionCall(f) => f.args.iter().find_map(default_blocker),
1298 E::Array(items) | E::Tuple(items) => items.iter().find_map(default_blocker),
1299 E::NamedTuple { fields, .. } => fields.iter().find_map(|(_, e)| default_blocker(e)),
1300 E::Subscript { expr, index, .. } => default_blocker(expr).or_else(|| default_blocker(index)),
1301 E::JsonbField { expr, .. } | E::JsonbIndex { expr, .. } => default_blocker(expr),
1302 E::Slice { expr, lower, upper, .. } => default_blocker(expr)
1303 .or_else(|| lower.as_deref().and_then(default_blocker))
1304 .or_else(|| upper.as_deref().and_then(default_blocker)),
1305 E::Literal(_) | E::Null | E::EnumLiteral { .. } | E::RawSql(_) => None,
1306 }
1307}
1308
1309pub fn column_default_sql(pyql: &str, schema: &SchemaDescriptor) -> Option<String> {
1314 let (sql, ir) = compile_scalar_default_typed(pyql, schema).ok()?;
1315 default_blocker(&ir).is_none().then_some(sql)
1316}
1317
1318pub fn inlined_pointer_defaults(td: &TypeDescriptor, schema: &SchemaDescriptor) -> Vec<(String, String)> {
1327 let properties = td
1328 .properties
1329 .iter()
1330 .filter(|p| !p.is_pk)
1331 .filter_map(|p| p.default_pyql.as_ref().map(|d| (p.name.clone(), d.clone())));
1332 let links = td
1333 .links
1334 .iter()
1335 .filter(|l| !l.is_junction_backed())
1336 .filter_map(|l| l.default_pyql.as_ref().map(|d| (l.name.clone(), d.clone())));
1337 properties
1338 .chain(links)
1339 .filter(|(_, pyql)| column_default_sql(pyql, schema).is_none())
1340 .collect()
1341}
1342
1343fn default_shape_element(pointer: &str, value: Expr) -> ShapeElement {
1345 ShapeElement {
1346 path: ast::Path::relative(pointer),
1347 splat: None,
1348 nested: None,
1349 compexpr: Some(value),
1350 op: ShapeOp::Assign,
1351 filter: None,
1352 order_by: vec![],
1353 offset: None,
1354 limit: None,
1355 marker_offset: None,
1356 }
1357}
1358
1359pub fn compile_inlined_default(
1366 type_name: &str,
1367 pointer: &str,
1368 pyql: &str,
1369 schema: &SchemaDescriptor,
1370) -> Result<(String, IrExpr), PyQLError> {
1371 let mut c = Compiler::new(schema);
1372 let td = c.resolve_type(type_name)?;
1373 let alias = c.fresh_alias();
1374 let element = default_shape_element(pointer, crate::parse::parse_pointer_expr(pyql)?);
1375 let mut assignments = c.compile_assignments(&[element], td, &alias)?;
1376 Ok(assignments.remove(0))
1377}
1378
1379struct Compiler<'a> {
1382 schema: &'a SchemaDescriptor,
1383 params: Vec<String>,
1385 alias_counter: usize,
1386 cte_types: HashMap<String, String>,
1388 group_bindings: HashMap<String, IrGroup>,
1392 cte_free_items: HashMap<String, IrFreeExpr>,
1397 multi_row_ctes: HashSet<String>,
1402 for_vars: HashMap<String, String>,
1404 for_var_types: HashMap<String, String>,
1408 for_var_ctes: HashMap<String, String>,
1413 for_var_slots: HashMap<String, String>,
1417 pending_insert_guard: Option<Expr>,
1420 pending_update_guard: Option<Expr>,
1424 pending_delete_guard: Option<Expr>,
1426 cte_declared_pointers: HashMap<String, Vec<ShapeElement>>,
1430 active_declared_pointers: Vec<ShapeElement>,
1432 expanding_computeds: Vec<String>,
1436 anchors: Vec<SelectAnchor>,
1442 hoisted_ctes: Vec<IrCteDef>,
1446 hoisted_binding_sources: HashMap<String, Expr>,
1450 cte_sql_names: HashMap<String, String>,
1455 cte_namespace: std::collections::HashSet<String>,
1459 for_vars_read: std::collections::HashSet<String>,
1462 for_scope: Vec<String>,
1464 link_prop_scope: Vec<Option<(String, String)>>,
1469 pending_detached: bool,
1472 modifier_anchor: Option<(String, String)>,
1477 tail_sorts: Vec<ast::SortExpr>,
1482 inline_bindings: std::collections::HashMap<String, IrExpr>,
1486 in_fn_body: bool,
1491 fns_needing_globals: Option<std::collections::HashSet<String>>,
1500 used_globals_arg: bool,
1503 fn_params: HashMap<String, String>,
1505 special_anchors: HashMap<String, (&'a TypeDescriptor, String)>,
1518 global_ctes: Vec<IrGlobalCte>,
1520 pending_nested_ctes: Vec<IrCteDef>,
1528 junction_read_overrides: HashMap<String, JunctionReadOverride>,
1534 nested_cte_counter: usize,
1535 warnings: Vec<String>,
1537 explicit_set_depth: usize,
1541 config: crate::ir::SessionConfig,
1544 implicit_id_in_shapes: bool,
1551}
1552
1553struct SelectAnchor {
1555 type_name: String,
1556 qualified: String,
1557 alias: String,
1558 detached: bool,
1559 declared_on: Option<(String, String)>,
1573}
1574
1575impl SelectAnchor {
1576 fn answers_to(&self, root: &str) -> bool {
1578 self.type_name == root
1579 || self.qualified == root
1580 || self
1581 .declared_on
1582 .as_ref()
1583 .is_some_and(|(name, qualified)| name == root || qualified == root)
1584 }
1585}
1586
1587pub const GLOBALS_ARG: &str = "__pylon_json_globals__";
1597
1598fn globals_arg_read(qualified: &str, pg_type: &str) -> String {
1600 let key = qualified.replace('\'', "''");
1601 if let Some(element) = pg_type.strip_suffix("[]") {
1602 format!(
1608 "(case when jsonb_typeof({GLOBALS_ARG} -> '{key}') = 'array' \
1609 then coalesce((select array_agg(value::{element}) \
1610 from jsonb_array_elements_text({GLOBALS_ARG} -> '{key}') as value), '{{}}'::{pg_type}) \
1611 else null end)"
1612 )
1613 } else {
1614 format!("(({GLOBALS_ARG} ->> '{key}')::{pg_type})")
1615 }
1616}
1617
1618impl<'a> Compiler<'a> {
1619 fn new(schema: &'a SchemaDescriptor) -> Self {
1620 Self::with_config(schema, crate::ir::SessionConfig::default())
1621 }
1622
1623 fn with_config(schema: &'a SchemaDescriptor, config: crate::ir::SessionConfig) -> Self {
1624 Compiler {
1625 schema,
1626 params: vec![],
1627 alias_counter: 0,
1628 cte_types: HashMap::new(),
1629 group_bindings: HashMap::new(),
1630 cte_free_items: HashMap::new(),
1631 multi_row_ctes: HashSet::new(),
1632 for_vars: HashMap::new(),
1633 for_var_types: HashMap::new(),
1634 for_var_ctes: HashMap::new(),
1635 for_var_slots: HashMap::new(),
1636 pending_insert_guard: None,
1637 pending_update_guard: None,
1638 pending_delete_guard: None,
1639 cte_declared_pointers: HashMap::new(),
1640 active_declared_pointers: vec![],
1641 expanding_computeds: vec![],
1642 fn_params: HashMap::new(),
1643 special_anchors: HashMap::new(),
1644 global_ctes: vec![],
1645 pending_nested_ctes: vec![],
1646 junction_read_overrides: HashMap::new(),
1647 nested_cte_counter: 0,
1648 warnings: vec![],
1649 explicit_set_depth: 0,
1650 config,
1651 implicit_id_in_shapes: true,
1652 anchors: Vec::new(),
1653 link_prop_scope: Vec::new(),
1654 hoisted_ctes: Vec::new(),
1655 hoisted_binding_sources: HashMap::new(),
1656 cte_sql_names: HashMap::new(),
1657 cte_namespace: std::collections::HashSet::new(),
1658 for_vars_read: std::collections::HashSet::new(),
1659 for_scope: Vec::new(),
1660 pending_detached: false,
1661 modifier_anchor: None,
1662 tail_sorts: vec![],
1663 inline_bindings: std::collections::HashMap::new(),
1664 in_fn_body: false,
1665 fns_needing_globals: None,
1666 used_globals_arg: false,
1667 }
1668 }
1669
1670 fn is_value_binding(&self, name: &str) -> bool {
1677 self.inline_bindings.contains_key(name) || self.cte_types.get(name).is_some_and(|t| !t.contains("::"))
1678 }
1679
1680 fn bind_inline_if_correlated(
1687 &mut self,
1688 name: &str,
1689 expr: &Expr,
1690 ctx: Option<(&TypeDescriptor, &str)>,
1691 ) -> Result<bool, PyQLError> {
1692 if (self.anchors.is_empty() && ctx.is_none()) || !matches!(expr, Expr::Path(p) if p.partial) {
1693 return Ok(false);
1694 }
1695 let ir = self.compile_expr_ctx(expr, ctx)?;
1696 self.inline_bindings.insert(name.to_string(), ir);
1697 Ok(true)
1698 }
1699
1700 fn cte_sql_name(&self, name: &str) -> String {
1702 self.cte_sql_names
1703 .get(name)
1704 .cloned()
1705 .unwrap_or_else(|| name.to_string())
1706 }
1707
1708 fn compile_binding_in_scope(&mut self, expr: &Expr) -> Result<(IrStmt, Option<String>), PyQLError> {
1712 let before = std::mem::take(&mut self.for_vars_read);
1713 let ir_stmt = compile_cte_binding(self, expr);
1714 let read = std::mem::replace(&mut self.for_vars_read, before);
1715 let correlated_to = self
1718 .for_scope
1719 .iter()
1720 .rev()
1721 .find(|slot| read.contains(*slot))
1722 .map(|slot| format!("_for_{slot}"));
1723 self.for_vars_read.extend(read);
1724 Ok((ir_stmt?, correlated_to))
1725 }
1726
1727 fn claim_in_cte_namespace(&mut self, base: &str) -> String {
1732 let taken = |namespace: &std::collections::HashSet<String>, candidate: &String| {
1735 namespace.contains(candidate)
1736 || namespace
1737 .iter()
1738 .any(|claimed| claimed != base && candidate.starts_with(&format!("{claimed}__")))
1739 };
1740 let mut candidate = base.to_string();
1741 let mut suffix = 1;
1742 while taken(&self.cte_namespace, &candidate) {
1743 candidate = format!("{base}__{suffix}");
1744 suffix += 1;
1745 }
1746 self.cte_namespace.insert(candidate.clone());
1747 candidate
1748 }
1749
1750 fn claim_generated_cte_name(&mut self, preferred: &str) -> String {
1754 self.claim_in_cte_namespace(preferred)
1755 }
1756
1757 fn claim_cte_sql_name(&mut self, name: &str) -> String {
1760 let base = match name.starts_with('_') {
1764 true => format!("w{name}"),
1765 false => name.to_string(),
1766 };
1767 let claimed = self.claim_in_cte_namespace(&base);
1768 if claimed == name {
1769 self.cte_sql_names.remove(name);
1770 } else {
1771 self.cte_sql_names.insert(name.to_string(), claimed.clone());
1772 }
1773 claimed
1774 }
1775
1776 fn register_cte(&mut self, name: &str, ir_stmt: &IrStmt) -> String {
1777 let type_name = cte_stmt_type(ir_stmt);
1778 self.cte_types.insert(name.to_string(), type_name.clone());
1779 if let IrStmt::Select(sel) = ir_stmt
1780 && let [IrRowSource::Free(item)] = sel.rows.as_slice()
1781 {
1782 self.cte_free_items.insert(name.to_string(), item.clone());
1783 }
1784 self.note_cte_cardinality(name, ir_stmt);
1785 type_name
1786 }
1787
1788 fn pins_an_exclusive_property(&self, sel: &IrSelect) -> bool {
1793 let [IrRowSource::Bound { source, .. }] = sel.rows.as_slice() else {
1794 return false;
1795 };
1796 let Some(filter) = &sel.filter else { return false };
1797 let Ok(td) = self.resolve_type(&source.type_name) else {
1798 return false;
1799 };
1800 Self::pins_exclusive(td, filter)
1801 }
1802
1803 fn pins_exclusive(td: &TypeDescriptor, filter: &IrExpr) -> bool {
1807 let IrExpr::BinOp(binop) = filter else { return false };
1808 match binop.op {
1809 ast::BinOpKind::And => Self::pins_exclusive(td, &binop.left) || Self::pins_exclusive(td, &binop.right),
1810 ast::BinOpKind::Eq => {
1811 let column = match (&binop.left, &binop.right) {
1812 (IrExpr::ColumnRef { column, .. }, _) => column,
1813 (_, IrExpr::ColumnRef { column, .. }) => column,
1814 _ => return false,
1815 };
1816 td.properties
1817 .iter()
1818 .any(|p| &p.name == column && (p.is_exclusive || p.is_pk))
1819 }
1820 _ => false,
1821 }
1822 }
1823
1824 fn note_cte_cardinality(&mut self, name: &str, ir_stmt: &IrStmt) {
1830 let single = match ir_stmt {
1831 IrStmt::Insert(_) => true,
1832 IrStmt::Select(sel) => {
1833 matches!(sel.limit, Some(IrExpr::Literal(IrLiteral::Int(1))))
1834 || matches!(
1835 sel.rows.as_slice(),
1836 [IrRowSource::Free(
1837 IrFreeExpr::Scalar(_)
1838 | IrFreeExpr::FreeObject(_)
1839 | IrFreeExpr::NamedTupleRow(_)
1840 | IrFreeExpr::Tuple(_)
1841 )]
1842 )
1843 || self.pins_an_exclusive_property(sel)
1844 }
1845 _ => false,
1846 };
1847 if !single {
1848 self.multi_row_ctes.insert(name.to_string());
1849 }
1850 }
1851
1852 fn resolve_cte_field_chain(&self, root: &str, steps: &[&str]) -> Option<Result<IrExpr, PyQLError>> {
1863 let (first, rest) = steps.split_first()?;
1864 let fields = match self.cte_free_items.get(root)? {
1865 IrFreeExpr::FreeObject(fields) => fields,
1866 _ => return None,
1867 };
1868 let mut current: &IrExpr = match fields.iter().find(|(n, _)| n == first) {
1869 Some((_, e)) => e,
1870 None => {
1871 return Some(Err(
1872 self.type_err(&format!("free object '{root}' has no field '{first}'"))
1873 ));
1874 }
1875 };
1876 let mut expr = IrExpr::CteFieldRef {
1877 name: root.to_string(),
1878 field: first.to_string(),
1879 pg_type: infer_ir_type(current).map(str::to_string),
1880 };
1881 for step in rest {
1882 if let IrExpr::NamedTuple {
1883 fields: nested,
1884 is_free_object: true,
1885 } = current
1886 {
1887 match nested.iter().find(|(n, _)| n == step) {
1888 Some((_, next)) => current = next,
1889 None => {
1890 return Some(Err(
1891 self.type_err(&format!("{step} is not a member of the nested free object"))
1892 ));
1893 }
1894 }
1895 }
1896 expr = IrExpr::JsonbField {
1897 expr: Box::new(expr),
1898 field: step.to_string(),
1899 };
1900 }
1901 Some(Ok(expr))
1902 }
1903
1904 fn resolve_cte_path(&self, p: &ast::Path) -> Option<Result<IrExpr, PyQLError>> {
1909 if p.partial || p.steps.len() < 2 {
1910 return None;
1911 }
1912 let ast::PathStep::Name(root) = &p.steps[0] else {
1913 return None;
1914 };
1915 let mut steps = Vec::with_capacity(p.steps.len() - 1);
1916 for step in &p.steps[1..] {
1917 match step {
1918 ast::PathStep::Name(n) => steps.push(n.as_str()),
1919 _ => return None,
1920 }
1921 }
1922 self.resolve_cte_field_chain(root, &steps)
1923 }
1924
1925 fn resolve_cte_name<'e>(&self, expr: &'e Expr) -> Option<&'e str> {
1927 if let Expr::Path(p) = expr
1928 && !p.partial
1929 && p.steps.len() == 1
1930 && let ast::PathStep::Name(n) = &p.steps[0]
1931 && self.cte_types.contains_key(n.as_str())
1932 {
1933 return Some(n.as_str());
1934 }
1935 None
1936 }
1937
1938 fn for_var_ref(&mut self, name: &str) -> IrExpr {
1943 let slot = self
1944 .for_var_slots
1945 .get(name)
1946 .cloned()
1947 .unwrap_or_else(|| name.to_string());
1948 self.for_vars_read.insert(slot.clone());
1949 let pg_type = self.for_vars.get(name).cloned();
1950 IrExpr::ForVar { name: slot, pg_type }
1951 }
1952
1953 fn fresh_alias(&mut self) -> String {
1954 let a = format!("t{}", self.alias_counter);
1955 self.alias_counter += 1;
1956 a
1957 }
1958
1959 fn fresh_nested_cte_name(&mut self) -> String {
1963 let n = self.nested_cte_counter;
1964 self.nested_cte_counter += 1;
1965 self.claim_generated_cte_name(&format!("_nested_dml_{n}"))
1966 }
1967
1968 fn param_index(&mut self, name: &str) -> usize {
1970 if let Some(i) = self.params.iter().position(|n| n == name) {
1971 return i;
1972 }
1973 let i = self.params.len();
1974 self.params.push(name.to_string());
1975 i
1976 }
1977
1978 fn resolve_global_pg_type(&self, scalar_type: &str) -> String {
1986 let builtin = match scalar_type.strip_prefix("std::").unwrap_or(scalar_type) {
1988 "str" => Some("text"),
1989 "int16" => Some("int2"),
1990 "int32" => Some("int4"),
1991 "int64" => Some("int8"),
1992 "float32" => Some("float4"),
1993 "float64" => Some("float8"),
1994 "decimal" => Some("numeric"),
1995 "bool" => Some("boolean"),
1996 "datetime" => Some("timestamptz"),
1997 "cal::local_datetime" => Some("timestamp"),
1998 "cal::local_date" => Some("date"),
1999 "cal::local_time" => Some("time"),
2000 "uuid" => Some("uuid"),
2001 "bytes" => Some("bytea"),
2002 "json" => Some("jsonb"),
2003 "duration" => Some("interval"),
2004 _ => None,
2005 };
2006 if let Some(t) = builtin {
2007 return t.to_string();
2008 }
2009 if let Some(inner) = scalar_type.strip_prefix("array<").and_then(|s| s.strip_suffix('>')) {
2010 return format!("{}[]", self.resolve_global_pg_type(inner));
2011 }
2012 if scalar_type.starts_with("tuple<") {
2013 return "jsonb".to_string();
2014 }
2015 if let Some(ed) = self.resolve_enum(scalar_type) {
2016 return format!("{}.\"{}\"", crate::sql::pg_schema_str(&ed.module), ed.name);
2017 }
2018 if self.resolve_named_tuple(scalar_type).is_some() {
2019 return "jsonb".to_string();
2020 }
2021 self.schema
2023 .scalars
2024 .iter()
2025 .find(|s| s.name == scalar_type || format!("{}::{}", s.module, s.name) == scalar_type)
2026 .map(|s| s.pg_type.clone())
2027 .unwrap_or_else(|| "text".to_string())
2028 }
2029
2030 fn try_compile_global_select(
2034 &mut self,
2035 outer: &ast::SelectStmt,
2036 result: &Expr,
2037 _distinct: bool,
2038 ) -> Result<Option<IrStmt>, PyQLError> {
2039 let (global_name, shape_elements): (&str, &[ast::ShapeElement]) = match result {
2040 Expr::Global(name) => (name.as_str(), &[]),
2041 Expr::Shape(sh) => match sh.expr.as_ref() {
2042 Some(Expr::Global(name)) => (name.as_str(), sh.elements.as_slice()),
2043 _ => return Ok(None),
2044 },
2045 _ => return Ok(None),
2046 };
2047
2048 let global = self
2049 .schema
2050 .globals
2051 .iter()
2052 .find(|g| g.name == global_name || format!("{}::{}", g.module, g.name) == global_name);
2053 let global = match global {
2054 Some(g) => g.clone(),
2055 None => return Ok(None),
2056 };
2057 let computed_expr = match global.computed_expr {
2058 Some(e) => e,
2059 None => return Ok(None), };
2061
2062 let inner_ast = crate::parse::parse(&computed_expr)?;
2063 let inner_sel = match inner_ast {
2064 Stmt::Select(sel) => sel,
2065 _ => return Ok(None),
2066 };
2067
2068 let merged_result = if shape_elements.is_empty() {
2069 inner_sel.result.clone()
2070 } else {
2071 Expr::Shape(Box::new(ast::ShapeExpr {
2072 expr: Some(inner_sel.result.clone()),
2073 elements: shape_elements.to_vec(),
2074 marker_offset: None,
2075 }))
2076 };
2077
2078 let merged_filter = match (&inner_sel.filter, &outer.filter) {
2079 (Some(a), Some(b)) => Some(Expr::BinOp(Box::new(ast::BinOp {
2080 left: a.clone(),
2081 op: ast::BinOpKind::And,
2082 right: b.clone(),
2083 }))),
2084 (Some(a), None) => Some(a.clone()),
2085 (None, b) => b.clone(),
2086 };
2087
2088 let merged = ast::SelectStmt {
2089 result: merged_result,
2090 filter: merged_filter,
2091 order_by: if outer.order_by.is_empty() {
2092 inner_sel.order_by.clone()
2093 } else {
2094 outer.order_by.clone()
2095 },
2096 offset: outer.offset.clone().or(inner_sel.offset.clone()),
2097 limit: outer.limit.clone().or(inner_sel.limit.clone()),
2098 lock: outer.lock.clone().or(inner_sel.lock.clone()),
2099 };
2100
2101 let ir = self.compile_stmt(&Stmt::Select(merged))?;
2102 Ok(Some(ir))
2103 }
2104
2105 fn project_free_object_field(expr: IrExpr, field: &str) -> IrExpr {
2113 if let IrExpr::NamedTuple {
2114 fields,
2115 is_free_object: true,
2116 } = &expr
2117 && let Some((_, value)) = fields.iter().find(|(name, _)| name == field)
2118 {
2119 return value.clone();
2120 }
2121 IrExpr::JsonbField {
2122 expr: Box::new(expr),
2123 field: field.to_string(),
2124 }
2125 }
2126
2127 fn union_of_relative_paths(expr: &Expr) -> Option<Vec<ast::Path>> {
2130 fn walk(expr: &Expr, out: &mut Vec<ast::Path>) -> bool {
2131 match expr {
2132 Expr::Union(a, b) => walk(a, out) && walk(b, out),
2133 Expr::Path(p) if p.partial => {
2134 out.push(p.clone());
2135 true
2136 }
2137 _ => false,
2138 }
2139 }
2140 if !matches!(expr, Expr::Union(_, _)) {
2141 return None;
2142 }
2143 let mut out = vec![];
2144 walk(expr, &mut out).then_some(out)
2145 }
2146
2147 fn coalesce_of_relative_paths(expr: &Expr) -> Option<Vec<ast::Path>> {
2152 fn walk(expr: &Expr, out: &mut Vec<ast::Path>) -> bool {
2153 match expr {
2154 Expr::BinOp(b) if b.op == ast::BinOpKind::Coalesce => walk(&b.left, out) && walk(&b.right, out),
2155 Expr::Path(p) if p.partial => {
2156 out.push(p.clone());
2157 true
2158 }
2159 _ => false,
2160 }
2161 }
2162 if !matches!(expr, Expr::BinOp(b) if b.op == ast::BinOpKind::Coalesce) {
2163 return None;
2164 }
2165 let mut out = vec![];
2166 walk(expr, &mut out).then_some(out)
2167 }
2168
2169 fn shape_over_subquery(&mut self, sh: &ast::ShapeExpr) -> Result<Option<IrExpr>, PyQLError> {
2176 fn push_shape(stmt: &Stmt, elements: &[ShapeElement]) -> Option<Stmt> {
2177 match stmt {
2178 Stmt::Select(sel) => {
2179 if matches!(sel.result, Expr::Shape(_)) {
2180 return None;
2181 }
2182 let mut shaped = sel.clone();
2183 shaped.result = Expr::Shape(Box::new(ast::ShapeExpr {
2184 expr: Some(sel.result.clone()),
2185 elements: elements.to_vec(),
2186 marker_offset: None,
2187 }));
2188 Some(Stmt::Select(shaped))
2189 }
2190 Stmt::With(w) => {
2191 let inner = push_shape(&w.stmt, elements)?;
2192 let mut carried = w.clone();
2193 carried.stmt = Box::new(inner);
2194 Some(Stmt::With(carried))
2195 }
2196 _ => None,
2197 }
2198 }
2199 let Some(Expr::SubQuery(stmt)) = sh.expr.as_ref() else {
2200 return Ok(None);
2201 };
2202 let Some(shaped) = push_shape(stmt.as_ref(), &sh.elements) else {
2203 return Ok(None);
2204 };
2205 let single = matches!(
2206 innermost_select(&shaped).and_then(|s| s.limit.as_ref()),
2207 Some(Expr::Literal(ast::Literal::Int(1)))
2208 );
2209 let IrStmt::Select(select) = self.compile_stmt(&shaped)? else {
2210 return Ok(None);
2211 };
2212 if !matches!(select.rows.as_slice(), [IrRowSource::Bound { .. }]) {
2213 return Ok(None);
2214 }
2215 Ok(Some(if single {
2216 IrExpr::ObjectSubquery(Box::new(select))
2217 } else {
2218 IrExpr::ArrayFromSelect(Box::new(IrArraySource::ObjectSelect(Box::new(select))))
2219 }))
2220 }
2221
2222 fn shape_over_subquery_projection(sh: &ast::ShapeExpr) -> Option<(&Stmt, Vec<String>)> {
2227 let inner = sh.expr.as_ref()?;
2228 if !matches!(inner, Expr::FieldAccess { .. }) {
2229 return None;
2230 }
2231 let (base, fields) = Self::peel_field_access_chain(inner);
2232 match base {
2233 Expr::SubQuery(stmt) => Some((stmt.as_ref(), fields)),
2234 _ => None,
2235 }
2236 }
2237
2238 fn junction_overrides_for(dml: &IrStmt, cte_name: &str) -> HashMap<String, JunctionReadOverride> {
2248 let appends = match dml {
2249 IrStmt::Insert(ins) => &ins.multi_link_appends,
2250 IrStmt::Update(upd) => &upd.multi_link_appends,
2251 _ => return HashMap::new(),
2252 };
2253 appends
2254 .iter()
2255 .enumerate()
2256 .filter(|(_, append)| append.values.link_props.is_empty())
2257 .map(|(i, append)| {
2258 let targets = match &append.values.source {
2261 IrMultiLinkValueSource::CteRef(target_cte) => Some(format!("@cte:{target_cte}")),
2262 _ => None,
2263 };
2264 (
2265 append.junction_table.clone(),
2266 JunctionReadOverride {
2267 junction: format!("@cte:{cte_name}__ml_add_{i}"),
2268 targets,
2269 },
2270 )
2271 })
2272 .collect()
2273 }
2274
2275 fn try_compile_walk_off_subquery(
2286 &mut self,
2287 expr: &Expr,
2288 ctx: Option<(&TypeDescriptor, &str)>,
2289 ) -> Result<Option<IrExpr>, PyQLError> {
2290 if !matches!(expr, Expr::PathStepOn { .. } | Expr::FieldAccess { .. }) {
2291 return Ok(None);
2292 }
2293 let (base, steps) = Self::peel_path_step_chain(expr);
2294 if !steps.iter().any(|step| !matches!(step, ast::PathStep::Name(_))) {
2295 return Ok(None);
2296 }
2297 let Expr::SubQuery(inner_stmt) = base else {
2298 return Ok(None);
2299 };
2300 let inner = compile_cte_binding(self, &Expr::SubQuery(inner_stmt.clone()))?;
2302 let cte_name = self.fresh_nested_cte_name();
2303 let overrides = Self::junction_overrides_for(&inner, &cte_name);
2304 let type_name = self.register_cte(&cte_name, &inner);
2305 self.hoisted_ctes.push(IrCteDef {
2306 name: cte_name.clone(),
2307 stmt: inner,
2308 type_name,
2309 correlated_to: None,
2310 });
2311 let mut path_steps = vec![ast::PathStep::Name(cte_name)];
2312 path_steps.extend(steps);
2313 let path = ast::Path {
2314 steps: path_steps,
2315 partial: false,
2316 };
2317 let previous = std::mem::replace(&mut self.junction_read_overrides, overrides);
2318 let ir = match ctx {
2319 Some((td, alias)) => self.compile_path(&path, td, alias),
2320 None => self.compile_free_path(&path),
2321 };
2322 self.junction_read_overrides = previous;
2323 Ok(Some(ir?))
2324 }
2325
2326 fn peel_path_step_chain(expr: &Expr) -> (&Expr, Vec<ast::PathStep>) {
2331 let mut steps = Vec::new();
2332 let mut current = expr;
2333 loop {
2334 match current {
2335 Expr::FieldAccess { expr: inner, field } => {
2336 steps.push(ast::PathStep::Name(field.clone()));
2337 current = inner;
2338 }
2339 Expr::PathStepOn { expr: inner, step } => {
2340 steps.push((**step).clone());
2341 current = inner;
2342 }
2343 _ => break,
2344 }
2345 }
2346 steps.reverse();
2347 (current, steps)
2348 }
2349
2350 fn peel_field_access_chain(expr: &Expr) -> (&Expr, Vec<String>) {
2351 let mut fields = Vec::new();
2352 let mut current = expr;
2353 while let Expr::FieldAccess { expr: inner, field } = current {
2354 fields.push(field.clone());
2355 current = inner;
2356 }
2357 fields.reverse();
2358 (current, fields)
2359 }
2360
2361 fn resolve_field_owner_select(&self, expr: &Expr) -> Option<ast::SelectStmt> {
2369 let sel = match expr {
2370 Expr::SubQuery(stmt) => match stmt.as_ref() {
2371 Stmt::Select(sel) => sel.clone(),
2372 _ => return None,
2373 },
2374 Expr::Global(name) => {
2375 let global = self
2376 .schema
2377 .globals
2378 .iter()
2379 .find(|g| g.name == *name || format!("{}::{}", g.module, g.name) == *name)?;
2380 let computed_expr = global.computed_expr.as_ref()?;
2381 match crate::parse::parse(computed_expr).ok()? {
2382 Stmt::Select(sel) => sel,
2383 _ => return None,
2384 }
2385 }
2386 _ => return None,
2387 };
2388 let mut sel = sel;
2392 if let Expr::Detached(inner) = &sel.result {
2393 sel.result = inner.as_ref().clone();
2394 }
2395 match &sel.result {
2396 Expr::Path(p) if !p.partial => Some(sel),
2397 _ => None,
2398 }
2399 }
2400
2401 fn compile_walk_off_bound_head(
2415 &mut self,
2416 outer: &ast::SelectStmt,
2417 inner_sel: &ast::SelectStmt,
2418 fields: &[String],
2419 trailing_shape: &[ShapeElement],
2420 ) -> Result<Option<IrStmt>, PyQLError> {
2421 let head = Expr::SubQuery(Box::new(Stmt::Select(inner_sel.clone())));
2422 let inner = compile_cte_binding(self, &head)?;
2423 let cte_name = self.fresh_nested_cte_name();
2424 let overrides = Self::junction_overrides_for(&inner, &cte_name);
2425 let type_name = self.register_cte(&cte_name, &inner);
2426 self.hoisted_ctes.push(IrCteDef {
2427 name: cte_name.clone(),
2428 stmt: inner,
2429 type_name,
2430 correlated_to: None,
2431 });
2432 let mut steps = vec![ast::PathStep::Name(cte_name)];
2433 steps.extend(fields.iter().cloned().map(ast::PathStep::Name));
2434 let walk = Expr::Path(ast::Path { steps, partial: false });
2435 let result = if trailing_shape.is_empty() {
2436 walk
2437 } else {
2438 Expr::Shape(Box::new(ast::ShapeExpr {
2439 expr: Some(walk),
2440 elements: trailing_shape.to_vec(),
2441 marker_offset: None,
2442 }))
2443 };
2444 let rerooted = ast::SelectStmt {
2445 result,
2446 filter: outer.filter.clone(),
2447 order_by: outer.order_by.clone(),
2448 offset: outer.offset.clone(),
2449 limit: outer.limit.clone(),
2450 lock: outer.lock.clone(),
2451 };
2452 let previous = std::mem::replace(&mut self.junction_read_overrides, overrides);
2453 let ir = self.compile_stmt(&Stmt::Select(rerooted));
2454 self.junction_read_overrides = previous;
2455 Ok(Some(ir?))
2456 }
2457
2458 fn try_compile_field_access_select(
2459 &mut self,
2460 outer: &ast::SelectStmt,
2461 result: &Expr,
2462 ) -> Result<Option<IrStmt>, PyQLError> {
2463 let (result, trailing_shape): (&Expr, &[ShapeElement]) = match result {
2467 Expr::Shape(sh) => match sh.expr.as_ref() {
2468 Some(inner @ Expr::FieldAccess { .. }) => (inner, sh.elements.as_slice()),
2469 _ => (result, &[]),
2470 },
2471 other => (other, &[]),
2472 };
2473 let (root, fields) = Self::peel_field_access_chain(result);
2474 if fields.is_empty() {
2475 return Ok(None);
2476 }
2477 let inner_sel = match self.resolve_field_owner_select(root) {
2478 Some(sel) => sel,
2479 None => return Ok(None),
2480 };
2481 let Expr::Path(type_path) = &inner_sel.result else {
2482 return Ok(None);
2483 };
2484 if inner_sel.limit.is_some() || inner_sel.offset.is_some() {
2490 return self.compile_walk_off_bound_head(outer, &inner_sel, &fields, trailing_shape);
2491 }
2492 let mut steps = type_path.steps.clone();
2493 let field_count = fields.len();
2494 steps.extend(fields.into_iter().map(ast::PathStep::Name));
2495 let merged_result = Expr::Path(ast::Path { steps, partial: false });
2496
2497 let merged_filter = match (&inner_sel.filter, &outer.filter) {
2498 (Some(a), Some(b)) => Some(Expr::BinOp(Box::new(ast::BinOp {
2499 left: a.clone(),
2500 op: ast::BinOpKind::And,
2501 right: b.clone(),
2502 }))),
2503 (Some(a), None) => Some(a.clone()),
2504 (None, b) => b.clone(),
2505 };
2506
2507 let merged = ast::SelectStmt {
2508 result: merged_result,
2509 filter: merged_filter,
2510 order_by: if outer.order_by.is_empty() {
2511 inner_sel.order_by.clone()
2512 } else {
2513 outer.order_by.clone()
2514 },
2515 offset: outer.offset.clone().or(inner_sel.offset.clone()),
2516 limit: outer.limit.clone().or(inner_sel.limit.clone()),
2517 lock: outer.lock.clone().or(inner_sel.lock.clone()),
2518 };
2519
2520 if outer.filter.is_none() {
2529 let merged = ast::SelectStmt {
2530 order_by: inner_sel.order_by.clone(),
2531 ..merged
2532 };
2533 let Expr::Path(merged_path) = &merged.result else {
2534 unreachable!("built as a path just above")
2535 };
2536 let ps = self.compile_path_select_with_tail(
2537 &merged,
2538 merged_path,
2539 trailing_shape,
2540 false,
2541 field_count,
2542 &outer.order_by,
2543 )?;
2544 return Ok(Some(IrStmt::PathSelect(ps)));
2545 }
2546
2547 let merged = if trailing_shape.is_empty() {
2548 merged
2549 } else {
2550 ast::SelectStmt {
2551 result: Expr::Shape(Box::new(ast::ShapeExpr {
2552 expr: Some(merged.result.clone()),
2553 elements: trailing_shape.to_vec(),
2554 marker_offset: None,
2555 })),
2556 ..merged
2557 }
2558 };
2559 let ir = self.compile_stmt(&Stmt::Select(merged))?;
2560 Ok(Some(ir))
2561 }
2562
2563 fn try_compile_alias_select(
2564 &mut self,
2565 outer: &ast::SelectStmt,
2566 result: &Expr,
2567 distinct: bool,
2568 ) -> Result<Option<IrStmt>, PyQLError> {
2569 let (path_name, shape_elements): (&str, &[ast::ShapeElement]) = match result {
2571 Expr::Path(p) if !p.partial && p.steps.len() == 1 => {
2572 if let ast::PathStep::Name(n) = &p.steps[0] {
2573 (n.as_str(), &[])
2574 } else {
2575 return Ok(None);
2576 }
2577 }
2578 Expr::Shape(sh) => match sh.expr.as_ref() {
2579 Some(Expr::Path(p)) if !p.partial && p.steps.len() == 1 => {
2580 if let ast::PathStep::Name(n) = &p.steps[0] {
2581 (n.as_str(), sh.elements.as_slice())
2582 } else {
2583 return Ok(None);
2584 }
2585 }
2586 _ => return Ok(None),
2587 },
2588 _ => return Ok(None),
2589 };
2590
2591 let alias = self
2593 .schema
2594 .aliases
2595 .iter()
2596 .find(|a| a.name == path_name || format!("{}::{}", a.module, a.name) == path_name);
2597 let alias = match alias {
2598 Some(a) => a.clone(),
2599 None => return Ok(None),
2600 };
2601
2602 let inner_ast = crate::parse::parse(&alias.expr)?;
2603 let inner_sel = match inner_ast {
2604 Stmt::Select(sel) => sel,
2605 _ => return Err(self.type_err(&format!("alias '{}' expression must be a select statement", alias.name))),
2606 };
2607
2608 let inner_base = match &inner_sel.result {
2620 Expr::Shape(sh) => sh.expr.clone(),
2621 other => Some(other.clone()),
2622 };
2623 let merged_result = if shape_elements.is_empty() {
2624 inner_sel.result.clone()
2625 } else {
2626 Expr::Shape(Box::new(ast::ShapeExpr {
2627 expr: inner_base,
2628 elements: shape_elements.to_vec(),
2629 marker_offset: None,
2630 }))
2631 };
2632
2633 let merged_filter = match (&inner_sel.filter, &outer.filter) {
2634 (Some(a), Some(b)) => Some(Expr::BinOp(Box::new(ast::BinOp {
2635 left: a.clone(),
2636 op: ast::BinOpKind::And,
2637 right: b.clone(),
2638 }))),
2639 (Some(a), None) => Some(a.clone()),
2640 (None, b) => b.clone(),
2641 };
2642
2643 let merged = ast::SelectStmt {
2644 result: merged_result,
2645 filter: merged_filter,
2646 order_by: if outer.order_by.is_empty() {
2647 inner_sel.order_by.clone()
2648 } else {
2649 outer.order_by.clone()
2650 },
2651 offset: outer.offset.clone().or(inner_sel.offset.clone()),
2652 limit: outer.limit.clone().or(inner_sel.limit.clone()),
2653 lock: outer.lock.clone().or(inner_sel.lock.clone()),
2654 };
2655
2656 let _ = distinct; let ir = self.compile_stmt(&Stmt::Select(merged))?;
2658 Ok(Some(ir))
2659 }
2660
2661 fn compile_global(&mut self, raw_name: &str) -> Result<IrExpr, PyQLError> {
2662 let global = self
2663 .schema
2664 .globals
2665 .iter()
2666 .find(|g| g.name == raw_name || format!("{}::{}", g.module, g.name) == raw_name);
2667 let global = global
2668 .ok_or_else(|| {
2669 PyQLError::Resolution(PyQLResolutionError::UnknownField(PyQLUnknownFieldError {
2670 message: format!("unknown global: {:?}", raw_name),
2671 position: Position { line: 0, col: 0 },
2672 }))
2673 })?
2674 .clone();
2675
2676 let qualified = format!("{}::{}", global.module, global.name);
2677 let cte_name = format!("__global__{}", qualified);
2678
2679 if let Some(computed_expr) = global.computed_expr {
2680 if self.global_ctes.iter().any(|g| g.cte_name() == cte_name) {
2682 return Ok(IrExpr::GlobalRef { cte_name });
2683 }
2684 let inner_ast = crate::parse::parse(&computed_expr)?;
2686 let inner_stmt = self.compile_stmt(&inner_ast)?;
2687 self.global_ctes
2688 .push(IrGlobalCte::Computed(Box::new(IrComputedGlobalCte {
2689 cte_name: cte_name.clone(),
2690 qualified_name: qualified,
2691 stmt: inner_stmt,
2692 })));
2693 Ok(IrExpr::GlobalRef { cte_name })
2694 } else if self.in_fn_body {
2695 let pg_type = self.resolve_global_pg_type(&global.scalar_type);
2699 self.used_globals_arg = true;
2700 Ok(IrExpr::TypeCast(Box::new(super::IrTypeCast {
2705 expr: IrExpr::RawSql(globals_arg_read(&qualified, &pg_type)),
2706 pg_type,
2707 tuple_shape: None,
2708 })))
2709 } else {
2710 let pg_type = self.resolve_global_pg_type(&global.scalar_type);
2712 let param_name = format!("__global__{}", qualified);
2713 let index = self.param_index(¶m_name);
2714 if !self.global_ctes.iter().any(|g| g.cte_name() == cte_name) {
2716 self.global_ctes.push(IrGlobalCte::Session(IrSessionGlobalCte {
2717 cte_name,
2718 qualified_name: qualified,
2719 param_index: index,
2720 pg_type: pg_type.clone(),
2721 }));
2722 }
2723 Ok(IrExpr::GlobalParam { index, pg_type })
2724 }
2725 }
2726
2727 fn cte_object_type(&self, name: &str) -> Option<String> {
2732 self.cte_types.get(name).filter(|t| t.contains("::")).cloned()
2733 }
2734
2735 fn resolve_type(&self, name: &str) -> Result<&'a TypeDescriptor, PyQLError> {
2736 self.schema
2738 .types
2739 .iter()
2740 .find(|t| t.name == name || format!("{}::{}", t.module, t.name) == name)
2741 .ok_or_else(|| {
2742 PyQLError::Resolution(PyQLResolutionError::UnknownType(PyQLUnknownTypeError {
2743 message: format!("unknown type '{name}'"),
2744 position: Position { line: 0, col: 0 },
2745 }))
2746 })
2747 }
2748
2749 fn resolve_enum(&self, name: &str) -> Option<&'a crate::schema::EnumDescriptor> {
2754 self.schema
2755 .enums
2756 .iter()
2757 .find(|e| e.name == name || format!("{}::{}", e.module, e.name) == name)
2758 .or_else(|| crate::stdlib::lookup_enum(name))
2759 }
2760
2761 fn resolve_channel(&self, name: &str) -> Option<&'a crate::schema::ChannelDescriptor> {
2765 self.schema.find_channel(name)
2766 }
2767
2768 fn resolve_scalar(&self, name: &str) -> Option<&'a crate::schema::ScalarDescriptor> {
2774 self.schema
2775 .scalars
2776 .iter()
2777 .find(|s| s.name == name || format!("{}::{}", s.module, s.name) == name)
2778 }
2779
2780 fn resolve_named_tuple(&self, name: &str) -> Option<&'a crate::schema::NamedTupleDescriptor> {
2784 self.schema
2785 .named_tuples
2786 .iter()
2787 .find(|nt| nt.name == name || format!("{}::{}", nt.module, nt.name) == name)
2788 }
2789
2790 fn tuple_member_to_json_member(&self, m: &crate::schema::TupleMemberDescriptor) -> crate::query::JsonMember {
2794 use crate::schema::TupleMemberKind;
2795 let kind = match &m.kind {
2796 TupleMemberKind::Scalar { .. } => crate::query::JsonMemberKind::Scalar,
2797 TupleMemberKind::Enum { module, name } => crate::query::JsonMemberKind::Enum {
2798 enum_type: format!("{}::{}", module, name),
2799 },
2800 TupleMemberKind::NamedTuple { module, name } => {
2801 let qname = format!("{}::{}", module, name);
2802 let nested_members = self
2803 .resolve_named_tuple(&qname)
2804 .map(|nt| {
2805 nt.members
2806 .iter()
2807 .map(|mm| self.tuple_member_to_json_member(mm))
2808 .collect()
2809 })
2810 .unwrap_or_default();
2811 crate::query::JsonMemberKind::Tuple {
2812 type_name: Some(qname),
2813 members: nested_members,
2814 }
2815 }
2816 TupleMemberKind::Tuple { members } => crate::query::JsonMemberKind::Tuple {
2817 type_name: None,
2818 members: members.iter().map(|mm| self.tuple_member_to_json_member(mm)).collect(),
2819 },
2820 };
2821 crate::query::JsonMember {
2822 key: m.name.clone(),
2823 kind,
2824 }
2825 }
2826
2827 fn ast_tuple_element_to_json_member(&self, elem: &ast::TupleTypeElement) -> crate::query::JsonMember {
2830 crate::query::JsonMember {
2831 key: elem.name.clone(),
2832 kind: self.ast_type_expr_to_json_member_kind(&elem.ty),
2833 }
2834 }
2835
2836 fn ast_type_expr_to_json_member_kind(&self, ty: &ast::TypeExpr) -> crate::query::JsonMemberKind {
2837 if let ast::TypeExpr::Tuple { elements } = ty {
2838 return crate::query::JsonMemberKind::Tuple {
2839 type_name: None,
2840 members: elements
2841 .iter()
2842 .map(|e| self.ast_tuple_element_to_json_member(e))
2843 .collect(),
2844 };
2845 }
2846 let Some((module, name)) = ty.as_named() else {
2847 return crate::query::JsonMemberKind::Scalar;
2848 };
2849 let qname = match module {
2850 Some(m) => format!("{}::{}", m, name),
2851 None => name.to_string(),
2852 };
2853 if let Some(ed) = self.resolve_enum(&qname) {
2854 return crate::query::JsonMemberKind::Enum {
2855 enum_type: format!("{}::{}", ed.module, ed.name),
2856 };
2857 }
2858 if let Some(nt) = self.resolve_named_tuple(&qname) {
2859 return crate::query::JsonMemberKind::Tuple {
2860 type_name: Some(format!("{}::{}", nt.module, nt.name)),
2861 members: nt.members.iter().map(|m| self.tuple_member_to_json_member(m)).collect(),
2862 };
2863 }
2864 crate::query::JsonMemberKind::Scalar
2865 }
2866
2867 fn resolve_tuple_cast_shape(&self, ty: &ast::TypeExpr) -> Option<TupleCastShape> {
2873 match ty {
2874 ast::TypeExpr::Tuple { elements } => Some(TupleCastShape {
2875 type_name: None,
2876 members: elements
2877 .iter()
2878 .map(|e| self.ast_tuple_element_to_json_member(e))
2879 .collect(),
2880 }),
2881 _ => {
2882 let (module, name) = ty.as_named()?;
2883 let qname = match module {
2884 Some(m) => format!("{}::{}", m, name),
2885 None => name.to_string(),
2886 };
2887 let nt = self.resolve_named_tuple(&qname)?;
2888 Some(TupleCastShape {
2889 type_name: Some(format!("{}::{}", nt.module, nt.name)),
2890 members: nt.members.iter().map(|m| self.tuple_member_to_json_member(m)).collect(),
2891 })
2892 }
2893 }
2894 }
2895
2896 fn type_expr_to_display_str(&self, ty: &ast::TypeExpr) -> String {
2900 match ty {
2901 ast::TypeExpr::Tuple { elements } => {
2902 let inner = elements
2903 .iter()
2904 .map(|e| match &e.name {
2905 Some(n) => format!("{}: {}", n, self.type_expr_to_display_str(&e.ty)),
2906 None => self.type_expr_to_display_str(&e.ty),
2907 })
2908 .collect::<Vec<_>>()
2909 .join(", ");
2910 format!("tuple<{}>", inner)
2911 }
2912 ast::TypeExpr::Array { element } => format!("array<{}>", self.type_expr_to_display_str(element)),
2913 ast::TypeExpr::Named { module, name } => match module {
2914 Some(m) => format!("{}::{}", m, name),
2915 None => type_expr_to_pg(ty)
2916 .map(|pg| pg_type_to_pyql(&pg).to_string())
2917 .unwrap_or_else(|_| name.clone()),
2918 },
2919 }
2920 }
2921
2922 fn resolve_property_tuple_shape(&self, prop: &PropertyDescriptor) -> Option<TupleCastShape> {
2927 if let Some(qname) = prop.pg_type.strip_prefix("__nt__:") {
2928 let nt = self.resolve_named_tuple(qname)?;
2929 return Some(TupleCastShape {
2930 type_name: Some(qname.to_string()),
2931 members: nt.members.iter().map(|m| self.tuple_member_to_json_member(m)).collect(),
2932 });
2933 }
2934 prop.tuple_members.as_ref().map(|members| TupleCastShape {
2935 type_name: None,
2936 members: members.iter().map(|m| self.tuple_member_to_json_member(m)).collect(),
2937 })
2938 }
2939
2940 fn scalar_cast_free_select(
2947 &mut self,
2948 tc: &ast::TypeCast,
2949 pg_type: String,
2950 distinct: bool,
2951 ) -> Result<IrStmt, PyQLError> {
2952 let cast_expr = match &tc.ty {
2953 ast::TypeExpr::Tuple { elements } => {
2954 let tuple_shape = self.resolve_tuple_cast_shape(&tc.ty);
2955 match self.try_compile_tuple_literal_cast_ctx(elements, &tc.expr, None)? {
2956 Some(ir) => IrExpr::TypeCast(Box::new(IrTypeCast {
2961 expr: ir,
2962 pg_type,
2963 tuple_shape,
2964 })),
2965 None => {
2966 let inner = self.compile_expr_ctx(&tc.expr, None)?;
2967 IrExpr::TypeCast(Box::new(IrTypeCast {
2968 expr: inner,
2969 pg_type,
2970 tuple_shape,
2971 }))
2972 }
2973 }
2974 }
2975 _ => {
2976 let inner = self.compile_expr_ctx(&tc.expr, None)?;
2977 let tuple_shape = self.resolve_tuple_cast_shape(&tc.ty);
2978 IrExpr::TypeCast(Box::new(IrTypeCast {
2979 expr: inner,
2980 pg_type,
2981 tuple_shape,
2982 }))
2983 }
2984 };
2985 Ok(IrStmt::Select(IrSelect {
2986 rows: vec![IrRowSource::Free(IrFreeExpr::Scalar(cast_expr))],
2987 filter: None,
2988 order_by: vec![],
2989 offset: None,
2990 limit: None,
2991 distinct,
2992 dml_source: None,
2993 polymorphic: false,
2994 poly_implementors: vec![],
2995 poly_columns: vec![],
2996 lock: None,
2997 }))
2998 }
2999
3000 fn resolve_cast_pg_type(&self, ty: &ast::TypeExpr) -> Result<String, PyQLError> {
3009 if matches!(ty, ast::TypeExpr::Tuple { .. }) {
3010 return Ok("jsonb".to_string());
3011 }
3012 if let ast::TypeExpr::Array { element } = ty {
3013 let element_pg = self.resolve_cast_pg_type(element)?;
3014 return Ok(format!("{}[]", element_pg));
3015 }
3016 let (module, name) = ty.as_named().expect("checked above: not Tuple/Array");
3017 let qname = match module {
3018 Some(m) => format!("{}::{}", m, name),
3019 None => name.to_string(),
3020 };
3021 if let Some(ed) = self.resolve_enum(&qname) {
3022 return Ok(format!("{}.\"{}\"", crate::sql::pg_schema_str(&ed.module), ed.name));
3023 }
3024 if self.resolve_named_tuple(&qname).is_some() {
3025 return Ok("jsonb".to_string());
3026 }
3027 if let Some(sd) = self.resolve_scalar(&qname) {
3033 return Ok(format!("{}.\"{}\"", crate::sql::pg_schema_str(&sd.module), sd.name));
3034 }
3035 type_expr_to_pg(ty)
3036 }
3037
3038 fn compile_tuple_element_cast_ctx(
3044 &mut self,
3045 target_ty: &ast::TypeExpr,
3046 value: &Expr,
3047 ctx: Option<(&TypeDescriptor, &str)>,
3048 ) -> Result<IrExpr, PyQLError> {
3049 if let ast::TypeExpr::Tuple { elements } = target_ty
3050 && let Some(ir) = self.try_compile_tuple_literal_cast_ctx(elements, value, ctx)?
3051 {
3052 return Ok(ir);
3053 }
3054 let inner = self.compile_expr_ctx(value, ctx)?;
3055 let pg_type = self.resolve_cast_pg_type(target_ty)?;
3056 Ok(IrExpr::TypeCast(Box::new(IrTypeCast {
3057 expr: inner,
3058 pg_type,
3059 tuple_shape: None,
3060 })))
3061 }
3062
3063 fn try_compile_tuple_literal_cast_ctx(
3072 &mut self,
3073 target_elements: &[ast::TupleTypeElement],
3074 source: &Expr,
3075 ctx: Option<(&TypeDescriptor, &str)>,
3076 ) -> Result<Option<IrExpr>, PyQLError> {
3077 let source_values: Vec<&Expr> = match source {
3078 Expr::Tuple(vals) if vals.len() == target_elements.len() => vals.iter().collect(),
3079 Expr::NamedTuple(fields) if fields.len() == target_elements.len() => {
3080 fields.iter().map(|(_, v)| v).collect()
3081 }
3082 _ => return Ok(None),
3083 };
3084 let named = target_elements.iter().all(|e| e.name.is_some());
3085 let mut casted = Vec::with_capacity(target_elements.len());
3086 for (elem, value) in target_elements.iter().zip(source_values) {
3087 casted.push(self.compile_tuple_element_cast_ctx(&elem.ty, value, ctx)?);
3088 }
3089 if named {
3090 let fields = target_elements
3091 .iter()
3092 .zip(casted)
3093 .map(|(e, v)| (e.name.clone().unwrap(), v))
3094 .collect();
3095 Ok(Some(IrExpr::NamedTuple {
3096 fields,
3097 is_free_object: false,
3098 }))
3099 } else {
3100 Ok(Some(IrExpr::Tuple(casted)))
3101 }
3102 }
3103
3104 fn try_compile_array_literal_cast_ctx(
3116 &mut self,
3117 element_ty: &ast::TypeExpr,
3118 source: &Expr,
3119 ctx: Option<(&TypeDescriptor, &str)>,
3120 ) -> Result<Option<IrExpr>, PyQLError> {
3121 let Expr::Array(elems) = source else { return Ok(None) };
3122 let casted = elems
3123 .iter()
3124 .map(|e| self.compile_tuple_element_cast_ctx(element_ty, e, ctx))
3125 .collect::<Result<Vec<_>, _>>()?;
3126 Ok(Some(IrExpr::Array(casted)))
3127 }
3128
3129 fn compile_enum_access(&self, type_ref: &str, variant: &str) -> Result<IrExpr, PyQLError> {
3130 let ed = self
3131 .resolve_enum(type_ref)
3132 .ok_or_else(|| self.type_err(&format!("unknown type '{}'", type_ref)))?;
3133 if !ed.members.iter().any(|m| m == variant) {
3134 return Err(self.type_err(&format!(
3135 "enum '{}::{}' has no member '{}'",
3136 ed.module, ed.name, variant
3137 )));
3138 }
3139 let pg_type = if crate::stdlib::lookup_enum(&format!("{}::{}", ed.module, ed.name)).is_some() {
3142 "text".to_string()
3143 } else {
3144 format!("{}.\"{}\"", crate::sql::pg_schema_str(&ed.module), ed.name)
3145 };
3146 Ok(IrExpr::EnumLiteral {
3147 pg_type,
3148 variant: variant.to_string(),
3149 })
3150 }
3151
3152 fn row_source_table(&self, name: &str, td: &TypeDescriptor) -> String {
3155 if let Some(cte) = self.for_var_ctes.get(name) {
3156 return format!("@cte:{cte}");
3157 }
3158 if self.cte_object_type(name).is_some() {
3159 return format!("@cte:{}", self.cte_sql_name(name));
3160 }
3161 td.table.clone()
3162 }
3163
3164 fn resolve_path_root(&self, root_name: &str) -> Result<&'a TypeDescriptor, PyQLError> {
3172 if let Some(qualified) = self.for_var_types.get(root_name) {
3173 return self.resolve_type(qualified);
3174 }
3175 match self.cte_types.get(root_name).filter(|t| t.contains("::")).cloned() {
3176 Some(bound) => self.resolve_type(&bound),
3177 None => self.resolve_type(root_name),
3178 }
3179 }
3180
3181 fn find_poly_implementors(&self, iface_qname: &str) -> Vec<IrPolyImplementor> {
3182 self.schema
3183 .types
3184 .iter()
3185 .filter(|t| !t.abstract_ && Self::is_or_implements(t, iface_qname))
3186 .map(|t| IrPolyImplementor {
3187 type_name: format!("{}::{}", t.module, t.name),
3188 table: t.table.clone(),
3189 module: t.module.clone(),
3190 })
3191 .collect()
3192 }
3193
3194 fn resolve_property<'t>(td: &'t TypeDescriptor, name: &str) -> Option<&'t PropertyDescriptor> {
3195 td.properties.iter().find(|p| p.name == name)
3196 }
3197
3198 fn resolve_link<'t>(td: &'t TypeDescriptor, name: &str) -> Option<&'t LinkDescriptor> {
3199 td.links.iter().find(|l| l.name == name)
3200 }
3201
3202 fn resolve_multilink<'t>(td: &'t TypeDescriptor, name: &str) -> Option<&'t MultiLinkDescriptor> {
3203 td.multilinks.iter().find(|m| m.name == name)
3204 }
3205
3206 fn compile_link_prop_ref(&mut self, prop_name: &str) -> Result<IrExpr, PyQLError> {
3210 let Some(scope) = self.link_prop_scope.last().cloned() else {
3211 return Err(self.type_err(&format!(
3212 "'@{prop_name}' is a link property, so it is only valid in the modifiers or shape of the \
3213 link it belongs to, e.g. 'locators: {{ … }} filter @{prop_name}'"
3214 )));
3215 };
3216 let Some((through_qname, junction_alias)) = scope else {
3217 return Err(self.type_err(&format!(
3218 "this link has no link properties, so '@{prop_name}' cannot be read — \
3219 declare the link with a `Through[...]` type to give it some"
3220 )));
3221 };
3222 let through_td = self.resolve_type(&through_qname)?;
3223 let Some(prop) = Self::resolve_property(through_td, prop_name) else {
3224 return Err(self.field_err(prop_name, &through_qname));
3225 };
3226 Ok(IrExpr::ColumnRef {
3227 alias: junction_alias,
3228 column: prop.name.clone(),
3229 pg_type: prop.pg_type.clone(),
3230 })
3231 }
3232
3233 fn link_target_reaches(&self, target: &str, current_qname: &str) -> bool {
3239 if target == current_qname {
3240 return true;
3241 }
3242 self.resolve_type(current_qname)
3243 .is_ok_and(|td| td.interfaces.iter().any(|i| i == target))
3244 }
3245
3246 fn resolve_computed(&self, td: &TypeDescriptor, name: &str) -> Option<crate::schema::ComputedDescriptor> {
3252 if let Some(cd) = td.computed.iter().find(|c| c.name == name) {
3253 return Some(cd.clone());
3254 }
3255 td.interfaces
3256 .iter()
3257 .filter_map(|iface| self.resolve_type(iface).ok())
3258 .find_map(|itd| itd.computed.iter().find(|c| c.name == name))
3259 .cloned()
3260 }
3261
3262 fn compile_stmt(&mut self, stmt: &Stmt) -> Result<IrStmt, PyQLError> {
3265 match stmt {
3266 Stmt::Select(s) => {
3267 let (distinct, result) = match &s.result {
3268 Expr::UnaryOp(u) if u.op == ast::UnaryOpKind::Distinct => (true, &u.operand),
3269 Expr::Detached(inner) => {
3278 self.pending_detached = true;
3279 (false, inner.as_ref())
3280 }
3281 other => (false, other),
3282 };
3283
3284 if let Expr::Shape(sh) = result
3288 && let Some(Expr::FunctionCall(assert)) = &sh.expr
3289 && assert.module.as_deref().is_none_or(|m| m == "std")
3290 && matches!(assert.name.as_str(), "assert_exists" | "assert_distinct")
3291 && let [Expr::FunctionCall(inner)] = assert.args.as_slice()
3292 && let Some(mut fs) = self.try_compile_fn_object_select(inner, &sh.elements, s, distinct)?
3293 {
3294 let message = self.assert_message(assert, None)?;
3295 let unchecked = ast::SelectStmt {
3296 result: Expr::FunctionCall(inner.clone()),
3297 filter: None,
3298 order_by: vec![],
3299 offset: None,
3300 limit: None,
3301 lock: None,
3302 };
3303 let every_row = self
3304 .try_compile_fn_object_select(inner, &[], &unchecked, false)?
3305 .ok_or_else(|| self.type_err("assert subject is not an object-returning function"))?;
3306 let checked = IrExpr::FunctionCall(IrFunctionCall {
3307 return_pg_type: None,
3308 schema: Some("_pylon".to_string()),
3309 name: assert.name.clone(),
3310 args: std::iter::once(IrExpr::ArrayFromSelect(Box::new(IrArraySource::StmtColumn {
3311 stmt: Box::new(IrStmt::FunctionSelect(every_row)),
3312 column: "id".to_string(),
3313 })))
3314 .chain(message)
3315 .collect(),
3316 sql_template: None,
3317 });
3318 let check = IrExpr::BinOp(Box::new(IrBinOp {
3319 left: IrExpr::FunctionCall(IrFunctionCall {
3320 return_pg_type: None,
3321 schema: None,
3322 name: "cardinality".to_string(),
3323 args: vec![checked],
3324 sql_template: None,
3325 }),
3326 op: ast::BinOpKind::Ge,
3327 right: IrExpr::Literal(IrLiteral::Int(0)),
3328 }));
3329 fs.filter = and_conditions(fs.filter, vec![check]);
3330 return Ok(IrStmt::FunctionSelect(fs));
3331 }
3332 if let Some(flattened) = flatten_shape_subject(result) {
3333 let mut flat = s.clone();
3334 flat.result = match distinct {
3335 true => Expr::UnaryOp(Box::new(ast::UnaryOp {
3336 op: ast::UnaryOpKind::Distinct,
3337 operand: flattened,
3338 })),
3339 false => flattened,
3340 };
3341 return self.compile_stmt(&Stmt::Select(flat));
3342 }
3343 if let Expr::Shape(sh) = result
3344 && let Some(Expr::SubQuery(inner)) = &sh.expr
3345 && let Stmt::Group(g) = inner.as_ref()
3346 {
3347 return self.compile_group_projection(s, g, &sh.elements).map(IrStmt::Group);
3348 }
3349
3350 if let Some(ir) = self.try_compile_alias_select(s, result, distinct)? {
3352 return Ok(ir);
3353 }
3354
3355 if let Some(ir) = self.try_compile_global_select(s, result, distinct)? {
3357 return Ok(ir);
3358 }
3359
3360 if let Some(ir) = self.try_compile_field_access_select(s, result)? {
3363 return Ok(ir);
3364 }
3365
3366 if let Expr::Shape(sh) = result
3368 && let Some(Expr::FunctionCall(fc)) = sh.expr.as_ref()
3369 && let Some(ir) = self.try_compile_fn_object_select(fc, &sh.elements, s, distinct)?
3370 {
3371 return Ok(IrStmt::FunctionSelect(ir));
3372 }
3373
3374 if let Expr::FunctionCall(fc) = result
3376 && let Some(ir) = self.try_compile_fn_object_select(fc, &[], s, distinct)?
3377 {
3378 return Ok(IrStmt::FunctionSelect(ir));
3379 }
3380
3381 if let Expr::Shape(sh) = result
3383 && let Some(Expr::FunctionCall(fc)) = sh.expr.as_ref()
3384 {
3385 if let Some(ir) = self.try_compile_vector_search(fc, &sh.elements, s)? {
3386 return Ok(IrStmt::VectorSearch(ir));
3387 }
3388 if let Some(ir) = self.try_compile_fts_search(fc, &sh.elements, s)? {
3389 return Ok(IrStmt::FtsSearch(ir));
3390 }
3391 }
3392 if let Expr::FunctionCall(fc) = result {
3394 if let Some(ir) = self.try_compile_vector_search(fc, &[], s)? {
3395 return Ok(IrStmt::VectorSearch(ir));
3396 }
3397 if let Some(ir) = self.try_compile_fts_search(fc, &[], s)? {
3398 return Ok(IrStmt::FtsSearch(ir));
3399 }
3400 }
3401
3402 if let Expr::Shape(sh) = result
3407 && let Some(Expr::TypeCast(tc)) = sh.expr.as_ref()
3408 && let Some((module, name)) = tc.ty.as_named()
3409 && module
3410 .map(|m| !["std", "cal", "math", "sys", "pgvector", "crypto", "postgis"].contains(&m))
3411 .unwrap_or(false)
3412 {
3413 let id_filter = Expr::BinOp(Box::new(ast::BinOp {
3414 left: Expr::Path(ast::Path::relative("id")),
3415 op: ast::BinOpKind::Eq,
3416 right: tc.expr.clone(),
3417 }));
3418 let merged_filter = match &s.filter {
3419 None => Some(id_filter),
3420 Some(existing) => Some(Expr::BinOp(Box::new(ast::BinOp {
3421 left: id_filter,
3422 op: ast::BinOpKind::And,
3423 right: existing.clone(),
3424 }))),
3425 };
3426 let qname = match module {
3429 Some(m) => format!("{}::{}", m, name),
3430 None => name.to_string(),
3431 };
3432 let synthetic = ast::SelectStmt {
3433 result: Expr::Shape(Box::new(ast::ShapeExpr {
3434 expr: Some(Expr::Path(ast::Path::absolute(&qname))),
3435 elements: sh.elements.clone(),
3436 marker_offset: None,
3437 })),
3438 filter: merged_filter,
3439 order_by: s.order_by.clone(),
3440 offset: s.offset.clone(),
3441 limit: s.limit.clone(),
3442 lock: s.lock.clone(),
3443 };
3444 return self
3445 .compile_select(&synthetic, &synthetic.result, distinct)
3446 .map(IrStmt::Select);
3447 }
3448 const STDLIB_MODULES: &[&str] = &["std", "cal", "math", "sys", "pgvector", "crypto", "postgis"];
3453 if let Expr::TypeCast(tc) = result {
3454 if matches!(&tc.ty, ast::TypeExpr::Tuple { .. }) {
3458 return self.scalar_cast_free_select(tc, "jsonb".to_string(), distinct);
3459 }
3460 if let Some((module, name)) = tc.ty.as_named() {
3461 let qname = match module {
3462 Some(m) => format!("{}::{}", m, name),
3463 None => name.to_string(),
3464 };
3465 if let Some(ed) = self.resolve_enum(&qname) {
3466 let pg_type = format!("{}.\"{}\"", crate::sql::pg_schema_str(&ed.module), ed.name);
3467 return self.scalar_cast_free_select(tc, pg_type, distinct);
3468 }
3469 if self.resolve_named_tuple(&qname).is_some() {
3470 return self.scalar_cast_free_select(tc, "jsonb".to_string(), distinct);
3471 }
3472 if let Some(sd) = self.resolve_scalar(&qname) {
3473 let pg_type = format!("{}.\"{}\"", crate::sql::pg_schema_str(&sd.module), sd.name);
3474 return self.scalar_cast_free_select(tc, pg_type, distinct);
3475 }
3476 if module.map(|m| !STDLIB_MODULES.contains(&m)).unwrap_or(false) {
3477 return self.compile_schema_cast_select(s, tc).map(IrStmt::Select);
3478 }
3479 }
3480 }
3481 if let Expr::Path(p) = result {
3483 if !p.partial
3484 && p.steps.len() == 2
3485 && let [ast::PathStep::Name(type_ref), ast::PathStep::Name(variant)] = p.steps.as_slice()
3486 && self.resolve_enum(type_ref).is_some()
3487 {
3488 let expr = self.compile_enum_access(type_ref, variant)?;
3489 return Ok(IrStmt::Select(IrSelect {
3490 rows: vec![IrRowSource::Free(IrFreeExpr::Scalar(expr))],
3491 filter: None,
3492 order_by: vec![],
3493 offset: None,
3494 limit: None,
3495 distinct,
3496 dml_source: None,
3497 polymorphic: false,
3498 poly_implementors: vec![],
3499 poly_columns: vec![],
3500 lock: None,
3501 }));
3502 }
3503 if let Some(resolved) = self.resolve_cte_path(p) {
3509 let expr = resolved?;
3510 return Ok(IrStmt::Select(IrSelect {
3511 rows: vec![IrRowSource::Free(IrFreeExpr::Scalar(expr))],
3512 filter: None,
3513 order_by: vec![],
3514 offset: None,
3515 limit: None,
3516 distinct,
3517 dml_source: None,
3518 polymorphic: false,
3519 poly_implementors: vec![],
3520 poly_columns: vec![],
3521 lock: None,
3522 }));
3523 }
3524 if !p.partial && p.steps.len() > 1 {
3525 return self.compile_path_select(s, p, &[], distinct).map(IrStmt::PathSelect);
3526 }
3527 }
3528 if let Expr::Shape(sh) = result
3529 && let Some(Expr::Path(p)) = sh.expr.as_ref()
3530 && !p.partial
3531 && p.steps.len() > 1
3532 {
3533 return self
3534 .compile_path_select(s, p, &sh.elements, distinct)
3535 .map(IrStmt::PathSelect);
3536 }
3537 if let Expr::FunctionCall(f) = result
3539 && (f.module.is_none() || f.module.as_deref() == Some("std"))
3540 && matches!(f.name.as_str(), "assert_exists" | "assert_distinct")
3541 && !f.args.is_empty()
3542 && let Expr::SubQuery(inner_stmt) = &f.args[0]
3543 {
3544 let offset = s.offset.as_ref().map(|e| self.compile_free_expr(e)).transpose()?;
3545 let limit = s.limit.as_ref().map(|e| self.compile_free_expr(e)).transpose()?;
3546 let message = self.assert_message(f, None)?;
3547 if let Ok(type_name) = self.dml_subject_type(inner_stmt)
3551 && let Ok(td) = self.resolve_type(&type_name)
3552 {
3553 let td_module = td.module.clone();
3554 let td_name = td.name.clone();
3555 let td_table = td.table.clone();
3556 let pk = td
3557 .properties
3558 .iter()
3559 .find(|p| p.is_pk)
3560 .map(|p| (p.name.clone(), p.pg_type.clone()))
3561 .unwrap_or_else(|| ("id".to_string(), "uuid".to_string()));
3562 let inner_ir = self.compile_stmt(inner_stmt)?;
3563 let alias = self.fresh_alias();
3564 let vetted = IrExpr::FunctionCall(IrFunctionCall {
3565 return_pg_type: None,
3566 schema: Some("_pylon".to_string()),
3567 name: f.name.clone(),
3568 args: std::iter::once(IrExpr::ArrayFromSelect(Box::new(IrArraySource::StmtColumn {
3569 stmt: Box::new(inner_ir),
3570 column: pk.0.clone(),
3571 })))
3572 .chain(message)
3573 .collect(),
3574 sql_template: None,
3575 });
3576 let filter = IrExpr::BinOp(Box::new(IrBinOp {
3577 left: IrExpr::ColumnRef {
3578 alias: alias.clone(),
3579 column: pk.0.clone(),
3580 pg_type: pk.1.clone(),
3581 },
3582 op: ast::BinOpKind::In,
3583 right: vetted,
3584 }));
3585 let shape = vec![IrShapePointer::Scalar(IrScalarPointer {
3586 implicit_id: false,
3587 marker_offset: None,
3588 alias: "id".to_string(),
3589 column: pk.0,
3590 pg_type: pk.1,
3591 tuple_shape: None,
3592 })];
3593 let source = IrSource {
3594 poly: None,
3595 type_name: format!("{td_module}::{td_name}"),
3596 table: td_table,
3597 alias,
3598 };
3599 let mut select = IrSelect::schema_bound(source, shape, Some(filter));
3600 select.offset = offset;
3601 select.limit = limit;
3602 select.distinct = distinct;
3603 return Ok(IrStmt::Select(select));
3604 }
3605 let inner = self.compile_subquery_to_array_source(inner_stmt)?;
3606 return Ok(IrStmt::Select(IrSelect {
3607 rows: vec![IrRowSource::Free(IrFreeExpr::AssertSet {
3608 fn_name: f.name.clone(),
3609 inner: Box::new(inner),
3610 message,
3611 })],
3612 filter: None,
3613 order_by: vec![],
3614 offset,
3615 limit,
3616 distinct,
3617 dml_source: None,
3618 polymorphic: false,
3619 poly_implementors: vec![],
3620 poly_columns: vec![],
3621 lock: None,
3622 }));
3623 }
3624 if let Some(expr) = self.aggregate_over_unpacked(s, result, distinct)? {
3628 return Ok(IrStmt::Select(IrSelect {
3629 rows: vec![IrRowSource::Free(IrFreeExpr::Scalar(expr))],
3630 filter: None,
3631 order_by: vec![],
3632 offset: None,
3633 limit: None,
3634 distinct: false,
3635 dml_source: None,
3636 polymorphic: false,
3637 poly_implementors: vec![],
3638 poly_columns: vec![],
3639 lock: None,
3640 }));
3641 }
3642 if let Some(root) = self.find_path_root_in_expr(result) {
3644 return self
3645 .compile_expr_as_path_select(s, result, &root, distinct)
3646 .map(IrStmt::PathSelect);
3647 }
3648 if let Expr::TypeIs { expr, ty } = result
3650 && let Expr::Path(p) = expr.as_ref()
3651 && !p.partial
3652 && p.steps.len() == 1
3653 && let ast::PathStep::Name(src_name) = &p.steps[0]
3654 && self.resolve_name_ref(src_name, true).is_none()
3655 {
3656 let src_td = self.resolve_type(src_name)?;
3657 {
3658 let src_qname = format!("{}::{}", src_td.module, src_td.name);
3659 let (ty_module, ty_name) = ty
3660 .as_named()
3661 .ok_or_else(|| self.type_err("cannot use IS with a tuple or array type"))?;
3662 let check_module = ty_module.unwrap_or(&src_td.module);
3663 let check_name = format!("{}::{}", check_module, ty_name);
3664 self.resolve_type(&check_name)?;
3665 let check_qname = check_name;
3666 let src_table = src_td.table.clone();
3667 let src_alias = self.fresh_alias();
3668 let src_td = self.resolve_type(&src_qname)?;
3669 let poly_implementors = if self.is_polymorphic(src_td) {
3670 self.find_poly_implementors(&src_qname)
3671 } else {
3672 vec![]
3673 };
3674 let bool_expr = self.type_check_bool_expr(&src_qname, &check_qname, src_td, &src_alias);
3675 let ps = IrPathSelect {
3676 root: IrSource {
3677 poly: None,
3678 type_name: src_qname,
3679 table: src_table,
3680 alias: src_alias,
3681 },
3682 joins: vec![],
3683 result: IrPathResult::Scalar(bool_expr, None),
3684 filter: s
3685 .filter
3686 .as_ref()
3687 .map(|_| Err(self.type_err("FILTER is not supported on type-is SELECT")))
3688 .transpose()?,
3689 order_by: vec![],
3690 offset: None,
3691 limit: None,
3692 distinct,
3693 poly_implementors,
3694 };
3695 return Ok(IrStmt::PathSelect(ps));
3696 }
3697 }
3698 if let Expr::Union(a, b) = result
3699 && !distinct
3700 && s.filter.is_none()
3701 && s.order_by.is_empty()
3702 && s.offset.is_none()
3703 && s.limit.is_none()
3704 && (self.is_scalar_walk(a) || self.is_scalar_walk(b))
3705 {
3706 return self.compile_scalar_union(result);
3707 }
3708 if !distinct
3710 && s.filter.is_none()
3711 && s.order_by.is_empty()
3712 && s.offset.is_none()
3713 && s.limit.is_none()
3714 && let Some(ps) = self.compile_shape_field_select(result, None)?
3715 {
3716 return Ok(IrStmt::PathSelect(ps));
3717 }
3718 if let Expr::Shape(sh) = result
3725 && let Some(subject) = sh.expr.as_ref()
3726 && !matches!(subject, Expr::Path(_))
3727 {
3728 let (base, fields) = Self::peel_field_access_chain(subject);
3729 if let Expr::SubQuery(inner_stmt) = base
3730 && !fields.is_empty()
3731 && matches!(inner_stmt.as_ref(), Stmt::Insert(_) | Stmt::Update(_) | Stmt::Delete(_))
3732 {
3733 let inner = self.compile_stmt(inner_stmt)?;
3734 let cte_name = self.fresh_nested_cte_name();
3735 let overrides = Self::junction_overrides_for(&inner, &cte_name);
3738 let type_name = self.register_cte(&cte_name, &inner);
3739 self.hoisted_ctes.push(IrCteDef {
3740 name: cte_name.clone(),
3741 stmt: inner,
3742 type_name,
3743 correlated_to: None,
3744 });
3745 let mut steps = vec![ast::PathStep::Name(cte_name)];
3746 steps.extend(fields.into_iter().map(ast::PathStep::Name));
3747 let rerooted = ast::SelectStmt {
3748 result: Expr::Shape(Box::new(ast::ShapeExpr {
3749 expr: Some(Expr::Path(ast::Path { steps, partial: false })),
3750 elements: sh.elements.clone(),
3751 marker_offset: None,
3752 })),
3753 filter: s.filter.clone(),
3754 order_by: s.order_by.clone(),
3755 offset: s.offset.clone(),
3756 limit: s.limit.clone(),
3757 lock: s.lock.clone(),
3758 };
3759 let previous = std::mem::replace(&mut self.junction_read_overrides, overrides);
3760 let compiled = self.compile_stmt(&Stmt::Select(rerooted));
3761 self.junction_read_overrides = previous;
3762 return compiled;
3763 }
3764 }
3765 if let Expr::Shape(sh) = result
3771 && let Some(Expr::SubQuery(inner_stmt)) = sh.expr.as_ref()
3772 && matches!(inner_stmt.as_ref(), Stmt::Select(_))
3773 && self.dml_subject_type(inner_stmt).is_err()
3774 {
3775 let inner = self.compile_stmt(inner_stmt)?;
3776 let cte_name = self.fresh_nested_cte_name();
3777 let type_name = self.register_cte(&cte_name, &inner);
3778 self.hoisted_ctes.push(IrCteDef {
3779 name: cte_name.clone(),
3780 stmt: inner,
3781 type_name,
3782 correlated_to: None,
3783 });
3784 let rerooted = ast::SelectStmt {
3785 result: Expr::Shape(Box::new(ast::ShapeExpr {
3786 expr: Some(Expr::Path(ast::Path {
3787 steps: vec![ast::PathStep::Name(cte_name)],
3788 partial: false,
3789 })),
3790 elements: sh.elements.clone(),
3791 marker_offset: None,
3792 })),
3793 filter: s.filter.clone(),
3794 order_by: s.order_by.clone(),
3795 offset: s.offset.clone(),
3796 limit: s.limit.clone(),
3797 lock: s.lock.clone(),
3798 };
3799 return self.compile_stmt(&Stmt::Select(rerooted));
3800 }
3801 if let Expr::SubQuery(inner) = result
3806 && matches!(inner.as_ref(), Stmt::For(_))
3807 && !distinct
3808 && s.filter.is_none()
3809 && s.order_by.is_empty()
3810 && s.offset.is_none()
3811 && s.limit.is_none()
3812 {
3813 return self.compile_stmt(inner);
3814 }
3815 self.check_union_type_compat(result)?;
3817 if self.is_free_result(result) {
3818 self.compile_free_select(s, result, distinct).map(IrStmt::Select)
3819 } else {
3820 self.compile_select(s, result, distinct).map(IrStmt::Select)
3821 }
3822 }
3823 Stmt::Insert(s) => self.compile_insert(s).map(IrStmt::Insert),
3824 Stmt::Update(s) => self.compile_update(s).map(IrStmt::Update),
3825 Stmt::Delete(s) => self.compile_delete(s).map(IrStmt::Delete),
3826 Stmt::Group(s) => self.compile_group(s).map(IrStmt::Group),
3827 Stmt::With(w) => {
3831 for alias in &w.aliases {
3832 if self.bind_inline_if_correlated(&alias.name, &alias.expr, None)?
3833 || self.bind_group(&alias.name, &alias.expr)?
3834 {
3835 continue;
3836 }
3837 let (ir_inner, correlated_to) = self.compile_binding_in_scope(&alias.expr)?;
3838 let sql_name = self.claim_cte_sql_name(&alias.name);
3839 let type_name = self.register_cte(&alias.name, &ir_inner);
3840 self.hoisted_binding_sources
3841 .insert(sql_name.clone(), alias.expr.clone());
3842 self.hoisted_ctes.push(IrCteDef {
3843 name: sql_name,
3844 stmt: ir_inner,
3845 type_name,
3846 correlated_to,
3847 });
3848 }
3849 self.compile_stmt(&w.stmt)
3850 }
3851 Stmt::For(f) => match self.compile_group_elements(f)? {
3852 Some(grp) => Ok(IrStmt::Group(grp)),
3853 None => self.compile_for(f).map(IrStmt::For),
3854 },
3855 Stmt::Analyze(inner) => self.compile_stmt(inner),
3859 }
3860 }
3861
3862 fn compile_schema_cast_select(&mut self, sel: &ast::SelectStmt, tc: &ast::TypeCast) -> Result<IrSelect, PyQLError> {
3866 let id_filter = Expr::BinOp(Box::new(ast::BinOp {
3867 left: Expr::Path(ast::Path::relative("id")),
3868 op: ast::BinOpKind::Eq,
3869 right: tc.expr.clone(),
3870 }));
3871 let merged_filter = match &sel.filter {
3872 None => Some(id_filter),
3873 Some(existing) => Some(Expr::BinOp(Box::new(ast::BinOp {
3874 left: id_filter,
3875 op: ast::BinOpKind::And,
3876 right: existing.clone(),
3877 }))),
3878 };
3879 let (module, name) = tc
3882 .ty
3883 .as_named()
3884 .ok_or_else(|| self.type_err("cannot use a tuple or array type as a schema object cast"))?;
3885 let qname = match module {
3890 Some(m) => format!("{}::{}", m, name),
3891 None => name.to_string(),
3892 };
3893 let synthetic = ast::SelectStmt {
3894 result: Expr::Path(ast::Path::absolute(&qname)),
3895 filter: merged_filter,
3896 order_by: sel.order_by.clone(),
3897 offset: sel.offset.clone(),
3898 limit: sel.limit.clone(),
3899 lock: sel.lock.clone(),
3900 };
3901 self.compile_select(&synthetic, &synthetic.result, false)
3902 }
3903
3904 fn compile_shape_anchored(
3913 &mut self,
3914 elements: &[ShapeElement],
3915 td: &'a TypeDescriptor,
3916 alias: &str,
3917 module: &str,
3918 ) -> Result<Vec<IrShapePointer>, PyQLError> {
3919 self.anchors.push(SelectAnchor {
3920 type_name: td.name.clone(),
3921 qualified: format!("{}::{}", td.module, td.name),
3922 alias: alias.to_string(),
3923 detached: false,
3924 declared_on: None,
3925 });
3926 let result = self.compile_shape(elements, td, alias, module);
3927 self.anchors.pop();
3928 result
3929 }
3930
3931 fn compile_path_select(
3932 &mut self,
3933 sel: &ast::SelectStmt,
3934 path: &ast::Path,
3935 shape_elements: &[ShapeElement],
3936 distinct: bool,
3937 ) -> Result<IrPathSelect, PyQLError> {
3938 self.compile_path_select_with_tail(sel, path, shape_elements, distinct, 0, &[])
3939 }
3940
3941 fn compile_path_select_with_tail(
3946 &mut self,
3947 sel: &ast::SelectStmt,
3948 path: &ast::Path,
3949 shape_elements: &[ShapeElement],
3950 distinct: bool,
3951 tail: usize,
3952 tail_sorts: &[ast::SortExpr],
3953 ) -> Result<IrPathSelect, PyQLError> {
3954 let outer_anchor = self.modifier_anchor.take();
3955 let outer_sorts = std::mem::replace(&mut self.tail_sorts, tail_sorts.to_vec());
3958 let mut result = self.compile_path_select_inner(sel, path, shape_elements, distinct, tail);
3959 self.tail_sorts = outer_sorts;
3960 self.modifier_anchor = outer_anchor;
3961 if let Ok(path_select) = &mut result {
3962 self.resolve_join_fanouts(path_select);
3963 }
3964 result
3965 }
3966
3967 fn compile_path_select_inner(
3968 &mut self,
3969 sel: &ast::SelectStmt,
3970 path: &ast::Path,
3971 shape_elements: &[ShapeElement],
3972 distinct: bool,
3973 mut tail: usize,
3974 ) -> Result<IrPathSelect, PyQLError> {
3975 use ast::PathStep;
3976
3977 let root_name = match &path.steps[0] {
3978 PathStep::Name(n) => n.as_str(),
3979 _ => return Err(self.type_err("path traversal must start with a type name")),
3980 };
3981 let root_td = self.resolve_path_root(root_name)?;
3988 let root_alias = self.fresh_alias();
3989 let root = IrSource {
3990 poly: None,
3991 type_name: format!("{}::{}", root_td.module, root_td.name),
3992 table: self.row_source_table(root_name, root_td),
3993 alias: root_alias.clone(),
3994 };
3995 let for_var_root = self.for_var_types.contains_key(root_name).then(|| {
3999 IrExpr::BinOp(Box::new(IrBinOp {
4000 left: IrExpr::ColumnRef {
4001 alias: root_alias.clone(),
4002 column: "id".to_string(),
4003 pg_type: "uuid".to_string(),
4004 },
4005 op: ast::BinOpKind::Eq,
4006 right: self.for_var_ref(root_name),
4007 }))
4008 });
4009
4010 let mut joins: Vec<IrPathJoin> = vec![];
4011 let mut current_td = root_td;
4012 let mut current_alias = root_alias;
4013
4014 let mut steps: Vec<PathStep> = path.steps[1..].to_vec();
4018 let mut pending_filters: Vec<(usize, Expr)> = vec![];
4021 let mut extra_conditions: Vec<IrExpr> = for_var_root.into_iter().collect();
4022 let mut junction_scope: Option<(String, String)> = None;
4025 let mut splices = 0usize;
4026 let mut idx = 0;
4027 if tail > 0 && tail >= steps.len() {
4028 self.modifier_anchor = Some((
4029 format!("{}::{}", current_td.module, current_td.name),
4030 current_alias.clone(),
4031 ));
4032 }
4033 while idx < steps.len() {
4034 let owned_step = steps[idx].clone();
4035 let step = &owned_step;
4036 let n_steps = steps.len();
4037 let is_last = |extra: usize| idx + extra == n_steps - 1;
4038 if tail > 0 && idx == n_steps - tail {
4039 self.modifier_anchor = Some((
4040 format!("{}::{}", current_td.module, current_td.name),
4041 current_alias.clone(),
4042 ));
4043 }
4044
4045 while let Some(pos) = pending_filters.iter().position(|(i, _)| *i + 1 == idx) {
4048 let (_, f) = pending_filters.remove(pos);
4049 let cond = self.compile_expr(&f, current_td, ¤t_alias)?;
4050 extra_conditions.push(cond);
4051 }
4052
4053 if let PathStep::TypeIntersection(type_ref) = step {
4055 let type_name = match &type_ref.module {
4056 Some(m) => format!("{}::{}", m, type_ref.name),
4057 None => type_ref.name.clone(),
4058 };
4059 let narrowed = self.resolve_type(&type_name)?;
4060 let narrowed_has_relation = !Self::backs_no_relation(narrowed);
4067 if narrowed_has_relation && narrowed.table != current_td.table {
4068 let target_alias = self.fresh_alias();
4069 let target = IrSource {
4070 poly: None,
4071 type_name: format!("{}::{}", narrowed.module, narrowed.name),
4072 table: narrowed.table.clone(),
4073 alias: target_alias.clone(),
4074 };
4075 joins.push(IrPathJoin::Single {
4076 source_alias: current_alias.clone(),
4077 fk_col: "id".to_string(),
4078 target,
4079 });
4080 current_alias = target_alias;
4081 }
4082 current_td = narrowed;
4083 idx += 1;
4084 continue;
4085 }
4086
4087 if let PathStep::Backlink(link_name) = step {
4089 let owner_hint = steps.get(idx + 1).and_then(|s| {
4091 if let PathStep::TypeIntersection(tr) = s {
4092 Some(tr.clone())
4093 } else {
4094 None
4095 }
4096 });
4097 let consumed_extra = if owner_hint.is_some() { 1 } else { 0 };
4098
4099 let owner_td: &TypeDescriptor = if let Some(ref tr) = owner_hint {
4100 let type_name = match &tr.module {
4101 Some(m) => format!("{}::{}", m, tr.name),
4102 None => tr.name.clone(),
4103 };
4104 let td = self.resolve_type(&type_name)?;
4105 let current_qname = format!("{}::{}", current_td.module, current_td.name);
4106 let link_targets_current =
4107 td.links
4108 .iter()
4109 .any(|l| l.name == *link_name && self.link_target_reaches(&l.target, ¤t_qname))
4110 || td.multilinks.iter().any(|ml| {
4111 ml.name == *link_name && self.link_target_reaches(&ml.target, ¤t_qname)
4112 });
4113 if link_targets_current {
4114 td
4115 } else {
4116 let narrowed = format!("{}::{}", td.module, td.name);
4119 let declaring: Vec<&'a TypeDescriptor> = self
4120 .schema
4121 .types
4122 .iter()
4123 .filter(|t| !t.abstract_ && Self::is_or_implements(t, &narrowed))
4124 .filter(|t| self.declares_backlink(t, link_name, ¤t_qname))
4125 .collect();
4126 match Self::without_inherited_owners(declaring).as_slice() {
4127 [only] => only,
4128 _ => {
4129 return Err(self.type_err(&format!(
4130 "link '{}::{}' does not target '{}'; backlink is not valid here",
4131 type_name, link_name, current_qname,
4132 )));
4133 }
4134 }
4135 }
4136 } else {
4137 let current_qname = format!("{}::{}", current_td.module, current_td.name);
4139 self.schema
4140 .types
4141 .iter()
4142 .filter(|t| {
4144 t.bases.is_empty()
4145 || !t.bases.iter().any(|base| {
4146 self.resolve_type(base).is_ok_and(|b| {
4147 b.links.iter().any(|l| l.name == *link_name)
4148 || b.multilinks.iter().any(|ml| ml.name == *link_name)
4149 })
4150 })
4151 })
4152 .find(|t| {
4153 t.links
4154 .iter()
4155 .any(|l| l.name == *link_name && self.link_target_reaches(&l.target, ¤t_qname))
4156 || t.multilinks.iter().any(|ml| {
4157 ml.name == *link_name && self.link_target_reaches(&ml.target, ¤t_qname)
4158 })
4159 })
4160 .ok_or_else(|| {
4161 self.type_err(&format!(
4162 "no type has a link '{}' targeting '{}'",
4163 link_name, current_qname,
4164 ))
4165 })?
4166 };
4167
4168 let target_alias = self.fresh_alias();
4169 let target = IrSource {
4170 poly: None,
4171 type_name: format!("{}::{}", owner_td.module, owner_td.name),
4172 table: owner_td.table.clone(),
4173 alias: target_alias.clone(),
4174 };
4175
4176 if let Some(l) = owner_td.links.iter().find(|l| l.name == *link_name) {
4180 if l.is_junction_backed() {
4181 let junction_alias = self.fresh_alias();
4182 let (junction_table, module, owner_col, current_col, _) =
4183 self.link_junction_info(owner_td, l)?;
4184 joins.push(IrPathJoin::BacklinkMulti {
4185 source_alias: current_alias.clone(),
4186 junction_alias,
4187 junction_table,
4188 module,
4189 owner_col,
4190 current_col,
4191 target,
4192 });
4193 } else {
4194 joins.push(IrPathJoin::BacklinkSingle {
4195 source_alias: current_alias.clone(),
4196 fk_col: format!("{}_id", link_name),
4197 target,
4198 });
4199 }
4200 } else {
4201 let ml = owner_td
4202 .multilinks
4203 .iter()
4204 .find(|ml| ml.name == *link_name)
4205 .unwrap()
4206 .clone();
4207 let junction_alias = self.fresh_alias();
4208 let (junction_table, module, _, _, _) = self.multilink_junction_info(owner_td, &ml)?;
4209 joins.push(IrPathJoin::BacklinkMulti {
4210 source_alias: current_alias.clone(),
4211 junction_alias,
4212 junction_table,
4213 module,
4214 owner_col: "source".to_string(),
4215 current_col: "target".to_string(),
4216 target,
4217 });
4218 }
4219
4220 if is_last(consumed_extra) {
4221 let shape =
4222 self.compile_shape_anchored(shape_elements, owner_td, &target_alias, &owner_td.module.clone())?;
4223 let result = IrPathResult::Object {
4224 alias: target_alias.clone(),
4225 type_name: format!("{}::{}", owner_td.module, owner_td.name),
4226 shape,
4227 };
4228 let (filter, order_by, offset, limit) = self.compile_path_modifiers_scoped(
4229 sel,
4230 owner_td,
4231 &target_alias,
4232 junction_scope.clone(),
4233 shape_elements,
4234 )?;
4235 return Ok(IrPathSelect {
4236 root,
4237 joins,
4238 result,
4239 filter: and_conditions(filter, extra_conditions),
4240 order_by,
4241 offset,
4242 limit,
4243 distinct,
4244 poly_implementors: vec![],
4245 });
4246 }
4247 current_td = owner_td;
4248 current_alias = target_alias;
4249 idx += 1 + consumed_extra;
4250 continue;
4251 }
4252
4253 let step_name = match step {
4254 PathStep::Name(n) => n.as_str(),
4255 _ => return Err(self.type_err("only name steps are supported in path traversal")),
4256 };
4257
4258 if step_name == "__type__" {
4262 if !is_last(0) {
4263 return Err(
4264 self.type_err("'__type__' is the type's name, not an object — it cannot be traversed further")
4265 );
4266 }
4267 let polymorphic = self.is_polymorphic(current_td);
4268 let expr = if polymorphic {
4269 IrExpr::ColumnRef {
4270 alias: current_alias.clone(),
4271 column: "__type__".to_string(),
4272 pg_type: "text".to_string(),
4273 }
4274 } else {
4275 IrExpr::Literal(IrLiteral::Str(format!("{}::{}", current_td.module, current_td.name)))
4276 };
4277 let poly_implementors = if polymorphic && joins.is_empty() && !root.table.starts_with("@cte:") {
4282 self.find_poly_implementors(&root.type_name.clone())
4283 } else {
4284 vec![]
4285 };
4286 let (filter, order_by, offset, limit) = self.compile_path_modifiers_scoped(
4287 sel,
4288 current_td,
4289 ¤t_alias,
4290 junction_scope.clone(),
4291 shape_elements,
4292 )?;
4293 return Ok(IrPathSelect {
4294 root,
4295 joins,
4296 result: IrPathResult::Scalar(expr, None),
4297 filter: and_conditions(filter, extra_conditions),
4298 order_by,
4299 offset,
4300 limit,
4301 distinct,
4302 poly_implementors,
4303 });
4304 }
4305
4306 if let Some(p) = current_td.properties.iter().find(|p| p.name == step_name) {
4308 if !is_last(0) {
4309 if p.pg_type.starts_with("__nt__:") || p.tuple_members.is_some() {
4313 let base = IrExpr::ColumnRef {
4314 alias: current_alias.clone(),
4315 column: p.name.clone(),
4316 pg_type: p.pg_type.clone(),
4317 };
4318 let remaining = &steps[idx + 1..];
4319 let mut ir: IrExpr = base;
4320 for step in remaining {
4321 let field = match step {
4322 ast::PathStep::Name(n) => n.clone(),
4323 _ => return Err(self.type_err("only field name steps are valid inside a named tuple")),
4324 };
4325 ir = IrExpr::JsonbField {
4326 expr: Box::new(ir),
4327 field,
4328 };
4329 }
4330 let (filter, order_by, offset, limit) = self.compile_path_modifiers_scoped(
4331 sel,
4332 current_td,
4333 ¤t_alias,
4334 junction_scope.clone(),
4335 shape_elements,
4336 )?;
4337 return Ok(IrPathSelect {
4338 root,
4339 joins,
4340 result: IrPathResult::Scalar(ir, None),
4341 filter: and_conditions(filter, extra_conditions),
4342 order_by,
4343 offset,
4344 limit,
4345 distinct,
4346 poly_implementors: vec![],
4347 });
4348 }
4349 return Err(self.type_err(&format!(
4350 "'{step_name}' is a scalar property, not a link — cannot traverse further"
4351 )));
4352 }
4353 let tuple_shape = self.resolve_property_tuple_shape(p);
4354 let result = IrPathResult::Scalar(
4355 IrExpr::ColumnRef {
4356 alias: current_alias.clone(),
4357 column: p.name.clone(),
4358 pg_type: p.pg_type.clone(),
4359 },
4360 tuple_shape,
4361 );
4362 let (filter, order_by, offset, limit) = self.compile_path_modifiers_scoped(
4363 sel,
4364 current_td,
4365 ¤t_alias,
4366 junction_scope.clone(),
4367 shape_elements,
4368 )?;
4369 return Ok(IrPathSelect {
4370 root,
4371 joins,
4372 result,
4373 filter: and_conditions(filter, extra_conditions),
4374 order_by,
4375 offset,
4376 limit,
4377 distinct,
4378 poly_implementors: vec![],
4379 });
4380 }
4381
4382 if let Some(l) = Self::resolve_link(current_td, step_name) {
4384 let target_td = self.resolve_type(&l.target)?;
4385 let target_alias = self.fresh_alias();
4386 let target = IrSource {
4387 poly: None,
4388 type_name: format!("{}::{}", target_td.module, target_td.name),
4389 table: target_td.table.clone(),
4390 alias: target_alias.clone(),
4391 };
4392 if l.is_junction_backed() {
4393 let join = self.build_multilink_join(current_td, &l.name, &l.target, &l.through)?;
4398 let junction_alias = self.fresh_alias();
4399 junction_scope = l.through.clone().map(|t| (t, junction_alias.clone()));
4400 joins.push(IrPathJoin::Multi {
4401 source_alias: current_alias.clone(),
4402 junction_alias,
4403 join,
4404 target,
4405 });
4406 } else {
4407 joins.push(IrPathJoin::Single {
4408 source_alias: current_alias.clone(),
4409 fk_col: format!("{}_id", l.name),
4410 target,
4411 });
4412 }
4413 if is_last(0) {
4414 let shape = self.compile_shape_anchored(
4415 shape_elements,
4416 target_td,
4417 &target_alias,
4418 &target_td.module.clone(),
4419 )?;
4420 let result = IrPathResult::Object {
4421 alias: target_alias.clone(),
4422 type_name: format!("{}::{}", target_td.module, target_td.name),
4423 shape,
4424 };
4425 let (filter, order_by, offset, limit) = self.compile_path_modifiers_scoped(
4426 sel,
4427 target_td,
4428 &target_alias,
4429 junction_scope.clone(),
4430 shape_elements,
4431 )?;
4432 return Ok(IrPathSelect {
4433 root,
4434 joins,
4435 result,
4436 filter: and_conditions(filter, extra_conditions),
4437 order_by,
4438 offset,
4439 limit,
4440 distinct,
4441 poly_implementors: vec![],
4442 });
4443 }
4444 current_td = target_td;
4445 current_alias = target_alias;
4446 idx += 1;
4447 continue;
4448 }
4449
4450 if let Some(ml) = Self::resolve_multilink(current_td, step_name) {
4452 let target_td = self.resolve_type(&ml.target)?;
4453 let target_alias = self.fresh_alias();
4454 let junction_alias = self.fresh_alias();
4455 let override_for_step = self
4459 .junction_read_overrides
4460 .get(&self.owner_junction(current_td, &ml.name, None))
4461 .cloned();
4462 let target = IrSource {
4463 poly: None,
4464 type_name: format!("{}::{}", target_td.module, target_td.name),
4465 table: match override_for_step.as_ref().and_then(|o| o.targets.clone()) {
4466 Some(cte) => cte,
4467 None => target_td.table.clone(),
4468 },
4469 alias: target_alias.clone(),
4470 };
4471 let join_info = if let Some(through_qname) = &ml.through {
4472 let through_td = self.resolve_type(through_qname)?;
4473 if through_td.junction {
4474 IrMultiLinkJoin::Standard {
4477 junction_table: self.owner_junction(current_td, &ml.name, Some(through_td)),
4478 module: current_td.module.clone(),
4479 }
4480 } else {
4481 let source_qname = format!("{}::{}", current_td.module, current_td.name);
4482 let source_col = through_td
4483 .links
4484 .iter()
4485 .find(|l| l.target == source_qname)
4486 .ok_or_else(|| {
4487 PyQLError::Type(PyQLTypeError {
4488 message: format!("through type {through_qname} has no link to {source_qname}"),
4489 position: Position { line: 0, col: 0 },
4490 })
4491 })?
4492 .name
4493 .clone();
4494 let target_col = through_td
4495 .links
4496 .iter()
4497 .find(|l| l.target == ml.target && l.name != source_col)
4498 .or_else(|| through_td.links.iter().find(|l| l.target == ml.target))
4499 .ok_or_else(|| {
4500 PyQLError::Type(PyQLTypeError {
4501 message: format!(
4502 "through type {through_qname} has no link to target {}",
4503 ml.target
4504 ),
4505 position: Position { line: 0, col: 0 },
4506 })
4507 })?
4508 .name
4509 .clone();
4510 IrMultiLinkJoin::Through {
4511 junction_table: through_td.table.clone(),
4512 module: through_td.module.clone(),
4513 source_col,
4514 target_col,
4515 }
4516 }
4517 } else {
4518 let junction_table = self.owner_junction(current_td, &ml.name, None);
4519 IrMultiLinkJoin::Standard {
4520 junction_table: match &override_for_step {
4521 Some(o) => o.junction.clone(),
4522 None => junction_table,
4523 },
4524 module: current_td.module.clone(),
4525 }
4526 };
4527 junction_scope = ml.through.clone().map(|t| (t, junction_alias.clone()));
4528 joins.push(IrPathJoin::Multi {
4529 source_alias: current_alias.clone(),
4530 junction_alias,
4531 join: join_info,
4532 target,
4533 });
4534 if is_last(0) {
4535 let shape = self.compile_shape_anchored(
4536 shape_elements,
4537 target_td,
4538 &target_alias,
4539 &target_td.module.clone(),
4540 )?;
4541 let result = IrPathResult::Object {
4542 alias: target_alias.clone(),
4543 type_name: format!("{}::{}", target_td.module, target_td.name),
4544 shape,
4545 };
4546 let (filter, order_by, offset, limit) = self.compile_path_modifiers_scoped(
4547 sel,
4548 target_td,
4549 &target_alias,
4550 junction_scope.clone(),
4551 shape_elements,
4552 )?;
4553 return Ok(IrPathSelect {
4554 root,
4555 joins,
4556 result,
4557 filter: and_conditions(filter, extra_conditions),
4558 order_by,
4559 offset,
4560 limit,
4561 distinct,
4562 poly_implementors: vec![],
4563 });
4564 }
4565 current_td = target_td;
4566 current_alias = target_alias;
4567 idx += 1;
4568 continue;
4569 }
4570
4571 let declared = match self.resolve_computed(current_td, step_name) {
4576 Some(cd) => Some(crate::parse::parse_pointer_expr(&cd.expression).map_err(PyQLError::Syntax)?),
4577 None if idx == 0 => self
4578 .active_declared_pointers
4579 .iter()
4580 .find(|d| path_leaf(&d.path).is_ok_and(|n| n == step_name))
4581 .and_then(|d| d.compexpr.clone()),
4582 None => None,
4583 };
4584 if let Some(parsed) = declared {
4585 let rewritten = Self::field_access_over_select(&parsed);
4588 let expr_ast = match &rewritten {
4589 Some((sel, _)) => Expr::SubQuery(Box::new(Stmt::Select(sel.clone()))),
4590 None => parsed,
4591 };
4592 let field_steps = rewritten.as_ref().map(|(_, n)| *n).unwrap_or(0);
4593
4594 if let Some((p, _, modifiers)) = Self::pointer_subject(&expr_ast)
4606 && p.partial
4607 && !p.steps.is_empty()
4608 && (!is_last(0)
4609 || self
4610 .walk_path_types(current_td, &p.steps, MAX_COMPUTED_SPLICES)
4611 .1
4612 .is_some())
4613 {
4614 if let Some(m) = modifiers
4619 && (m.limit.is_some() || m.offset.is_some() || !m.order_by.is_empty())
4620 {
4621 let mut inner_steps =
4622 vec![PathStep::Name(format!("{}::{}", current_td.module, current_td.name))];
4623 inner_steps.extend(p.steps.iter().cloned());
4624 let inner_path = ast::Path {
4625 steps: inner_steps,
4626 partial: false,
4627 };
4628 let inner_sel = ast::SelectStmt {
4629 result: Expr::Path(inner_path.clone()),
4630 filter: m.filter.clone(),
4631 order_by: m.order_by.clone(),
4632 offset: m.offset.clone(),
4633 limit: m.limit.clone(),
4634 lock: None,
4635 };
4636 let mut inner =
4637 self.compile_path_select_with_tail(&inner_sel, &inner_path, &[], false, field_steps, &[])?;
4638 Self::correlate_path_select(&mut inner, ¤t_alias);
4639 let IrPathResult::Object { type_name, .. } = &inner.result else {
4640 return Err(self.type_err(&format!(
4641 "computed pointer '{step_name}' is a scalar — a path cannot continue through it"
4642 )));
4643 };
4644 let target_td = self.resolve_type(&type_name.clone())?;
4645 let target_alias = self.fresh_alias();
4646 let target = IrSource {
4647 poly: None,
4648 type_name: format!("{}::{}", target_td.module, target_td.name),
4649 table: target_td.table.clone(),
4650 alias: target_alias.clone(),
4651 };
4652 joins.push(IrPathJoin::Lateral {
4653 inner: Box::new(inner),
4654 target,
4655 });
4656 if is_last(0) {
4657 let module = target_td.module.clone();
4658 let shape =
4659 self.compile_shape_anchored(shape_elements, target_td, &target_alias, &module)?;
4660 let result = IrPathResult::Object {
4661 alias: target_alias.clone(),
4662 type_name: format!("{}::{}", target_td.module, target_td.name),
4663 shape,
4664 };
4665 let (filter, order_by, offset, limit) = self.compile_path_modifiers_scoped(
4666 sel,
4667 target_td,
4668 &target_alias,
4669 junction_scope,
4670 shape_elements,
4671 )?;
4672 return Ok(IrPathSelect {
4673 root,
4674 joins,
4675 result,
4676 filter: and_conditions(filter, extra_conditions),
4677 order_by,
4678 offset,
4679 limit,
4680 distinct,
4681 poly_implementors: vec![],
4682 });
4683 }
4684 current_td = target_td;
4685 current_alias = target_alias;
4686 idx += 1;
4687 continue;
4688 }
4689 splices += 1;
4690 if splices > MAX_COMPUTED_SPLICES {
4691 return Err(self.type_err(&format!(
4692 "computed pointer '{step_name}' expands into itself — \
4693 a path cannot be resolved through a cycle of computed pointers"
4694 )));
4695 }
4696 let filter = modifiers.and_then(|m| m.filter.clone());
4697 let spliced = p.steps.clone();
4698 if tail > 0 && idx >= steps.len() - tail {
4699 tail += spliced.len() - 1;
4700 }
4701 let landing = idx + spliced.len() - 1;
4702 steps.splice(idx..idx + 1, spliced);
4703 let shift = landing - idx;
4706 for (i, _) in pending_filters.iter_mut() {
4707 if *i > idx {
4708 *i += shift;
4709 }
4710 }
4711 if let Some(f) = filter {
4712 pending_filters.push((landing, f));
4713 }
4714 continue;
4715 }
4716
4717 if let Some((fc, modifiers)) = Self::function_subject(&expr_ast)
4723 && let Some(fd) = self.resolve_object_fn(fc)
4724 {
4725 if fd.params.len() != fc.args.len() {
4726 return Err(self.type_err(&format!(
4727 "function '{}::{}' expects {} argument(s), got {}",
4728 fd.module,
4729 fd.name,
4730 fd.params.len(),
4731 fc.args.len()
4732 )));
4733 }
4734 let (fn_module, fn_name, return_type_name) =
4735 (fd.module.clone(), fd.name.clone(), fd.return_pg_type.clone());
4736 let mut args = fc
4737 .args
4738 .iter()
4739 .map(|a| self.compile_expr(a, current_td, ¤t_alias))
4740 .collect::<Result<Vec<_>, _>>()?;
4741 let qualified = format!("{fn_module}::{fn_name}");
4742 if let Some(globals) = self.globals_arg_for_call(&qualified)? {
4743 args.insert(0, globals);
4744 }
4745 let target_td = self.resolve_type(&return_type_name)?;
4746 let target_alias = self.fresh_alias();
4747 let target = IrSource {
4748 poly: None,
4749 type_name: format!("{}::{}", target_td.module, target_td.name),
4750 table: target_td.table.clone(),
4751 alias: target_alias.clone(),
4752 };
4753 joins.push(IrPathJoin::Function {
4754 fn_module,
4755 fn_name,
4756 args,
4757 target,
4758 });
4759 if let Some(f) = modifiers.and_then(|m| m.filter.clone()) {
4760 let cond = self.compile_expr(&f, target_td, &target_alias)?;
4761 extra_conditions.push(cond);
4762 }
4763 if is_last(0) {
4764 let shape = self.compile_shape_anchored(
4765 shape_elements,
4766 target_td,
4767 &target_alias,
4768 &target_td.module.clone(),
4769 )?;
4770 let result = IrPathResult::Object {
4771 alias: target_alias.clone(),
4772 type_name: format!("{}::{}", target_td.module, target_td.name),
4773 shape,
4774 };
4775 let (filter, order_by, offset, limit) = self.compile_path_modifiers_scoped(
4776 sel,
4777 target_td,
4778 &target_alias,
4779 junction_scope.clone(),
4780 shape_elements,
4781 )?;
4782 return Ok(IrPathSelect {
4783 root,
4784 joins,
4785 result,
4786 filter: and_conditions(filter, extra_conditions),
4787 order_by,
4788 offset,
4789 limit,
4790 distinct,
4791 poly_implementors: vec![],
4792 });
4793 }
4794 current_td = target_td;
4795 current_alias = target_alias;
4796 idx += 1;
4797 continue;
4798 }
4799
4800 if !is_last(0) {
4801 return Err(self.type_err(&format!(
4802 "'{step_name}' is a computed pointer — it has no stored column to traverse further through"
4803 )));
4804 }
4805 let expanding = format!("{}::{}.{step_name}", current_td.module, current_td.name);
4806 if self.expanding_computeds.contains(&expanding) {
4807 return Err(self.type_err(&format!(
4808 "computed pointer '{step_name}' expands into itself — \
4809 a path cannot be resolved through a cycle of computed pointers"
4810 )));
4811 }
4812 self.expanding_computeds.push(expanding);
4813 let expr = self.compile_expr(&expr_ast, current_td, ¤t_alias);
4814 self.expanding_computeds.pop();
4815 let expr = expr?;
4816 let (filter, order_by, offset, limit) = self.compile_path_modifiers_scoped(
4817 sel,
4818 current_td,
4819 ¤t_alias,
4820 junction_scope.clone(),
4821 shape_elements,
4822 )?;
4823 return Ok(IrPathSelect {
4824 root,
4825 joins,
4826 result: IrPathResult::Scalar(expr, None),
4827 filter: and_conditions(filter, extra_conditions),
4828 order_by,
4829 offset,
4830 limit,
4831 distinct,
4832 poly_implementors: vec![],
4833 });
4834 }
4835
4836 return Err(self.field_err(step_name, &format!("{}::{}", current_td.module, current_td.name)));
4837 }
4838
4839 if matches!(steps.last(), Some(PathStep::TypeIntersection(_))) {
4845 let module = current_td.module.clone();
4846 let type_name = format!("{}::{}", current_td.module, current_td.name);
4847 let shape = self.compile_shape_anchored(shape_elements, current_td, ¤t_alias, &module)?;
4848 let (filter, order_by, offset, limit) = self.compile_path_modifiers_scoped(
4849 sel,
4850 current_td,
4851 ¤t_alias,
4852 junction_scope.clone(),
4853 shape_elements,
4854 )?;
4855 return Ok(IrPathSelect {
4856 root,
4857 joins,
4858 result: IrPathResult::Object {
4859 alias: current_alias,
4860 type_name,
4861 shape,
4862 },
4863 filter: and_conditions(filter, extra_conditions),
4864 order_by,
4865 offset,
4866 limit,
4867 distinct,
4868 poly_implementors: vec![],
4869 });
4870 }
4871
4872 Err(self.type_err("empty path traversal"))
4874 }
4875
4876 fn compile_path_modifiers_scoped(
4881 &mut self,
4882 sel: &ast::SelectStmt,
4883 td: &TypeDescriptor,
4884 alias: &str,
4885 junction: Option<(String, String)>,
4886 shape: &[ShapeElement],
4887 ) -> Result<SelectModifiers, PyQLError> {
4888 let pushed = junction.is_some();
4889 if pushed {
4890 self.link_prop_scope.push(junction);
4891 }
4892 let outer_declared = self.active_declared_pointers.clone();
4897 self.active_declared_pointers
4898 .extend(shape.iter().filter(|el| el.compexpr.is_some()).cloned());
4899 let anchored = self.modifier_anchor.take();
4900 let tail_sorts = std::mem::take(&mut self.tail_sorts);
4901 let result = match &anchored {
4902 Some((qualified, anchor_alias)) => {
4903 let anchor_td = self.resolve_type(qualified)?;
4904 let anchor_alias = anchor_alias.clone();
4905 self.compile_path_modifiers(sel, anchor_td, &anchor_alias)
4906 }
4907 None => self.compile_path_modifiers(sel, td, alias),
4908 };
4909 let result = result.and_then(|(filter, mut order_by, offset, limit)| {
4910 if !tail_sorts.is_empty() {
4911 let sorting = ast::SelectStmt {
4912 result: sel.result.clone(),
4913 filter: None,
4914 order_by: tail_sorts,
4915 offset: None,
4916 limit: None,
4917 lock: None,
4918 };
4919 let (_, sorts, _, _) = self.compile_path_modifiers(&sorting, td, alias)?;
4920 order_by = sorts;
4921 }
4922 Ok((filter, order_by, offset, limit))
4923 });
4924 if pushed {
4925 self.link_prop_scope.pop();
4926 }
4927 self.active_declared_pointers = outer_declared;
4928 result
4929 }
4930
4931 fn compile_path_modifiers(
4932 &mut self,
4933 sel: &ast::SelectStmt,
4934 td: &TypeDescriptor,
4935 alias: &str,
4936 ) -> Result<SelectModifiers, PyQLError> {
4937 self.anchors.push(SelectAnchor {
4938 type_name: td.name.clone(),
4939 qualified: format!("{}::{}", td.module, td.name),
4940 alias: alias.to_string(),
4941 detached: std::mem::take(&mut self.pending_detached),
4942 declared_on: None,
4943 });
4944 let result = self.compile_path_modifiers_inner(sel, td, alias);
4945 self.anchors.pop();
4946 result
4947 }
4948
4949 fn compile_path_modifiers_inner(
4950 &mut self,
4951 sel: &ast::SelectStmt,
4952 td: &TypeDescriptor,
4953 alias: &str,
4954 ) -> Result<SelectModifiers, PyQLError> {
4955 let filter = sel
4956 .filter
4957 .as_ref()
4958 .map(|f| self.compile_expr(f, td, alias))
4959 .transpose()?;
4960 let order_by = sel
4961 .order_by
4962 .iter()
4963 .map(|s| self.compile_sort(s, td, alias))
4964 .collect::<Result<Vec<_>, _>>()?;
4965 let offset = sel
4966 .offset
4967 .as_ref()
4968 .map(|e| self.compile_expr(e, td, alias))
4969 .transpose()?;
4970 let limit = sel
4971 .limit
4972 .as_ref()
4973 .map(|e| self.compile_expr(e, td, alias))
4974 .transpose()?;
4975 Ok((filter, order_by, offset, limit))
4976 }
4977
4978 fn find_path_root_in_expr(&self, expr: &Expr) -> Option<String> {
4983 match expr {
4984 Expr::Path(p) if !p.partial && p.steps.len() > 1 => {
4985 if let ast::PathStep::Name(root) = &p.steps[0]
4986 && (self.resolve_type(root).is_ok() || self.cte_object_type(root).is_some())
4987 {
4988 return Some(root.clone());
4989 }
4990 None
4991 }
4992 Expr::FunctionCall(f) => f.args.iter().find_map(|a| self.find_path_root_in_expr(a)),
4993 Expr::BinOp(b) => self
4994 .find_path_root_in_expr(&b.left)
4995 .or_else(|| self.find_path_root_in_expr(&b.right)),
4996 Expr::UnaryOp(u) => self.find_path_root_in_expr(&u.operand),
4997 _ => None,
4998 }
4999 }
5000
5001 fn rewrite_abs_to_partial(expr: Expr, root_name: &str) -> Expr {
5003 match expr {
5004 Expr::Path(ref p) if !p.partial => {
5005 if let ast::PathStep::Name(first) = &p.steps[0]
5006 && first == root_name
5007 && p.steps.len() > 1
5008 {
5009 return Expr::Path(ast::Path {
5010 steps: p.steps[1..].to_vec(),
5011 partial: true,
5012 });
5013 }
5014 expr
5015 }
5016 Expr::FunctionCall(f) => Expr::FunctionCall(ast::FunctionCall {
5017 module: f.module,
5018 name: f.name,
5019 args: f
5020 .args
5021 .into_iter()
5022 .map(|a| Self::rewrite_abs_to_partial(a, root_name))
5023 .collect(),
5024 kwargs: f.kwargs,
5025 }),
5026 Expr::BinOp(b) => Expr::BinOp(Box::new(ast::BinOp {
5027 left: Self::rewrite_abs_to_partial(b.left, root_name),
5028 op: b.op,
5029 right: Self::rewrite_abs_to_partial(b.right, root_name),
5030 })),
5031 Expr::UnaryOp(u) => Expr::UnaryOp(Box::new(ast::UnaryOp {
5032 op: u.op,
5033 operand: Self::rewrite_abs_to_partial(u.operand, root_name),
5034 })),
5035 other => other,
5036 }
5037 }
5038
5039 fn shape_alias_paths(elements: &[ShapeElement]) -> Option<Vec<(String, ast::Path)>> {
5045 let mut defs = Vec::with_capacity(elements.len());
5046 for element in elements {
5047 let [ast::PathStep::Name(name)] = element.path.steps.as_slice() else {
5048 return None;
5049 };
5050 match &element.compexpr {
5051 Some(Expr::Path(p)) if p.partial => defs.push((name.clone(), p.clone())),
5052 _ => return None,
5053 }
5054 }
5055 Some(defs)
5056 }
5057
5058 fn substitute_shape_aliases(expr: Expr, defs: &[(String, ast::Path)]) -> Expr {
5062 match expr {
5063 Expr::Path(ref p) if p.partial => {
5064 let Some(ast::PathStep::Name(first)) = p.steps.first() else {
5065 return expr;
5066 };
5067 let Some((_, definition)) = defs.iter().find(|(name, _)| name == first) else {
5068 return expr;
5069 };
5070 let mut steps = definition.steps.clone();
5071 steps.extend(p.steps[1..].iter().cloned());
5072 Expr::Path(ast::Path { steps, partial: true })
5073 }
5074 Expr::FunctionCall(f) => Expr::FunctionCall(ast::FunctionCall {
5075 module: f.module,
5076 name: f.name,
5077 args: f
5078 .args
5079 .into_iter()
5080 .map(|a| Self::substitute_shape_aliases(a, defs))
5081 .collect(),
5082 kwargs: f.kwargs,
5083 }),
5084 Expr::BinOp(b) => Expr::BinOp(Box::new(ast::BinOp {
5085 left: Self::substitute_shape_aliases(b.left, defs),
5086 op: b.op,
5087 right: Self::substitute_shape_aliases(b.right, defs),
5088 })),
5089 Expr::UnaryOp(u) => Expr::UnaryOp(Box::new(ast::UnaryOp {
5090 op: u.op,
5091 operand: Self::substitute_shape_aliases(u.operand, defs),
5092 })),
5093 other => other,
5094 }
5095 }
5096
5097 fn aggregate_over_unpacked(
5104 &mut self,
5105 sel: &ast::SelectStmt,
5106 result: &Expr,
5107 distinct: bool,
5108 ) -> Result<Option<IrExpr>, PyQLError> {
5109 let Expr::FunctionCall(f) = result else {
5110 return Ok(None);
5111 };
5112 let [Expr::FunctionCall(unpack)] = f.args.as_slice() else {
5113 return Ok(None);
5114 };
5115 if !f.kwargs.is_empty()
5116 || !unpack.kwargs.is_empty()
5117 || unpack.module.as_deref().unwrap_or("std") != "std"
5118 || unpack.name != "array_unpack"
5119 {
5120 return Ok(None);
5121 }
5122 let [array] = unpack.args.as_slice() else {
5123 return Ok(None);
5124 };
5125 let namespace = f.module.as_deref().unwrap_or("std");
5126 let Some(sql_name) =
5127 crate::stdlib::lookup(namespace, &f.name)
5128 .into_iter()
5129 .find_map(|d| match &d.impl_strategy {
5130 crate::stdlib::ImplStrategy::SqlBuiltin(sql_name) if d.is_aggregate() => Some(sql_name.to_string()),
5131 _ => None,
5132 })
5133 else {
5134 return Ok(None);
5135 };
5136
5137 let elements = match array {
5141 Expr::Path(path) if !path.partial && path.steps.len() > 1 => {
5142 let walk = ast::Path {
5143 partial: false,
5144 steps: path.steps.clone(),
5145 };
5146 let mut path_select = self.compile_path_select(sel, &walk, &[], distinct)?;
5147 let IrPathResult::Scalar(column, _) = path_select.result else {
5148 return Ok(None);
5149 };
5150 path_select.result = IrPathResult::Scalar(
5151 IrExpr::FunctionCall(IrFunctionCall {
5152 return_pg_type: None,
5153 schema: None,
5154 name: "unnest".to_string(),
5155 args: vec![column],
5156 sql_template: None,
5157 }),
5158 None,
5159 );
5160 IrExpr::ArrayFromSelect(Box::new(IrArraySource::PathSelect(Box::new(path_select))))
5161 }
5162 _ if sel.filter.is_none()
5163 && sel.order_by.is_empty()
5164 && sel.offset.is_none()
5165 && sel.limit.is_none()
5166 && !distinct =>
5167 {
5168 self.compile_expr_ctx(array, None)?
5169 }
5170 _ => return Ok(None),
5171 };
5172
5173 let over_nothing = aggregate_over_nothing_sql(&f.name).unwrap_or("NULL");
5176 Ok(Some(IrExpr::FunctionCall(IrFunctionCall {
5177 return_pg_type: None,
5178 schema: None,
5179 name: sql_name.clone(),
5180 args: vec![elements],
5181 sql_template: Some(format!(
5182 "(SELECT coalesce({sql_name}(\"_s\".\"v\"), {over_nothing}) FROM unnest($1) AS \"_s\"(\"v\"))"
5183 )),
5184 })))
5185 }
5186
5187 fn compile_expr_as_path_select(
5191 &mut self,
5192 sel: &ast::SelectStmt,
5193 result: &Expr,
5194 root_type_name: &str,
5195 distinct: bool,
5196 ) -> Result<IrPathSelect, PyQLError> {
5197 let (agg_arg, agg_distinct) = match result {
5208 Expr::FunctionCall(f) if f.args.len() == 1 => match &f.args[0] {
5209 Expr::UnaryOp(u) if matches!(u.op, ast::UnaryOpKind::Distinct) => (Some(&u.operand), true),
5210 other => (Some(other), false),
5211 },
5212 _ => (None, false),
5213 };
5214 if let Expr::FunctionCall(f) = result
5215 && f.args.len() == 1
5216 && let Some(Expr::Path(p)) = agg_arg
5217 && !p.partial
5218 && p.steps.len() > 1
5219 {
5220 let arg_path = ast::Path {
5221 partial: false,
5222 steps: p.steps.clone(),
5223 };
5224 let mut ps = self.compile_path_select(sel, &arg_path, &[], distinct)?;
5225 let aggregated = match ps.result.clone() {
5229 IrPathResult::Scalar(column, _) => Some(column),
5230 IrPathResult::Object { alias, .. } => Some(IrExpr::ColumnRef {
5231 alias,
5232 column: "id".to_string(),
5233 pg_type: "uuid".to_string(),
5234 }),
5235 };
5236 if let Some(column) = aggregated {
5237 let mut args = vec![column];
5238 if !f.kwargs.is_empty() {
5239 if !matches!(f.name.as_str(), "assert_single" | "assert_exists" | "assert_distinct") {
5240 return Err(self.type_err(&format!(
5241 "function '{}' does not take named arguments, got '{}'",
5242 f.name, f.kwargs[0].0
5243 )));
5244 }
5245 args.extend(self.assert_message(f, None)?);
5246 }
5247 let mut call = self.resolve_fn_call(f.module.as_deref(), &f.name, args)?;
5248 if agg_distinct {
5249 let IrExpr::FunctionCall(fc) = &mut call else {
5250 return Err(self.type_err(&format!(
5251 "'{}' does not take a distinct argument — it does not resolve to an aggregate",
5252 f.name
5253 )));
5254 };
5255 if fc.schema.is_some() || fc.sql_template.is_some() {
5256 return Err(self.type_err(&format!(
5257 "'distinct' inside '{}' is not supported — only a plain SQL aggregate can take it",
5258 f.name
5259 )));
5260 }
5261 fc.sql_template = Some(format!("{}(DISTINCT $1)", fc.name));
5262 }
5263 ps.result = IrPathResult::Scalar(aggregate_over_nothing(&f.name, call), None);
5264 return Ok(ps);
5265 }
5266 }
5267
5268 if let Expr::BinOp(b) = result {
5272 let (path_expr, value_expr, flip) = match (&b.left, &b.right) {
5273 (Expr::Path(p), v) if !p.partial => (p, v, false),
5274 (v, Expr::Path(p)) if !p.partial => (p, v, true),
5275 _ => return self.compile_expr_as_path_select_fallback(sel, result, root_type_name, distinct),
5276 };
5277 let root_td = match self.cte_object_type(root_type_name) {
5279 Some(t) => self.resolve_type(&t)?,
5280 None => self.resolve_type(root_type_name)?,
5281 };
5282 let ast_path = ast::Path {
5284 partial: false,
5285 steps: path_expr.steps.clone(),
5286 };
5287 let mut ps = self.compile_path_select(sel, &ast_path, &[], distinct)?;
5288 let val_ir = self.compile_expr(value_expr, root_td, &ps.root.alias)?;
5289 let scalar_col = match ps.result {
5291 IrPathResult::Scalar(e, _) => e,
5292 IrPathResult::Object { type_name, .. } => {
5293 let val_type = infer_ir_type(&val_ir).map(pg_type_to_pyql).unwrap_or("unknown");
5294 return Err(PyQLError::Type(PyQLTypeError {
5295 message: format!(
5296 "operator '{}' cannot be applied to operands of type '{}' and '{}'",
5297 b.op, type_name, val_type,
5298 ),
5299 position: Position { line: 0, col: 0 },
5300 }));
5301 }
5302 };
5303 let (l, r) = if flip {
5304 (val_ir, scalar_col)
5305 } else {
5306 (scalar_col, val_ir)
5307 };
5308 ps.result = IrPathResult::Scalar(
5309 IrExpr::BinOp(Box::new(IrBinOp {
5310 left: true_division_operand(&b.op, l, &r),
5311 op: b.op.clone(),
5312 right: r,
5313 })),
5314 None,
5315 );
5316 return Ok(ps);
5317 }
5318 self.compile_expr_as_path_select_fallback(sel, result, root_type_name, distinct)
5319 }
5320
5321 fn compile_expr_as_path_select_fallback(
5322 &mut self,
5323 sel: &ast::SelectStmt,
5324 result: &Expr,
5325 root_type_name: &str,
5326 distinct: bool,
5327 ) -> Result<IrPathSelect, PyQLError> {
5328 let cte_object_type = self.cte_object_type(root_type_name);
5329 let td = match &cte_object_type {
5330 Some(t) => self.resolve_type(t)?,
5331 None => self.resolve_type(root_type_name)?,
5332 };
5333 let alias = self.fresh_alias();
5334 let root = IrSource {
5335 poly: None,
5336 type_name: format!("{}::{}", td.module, td.name),
5337 table: match &cte_object_type {
5338 Some(_) => format!("@cte:{}", root_type_name),
5339 None => td.table.clone(),
5340 },
5341 alias: alias.clone(),
5342 };
5343 let rewritten = Self::rewrite_abs_to_partial(result.clone(), root_type_name);
5344 let expr = self.compile_expr(&rewritten, td, &alias)?;
5345 let (filter, order_by, offset, limit) = self.compile_path_modifiers(sel, td, &alias)?;
5346 Ok(IrPathSelect {
5347 root,
5348 joins: vec![],
5349 result: IrPathResult::Scalar(expr, None),
5350 filter,
5351 order_by,
5352 offset,
5353 limit,
5354 distinct,
5355 poly_implementors: vec![],
5356 })
5357 }
5358
5359 fn compile_subquery_to_array_source(&mut self, stmt: &Stmt) -> Result<IrArraySource, PyQLError> {
5362 match self.compile_stmt(stmt)? {
5363 IrStmt::Select(s) if matches!(s.rows.as_slice(), [IrRowSource::Bound { .. }]) => {
5364 Ok(IrArraySource::Select(s))
5365 }
5366 IrStmt::PathSelect(ps) => Ok(IrArraySource::PathSelect(Box::new(ps))),
5367 _ => Err(self.type_err("assert functions require a schema-bound SELECT as argument")),
5368 }
5369 }
5370
5371 fn compile_exists_ctx(
5379 &mut self,
5380 operand: &Expr,
5381 ctx: Option<(&TypeDescriptor, &str)>,
5382 ) -> Result<IrExpr, PyQLError> {
5383 match operand {
5384 Expr::Parameter(name) => {
5386 let idx = self.param_index(name);
5387 Ok(ir_is_not_null(IrExpr::Param { index: idx }))
5388 }
5389 Expr::TypeCast(_) => {
5391 let inner = self.compile_expr_ctx(operand, ctx)?;
5392 Ok(ir_is_not_null(inner))
5393 }
5394
5395 Expr::Path(p)
5397 if ctx.is_some() && p.partial && matches!(p.steps.first(), Some(ast::PathStep::Backlink(_))) =>
5398 {
5399 let (td, alias) = ctx.unwrap();
5400 let current_qname = format!("{}::{}", td.module, td.name);
5401 let exists = self.compile_backlink_as_exists(&p.steps, None, ¤t_qname, alias)?;
5402 Ok(exists)
5403 }
5404
5405 Expr::Path(p) if ctx.is_some() && p.partial && p.steps.len() == 1 => {
5409 let (td, alias) = ctx.unwrap();
5410 if let ast::PathStep::LinkProp(prop_name) = &p.steps[0] {
5415 let prop = self.compile_link_prop_ref(prop_name)?;
5416 return Ok(ir_is_not_null(prop));
5417 }
5418 if let ast::PathStep::Name(n) = &p.steps[0]
5422 && Self::resolve_property(td, n).is_none()
5423 && Self::resolve_link(td, n).is_none()
5424 && Self::resolve_multilink(td, n).is_none()
5425 && self.resolve_computed(td, n).is_none()
5426 && let Some(expr) = self
5427 .active_declared_pointers
5428 .iter()
5429 .find(|d| path_leaf(&d.path).is_ok_and(|name| name == n))
5430 .and_then(|d| d.compexpr.clone())
5431 {
5432 let inner = self.compile_expr(&expr, td, alias)?;
5433 return Ok(ir_is_not_null(inner));
5434 }
5435 if let ast::PathStep::TypeIntersection(type_ref) = &p.steps[0] {
5439 let narrowed = self.resolve_type(&type_ref.qualified_name())?;
5440 let check = format!("{}::{}", narrowed.module, narrowed.name);
5441 let source = format!("{}::{}", td.module, td.name);
5442 return Ok(self.type_check_bool_expr(&source, &check, td, alias));
5443 }
5444 let pointer_name = match &p.steps[0] {
5445 ast::PathStep::Name(n) => n.as_str(),
5446 _ => return Err(self.type_err("exists: invalid path step")),
5447 };
5448 if let Some(prop) = Self::resolve_property(td, pointer_name) {
5449 return Ok(ir_is_not_null(IrExpr::ColumnRef {
5450 alias: alias.to_string(),
5451 column: prop.name.clone(),
5452 pg_type: prop.pg_type.clone(),
5453 }));
5454 }
5455 if let Some(link) = Self::resolve_link(td, pointer_name) {
5456 if link.is_junction_backed() {
5457 return self.compile_junction_link_exists_check(link, td, alias);
5458 }
5459 return Ok(ir_is_not_null(IrExpr::ColumnRef {
5460 alias: alias.to_string(),
5461 column: format!("{}_id", link.name),
5462 pg_type: "uuid".to_string(),
5463 }));
5464 }
5465 if Self::resolve_multilink(td, pointer_name).is_some() {
5466 return self.compile_multilink_exists_check(pointer_name, td, alias);
5467 }
5468 if let Some(cd) = self.resolve_computed(td, pointer_name) {
5474 let expr_ast = crate::parse::parse_pointer_expr(&cd.expression).map_err(PyQLError::Syntax)?;
5475 return self.compile_exists_ctx(&expr_ast, ctx);
5476 }
5477 Err(self.field_err(pointer_name, &format!("{}::{}", td.module, td.name)))
5478 }
5479
5480 Expr::SubQuery(stmt)
5486 if ctx.is_some()
5487 && matches!(
5488 stmt.as_ref(),
5489 Stmt::Select(inner) if matches!(&inner.result, Expr::Path(p) if p.partial)
5490 ) =>
5491 {
5492 let Stmt::Select(inner) = stmt.as_ref() else {
5493 unreachable!("checked by the guard")
5494 };
5495 let Expr::Path(path) = &inner.result else {
5496 unreachable!("checked by the guard")
5497 };
5498 let (td, alias) = ctx.expect("checked by the guard");
5499 let mut steps = vec![ast::PathStep::Name(format!("{}::{}", td.module, td.name))];
5500 steps.extend(path.steps.iter().cloned());
5501 let full_path = ast::Path { steps, partial: false };
5502 let rooted = ast::SelectStmt {
5503 result: Expr::Path(full_path.clone()),
5504 filter: inner.filter.clone(),
5505 order_by: inner.order_by.clone(),
5506 offset: inner.offset.clone(),
5507 limit: inner.limit.clone(),
5508 lock: None,
5509 };
5510 let mut ps = self.compile_path_select(&rooted, &full_path, &[], false)?;
5511 Self::correlate_path_select(&mut ps, alias);
5512 Ok(IrExpr::UnaryOp(Box::new(IrUnaryOp {
5513 op: ast::UnaryOpKind::Exists,
5514 operand: IrExpr::PathSubquery(Box::new(ps)),
5515 })))
5516 }
5517
5518 Expr::SubQuery(stmt) => self.compile_subquery_exists(stmt),
5520
5521 operand if let Some(name) = self.resolve_cte_name(operand) => {
5524 let object = self.cte_types.get(name).is_some_and(|bound| bound.contains("::"));
5525 Ok(IrExpr::ExistsOverCte {
5526 cte: name.to_string(),
5527 column: (!object).then(|| "v".to_string()),
5528 })
5529 }
5530
5531 other => {
5537 let inner = self.compile_expr_ctx(other, ctx)?;
5538 if let IrExpr::SetOp { op, left, right, .. } = inner {
5541 return Ok(IrExpr::SetOp {
5542 op,
5543 left,
5544 right,
5545 mode: super::SetOpMode::Exists,
5546 });
5547 }
5548 Ok(ir_is_not_null(inner))
5549 }
5550 }
5551 }
5552
5553 fn compile_subquery_exists(&mut self, stmt: &Stmt) -> Result<IrExpr, PyQLError> {
5555 match self.compile_stmt(stmt)? {
5556 IrStmt::Select(s) => {
5557 let mut rows = s.rows;
5558 if rows.len() != 1 {
5559 return Err(self.type_err("exists requires a schema-bound SELECT expression"));
5560 }
5561 let IrRowSource::Bound { source, .. } = rows.remove(0) else {
5562 return Err(self.type_err("exists requires a schema-bound SELECT expression"));
5563 };
5564 let inner = IrExpr::Subquery(Box::new(IrSelect::schema_bound(source, vec![], s.filter)));
5565 Ok(IrExpr::UnaryOp(Box::new(IrUnaryOp {
5566 op: ast::UnaryOpKind::Exists,
5567 operand: inner,
5568 })))
5569 }
5570 IrStmt::PathSelect(ps) => {
5571 Ok(IrExpr::UnaryOp(Box::new(IrUnaryOp {
5574 op: ast::UnaryOpKind::Exists,
5575 operand: IrExpr::Subquery(Box::new(IrSelect::schema_bound(ps.root, vec![], ps.filter))),
5576 })))
5577 }
5578 _ => Err(self.type_err("exists requires a SELECT expression")),
5579 }
5580 }
5581
5582 fn junction_correlation_select(
5591 &mut self,
5592 td: &TypeDescriptor,
5593 alias: &str,
5594 name: &str,
5595 target: &str,
5596 through: &Option<String>,
5597 ) -> Result<IrSelect, PyQLError> {
5598 let jt_alias = self.fresh_alias();
5599 let (jt_table, jt_module, jt_src_col, _, _) = self.junction_info_for(td, name, target, through)?;
5600
5601 let filter = IrExpr::BinOp(Box::new(IrBinOp {
5602 left: IrExpr::ColumnRef {
5603 alias: jt_alias.clone(),
5604 column: jt_src_col,
5605 pg_type: "uuid".to_string(),
5606 },
5607 op: ast::BinOpKind::Eq,
5608 right: IrExpr::ColumnRef {
5609 alias: alias.to_string(),
5610 column: "id".to_string(),
5611 pg_type: "uuid".to_string(),
5612 },
5613 }));
5614 Ok(IrSelect::schema_bound(
5615 IrSource {
5616 poly: None,
5617 type_name: format!("{}::__jt__", jt_module),
5618 table: jt_table,
5619 alias: jt_alias,
5620 },
5621 vec![],
5622 Some(filter),
5623 ))
5624 }
5625
5626 fn multilink_correlation_select(
5627 &mut self,
5628 ml_name: &str,
5629 td: &TypeDescriptor,
5630 alias: &str,
5631 ) -> Result<IrSelect, PyQLError> {
5632 let ml = Self::resolve_multilink(td, ml_name).unwrap();
5633 let (name, target, through) = (ml.name.clone(), ml.target.clone(), ml.through.clone());
5634 self.junction_correlation_select(td, alias, &name, &target, &through)
5635 }
5636
5637 fn compile_multilink_exists_check(
5638 &mut self,
5639 ml_name: &str,
5640 td: &TypeDescriptor,
5641 alias: &str,
5642 ) -> Result<IrExpr, PyQLError> {
5643 let inner = self.multilink_correlation_select(ml_name, td, alias)?;
5644 Ok(IrExpr::UnaryOp(Box::new(IrUnaryOp {
5645 op: ast::UnaryOpKind::Exists,
5646 operand: IrExpr::Subquery(Box::new(inner)),
5647 })))
5648 }
5649
5650 fn compile_junction_link_exists_check(
5653 &mut self,
5654 l: &LinkDescriptor,
5655 td: &TypeDescriptor,
5656 alias: &str,
5657 ) -> Result<IrExpr, PyQLError> {
5658 let (name, target, through) = (l.name.clone(), l.target.clone(), l.through.clone());
5659 let inner = self.junction_correlation_select(td, alias, &name, &target, &through)?;
5660 Ok(IrExpr::UnaryOp(Box::new(IrUnaryOp {
5661 op: ast::UnaryOpKind::Exists,
5662 operand: IrExpr::Subquery(Box::new(inner)),
5663 })))
5664 }
5665
5666 fn junction_target_id_expr(
5673 &mut self,
5674 td: &TypeDescriptor,
5675 l: &LinkDescriptor,
5676 alias: &str,
5677 ) -> Result<IrExpr, PyQLError> {
5678 let (name, target, through) = (l.name.clone(), l.target.clone(), l.through.clone());
5679 let (jt_table, jt_module, jt_src_col, jt_tgt_col, _) = self.junction_info_for(td, &name, &target, &through)?;
5680 let jt_alias = self.fresh_alias();
5681 let filter = IrExpr::BinOp(Box::new(IrBinOp {
5682 left: IrExpr::ColumnRef {
5683 alias: jt_alias.clone(),
5684 column: jt_src_col,
5685 pg_type: "uuid".to_string(),
5686 },
5687 op: ast::BinOpKind::Eq,
5688 right: IrExpr::ColumnRef {
5689 alias: alias.to_string(),
5690 column: "id".to_string(),
5691 pg_type: "uuid".to_string(),
5692 },
5693 }));
5694 let select = IrSelect::schema_bound(
5695 IrSource {
5696 poly: None,
5697 type_name: format!("{}::__jt__", jt_module),
5698 table: jt_table,
5699 alias: jt_alias,
5700 },
5701 vec![IrShapePointer::Scalar(IrScalarPointer {
5702 implicit_id: false,
5703 marker_offset: None,
5704 alias: "target".to_string(),
5705 column: jt_tgt_col,
5706 pg_type: "uuid".to_string(),
5707 tuple_shape: None,
5708 })],
5709 Some(filter),
5710 );
5711 Ok(IrExpr::Subquery(Box::new(select)))
5712 }
5713
5714 fn check_union_type_compat(&self, expr: &Expr) -> Result<(), PyQLError> {
5718 let Expr::Union(a, b) = expr else { return Ok(()) };
5719 let a_free = self.is_free_result(a);
5720 let b_free = self.is_free_result(b);
5721 if a_free != b_free {
5722 let left = self.union_operand_type_display(a);
5723 let right = self.union_operand_type_display(b);
5724 return Err(PyQLError::Type(PyQLTypeError {
5725 message: format!(
5726 "operator 'UNION' cannot be applied to operands of type '{}' and '{}'",
5727 left, right,
5728 ),
5729 position: Position { line: 0, col: 0 },
5730 }));
5731 }
5732 Ok(())
5733 }
5734
5735 fn union_operand_type_display(&self, expr: &Expr) -> String {
5736 match expr {
5737 Expr::Path(p) if !p.partial && p.steps.len() == 1 => {
5738 if let ast::PathStep::Name(n) = &p.steps[0]
5739 && let Some(t) = self.cte_types.get(n.as_str())
5740 {
5741 if t.contains("::") {
5742 return t.clone(); }
5744 if !t.is_empty() {
5745 return pg_type_to_pyql(t).to_string(); }
5747 }
5748 if let Ok(td) = self.resolve_type(
5750 p.steps
5751 .first()
5752 .and_then(|s| {
5753 if let ast::PathStep::Name(n) = s {
5754 Some(n.as_str())
5755 } else {
5756 None
5757 }
5758 })
5759 .unwrap_or(""),
5760 ) {
5761 return format!("{}::{}", td.module, td.name);
5762 }
5763 }
5764 Expr::Literal(Literal::Int(_)) => return "std::int64".to_string(),
5765 Expr::Literal(Literal::Str(_)) => return "std::str".to_string(),
5766 Expr::Literal(Literal::Float(_)) => return "std::float64".to_string(),
5767 Expr::Literal(Literal::Bool(_)) => return "std::bool".to_string(),
5768 _ => {}
5769 }
5770 "unknown".to_string()
5771 }
5772
5773 fn is_free_result(&self, expr: &Expr) -> bool {
5774 let expr = match expr {
5775 Expr::UnaryOp(u) if u.op == ast::UnaryOpKind::Distinct => &u.operand,
5776 Expr::Detached(inner) => inner.as_ref(),
5777 other => other,
5778 };
5779 match expr {
5780 Expr::Path(p) if !p.partial => {
5781 if p.steps.len() == 1
5782 && let ast::PathStep::Name(n) = &p.steps[0]
5783 {
5784 if self.for_vars.contains_key(n.as_str()) {
5788 return !self.for_var_types.contains_key(n.as_str());
5789 }
5790 if self.is_value_binding(n.as_str()) {
5792 return true;
5793 }
5794 if self.fn_params.contains_key(n.as_str()) {
5799 return true;
5800 }
5801 }
5802 false
5803 }
5804 Expr::Shape(s) if s.expr.is_some() => false,
5805 Expr::SubQuery(_) => false,
5806 Expr::Union(a, b) => self.is_free_result(a) && self.is_free_result(b),
5807 Expr::IfElse(ie) => {
5812 let empty = |e: &Expr| matches!(e, Expr::Set(items) if items.is_empty());
5813 match (empty(&ie.if_expr), empty(&ie.else_expr)) {
5814 (true, true) => true,
5815 (true, false) => self.is_free_result(&ie.else_expr),
5816 (false, true) => self.is_free_result(&ie.if_expr),
5817 (false, false) => self.is_free_result(&ie.if_expr) || self.is_free_result(&ie.else_expr),
5818 }
5819 }
5820 _ => true,
5821 }
5822 }
5823
5824 fn is_free_cte_ref(&self, expr: &Expr) -> bool {
5829 let Expr::Path(p) = expr else { return false };
5830 if p.partial || p.steps.len() != 1 {
5831 return false;
5832 }
5833 let ast::PathStep::Name(n) = &p.steps[0] else {
5834 return false;
5835 };
5836 self.is_value_binding(n.as_str())
5837 }
5838
5839 fn dml_as_value(&mut self, stmt: &Stmt) -> Result<IrExpr, PyQLError> {
5847 let (cte_name, type_name) = self.hoist_dml_as_cte(stmt)?;
5848 let source = IrSource {
5852 poly: None,
5853 type_name,
5854 table: format!("@cte:{cte_name}"),
5855 alias: self.fresh_alias(),
5856 };
5857 Ok(IrExpr::Subquery(Box::new(IrSelect::schema_bound(
5858 source,
5859 vec![IrShapePointer::Scalar(IrScalarPointer {
5862 implicit_id: false,
5863 marker_offset: None,
5864 alias: "id".to_string(),
5865 column: "id".to_string(),
5866 pg_type: "uuid".to_string(),
5867 tuple_shape: None,
5868 })],
5869 None,
5870 ))))
5871 }
5872
5873 fn collect_union_items(&mut self, expr: &Expr, items: &mut Vec<IrFreeExpr>) -> Result<(), PyQLError> {
5874 match expr {
5875 Expr::Union(a, b) => {
5876 self.collect_union_items(a, items)?;
5877 self.collect_union_items(b, items)?;
5878 }
5879 Expr::Set(exprs) => {
5880 for e in exprs {
5881 self.collect_union_items(e, items)?;
5882 }
5883 }
5884 Expr::SubQuery(stmt) if matches!(stmt.as_ref(), Stmt::Insert(_) | Stmt::Update(_) | Stmt::Delete(_)) => {
5891 items.push(IrFreeExpr::Scalar(self.dml_as_value(stmt.as_ref())?));
5892 }
5893 other => {
5894 items.push(IrFreeExpr::Scalar(self.compile_free_expr(other)?));
5895 }
5896 }
5897 Ok(())
5898 }
5899
5900 fn shape_over_binding(&mut self, expr: &Expr) -> Result<Option<IrExpr>, PyQLError> {
5906 let Expr::Shape(sh) = expr else {
5907 return Ok(None);
5908 };
5909 let Some(Expr::Path(p)) = sh.expr.as_ref() else {
5910 return Ok(None);
5911 };
5912 let Some(ast::PathStep::Name(root)) = p.steps.first() else {
5913 return Ok(None);
5914 };
5915 if p.partial || (self.cte_object_type(root).is_none() && !self.for_var_types.contains_key(root)) {
5916 return Ok(None);
5917 }
5918 self.free_object_link_field(expr)
5919 }
5920
5921 fn free_object_link_field(&mut self, expr: &Expr) -> Result<Option<IrExpr>, PyQLError> {
5927 let Expr::Shape(sh) = expr else {
5928 return Ok(None);
5929 };
5930 let Some(Expr::Path(p)) = sh.expr.as_ref() else {
5931 return Ok(None);
5932 };
5933 if p.partial {
5934 return Ok(None);
5935 }
5936 let rest_steps = p.steps.as_slice();
5937 let Some(ast::PathStep::Name(name)) = rest_steps.first() else {
5938 return Ok(None);
5939 };
5940 let cte_name = self.cte_object_type(name).map(|_| name.clone());
5941 let Ok(td) = self.resolve_path_root(name) else {
5942 return Ok(None);
5943 };
5944 let table = self.row_source_table(name, td);
5945 if rest_steps.len() > 1 {
5949 let synthetic = ast::SelectStmt {
5950 result: Expr::Path(p.clone()),
5951 filter: None,
5952 order_by: vec![],
5953 offset: None,
5954 limit: None,
5955 lock: None,
5956 };
5957 let path_select = self.compile_path_select(&synthetic, p, &sh.elements, false)?;
5958 return Ok(Some(IrExpr::ObjectPathSubquery(Box::new(path_select))));
5959 }
5960 let alias = self.fresh_alias();
5961 let source = IrSource {
5962 poly: self.poly_fanout_for(&format!("{}::{}", td.module, td.name)),
5963 type_name: format!("{}::{}", td.module, td.name),
5964 table,
5965 alias: alias.clone(),
5966 };
5967 let outer_declared = std::mem::replace(
5970 &mut self.active_declared_pointers,
5971 cte_name
5972 .as_deref()
5973 .and_then(|n| self.cte_declared_pointers.get(n).cloned())
5974 .unwrap_or_default(),
5975 );
5976 let shape = self.compile_shape(&sh.elements, td, &alias, &td.module);
5977 self.active_declared_pointers = outer_declared;
5978 let filter = self.for_var_types.contains_key(name).then(|| {
5981 IrExpr::BinOp(Box::new(IrBinOp {
5982 left: IrExpr::ColumnRef {
5983 alias: alias.clone(),
5984 column: "id".to_string(),
5985 pg_type: "uuid".to_string(),
5986 },
5987 op: ast::BinOpKind::Eq,
5988 right: self.for_var_ref(name),
5989 }))
5990 });
5991 Ok(Some(IrExpr::ObjectSubquery(Box::new(IrSelect::schema_bound(
5992 source, shape?, filter,
5993 )))))
5994 }
5995
5996 fn free_object_field(&mut self, expr: &Expr) -> Result<IrExpr, PyQLError> {
6000 if let Some(object) = self.free_object_link_field(expr)? {
6001 return Ok(object);
6002 }
6003 match expr {
6004 Expr::SubQuery(stmt) if matches!(stmt.as_ref(), Stmt::Insert(_) | Stmt::Update(_) | Stmt::Delete(_)) => {
6005 self.dml_as_value(stmt.as_ref())
6006 }
6007 other => self.compile_free_expr(other),
6008 }
6009 }
6010
6011 fn compile_free_select(
6012 &mut self,
6013 sel: &ast::SelectStmt,
6014 result_expr: &Expr,
6015 distinct: bool,
6016 ) -> Result<IrSelect, PyQLError> {
6017 if let Expr::IfElse(ie) = result_expr {
6021 let is_empty_set = |expr: &Expr| matches!(expr, Expr::Set(items) if items.is_empty());
6022 let is_free_object = |expr: &Expr| matches!(expr, Expr::Shape(sh) if sh.expr.is_none());
6023 let guarded = if is_free_object(&ie.if_expr) && is_empty_set(&ie.else_expr) {
6024 Some((&ie.if_expr, false))
6025 } else if is_empty_set(&ie.if_expr) && is_free_object(&ie.else_expr) {
6026 Some((&ie.else_expr, true))
6027 } else {
6028 None
6029 };
6030 if let Some((object, negated)) = guarded {
6031 let condition = self.compile_free_expr(&ie.condition)?;
6032 let condition = if negated {
6033 IrExpr::UnaryOp(Box::new(IrUnaryOp {
6034 op: ast::UnaryOpKind::Not,
6035 operand: condition,
6036 }))
6037 } else {
6038 condition
6039 };
6040 let mut select = self.compile_free_select(sel, object, distinct)?;
6041 select.filter = and_conditions(select.filter, vec![condition]);
6042 return Ok(select);
6043 }
6044 }
6045 let items: Vec<IrFreeExpr> = match result_expr {
6046 Expr::Union(_, _) | Expr::Set(_) => {
6047 let mut union_items = vec![];
6048 self.collect_union_items(result_expr, &mut union_items)?;
6049 let mut first: Option<(String, String)> = None; for item in &union_items {
6052 if let IrFreeExpr::Scalar(expr) = item
6053 && let Some(t) = infer_ir_type(expr)
6054 {
6055 let t = t.to_string();
6056 if let Some((ft, fq)) = &first {
6057 if !types_compatible(ft, &t) {
6058 return Err(PyQLError::Type(PyQLTypeError {
6059 message: format!(
6060 "operator 'UNION' cannot be applied to operands of type '{}' and '{}'",
6061 fq,
6062 pg_type_to_pyql(&t),
6063 ),
6064 position: Position { line: 0, col: 0 },
6065 }));
6066 }
6067 } else {
6068 first = Some((t.clone(), pg_type_to_pyql(&t).to_string()));
6069 }
6070 }
6071 }
6072 union_items
6073 }
6074 Expr::Path(p) if !p.partial && p.steps.len() == 1 => {
6075 if let ast::PathStep::Name(n) = &p.steps[0] {
6076 if self
6077 .cte_types
6078 .get(n.as_str())
6079 .map(|t| !t.contains("::"))
6080 .unwrap_or(false)
6081 {
6082 vec![IrFreeExpr::CtePassthrough(n.clone())]
6083 } else {
6084 vec![IrFreeExpr::Scalar(self.compile_free_expr(result_expr)?)]
6085 }
6086 } else {
6087 vec![IrFreeExpr::Scalar(self.compile_free_expr(result_expr)?)]
6088 }
6089 }
6090 Expr::Shape(s) if s.expr.is_none() => {
6091 let fields = s
6092 .elements
6093 .iter()
6094 .map(|el| -> Result<(String, IrExpr), PyQLError> {
6095 let name = path_leaf(&el.path)?.to_string();
6096 let expr = el.compexpr.as_ref().ok_or_else(|| {
6097 self.type_err("free object field must have a value expression (':= expr')")
6098 })?;
6099 Ok((name, self.free_object_field(expr)?))
6100 })
6101 .collect::<Result<Vec<_>, _>>()?;
6102 vec![IrFreeExpr::FreeObject(fields)]
6103 }
6104 Expr::Tuple(exprs) => {
6105 let ir = exprs
6109 .iter()
6110 .map(|e| self.free_object_field(e))
6111 .collect::<Result<_, _>>()?;
6112 vec![IrFreeExpr::Tuple(ir)]
6113 }
6114 Expr::NamedTuple(fields) => {
6115 let ir = fields
6116 .iter()
6117 .map(|(name, e)| Ok((name.clone(), self.free_object_field(e)?)))
6118 .collect::<Result<Vec<_>, PyQLError>>()?;
6119 let holds_an_object = ir.iter().any(|(_, e)| match e {
6125 IrExpr::ObjectSubquery(_) | IrExpr::ObjectPathSubquery(_) => true,
6126 IrExpr::ArrayFromSelect(source) => match source.as_ref() {
6127 IrArraySource::ObjectSelect(_) | IrArraySource::ObjectFunction(_) | IrArraySource::Group(_) => {
6128 true
6129 }
6130 IrArraySource::PathSelect(ps) => matches!(ps.result, IrPathResult::Object { .. }),
6131 _ => false,
6132 },
6133 _ => false,
6134 });
6135 if holds_an_object {
6136 vec![IrFreeExpr::NamedTupleRow(ir)]
6137 } else {
6138 vec![IrFreeExpr::Scalar(IrExpr::NamedTuple {
6139 fields: ir,
6140 is_free_object: false,
6141 })]
6142 }
6143 }
6144 other => vec![IrFreeExpr::Scalar(self.compile_free_expr(other)?)],
6145 };
6146
6147 let order_by = sel
6148 .order_by
6149 .iter()
6150 .map(|s| self.compile_sort_ctx(s, None))
6151 .collect::<Result<Vec<_>, _>>()?;
6152
6153 let offset = sel.offset.as_ref().map(|e| self.compile_free_expr(e)).transpose()?;
6154 let limit = sel.limit.as_ref().map(|e| self.compile_free_expr(e)).transpose()?;
6155 let filter = sel.filter.as_ref().map(|f| self.compile_free_filter(f)).transpose()?;
6156
6157 Ok(IrSelect {
6158 rows: items.into_iter().map(IrRowSource::Free).collect(),
6159 filter,
6160 order_by,
6161 offset,
6162 limit,
6163 distinct,
6164 dml_source: None,
6165 polymorphic: false,
6166 poly_implementors: vec![],
6167 poly_columns: vec![],
6168 lock: None,
6169 })
6170 }
6171
6172 fn compile_free_filter(&mut self, filter: &Expr) -> Result<IrExpr, PyQLError> {
6180 let Some(root) = self.find_path_root_in_expr(filter) else {
6181 return self.compile_free_expr(filter);
6182 };
6183 let td = self.resolve_path_root(&root)?;
6186 let table = self.row_source_table(&root, td);
6187 let alias = self.fresh_alias();
6188 let source = IrSource {
6189 poly: None,
6190 type_name: format!("{}::{}", td.module, td.name),
6191 table,
6192 alias: alias.clone(),
6193 };
6194 let condition = self.compile_expr(&Self::rewrite_abs_to_partial(filter.clone(), &root), td, &alias)?;
6195 self.warnings.push(format!(
6196 "possibly more than one element returned by an expression in a FILTER clause \
6197 (every '{root}'); wrap with any() to make intent explicit",
6198 ));
6199 Ok(IrExpr::UnaryOp(Box::new(IrUnaryOp {
6200 op: ast::UnaryOpKind::Exists,
6201 operand: IrExpr::Subquery(Box::new(IrSelect::schema_bound(source, vec![], Some(condition)))),
6202 })))
6203 }
6204
6205 fn common_union_type(&self, branches: &[(String, String)]) -> Option<String> {
6215 let covers_all = |candidate: &String| {
6216 branches.iter().all(|(t, _)| {
6217 t == candidate
6218 || self
6219 .resolve_type(t)
6220 .map(|td| Self::is_or_implements(td, candidate))
6221 .unwrap_or(false)
6222 })
6223 };
6224 if let Some(branch) = branches.iter().map(|(t, _)| t.clone()).find(&covers_all) {
6225 return Some(branch);
6226 }
6227 let first = self.resolve_type(&branches.first()?.0).ok()?;
6232 first
6233 .interfaces
6234 .iter()
6235 .chain(first.parents.iter())
6236 .find(|ancestor| covers_all(ancestor))
6237 .cloned()
6238 }
6239
6240 fn object_union_branches(&self, expr: &Expr) -> Option<Vec<(String, String)>> {
6241 fn flatten<'e>(expr: &'e Expr, out: &mut Vec<&'e Expr>) {
6242 match expr {
6243 Expr::Union(a, b) => {
6244 flatten(a, out);
6245 flatten(b, out);
6246 }
6247 other => out.push(other),
6248 }
6249 }
6250 if !matches!(expr, Expr::Union(_, _)) {
6251 return None;
6252 }
6253 let mut operands: Vec<&Expr> = vec![];
6254 flatten(expr, &mut operands);
6255
6256 let mut branches: Vec<(String, String)> = vec![];
6257 for operand in operands {
6258 let Expr::Path(path) = operand else { return None };
6259 if path.partial || path.steps.len() != 1 {
6260 return None;
6261 }
6262 let ast::PathStep::Name(name) = &path.steps[0] else {
6263 return None;
6264 };
6265 match self.cte_object_type(name) {
6266 Some(qualified) => branches.push((qualified, format!("@cte:{}", self.cte_sql_name(name)))),
6267 None => match self.resolve_type(name) {
6268 Ok(td) => branches.push((format!("{}::{}", td.module, td.name), td.table.clone())),
6269 Err(_) => return None,
6270 },
6271 }
6272 }
6273 Some(branches)
6274 }
6275
6276 fn name_union_operands(&mut self, expr: &Expr) -> Result<Option<Expr>, PyQLError> {
6292 let Expr::Union(left, right) = expr else {
6293 return Ok(None);
6294 };
6295 let mut rewritten = false;
6296 let mut name_one = |compiler: &mut Self, operand: &Expr| -> Result<Expr, PyQLError> {
6297 match operand {
6298 Expr::Union(_, _) => match compiler.name_union_operands(operand)? {
6299 Some(inner) => {
6300 rewritten = true;
6301 Ok(inner)
6302 }
6303 None => Ok(operand.clone()),
6304 },
6305 Expr::SubQuery(stmt) => {
6306 let (cte_name, type_name) = compiler.hoist_dml_as_cte(stmt.as_ref())?;
6307 let Ok(td) = compiler.resolve_type(&type_name) else {
6311 return Ok(operand.clone());
6312 };
6313 compiler
6314 .cte_types
6315 .insert(cte_name.clone(), format!("{}::{}", td.module, td.name));
6316 rewritten = true;
6317 Ok(Expr::Path(ast::Path {
6318 steps: vec![ast::PathStep::Name(cte_name)],
6319 partial: false,
6320 }))
6321 }
6322 Expr::Path(path) if !path.partial && path.steps.len() > 1 => {
6326 let synthetic = Stmt::Select(ast::SelectStmt {
6327 result: operand.clone(),
6328 filter: None,
6329 order_by: vec![],
6330 offset: None,
6331 limit: None,
6332 lock: None,
6333 });
6334 let Ok((cte_name, type_name)) = compiler.hoist_dml_as_cte(&synthetic) else {
6335 return Ok(operand.clone());
6336 };
6337 let Ok(td) = compiler.resolve_type(&type_name) else {
6338 return Ok(operand.clone());
6339 };
6340 compiler
6341 .cte_types
6342 .insert(cte_name.clone(), format!("{}::{}", td.module, td.name));
6343 rewritten = true;
6344 Ok(Expr::Path(ast::Path {
6345 steps: vec![ast::PathStep::Name(cte_name)],
6346 partial: false,
6347 }))
6348 }
6349 other => Ok(other.clone()),
6350 }
6351 };
6352 let left = name_one(self, left)?;
6353 let right = name_one(self, right)?;
6354 if !rewritten {
6355 return Ok(None);
6356 }
6357 Ok(Some(Expr::Union(Box::new(left), Box::new(right))))
6358 }
6359
6360 fn set_mutation_guard(&mut self, branch: &Expr, condition: Expr) {
6370 let stmt = match branch {
6371 Expr::SubQuery(stmt) => Some(stmt.as_ref()),
6372 Expr::Shape(sh) => match sh.expr.as_ref() {
6373 Some(Expr::SubQuery(stmt)) => Some(stmt.as_ref()),
6374 _ => None,
6375 },
6376 _ => None,
6377 };
6378 match stmt {
6379 Some(Stmt::Update(_)) => self.pending_update_guard = Some(condition),
6380 Some(Stmt::Delete(_)) => self.pending_delete_guard = Some(condition),
6381 _ => self.pending_insert_guard = Some(condition),
6382 }
6383 }
6384
6385 fn object_if_else_as_union(&mut self, expr: &Expr) -> Option<Expr> {
6386 let Expr::IfElse(ie) = expr else {
6387 return None;
6388 };
6389 fn is_empty_set(expr: &Expr) -> bool {
6390 matches!(expr, Expr::Set(items) if items.is_empty())
6391 }
6392 let yields_objects = |branch: &Expr| match branch {
6393 Expr::Path(p) if !p.partial && p.steps.len() == 1 => match &p.steps[0] {
6394 ast::PathStep::Name(n) => self.cte_object_type(n).is_some() || self.resolve_type(n).is_ok(),
6395 _ => false,
6396 },
6397 Expr::SubQuery(stmt) => {
6398 if let Some(ast::SelectStmt {
6402 result: Expr::Path(path),
6403 ..
6404 }) = innermost_select(stmt.as_ref())
6405 && !path.partial
6406 && path.steps.len() > 1
6407 && let Some(ast::PathStep::Name(root)) = path.steps.first()
6408 && let Ok(root_td) = self.resolve_path_root(root)
6409 {
6410 return matches!(
6411 self.walk_path_types(root_td, &path.steps[1..], MAX_COMPUTED_SPLICES),
6412 (_, Some(_))
6413 );
6414 }
6415 matches!(
6416 self.dml_subject_type(stmt.as_ref()),
6417 Ok(name) if self.resolve_type(&name).is_ok()
6418 )
6419 }
6420 Expr::Shape(sh) => match sh.expr.as_ref() {
6422 Some(Expr::SubQuery(stmt)) => {
6423 matches!(self.dml_subject_type(stmt.as_ref()), Ok(name) if self.resolve_type(&name).is_ok())
6424 }
6425 Some(Expr::Path(p)) if !p.partial && p.steps.len() == 1 => match &p.steps[0] {
6426 ast::PathStep::Name(n) => self.cte_object_type(n).is_some() || self.resolve_type(n).is_ok(),
6427 _ => false,
6428 },
6429 _ => false,
6430 },
6431 _ => false,
6432 };
6433 fn mutating_stmt(branch: &Expr) -> Option<&Stmt> {
6444 match branch {
6445 Expr::SubQuery(stmt) => Some(stmt.as_ref()),
6446 Expr::Shape(sh) => match sh.expr.as_ref() {
6447 Some(Expr::SubQuery(stmt)) => Some(stmt.as_ref()),
6448 _ => None,
6449 },
6450 _ => None,
6451 }
6452 .filter(|stmt| matches!(stmt, Stmt::Insert(_) | Stmt::Update(_) | Stmt::Delete(_)))
6453 }
6454 let (if_yields_objects, else_yields_objects) = (yields_objects(&ie.if_expr), yields_objects(&ie.else_expr));
6459 let guard = |branch: &Expr, condition: Expr| {
6460 Expr::SubQuery(Box::new(Stmt::Select(ast::SelectStmt {
6461 result: branch.clone(),
6462 filter: Some(condition),
6463 order_by: vec![],
6464 offset: None,
6465 limit: None,
6466 lock: None,
6467 })))
6468 };
6469 let negated = Expr::UnaryOp(Box::new(ast::UnaryOp {
6470 op: ast::UnaryOpKind::Not,
6471 operand: ie.condition.clone(),
6472 }));
6473 let guarded_insert = |branch: &Expr, other: &Expr| {
6474 mutating_stmt(branch).is_some() && matches!(other, Expr::Set(items) if items.is_empty())
6475 };
6476 if guarded_insert(&ie.if_expr, &ie.else_expr) {
6477 self.set_mutation_guard(&ie.if_expr, ie.condition.clone());
6478 return Some(ie.if_expr.clone());
6479 }
6480 if guarded_insert(&ie.else_expr, &ie.if_expr) {
6481 let negated = Expr::UnaryOp(Box::new(ast::UnaryOp {
6482 op: ast::UnaryOpKind::Not,
6483 operand: ie.condition.clone(),
6484 }));
6485 self.set_mutation_guard(&ie.else_expr, negated);
6486 return Some(ie.else_expr.clone());
6487 }
6488 if let (Some(if_stmt), Some(else_stmt)) = (mutating_stmt(&ie.if_expr), mutating_stmt(&ie.else_expr))
6495 && std::mem::discriminant(if_stmt) != std::mem::discriminant(else_stmt)
6496 {
6497 self.set_mutation_guard(&ie.if_expr, ie.condition.clone());
6498 self.set_mutation_guard(&ie.else_expr, negated);
6499 return Some(Expr::Union(
6500 Box::new(ie.if_expr.clone()),
6501 Box::new(ie.else_expr.clone()),
6502 ));
6503 }
6504 let reads_objects = |branch: &Expr, yields: bool| mutating_stmt(branch).is_none() && yields;
6509 if mutating_stmt(&ie.if_expr).is_some() && reads_objects(&ie.else_expr, else_yields_objects) {
6510 self.set_mutation_guard(&ie.if_expr, ie.condition.clone());
6511 return Some(Expr::Union(
6512 Box::new(ie.if_expr.clone()),
6513 Box::new(guard(&ie.else_expr, negated)),
6514 ));
6515 }
6516 if mutating_stmt(&ie.else_expr).is_some() && reads_objects(&ie.if_expr, if_yields_objects) {
6517 self.set_mutation_guard(&ie.else_expr, negated);
6518 return Some(Expr::Union(
6519 Box::new(guard(&ie.if_expr, ie.condition.clone())),
6520 Box::new(ie.else_expr.clone()),
6521 ));
6522 }
6523 if mutating_stmt(&ie.if_expr).is_some() || mutating_stmt(&ie.else_expr).is_some() {
6524 return None;
6525 }
6526 let (if_empty, else_empty) = (is_empty_set(&ie.if_expr), is_empty_set(&ie.else_expr));
6527 if if_empty && else_empty {
6528 return None;
6529 }
6530 if !(if_empty || if_yields_objects) || !(else_empty || else_yields_objects) {
6531 return None;
6532 }
6533 if else_empty {
6534 return Some(guard(&ie.if_expr, ie.condition.clone()));
6535 }
6536 if if_empty {
6537 return Some(guard(&ie.else_expr, negated));
6538 }
6539 Some(Expr::Union(
6540 Box::new(guard(&ie.if_expr, ie.condition.clone())),
6541 Box::new(guard(&ie.else_expr, negated)),
6542 ))
6543 }
6544
6545 fn object_coalesce_as_if_else(expr: &Expr) -> Option<Expr> {
6550 fn as_if_else(b: &ast::BinOp) -> Option<Expr> {
6551 (b.op == ast::BinOpKind::Coalesce).then(|| {
6552 Expr::IfElse(Box::new(ast::IfElse {
6553 condition: Expr::UnaryOp(Box::new(ast::UnaryOp {
6554 op: ast::UnaryOpKind::Exists,
6555 operand: b.left.clone(),
6556 })),
6557 if_expr: b.left.clone(),
6558 else_expr: b.right.clone(),
6559 }))
6560 })
6561 }
6562 match expr {
6563 Expr::BinOp(b) => as_if_else(b),
6564 Expr::Shape(sh) => match sh.expr.as_ref() {
6565 Some(Expr::BinOp(b)) => Some(Expr::Shape(Box::new(ast::ShapeExpr {
6566 expr: Some(as_if_else(b)?),
6567 elements: sh.elements.clone(),
6568 marker_offset: sh.marker_offset,
6569 }))),
6570 _ => None,
6571 },
6572 _ => None,
6573 }
6574 }
6575
6576 fn try_compile_object_union_select(
6577 &mut self,
6578 sel: &ast::SelectStmt,
6579 result_expr: &Expr,
6580 distinct: bool,
6581 ) -> Result<Option<IrSelect>, PyQLError> {
6582 let coalesced;
6583 let result_expr = match Self::object_coalesce_as_if_else(result_expr) {
6584 Some(rewritten) => {
6585 coalesced = rewritten;
6586 &coalesced
6587 }
6588 None => result_expr,
6589 };
6590 let as_union;
6591 let (union_expr, shape_elements): (&Expr, &[ShapeElement]) = match result_expr {
6592 Expr::Union(_, _) => (result_expr, &[]),
6593 Expr::IfElse(_) => match self.object_if_else_as_union(result_expr) {
6594 Some(rewritten @ Expr::SubQuery(_)) => {
6597 return self.compile_select(sel, &rewritten, distinct).map(Some);
6598 }
6599 Some(rewritten) if !matches!(rewritten, Expr::Union(_, _)) => {
6600 return self.compile_select(sel, &rewritten, distinct).map(Some);
6601 }
6602 Some(rewritten) => {
6603 as_union = rewritten;
6604 (&as_union, &[] as &[ShapeElement])
6605 }
6606 None => return Ok(None),
6607 },
6608 Expr::Shape(shape) => match &shape.expr {
6609 Some(inner @ Expr::Union(_, _)) => (inner, shape.elements.as_slice()),
6610 Some(inner @ Expr::IfElse(_)) => match self.object_if_else_as_union(inner) {
6611 Some(Expr::SubQuery(stmt)) => {
6612 let shaped = Expr::Shape(Box::new(ast::ShapeExpr {
6613 expr: Some(Expr::SubQuery(stmt)),
6614 elements: shape.elements.clone(),
6615 marker_offset: None,
6616 }));
6617 return self.compile_select(sel, &shaped, distinct).map(Some);
6618 }
6619 Some(rewritten) if !matches!(rewritten, Expr::Union(_, _)) => {
6620 let shaped = match rewritten {
6621 Expr::Shape(_) => rewritten,
6622 other => Expr::Shape(Box::new(ast::ShapeExpr {
6623 expr: Some(other),
6624 elements: shape.elements.clone(),
6625 marker_offset: None,
6626 })),
6627 };
6628 return self.compile_select(sel, &shaped, distinct).map(Some);
6629 }
6630 Some(rewritten) => {
6631 as_union = rewritten;
6632 (&as_union, shape.elements.as_slice())
6633 }
6634 None => return Ok(None),
6635 },
6636 _ => return Ok(None),
6637 },
6638 _ => return Ok(None),
6639 };
6640
6641 let named;
6642 let union_expr = match self.name_union_operands(union_expr)? {
6643 Some(rewritten) => {
6644 named = rewritten;
6645 &named
6646 }
6647 None => union_expr,
6648 };
6649 let Some(branches) = self.object_union_branches(union_expr) else {
6650 return Ok(None);
6651 };
6652
6653 let Some(first_type) = self.common_union_type(&branches) else {
6654 let (first_type, _) = &branches[0];
6655 let (other, _) = branches
6656 .iter()
6657 .find(|(t, _)| t != first_type)
6658 .expect("no common type means at least two differ");
6659 return Err(self.type_err(&format!(
6660 "operator 'UNION' cannot be applied to operands of type '{first_type}' and '{other}'"
6661 )));
6662 };
6663 let first_type = &first_type;
6664 if sel.lock.is_some() {
6665 return Err(self.type_err(
6666 "FOR UPDATE/SHARE cannot be used on a UNION — its rows come from more than one \
6667 source, which a single locking clause can't target",
6668 ));
6669 }
6670 let td = self.resolve_type(first_type)?;
6671 let alias = self.fresh_alias();
6672
6673 self.anchors.push(SelectAnchor {
6674 type_name: td.name.clone(),
6675 qualified: format!("{}::{}", td.module, td.name),
6676 alias: alias.clone(),
6677 detached: std::mem::take(&mut self.pending_detached),
6678 declared_on: None,
6679 });
6680 let clauses = (|compiler: &mut Self| -> Result<_, PyQLError> {
6681 let shape = compiler.compile_shape(shape_elements, td, &alias, &td.module)?;
6682 let filter = sel
6683 .filter
6684 .as_ref()
6685 .map(|f| compiler.compile_expr(f, td, &alias))
6686 .transpose()?;
6687 let order_by = sel
6688 .order_by
6689 .iter()
6690 .map(|o| compiler.compile_sort(o, td, &alias))
6691 .collect::<Result<Vec<_>, _>>()?;
6692 let offset = sel
6693 .offset
6694 .as_ref()
6695 .map(|e| compiler.compile_expr(e, td, &alias))
6696 .transpose()?;
6697 let limit = sel
6698 .limit
6699 .as_ref()
6700 .map(|e| compiler.compile_expr(e, td, &alias))
6701 .transpose()?;
6702 Ok((shape, filter, order_by, offset, limit))
6703 })(self);
6704 self.anchors.pop();
6705 let (shape, mut filter, order_by, offset, limit) = clauses?;
6706 if let Some(var) = Self::subject_name(result_expr).filter(|n| self.for_var_types.contains_key(n)) {
6709 let narrowed = IrExpr::BinOp(Box::new(IrBinOp {
6710 left: IrExpr::ColumnRef {
6711 alias: alias.clone(),
6712 column: "id".to_string(),
6713 pg_type: "uuid".to_string(),
6714 },
6715 op: ast::BinOpKind::Eq,
6716 right: self.for_var_ref(&var),
6717 }));
6718 filter = and_conditions(filter, vec![narrowed]);
6719 }
6720
6721 let heterogeneous = branches.iter().any(|(t, _)| t != first_type);
6726 let common_columns = if heterogeneous {
6727 self.collect_poly_info(first_type).1
6728 } else {
6729 vec![]
6730 };
6731 let rows = branches
6732 .into_iter()
6733 .map(|(type_name, table)| IrRowSource::Bound {
6734 source: IrSource {
6735 poly: if heterogeneous {
6736 self.poly_fanout_for(&type_name)
6737 } else {
6738 None
6739 },
6740 type_name,
6741 table,
6742 alias: alias.clone(),
6743 },
6744 shape: shape.clone(),
6745 })
6746 .collect();
6747
6748 Ok(Some(IrSelect {
6749 rows,
6750 filter,
6751 order_by,
6752 offset,
6753 limit,
6754 distinct,
6755 dml_source: None,
6756 polymorphic: false,
6757 poly_implementors: vec![],
6758 poly_columns: common_columns,
6759 lock: None,
6760 }))
6761 }
6762
6763 fn subject_name(result_expr: &Expr) -> Option<String> {
6766 let path = match result_expr {
6767 Expr::Path(p) => p,
6768 Expr::Shape(sh) => match sh.expr.as_ref()? {
6769 Expr::Path(p) => p,
6770 _ => return None,
6771 },
6772 _ => return None,
6773 };
6774 if path.partial {
6775 return None;
6776 }
6777 match path.steps.as_slice() {
6778 [ast::PathStep::Name(n)] => Some(n.clone()),
6779 _ => None,
6780 }
6781 }
6782
6783 fn compile_select(
6784 &mut self,
6785 sel: &ast::SelectStmt,
6786 result_expr: &Expr,
6787 distinct: bool,
6788 ) -> Result<IrSelect, PyQLError> {
6789 if let Some(union_select) = self.try_compile_object_union_select(sel, result_expr, distinct)? {
6790 return Ok(union_select);
6791 }
6792 let (type_name, shape_elements, inner_stmt, cte_name) = self.extract_type_and_shape(result_expr)?;
6793 let td = self.resolve_type(&type_name)?;
6794 let alias = self.fresh_alias();
6795 let table = match cte_name {
6796 Some(ref cte) => format!("@cte:{}", cte),
6797 None => Self::subject_name(result_expr)
6798 .map(|name| self.row_source_table(&name, td))
6799 .unwrap_or_else(|| td.table.clone()),
6800 };
6801 let source = IrSource {
6802 poly: self.poly_fanout_for(&format!("{}::{}", td.module, td.name)),
6803 type_name: format!("{}::{}", td.module, td.name),
6804 table,
6805 alias: alias.clone(),
6806 };
6807
6808 self.anchors.push(SelectAnchor {
6812 type_name: td.name.clone(),
6813 qualified: format!("{}::{}", td.module, td.name),
6814 alias: alias.clone(),
6815 detached: std::mem::take(&mut self.pending_detached),
6816 declared_on: None,
6817 });
6818 let declared = match cte_name.as_deref() {
6822 Some(name) => self.cte_declared_pointers.get(name).cloned().unwrap_or_default(),
6823 None => inner_stmt
6827 .and_then(innermost_select)
6828 .and_then(|inner| match &inner.result {
6829 Expr::Shape(sh) => Some(sh.elements.clone()),
6830 _ => None,
6831 })
6832 .unwrap_or_default(),
6833 };
6834 let splat_over_declared: Vec<ShapeElement>;
6836 let shape_elements = if shape_elements
6837 .iter()
6838 .any(|el| el.splat.is_some() && !matches!(el.path.steps.first(), Some(ast::PathStep::TypeIntersection(_))))
6839 {
6840 let named = |name: &str| {
6841 shape_elements
6842 .iter()
6843 .any(|el| el.splat.is_none() && path_leaf(&el.path).is_ok_and(|n| n == name))
6844 };
6845 let extra: Vec<ShapeElement> = declared
6846 .iter()
6847 .filter(|d| d.compexpr.is_some())
6848 .filter_map(|d| {
6849 let name = path_leaf(&d.path).ok()?;
6850 (!named(name)).then(|| ShapeElement {
6851 path: ast::Path::relative(name),
6852 splat: None,
6853 nested: None,
6854 compexpr: None,
6855 op: ast::ShapeOp::Assign,
6856 filter: None,
6857 order_by: vec![],
6858 offset: None,
6859 limit: None,
6860 marker_offset: None,
6861 })
6862 })
6863 .collect();
6864 splat_over_declared = shape_elements.iter().cloned().chain(extra).collect();
6865 &splat_over_declared[..]
6866 } else {
6867 shape_elements
6868 };
6869 let outer_declared = std::mem::replace(&mut self.active_declared_pointers, declared);
6870 let clauses = (|compiler: &mut Self| -> Result<_, PyQLError> {
6871 let shape = compiler.compile_shape(shape_elements, td, &alias, &td.module)?;
6872 compiler
6878 .active_declared_pointers
6879 .extend(shape_elements.iter().filter(|el| el.compexpr.is_some()).cloned());
6880 let filter = sel
6881 .filter
6882 .as_ref()
6883 .map(|f| compiler.compile_expr(f, td, &alias))
6884 .transpose()?;
6885 let order_by = sel
6886 .order_by
6887 .iter()
6888 .map(|s| compiler.compile_sort(s, td, &alias))
6889 .collect::<Result<Vec<_>, _>>()?;
6890 let offset = sel
6891 .offset
6892 .as_ref()
6893 .map(|e| compiler.compile_expr(e, td, &alias))
6894 .transpose()?;
6895 let limit = sel
6896 .limit
6897 .as_ref()
6898 .map(|e| compiler.compile_expr(e, td, &alias))
6899 .transpose()?;
6900 Ok((shape, filter, order_by, offset, limit))
6901 })(self);
6902 self.anchors.pop();
6903 self.active_declared_pointers = outer_declared;
6904 let (shape, mut filter, order_by, offset, limit) = clauses?;
6905
6906 if let Some(var) = Self::subject_name(result_expr).filter(|n| self.for_var_types.contains_key(n)) {
6909 let narrowed = IrExpr::BinOp(Box::new(IrBinOp {
6910 left: IrExpr::ColumnRef {
6911 alias: alias.clone(),
6912 column: "id".to_string(),
6913 pg_type: "uuid".to_string(),
6914 },
6915 op: ast::BinOpKind::Eq,
6916 right: self.for_var_ref(&var),
6917 }));
6918 filter = and_conditions(filter, vec![narrowed]);
6919 }
6920
6921 let dml_source = inner_stmt.map(|s| self.compile_stmt(s).map(Box::new)).transpose()?;
6923 let mut shape = shape;
6924 if let Some(dml) = &dml_source {
6925 let dml_cte = cte_name.as_deref().unwrap_or(crate::sql::DML_CTE);
6929 Self::read_nested_links_from_their_ctes(dml, Some(dml_cte), &mut shape);
6930 }
6931
6932 let polymorphic = self.is_polymorphic(td);
6933 let (poly_implementors, poly_columns) = if polymorphic {
6934 let iface_qname = format!("{}::{}", td.module, td.name);
6935 (self.find_poly_implementors(&iface_qname), Self::poly_dml_columns(td))
6936 } else {
6937 (vec![], vec![])
6938 };
6939
6940 let lock = match &sel.lock {
6948 None => None,
6949 Some(lc) => {
6950 if distinct {
6951 return Err(self.type_err(
6952 "FOR UPDATE/SHARE cannot be combined with DISTINCT — Postgres can't \
6953 guarantee the result rows map 1:1 to physical table rows",
6954 ));
6955 }
6956 if polymorphic {
6957 return Err(self.type_err(
6958 "FOR UPDATE/SHARE cannot be used on an interface type — its rows span \
6959 multiple underlying tables, which a single locking clause can't target",
6960 ));
6961 }
6962 if dml_source.is_some() {
6963 return Err(self.type_err(
6964 "FOR UPDATE/SHARE cannot be used on SELECT (INSERT/UPDATE/DELETE …) — \
6965 there's nothing left to lock once the DML has already run",
6966 ));
6967 }
6968 Some(IrLockClause {
6969 strength: match lc.strength {
6970 ast::LockStrength::Update => IrLockStrength::Update,
6971 ast::LockStrength::NoKeyUpdate => IrLockStrength::NoKeyUpdate,
6972 ast::LockStrength::Share => IrLockStrength::Share,
6973 ast::LockStrength::KeyShare => IrLockStrength::KeyShare,
6974 },
6975 wait: match lc.wait {
6976 ast::LockWait::Block => IrLockWait::Block,
6977 ast::LockWait::NoWait => IrLockWait::NoWait,
6978 ast::LockWait::SkipLocked => IrLockWait::SkipLocked,
6979 },
6980 })
6981 }
6982 };
6983
6984 Ok(IrSelect {
6985 rows: vec![IrRowSource::Bound { source, shape }],
6986 filter,
6987 order_by,
6988 offset,
6989 limit,
6990 distinct,
6991 dml_source,
6992 polymorphic,
6993 poly_implementors,
6994 poly_columns,
6995 lock,
6996 })
6997 }
6998
6999 fn extract_type_and_shape<'e>(&self, expr: &'e Expr) -> Result<TypeAndShape<'e>, PyQLError> {
7004 match expr {
7005 Expr::Shape(s) => {
7006 let (type_name, cte_name, inner) = match s.expr.as_ref() {
7008 Some(Expr::SubQuery(stmt)) => (self.dml_subject_type(stmt)?, None, Some(stmt.as_ref())),
7009 Some(Expr::Path(p)) if !p.partial && p.steps.len() == 1 => {
7010 if let ast::PathStep::Name(n) = &p.steps[0] {
7011 if let Some(t) = self.for_var_types.get(n.as_str()) {
7012 (t.clone(), None, None)
7013 } else if let Some(t) = self.cte_types.get(n.as_str()) {
7014 if t.contains("::") {
7015 (t.clone(), Some(n.clone()), None)
7016 } else {
7017 (self.expr_as_type_name(s.expr.as_ref().unwrap())?, None, None)
7018 }
7019 } else {
7020 (self.expr_as_type_name(s.expr.as_ref().unwrap())?, None, None)
7021 }
7022 } else {
7023 (self.expr_as_type_name(s.expr.as_ref().unwrap())?, None, None)
7024 }
7025 }
7026 Some(inner) => (self.expr_as_type_name(inner)?, None, None),
7027 None => {
7028 return Err(PyQLError::Type(PyQLTypeError {
7029 message: "shape without subject expression".into(),
7030 position: Position { line: 0, col: 0 },
7031 }));
7032 }
7033 };
7034 Ok((type_name, &s.elements, inner, cte_name))
7035 }
7036 Expr::SubQuery(stmt) => Ok((self.dml_subject_type(stmt)?, &[], Some(stmt.as_ref()), None)),
7038 Expr::Path(p) if !p.partial && p.steps.len() == 1 => {
7040 if let ast::PathStep::Name(n) = &p.steps[0] {
7041 if let Some(t) = self.for_var_types.get(n.as_str()) {
7042 return Ok((t.clone(), &[], None, None));
7043 }
7044 if let Some(t) = self.cte_types.get(n.as_str())
7045 && t.contains("::")
7046 {
7047 return Ok((t.clone(), &[], None, Some(n.clone())));
7048 }
7049 }
7050 Ok((self.expr_as_type_name(expr)?, &[], None, None))
7051 }
7052 _ => Ok((self.expr_as_type_name(expr)?, &[], None, None)),
7053 }
7054 }
7055
7056 fn is_json_expr(&self, ast: &Expr, ir: &IrExpr) -> bool {
7060 if matches!(infer_ir_type(ir), Some("jsonb")) {
7061 return true;
7062 }
7063 let Expr::Path(path) = ast else {
7064 return false;
7065 };
7066 if path.partial || path.steps.len() != 1 {
7067 return false;
7068 }
7069 let ast::PathStep::Name(name) = &path.steps[0] else {
7070 return false;
7071 };
7072 self.for_vars.get(name.as_str()).is_some_and(|t| t == "jsonb")
7073 }
7074
7075 fn set_returning_element_type(&mut self, expr: &Expr) -> Result<Option<String>, PyQLError> {
7081 if !expr_returns_set(expr) {
7082 return Ok(None);
7083 }
7084 let Expr::FunctionCall(call) = expr else {
7085 return Ok(None);
7086 };
7087 let mut declared = crate::stdlib::lookup(call.module.as_deref().unwrap_or("std"), &call.name)
7090 .into_iter()
7091 .map(|descriptor| match &descriptor.return_type {
7092 crate::stdlib::PylonType::Set(element) => element.scalar_pg_type(),
7093 _ => None,
7094 });
7095 if let Some(element) = declared.next().flatten()
7096 && declared.all(|other| other == Some(element))
7097 {
7098 return Ok(Some(element.to_string()));
7099 }
7100 let [argument] = call.args.as_slice() else {
7101 return Ok(None);
7102 };
7103 let compiled = self.compile_free_expr(argument)?;
7104 let Some(array_type) = infer_ir_type(&compiled) else {
7105 return Ok(None);
7106 };
7107 Ok(array_type
7108 .strip_suffix("[]")
7109 .map(|element| literal_sentinel_to_pg(element).to_string()))
7110 }
7111
7112 fn compile_for(&mut self, f: &ast::ForStmt) -> Result<IrFor, PyQLError> {
7113 let iterated_binding = self.resolve_cte_name(&f.iterator).map(|name| self.cte_sql_name(name));
7116 let (iterator, pg_type, yielded_object_type) = match &f.iterator {
7118 Expr::Set(elems) => {
7119 let compiled: Result<Vec<_>, _> = elems.iter().map(|e| self.compile_free_expr(e)).collect();
7120 let exprs = compiled?;
7121 let raw = exprs.first().and_then(|e| infer_ir_type(e)).unwrap_or("text");
7122 let pg_type = literal_sentinel_to_pg(raw).to_string();
7123 (
7124 IrForIterator::Values {
7125 exprs,
7126 pg_type: pg_type.clone(),
7127 },
7128 pg_type,
7129 None,
7130 )
7131 }
7132 Expr::SubQuery(stmt) => {
7135 let inner = self.compile_stmt(stmt)?;
7136 if !matches!(inner, IrStmt::Select(_) | IrStmt::PathSelect(_)) {
7137 return Err(self.type_err(
7138 "for-loop iterator: only a select can be iterated over — \
7139 bind the statement in a `with` first",
7140 ));
7141 }
7142 let yielded = cte_stmt_type(&inner);
7143 let scalar = !yielded.contains("::");
7144 let pg_type = if scalar {
7145 let raw = if yielded.is_empty() { "text" } else { yielded.as_str() };
7146 literal_sentinel_to_pg(raw).to_string()
7147 } else {
7148 "uuid".to_string()
7149 };
7150 (
7151 IrForIterator::Query {
7152 stmt: Box::new(inner),
7153 scalar,
7154 },
7155 pg_type,
7156 (!scalar).then_some(yielded),
7157 )
7158 }
7159 Expr::Path(p)
7163 if !p.partial
7164 && p.steps.len() == 1
7165 && matches!(&p.steps[0], ast::PathStep::Name(n) if self.cte_types.contains_key(n.as_str())) =>
7166 {
7167 let ast::PathStep::Name(name) = &p.steps[0] else {
7168 unreachable!("checked by the guard")
7169 };
7170 let yielded = self.cte_types.get(name.as_str()).cloned().unwrap_or_default();
7171 let scalar = !yielded.contains("::");
7172 let pg_type = if scalar {
7173 let raw = if yielded.is_empty() { "text" } else { yielded.as_str() };
7174 literal_sentinel_to_pg(raw).to_string()
7175 } else {
7176 "uuid".to_string()
7177 };
7178 let source = IrSource {
7179 poly: None,
7180 type_name: yielded.clone(),
7181 table: format!("@cte:{}", self.cte_sql_name(name)),
7182 alias: self.fresh_alias(),
7183 };
7184 (
7185 IrForIterator::Query {
7186 stmt: Box::new(IrStmt::Select(IrSelect::schema_bound(source, vec![], None))),
7187 scalar,
7188 },
7189 pg_type,
7190 (!scalar).then_some(yielded),
7191 )
7192 }
7193 Expr::Path(p) if !p.partial && p.steps.len() > 1 => {
7197 let synthetic = ast::SelectStmt {
7198 result: Expr::Path(p.clone()),
7199 filter: None,
7200 order_by: vec![],
7201 offset: None,
7202 limit: None,
7203 lock: None,
7204 };
7205 let inner = self.compile_stmt(&Stmt::Select(synthetic))?;
7206 let yielded = cte_stmt_type(&inner);
7207 let scalar = !yielded.contains("::");
7208 let pg_type = if scalar {
7209 let raw = if yielded.is_empty() { "text" } else { yielded.as_str() };
7210 literal_sentinel_to_pg(raw).to_string()
7211 } else {
7212 "uuid".to_string()
7213 };
7214 (
7215 IrForIterator::Query {
7216 stmt: Box::new(inner),
7217 scalar,
7218 },
7219 pg_type,
7220 (!scalar).then_some(yielded),
7221 )
7222 }
7223 other => {
7224 let e = self.compile_free_expr(other)?;
7225 let raw = match self.set_returning_element_type(other)? {
7229 Some(element) => element,
7230 None => literal_sentinel_to_pg(infer_ir_type(&e).unwrap_or("text")).to_string(),
7231 };
7232 let pg_type = raw;
7233 let iterator = if expr_returns_set(other) {
7234 IrForIterator::SetReturning {
7235 expr: e,
7236 pg_type: pg_type.clone(),
7237 }
7238 } else {
7239 IrForIterator::Values {
7240 exprs: vec![e],
7241 pg_type: pg_type.clone(),
7242 }
7243 };
7244 (iterator, pg_type, None)
7245 }
7246 };
7247
7248 let slot = self
7252 .claim_generated_cte_name(&format!("_for_{}", f.var))
7253 .strip_prefix("_for_")
7254 .unwrap_or(&f.var)
7255 .to_string();
7256 let prev_slot = self.for_var_slots.insert(f.var.clone(), slot.clone());
7257 let prev = self.for_vars.insert(f.var.clone(), pg_type.clone());
7259 let prev_cte = match iterated_binding {
7260 Some(name) => self.for_var_ctes.insert(f.var.clone(), name),
7261 None => self.for_var_ctes.remove(&f.var),
7262 };
7263 let prev_type = match yielded_object_type {
7264 Some(qualified) => self.for_var_types.insert(f.var.clone(), qualified),
7265 None => self.for_var_types.remove(&f.var),
7266 };
7267 let hoisted_before = self.hoisted_ctes.len();
7268 self.for_scope.push(slot.clone());
7269 let body = self.compile_stmt(&f.body);
7270 self.for_scope.pop();
7271 let body = body?;
7272 let body_ctes: Vec<IrCteDef> = self.hoisted_ctes.split_off(hoisted_before);
7273 match prev {
7275 Some(old) => {
7276 self.for_vars.insert(f.var.clone(), old);
7277 }
7278 None => {
7279 self.for_vars.remove(&f.var);
7280 }
7281 }
7282 match prev_type {
7283 Some(old) => {
7284 self.for_var_types.insert(f.var.clone(), old);
7285 }
7286 None => {
7287 self.for_var_types.remove(&f.var);
7288 }
7289 }
7290 match prev_cte {
7291 Some(old) => {
7292 self.for_var_ctes.insert(f.var.clone(), old);
7293 }
7294 None => {
7295 self.for_var_ctes.remove(&f.var);
7296 }
7297 }
7298 match prev_slot {
7299 Some(old) => {
7300 self.for_var_slots.insert(f.var.clone(), old);
7301 }
7302 None => {
7303 self.for_var_slots.remove(&f.var);
7304 }
7305 }
7306
7307 let body_kind = match &body {
7312 IrStmt::Insert(_) | IrStmt::Select(_) | IrStmt::PathSelect(_) => None,
7313 IrStmt::Update(upd)
7321 if upd.assignments.is_empty()
7322 && upd.rewrites.is_empty()
7323 && !upd.multi_link_appends.is_empty()
7324 && upd
7325 .multi_link_appends
7326 .iter()
7327 .all(|a| crate::sql::append_value_is_the_loop_variable(&a.values, &f.var))
7328 && upd.multi_link_clears.is_empty()
7329 && upd.multi_link_replaces.is_empty()
7330 && upd.multi_link_removals.is_empty()
7331 && upd.poly_implementors.is_empty() =>
7332 {
7333 None
7334 }
7335 IrStmt::Update(upd)
7336 if upd.multi_link_appends.is_empty()
7337 && upd.multi_link_clears.is_empty()
7338 && upd.multi_link_replaces.is_empty()
7339 && upd.multi_link_removals.is_empty()
7340 && upd.poly_implementors.is_empty() =>
7341 {
7342 None
7343 }
7344 IrStmt::Update(_) if std::env::var("PYLON_DBG_FORUPD").is_ok() => None,
7345 IrStmt::Update(upd)
7350 if upd.assignments.is_empty()
7351 && upd.rewrites.is_empty()
7352 && !upd.multi_link_appends.is_empty()
7353 && upd.multi_link_clears.is_empty()
7354 && upd.multi_link_replaces.is_empty()
7355 && upd.multi_link_removals.is_empty()
7356 && upd.poly_implementors.is_empty()
7357 && upd
7358 .multi_link_appends
7359 .iter()
7360 .all(|a| crate::sql::per_iteration_insert(a, &upd.nested_ctes).is_some()) =>
7361 {
7362 None
7363 }
7364 IrStmt::Update(_) => Some("update"),
7365 IrStmt::Delete(_) => Some("delete"),
7366 IrStmt::For(inner) if matches!(inner.body.as_ref(), IrStmt::Insert(_)) => None,
7371 IrStmt::For(_) => Some("nested for"),
7372 IrStmt::Group(_) => Some("group"),
7373 _ => Some("this statement"),
7374 };
7375 if let Some(kind) = body_kind {
7376 return Err(self.type_err(&format!(
7377 "for-loop body: {kind} is not supported as a `for` body — use insert or select"
7378 )));
7379 }
7380
7381 Ok(IrFor {
7382 var_name: slot,
7383 iterator,
7384 body: Box::new(body),
7385 body_ctes,
7386 })
7387 }
7388
7389 fn compile_group(&mut self, g: &ast::GroupStmt) -> Result<IrGroup, PyQLError> {
7390 let mut walked: Option<&ast::Path> = None;
7397 let (type_name, cte_name) = match &g.subject {
7398 Expr::Path(p) if !p.partial => match p.steps.as_slice() {
7399 [ast::PathStep::Name(n)] => {
7400 if let Some(t) = self.cte_types.get(n.as_str()) {
7401 (t.clone(), Some(n.clone()))
7402 } else {
7403 (n.clone(), None)
7404 }
7405 }
7406 [ast::PathStep::Name(root), rest @ ..]
7407 if !rest.is_empty()
7408 && let Ok(root_td) = self.resolve_path_root(root)
7409 && let (_, Some(target)) = self.walk_path_types(root_td, rest, MAX_COMPUTED_SPLICES) =>
7410 {
7411 walked = Some(p);
7412 (format!("{}::{}", target.module, target.name), None)
7413 }
7414 _ => {
7415 return Err(PyQLError::Type(PyQLTypeError {
7416 message: format!("unsupported group subject: {:?}", g.subject),
7417 position: Position { line: 0, col: 0 },
7418 }));
7419 }
7420 },
7421 _ => {
7422 return Err(PyQLError::Type(PyQLTypeError {
7423 message: "group subject must be a type name".to_string(),
7424 position: Position { line: 0, col: 0 },
7425 }));
7426 }
7427 };
7428
7429 let td = self.resolve_type(&type_name)?;
7430 let alias = self.fresh_alias();
7431 let fq_type_name = format!("{}::{}", td.module, td.name);
7432 let table = match cte_name {
7433 Some(ref cte) => format!("@cte:{}", cte),
7434 None => td.table.clone(),
7435 };
7436 let source = IrSource {
7437 poly: self.poly_fanout_for(&fq_type_name),
7438 type_name: fq_type_name.clone(),
7439 table,
7440 alias: alias.clone(),
7441 };
7442 let module = td.module.clone();
7443
7444 let shape = self.compile_shape(g.shape.as_deref().unwrap_or(&[]), td, &alias, &module)?;
7446
7447 let td = self.resolve_type(&type_name)?;
7449 let mut using_map: HashMap<String, IrExpr> = HashMap::new();
7450 for (alias_name, expr) in &g.using {
7451 let ir = self.compile_expr(expr, td, &alias)?;
7452 using_map.insert(alias_name.clone(), ir);
7453 }
7454
7455 let mut keys: Vec<(String, IrExpr)> = vec![];
7457 let td = self.resolve_type(&type_name)?;
7458 for by_expr in &g.by {
7459 match by_expr {
7460 Expr::Path(p) if p.partial && p.steps.len() == 1 => {
7461 if let ast::PathStep::Name(prop) = &p.steps[0] {
7462 let ir = self.compile_expr(by_expr, td, &alias)?;
7464 keys.push((prop.clone(), ir));
7465 } else {
7466 return Err(PyQLError::Type(PyQLTypeError {
7467 message: "group by path must be a simple property".to_string(),
7468 position: Position { line: 0, col: 0 },
7469 }));
7470 }
7471 }
7472 Expr::Path(p) if !p.partial && p.steps.len() == 1 => {
7473 if let ast::PathStep::Name(name) = &p.steps[0] {
7474 let ir = using_map.get(name).ok_or_else(|| {
7476 PyQLError::Type(PyQLTypeError {
7477 message: format!("group by references unknown alias '{}'", name),
7478 position: Position { line: 0, col: 0 },
7479 })
7480 })?;
7481 keys.push((name.clone(), ir.clone()));
7482 } else {
7483 return Err(PyQLError::Type(PyQLTypeError {
7484 message: "group by identifier must be a simple name".to_string(),
7485 position: Position { line: 0, col: 0 },
7486 }));
7487 }
7488 }
7489 _ => {
7490 return Err(PyQLError::Type(PyQLTypeError {
7491 message: format!("unsupported group by expression: {:?}", by_expr),
7492 position: Position { line: 0, col: 0 },
7493 }));
7494 }
7495 }
7496 }
7497
7498 let td = self.resolve_type(&type_name)?;
7502 let synthetic = ast::SelectStmt {
7503 result: g.subject.clone(),
7504 filter: g.filter.clone(),
7505 order_by: g.order_by.clone(),
7506 offset: g.offset.clone(),
7507 limit: g.limit.clone(),
7508 lock: None,
7509 };
7510 let (filter, order_by, offset, limit) = self.compile_path_modifiers(&synthetic, td, &alias)?;
7511 let filter = match walked {
7514 Some(subject) => {
7515 let subject = subject.clone();
7516 let rows = self.compile_subject_path_rows(&subject, &alias)?;
7517 and_conditions(filter, vec![rows])
7518 }
7519 None => filter,
7520 };
7521
7522 Ok(IrGroup {
7523 source,
7524 shape,
7525 keys,
7526 filter,
7527 order_by,
7528 offset,
7529 limit,
7530 output: IrGroupOutput::Groups,
7531 })
7532 }
7533
7534 fn bind_group(&mut self, name: &str, expr: &Expr) -> Result<bool, PyQLError> {
7535 let Expr::SubQuery(stmt) = expr else {
7536 return Ok(false);
7537 };
7538 let Stmt::Group(g) = stmt.as_ref() else {
7539 return Ok(false);
7540 };
7541 let grp = self.compile_group(g)?;
7542 self.group_bindings.insert(name.to_string(), grp);
7543 Ok(true)
7544 }
7545
7546 fn compile_group_projection(
7552 &mut self,
7553 sel: &ast::SelectStmt,
7554 g: &ast::GroupStmt,
7555 elements: &[ShapeElement],
7556 ) -> Result<IrGroup, PyQLError> {
7557 if sel.filter.is_some() {
7558 return Err(self.type_err("a shape over a `group` does not support filter"));
7559 }
7560 if !g.order_by.is_empty() || g.offset.is_some() || g.limit.is_some() {
7561 return Err(self.type_err(
7562 "a `group` read through a shape does not support order by, offset or limit on its elements",
7563 ));
7564 }
7565 let mut grp = self.compile_group(g)?;
7566 let keys: Vec<String> = grp.keys.iter().map(|(name, _)| name.clone()).collect();
7570 for (name, key) in &grp.keys {
7571 self.inline_bindings.insert(group_key_binding(name), key.clone());
7572 }
7573 let compiled = self.compile_group_projection_parts(sel, &grp, &keys, elements);
7574 for name in &keys {
7575 self.inline_bindings.remove(&group_key_binding(name));
7576 }
7577 let (pointers, order_by, offset, limit) = compiled?;
7578 grp.output = IrGroupOutput::Projection(Box::new(IrGroupProjection {
7579 pointers,
7580 order_by,
7581 offset,
7582 limit,
7583 }));
7584 Ok(grp)
7585 }
7586
7587 #[allow(clippy::type_complexity)]
7588 fn compile_group_projection_parts(
7589 &mut self,
7590 sel: &ast::SelectStmt,
7591 grp: &IrGroup,
7592 keys: &[String],
7593 elements: &[ShapeElement],
7594 ) -> Result<(Vec<IrShapePointer>, Vec<IrSort>, Option<IrExpr>, Option<IrExpr>), PyQLError> {
7595 let td = self.resolve_type(&grp.source.type_name)?;
7596 let mut rewritten = Vec::with_capacity(elements.len());
7597 for element in elements {
7598 let (Some(compexpr), [ast::PathStep::Name(_)]) = (&element.compexpr, element.path.steps.as_slice()) else {
7599 return Err(self.type_err(
7600 "only computed pointers (`name := …`) can be read off a `group` — e.g. `k := .key.name`",
7601 ));
7602 };
7603 let mut element = element.clone();
7604 element.compexpr = Some(self.rewrite_group_refs(compexpr, keys, td)?);
7605 rewritten.push(element);
7606 }
7607 let alias = grp.source.alias.clone();
7608 let module = td.module.clone();
7609 let pointers = self.without_implicit_id(|this| this.compile_shape(&rewritten, td, &alias, &module))?;
7613 let order_by = sel
7614 .order_by
7615 .iter()
7616 .map(|sort| {
7617 Ok(ast::SortExpr {
7618 expr: self.rewrite_group_refs(&sort.expr, keys, td)?,
7619 direction: sort.direction.clone(),
7620 nones: sort.nones.clone(),
7621 })
7622 })
7623 .collect::<Result<Vec<_>, PyQLError>>()?;
7624 let synthetic = ast::SelectStmt {
7625 result: Expr::Path(ast::Path::absolute(grp.source.type_name.clone())),
7626 filter: None,
7627 order_by,
7628 offset: sel.offset.clone(),
7629 limit: sel.limit.clone(),
7630 lock: None,
7631 };
7632 let (_, order_by, offset, limit) = self.compile_path_modifiers(&synthetic, td, &alias)?;
7633 Ok((pointers, order_by, offset, limit))
7634 }
7635
7636 fn rewrite_group_refs(&self, expr: &Expr, keys: &[String], td: &TypeDescriptor) -> Result<Expr, PyQLError> {
7637 const AGGREGATES: &[&str] = &["count", "sum", "min", "max", "avg", "array_agg", "all", "any"];
7638 let rewrite = |e: &Expr| self.rewrite_group_refs(e, keys, td);
7639 Ok(match expr {
7640 Expr::Path(p) if p.partial => {
7641 let name = match p.steps.first() {
7642 Some(ast::PathStep::Name(name)) => name.as_str(),
7643 _ => "",
7644 };
7645 match (name, p.steps.get(1..).unwrap_or_default()) {
7646 ("key", [ast::PathStep::Name(key)]) if keys.contains(key) => {
7647 Expr::Path(ast::Path::absolute(group_key_binding(key)))
7648 }
7649 ("key", [ast::PathStep::Name(key)]) => {
7650 return Err(self.type_err(&format!("'{key}' is not a key of this group")));
7651 }
7652 ("elements", _) => {
7653 return Err(self.type_err(
7654 "`.elements` of a group can only be read through an aggregate over one \
7655 of their properties — e.g. `count(.elements)`, `sum(.elements.amount)`",
7656 ));
7657 }
7658 _ => {
7659 return Err(self.type_err(&format!(
7660 "a group has no pointer '{name}' — read `.key.<name>` or aggregate over `.elements`"
7661 )));
7662 }
7663 }
7664 }
7665 Expr::FunctionCall(f)
7666 if f.args.len() == 1
7667 && f.kwargs.is_empty()
7668 && f.module.as_deref().is_none_or(|m| m == "std")
7669 && AGGREGATES.contains(&f.name.as_str())
7670 && let Expr::Path(p) = &f.args[0]
7671 && p.partial
7672 && matches!(p.steps.first(), Some(ast::PathStep::Name(n)) if n == "elements") =>
7673 {
7674 let property = match p.steps.get(1..).unwrap_or_default() {
7675 [] => "id",
7676 [ast::PathStep::Name(prop)] if td.properties.iter().any(|d| &d.name == prop) => prop.as_str(),
7677 _ => {
7678 return Err(self.type_err(&format!(
7679 "'{}' over a group's elements only reads one of their own properties",
7680 f.name
7681 )));
7682 }
7683 };
7684 Expr::FunctionCall(ast::FunctionCall {
7685 module: f.module.clone(),
7686 name: f.name.clone(),
7687 args: vec![Expr::Path(ast::Path::relative(property))],
7688 kwargs: vec![],
7689 })
7690 }
7691 Expr::FunctionCall(f) => Expr::FunctionCall(ast::FunctionCall {
7692 module: f.module.clone(),
7693 name: f.name.clone(),
7694 args: f.args.iter().map(rewrite).collect::<Result<_, _>>()?,
7695 kwargs: f
7696 .kwargs
7697 .iter()
7698 .map(|(k, v)| Ok((k.clone(), rewrite(v)?)))
7699 .collect::<Result<_, PyQLError>>()?,
7700 }),
7701 Expr::BinOp(b) => Expr::BinOp(Box::new(ast::BinOp {
7702 left: rewrite(&b.left)?,
7703 op: b.op.clone(),
7704 right: rewrite(&b.right)?,
7705 })),
7706 Expr::UnaryOp(u) => Expr::UnaryOp(Box::new(ast::UnaryOp {
7707 op: u.op.clone(),
7708 operand: rewrite(&u.operand)?,
7709 })),
7710 Expr::TypeCast(c) => Expr::TypeCast(Box::new(ast::TypeCast {
7711 expr: rewrite(&c.expr)?,
7712 ty: c.ty.clone(),
7713 })),
7714 Expr::IfElse(ie) => Expr::IfElse(Box::new(ast::IfElse {
7715 if_expr: rewrite(&ie.if_expr)?,
7716 condition: rewrite(&ie.condition)?,
7717 else_expr: rewrite(&ie.else_expr)?,
7718 })),
7719 other => other.clone(),
7720 })
7721 }
7722
7723 fn compile_group_elements(&mut self, f: &ast::ForStmt) -> Result<Option<IrGroup>, PyQLError> {
7726 let Stmt::Select(body) = f.body.as_ref() else {
7727 return Ok(None);
7728 };
7729 let unmodified =
7730 body.filter.is_none() && body.order_by.is_empty() && body.offset.is_none() && body.limit.is_none();
7731 let (inner, shape) = match &body.result {
7732 Expr::Shape(sh) if unmodified => match &sh.expr {
7733 Some(Expr::SubQuery(stmt)) => match stmt.as_ref() {
7734 Stmt::Select(inner) => (inner, Some(sh.elements.as_slice())),
7735 _ => return Ok(None),
7736 },
7737 _ => return Ok(None),
7738 },
7739 _ => (body, None),
7740 };
7741 let reads_elements = matches!(&inner.result, Expr::Path(p) if !p.partial
7742 && matches!(p.steps.as_slice(), [ast::PathStep::Name(var), ast::PathStep::Name(elements)]
7743 if var == &f.var && elements == "elements"));
7744 if !reads_elements {
7745 return Ok(None);
7746 }
7747 let mut grp = match &f.iterator {
7748 Expr::SubQuery(stmt) => match stmt.as_ref() {
7749 Stmt::Group(g) => self.compile_group(g)?,
7750 _ => return Ok(None),
7751 },
7752 Expr::Path(p) if !p.partial => match p.steps.as_slice() {
7753 [ast::PathStep::Name(name)] => match self.group_bindings.get(name) {
7754 Some(grp) => grp.clone(),
7755 None => return Ok(None),
7756 },
7757 _ => return Ok(None),
7758 },
7759 _ => return Ok(None),
7760 };
7761 let ordered = !inner.order_by.is_empty() || inner.offset.is_some() || inner.limit.is_some();
7762 if ordered && (!grp.order_by.is_empty() || grp.offset.is_some() || grp.limit.is_some()) {
7763 return Err(self.type_err(
7764 "a group that already orders or limits its elements cannot be ordered or limited again by a `for` over it",
7765 ));
7766 }
7767 let td = self.resolve_type(&grp.source.type_name)?;
7768 let alias = grp.source.alias.clone();
7769 let synthetic = ast::SelectStmt {
7770 result: Expr::Path(ast::Path::absolute(grp.source.type_name.clone())),
7771 ..inner.clone()
7772 };
7773 let (filter, order_by, offset, limit) = self.compile_path_modifiers(&synthetic, td, &alias)?;
7774 grp.filter = and_conditions(grp.filter, filter.into_iter().collect());
7775 if ordered {
7776 grp.order_by = order_by;
7777 grp.offset = offset;
7778 grp.limit = limit;
7779 }
7780 if let Some(shape) = shape {
7781 let module = td.module.clone();
7782 grp.shape = self.compile_shape(shape, td, &alias, &module)?;
7783 }
7784 grp.output = IrGroupOutput::Elements;
7785 Ok(Some(grp))
7786 }
7787
7788 fn compile_link_key(&mut self, expr: &Expr, td: &TypeDescriptor, alias: &str) -> Result<Option<IrExpr>, PyQLError> {
7792 let value = match expr {
7796 Expr::Set(elements) if elements.len() == 1 => &elements[0],
7797 other => other,
7798 };
7799 match value {
7800 Expr::SubQuery(inner_stmt)
7803 if matches!(inner_stmt.as_ref(), Stmt::Select(sel) if matches!(sel.result, Expr::Set(_))) =>
7804 {
7805 let IrStmt::Select(select) = self.compile_stmt(inner_stmt)? else {
7806 return Ok(None);
7807 };
7808 if !select.rows.iter().all(|r| matches!(r, IrRowSource::Free(IrFreeExpr::Scalar(_)))) {
7809 return Ok(None);
7810 }
7811 Ok(Some(IrExpr::ScalarSubquery(Box::new(select))))
7812 }
7813 Expr::SubQuery(inner_stmt) => self.compile_link_subquery(inner_stmt).map(Some),
7816 Expr::FunctionCall(fc) => self.try_compile_fn_scalar_subquery(fc, &["id".to_string()], None, None),
7821 Expr::IfElse(ie)
7825 if [&ie.if_expr, &ie.else_expr].iter().any(|b| {
7826 matches!(b, Expr::SubQuery(s) if matches!(s.as_ref(), Stmt::Insert(_) | Stmt::Update(_) | Stmt::Delete(_)))
7827 }) =>
7828 {
7829 let Some(rewritten) = self.object_if_else_as_union(value) else {
7830 return Ok(None);
7831 };
7832 self.compile_link_key(&rewritten, td, alias)
7833 }
7834 Expr::Union(a, b) => {
7835 let (Some(left), Some(right)) = (self.compile_link_key(a, td, alias)?, self.compile_link_key(b, td, alias)?)
7836 else {
7837 return Ok(None);
7838 };
7839 let hoisted = |key: IrExpr| match key {
7843 IrExpr::ColumnRef { alias, column, .. } if column == "id" => IrExpr::CteRef {
7844 name: alias,
7845 scalar: false,
7846 pg_type: None,
7847 },
7848 other => other,
7849 };
7850 Ok(Some(IrExpr::FunctionCall(IrFunctionCall {
7851 return_pg_type: None,
7852 schema: None,
7853 name: "coalesce".to_string(),
7854 args: vec![hoisted(left), hoisted(right)],
7855 sql_template: None,
7856 })))
7857 }
7858 Expr::IfElse(ie) => {
7861 if ![&ie.if_expr, &ie.else_expr].iter().any(|b| matches!(b, Expr::FunctionCall(_))) {
7862 return Ok(None);
7863 }
7864 Ok(Some(IrExpr::IfElse(Box::new(IrIfElse {
7865 condition: self.compile_expr(&ie.condition, td, alias)?,
7866 if_: self.compile_link_branch(&ie.if_expr, td, alias)?,
7867 else_: self.compile_link_branch(&ie.else_expr, td, alias)?,
7868 }))))
7869 }
7870 _ => Ok(None),
7871 }
7872 }
7873
7874 fn compile_link_branch(&mut self, branch: &Expr, td: &TypeDescriptor, alias: &str) -> Result<IrExpr, PyQLError> {
7875 if matches!(branch, Expr::Set(elements) if elements.is_empty()) {
7876 return Ok(IrExpr::Null);
7877 }
7878 match self.compile_link_key(branch, td, alias)? {
7879 Some(key) => Ok(key),
7880 None => self.compile_expr(branch, td, alias),
7881 }
7882 }
7883
7884 fn compile_shape_field_select(
7888 &mut self,
7889 expr: &Expr,
7890 ctx: Option<(&TypeDescriptor, &str)>,
7891 ) -> Result<Option<IrPathSelect>, PyQLError> {
7892 let Expr::FieldAccess { expr: inner, field } = expr else {
7893 return Ok(None);
7894 };
7895 let Expr::Shape(sh) = inner.as_ref() else {
7896 return Ok(None);
7897 };
7898 let Some(Expr::Path(base)) = sh.expr.as_ref() else {
7899 return Ok(None);
7900 };
7901 let Some(value) = sh
7902 .elements
7903 .iter()
7904 .find(|el| matches!(el.path.steps.as_slice(), [ast::PathStep::Name(n)] if n == field))
7905 .and_then(|el| el.compexpr.clone())
7906 else {
7907 return Ok(None);
7908 };
7909 let (rooted, correlate) = if base.partial {
7910 let Some((td, alias)) = ctx else {
7911 return Ok(None);
7912 };
7913 let mut steps = vec![ast::PathStep::Name(format!("{}::{}", td.module, td.name))];
7914 steps.extend(base.steps.iter().cloned());
7915 (ast::Path { steps, partial: false }, Some(alias.to_string()))
7916 } else if base.steps.len() > 1 {
7917 (base.clone(), None)
7918 } else if let [ast::PathStep::Name(root)] = base.steps.as_slice()
7919 && let Ok(root_td) = self.resolve_path_root(root)
7920 {
7921 let narrowing = ast::PathStep::TypeIntersection(ast::ObjectRef {
7923 module: Some(root_td.module.clone()),
7924 name: root_td.name.clone(),
7925 });
7926 (
7927 ast::Path {
7928 steps: vec![base.steps[0].clone(), narrowing],
7929 partial: false,
7930 },
7931 None,
7932 )
7933 } else {
7934 return Ok(None);
7935 };
7936 let synthetic = ast::SelectStmt {
7937 result: Expr::Path(rooted.clone()),
7938 filter: None,
7939 order_by: vec![],
7940 offset: None,
7941 limit: None,
7942 lock: None,
7943 };
7944 let mut ps = self.compile_path_select(&synthetic, &rooted, &[], false)?;
7945 if let Some(alias) = correlate {
7946 Self::correlate_path_select(&mut ps, &alias);
7947 }
7948 let IrPathResult::Object { alias, type_name, .. } = &ps.result else {
7949 return Err(self.type_err(&format!(
7950 "'{field}' is read off a walk that lands on a value, which has no shape"
7951 )));
7952 };
7953 let (alias, type_name) = (alias.clone(), type_name.clone());
7954 let td = self.resolve_type(&type_name)?;
7955 let value = self.compile_expr(&value, td, &alias)?;
7956 let value = if yields_array(&value) {
7959 IrExpr::FunctionCall(IrFunctionCall {
7960 return_pg_type: None,
7961 schema: None,
7962 name: "unnest".to_string(),
7963 args: vec![value],
7964 sql_template: None,
7965 })
7966 } else {
7967 value
7968 };
7969 ps.result = IrPathResult::Scalar(value, None);
7970 Ok(Some(ps))
7971 }
7972
7973 fn is_scalar_walk(&self, expr: &Expr) -> bool {
7976 let Expr::Path(p) = expr else { return false };
7977 let (Some(ast::PathStep::Name(root)), false) = (p.steps.first(), p.partial) else {
7978 return false;
7979 };
7980 let (Some(ast::PathStep::Name(leaf)), [_, middle @ .., _]) = (p.steps.last(), p.steps.as_slice()) else {
7981 return false;
7982 };
7983 let Ok(root_td) = self.resolve_path_root(root) else {
7984 return false;
7985 };
7986 let owner = if middle.is_empty() {
7987 Some(root_td)
7988 } else {
7989 self.walk_path_types(root_td, middle, MAX_COMPUTED_SPLICES).1
7990 };
7991 owner.is_some_and(|td| Self::resolve_property(td, leaf).is_some())
7992 }
7993
7994 fn compile_scalar_union(&mut self, expr: &Expr) -> Result<IrStmt, PyQLError> {
7997 fn operands<'e>(expr: &'e Expr, out: &mut Vec<&'e Expr>) {
7998 match expr {
7999 Expr::Union(a, b) => {
8000 operands(a, out);
8001 operands(b, out);
8002 }
8003 other => out.push(other),
8004 }
8005 }
8006 let mut exprs = vec![];
8007 operands(expr, &mut exprs);
8008 let mut branches = Vec::with_capacity(exprs.len());
8009 for operand in exprs {
8010 let stmt = match operand {
8011 Expr::SubQuery(stmt) => stmt.as_ref().clone(),
8012 other => Stmt::Select(ast::SelectStmt {
8013 result: other.clone(),
8014 filter: None,
8015 order_by: vec![],
8016 offset: None,
8017 limit: None,
8018 lock: None,
8019 }),
8020 };
8021 branches.push(self.compile_stmt(&stmt)?);
8022 }
8023 let types: Vec<String> = branches.iter().map(cte_stmt_type).collect();
8024 let first = types.iter().find(|t| !t.is_empty()).cloned().unwrap_or_default();
8025 if let Some(other) = types
8026 .iter()
8027 .find(|t| t.contains("::") || (!t.is_empty() && **t != first))
8028 {
8029 let display = |t: &str| {
8030 if t.contains("::") {
8031 t.to_string()
8032 } else {
8033 pg_type_to_pyql(t).to_string()
8034 }
8035 };
8036 return Err(self.type_err(&format!(
8037 "operator 'UNION' cannot be applied to operands of type '{}' and '{}'",
8038 display(&first),
8039 display(other)
8040 )));
8041 }
8042 Ok(IrStmt::ScalarUnion(branches))
8043 }
8044
8045 fn dml_subject_type(&self, stmt: &Stmt) -> Result<String, PyQLError> {
8047 match stmt {
8048 Stmt::Insert(ins) => Ok(ins.subject.qualified_name()),
8049 Stmt::Update(upd) => self.subject_type_name(&upd.subject),
8050 Stmt::Delete(del) => self.subject_type_name(&del.subject),
8051 Stmt::With(w) => self.dml_subject_type(&w.stmt),
8052 Stmt::For(f) => self.dml_subject_type(&f.body),
8053 Stmt::Analyze(inner) => self.dml_subject_type(inner),
8054 Stmt::Group(g) => self.expr_as_type_name(&g.subject),
8055 Stmt::Select(sel) => {
8056 if let Expr::TypeCast(tc) = &sel.result
8058 && let Some((module, name)) = tc.ty.as_named()
8059 && module.map(|m| m != "std").unwrap_or(false)
8060 {
8061 return Ok(name.to_string());
8062 }
8063 if let Expr::Path(path) = &sel.result
8066 && !path.partial
8067 && path.steps.len() > 1
8068 && let Some(ast::PathStep::Name(root)) = path.steps.first()
8069 && let Ok(root_td) = self.resolve_path_root(root)
8070 && let (_, Some(target)) = self.walk_path_types(root_td, &path.steps[1..], MAX_COMPUTED_SPLICES)
8071 {
8072 return Ok(format!("{}::{}", target.module, target.name));
8073 }
8074 let (type_name, _, _, _) = self.extract_type_and_shape(&sel.result)?;
8076 Ok(type_name)
8077 }
8078 }
8079 }
8080
8081 fn compile_subject_path_rows(&mut self, subject: &ast::Path, target_alias: &str) -> Result<IrExpr, PyQLError> {
8084 let mut steps = subject.steps.clone();
8088 steps.push(ast::PathStep::Name("id".to_string()));
8089 let ids = ast::Path { steps, partial: false };
8090 let synthetic = ast::SelectStmt {
8091 result: Expr::Path(ids.clone()),
8092 filter: None,
8093 order_by: vec![],
8094 offset: None,
8095 limit: None,
8096 lock: None,
8097 };
8098 let rows = self.compile_path_select(&synthetic, &ids, &[], false)?;
8099 Ok(IrExpr::BinOp(Box::new(IrBinOp {
8100 left: IrExpr::ColumnRef {
8101 alias: target_alias.to_string(),
8102 column: "id".to_string(),
8103 pg_type: "uuid".to_string(),
8104 },
8105 op: ast::BinOpKind::In,
8106 right: IrExpr::ArrayFromSelect(Box::new(IrArraySource::PathSelect(Box::new(rows)))),
8107 })))
8108 }
8109
8110 fn subject_type_name(&self, subject: &Expr) -> Result<String, PyQLError> {
8113 if let Expr::Path(p) = subject
8114 && p.steps.len() > 1
8115 && let Some(ast::PathStep::Name(root)) = p.steps.first()
8116 && let Ok(root_td) = self.resolve_path_root(root)
8117 && let (_, Some(target)) = self.walk_path_types(root_td, &p.steps[1..], MAX_COMPUTED_SPLICES)
8118 {
8119 return Ok(format!("{}::{}", target.module, target.name));
8120 }
8121 if let Expr::Path(p) = subject
8126 && !p.partial
8127 && let [ast::PathStep::Name(root)] = p.steps.as_slice()
8128 && let Some(bound) = self.cte_object_type(root)
8129 {
8130 return Ok(bound);
8131 }
8132 self.expr_as_type_name(subject)
8133 }
8134
8135 fn expr_as_type_name(&self, expr: &Expr) -> Result<String, PyQLError> {
8136 if std::env::var("PYLON_DBG_SUBJ").is_ok() {
8137 eprintln!(
8138 "DBG subj {:.90?}
8139{}",
8140 expr,
8141 std::backtrace::Backtrace::force_capture()
8142 );
8143 }
8144 match expr {
8145 Expr::Detached(inner) => self.expr_as_type_name(inner),
8148 Expr::Path(p) if !p.partial && p.steps.len() == 1 => {
8149 if let ast::PathStep::Name(n) = &p.steps[0] {
8150 return Ok(n.clone());
8151 }
8152 Err(self.type_err("expected a type name"))
8153 }
8154 Expr::Union(_, _) => {
8157 let branches = self
8158 .object_union_branches(expr)
8159 .ok_or_else(|| self.type_err("expected a type name as SELECT subject"))?;
8160 self.common_union_type(&branches)
8161 .ok_or_else(|| self.type_err("expected a type name as SELECT subject"))
8162 }
8163 Expr::Path(p)
8167 if !p.partial
8168 && p.steps.len() > 1
8169 && let Some(ast::PathStep::Name(root)) = p.steps.first()
8170 && let Ok(root_td) = self.resolve_path_root(root)
8171 && let (_, Some(target)) = self.walk_path_types(root_td, &p.steps[1..], MAX_COMPUTED_SPLICES) =>
8172 {
8173 Ok(format!("{}::{}", target.module, target.name))
8174 }
8175 _ => Err(self.type_err("expected a type name as SELECT subject")),
8176 }
8177 }
8178
8179 fn compile_insert(&mut self, ins: &ast::InsertStmt) -> Result<IrInsert, PyQLError> {
8182 let outer_pending_nested_ctes = std::mem::take(&mut self.pending_nested_ctes);
8188 let type_name = ins.subject.qualified_name();
8189 let td = self.resolve_type(&type_name)?;
8190 if td.abstract_ && td.materialized {
8191 return Err(self.type_err(&format!(
8192 "cannot insert into interface type '{}::{}'; insert into a concrete type instead",
8193 td.module, td.name
8194 )));
8195 }
8196 let alias = self.fresh_alias();
8197 let target = IrSource {
8198 poly: None,
8199 type_name: format!("{}::{}", td.module, td.name),
8200 table: td.table.clone(),
8201 alias: alias.clone(),
8202 };
8203
8204 let mut shape = ins.shape.clone();
8208 for link in &td.links {
8209 let unset = !shape.iter().any(|el| path_leaf(&el.path).is_ok_and(|n| n == link.name));
8210 for rw in link.rewrites.iter().filter(|rw| rw.on & 1 != 0) {
8211 if let Some(value) = subject_default_insert(&rw.handler, &link.name)
8212 && unset
8213 {
8214 shape.push(ShapeElement {
8215 path: ast::Path::relative(&link.name),
8216 splat: None,
8217 nested: None,
8218 compexpr: Some(value),
8219 op: ShapeOp::Assign,
8220 filter: None,
8221 order_by: vec![],
8222 offset: None,
8223 limit: None,
8224 marker_offset: None,
8225 });
8226 }
8227 }
8228 }
8229 for (pointer, pyql) in inlined_pointer_defaults(td, self.schema) {
8235 if shape.iter().any(|el| path_leaf(&el.path).is_ok_and(|n| n == pointer)) {
8236 continue;
8237 }
8238 let value = crate::parse::parse_pointer_expr(&pyql)?;
8239 shape.push(default_shape_element(&pointer, value));
8240 }
8241 let mut multi_link_appends = vec![];
8242 let mut scalar_elements: Vec<ShapeElement> = vec![];
8243 for el in &shape {
8244 let pointer_name = match path_leaf(&el.path) {
8245 Ok(n) => n,
8246 Err(_) => {
8247 scalar_elements.push(el.clone());
8248 continue;
8249 }
8250 };
8251 if let Some(ml) = Self::resolve_multilink(td, pointer_name) {
8252 match el.op {
8253 ShapeOp::Remove => {
8254 return Err(self.type_err(&format!(
8255 "cannot use `-=` for multi-link '{pointer_name}' in an insert; \
8256 there is nothing to remove from yet"
8257 )));
8258 }
8259 ShapeOp::Assign | ShapeOp::Append => {
8260 if let Some(expr) = &el.compexpr {
8261 let (jt, module, src_col, tgt_col, through_td) =
8262 self.own_multilink_junction_info(td, ml)?;
8263 let values = self.compile_multilink_values(expr, td, &alias, through_td)?;
8264 multi_link_appends.push(IrMultiLinkMutation {
8265 junction_table: jt,
8266 module,
8267 source_col: src_col,
8268 target_col: tgt_col,
8269 values,
8270 single: false,
8271 });
8272 }
8273 }
8274 }
8275 } else if let Some(l) = Self::resolve_link(td, pointer_name).filter(|l| l.is_junction_backed()) {
8276 if matches!(el.op, ShapeOp::Remove) {
8281 return Err(self.type_err(&format!(
8282 "cannot use `-=` for link '{pointer_name}' in an insert; \
8283 there is nothing to remove from yet"
8284 )));
8285 }
8286 if let Some(expr) = &el.compexpr {
8287 if !is_empty_set_expr(expr) {
8291 let (jt, module, src_col, tgt_col, through_td) = self.own_link_junction_info(td, l)?;
8292 let values = self.compile_multilink_values(expr, td, &alias, through_td)?;
8293 multi_link_appends.push(IrMultiLinkMutation {
8294 junction_table: jt,
8295 module,
8296 source_col: src_col,
8297 target_col: tgt_col,
8298 values,
8299 single: true,
8300 });
8301 }
8302 }
8303 } else {
8304 scalar_elements.push(el.clone());
8305 }
8306 }
8307
8308 let assignments = self.compile_assignments(&scalar_elements, td, &alias)?;
8309 let rewrites = vec![];
8312 let unless_conflict = match ins.unless_conflict.as_ref() {
8313 Some(uc) => {
8314 let (conflict, else_appends) = self.compile_conflict(uc, td)?;
8315 multi_link_appends.extend(else_appends);
8316 Some(conflict)
8317 }
8318 None => None,
8319 };
8320 let returning = Self::pk_returning(td);
8321 let type_name = format!("{}::{}", td.module, td.name);
8322 let enqueue_vector = td
8323 .vector_indexes
8324 .iter()
8325 .map(|vi| VectorEnqueueInfo {
8326 type_name: type_name.clone(),
8327 index_name: vi.index_name.clone(),
8328 })
8329 .collect();
8330 let enqueue_search = collect_search_enqueue(td, &type_name, "index");
8331 let nested_ctes = std::mem::replace(&mut self.pending_nested_ctes, outer_pending_nested_ctes);
8332
8333 let guard = match self.pending_insert_guard.take() {
8334 Some(condition) => Some(self.compile_expr(&condition, td, &alias)?),
8335 None => None,
8336 };
8337 let id_default_sql =
8341 Self::resolve_property(td, "id").map(|p| p.default_sql.clone().unwrap_or_else(|| "uuidv7()".to_string()));
8342 Ok(IrInsert {
8343 guard,
8344 target,
8345 assignments,
8346 unless_conflict,
8347 rewrites,
8348 id_default_sql,
8349 returning,
8350 enqueue_vector,
8351 enqueue_search,
8352 multi_link_appends,
8353 nested_ctes,
8354 })
8355 }
8356
8357 fn compile_assignments(
8358 &mut self,
8359 elements: &[ShapeElement],
8360 td: &TypeDescriptor,
8361 alias: &str,
8362 ) -> Result<Vec<(String, IrExpr)>, PyQLError> {
8363 self.compile_assignments_inner(elements, td, alias, false)
8364 }
8365
8366 fn compile_assignments_for_update(
8367 &mut self,
8368 elements: &[ShapeElement],
8369 td: &TypeDescriptor,
8370 alias: &str,
8371 ) -> Result<Vec<(String, IrExpr)>, PyQLError> {
8372 self.compile_assignments_inner(elements, td, alias, true)
8373 }
8374
8375 fn compile_assignments_inner(
8376 &mut self,
8377 elements: &[ShapeElement],
8378 td: &TypeDescriptor,
8379 alias: &str,
8380 deny_readonly: bool,
8381 ) -> Result<Vec<(String, IrExpr)>, PyQLError> {
8382 self.without_implicit_id(|this| this.compile_assignments_rows(elements, td, alias, deny_readonly))
8386 }
8387
8388 fn compile_assignments_rows(
8389 &mut self,
8390 elements: &[ShapeElement],
8391 td: &TypeDescriptor,
8392 alias: &str,
8393 deny_readonly: bool,
8394 ) -> Result<Vec<(String, IrExpr)>, PyQLError> {
8395 elements
8396 .iter()
8397 .map(|el| {
8398 let pointer_name = path_leaf(&el.path)?;
8399 let expr = el.compexpr.as_ref().ok_or_else(|| {
8400 PyQLError::Type(PyQLTypeError {
8401 message: format!("INSERT pointer '{pointer_name}' has no value expression"),
8402 position: Position { line: 0, col: 0 },
8403 })
8404 })?;
8405
8406 let column = if let Some(p) = Self::resolve_property(td, pointer_name) {
8408 if p.is_pk && (deny_readonly || !self.config.allow_user_specified_id) {
8413 return Err(PyQLError::Type(PyQLTypeError {
8414 message: "cannot assign to property 'id'".to_string(),
8415 position: Position { line: 0, col: 0 },
8416 }));
8417 }
8418 if deny_readonly && p.is_readonly {
8419 return Err(PyQLError::Type(PyQLTypeError {
8420 message: format!("cannot update property '{pointer_name}': it is declared as read-only"),
8421 position: Position { line: 0, col: 0 },
8422 }));
8423 }
8424 p.name.clone()
8425 } else if let Some(l) = Self::resolve_link(td, pointer_name) {
8426 if deny_readonly && l.is_readonly {
8427 return Err(PyQLError::Type(PyQLTypeError {
8428 message: format!("cannot update link '{pointer_name}': it is declared as read-only"),
8429 position: Position { line: 0, col: 0 },
8430 }));
8431 }
8432 if l.is_junction_backed() {
8433 return Err(PyQLError::Type(PyQLTypeError {
8440 message: format!(
8441 "'{pointer_name}' is a junction-backed link and cannot be \
8442 assigned inside an UNLESS CONFLICT ELSE clause"
8443 ),
8444 position: Position { line: 0, col: 0 },
8445 }));
8446 }
8447 let fk_col = format!("{}_id", l.name);
8453 if let Some(ir_expr) = self.compile_link_key(expr, td, alias)? {
8454 return Ok((fk_col, ir_expr));
8455 }
8456 fk_col
8457 } else if Self::resolve_multilink(td, pointer_name).is_some() {
8458 return Err(PyQLError::Type(PyQLTypeError {
8466 message: format!(
8467 "'{pointer_name}' is a multi-link and cannot be mutated inside an \
8468 UNLESS CONFLICT ELSE clause"
8469 ),
8470 position: Position { line: 0, col: 0 },
8471 }));
8472 } else if Self::resolve_multilink(td, pointer_name).is_some() {
8473 return Err(self.field_err(pointer_name, &format!("{}::{}", td.module, td.name)));
8474 } else if self.resolve_computed(td, pointer_name).is_some() {
8475 return Err(self.type_err(&format!(
8480 "cannot assign to '{pointer_name}': it is a computed pointer on \
8481 {}::{}, which has no stored column to write",
8482 td.module, td.name
8483 )));
8484 } else {
8485 return Err(self.field_err(pointer_name, &format!("{}::{}", td.module, td.name)));
8486 };
8487
8488 let ir_expr = if matches!(expr, Expr::Set(v) if v.is_empty()) {
8490 IrExpr::Null
8491 } else {
8492 self.compile_expr(expr, td, alias)?
8493 };
8494 Ok((column, ir_expr))
8495 })
8496 .collect()
8497 }
8498
8499 fn compile_update(&mut self, upd: &ast::UpdateStmt) -> Result<IrUpdate, PyQLError> {
8502 let pending_guard = self.pending_update_guard.take();
8505 let outer_pending_nested_ctes = std::mem::take(&mut self.pending_nested_ctes);
8507 if !matches!(&upd.subject, Expr::Path(_)) {
8513 let (base, fields) = Self::peel_field_access_chain(&upd.subject);
8514 if let Expr::SubQuery(inner_stmt) = base
8515 && !fields.is_empty()
8516 && matches!(inner_stmt.as_ref(), Stmt::Select(_))
8517 {
8518 let inner = self.compile_stmt(inner_stmt)?;
8519 let cte_name = self.fresh_nested_cte_name();
8520 let type_name = self.register_cte(&cte_name, &inner);
8521 self.hoisted_ctes.push(IrCteDef {
8522 name: cte_name.clone(),
8523 stmt: inner,
8524 type_name,
8525 correlated_to: None,
8526 });
8527 let mut steps = vec![ast::PathStep::Name(cte_name)];
8528 steps.extend(fields.into_iter().map(ast::PathStep::Name));
8529 let rerooted = ast::UpdateStmt {
8530 subject: Expr::Path(ast::Path { steps, partial: false }),
8531 filter: upd.filter.clone(),
8532 shape: upd.shape.clone(),
8533 };
8534 self.pending_nested_ctes = outer_pending_nested_ctes;
8535 self.pending_update_guard = pending_guard;
8536 return self.compile_update(&rerooted);
8537 }
8538 }
8539 if let Expr::Path(subject) = &upd.subject
8544 && subject.steps.len() > 1
8545 && let Some(ast::PathStep::Name(root)) = subject.steps.first()
8546 && let Ok(root_td) = self.resolve_path_root(root)
8547 && let (_, Some(target_td)) = self.walk_path_types(root_td, &subject.steps[1..], MAX_COMPUTED_SPLICES)
8548 {
8549 let narrowed = ast::UpdateStmt {
8550 subject: Expr::Path(ast::Path {
8551 steps: vec![ast::PathStep::Name(format!("{}::{}", target_td.module, target_td.name))],
8552 partial: false,
8553 }),
8554 filter: upd.filter.clone(),
8555 shape: upd.shape.clone(),
8556 };
8557 self.pending_nested_ctes = outer_pending_nested_ctes;
8558 self.pending_update_guard = pending_guard;
8559 let mut ir = self.compile_update(&narrowed)?;
8560 let rows = self.compile_subject_path_rows(subject, &ir.target.alias)?;
8564 ir.filter = Some(and_conditions(ir.filter.take(), vec![rows]).expect("row set is present"));
8565 return Ok(ir);
8566 }
8567 let type_name = self.expr_as_type_name(&upd.subject)?;
8568 let bound_rows = self.cte_object_type(&type_name);
8573 let iterated = self
8577 .for_var_types
8578 .contains_key(&type_name)
8579 .then(|| self.for_var_ref(&type_name));
8580 let td = self.resolve_path_root(&type_name)?;
8581 let alias = self.fresh_alias();
8582 let target = IrSource {
8583 poly: None,
8584 type_name: format!("{}::{}", td.module, td.name),
8585 table: td.table.clone(),
8586 alias: alias.clone(),
8587 };
8588
8589 let declared_filter = upd
8590 .filter
8591 .as_ref()
8592 .map(|f| self.compile_expr(f, td, &alias))
8593 .transpose()?;
8594 let filter = match bound_rows {
8595 Some(_) => {
8596 let membership = IrExpr::BinOp(Box::new(IrBinOp {
8597 left: IrExpr::ColumnRef {
8598 alias: alias.clone(),
8599 column: "id".to_string(),
8600 pg_type: "uuid".to_string(),
8601 },
8602 op: ast::BinOpKind::In,
8603 right: IrExpr::ArrayFromSelect(Box::new(IrArraySource::Select(IrSelect::schema_bound(
8604 IrSource {
8605 poly: None,
8606 type_name: format!("{}::{}", td.module, td.name),
8607 table: format!("@cte:{type_name}"),
8608 alias: self.fresh_alias(),
8609 },
8610 vec![],
8611 None,
8612 )))),
8613 }));
8614 Some(and_conditions(declared_filter, vec![membership]).expect("membership is present"))
8615 }
8616 None => declared_filter,
8617 };
8618 let filter = match iterated {
8619 Some(value) => {
8620 let this_row = IrExpr::BinOp(Box::new(IrBinOp {
8621 left: IrExpr::ColumnRef {
8622 alias: alias.clone(),
8623 column: "id".to_string(),
8624 pg_type: "uuid".to_string(),
8625 },
8626 op: ast::BinOpKind::Eq,
8627 right: value,
8628 }));
8629 Some(and_conditions(filter, vec![this_row]).expect("the row condition is present"))
8630 }
8631 None => filter,
8632 };
8633 let filter = match pending_guard {
8637 Some(condition) => {
8638 let guard = self.compile_expr(&condition, td, &alias)?;
8639 and_conditions(filter, vec![guard])
8640 }
8641 None => filter,
8642 };
8643
8644 let mut multi_link_clears = vec![];
8646 let mut multi_link_replaces = vec![];
8647 let mut multi_link_appends = vec![];
8648 let mut multi_link_removals = vec![];
8649 let mut scalar_elements: Vec<ShapeElement> = vec![];
8650
8651 for el in &upd.shape {
8652 let pointer_name = match path_leaf(&el.path) {
8653 Ok(n) => n,
8654 Err(_) => {
8655 scalar_elements.push(el.clone());
8656 continue;
8657 }
8658 };
8659
8660 if let Some(ml) = Self::resolve_multilink(td, pointer_name) {
8661 let (jt, module, src_col, tgt_col, through_td) = self.own_multilink_junction_info(td, ml)?;
8662
8663 match el.op {
8664 ShapeOp::Assign => {
8665 let is_empty = el
8666 .compexpr
8667 .as_ref()
8668 .map(|e| matches!(e, Expr::Set(v) if v.is_empty()))
8669 .unwrap_or(false);
8670 if is_empty {
8671 multi_link_clears.push(IrMultiLinkClear {
8673 junction_table: jt,
8674 module,
8675 source_col: src_col,
8676 });
8677 } else if let Some(expr) = &el.compexpr {
8678 multi_link_clears.push(IrMultiLinkClear {
8680 junction_table: jt.clone(),
8681 module: module.clone(),
8682 source_col: src_col.clone(),
8683 });
8684 let values = self.compile_multilink_values(expr, td, &alias, through_td)?;
8685 multi_link_replaces.push(IrMultiLinkMutation {
8686 junction_table: jt,
8687 module,
8688 source_col: src_col,
8689 target_col: tgt_col,
8690 values,
8691 single: false,
8692 });
8693 }
8694 }
8695 ShapeOp::Append => {
8696 if let Some(expr) = &el.compexpr {
8697 let values = self.compile_multilink_values(expr, td, &alias, through_td)?;
8698 multi_link_appends.push(IrMultiLinkMutation {
8699 junction_table: jt,
8700 module,
8701 source_col: src_col,
8702 target_col: tgt_col,
8703 values,
8704 single: false,
8705 });
8706 }
8707 }
8708 ShapeOp::Remove => {
8709 if let Some(expr) = &el.compexpr {
8710 let values = self.compile_multilink_values(expr, td, &alias, through_td)?;
8711 if has_any_link_props(&values) {
8712 return Err(self.type_err(
8713 "link properties (`@prop := value`) cannot be assigned \
8714 when removing a link (`-=`)",
8715 ));
8716 }
8717 multi_link_removals.push(IrMultiLinkMutation {
8718 junction_table: jt,
8719 module,
8720 source_col: src_col,
8721 target_col: tgt_col,
8722 values,
8723 single: false,
8724 });
8725 }
8726 }
8727 }
8728 } else if let Some(l) = Self::resolve_link(td, pointer_name).filter(|l| l.is_junction_backed()) {
8729 if !matches!(el.op, ShapeOp::Assign) {
8733 return Err(self.type_err(&format!(
8734 "'{pointer_name}' is a single link; only `:=` is supported, not `+=`/`-=`"
8735 )));
8736 }
8737 let (jt, module, src_col, tgt_col, through_td) = self.own_link_junction_info(td, l)?;
8738 let is_empty = el.compexpr.as_ref().map(is_empty_set_expr).unwrap_or(false);
8739 if is_empty {
8740 multi_link_clears.push(IrMultiLinkClear {
8742 junction_table: jt,
8743 module,
8744 source_col: src_col,
8745 });
8746 } else if let Some(expr) = &el.compexpr {
8747 multi_link_clears.push(IrMultiLinkClear {
8748 junction_table: jt.clone(),
8749 module: module.clone(),
8750 source_col: src_col.clone(),
8751 });
8752 let values = self.compile_multilink_values(expr, td, &alias, through_td)?;
8753 multi_link_replaces.push(IrMultiLinkMutation {
8754 junction_table: jt,
8755 module,
8756 source_col: src_col,
8757 target_col: tgt_col,
8758 values,
8759 single: true,
8760 });
8761 }
8762 } else {
8763 scalar_elements.push(el.clone());
8764 }
8765 }
8766
8767 let assignments = self.compile_assignments_for_update(&scalar_elements, td, &alias)?;
8768 let rewrites = vec![];
8770 let returning = Self::pk_returning(td);
8771
8772 let (poly_implementors, poly_columns) = if self.is_polymorphic(td) {
8773 (
8774 self.find_poly_implementors(&format!("{}::{}", td.module, td.name)),
8775 Self::poly_dml_columns(td),
8776 )
8777 } else {
8778 (vec![], vec![])
8779 };
8780
8781 let written_cols: std::collections::HashSet<&str> = assignments.iter().map(|(c, _)| c.as_str()).collect();
8783 let type_name = format!("{}::{}", td.module, td.name);
8784 let enqueue_vector: Vec<VectorEnqueueInfo> = td
8785 .vector_indexes
8786 .iter()
8787 .filter(|vi| vi.pointers.iter().any(|f| written_cols.contains(f.as_str())))
8788 .map(|vi| VectorEnqueueInfo {
8789 type_name: type_name.clone(),
8790 index_name: vi.index_name.clone(),
8791 })
8792 .collect();
8793 let enqueue_search = collect_search_enqueue(td, &type_name, "index");
8794 let nested_ctes = std::mem::replace(&mut self.pending_nested_ctes, outer_pending_nested_ctes);
8800
8801 Ok(IrUpdate {
8802 target,
8803 filter,
8804 assignments,
8805 rewrites,
8806 returning,
8807 multi_link_clears,
8808 multi_link_replaces,
8809 multi_link_appends,
8810 multi_link_removals,
8811 poly_implementors,
8812 poly_columns,
8813 enqueue_vector,
8814 enqueue_search,
8815 nested_ctes,
8816 })
8817 }
8818
8819 fn junction_info_for(
8828 &mut self,
8829 td: &TypeDescriptor,
8830 name: &str,
8831 target: &str,
8832 through: &Option<String>,
8833 ) -> Result<(String, String, String, String, Option<&'a TypeDescriptor>), PyQLError> {
8834 let (junction_table, module, source_col, target_col, through_td) =
8835 self.own_junction_info_for(td, name, target, through)?;
8836 let owner_derived = through_td.is_none_or(|through_td| through_td.junction);
8837 let junction_table = if owner_derived {
8838 self.owner_junction(td, name, through_td)
8839 } else {
8840 junction_table
8841 };
8842 Ok((junction_table, module, source_col, target_col, through_td))
8843 }
8844
8845 fn junction_tables_of(&mut self, td: &TypeDescriptor) -> Result<Vec<String>, PyQLError> {
8850 let mut tables = vec![];
8851 for multilink in &td.multilinks {
8852 match &multilink.through {
8853 Some(through) => {
8854 let through_td = self.resolve_type(through)?;
8855 tables.push(format!("\"{}\"", through_td.table));
8856 }
8857 None => {
8858 tables.push(format!("\"{}.{}\"", td.table, multilink.name));
8859 for implementor in self.find_poly_implementors(&format!("{}::{}", td.module, td.name)) {
8860 tables.push(format!("\"{}.{}\"", implementor.table, multilink.name));
8861 }
8862 }
8863 }
8864 }
8865 Ok(tables)
8866 }
8867
8868 fn owner_junction(&self, td: &TypeDescriptor, name: &str, through_td: Option<&TypeDescriptor>) -> String {
8871 let own = format!("{}.{}", td.table, name);
8872 if !self.has_subtypes(td) {
8873 return own;
8874 }
8875 let tables = self
8876 .find_poly_implementors(&format!("{}::{}", td.module, td.name))
8877 .into_iter()
8878 .map(|implementor| (implementor.module, format!("{}.{}", implementor.table, name)))
8879 .collect::<Vec<_>>();
8880 let mut columns = vec!["source".to_string(), "target".to_string()];
8881 if let Some(through_td) = through_td {
8882 columns.extend(Self::poly_dml_columns(through_td).into_iter().filter(|c| c != "id"));
8883 }
8884 super::inherited_junction(&tables, &columns)
8885 }
8886
8887 fn own_junction_info_for(
8890 &mut self,
8891 td: &TypeDescriptor,
8892 name: &str,
8893 target: &str,
8894 through: &Option<String>,
8895 ) -> Result<(String, String, String, String, Option<&'a TypeDescriptor>), PyQLError> {
8896 match through {
8897 None => Ok((
8898 format!("{}.{}", td.table, name),
8899 td.module.clone(),
8900 "source".to_string(),
8901 "target".to_string(),
8902 None,
8903 )),
8904 Some(through_qname) => {
8905 let through_td = self.resolve_type(through_qname)?;
8906 let src_type = format!("{}::{}", td.module, td.name);
8907 let source_col = through_td
8908 .links
8909 .iter()
8910 .find(|l| l.target == src_type)
8911 .map(|l| l.name.clone())
8912 .unwrap_or_else(|| "source".to_string());
8913 let target_col = through_td
8920 .links
8921 .iter()
8922 .find(|l| l.target == target && l.name != source_col)
8923 .or_else(|| through_td.links.iter().find(|l| l.target == target))
8924 .map(|l| l.name.clone())
8925 .unwrap_or_else(|| "target".to_string());
8926 let (junction_table, junction_module) = if through_td.junction {
8941 (format!("{}.{}", td.table, name), td.module.clone())
8942 } else {
8943 (through_td.table.clone(), through_td.module.clone())
8944 };
8945 Ok((
8946 junction_table,
8947 junction_module,
8948 source_col,
8949 target_col,
8950 Some(through_td),
8951 ))
8952 }
8953 }
8954 }
8955
8956 fn multilink_junction_info(
8957 &mut self,
8958 td: &TypeDescriptor,
8959 ml: &MultiLinkDescriptor,
8960 ) -> Result<(String, String, String, String, Option<&'a TypeDescriptor>), PyQLError> {
8961 self.junction_info_for(td, &ml.name, &ml.target, &ml.through)
8962 }
8963
8964 fn own_multilink_junction_info(
8965 &mut self,
8966 td: &TypeDescriptor,
8967 ml: &MultiLinkDescriptor,
8968 ) -> Result<(String, String, String, String, Option<&'a TypeDescriptor>), PyQLError> {
8969 self.own_junction_info_for(td, &ml.name, &ml.target, &ml.through)
8970 }
8971
8972 fn own_link_junction_info(
8973 &mut self,
8974 td: &TypeDescriptor,
8975 l: &LinkDescriptor,
8976 ) -> Result<(String, String, String, String, Option<&'a TypeDescriptor>), PyQLError> {
8977 self.own_junction_info_for(td, &l.name, &l.target, &l.through)
8978 }
8979
8980 fn link_junction_info(
8982 &mut self,
8983 td: &TypeDescriptor,
8984 l: &LinkDescriptor,
8985 ) -> Result<(String, String, String, String, Option<&'a TypeDescriptor>), PyQLError> {
8986 self.junction_info_for(td, &l.name, &l.target, &l.through)
8987 }
8988
8989 fn build_multilink_join(
8994 &mut self,
8995 td: &TypeDescriptor,
8996 name: &str,
8997 target: &str,
8998 through: &Option<String>,
8999 ) -> Result<IrMultiLinkJoin, PyQLError> {
9000 if let Some(through_qname) = through {
9001 let through_td = self.resolve_type(through_qname)?;
9002 if through_td.junction {
9003 Ok(IrMultiLinkJoin::Standard {
9008 junction_table: self.owner_junction(td, name, Some(through_td)),
9009 module: td.module.clone(),
9010 })
9011 } else {
9012 let source_qname = format!("{}::{}", td.module, td.name);
9013 let source_col = through_td
9014 .links
9015 .iter()
9016 .find(|l| l.target == source_qname)
9017 .ok_or_else(|| {
9018 PyQLError::Type(PyQLTypeError {
9019 message: format!("through type {through_qname} has no link to {source_qname}"),
9020 position: Position { line: 0, col: 0 },
9021 })
9022 })?
9023 .name
9024 .clone();
9025 let target_col = through_td
9026 .links
9027 .iter()
9028 .find(|l| l.target == target && l.name != source_col)
9029 .or_else(|| through_td.links.iter().find(|l| l.target == target))
9030 .ok_or_else(|| {
9031 PyQLError::Type(PyQLTypeError {
9032 message: format!("through type {through_qname} has no link to target {target}"),
9033 position: Position { line: 0, col: 0 },
9034 })
9035 })?
9036 .name
9037 .clone();
9038 Ok(IrMultiLinkJoin::Through {
9039 junction_table: through_td.table.clone(),
9040 module: through_td.module.clone(),
9041 source_col,
9042 target_col,
9043 })
9044 }
9045 } else {
9046 let junction_table = self.owner_junction(td, name, None);
9047 Ok(IrMultiLinkJoin::Standard {
9048 junction_table: match self.junction_read_overrides.get(&junction_table) {
9049 Some(o) => o.junction.clone(),
9050 None => junction_table,
9051 },
9052 module: td.module.clone(),
9053 })
9054 }
9055 }
9056
9057 fn compile_multilink_values(
9066 &mut self,
9067 expr: &Expr,
9068 td: &'a TypeDescriptor,
9069 alias: &str,
9070 through_td: Option<&'a TypeDescriptor>,
9071 ) -> Result<IrMultiLinkValues, PyQLError> {
9072 self.without_implicit_id(|this| this.compile_multilink_values_inner(expr, td, alias, through_td))
9076 }
9077
9078 fn compile_multilink_values_inner(
9079 &mut self,
9080 expr: &Expr,
9081 td: &'a TypeDescriptor,
9082 alias: &str,
9083 through_td: Option<&'a TypeDescriptor>,
9084 ) -> Result<IrMultiLinkValues, PyQLError> {
9085 if let Expr::Union(a, b) = expr {
9088 let left = self.compile_multilink_values(a, td, alias, through_td)?;
9089 let right = self.compile_multilink_values(b, td, alias, through_td)?;
9090 return Ok(IrMultiLinkValues {
9091 source: IrMultiLinkValueSource::Union(Box::new(left), Box::new(right)),
9092 link_props: vec![],
9093 });
9094 }
9095
9096 if matches!(expr, Expr::IfElse(_))
9101 && let Some(rewritten) = self.object_if_else_as_union(expr)
9102 {
9103 return self.compile_multilink_values(&rewritten, td, alias, through_td);
9104 }
9105
9106 if let Expr::SubQuery(inner) = expr
9111 && let Stmt::Select(sel) = inner.as_ref()
9112 && let Expr::Shape(sh) = &sel.result
9113 && !sh.elements.is_empty()
9114 && sh
9115 .elements
9116 .iter()
9117 .all(|el| matches!(el.path.steps.as_slice(), [ast::PathStep::LinkProp(_)]))
9118 && let Some(base) = sh.expr.clone()
9119 {
9120 let lifted = Expr::Shape(Box::new(ast::ShapeExpr {
9121 expr: Some(Expr::SubQuery(Box::new(Stmt::Select(ast::SelectStmt {
9122 result: base,
9123 ..sel.clone()
9124 })))),
9125 elements: sh.elements.clone(),
9126 marker_offset: sh.marker_offset,
9127 }));
9128 return self.compile_multilink_values(&lifted, td, alias, through_td);
9129 }
9130
9131 if let Expr::Shape(shape) = expr {
9134 let inner_expr = shape
9135 .expr
9136 .as_ref()
9137 .ok_or_else(|| self.type_err("multilink value shape must have a base expression"))?;
9138 let mut inner = self.compile_multilink_values(inner_expr, td, alias, through_td)?;
9139 let walked_junction = match (&inner.source, inner_expr) {
9143 (IrMultiLinkValueSource::PathSelect(ps), Expr::SubQuery(stmt)) => {
9144 match (stmt.as_ref(), ps.joins.last()) {
9145 (Stmt::Select(sel), Some(IrPathJoin::Multi { junction_alias, .. })) => match &sel.result {
9146 Expr::Path(p) if p.partial => match p.steps.as_slice() {
9147 [ast::PathStep::Name(link)] => Self::resolve_multilink(td, link)
9148 .and_then(|m| m.through.clone())
9149 .map(|through| (through, junction_alias.clone())),
9150 _ => None,
9151 },
9152 _ => None,
9153 },
9154 _ => None,
9155 }
9156 }
9157 _ => None,
9158 };
9159
9160 let Some(through) = through_td else {
9161 return Err(self.type_err(
9162 "link properties (`@prop := value`) are only valid on a multi-link \
9163 declared with `Through[...]`",
9164 ));
9165 };
9166
9167 for el in &shape.elements {
9168 let prop_name = match el.path.steps.as_slice() {
9169 [ast::PathStep::LinkProp(name)] => name.clone(),
9170 _ => return Err(self.type_err("only `@prop := value` link-property assignments are valid here")),
9171 };
9172 let prop = Self::resolve_property(through, &prop_name)
9173 .ok_or_else(|| self.field_err(&prop_name, &format!("{}::{}", through.module, through.name)))?;
9174 if prop.is_readonly {
9175 return Err(self.type_err(&format!(
9176 "cannot set link property '{prop_name}': it is declared as read-only"
9177 )));
9178 }
9179 let value_expr = el
9180 .compexpr
9181 .as_ref()
9182 .ok_or_else(|| self.type_err(&format!("link property '{prop_name}' must be assigned a value")))?;
9183 let scoped = walked_junction.is_some();
9184 if scoped {
9185 self.link_prop_scope.push(walked_junction.clone());
9186 }
9187 let ir_expr = self.compile_expr(value_expr, td, alias);
9188 if scoped {
9189 self.link_prop_scope.pop();
9190 }
9191 inner.link_props.push((prop.name.clone(), ir_expr?));
9192 }
9193 return Ok(inner);
9194 }
9195
9196 if let Expr::FunctionCall(f) = expr
9199 && (f.module.is_none() || f.module.as_deref() == Some("std"))
9200 && matches!(f.name.as_str(), "assert_exists" | "assert_distinct")
9201 && let [arg] = f.args.as_slice()
9202 {
9203 let arg = arg.clone();
9204 let inner = self.compile_multilink_values(&arg, td, alias, through_td)?;
9205 let mut props = vec![];
9209 crate::sql::collect_link_prop_names(&inner, &mut props);
9210 if !props.is_empty() {
9211 return Err(self.type_err(&format!(
9212 "'{}' cannot be applied to a link that carries link properties ({}) — the check reads the targets alone",
9213 f.name,
9214 props.join(", "),
9215 )));
9216 }
9217 let message = self.assert_message(f, Some((td, alias)))?;
9218 return Ok(IrMultiLinkValues {
9219 source: IrMultiLinkValueSource::Asserted {
9220 fn_name: f.name.clone(),
9221 inner: Box::new(inner),
9222 message,
9223 },
9224 link_props: vec![],
9225 });
9226 }
9227
9228 if let Expr::SubQuery(inner) = expr
9234 && let Stmt::Select(sel) = inner.as_ref()
9235 && sel.filter.is_none()
9236 && sel.order_by.is_empty()
9237 && sel.offset.is_none()
9238 && sel.limit.is_none()
9239 && matches!(sel.result, Expr::Set(_) | Expr::Union(_, _))
9240 {
9241 let result = sel.result.clone();
9242 return self.compile_multilink_values(&result, td, alias, through_td);
9243 }
9244
9245 if let Some(name) = self.resolve_cte_name(expr) {
9247 return Ok(IrMultiLinkValues {
9248 source: IrMultiLinkValueSource::CteRef(name.to_string()),
9249 link_props: vec![],
9250 });
9251 }
9252
9253 if let Expr::Set(elements) = expr
9255 && !elements.is_empty()
9256 {
9257 let mut combined: Option<IrMultiLinkValues> = None;
9258 for element in elements {
9259 let one = self.compile_multilink_values(element, td, alias, through_td)?;
9260 combined = Some(match combined {
9261 None => one,
9262 Some(previous) => IrMultiLinkValues {
9263 source: IrMultiLinkValueSource::Union(Box::new(previous), Box::new(one)),
9264 link_props: vec![],
9265 },
9266 });
9267 }
9268 return Ok(combined.expect("elements is non-empty"));
9269 }
9270
9271 if let Expr::SubQuery(inner) = expr
9275 && let Stmt::For(_) = inner.as_ref()
9276 {
9277 let (cte_name, _) = self.hoist_dml_as_cte(inner.as_ref())?;
9278 return Ok(IrMultiLinkValues {
9279 source: IrMultiLinkValueSource::CteRef(cte_name),
9280 link_props: vec![],
9281 });
9282 }
9283
9284 if let Expr::SubQuery(inner) = expr
9287 && matches!(inner.as_ref(), Stmt::Insert(_) | Stmt::Update(_) | Stmt::Delete(_))
9288 {
9289 let cte_name = self.hoist_nested_dml(inner)?;
9290 return Ok(IrMultiLinkValues {
9291 source: IrMultiLinkValueSource::CteRef(cte_name),
9292 link_props: vec![],
9293 });
9294 }
9295
9296 if let Expr::SubQuery(inner) = expr
9301 && let Stmt::Select(sel) = inner.as_ref()
9302 && let Expr::Path(path) = &sel.result
9303 && path.partial
9304 {
9305 let mut steps = vec![ast::PathStep::Name(format!("{}::{}", td.module, td.name))];
9306 steps.extend(path.steps.iter().cloned());
9307 let rooted = ast::Path { steps, partial: false };
9308 let synthetic = ast::SelectStmt {
9309 result: Expr::Path(rooted.clone()),
9310 filter: sel.filter.clone(),
9311 order_by: sel.order_by.clone(),
9312 offset: sel.offset.clone(),
9313 limit: sel.limit.clone(),
9314 lock: None,
9315 };
9316 let mut ps = self.compile_path_select(&synthetic, &rooted, &[], false)?;
9317 Self::correlate_path_select(&mut ps, alias);
9318 return Ok(IrMultiLinkValues {
9319 source: IrMultiLinkValueSource::PathSelect(Box::new(ps)),
9320 link_props: vec![],
9321 });
9322 }
9323
9324 if let Expr::SubQuery(inner) = expr {
9326 return match self.compile_stmt(inner)? {
9327 IrStmt::Select(s) if matches!(s.rows.as_slice(), [IrRowSource::Bound { .. }]) => {
9328 Ok(IrMultiLinkValues {
9329 source: IrMultiLinkValueSource::Select(Box::new(s)),
9330 link_props: vec![],
9331 })
9332 }
9333 IrStmt::PathSelect(ps) => Ok(IrMultiLinkValues {
9334 source: IrMultiLinkValueSource::PathSelect(Box::new(ps)),
9335 link_props: vec![],
9336 }),
9337 _ => Err(self.type_err("multilink value must resolve to a SELECT or path query")),
9338 };
9339 }
9340
9341 if let Expr::FunctionCall(fc) = expr {
9342 let synthetic = ast::SelectStmt {
9343 result: expr.clone(),
9344 filter: None,
9345 order_by: vec![],
9346 offset: None,
9347 limit: None,
9348 lock: None,
9349 };
9350 if let Some(fs) = self.try_compile_fn_object_select(fc, &[], &synthetic, false)? {
9351 return Ok(IrMultiLinkValues {
9352 source: IrMultiLinkValueSource::Function(Box::new(fs)),
9353 link_props: vec![],
9354 });
9355 }
9356 }
9357
9358 if let Expr::Path(p) = expr
9360 && !p.partial
9361 {
9362 let fake_sel = ast::SelectStmt {
9363 result: expr.clone(),
9364 filter: None,
9365 order_by: vec![],
9366 offset: None,
9367 limit: None,
9368 lock: None,
9369 };
9370 return match self.compile_stmt(&Stmt::Select(fake_sel))? {
9371 IrStmt::PathSelect(ps) => Ok(IrMultiLinkValues {
9372 source: IrMultiLinkValueSource::PathSelect(Box::new(ps)),
9373 link_props: vec![],
9374 }),
9375 IrStmt::Select(s) if matches!(s.rows.as_slice(), [IrRowSource::Bound { .. }]) => {
9376 Ok(IrMultiLinkValues {
9377 source: IrMultiLinkValueSource::Select(Box::new(s)),
9378 link_props: vec![],
9379 })
9380 }
9381 _ => Err(self.type_err("expected a path expression for multilink value")),
9382 };
9383 }
9384
9385 Err(self.type_err("multilink value must be a CTE reference, parenthesised subquery, or type path"))
9386 }
9387
9388 fn compile_delete(&mut self, del: &ast::DeleteStmt) -> Result<IrDelete, PyQLError> {
9391 let pending_guard = self.pending_delete_guard.take();
9394 let type_name = self.expr_as_type_name(&del.subject)?;
9395 let bound_rows = self.cte_object_type(&type_name);
9400 let td = self.resolve_path_root(&type_name)?;
9401 let alias = self.fresh_alias();
9402 let target = IrSource {
9403 poly: None,
9404 type_name: format!("{}::{}", td.module, td.name),
9405 table: td.table.clone(),
9406 alias: alias.clone(),
9407 };
9408
9409 let declared_filter = del
9410 .filter
9411 .as_ref()
9412 .map(|f| self.compile_expr(f, td, &alias))
9413 .transpose()?;
9414 let filter = match bound_rows {
9415 Some(_) => {
9416 let membership = IrExpr::BinOp(Box::new(IrBinOp {
9417 left: IrExpr::ColumnRef {
9418 alias: alias.clone(),
9419 column: "id".to_string(),
9420 pg_type: "uuid".to_string(),
9421 },
9422 op: ast::BinOpKind::In,
9423 right: IrExpr::ArrayFromSelect(Box::new(IrArraySource::Select(IrSelect::schema_bound(
9424 IrSource {
9425 poly: None,
9426 type_name: format!("{}::{}", td.module, td.name),
9427 table: format!("@cte:{type_name}"),
9428 alias: self.fresh_alias(),
9429 },
9430 vec![],
9431 None,
9432 )))),
9433 }));
9434 Some(and_conditions(declared_filter, vec![membership]).expect("membership is present"))
9435 }
9436 None => declared_filter,
9437 };
9438 let filter = match pending_guard {
9439 Some(condition) => {
9440 let guard = self.compile_expr(&condition, td, &alias)?;
9441 and_conditions(filter, vec![guard])
9442 }
9443 None => filter,
9444 };
9445
9446 let returning = Self::pk_returning(td);
9447
9448 let (poly_implementors, poly_columns) = if self.is_polymorphic(td) {
9449 (
9450 self.find_poly_implementors(&format!("{}::{}", td.module, td.name)),
9451 Self::poly_dml_columns(td),
9452 )
9453 } else {
9454 (vec![], vec![])
9455 };
9456 let qname = format!("{}::{}", td.module, td.name);
9457 let enqueue_search = collect_search_enqueue(td, &qname, "delete");
9458
9459 Ok(IrDelete {
9460 target,
9461 filter,
9462 returning,
9463 poly_implementors,
9464 poly_columns,
9465 enqueue_search,
9466 })
9467 }
9468
9469 fn compile_shape(
9472 &mut self,
9473 elements: &[ShapeElement],
9474 td: &TypeDescriptor,
9475 alias: &str,
9476 module: &str,
9477 ) -> Result<Vec<IrShapePointer>, PyQLError> {
9478 if elements.is_empty() {
9479 return Ok(Self::pk_returning(td));
9481 }
9482
9483 let mut pointers = vec![];
9484 for el in elements {
9485 if let Some(splat) = &el.splat {
9486 if let Some(ast::PathStep::TypeIntersection(type_ref)) = el.path.steps.first() {
9488 let type_ref = type_ref.clone();
9489 pointers.extend(self.compile_type_intersection_splat(&type_ref, splat, td, alias)?);
9490 } else {
9491 pointers.extend(self.compile_splat(splat, td, alias, module)?);
9492 }
9493 } else {
9494 pointers.push(self.compile_shape_element(el, td, alias, module)?);
9495 }
9496 }
9497 self.prepend_implicit_id(&mut pointers, td);
9498 Ok(pointers)
9499 }
9500
9501 fn prepend_implicit_id(&self, pointers: &mut Vec<IrShapePointer>, td: &TypeDescriptor) {
9510 if !self.implicit_id_in_shapes {
9511 return;
9512 }
9513 let Some(pk) = td.properties.iter().find(|p| p.is_pk) else {
9514 return;
9515 };
9516 if pointers.iter().any(|p| p.alias() == pk.name) {
9517 return;
9518 }
9519 pointers.insert(
9520 0,
9521 IrShapePointer::Scalar(IrScalarPointer {
9522 implicit_id: true,
9523 marker_offset: None,
9524 alias: pk.name.clone(),
9525 column: pk.name.clone(),
9526 pg_type: pk.pg_type.clone(),
9527 tuple_shape: None,
9528 }),
9529 );
9530 }
9531
9532 fn without_implicit_id<T>(&mut self, body: impl FnOnce(&mut Self) -> Result<T, PyQLError>) -> Result<T, PyQLError> {
9535 let saved = std::mem::replace(&mut self.implicit_id_in_shapes, false);
9536 let result = body(self);
9537 self.implicit_id_in_shapes = saved;
9538 result
9539 }
9540
9541 fn compile_splat(
9543 &mut self,
9544 splat: &ast::Splat,
9545 td: &TypeDescriptor,
9546 alias: &str,
9547 module: &str,
9548 ) -> Result<Vec<IrShapePointer>, PyQLError> {
9549 let mut pointers: Vec<IrShapePointer> = td
9550 .properties
9551 .iter()
9552 .map(|p| {
9553 IrShapePointer::Scalar(IrScalarPointer {
9554 implicit_id: false,
9555 marker_offset: None,
9556 alias: p.name.clone(),
9557 column: p.name.clone(),
9558 pg_type: p.pg_type.clone(),
9559 tuple_shape: self.resolve_property_tuple_shape(p),
9560 })
9561 })
9562 .collect();
9563
9564 for cd in &td.computed.clone() {
9565 if matches!(splat, ast::Splat::Shallow) && self.computed_is_object_valued(cd, td) {
9569 continue;
9570 }
9571 pointers.push(self.compile_declared_computed(cd, td, alias, module, None, &[])?);
9572 }
9573
9574 if matches!(splat, ast::Splat::Deep) {
9575 for l in &td.links {
9576 let target_td = self.resolve_type(&l.target)?;
9577 let sub_alias = self.fresh_alias();
9578 let target_module = target_td.module.clone();
9584 let sub_shape = self.compile_splat(&ast::Splat::Shallow, target_td, &sub_alias, &target_module)?;
9585 let subquery = IrSelect::schema_bound(
9586 IrSource {
9587 poly: self.link_target_fanout(target_td),
9588 type_name: format!("{}::{}", target_td.module, target_td.name),
9589 table: target_td.table.clone(),
9590 alias: sub_alias.clone(),
9591 },
9592 sub_shape,
9593 None,
9594 );
9595 let correlation = if l.is_junction_backed() {
9596 let join = self.build_multilink_join(td, &l.name, &l.target, &l.through)?;
9597 IrSingleLinkCorrelation::Junction {
9598 join,
9599 target_pk: "id".to_string(),
9600 }
9601 } else {
9602 IrSingleLinkCorrelation::Fk {
9603 fk_column: format!("{}_id", l.name),
9604 target_pk: "id".to_string(),
9605 }
9606 };
9607 pointers.push(IrShapePointer::SingleLink(IrSingleLinkPointer {
9608 marker_offset: None,
9609 alias: l.name.clone(),
9610 correlation,
9611 subquery,
9612 link_properties: vec![],
9613 }));
9614 }
9615
9616 for ml in &td.multilinks {
9617 let sub_alias = self.fresh_alias();
9618 let target_td = self.resolve_type(&ml.target)?;
9619 let target_module = target_td.module.clone();
9622 let sub_shape = self.compile_splat(&ast::Splat::Shallow, target_td, &sub_alias, &target_module)?;
9623
9624 let join = if let Some(through_qname) = &ml.through {
9625 let through_td = self.resolve_type(through_qname)?;
9626 if through_td.junction {
9627 IrMultiLinkJoin::Standard {
9629 junction_table: self.owner_junction(td, &ml.name, Some(through_td)),
9630 module: td.module.clone(),
9631 }
9632 } else {
9633 let source_qname = format!("{}::{}", td.module, td.name);
9634 let source_col = through_td
9635 .links
9636 .iter()
9637 .find(|l| l.target == source_qname)
9638 .ok_or_else(|| {
9639 PyQLError::Type(PyQLTypeError {
9640 message: format!(
9641 "through type {through_qname} has no link to source type {source_qname}"
9642 ),
9643 position: Position { line: 0, col: 0 },
9644 })
9645 })?
9646 .name
9647 .clone();
9648 let target_col = through_td
9649 .links
9650 .iter()
9651 .find(|l| l.target == ml.target && l.name != source_col)
9652 .or_else(|| through_td.links.iter().find(|l| l.target == ml.target))
9653 .ok_or_else(|| {
9654 PyQLError::Type(PyQLTypeError {
9655 message: format!(
9656 "through type {through_qname} has no link to target type {}",
9657 ml.target
9658 ),
9659 position: Position { line: 0, col: 0 },
9660 })
9661 })?
9662 .name
9663 .clone();
9664 IrMultiLinkJoin::Through {
9665 junction_table: through_td.table.clone(),
9666 module: through_td.module.clone(),
9667 source_col,
9668 target_col,
9669 }
9670 }
9671 } else {
9672 IrMultiLinkJoin::Standard {
9676 junction_table: self.owner_junction(td, &ml.name, None),
9677 module: td.module.clone(),
9678 }
9679 };
9680
9681 let subquery = self.link_target_select(
9682 target_td,
9683 IrSource {
9684 poly: None,
9685 type_name: format!("{}::{}", target_td.module, target_td.name),
9686 table: target_td.table.clone(),
9687 alias: sub_alias.clone(),
9688 },
9689 sub_shape,
9690 );
9691
9692 let link_properties = self.splat_link_properties(ml)?;
9693 pointers.push(IrShapePointer::MultiLink(IrMultiLinkPointer {
9694 marker_offset: None,
9695 alias: ml.name.clone(),
9696 join,
9697 subquery,
9698 link_properties,
9699 single: false,
9700 }));
9701 }
9702 }
9703
9704 Ok(pointers)
9705 }
9706
9707 fn splat_link_properties(&self, ml: &MultiLinkDescriptor) -> Result<Vec<IrLinkProp>, PyQLError> {
9710 let Some(through_qname) = &ml.through else {
9711 return Ok(vec![]);
9712 };
9713 let through_td = self.resolve_type(through_qname)?;
9714 if !through_td.junction {
9715 return Ok(vec![]);
9716 }
9717 Ok(through_td
9718 .properties
9719 .iter()
9720 .filter(|p| p.name != "id")
9721 .map(|p| IrLinkProp { name: p.name.clone() })
9722 .collect())
9723 }
9724
9725 fn compile_type_intersection_splat(
9729 &mut self,
9730 type_ref: &ast::ObjectRef,
9731 splat: &ast::Splat,
9732 parent_td: &TypeDescriptor,
9733 parent_alias: &str,
9734 ) -> Result<Vec<IrShapePointer>, PyQLError> {
9735 let type_name = match &type_ref.module {
9736 Some(m) => format!("{}::{}", m, type_ref.name),
9737 None => type_ref.name.clone(),
9738 };
9739 let concrete_td = self.resolve_type(&type_name)?;
9740 let concrete_qname = format!("{}::{}", concrete_td.module, concrete_td.name);
9741 let concrete_table = concrete_td.table.clone();
9742
9743 let interface_props: std::collections::HashSet<String> =
9745 parent_td.properties.iter().map(|p| p.name.clone()).collect();
9746
9747 let props: Vec<_> = concrete_td
9749 .properties
9750 .iter()
9751 .filter(|p| !interface_props.contains(&p.name))
9752 .cloned()
9753 .collect();
9754
9755 let interface_computed: std::collections::HashSet<String> =
9759 parent_td.computed.iter().map(|c| c.name.clone()).collect();
9760 let computed: Vec<_> = concrete_td
9761 .computed
9762 .iter()
9763 .filter(|c| !interface_computed.contains(&c.name))
9764 .filter(|c| !matches!(splat, ast::Splat::Shallow) || !self.computed_is_object_valued(c, concrete_td))
9765 .cloned()
9766 .collect();
9767
9768 let links: Vec<_> = if matches!(splat, ast::Splat::Deep) {
9770 concrete_td.links.to_vec()
9771 } else {
9772 vec![]
9773 };
9774
9775 let mut pointers = vec![];
9776 for prop in props {
9777 let sub_alias = self.fresh_alias();
9778 let filter = IrExpr::BinOp(Box::new(IrBinOp {
9779 left: IrExpr::ColumnRef {
9780 alias: sub_alias.clone(),
9781 column: "id".to_string(),
9782 pg_type: "uuid".to_string(),
9783 },
9784 op: ast::BinOpKind::Eq,
9785 right: IrExpr::ColumnRef {
9786 alias: parent_alias.to_string(),
9787 column: "id".to_string(),
9788 pg_type: "uuid".to_string(),
9789 },
9790 }));
9791 let subquery = IrSelect::schema_bound(
9792 IrSource {
9793 poly: None,
9794 type_name: concrete_qname.clone(),
9795 table: concrete_table.clone(),
9796 alias: sub_alias,
9797 },
9798 vec![IrShapePointer::Scalar(IrScalarPointer {
9799 implicit_id: false,
9800 marker_offset: None,
9801 alias: prop.name.clone(),
9802 column: prop.name.clone(),
9803 pg_type: prop.pg_type.clone(),
9804 tuple_shape: self.resolve_property_tuple_shape(&prop),
9805 })],
9806 Some(filter),
9807 );
9808 pointers.push(IrShapePointer::Computed(IrComputedPointer {
9809 marker_offset: None,
9810 alias: prop.name.clone(),
9811 expr: IrExpr::Subquery(Box::new(subquery)),
9812 }));
9813 }
9814
9815 for cd in computed {
9816 let sub_alias = self.fresh_alias();
9817 let filter = IrExpr::BinOp(Box::new(IrBinOp {
9818 left: IrExpr::ColumnRef {
9819 alias: sub_alias.clone(),
9820 column: "id".to_string(),
9821 pg_type: "uuid".to_string(),
9822 },
9823 op: ast::BinOpKind::Eq,
9824 right: IrExpr::ColumnRef {
9825 alias: parent_alias.to_string(),
9826 column: "id".to_string(),
9827 pg_type: "uuid".to_string(),
9828 },
9829 }));
9830 let expr_ast = crate::parse::parse_pointer_expr(&cd.expression).map_err(PyQLError::Syntax)?;
9831 let inner_ir = self.compile_expr(&expr_ast, concrete_td, &sub_alias)?;
9832 let subquery = IrSelect::schema_bound(
9833 IrSource {
9834 poly: None,
9835 type_name: concrete_qname.clone(),
9836 table: concrete_table.clone(),
9837 alias: sub_alias,
9838 },
9839 vec![IrShapePointer::Computed(IrComputedPointer {
9840 marker_offset: None,
9841 alias: cd.name.clone(),
9842 expr: inner_ir,
9843 })],
9844 Some(filter),
9845 );
9846 pointers.push(IrShapePointer::Computed(IrComputedPointer {
9847 marker_offset: None,
9848 alias: cd.name.clone(),
9849 expr: IrExpr::Subquery(Box::new(subquery)),
9850 }));
9851 }
9852
9853 for link in links {
9855 let target_td = self.resolve_type(&link.target)?;
9856 let sub_alias = self.fresh_alias();
9857 let filter = if link.is_junction_backed() {
9858 let (jt_table, jt_module, jt_src_col, jt_tgt_col, _) =
9859 self.junction_info_for(concrete_td, &link.name, &link.target, &link.through)?;
9860 let jt_alias = self.fresh_alias();
9861 let jt_filter = IrExpr::BinOp(Box::new(IrBinOp {
9862 left: IrExpr::BinOp(Box::new(IrBinOp {
9863 left: IrExpr::ColumnRef {
9864 alias: jt_alias.clone(),
9865 column: jt_src_col,
9866 pg_type: "uuid".to_string(),
9867 },
9868 op: ast::BinOpKind::Eq,
9869 right: IrExpr::ColumnRef {
9870 alias: parent_alias.to_string(),
9871 column: "id".to_string(),
9872 pg_type: "uuid".to_string(),
9873 },
9874 })),
9875 op: ast::BinOpKind::And,
9876 right: IrExpr::BinOp(Box::new(IrBinOp {
9877 left: IrExpr::ColumnRef {
9878 alias: jt_alias.clone(),
9879 column: jt_tgt_col,
9880 pg_type: "uuid".to_string(),
9881 },
9882 op: ast::BinOpKind::Eq,
9883 right: IrExpr::ColumnRef {
9884 alias: sub_alias.clone(),
9885 column: "id".to_string(),
9886 pg_type: "uuid".to_string(),
9887 },
9888 })),
9889 }));
9890 let exists_select = IrSelect::schema_bound(
9891 IrSource {
9892 poly: None,
9893 type_name: format!("{}::__jt__", jt_module),
9894 table: jt_table,
9895 alias: jt_alias,
9896 },
9897 vec![],
9898 Some(jt_filter),
9899 );
9900 IrExpr::UnaryOp(Box::new(IrUnaryOp {
9901 op: ast::UnaryOpKind::Exists,
9902 operand: IrExpr::Subquery(Box::new(exists_select)),
9903 }))
9904 } else {
9905 IrExpr::BinOp(Box::new(IrBinOp {
9906 left: IrExpr::ColumnRef {
9907 alias: sub_alias.clone(),
9908 column: "id".to_string(),
9909 pg_type: "uuid".to_string(),
9910 },
9911 op: ast::BinOpKind::Eq,
9912 right: IrExpr::ColumnRef {
9913 alias: parent_alias.to_string(),
9914 column: format!("{}_id", link.name),
9915 pg_type: "uuid".to_string(),
9916 },
9917 }))
9918 };
9919 let sub_shape = Self::pk_returning(target_td);
9920 let subquery = IrSelect::schema_bound(
9921 IrSource {
9922 poly: self.link_target_fanout(target_td),
9923 type_name: format!("{}::{}", target_td.module, target_td.name),
9924 table: target_td.table.clone(),
9925 alias: sub_alias,
9926 },
9927 sub_shape,
9928 Some(filter),
9929 );
9930 pointers.push(IrShapePointer::Computed(IrComputedPointer {
9931 marker_offset: None,
9932 alias: link.name.clone(),
9933 expr: IrExpr::Subquery(Box::new(subquery)),
9934 }));
9935 }
9936
9937 Ok(pointers)
9938 }
9939
9940 fn compile_type_intersection_pointer(
9942 &mut self,
9943 type_ref: &ast::ObjectRef,
9944 tail_steps: &[ast::PathStep],
9945 parent_alias: &str,
9946 marker_offset: Option<usize>,
9947 ) -> Result<IrShapePointer, PyQLError> {
9948 let expr = self.compile_type_intersection_expr_steps(type_ref, tail_steps, parent_alias)?;
9949 let alias = match tail_steps.last() {
9951 Some(ast::PathStep::Name(n)) => n.clone(),
9952 _ => return Err(self.type_err("type intersection must end with a pointer name")),
9953 };
9954 Ok(IrShapePointer::Computed(IrComputedPointer {
9955 alias,
9956 expr,
9957 marker_offset,
9958 }))
9959 }
9960
9961 fn compile_type_intersection_expr(
9963 &mut self,
9964 steps: &[ast::PathStep],
9965 td: &TypeDescriptor,
9966 parent_alias: &str,
9967 ) -> Result<IrExpr, PyQLError> {
9968 use ast::PathStep;
9969 let type_ref = match steps.first() {
9970 Some(PathStep::TypeIntersection(tr)) => tr.clone(),
9971 _ => return Err(self.type_err("expected type intersection")),
9972 };
9973 match self.compile_type_intersection_expr_steps(&type_ref, &steps[1..], parent_alias) {
9978 Ok(ir) => Ok(ir),
9979 Err(fast_path_err) => {
9980 let p = ast::Path {
9981 steps: steps.to_vec(),
9982 partial: true,
9983 };
9984 self.compile_partial_path_as_subquery(&p, td, parent_alias)
9985 .map_err(|_| fast_path_err)
9986 }
9987 }
9988 }
9989
9990 fn compile_type_intersection_expr_steps(
9992 &mut self,
9993 type_ref: &ast::ObjectRef,
9994 tail_steps: &[ast::PathStep],
9995 parent_alias: &str,
9996 ) -> Result<IrExpr, PyQLError> {
9997 use ast::PathStep;
9998 let type_name = match &type_ref.module {
9999 Some(m) => format!("{}::{}", m, type_ref.name),
10000 None => type_ref.name.clone(),
10001 };
10002 let concrete_td = self.resolve_type(&type_name)?;
10003 let concrete_qname = format!("{}::{}", concrete_td.module, concrete_td.name);
10004 let concrete_table = concrete_td.table.clone();
10005
10006 let pointer_name = match tail_steps.first() {
10007 Some(PathStep::Name(n)) => n.as_str(),
10008 _ => return Err(self.type_err("type intersection must be followed by a pointer name, e.g. [is Type].name")),
10009 };
10010
10011 let prop = concrete_td
10012 .properties
10013 .iter()
10014 .find(|p| p.name == pointer_name)
10015 .ok_or_else(|| self.field_err(pointer_name, &concrete_qname))?;
10016 let prop_name = prop.name.clone();
10017 let prop_type = prop.pg_type.clone();
10018
10019 let sub_alias = self.fresh_alias();
10020 let filter = IrExpr::BinOp(Box::new(IrBinOp {
10021 left: IrExpr::ColumnRef {
10022 alias: sub_alias.clone(),
10023 column: "id".to_string(),
10024 pg_type: "uuid".to_string(),
10025 },
10026 op: ast::BinOpKind::Eq,
10027 right: IrExpr::ColumnRef {
10028 alias: parent_alias.to_string(),
10029 column: "id".to_string(),
10030 pg_type: "uuid".to_string(),
10031 },
10032 }));
10033
10034 let poly = self.poly_fanout_for(&concrete_qname);
10035 Ok(IrExpr::Subquery(Box::new(IrSelect::schema_bound(
10036 IrSource {
10037 poly,
10038 type_name: concrete_qname,
10039 table: concrete_table,
10040 alias: sub_alias,
10041 },
10042 vec![IrShapePointer::Scalar(IrScalarPointer {
10043 implicit_id: false,
10044 marker_offset: None,
10045 alias: prop_name.clone(),
10046 column: prop_name,
10047 pg_type: prop_type,
10048 tuple_shape: self.resolve_property_tuple_shape(prop),
10049 })],
10050 Some(filter),
10051 ))))
10052 }
10053
10054 fn compile_shape_element(
10055 &mut self,
10056 el: &ShapeElement,
10057 td: &TypeDescriptor,
10058 alias: &str,
10059 module: &str,
10060 ) -> Result<IrShapePointer, PyQLError> {
10061 if let Some(ast::PathStep::TypeIntersection(type_ref)) = el.path.steps.first()
10063 && el.compexpr.is_none()
10064 && el.path.steps.len() >= 2
10065 {
10066 if let Some(nested) = el.nested.as_deref()
10070 && !nested.is_empty()
10071 && let Some(ast::PathStep::Name(leaf)) = el.path.steps.last()
10072 && let Some(ptr) = self.try_compile_pointer_expr(
10073 &leaf.clone(),
10074 &Expr::Path(el.path.clone()),
10075 td,
10076 alias,
10077 module,
10078 el.marker_offset,
10079 nested,
10080 )?
10081 {
10082 return Ok(ptr);
10083 }
10084 let type_ref = type_ref.clone();
10085 return self.compile_type_intersection_pointer(&type_ref, &el.path.steps[1..], alias, el.marker_offset);
10086 }
10087
10088 let pointer_name = path_leaf(&el.path)?;
10089
10090 if pointer_name == "__type__" && el.compexpr.is_none() {
10094 let expr = if self.is_polymorphic(td) {
10095 IrExpr::ColumnRef {
10096 alias: alias.to_string(),
10097 column: "__type__".to_string(),
10098 pg_type: "text".to_string(),
10099 }
10100 } else {
10101 IrExpr::Literal(IrLiteral::Str(format!("{}::{}", td.module, td.name)))
10102 };
10103 return Ok(IrShapePointer::Computed(IrComputedPointer {
10104 marker_offset: el.marker_offset,
10105 alias: "__type__".to_string(),
10106 expr,
10107 }));
10108 }
10109
10110 if let Some(compexpr) = &el.compexpr
10117 && let Some((call, value_expr, check_expr)) =
10118 Self::asserted_pointer_expr(compexpr, el.nested.as_deref().unwrap_or(&[]))
10119 {
10120 let element_for = |expr: Expr| ShapeElement {
10121 compexpr: Some(expr),
10122 nested: None,
10123 ..el.clone()
10124 };
10125 if let Ok(ptr) = self.compile_shape_element(&element_for(value_expr), td, alias, module)
10130 && ptr.is_object_pointer()
10131 {
10132 let check = match check_expr {
10133 Some(expr) => Some(self.compile_shape_element(&element_for(expr), td, alias, module)?),
10134 None => None,
10135 };
10136 let message = self.assert_message(&call, Some((td, alias)))?;
10137 return Ok(IrShapePointer::Asserted(Box::new(IrAssertedPointer {
10138 fn_name: call.name,
10139 inner: ptr,
10140 check,
10141 message,
10142 })));
10143 }
10144 }
10145
10146 if let Some(compexpr) = &el.compexpr
10150 && let Some(guarded) = guarded_object_branch(compexpr)
10151 {
10152 let element = ShapeElement {
10153 compexpr: Some(guarded),
10154 ..el.clone()
10155 };
10156 return self.compile_shape_element(&element, td, alias, module);
10157 }
10158
10159 if let Some(compexpr) = &el.compexpr {
10161 let inner_declared: Vec<ShapeElement> = Self::replaced_subject_shape(compexpr)
10169 .iter()
10170 .filter(|e| e.compexpr.is_some())
10171 .cloned()
10172 .collect();
10173 let restore = (!inner_declared.is_empty()).then(|| {
10174 let mut scope = self.active_declared_pointers.clone();
10175 scope.extend(inner_declared);
10176 std::mem::replace(&mut self.active_declared_pointers, scope)
10177 });
10178 let pointer = self.try_compile_pointer_expr(
10179 pointer_name,
10180 compexpr,
10181 td,
10182 alias,
10183 module,
10184 el.marker_offset,
10185 el.nested.as_deref().unwrap_or(&[]),
10186 );
10187 if let Some(previous) = restore {
10188 self.active_declared_pointers = previous;
10189 }
10190 if let Some(ptr) = pointer? {
10191 return Ok(ptr);
10192 }
10193 if let Some((fc, call_nested)) = Self::object_call_with_shape(compexpr, el.nested.as_deref())
10198 && let Some(fs) = self.compile_fn_object_source(fc, &call_nested)?
10199 {
10200 return Ok(IrShapePointer::Computed(IrComputedPointer {
10201 marker_offset: el.marker_offset,
10202 alias: pointer_name.to_string(),
10203 expr: IrExpr::ArrayFromSelect(Box::new(IrArraySource::ObjectFunction(Box::new(fs)))),
10204 }));
10205 }
10206 let ir = self.compile_expr(compexpr, td, alias)?;
10207 if let IrExpr::ArrayFromSelect(src) = ir {
10209 if let IrArraySource::RawExpr {
10210 source,
10211 poly_implementors,
10212 poly_columns,
10213 expr,
10214 } = *src
10215 {
10216 return Ok(IrShapePointer::ScalarSet(IrScalarSetPointer {
10217 alias: pointer_name.to_string(),
10218 source,
10219 poly_implementors,
10220 poly_columns,
10221 bool_expr: expr,
10222 }));
10223 }
10224 return Ok(IrShapePointer::Computed(IrComputedPointer {
10225 marker_offset: el.marker_offset,
10226 alias: pointer_name.to_string(),
10227 expr: IrExpr::ArrayFromSelect(src),
10228 }));
10229 }
10230 return Ok(IrShapePointer::Computed(IrComputedPointer {
10231 marker_offset: el.marker_offset,
10232 alias: pointer_name.to_string(),
10233 expr: ir,
10234 }));
10235 }
10236
10237 if let Some(p) = Self::resolve_property(td, pointer_name) {
10239 return Ok(IrShapePointer::Scalar(IrScalarPointer {
10240 implicit_id: false,
10241 marker_offset: el.marker_offset,
10242 alias: pointer_name.to_string(),
10243 column: p.name.clone(),
10244 pg_type: p.pg_type.clone(),
10245 tuple_shape: self.resolve_property_tuple_shape(p),
10246 }));
10247 }
10248
10249 if let Some(l) = Self::resolve_link(td, pointer_name) {
10251 let target_td = self.resolve_type(&l.target)?;
10252 let sub_alias = self.fresh_alias();
10253 let nested_elements = el.nested.as_deref().unwrap_or(&[]);
10254
10255 let (regular_els, link_properties): (Vec<ShapeElement>, Vec<IrLinkProp>) = if l.is_junction_backed() {
10260 let mut regular = Vec::new();
10261 let mut props = Vec::new();
10262 for nel in nested_elements {
10263 if let [ast::PathStep::LinkProp(name)] = nel.path.steps.as_slice() {
10264 props.push(IrLinkProp { name: name.clone() });
10265 } else {
10266 regular.push(nel.clone());
10267 }
10268 }
10269 (regular, props)
10270 } else {
10271 (nested_elements.to_vec(), vec![])
10272 };
10273
10274 let sub_shape = self.compile_shape(®ular_els, target_td, &sub_alias, &target_td.module.clone())?;
10275 let subquery = self.link_target_select(
10276 target_td,
10277 IrSource {
10278 poly: None,
10279 type_name: format!("{}::{}", target_td.module, target_td.name),
10280 table: target_td.table.clone(),
10281 alias: sub_alias,
10282 },
10283 sub_shape,
10284 );
10285 let correlation = if l.is_junction_backed() {
10286 let join = self.build_multilink_join(td, &l.name, &l.target, &l.through)?;
10287 IrSingleLinkCorrelation::Junction {
10288 join,
10289 target_pk: "id".to_string(),
10290 }
10291 } else {
10292 IrSingleLinkCorrelation::Fk {
10293 fk_column: format!("{}_id", l.name),
10294 target_pk: "id".to_string(),
10295 }
10296 };
10297 return Ok(IrShapePointer::SingleLink(IrSingleLinkPointer {
10298 marker_offset: el.marker_offset,
10299 alias: pointer_name.to_string(),
10300 correlation,
10301 subquery,
10302 link_properties,
10303 }));
10304 }
10305
10306 if Self::resolve_multilink(td, pointer_name).is_some() {
10308 return self.compile_multilink_pointer(pointer_name, pointer_name, td, alias, module, el);
10309 }
10310
10311 if let Some(cd) = self.resolve_computed(td, pointer_name) {
10313 return self.compile_declared_computed(
10314 &cd,
10315 td,
10316 alias,
10317 module,
10318 el.marker_offset,
10319 el.nested.as_deref().unwrap_or(&[]),
10320 );
10321 }
10322
10323 if let Some(declared) = self
10326 .active_declared_pointers
10327 .iter()
10328 .find(|d| path_leaf(&d.path).is_ok_and(|n| n == pointer_name))
10329 .cloned()
10330 && let Some(expr) = declared.compexpr.clone()
10331 {
10332 let nested = if el.nested.as_deref().unwrap_or(&[]).is_empty() {
10333 declared.nested.clone().unwrap_or_default()
10334 } else {
10335 el.nested.clone().unwrap_or_default()
10336 };
10337 return self.compile_computed_expr(pointer_name, &expr, td, alias, module, el.marker_offset, &nested, None);
10338 }
10339
10340 Err(self.field_err(pointer_name, &format!("{}::{}", td.module, td.name)))
10341 }
10342
10343 #[allow(clippy::too_many_arguments)]
10350 fn compile_chained_link_pointer(
10351 &mut self,
10352 pointer_name: &str,
10353 path: &ast::Path,
10354 td: &TypeDescriptor,
10355 alias: &str,
10356 nested: &[ShapeElement],
10357 modifiers: Option<&ast::SelectStmt>,
10358 multi: bool,
10359 tail: usize,
10360 ) -> Result<IrShapePointer, PyQLError> {
10361 let mut steps = vec![ast::PathStep::Name(format!("{}::{}", td.module, td.name))];
10362 steps.extend(path.steps.iter().cloned());
10363 let full_path = ast::Path { steps, partial: false };
10364 let synthetic = ast::SelectStmt {
10365 result: Expr::Path(full_path.clone()),
10366 filter: modifiers.and_then(|m| m.filter.clone()),
10367 order_by: modifiers.map(|m| m.order_by.clone()).unwrap_or_default(),
10368 offset: modifiers.and_then(|m| m.offset.clone()),
10369 limit: modifiers.and_then(|m| m.limit.clone()),
10370 lock: None,
10371 };
10372 let mut path_select = self.compile_path_select_with_tail(&synthetic, &full_path, nested, false, tail, &[])?;
10373 Self::correlate_path_select(&mut path_select, alias);
10374 let expr = if multi {
10378 IrExpr::ArrayFromSelect(Box::new(IrArraySource::PathSelect(Box::new(path_select))))
10379 } else {
10380 IrExpr::ObjectPathSubquery(Box::new(path_select))
10381 };
10382 Ok(IrShapePointer::Computed(IrComputedPointer {
10383 marker_offset: None,
10384 alias: pointer_name.to_string(),
10385 expr,
10386 }))
10387 }
10388
10389 fn compile_multilink_pointer(
10390 &mut self,
10391 output_alias: &str,
10392 ml_name: &str,
10393 td: &TypeDescriptor,
10394 _parent_alias: &str,
10395 module: &str,
10396 el: &ShapeElement,
10397 ) -> Result<IrShapePointer, PyQLError> {
10398 let ml = Self::resolve_multilink(td, ml_name)
10399 .expect("caller verified multilink exists")
10400 .clone();
10401 let target_td = self.resolve_type(&ml.target)?;
10402 let sub_alias = self.fresh_alias();
10403 let nested_elements = el.nested.as_deref().unwrap_or(&[]);
10404
10405 let mut link_properties: Vec<IrLinkProp> = Vec::new();
10407 let mut regular_els: Vec<ShapeElement> = Vec::new();
10408 for nel in nested_elements {
10409 if let [ast::PathStep::LinkProp(name)] = nel.path.steps.as_slice() {
10410 link_properties.push(IrLinkProp { name: name.clone() });
10411 } else {
10412 regular_els.push(nel.clone());
10413 }
10414 }
10415 let has_splat = nested_elements.iter().any(|nel| {
10416 nel.splat.is_some() && !matches!(nel.path.steps.first(), Some(ast::PathStep::TypeIntersection(_)))
10417 });
10418 if has_splat {
10419 for prop in self.splat_link_properties(&ml)? {
10420 if !link_properties.iter().any(|existing| existing.name == prop.name) {
10421 link_properties.push(prop);
10422 }
10423 }
10424 }
10425
10426 let sub_shape = self.compile_shape(®ular_els, target_td, &sub_alias, &target_td.module.clone())?;
10427
10428 let join = if let Some(through_qname) = &ml.through {
10429 let through_td = self.resolve_type(through_qname)?;
10430 if through_td.junction {
10431 IrMultiLinkJoin::Standard {
10435 junction_table: self.owner_junction(td, ml_name, Some(through_td)),
10436 module: td.module.clone(),
10437 }
10438 } else {
10439 let source_qname = format!("{}::{}", td.module, td.name);
10440 let source_col = through_td
10441 .links
10442 .iter()
10443 .find(|l| l.target == source_qname)
10444 .ok_or_else(|| {
10445 PyQLError::Type(PyQLTypeError {
10446 message: format!("through type {through_qname} has no link to source type {source_qname}"),
10447 position: Position { line: 0, col: 0 },
10448 })
10449 })?
10450 .name
10451 .clone();
10452 let target_col = through_td
10453 .links
10454 .iter()
10455 .find(|l| l.target == ml.target && l.name != source_col)
10456 .or_else(|| through_td.links.iter().find(|l| l.target == ml.target))
10457 .ok_or_else(|| {
10458 PyQLError::Type(PyQLTypeError {
10459 message: format!("through type {through_qname} has no link to target type {}", ml.target),
10460 position: Position { line: 0, col: 0 },
10461 })
10462 })?
10463 .name
10464 .clone();
10465 IrMultiLinkJoin::Through {
10466 junction_table: through_td.table.clone(),
10467 module: through_td.module.clone(),
10468 source_col,
10469 target_col,
10470 }
10471 }
10472 } else {
10473 IrMultiLinkJoin::Standard {
10474 junction_table: self.owner_junction(td, ml_name, None),
10475 module: module.to_string(),
10476 }
10477 };
10478
10479 self.link_prop_scope
10482 .push(ml.through.clone().map(|t| (t, "jt".to_string())));
10483 let modifiers = (|c: &mut Self| -> Result<SelectModifiers, PyQLError> {
10484 Ok((
10485 el.filter
10486 .as_ref()
10487 .map(|f| c.compile_expr(f, target_td, &sub_alias))
10488 .transpose()?,
10489 el.order_by
10490 .iter()
10491 .map(|s| c.compile_sort(s, target_td, &sub_alias))
10492 .collect::<Result<_, _>>()?,
10493 el.offset
10494 .as_ref()
10495 .map(|e| c.compile_expr(e, target_td, &sub_alias))
10496 .transpose()?,
10497 el.limit
10498 .as_ref()
10499 .map(|e| c.compile_expr(e, target_td, &sub_alias))
10500 .transpose()?,
10501 ))
10502 })(self);
10503 self.link_prop_scope.pop();
10504 let (filter, order_by, offset, limit) = modifiers?;
10505 let subquery = IrSelect {
10506 rows: vec![IrRowSource::Bound {
10507 source: IrSource {
10508 poly: self.link_target_fanout(target_td),
10509 type_name: format!("{}::{}", target_td.module, target_td.name),
10510 table: target_td.table.clone(),
10511 alias: sub_alias.clone(),
10512 },
10513 shape: sub_shape,
10514 }],
10515 filter,
10516 order_by,
10517 offset,
10518 limit,
10519 distinct: false,
10520 dml_source: None,
10521 polymorphic: false,
10522 poly_implementors: vec![],
10523 poly_columns: vec![],
10524 lock: None,
10525 };
10526
10527 Ok(IrShapePointer::MultiLink(IrMultiLinkPointer {
10528 marker_offset: el.marker_offset,
10529 alias: output_alias.to_string(),
10530 join,
10531 subquery,
10532 link_properties,
10533 single: false,
10534 }))
10535 }
10536
10537 fn compile_backlink_pointer(
10547 &mut self,
10548 output_alias: &str,
10549 path: &ast::Path,
10550 current_qname: &str,
10551 nested_elements: &[ShapeElement],
10552 marker_offset: Option<usize>,
10553 modifiers: Option<&ast::SelectStmt>,
10554 ) -> Result<IrShapePointer, PyQLError> {
10555 use ast::PathStep;
10556
10557 let backlink_name = match path.steps.first() {
10558 Some(PathStep::Backlink(n)) => n.clone(),
10559 _ => return Err(self.type_err("internal: expected backlink step")),
10560 };
10561 let type_ref = match path.steps.get(1) {
10562 Some(PathStep::TypeIntersection(tr)) => tr,
10563 _ => {
10564 return Err(PyQLError::Type(PyQLTypeError {
10565 message: format!(
10566 "backlink '.< {backlink_name}' requires a type intersection, \
10567 e.g.: .< {backlink_name}[is SomeType]"
10568 ),
10569 position: Position { line: 0, col: 0 },
10570 }));
10571 }
10572 };
10573 if path.steps.len() > 2 {
10574 return Err(self.type_err(
10575 "further path traversal after a backlink shape is not yet supported \
10576 (e.g. '.<link[is Type].property') — attach a nested shape instead: \
10577 '.<link[is Type] { property }'",
10578 ));
10579 }
10580
10581 let type_name = match &type_ref.module {
10582 Some(m) => format!("{}::{}", m, type_ref.name),
10583 None => type_ref.name.clone(),
10584 };
10585 let owner_td = self.backlink_owner(self.resolve_type(&type_name)?, &backlink_name, current_qname)?;
10586 let owner_qname = format!("{}::{}", owner_td.module, owner_td.name);
10587
10588 let join = if let Some(l) = owner_td
10589 .links
10590 .iter()
10591 .find(|l| l.name == backlink_name && self.link_target_reaches(&l.target, current_qname))
10592 {
10593 if l.is_junction_backed() {
10594 let (junction_table, module, owner_col, current_col, _) = self.link_junction_info(owner_td, l)?;
10595 IrMultiLinkJoin::BacklinkJunction {
10596 junction_table,
10597 module,
10598 owner_col,
10599 current_col,
10600 }
10601 } else {
10602 IrMultiLinkJoin::BacklinkFk {
10603 fk_col: format!("{}_id", backlink_name),
10604 }
10605 }
10606 } else if let Some(ml) = owner_td
10607 .multilinks
10608 .iter()
10609 .find(|ml| ml.name == backlink_name && self.link_target_reaches(&ml.target, current_qname))
10610 .cloned()
10611 {
10612 let (junction_table, module, _, _, _) = self.multilink_junction_info(owner_td, &ml)?;
10613 IrMultiLinkJoin::BacklinkJunction {
10614 junction_table,
10615 module,
10616 owner_col: "source".to_string(),
10617 current_col: "target".to_string(),
10618 }
10619 } else {
10620 return Err(PyQLError::Type(PyQLTypeError {
10621 message: format!(
10622 "type {} has no link or multi-link '{}' pointing to {}",
10623 owner_qname, backlink_name, current_qname,
10624 ),
10625 position: Position { line: 0, col: 0 },
10626 }));
10627 };
10628
10629 let sub_alias = self.fresh_alias();
10630 let sub_shape = self.compile_shape(nested_elements, owner_td, &sub_alias, &owner_td.module.clone())?;
10631
10632 let (filter, order_by, offset, limit) = match modifiers {
10641 Some(sel) => self.compile_path_modifiers(sel, owner_td, &sub_alias)?,
10642 None => (None, vec![], None, None),
10643 };
10644 let subquery = IrSelect {
10645 rows: vec![IrRowSource::Bound {
10646 source: IrSource {
10647 poly: self.poly_fanout_for(&owner_qname),
10648 type_name: owner_qname,
10649 table: owner_td.table.clone(),
10650 alias: sub_alias.clone(),
10651 },
10652 shape: sub_shape,
10653 }],
10654 filter,
10655 order_by,
10656 offset,
10657 limit,
10658 distinct: false,
10659 dml_source: None,
10660 polymorphic: false,
10661 poly_implementors: vec![],
10662 poly_columns: vec![],
10663 lock: None,
10664 };
10665
10666 Ok(IrShapePointer::MultiLink(IrMultiLinkPointer {
10667 alias: output_alias.to_string(),
10668 join,
10669 subquery,
10670 link_properties: vec![],
10671 marker_offset,
10672 single: self.backlink_is_single(owner_td, &backlink_name, current_qname) || limits_to_one(modifiers),
10673 }))
10674 }
10675
10676 fn compile_fn_object_source(
10684 &mut self,
10685 fc: &ast::FunctionCall,
10686 nested: &[ShapeElement],
10687 ) -> Result<Option<IrFunctionSelect>, PyQLError> {
10688 let Some(fd) = self.schema.functions.iter().find(|f| {
10689 let module_matches = fc.module.as_deref().map(|m| m == f.module.as_str()).unwrap_or(true);
10690 module_matches && f.name == fc.name && f.return_is_object
10691 }) else {
10692 return Ok(None);
10693 };
10694 let (fn_module, fn_name, return_type_name) = (fd.module.clone(), fd.name.clone(), fd.return_pg_type.clone());
10695 let return_td = self.resolve_type(&return_type_name)?;
10696 let alias = self.fresh_alias();
10697 let shape = if nested.is_empty() {
10698 Self::pk_returning(return_td)
10699 } else {
10700 self.compile_shape(nested, return_td, &alias, &return_td.module)?
10701 };
10702 let mut args = fc
10703 .args
10704 .iter()
10705 .map(|a| self.compile_free_expr(a))
10706 .collect::<Result<Vec<_>, _>>()?;
10707 if let Some(globals) = self.globals_arg_for_call(&format!("{fn_module}::{fn_name}"))? {
10710 args.insert(0, globals);
10711 }
10712 Ok(Some(IrFunctionSelect {
10713 fn_module,
10714 fn_name,
10715 fn_args: args,
10716 alias,
10717 type_name: format!("{}::{}", return_td.module, return_td.name),
10718 polymorphic: false,
10719 poly_implementors: vec![],
10720 poly_columns: vec![],
10721 shape,
10722 filter: None,
10723 order_by: vec![],
10724 offset: None,
10725 limit: None,
10726 distinct: false,
10727 }))
10728 }
10729
10730 fn asserted_pointer_expr(expr: &Expr, nested: &[ShapeElement]) -> Option<(ast::FunctionCall, Expr, Option<Expr>)> {
10738 if let Expr::Shape(sh) = expr
10746 && let Some(subquery @ Expr::SubQuery(_)) = sh.expr.as_ref()
10747 {
10748 return Self::asserted_pointer_expr(subquery, &sh.elements);
10749 }
10750 if let Expr::SubQuery(stmt) = expr
10751 && let Stmt::Select(sel) = stmt.as_ref()
10752 {
10753 let (call, inner, _) = Self::asserted_pointer_expr(&sel.result, nested)?;
10754 let rebuild = |offset: Option<Expr>, limit: Option<Expr>| {
10755 Expr::SubQuery(Box::new(Stmt::Select(ast::SelectStmt {
10756 result: inner.clone(),
10757 filter: sel.filter.clone(),
10758 order_by: sel.order_by.clone(),
10759 offset,
10760 limit,
10761 lock: sel.lock.clone(),
10762 })))
10763 };
10764 let value = rebuild(sel.offset.clone(), sel.limit.clone());
10765 let check = (sel.offset.is_some() || sel.limit.is_some()).then(|| rebuild(None, None));
10766 return Some((call, value, check));
10767 }
10768 let (call, nested) = match expr {
10772 Expr::Shape(sh) => match sh.expr.as_ref() {
10773 Some(Expr::FunctionCall(f)) => (f, sh.elements.clone()),
10774 _ => return None,
10775 },
10776 Expr::FunctionCall(f) => (f, nested.to_vec()),
10777 _ => return None,
10778 };
10779 if call.module.is_some() && call.module.as_deref() != Some("std") {
10780 return None;
10781 }
10782 if !matches!(call.name.as_str(), "assert_exists" | "assert_distinct") {
10783 return None;
10784 }
10785 let [arg] = call.args.as_slice() else {
10786 return None;
10787 };
10788 let inner = match (&arg, nested.is_empty()) {
10793 (Expr::Shape(_), _) | (_, true) => arg.clone(),
10794 _ => Expr::Shape(Box::new(ast::ShapeExpr {
10795 expr: Some(arg.clone()),
10796 elements: nested.clone(),
10797 marker_offset: None,
10798 })),
10799 };
10800 Some((call.clone(), inner, None))
10801 }
10802
10803 fn object_call_with_shape<'e>(
10807 expr: &'e Expr,
10808 nested: Option<&[ShapeElement]>,
10809 ) -> Option<(&'e ast::FunctionCall, Vec<ShapeElement>)> {
10810 match expr {
10811 Expr::Shape(sh) => match sh.expr.as_ref() {
10812 Some(Expr::FunctionCall(f)) => Some((f, sh.elements.clone())),
10813 _ => None,
10814 },
10815 Expr::FunctionCall(f) => Some((f, nested.unwrap_or(&[]).to_vec())),
10816 _ => None,
10817 }
10818 }
10819
10820 fn compile_declared_computed(
10821 &mut self,
10822 cd: &crate::schema::ComputedDescriptor,
10823 td: &TypeDescriptor,
10824 alias: &str,
10825 module: &str,
10826 marker_offset: Option<usize>,
10827 nested: &[ShapeElement],
10828 ) -> Result<IrShapePointer, PyQLError> {
10829 let expr_ast = crate::parse::parse_pointer_expr(&cd.expression).map_err(PyQLError::Syntax)?;
10830 let declared_on = self.computed_declared_on(td, &cd.name);
10831 self.compile_computed_expr(
10832 &cd.name,
10833 &expr_ast,
10834 td,
10835 alias,
10836 module,
10837 marker_offset,
10838 nested,
10839 declared_on,
10840 )
10841 }
10842
10843 fn computed_declared_on(&self, td: &TypeDescriptor, name: &str) -> Option<(String, String)> {
10846 td.bases
10847 .iter()
10848 .chain(td.parents.iter())
10849 .chain(td.interfaces.iter())
10850 .filter_map(|qualified| self.resolve_type(qualified).ok())
10851 .find(|ancestor| ancestor.computed.iter().any(|c| c.name == name))
10852 .map(|ancestor| (ancestor.name.clone(), format!("{}::{}", ancestor.module, ancestor.name)))
10853 }
10854
10855 #[allow(clippy::too_many_arguments)]
10858 fn compile_computed_expr(
10859 &mut self,
10860 name: &str,
10861 expr_ast: &Expr,
10862 td: &TypeDescriptor,
10863 alias: &str,
10864 module: &str,
10865 marker_offset: Option<usize>,
10866 nested: &[ShapeElement],
10867 declared_on: Option<(String, String)>,
10868 ) -> Result<IrShapePointer, PyQLError> {
10869 self.anchors.push(SelectAnchor {
10873 type_name: td.name.clone(),
10874 qualified: format!("{}::{}", td.module, td.name),
10875 alias: alias.to_string(),
10876 detached: false,
10877 declared_on,
10878 });
10879 let result = (|compiler: &mut Self| -> Result<IrShapePointer, PyQLError> {
10880 if let Some(ptr) =
10881 compiler.try_compile_pointer_expr(name, expr_ast, td, alias, module, marker_offset, nested)?
10882 {
10883 return Ok(ptr);
10884 }
10885 if let Expr::FunctionCall(fc) = expr_ast
10890 && let Some(fs) = compiler.compile_fn_object_source(fc, nested)?
10891 {
10892 return Ok(IrShapePointer::Computed(IrComputedPointer {
10893 marker_offset,
10894 alias: name.to_string(),
10895 expr: IrExpr::ArrayFromSelect(Box::new(IrArraySource::ObjectFunction(Box::new(fs)))),
10896 }));
10897 }
10898 let ir = compiler.compile_expr(expr_ast, td, alias)?;
10899 Ok(IrShapePointer::Computed(IrComputedPointer {
10900 marker_offset,
10901 alias: name.to_string(),
10902 expr: ir,
10903 }))
10904 })(self);
10905 self.anchors.pop();
10906 result
10907 }
10908
10909 fn split_path_result(result: &Expr) -> Option<(&ast::Path, &[ShapeElement])> {
10913 match result {
10914 Expr::Path(p) => Some((p, &[])),
10915 Expr::Detached(inner) => Self::split_path_result(inner),
10919 Expr::Shape(sh) => match &sh.expr {
10920 Some(Expr::Path(p)) => Some((p, sh.elements.as_slice())),
10921 _ => None,
10922 },
10923 _ => None,
10924 }
10925 }
10926
10927 fn function_subject(e: &Expr) -> Option<(&ast::FunctionCall, Option<&ast::SelectStmt>)> {
10931 match e {
10932 Expr::FunctionCall(fc) => Some((fc, None)),
10933 Expr::SubQuery(stmt) => match stmt.as_ref() {
10934 Stmt::Select(sel) => match &sel.result {
10935 Expr::FunctionCall(fc) => Some((fc, Some(sel))),
10936 _ => None,
10937 },
10938 _ => None,
10939 },
10940 _ => None,
10941 }
10942 }
10943
10944 fn resolve_object_fn(&self, fc: &ast::FunctionCall) -> Option<&'a FunctionDescriptor> {
10946 self.schema.functions.iter().find(|f| {
10947 let module_matches = fc.module.as_deref().map(|m| m == f.module.as_str()).unwrap_or(true);
10948 module_matches && f.name == fc.name && f.return_is_object
10949 })
10950 }
10951
10952 fn replaced_subject_shape(e: &Expr) -> &[ShapeElement] {
10957 let Expr::Shape(sh) = e else { return &[] };
10958 if sh.elements.is_empty() {
10959 return &[];
10960 }
10961 match sh.expr.as_ref() {
10962 Some(inner) => Self::pointer_subject(inner).map(|(_, nested, _)| nested).unwrap_or(&[]),
10963 None => &[],
10964 }
10965 }
10966
10967 fn pointer_subject(e: &Expr) -> Option<(&ast::Path, &[ShapeElement], Option<&ast::SelectStmt>)> {
10972 match e {
10973 Expr::Path(p) => Some((p, &[], None)),
10974 Expr::SubQuery(stmt) => match stmt.as_ref() {
10975 Stmt::Select(sel) => {
10976 let (p, inner) = Self::split_path_result(&sel.result)?;
10977 Some((p, inner, Some(sel)))
10978 }
10979 _ => None,
10980 },
10981 Expr::Shape(sh) => {
10982 let (path, inner, modifiers) = Self::pointer_subject(sh.expr.as_ref()?)?;
10983 let nested = if sh.elements.is_empty() {
10984 inner
10985 } else {
10986 sh.elements.as_slice()
10987 };
10988 Some((path, nested, modifiers))
10989 }
10990 _ => None,
10991 }
10992 }
10993
10994 fn field_access_over_select(expr: &Expr) -> Option<(ast::SelectStmt, usize)> {
11000 let (root, fields) = Self::peel_field_access_chain(expr);
11001 if fields.is_empty() {
11002 return None;
11003 }
11004 let Expr::SubQuery(stmt) = root else { return None };
11005 let Stmt::Select(sel) = stmt.as_ref() else {
11006 return None;
11007 };
11008 let Expr::Path(path) = &sel.result else { return None };
11009 if !path.partial {
11010 return None;
11011 }
11012 let mut steps = path.steps.clone();
11013 let count = fields.len();
11014 steps.extend(fields.into_iter().map(ast::PathStep::Name));
11015 Some((
11016 ast::SelectStmt {
11017 result: Expr::Path(ast::Path { steps, partial: true }),
11018 ..sel.clone()
11019 },
11020 count,
11021 ))
11022 }
11023
11024 #[allow(clippy::too_many_arguments)]
11045 fn try_compile_pointer_expr(
11046 &mut self,
11047 pointer_name: &str,
11048 compexpr: &Expr,
11049 td: &TypeDescriptor,
11050 alias: &str,
11051 module: &str,
11052 marker_offset: Option<usize>,
11053 nested_override: &[ShapeElement],
11054 ) -> Result<Option<IrShapePointer>, PyQLError> {
11055 if let Some((sel, tail)) = Self::field_access_over_select(compexpr)
11065 && let Expr::Path(path) = &sel.result
11066 {
11067 let (head_steps, tail_steps) = path.steps.split_at(path.steps.len() - tail);
11068 let (head_multi, head_target) = self.walk_path_types(td, head_steps, MAX_COMPUTED_SPLICES);
11069 let Some(head_target) = head_target else {
11070 return Ok(None);
11071 };
11072 let (tail_multi, target) = self.walk_path_types(head_target, tail_steps, MAX_COMPUTED_SPLICES);
11073 if target.is_none() {
11074 return Ok(None);
11075 }
11076 let multi = tail_multi || (head_multi && !limits_to_one(Some(&sel)));
11077 if !multi && nested_override.is_empty() {
11078 return Ok(None);
11079 }
11080 let path = path.clone();
11081 return self
11082 .compile_chained_link_pointer(pointer_name, &path, td, alias, nested_override, Some(&sel), multi, tail)
11083 .map(Some);
11084 }
11085 let Some((path, declared_nested, modifiers)) = Self::pointer_subject(compexpr) else {
11086 return Ok(None);
11087 };
11088 if !path.partial {
11089 return Ok(None);
11090 }
11091 let nested = if nested_override.is_empty() {
11095 declared_nested
11096 } else {
11097 nested_override
11098 };
11099
11100 let ml_name = match path.steps.as_slice() {
11104 [ast::PathStep::Name(n)] if Self::resolve_multilink(td, n).is_some() => n.clone(),
11105 [ast::PathStep::Backlink(_)] | [ast::PathStep::Backlink(_), ast::PathStep::TypeIntersection(_)] => {
11110 let current_qname = format!("{}::{}", td.module, td.name);
11111 let path = path.clone();
11112 let nested = nested.to_vec();
11113 return self
11114 .compile_backlink_pointer(pointer_name, &path, ¤t_qname, &nested, marker_offset, modifiers)
11115 .map(Some);
11116 }
11117 steps
11127 if matches!(
11128 steps.first(),
11129 Some(ast::PathStep::Name(_) | ast::PathStep::TypeIntersection(_))
11130 ) =>
11131 {
11132 let (multi, target) = self.walk_path_types(td, steps, MAX_COMPUTED_SPLICES);
11133 if target.is_none() || (!multi && nested.is_empty()) {
11139 return Ok(None);
11140 }
11141 let path = path.clone();
11142 let nested = nested.to_vec();
11143 return self
11144 .compile_chained_link_pointer(
11145 pointer_name,
11146 &path,
11147 td,
11148 alias,
11149 &nested,
11150 modifiers,
11151 multi && !limits_to_one(modifiers),
11152 0,
11153 )
11154 .map(Some);
11155 }
11156 _ => return Ok(None),
11157 };
11158
11159 let synthetic = ShapeElement {
11160 path: path.clone(),
11161 splat: None,
11162 nested: Some(nested.to_vec()),
11163 compexpr: None,
11164 op: ast::ShapeOp::Assign,
11165 filter: modifiers.and_then(|s| s.filter.clone()),
11166 order_by: modifiers.map(|s| s.order_by.clone()).unwrap_or_default(),
11167 offset: modifiers.and_then(|s| s.offset.clone()),
11168 limit: modifiers.and_then(|s| s.limit.clone()),
11169 marker_offset,
11170 };
11171 let mut pointer = self.compile_multilink_pointer(pointer_name, &ml_name, td, alias, module, &synthetic)?;
11172 if let IrShapePointer::MultiLink(link) = &mut pointer {
11173 link.single = limits_to_one(modifiers);
11174 }
11175 Ok(Some(pointer))
11176 }
11177
11178 fn compile_subquery_expr(
11198 &mut self,
11199 stmt: &Stmt,
11200 extra_fields: &[String],
11201 ctx: Option<(&TypeDescriptor, &str)>,
11202 outer_shape: &[ShapeElement],
11203 ) -> Result<IrExpr, PyQLError> {
11204 if let Stmt::With(w) = stmt {
11208 for alias in &w.aliases {
11209 if self.bind_inline_if_correlated(&alias.name, &alias.expr, ctx)?
11210 || self.bind_group(&alias.name, &alias.expr)?
11211 {
11212 continue;
11213 }
11214 match self.hoisted_binding_sources.get(&alias.name) {
11222 Some(existing) if *existing == alias.expr => continue,
11223 Some(_) => {
11224 return Err(self.type_err(&format!(
11225 "two different `with` bindings named '{}' end up in one statement; \
11226 rename one of them",
11227 alias.name
11228 )));
11229 }
11230 None => {}
11231 }
11232 let (ir_stmt, correlated_to) = self.compile_binding_in_scope(&alias.expr)?;
11233 let sql_name = self.claim_cte_sql_name(&alias.name);
11234 let type_name = self.register_cte(&alias.name, &ir_stmt);
11235 self.hoisted_binding_sources
11236 .insert(alias.name.clone(), alias.expr.clone());
11237 self.hoisted_ctes.push(IrCteDef {
11238 name: sql_name,
11239 stmt: ir_stmt,
11240 type_name,
11241 correlated_to,
11242 });
11243 }
11244 let inner = (*w.stmt).clone();
11245 return self.compile_subquery_expr(&inner, extra_fields, ctx, outer_shape);
11246 }
11247
11248 if extra_fields.is_empty()
11249 && let Stmt::Select(sel) = stmt
11250 && let Expr::Shape(sh) = &sel.result
11251 && let Some(Expr::SubQuery(subject)) = &sh.expr
11252 && matches!(subject.as_ref(), Stmt::Group(_))
11253 && let IrStmt::Group(grp) = self.compile_stmt(stmt)?
11254 {
11255 return Ok(IrExpr::ArrayFromSelect(Box::new(IrArraySource::Group(Box::new(grp)))));
11256 }
11257
11258 if let Stmt::Select(inner) = stmt
11262 && let Expr::Shape(sh) = &inner.result
11263 && let Some(subject) = sh.expr.as_ref()
11264 && let Some(operands) = Self::union_of_relative_paths(subject)
11265 && let Some((td, alias)) = ctx
11266 {
11267 let elements = sh.elements.clone();
11268 let operands_reach_many = self.relative_paths_reach_many(td, &operands);
11269 let mut branches = Vec::with_capacity(operands.len());
11270 for path in operands {
11271 let mut steps = vec![ast::PathStep::Name(format!("{}::{}", td.module, td.name))];
11272 steps.extend(path.steps.iter().cloned());
11273 let rooted = ast::Path { steps, partial: false };
11274 let synthetic = ast::SelectStmt {
11275 result: Expr::Path(rooted.clone()),
11276 filter: inner.filter.clone(),
11277 order_by: vec![],
11278 offset: None,
11279 limit: None,
11280 lock: None,
11281 };
11282 let mut ps = self.compile_path_select(&synthetic, &rooted, &elements, false)?;
11283 Self::correlate_path_select(&mut ps, alias);
11284 branches.push(ps);
11285 }
11286 let limit = inner
11287 .limit
11288 .as_ref()
11289 .map(|l| self.compile_free_expr(l))
11290 .transpose()?
11291 .map(Box::new);
11292 let multi = !matches!(limit.as_deref(), Some(IrExpr::Literal(IrLiteral::Int(1)))) && operands_reach_many;
11293 return Ok(IrExpr::ObjectPathUnion { branches, limit, multi });
11294 }
11295 if matches!(stmt, Stmt::Insert(_) | Stmt::Update(_) | Stmt::Delete(_))
11306 && !extra_fields.is_empty()
11307 && ctx.is_none()
11308 {
11309 let type_name = self.dml_subject_type(stmt)?;
11310 let inner = self.compile_stmt(stmt)?;
11311 let cte_name = self.fresh_nested_cte_name();
11312 self.register_cte(&cte_name, &inner);
11318 self.hoisted_ctes.push(IrCteDef {
11319 name: cte_name.clone(),
11320 stmt: inner,
11321 type_name,
11322 correlated_to: None,
11323 });
11324 let mut steps = vec![ast::PathStep::Name(cte_name)];
11325 steps.extend(extra_fields.iter().cloned().map(ast::PathStep::Name));
11326 return self.compile_free_path(&ast::Path { steps, partial: false });
11327 }
11328
11329 let Stmt::Select(sel) = stmt else {
11330 return Err(self.subquery_expr_err(stmt));
11331 };
11332 let has_modifiers =
11333 sel.filter.is_some() || !sel.order_by.is_empty() || sel.offset.is_some() || sel.limit.is_some();
11334
11335 let Some((path, shape_els)) = Self::split_path_result(&sel.result) else {
11336 if let Expr::FunctionCall(fc) = &sel.result {
11339 let fc = fc.clone();
11340 if let Some(ir) = self.try_compile_fn_scalar_subquery(&fc, extra_fields, Some(sel), ctx)? {
11341 return Ok(ir);
11342 }
11343 }
11344 if has_modifiers {
11349 let inner = self.compile_stmt(stmt)?;
11350 let IrStmt::Select(select) = inner else {
11351 return Err(self.subquery_expr_err(stmt));
11352 };
11353 if !select
11354 .rows
11355 .iter()
11356 .all(|r| matches!(r, IrRowSource::Free(IrFreeExpr::Scalar(_))))
11357 {
11358 return Err(self.subquery_expr_err(stmt));
11359 }
11360 let mut ir = IrExpr::ScalarSubquery(Box::new(select));
11361 for field in extra_fields {
11362 ir = Self::project_free_object_field(ir, field);
11363 }
11364 return Ok(ir);
11365 }
11366 let mut ir = self.compile_expr_ctx(&sel.result, ctx)?;
11367 for field in extra_fields {
11368 ir = Self::project_free_object_field(ir, field);
11369 }
11370 return Ok(ir);
11371 };
11372 let rewritten_sel;
11379 let mut sel = sel;
11380 let mut shape_els = shape_els;
11381 let mut extra_steps: Vec<ast::PathStep> = extra_fields.iter().map(|f| ast::PathStep::Name(f.clone())).collect();
11382 if !shape_els.is_empty()
11383 && let Some(defs) = Self::shape_alias_paths(shape_els)
11384 {
11385 extra_steps = extra_fields
11386 .iter()
11387 .flat_map(|field| match defs.iter().find(|(name, _)| name == field) {
11388 Some((_, definition)) => definition.steps.clone(),
11389 None => vec![ast::PathStep::Name(field.clone())],
11390 })
11391 .collect();
11392 rewritten_sel = ast::SelectStmt {
11393 result: sel.result.clone(),
11394 filter: sel.filter.clone().map(|f| Self::substitute_shape_aliases(f, &defs)),
11395 order_by: sel
11396 .order_by
11397 .iter()
11398 .map(|o| ast::SortExpr {
11399 expr: Self::substitute_shape_aliases(o.expr.clone(), &defs),
11400 direction: o.direction.clone(),
11401 nones: o.nones.clone(),
11402 })
11403 .collect(),
11404 offset: sel.offset.clone(),
11405 limit: sel.limit.clone(),
11406 lock: sel.lock.clone(),
11407 };
11408 sel = &rewritten_sel;
11409 shape_els = &[];
11410 }
11411
11412 if !shape_els.is_empty()
11423 && extra_fields.is_empty()
11424 && !path.partial
11425 && let [ast::PathStep::Name(type_name)] = path.steps.as_slice()
11426 && let Some(root_td) = match self.cte_object_type(type_name) {
11427 Some(_) => self.resolve_path_root(type_name).ok(),
11428 None => self.resolve_type(type_name).ok(),
11429 }
11430 {
11431 let alias = self.fresh_alias();
11432 let binding = self.cte_object_type(type_name).map(|_| type_name.clone());
11433 let table = match &binding {
11434 Some(name) => self.row_source_table(name, root_td),
11435 None => root_td.table.clone(),
11436 };
11437 let outer_declared = binding.as_ref().map(|name| {
11438 let declared = self.cte_declared_pointers.get(name).cloned().unwrap_or_default();
11439 std::mem::replace(&mut self.active_declared_pointers, declared)
11440 });
11441 let modifiers = self.compile_path_modifiers(sel, root_td, &alias);
11442 let module = root_td.module.clone();
11443 let shape = self.compile_shape(shape_els, root_td, &alias, &module);
11444 if let Some(outer) = outer_declared {
11445 self.active_declared_pointers = outer;
11446 }
11447 let (filter, order_by, offset, limit) = modifiers?;
11448 let shape = shape?;
11449 let source = IrSource {
11450 poly: self.poly_fanout_for(&format!("{}::{}", root_td.module, root_td.name)),
11451 type_name: format!("{}::{}", root_td.module, root_td.name),
11452 table,
11453 alias,
11454 };
11455 let single = selects_at_most_one(sel, root_td);
11456 let mut select = IrSelect::schema_bound(source, shape, filter);
11457 select.order_by = order_by;
11458 select.offset = offset;
11459 select.limit = limit;
11460 return Ok(if single {
11463 IrExpr::ObjectSubquery(Box::new(select))
11464 } else {
11465 IrExpr::ArrayFromSelect(Box::new(IrArraySource::ObjectSelect(Box::new(select))))
11466 });
11467 }
11468
11469 if !shape_els.is_empty() && extra_fields.is_empty() {
11470 return Err(self.type_err(
11471 "a sub-select with a shape is not valid in expression context — \
11472 assign it to a computed pointer instead, or project a property \
11473 off it, e.g. '(select .emails limit 1).address'",
11474 ));
11475 }
11476
11477 if !path.partial
11480 && let [ast::PathStep::Name(name)] = path.steps.as_slice()
11481 && self.is_value_binding(name)
11482 {
11483 let mut ir = self.compile_expr_ctx(&sel.result, ctx)?;
11484 for field in extra_fields {
11485 ir = Self::project_free_object_field(ir, field);
11486 }
11487 return Ok(ir);
11488 }
11489
11490 if !path.partial
11494 && extra_fields.is_empty()
11495 && let [ast::PathStep::Name(type_name)] = path.steps.as_slice()
11496 && let Ok(root_td) = self.resolve_type(type_name)
11497 {
11498 let alias = self.fresh_alias();
11499 let (filter, order_by, offset, limit) = self.compile_path_modifiers(sel, root_td, &alias)?;
11500 let source = IrSource {
11501 poly: None,
11502 type_name: format!("{}::{}", root_td.module, root_td.name),
11503 table: root_td.table.clone(),
11504 alias,
11505 };
11506 let pk = root_td.properties.iter().find(|p| p.is_pk);
11510 let shape = vec![IrShapePointer::Scalar(IrScalarPointer {
11511 implicit_id: false,
11512 marker_offset: None,
11513 alias: "id".to_string(),
11514 column: pk.map(|p| p.name.clone()).unwrap_or_else(|| "id".to_string()),
11515 pg_type: pk.map(|p| p.pg_type.clone()).unwrap_or_else(|| "uuid".to_string()),
11516 tuple_shape: None,
11517 })];
11518 let mut select = IrSelect::schema_bound(source, shape, filter);
11519 select.order_by = order_by;
11520 select.offset = offset;
11521 select.limit = limit;
11522 return Ok(IrExpr::Subquery(Box::new(select)));
11523 }
11524
11525 let mut steps = path.steps.clone();
11526 let correlate = if path.partial {
11527 let Some((td, alias)) = ctx else {
11528 return Err(self.type_err(
11529 "a relative path in a sub-select needs an enclosing object — \
11530 write the type name explicitly, e.g. '(select Person.name)'",
11531 ));
11532 };
11533 steps.insert(0, ast::PathStep::Name(format!("{}::{}", td.module, td.name)));
11534 Some(alias.to_string())
11535 } else {
11536 None
11537 };
11538 steps.extend(extra_steps.iter().cloned());
11539 let full_path = ast::Path { steps, partial: false };
11540
11541 let mut ps = self.compile_path_select_with_tail(sel, &full_path, outer_shape, false, extra_steps.len(), &[])?;
11542 if let Some(outer_alias) = correlate {
11543 Self::correlate_path_select(&mut ps, &outer_alias);
11544 }
11545 if !outer_shape.is_empty() && matches!(ps.result, IrPathResult::Object { .. }) {
11550 return Ok(IrExpr::ObjectPathSubquery(Box::new(ps)));
11551 }
11552 let single = matches!(&sel.limit, Some(Expr::Literal(ast::Literal::Int(1))));
11558 let multi = match &full_path.steps[0] {
11559 ast::PathStep::Name(root) => {
11560 let root_td = self.resolve_path_root(root)?;
11561 let every_row_of_a_type = !path.partial
11564 && self.cte_object_type(root).is_none()
11565 && self.resolve_type(root).is_ok()
11566 && !selects_at_most_one(sel, root_td);
11567 every_row_of_a_type || self.path_crosses_multi(root_td, &full_path.steps[1..])
11568 }
11569 _ => false,
11570 };
11571 if multi && !single && matches!(ps.result, IrPathResult::Scalar(..)) {
11572 return Ok(IrExpr::ArrayFromSelect(Box::new(IrArraySource::PathSelect(Box::new(
11573 ps,
11574 )))));
11575 }
11576 Ok(IrExpr::PathSubquery(Box::new(ps)))
11577 }
11578
11579 fn try_compile_fn_scalar_subquery(
11594 &mut self,
11595 fc: &ast::FunctionCall,
11596 fields: &[String],
11597 modifiers: Option<&ast::SelectStmt>,
11598 ctx: Option<(&TypeDescriptor, &str)>,
11599 ) -> Result<Option<IrExpr>, PyQLError> {
11600 let fd = self.schema.functions.iter().find(|f| {
11601 let module_matches = fc.module.as_deref().map(|m| m == f.module.as_str()).unwrap_or(true);
11602 module_matches && f.name == fc.name && f.return_is_object
11603 });
11604 let Some(fd) = fd else { return Ok(None) };
11605 let (fn_module, fn_name, return_type_name, polymorphic, params) = (
11606 fd.module.clone(),
11607 fd.name.clone(),
11608 fd.return_pg_type.clone(),
11609 fd.return_is_polymorphic,
11610 fd.params.clone(),
11611 );
11612
11613 let qualified = format!("{fn_module}::{fn_name}");
11614 let [field] = fields else {
11615 return Err(self.type_err(&format!(
11616 "function '{qualified}' returns objects, so using it inside an expression needs \
11617 one of its properties, e.g. '{qualified}(…).name'"
11618 )));
11619 };
11620 if params.len() != fc.args.len() {
11621 return Err(self.type_err(&format!(
11622 "function '{qualified}' expects {} argument(s), got {}",
11623 params.len(),
11624 fc.args.len()
11625 )));
11626 }
11627
11628 let mut fn_args = fc
11629 .args
11630 .iter()
11631 .map(|a| self.compile_expr_ctx(a, ctx))
11632 .collect::<Result<Vec<_>, _>>()?;
11633 if let Some(globals) = self.globals_arg_for_call(&qualified)? {
11634 fn_args.insert(0, globals);
11635 }
11636
11637 let td = self.resolve_type(&return_type_name)?;
11638 let alias = self.fresh_alias();
11639 let Some(prop) = Self::resolve_property(td, field) else {
11640 return Err(self.field_err(field, &return_type_name));
11641 };
11642 let projected = IrShapePointer::Computed(IrComputedPointer {
11643 marker_offset: None,
11644 alias: field.clone(),
11645 expr: IrExpr::ColumnRef {
11646 alias: alias.clone(),
11647 column: prop.name.clone(),
11648 pg_type: prop.pg_type.clone(),
11649 },
11650 });
11651
11652 let (poly_implementors, poly_columns) = if polymorphic {
11653 self.collect_poly_info(&return_type_name)
11654 } else {
11655 (vec![], vec![])
11656 };
11657 let (filter, order_by, offset, limit) = match modifiers {
11658 Some(sel) => {
11659 let td = self.resolve_type(&return_type_name)?;
11660 self.compile_path_modifiers(sel, td, &alias)?
11661 }
11662 None => (None, vec![], None, None),
11663 };
11664
11665 Ok(Some(IrExpr::FnSubquery(Box::new(IrFunctionSelect {
11666 fn_module,
11667 fn_name,
11668 fn_args,
11669 alias,
11670 type_name: return_type_name,
11671 polymorphic,
11672 poly_implementors,
11673 poly_columns,
11674 shape: vec![projected],
11675 filter,
11676 order_by,
11677 offset,
11678 limit,
11679 distinct: false,
11680 }))))
11681 }
11682
11683 fn computed_is_object_valued(&self, cd: &crate::schema::ComputedDescriptor, td: &TypeDescriptor) -> bool {
11691 let Ok(expr) = crate::parse::parse_pointer_expr(&cd.expression) else {
11692 return false;
11693 };
11694 let expr = match Self::field_access_over_select(&expr) {
11695 Some((sel, _)) => Expr::SubQuery(Box::new(Stmt::Select(sel))),
11696 None => expr,
11697 };
11698 let Some((path, _, _)) = Self::pointer_subject(&expr) else {
11699 return false;
11700 };
11701 if !path.partial {
11702 return false;
11703 }
11704 match path.steps.as_slice() {
11705 [ast::PathStep::Backlink(_)] | [ast::PathStep::Backlink(_), ast::PathStep::TypeIntersection(_)] => true,
11709 steps => self.walk_path_types(td, steps, MAX_COMPUTED_SPLICES).1.is_some(),
11710 }
11711 }
11712
11713 fn path_crosses_multi(&self, td: &TypeDescriptor, steps: &[ast::PathStep]) -> bool {
11719 self.walk_path_types(td, steps, MAX_COMPUTED_SPLICES).0
11720 }
11721
11722 fn coalesce_path_branches(
11726 &mut self,
11727 td: &TypeDescriptor,
11728 alias: &str,
11729 operands: &[ast::Path],
11730 elements: &[ast::ShapeElement],
11731 ) -> Result<Vec<IrPathSelect>, PyQLError> {
11732 let mut branches: Vec<IrPathSelect> = Vec::with_capacity(operands.len());
11733 for path in operands {
11734 let mut steps = vec![ast::PathStep::Name(format!("{}::{}", td.module, td.name))];
11735 steps.extend(path.steps.iter().cloned());
11736 let rooted = ast::Path { steps, partial: false };
11737 let synthetic = ast::SelectStmt {
11738 result: Expr::Path(rooted.clone()),
11739 filter: None,
11740 order_by: vec![],
11741 offset: None,
11742 limit: None,
11743 lock: None,
11744 };
11745 let mut ps = self.compile_path_select(&synthetic, &rooted, elements, false)?;
11746 Self::correlate_path_select(&mut ps, alias);
11747 let guards: Vec<IrExpr> = branches
11748 .iter()
11749 .map(|earlier| {
11750 IrExpr::UnaryOp(Box::new(IrUnaryOp {
11751 op: ast::UnaryOpKind::Not,
11752 operand: IrExpr::UnaryOp(Box::new(IrUnaryOp {
11753 op: ast::UnaryOpKind::Exists,
11754 operand: IrExpr::PathSubquery(Box::new(earlier.clone())),
11755 })),
11756 }))
11757 })
11758 .collect();
11759 if !guards.is_empty() {
11760 ps.filter = and_conditions(ps.filter, guards);
11761 }
11762 branches.push(ps);
11763 }
11764 Ok(branches)
11765 }
11766
11767 fn path_lands_on_objects(&self, td: &TypeDescriptor, path: &ast::Path) -> bool {
11772 let Some(last) = path
11773 .steps
11774 .iter()
11775 .rposition(|step| matches!(step, ast::PathStep::Name(_) | ast::PathStep::Backlink(_)))
11776 else {
11777 return false;
11778 };
11779 match &path.steps[last] {
11780 ast::PathStep::Backlink(_) => true,
11781 ast::PathStep::Name(name) => self
11782 .walk_path_types(td, &path.steps[..last], MAX_COMPUTED_SPLICES)
11783 .1
11784 .is_some_and(|owner| {
11785 Self::resolve_multilink(owner, name).is_some() || Self::resolve_link(owner, name).is_some()
11786 }),
11787 _ => false,
11788 }
11789 }
11790
11791 fn relative_paths_reach_many(&self, td: &TypeDescriptor, operands: &[ast::Path]) -> bool {
11796 operands.iter().any(|path| {
11797 let (root_td, rest) = match path.steps.first() {
11798 Some(ast::PathStep::TypeIntersection(tr)) => {
11799 let name = match &tr.module {
11800 Some(module) => format!("{}::{}", module, tr.name),
11801 None => tr.name.clone(),
11802 };
11803 match self.resolve_type(&name) {
11804 Ok(resolved) => (resolved, &path.steps[1..]),
11805 Err(_) => return true,
11806 }
11807 }
11808 _ => (td, &path.steps[..]),
11809 };
11810 self.path_crosses_multi(root_td, rest)
11811 })
11812 }
11813
11814 fn walk_path_types(
11823 &self,
11824 td: &'a TypeDescriptor,
11825 steps: &[ast::PathStep],
11826 depth: usize,
11827 ) -> (bool, Option<&'a TypeDescriptor>) {
11828 let mut current = td;
11829 let mut multi = false;
11830 for (i, step) in steps.iter().enumerate() {
11831 match step {
11832 ast::PathStep::Backlink(backlink_name) => {
11836 let Some(ast::PathStep::TypeIntersection(tr)) = steps.get(i + 1) else {
11837 return (true, None);
11838 };
11839 let owner_name = match &tr.module {
11840 Some(m) => format!("{}::{}", m, tr.name),
11841 None => tr.name.clone(),
11842 };
11843 let current_qname = format!("{}::{}", current.module, current.name);
11844 let single = self
11845 .resolve_type(&owner_name)
11846 .is_ok_and(|owner| self.backlink_is_single(owner, backlink_name, ¤t_qname));
11847 multi |= !single;
11848 continue;
11849 }
11850 ast::PathStep::TypeIntersection(tr) => {
11851 let name = match &tr.module {
11852 Some(m) => format!("{}::{}", m, tr.name),
11853 None => tr.name.clone(),
11854 };
11855 match self.resolve_type(&name) {
11856 Ok(t) => current = t,
11857 Err(_) => return (multi, None),
11858 }
11859 }
11860 ast::PathStep::Name(n) => {
11861 if let Some(ml) = Self::resolve_multilink(current, n) {
11862 multi = true;
11863 match self.resolve_type(&ml.target) {
11864 Ok(t) => current = t,
11865 Err(_) => return (multi, None),
11866 }
11867 continue;
11868 }
11869 if let Some(target) = Self::resolve_link(current, n).map(|l| l.target.clone()) {
11870 match self.resolve_type(&target) {
11871 Ok(t) => current = t,
11872 Err(_) => return (multi, None),
11873 }
11874 continue;
11875 }
11876 if depth > 0
11879 && let Some(cd) = self.resolve_computed(current, n)
11880 && let Ok(expr) = crate::parse::parse_pointer_expr(&cd.expression)
11881 {
11882 if let Some((p, _, modifiers)) = Self::pointer_subject(&expr)
11883 && p.partial
11884 {
11885 let (m, t) = self.walk_path_types(current, &p.steps, depth - 1);
11886 let capped = modifiers
11889 .is_some_and(|m| matches!(&m.limit, Some(Expr::Literal(ast::Literal::Int(1)))));
11890 multi = multi || (m && !capped);
11891 match t {
11892 Some(t) => current = t,
11893 None => return (multi, None),
11894 }
11895 continue;
11896 }
11897 if let Some((fc, _)) = Self::function_subject(&expr)
11898 && let Some(fd) = self.resolve_object_fn(fc)
11899 {
11900 multi = multi || fd.return_is_set;
11901 match self.resolve_type(&fd.return_pg_type) {
11902 Ok(t) => current = t,
11903 Err(_) => return (multi, None),
11904 }
11905 continue;
11906 }
11907 }
11908 return (multi, None);
11910 }
11911 _ => return (multi, None),
11912 }
11913 }
11914 (multi, Some(current))
11915 }
11916
11917 fn compile_partial_path_as_subquery(
11932 &mut self,
11933 p: &ast::Path,
11934 td: &TypeDescriptor,
11935 alias: &str,
11936 ) -> Result<IrExpr, PyQLError> {
11937 let (root_name, rest) = match p.steps.first() {
11938 Some(ast::PathStep::TypeIntersection(tr)) => {
11939 let name = match &tr.module {
11940 Some(m) => format!("{}::{}", m, tr.name),
11941 None => tr.name.clone(),
11942 };
11943 (name, &p.steps[1..])
11944 }
11945 _ => (format!("{}::{}", td.module, td.name), &p.steps[..]),
11946 };
11947 let root_td = self.resolve_type(&root_name)?;
11948 let multi = self.path_crosses_multi(root_td, rest);
11949
11950 let mut steps = vec![ast::PathStep::Name(root_name)];
11951 steps.extend(rest.iter().cloned());
11952 let full_path = ast::Path { steps, partial: false };
11953 let synthetic = ast::SelectStmt {
11954 result: Expr::Path(full_path.clone()),
11955 filter: None,
11956 order_by: vec![],
11957 offset: None,
11958 limit: None,
11959 lock: None,
11960 };
11961 let mut ps = self.compile_path_select(&synthetic, &full_path, &[], false)?;
11962 Self::correlate_path_select(&mut ps, alias);
11963 if multi && matches!(ps.result, IrPathResult::Scalar(..)) {
11964 Ok(IrExpr::ArrayFromSelect(Box::new(IrArraySource::PathSelect(Box::new(
11965 ps,
11966 )))))
11967 } else {
11968 Ok(IrExpr::PathSubquery(Box::new(ps)))
11969 }
11970 }
11971
11972 fn relative_subselect(
11981 &mut self,
11982 stmt: &Stmt,
11983 ctx: Option<(&TypeDescriptor, &str)>,
11984 ) -> Result<Option<IrPathSelect>, PyQLError> {
11985 let (Some((td, alias)), Stmt::Select(sel)) = (ctx, stmt) else {
11986 return Ok(None);
11987 };
11988 let (path, shape): (&ast::Path, &[ShapeElement]) = match &sel.result {
11989 Expr::Path(p) if p.partial => (p, &[]),
11990 Expr::Shape(sh) => match sh.expr.as_ref() {
11991 Some(Expr::Path(p)) if p.partial => (p, sh.elements.as_slice()),
11992 _ => return Ok(None),
11993 },
11994 _ => return Ok(None),
11995 };
11996 let mut steps = vec![ast::PathStep::Name(format!("{}::{}", td.module, td.name))];
11997 steps.extend(path.steps.iter().cloned());
11998 let rooted = ast::Path { steps, partial: false };
11999 let synthetic = ast::SelectStmt {
12000 result: Expr::Path(rooted.clone()),
12001 filter: sel.filter.clone(),
12002 order_by: sel.order_by.clone(),
12003 offset: sel.offset.clone(),
12004 limit: sel.limit.clone(),
12005 lock: None,
12006 };
12007 let mut ps = self.compile_path_select(&synthetic, &rooted, shape, false)?;
12008 Self::correlate_path_select(&mut ps, alias);
12009 Ok(Some(ps))
12010 }
12011
12012 fn correlate_path_select(ps: &mut IrPathSelect, outer_alias: &str) {
12016 if outer_alias == "NEW" || outer_alias == "OLD" {
12019 ps.root.table = format!("@row:{outer_alias}");
12020 ps.root.poly = None;
12021 }
12022 let correlation = IrExpr::BinOp(Box::new(IrBinOp {
12023 left: IrExpr::ColumnRef {
12024 alias: ps.root.alias.clone(),
12025 column: "id".to_string(),
12026 pg_type: "uuid".to_string(),
12027 },
12028 op: ast::BinOpKind::Eq,
12029 right: IrExpr::ColumnRef {
12030 alias: outer_alias.to_string(),
12031 column: "id".to_string(),
12032 pg_type: "uuid".to_string(),
12033 },
12034 }));
12035 ps.filter = Some(match ps.filter.take() {
12036 Some(existing) => IrExpr::BinOp(Box::new(IrBinOp {
12037 left: correlation,
12038 op: ast::BinOpKind::And,
12039 right: existing,
12040 })),
12041 None => correlation,
12042 });
12043 }
12044
12045 fn subquery_expr_err(&self, stmt: &Stmt) -> PyQLError {
12049 let what = match stmt {
12050 Stmt::Insert(_) => "an insert",
12051 Stmt::Update(_) => "an update",
12052 Stmt::Delete(_) => "a delete",
12053 Stmt::With(_) => "a `with` block",
12054 Stmt::For(_) => "a `for` loop",
12055 Stmt::Group(_) => "a `group`",
12056 _ => "this sub-statement",
12057 };
12058 self.type_err(&format!(
12059 "{what} cannot stand in for a value — a sub-statement is only valid in expression \
12060 position as a select over a path, e.g. '(select .emails filter .primary limit 1).address'"
12061 ))
12062 }
12063
12064 fn set_operands(expr: &Expr) -> Option<Vec<&Expr>> {
12072 match expr {
12073 Expr::Set(elems) if elems.is_empty() => None,
12077 Expr::Set(elems) => Some(elems.iter().collect()),
12078 Expr::Union(left, right) => {
12079 let mut operands = Self::set_operands(left).unwrap_or_else(|| vec![left.as_ref()]);
12080 operands.extend(Self::set_operands(right).unwrap_or_else(|| vec![right.as_ref()]));
12081 Some(operands)
12082 }
12083 _ => None,
12084 }
12085 }
12086
12087 fn compile_expr_ctx(&mut self, expr: &Expr, ctx: Option<(&TypeDescriptor, &str)>) -> Result<IrExpr, PyQLError> {
12097 match expr {
12098 Expr::Path(p) => match ctx {
12105 Some((td, alias)) => self.compile_path(p, td, alias),
12106 None => self.compile_free_path(p),
12107 },
12108
12109 Expr::PathStepOn { .. } => match self.try_compile_walk_off_subquery(expr, ctx)? {
12112 Some(ir) => Ok(ir),
12113 None => Err(self.type_err(
12114 "a type intersection, link property or backlink needs a path, a binding \
12115 or a sub-select to walk off",
12116 )),
12117 },
12118
12119 Expr::Literal(lit) => Ok(IrExpr::Literal(match lit {
12120 Literal::Str(s) => IrLiteral::Str(s.clone()),
12121 Literal::Int(n) => IrLiteral::Int(*n),
12122 Literal::Float(f) => IrLiteral::Float(*f),
12123 Literal::Bool(b) => IrLiteral::Bool(*b),
12124 })),
12125
12126 Expr::Parameter(name) => {
12127 let index = self.param_index(name);
12128 Ok(IrExpr::Param { index })
12129 }
12130
12131 Expr::Global(name) => self.compile_global(name),
12132
12133 Expr::Index { expr: e, index: i } => {
12134 let ir_expr = self.compile_expr_ctx(e, ctx)?;
12135 if self.is_json_expr(e, &ir_expr) {
12136 return match &**i {
12137 Expr::Literal(Literal::Str(key)) => Ok(IrExpr::JsonbField {
12138 expr: Box::new(ir_expr),
12139 field: key.clone(),
12140 }),
12141 Expr::Literal(Literal::Int(n)) if *n >= 0 => Ok(IrExpr::JsonbIndex {
12142 expr: Box::new(ir_expr),
12143 index: *n as usize,
12144 }),
12145 _ => Err(self.type_err("indexing json needs a literal key or a literal position")),
12146 };
12147 }
12148 let ir_index = self.compile_expr_ctx(i, ctx)?;
12149 let is_array = is_array_expr(&ir_expr);
12150 Ok(IrExpr::Subscript {
12151 expr: Box::new(ir_expr),
12152 index: Box::new(ir_index),
12153 is_array,
12154 })
12155 }
12156
12157 Expr::Slice {
12158 expr: e,
12159 lower: lo,
12160 upper: hi,
12161 } => {
12162 let ir_expr = self.compile_expr_ctx(e, ctx)?;
12163 let is_array = is_array_expr(&ir_expr);
12164 let ir_lower = lo.as_ref().map(|x| self.compile_expr_ctx(x, ctx)).transpose()?;
12165 let ir_upper = hi.as_ref().map(|x| self.compile_expr_ctx(x, ctx)).transpose()?;
12166 Ok(IrExpr::Slice {
12167 expr: Box::new(ir_expr),
12168 lower: ir_lower.map(Box::new),
12169 upper: ir_upper.map(Box::new),
12170 is_array,
12171 })
12172 }
12173
12174 Expr::TypeCast(tc) => {
12175 if matches!(&tc.expr, Expr::Set(elems) if elems.is_empty()) {
12184 return Ok(IrExpr::Null);
12185 }
12186 if let ast::TypeExpr::Tuple { elements } = &tc.ty
12187 && let Some(ir) = self.try_compile_tuple_literal_cast_ctx(elements, &tc.expr, ctx)?
12188 {
12189 let pg_type = self.resolve_cast_pg_type(&tc.ty)?;
12190 let tuple_shape = self.resolve_tuple_cast_shape(&tc.ty);
12191 return Ok(IrExpr::TypeCast(Box::new(IrTypeCast {
12192 expr: ir,
12193 pg_type,
12194 tuple_shape,
12195 })));
12196 }
12197 if let ast::TypeExpr::Array { element } = &tc.ty
12198 && let Some(ir) = self.try_compile_array_literal_cast_ctx(element, &tc.expr, ctx)?
12199 {
12200 let pg_type = self.resolve_cast_pg_type(&tc.ty)?;
12201 return Ok(IrExpr::TypeCast(Box::new(IrTypeCast {
12202 expr: ir,
12203 pg_type,
12204 tuple_shape: None,
12205 })));
12206 }
12207 const STDLIB_MODULES: &[&str] = &["std", "cal", "math", "sys", "pgvector", "crypto", "postgis"];
12214 if let Some((module, name)) = tc.ty.as_named()
12215 && module.map(|m| !STDLIB_MODULES.contains(&m)).unwrap_or(false)
12216 {
12217 let qname = match module {
12218 Some(m) => format!("{m}::{name}"),
12219 None => name.to_string(),
12220 };
12221 if self.resolve_enum(&qname).is_none()
12222 && self.resolve_scalar(&qname).is_none()
12223 && self.resolve_named_tuple(&qname).is_none()
12224 && self.resolve_type(&qname).is_ok()
12225 {
12226 return self.compile_expr_ctx(&tc.expr, ctx);
12227 }
12228 }
12229 let inner = if casts_to_json(&tc.ty) {
12233 self.without_implicit_id(|this| this.compile_expr_ctx(&tc.expr, ctx))?
12234 } else {
12235 self.compile_expr_ctx(&tc.expr, ctx)?
12236 };
12237 let pg_type = self.resolve_cast_pg_type(&tc.ty)?;
12238
12239 if infer_ir_type(&inner) == Some("jsonb") {
12249 if let ast::TypeExpr::Array { element } = &tc.ty {
12250 let elem_pg = self.resolve_cast_pg_type(element)?;
12251 let sql_template =
12252 format!("ARRAY(SELECT (elem #>> '{{}}')::{elem_pg} FROM jsonb_array_elements($1) AS elem)");
12253 return Ok(IrExpr::FunctionCall(super::IrFunctionCall {
12254 return_pg_type: None,
12255 schema: None,
12256 name: "jsonb_array_cast".to_string(),
12257 args: vec![inner],
12258 sql_template: Some(sql_template),
12259 }));
12260 }
12261 if matches!(
12262 pg_type.as_str(),
12263 "uuid" | "timestamptz" | "timestamp" | "date" | "time" | "interval"
12264 ) {
12265 let sql_template = format!("(($1 #>> '{{}}'))::{pg_type}");
12266 return Ok(IrExpr::FunctionCall(super::IrFunctionCall {
12267 return_pg_type: None,
12268 schema: None,
12269 name: "jsonb_scalar_cast".to_string(),
12270 args: vec![inner],
12271 sql_template: Some(sql_template),
12272 }));
12273 }
12274 }
12275
12276 if infer_ir_type(&inner) == Some("text")
12285 && let Some((module, name)) = tc.ty.as_named()
12286 {
12287 let parser = match (module, name) {
12288 (None | Some("std"), "datetime") => Some(("std", "to_datetime")),
12289 (Some("cal"), "local_datetime") => Some(("cal", "to_local_datetime")),
12290 (Some("cal"), "local_date") => Some(("cal", "to_local_date")),
12291 (Some("cal"), "local_time") => Some(("cal", "to_local_time")),
12292 _ => None,
12293 };
12294 if let Some((ns, fn_name)) = parser {
12295 return self.resolve_fn_call(Some(ns), fn_name, vec![inner]);
12296 }
12297 let helper = match (module, name) {
12302 (None | Some("std"), "duration") => Some("duration_in"),
12303 (Some("cal"), "date_duration") => Some("date_duration_in"),
12304 _ => None,
12305 };
12306 if let Some(helper) = helper {
12307 return Ok(IrExpr::FunctionCall(super::IrFunctionCall {
12308 return_pg_type: Some("interval".to_string()),
12309 schema: Some("_pylon".to_string()),
12310 name: helper.to_string(),
12311 args: vec![inner],
12312 sql_template: None,
12313 }));
12314 }
12315 }
12316
12317 if pg_type == "text"
12326 && matches!(
12327 infer_ir_type(&inner),
12328 Some("timestamptz" | "timestamp" | "date" | "time" | "interval")
12329 )
12330 {
12331 return self.resolve_fn_call(Some("std"), "to_str", vec![inner]);
12332 }
12333
12334 let inner = match (pg_type.as_str(), infer_ir_type(&inner)) {
12337 ("int2" | "int8", Some("boolean")) => IrExpr::TypeCast(Box::new(IrTypeCast {
12338 expr: inner,
12339 pg_type: "int4".to_string(),
12340 tuple_shape: None,
12341 })),
12342 _ => inner,
12343 };
12344
12345 let tuple_shape = self.resolve_tuple_cast_shape(&tc.ty);
12346 Ok(IrExpr::TypeCast(Box::new(IrTypeCast {
12347 expr: inner,
12348 pg_type,
12349 tuple_shape,
12350 })))
12351 }
12352
12353 Expr::BinOp(b)
12358 if b.op == ast::BinOpKind::Coalesce
12359 && ctx.is_some()
12360 && Self::coalesce_of_relative_paths(expr).is_some_and(|operands| {
12361 let td = ctx.expect("checked").0;
12362 self.relative_paths_reach_many(td, &operands)
12363 && operands.iter().all(|path| self.path_lands_on_objects(td, path))
12364 }) =>
12365 {
12366 let (td, alias) = ctx.expect("checked by the guard");
12367 let operands = Self::coalesce_of_relative_paths(expr).expect("checked by the guard");
12368 let branches = self.coalesce_path_branches(td, alias, &operands, &[])?;
12369 Ok(IrExpr::ObjectPathUnion {
12370 branches,
12371 limit: None,
12372 multi: true,
12373 })
12374 }
12375
12376 Expr::BinOp(b) => {
12377 let yields_values = matches!(
12380 b.op,
12381 ast::BinOpKind::Add
12382 | ast::BinOpKind::Sub
12383 | ast::BinOpKind::Mul
12384 | ast::BinOpKind::Div
12385 | ast::BinOpKind::FloorDiv
12386 | ast::BinOpKind::Mod
12387 | ast::BinOpKind::Pow
12388 | ast::BinOpKind::Coalesce
12389 | ast::BinOpKind::Concat
12390 );
12391 if let Some((td, alias)) = ctx
12392 && !yields_values
12393 {
12394 if let Some(exists) = self.try_backlink_exists(b, td, alias)? {
12395 return Ok(exists);
12396 }
12397 if let Some(exists) = self.try_multilink_exists(b, td, alias)? {
12398 return Ok(exists);
12399 }
12400 }
12401 if matches!(b.op, ast::BinOpKind::In | ast::BinOpKind::NotIn)
12411 && let Expr::Set(elems) = &b.right
12412 {
12413 let left = self.compile_expr_ctx(&b.left, ctx)?;
12414 let items = elems
12415 .iter()
12416 .map(|e| self.compile_expr_ctx(e, ctx))
12417 .collect::<Result<Vec<_>, _>>()?;
12418 let right = IrExpr::Array(items);
12419 return Ok(IrExpr::BinOp(Box::new(IrBinOp {
12420 left,
12421 op: b.op.clone(),
12422 right,
12423 })));
12424 }
12425 if matches!(b.op, ast::BinOpKind::Eq | ast::BinOpKind::Ne) {
12431 let id = ["id".to_string()];
12432 let left_id = match &b.left {
12433 Expr::FunctionCall(fc) => self.try_compile_fn_scalar_subquery(fc, &id, None, ctx)?,
12434 _ => None,
12435 };
12436 let right_id = match &b.right {
12437 Expr::FunctionCall(fc) => self.try_compile_fn_scalar_subquery(fc, &id, None, ctx)?,
12438 _ => None,
12439 };
12440 if left_id.is_some() || right_id.is_some() {
12441 let left = match left_id {
12442 Some(e) => e,
12443 None => self.compile_expr_ctx(&b.left, ctx)?,
12444 };
12445 let right = match right_id {
12446 Some(e) => e,
12447 None => self.compile_expr_ctx(&b.right, ctx)?,
12448 };
12449 return Ok(IrExpr::BinOp(Box::new(IrBinOp {
12450 left,
12451 op: b.op.clone(),
12452 right,
12453 })));
12454 }
12455 }
12456 let mut left = self.compile_expr_ctx(&b.left, ctx)?;
12457 let mut right = self.compile_expr_ctx(&b.right, ctx)?;
12458 if matches!(
12462 b.op,
12463 ast::BinOpKind::Lt | ast::BinOpKind::Le | ast::BinOpKind::Gt | ast::BinOpKind::Ge
12464 ) && yields_array(&left) != yields_array(&right)
12465 {
12466 left = set_walk_as_scalar(left);
12467 right = set_walk_as_scalar(right);
12468 }
12469 if b.op == ast::BinOpKind::Coalesce && (yields_array(&left) || yields_array(&right)) {
12473 let as_set = |value: IrExpr| {
12474 if yields_array(&value) {
12475 value
12476 } else {
12477 IrExpr::FunctionCall(IrFunctionCall {
12478 return_pg_type: None,
12479 schema: None,
12480 name: "array_remove".to_string(),
12481 args: vec![value],
12482 sql_template: Some("array_remove(ARRAY[$1], NULL)".to_string()),
12483 })
12484 }
12485 };
12486 let left = as_set(left);
12487 let condition = IrExpr::FunctionCall(IrFunctionCall {
12488 return_pg_type: None,
12489 schema: None,
12490 name: "cardinality".to_string(),
12491 args: vec![left.clone()],
12492 sql_template: Some("(cardinality($1) > 0)".to_string()),
12493 });
12494 return Ok(IrExpr::IfElse(Box::new(IrIfElse {
12495 condition,
12496 if_: left,
12497 else_: as_set(right),
12498 })));
12499 }
12500 if matches!(b.op, ast::BinOpKind::Eq | ast::BinOpKind::Ne) {
12505 let is_set = |e: &IrExpr| match e {
12508 IrExpr::ArrayFromSelect(_) => true,
12509 IrExpr::CteRef { name, .. } => self.multi_row_ctes.contains(name),
12510 _ => false,
12511 };
12512 let flipped = is_set(&left) && !is_set(&right) && !is_array_expr(&right);
12513 let straight = is_set(&right) && !is_set(&left) && !is_array_expr(&left);
12514 if flipped || straight {
12515 let (value, set) = if straight { (left, right) } else { (right, left) };
12516 let membership = IrExpr::BinOp(Box::new(IrBinOp {
12517 left: value,
12518 op: ast::BinOpKind::In,
12519 right: set,
12520 }));
12521 return Ok(if matches!(b.op, ast::BinOpKind::Ne) {
12522 IrExpr::UnaryOp(Box::new(IrUnaryOp {
12523 op: ast::UnaryOpKind::Not,
12524 operand: membership,
12525 }))
12526 } else {
12527 membership
12528 });
12529 }
12530 }
12531 if let (Some(lt), Some(rt)) = (infer_ir_type(&left), infer_ir_type(&right))
12532 && !types_compatible(lt, rt)
12533 && !datetime_arithmetic_compatible(&b.op, lt, rt)
12534 {
12535 return Err(PyQLError::Type(PyQLTypeError {
12536 message: format!(
12537 "operator '{op}' cannot be applied to operands of type \
12538 '{lq}' and '{rq}'",
12539 op = b.op,
12540 lq = pg_type_to_pyql(lt),
12541 rq = pg_type_to_pyql(rt),
12542 ),
12543 position: Position { line: 0, col: 0 },
12544 }));
12545 }
12546 Ok(IrExpr::BinOp(Box::new(IrBinOp {
12547 left: true_division_operand(&b.op, left, &right),
12548 op: b.op.clone(),
12549 right,
12550 })))
12551 }
12552
12553 Expr::FunctionCall(f) => {
12554 if (f.module.is_none() || f.module.as_deref() == Some("std")) && f.name == "notify" {
12559 return self.compile_notify(f, ctx);
12560 }
12561 if (f.module.is_none() || f.module.as_deref() == Some("std")) && f.name == "notify_raw" {
12562 return self.compile_notify_raw(f, ctx);
12563 }
12564
12565 if ctx.is_none()
12568 && (f.module.is_none() || f.module.as_deref() == Some("std"))
12569 && (f.name == "sequence_next" || f.name == "sequence_reset")
12570 {
12571 return self.compile_sequence_fn(f);
12572 }
12573
12574 let assert_names: &[&str] = if ctx.is_some() {
12579 &["assert_single", "assert_exists", "assert_distinct"]
12580 } else {
12581 &["assert_single"]
12582 };
12583 if (f.module.is_none() || f.module.as_deref() == Some("std"))
12584 && assert_names.contains(&f.name.as_str())
12585 && !f.args.is_empty()
12586 && let Expr::SubQuery(inner_stmt) = &f.args[0]
12587 {
12588 let inner = match self.relative_subselect(inner_stmt, ctx)? {
12589 Some(ps) => IrArraySource::PathSelect(Box::new(ps)),
12590 None => self.compile_subquery_to_array_source(inner_stmt)?,
12591 };
12592 let fn_pg = match f.name.as_str() {
12593 "assert_single" => "assert_single",
12594 "assert_exists" => "assert_exists",
12595 _ => "assert_distinct",
12596 };
12597 let mut args = vec![IrExpr::ArrayFromSelect(Box::new(inner))];
12598 args.extend(self.assert_message(f, ctx)?);
12599 return Ok(IrExpr::FunctionCall(IrFunctionCall {
12600 return_pg_type: None,
12601 schema: Some("_pylon".to_string()),
12602 name: fn_pg.to_string(),
12603 args,
12604 sql_template: None,
12605 }));
12606 }
12607
12608 if let Some((td, alias)) = ctx
12612 && (f.module.is_none() || f.module.as_deref() == Some("std"))
12613 && f.name == "contains"
12614 && f.args.len() == 2
12615 && let Expr::Path(p) = &f.args[0]
12616 && p.partial
12617 && p.steps.len() >= 2
12618 && let ast::PathStep::Name(ln) = &p.steps[0]
12619 && Self::resolve_multilink(td, ln).is_some()
12620 {
12621 let synthetic = ast::BinOp {
12622 left: f.args[0].clone(),
12623 op: ast::BinOpKind::Eq,
12624 right: f.args[1].clone(),
12625 };
12626 if let Some(exists) = self.try_multilink_exists(&synthetic, td, alias)? {
12627 return Ok(exists);
12628 }
12629 }
12630
12631 if f.args.len() == 1
12637 && f.kwargs.is_empty()
12638 && let Some(mut ps) = self.compile_shape_field_select(&f.args[0], ctx)?
12639 {
12640 let IrPathResult::Scalar(column, _) = ps.result else {
12641 return Err(self.type_err("a shape's pointer read off a walk is a value"));
12642 };
12643 let ns = f.module.as_deref().unwrap_or("std");
12644 let aggregate =
12645 crate::stdlib::lookup(ns, &f.name)
12646 .into_iter()
12647 .find_map(|d| match &d.impl_strategy {
12648 crate::stdlib::ImplStrategy::SqlBuiltin(sql_name) if d.is_aggregate() => {
12649 Some(sql_name.to_string())
12650 }
12651 _ => None,
12652 });
12653 let Some(sql_name) = aggregate else {
12654 return Err(self.type_err(&format!(
12655 "'{}' over a shape's pointer read off a walk needs an aggregate",
12656 f.name
12657 )));
12658 };
12659 let over_nothing = aggregate_over_nothing_sql(&f.name);
12662 let subquery = if matches!(&column, IrExpr::FunctionCall(f) if f.name == "unnest" && f.sql_template.is_none())
12665 {
12666 ps.result = IrPathResult::Scalar(column, None);
12667 IrExpr::FunctionCall(IrFunctionCall {
12668 return_pg_type: None,
12669 schema: None,
12670 name: sql_name.clone(),
12671 args: vec![IrExpr::ArrayFromSelect(Box::new(IrArraySource::PathSelect(Box::new(
12672 ps,
12673 ))))],
12674 sql_template: Some(format!(
12675 "(SELECT {sql_name}(\"_s\".\"v\") FROM unnest($1) AS \"_s\"(\"v\"))"
12676 )),
12677 })
12678 } else {
12679 let aggregate = IrExpr::FunctionCall(IrFunctionCall {
12680 return_pg_type: None,
12681 schema: None,
12682 name: sql_name,
12683 args: vec![column],
12684 sql_template: None,
12685 });
12686 ps.result = IrPathResult::Scalar(aggregate, None);
12687 IrExpr::PathSubquery(Box::new(ps))
12688 };
12689 return Ok(match over_nothing {
12690 Some(value) => IrExpr::FunctionCall(IrFunctionCall {
12691 return_pg_type: None,
12692 schema: None,
12693 name: "coalesce".to_string(),
12694 args: vec![subquery, IrExpr::RawSql(value.to_string())],
12695 sql_template: None,
12696 }),
12697 None => subquery,
12698 });
12699 }
12700 if f.args.len() == 1 {
12701 let arg = &f.args[0];
12702 if matches!(arg, Expr::Intersect(_, _) | Expr::Except(_, _)) {
12705 use crate::stdlib::{ImplStrategy, lookup};
12706 let ns = f.module.as_deref().unwrap_or("std");
12707 let best = lookup(ns, &f.name)
12708 .iter()
12709 .find(|d| d.params.len() == 1)
12710 .map(|d| d.impl_strategy.clone());
12711 if let Some(ImplStrategy::SqlBuiltin(sql_name)) = best
12712 && let IrExpr::SetOp { op, left, right, .. } = self.compile_expr_ctx(arg, ctx)?
12713 {
12714 return Ok(IrExpr::SetOp {
12715 op,
12716 left,
12717 right,
12718 mode: super::SetOpMode::Aggregate(sql_name.to_string()),
12719 });
12720 }
12721 }
12722 if let Expr::FunctionCall(unpack) = arg
12727 && unpack.module.as_deref().unwrap_or("std") == "std"
12728 && unpack.name == "array_unpack"
12729 && unpack.kwargs.is_empty()
12730 && let [array] = unpack.args.as_slice()
12731 && let Some(sql_name) = crate::stdlib::lookup(f.module.as_deref().unwrap_or("std"), &f.name)
12732 .into_iter()
12733 .find_map(|d| match &d.impl_strategy {
12734 crate::stdlib::ImplStrategy::SqlBuiltin(sql_name) if d.is_aggregate() => {
12735 Some(sql_name.to_string())
12736 }
12737 _ => None,
12738 })
12739 {
12740 let values = self.compile_expr_ctx(array, ctx)?;
12741 if !yields_array(&values) {
12745 let over_nothing = aggregate_over_nothing_sql(&f.name).unwrap_or("NULL");
12746 return Ok(IrExpr::FunctionCall(IrFunctionCall {
12747 return_pg_type: None,
12748 schema: None,
12749 name: sql_name.clone(),
12750 args: vec![values],
12751 sql_template: Some(format!(
12752 "(SELECT coalesce({sql_name}(\"_s\".\"v\"), {over_nothing}) FROM unnest($1) AS \"_s\"(\"v\"))"
12753 )),
12754 }));
12755 }
12756 }
12757 if matches!(arg, Expr::FieldAccess { .. })
12761 && matches!(Self::peel_field_access_chain(arg).0, Expr::SubQuery(_))
12762 {
12763 let ns = f.module.as_deref().unwrap_or("std");
12764 let aggregate =
12765 crate::stdlib::lookup(ns, &f.name)
12766 .into_iter()
12767 .find_map(|d| match &d.impl_strategy {
12768 crate::stdlib::ImplStrategy::SqlBuiltin(sql_name) if d.is_aggregate() => {
12769 Some(sql_name.to_string())
12770 }
12771 _ => None,
12772 });
12773 if let Some(sql_name) = aggregate {
12774 let values = self.compile_expr_ctx(arg, ctx)?;
12775 if matches!(values, IrExpr::ArrayFromSelect(_)) {
12776 let over_nothing = aggregate_over_nothing_sql(&f.name).unwrap_or("NULL");
12777 return Ok(IrExpr::FunctionCall(IrFunctionCall {
12778 return_pg_type: None,
12779 schema: None,
12780 name: sql_name.clone(),
12781 args: vec![values],
12782 sql_template: Some(format!(
12783 "(SELECT coalesce({sql_name}(\"_s\".\"v\"), {over_nothing}) FROM unnest($1) AS \"_s\"(\"v\"))"
12784 )),
12785 }));
12786 }
12787 return Ok(IrExpr::FunctionCall(IrFunctionCall {
12788 return_pg_type: None,
12789 schema: None,
12790 name: sql_name,
12791 args: vec![values],
12792 sql_template: None,
12793 }));
12794 }
12795 }
12796 if let Expr::BinOp(b) = arg
12800 && b.op == ast::BinOpKind::Coalesce
12801 {
12802 let values = self.compile_expr_ctx(arg, ctx)?;
12803 let ns = f.module.as_deref().unwrap_or("std");
12804 let aggregate =
12805 crate::stdlib::lookup(ns, &f.name)
12806 .into_iter()
12807 .find_map(|d| match &d.impl_strategy {
12808 crate::stdlib::ImplStrategy::SqlBuiltin(sql_name) if d.is_aggregate() => {
12809 Some(sql_name.to_string())
12810 }
12811 _ => None,
12812 });
12813 if let Some(sql_name) = aggregate
12814 && yields_array(&values)
12815 {
12816 let over_nothing = aggregate_over_nothing_sql(&f.name).unwrap_or("NULL");
12817 return Ok(IrExpr::FunctionCall(IrFunctionCall {
12818 return_pg_type: None,
12819 schema: None,
12820 name: sql_name.clone(),
12821 args: vec![values],
12822 sql_template: Some(format!(
12823 "(SELECT coalesce({sql_name}(\"_s\".\"v\"), {over_nothing}) FROM unnest($1) AS \"_s\"(\"v\"))"
12824 )),
12825 }));
12826 }
12827 return self.resolve_fn_call(f.module.as_deref(), &f.name, vec![values]);
12828 }
12829 if let Some(name) = self.resolve_cte_name(arg) {
12834 use crate::stdlib::{ImplStrategy, lookup};
12835 let ns = f.module.as_deref().unwrap_or("std");
12836 let overloads = lookup(ns, &f.name);
12837 let best = overloads
12838 .iter()
12839 .find(|d| d.params.len() == 1)
12840 .or_else(|| overloads.first());
12841 if let Some(ImplStrategy::SqlBuiltin(sql_name)) = best.map(|d| &d.impl_strategy)
12842 && best.is_some_and(|d| d.params.first().is_some_and(|p| p.ty.is_set()))
12843 {
12844 let object = self.cte_types.get(name).is_some_and(|bound| bound.contains("::"));
12845 return Ok(IrExpr::AggOverCte {
12846 fn_name: sql_name.to_string(),
12847 cte: name.to_string(),
12848 column: (!object).then(|| "v".to_string()),
12849 });
12850 }
12851 }
12852 if let Some((td, alias)) = ctx {
12853 if let Expr::SubQuery(stmt) = arg
12857 && f.name != "array_agg"
12858 && let Some(sql_name) = crate::stdlib::lookup(f.module.as_deref().unwrap_or("std"), &f.name)
12859 .into_iter()
12860 .find_map(|d| match &d.impl_strategy {
12861 crate::stdlib::ImplStrategy::SqlBuiltin(sql_name) if d.is_aggregate() => {
12862 Some(sql_name.to_string())
12863 }
12864 _ => None,
12865 })
12866 && let Some(mut ps) = self.relative_subselect(stmt, ctx)?
12867 {
12868 if let IrPathResult::Object { alias, .. } = &ps.result {
12870 let id = IrExpr::ColumnRef {
12871 alias: alias.clone(),
12872 column: "id".to_string(),
12873 pg_type: "uuid".to_string(),
12874 };
12875 ps.result = IrPathResult::Scalar(id, None);
12876 }
12877 let aggregate = IrExpr::FunctionCall(IrFunctionCall {
12878 return_pg_type: None,
12879 schema: None,
12880 name: sql_name.clone(),
12881 args: vec![IrExpr::ArrayFromSelect(Box::new(IrArraySource::PathSelect(Box::new(
12882 ps,
12883 ))))],
12884 sql_template: Some(format!(
12885 "(SELECT {sql_name}(\"_s\".\"v\") FROM unnest($1) AS \"_s\"(\"v\"))"
12886 )),
12887 });
12888 return Ok(aggregate_over_nothing(&f.name, aggregate));
12889 }
12890 if let Expr::Path(p) = arg
12896 && !p.partial
12897 && p.steps.len() > 1
12898 && let Some(root) = self.find_path_root_in_expr(arg)
12899 {
12900 let synthetic = ast::SelectStmt {
12901 result: Expr::FunctionCall(f.clone()),
12902 filter: None,
12903 order_by: vec![],
12904 offset: None,
12905 limit: None,
12906 lock: None,
12907 };
12908 let ps = self.compile_expr_as_path_select(&synthetic, &synthetic.result, &root, false)?;
12909 return Ok(aggregate_over_nothing(&f.name, IrExpr::PathSubquery(Box::new(ps))));
12910 }
12911 if let Expr::Path(p) = arg
12924 && p.partial
12925 && (p.steps.len() > 1 && self.path_crosses_multi(td, &p.steps)
12926 || matches!(p.steps.first(), Some(ast::PathStep::Backlink(_)))
12927 || matches!(p.steps.as_slice(), [ast::PathStep::Name(name)]
12928 if Self::resolve_multilink(td, name).is_none()
12929 && self.path_crosses_multi(td, &p.steps)))
12930 {
12931 let mut steps = vec![ast::PathStep::Name(format!("{}::{}", td.module, td.name))];
12932 steps.extend(p.steps.iter().cloned());
12933 let rooted = ast::Path { steps, partial: false };
12934 let mut call = f.clone();
12935 call.args[0] = Expr::Path(rooted);
12936 let synthetic = ast::SelectStmt {
12937 result: Expr::FunctionCall(call),
12938 filter: None,
12939 order_by: vec![],
12940 offset: None,
12941 limit: None,
12942 lock: None,
12943 };
12944 let root = format!("{}::{}", td.module, td.name);
12945 let mut ps =
12946 self.compile_expr_as_path_select(&synthetic, &synthetic.result, &root, false)?;
12947 Self::correlate_path_select(&mut ps, alias);
12948 return Ok(aggregate_over_nothing(&f.name, IrExpr::PathSubquery(Box::new(ps))));
12949 }
12950 if let Expr::Path(p) = arg
12951 && p.partial
12952 && p.steps.len() == 1
12953 && let ast::PathStep::Name(ml_name) = &p.steps[0]
12954 && Self::resolve_multilink(td, ml_name).is_some()
12955 {
12956 use crate::stdlib::{ImplStrategy, lookup};
12957 let ns = f.module.as_deref().unwrap_or("std");
12958 let overloads = lookup(ns, &f.name);
12959 let best = overloads
12960 .iter()
12961 .find(|d| d.params.len() == 1)
12962 .or_else(|| overloads.first());
12963 if let Some(ImplStrategy::SqlBuiltin(sql_name)) = best.map(|d| &d.impl_strategy) {
12964 let fn_name = sql_name.to_string();
12965 let inner = self.multilink_correlation_select(ml_name, td, alias)?;
12966 return Ok(IrExpr::AggOverQuery {
12967 fn_name,
12968 inner: Box::new(inner),
12969 });
12970 }
12971 }
12972 } else {
12973 if let Expr::Path(p) = arg
12981 && !p.partial
12982 && p.steps.len() > 1
12983 && let Some(root) = self.find_path_root_in_expr(arg)
12984 {
12985 let synthetic = ast::SelectStmt {
12986 result: Expr::FunctionCall(f.clone()),
12987 filter: None,
12988 order_by: vec![],
12989 offset: None,
12990 limit: None,
12991 lock: None,
12992 };
12993 let ps = self.compile_expr_as_path_select(&synthetic, &synthetic.result, &root, false)?;
12994 return Ok(aggregate_over_nothing(&f.name, IrExpr::PathSubquery(Box::new(ps))));
12995 }
12996 if let Expr::SubQuery(stmt) = arg
13001 && matches!(stmt.as_ref(), Stmt::Insert(_) | Stmt::Update(_) | Stmt::Delete(_))
13002 {
13003 use crate::stdlib::{ImplStrategy, lookup};
13004 let ns = f.module.as_deref().unwrap_or("std");
13005 let overloads = lookup(ns, &f.name);
13006 let best = overloads
13007 .iter()
13008 .find(|d| d.params.len() == 1)
13009 .or_else(|| overloads.first())
13010 .cloned();
13011 if let Some(d) = best
13012 && let ImplStrategy::SqlBuiltin(sql_name) = &d.impl_strategy
13013 {
13014 let fn_name = sql_name.to_string();
13015 let (cte_name, type_name) = self.hoist_dml_as_cte(stmt.as_ref())?;
13016 let td = self.resolve_type(&type_name)?;
13017 let source = IrSource {
13018 poly: None,
13019 type_name: format!("{}::{}", td.module, td.name),
13020 table: format!("@cte:{cte_name}"),
13021 alias: self.fresh_alias(),
13022 };
13023 let inner = IrSelect::schema_bound(source, Self::pk_returning(td), None);
13024 return Ok(IrExpr::AggOverQuery {
13025 fn_name,
13026 inner: Box::new(inner),
13027 });
13028 }
13029 }
13030 let inner_sel: Option<ast::SelectStmt> = match arg {
13031 Expr::Path(p) if !p.partial => {
13032 let qname = p
13034 .steps
13035 .iter()
13036 .filter_map(|s| {
13037 if let ast::PathStep::Name(n) = s {
13038 Some(n.as_str())
13039 } else {
13040 None
13041 }
13042 })
13043 .collect::<Vec<_>>()
13044 .join("::");
13045 let is_schema_type = self
13046 .schema
13047 .types
13048 .iter()
13049 .any(|t| format!("{}::{}", t.module, t.name) == qname || t.name == qname);
13050 if is_schema_type {
13051 Some(ast::SelectStmt {
13052 result: arg.clone(),
13053 filter: None,
13054 order_by: vec![],
13055 offset: None,
13056 limit: None,
13057 lock: None,
13058 })
13059 } else {
13060 None
13061 }
13062 }
13063 Expr::SubQuery(stmt) => {
13064 if let ast::Stmt::Select(inner) = stmt.as_ref() {
13065 Some(inner.clone())
13066 } else {
13067 None
13068 }
13069 }
13070 _ => None,
13071 };
13072 if let Some(sel) = inner_sel {
13073 use crate::stdlib::{ImplStrategy, lookup};
13074 let ns = f.module.as_deref().unwrap_or("std");
13075 let overloads = lookup(ns, &f.name);
13076 let best = overloads
13077 .iter()
13078 .find(|d| d.params.len() == 1)
13079 .or_else(|| overloads.first());
13080 let (inner_result, inner_distinct) = match &sel.result {
13090 Expr::UnaryOp(u) if matches!(u.op, ast::UnaryOpKind::Distinct) => (&u.operand, true),
13091 other => (other, false),
13092 };
13093 if let Expr::Path(inner_path) = inner_result
13094 && !inner_path.partial
13095 && inner_path.steps.len() > 1
13096 && let Some(ast::PathStep::Name(root)) = inner_path.steps.first()
13097 && let Ok(root_td) = self.resolve_path_root(root)
13098 && self
13099 .walk_path_types(root_td, &inner_path.steps[1..], MAX_COMPUTED_SPLICES)
13100 .1
13101 .is_none()
13102 {
13103 let root = root.clone();
13104 let mut inner_call = f.clone();
13105 inner_call.args = vec![if inner_distinct {
13106 Expr::UnaryOp(Box::new(ast::UnaryOp {
13107 op: ast::UnaryOpKind::Distinct,
13108 operand: inner_result.clone(),
13109 }))
13110 } else {
13111 inner_result.clone()
13112 }];
13113 let call = ast::SelectStmt {
13114 result: Expr::FunctionCall(inner_call),
13115 ..sel.clone()
13116 };
13117 let ps = self.compile_expr_as_path_select(&call, &call.result, &root, false)?;
13118 return Ok(IrExpr::PathSubquery(Box::new(ps)));
13119 }
13120 if let Some(d) = best
13121 && let ImplStrategy::SqlBuiltin(sql_name) = &d.impl_strategy
13122 {
13123 let fn_name = sql_name.to_string();
13124 let inner_ir = self.compile_select(&sel, &sel.result, false)?;
13125 if fn_name == "array_agg"
13130 && matches!(inner_ir.rows.as_slice(), [IrRowSource::Bound { .. }])
13131 {
13132 return Ok(IrExpr::ArrayFromSelect(Box::new(IrArraySource::ObjectSelect(
13133 Box::new(inner_ir),
13134 ))));
13135 }
13136 return Ok(IrExpr::AggOverQuery {
13137 fn_name,
13138 inner: Box::new(inner_ir),
13139 });
13140 }
13141 }
13142 }
13143 }
13144
13145 if let Some(operands) = f.args.iter().find_map(Self::set_operands) {
13153 use crate::stdlib::{ImplStrategy, lookup};
13154 let ns = f.module.as_deref().unwrap_or("std");
13155 let overloads = lookup(ns, &f.name);
13156 let best = overloads
13157 .iter()
13158 .find(|d| d.params.len() == f.args.len())
13159 .or_else(|| overloads.first());
13160 let (schema, fn_name) = match best.map(|d| &d.impl_strategy) {
13161 Some(ImplStrategy::SqlBuiltin(sql_name)) => (None, sql_name.to_string()),
13162 Some(_) => {
13163 return Err(self.type_err(&format!(
13164 "function '{}::{}' cannot be called with a set literal in this context",
13165 ns, f.name
13166 )));
13167 }
13168 None => return Err(self.type_err(&format!("function '{}::{}' does not exist", ns, f.name))),
13169 };
13170 let elems = operands
13171 .iter()
13172 .map(|e| self.compile_expr_ctx(e, ctx))
13173 .collect::<Result<Vec<_>, _>>()?;
13174 return Ok(IrExpr::AggOverSet { fn_name, schema, elems });
13175 }
13176
13177 if let Some((td, _)) = ctx
13186 && f.module.as_deref().unwrap_or("std") == "std"
13187 && matches!(f.name.as_str(), "any" | "all")
13188 && f.kwargs.is_empty()
13189 && let [condition] = f.args.as_slice()
13190 && let Some(per_element) = per_element_of_one_multilink(condition, td)
13191 {
13192 let (link, element_condition) = per_element;
13193 let condition = if f.name == "all" {
13194 Expr::UnaryOp(Box::new(ast::UnaryOp {
13195 op: ast::UnaryOpKind::Not,
13196 operand: element_condition,
13197 }))
13198 } else {
13199 element_condition
13200 };
13201 let exists = Expr::UnaryOp(Box::new(ast::UnaryOp {
13202 op: ast::UnaryOpKind::Exists,
13203 operand: Expr::SubQuery(Box::new(Stmt::Select(ast::SelectStmt {
13204 result: Expr::Path(ast::Path {
13205 steps: link,
13206 partial: true,
13207 }),
13208 filter: Some(condition),
13209 order_by: vec![],
13210 offset: None,
13211 limit: None,
13212 lock: None,
13213 }))),
13214 }));
13215 let exists = if f.name == "all" {
13216 Expr::UnaryOp(Box::new(ast::UnaryOp {
13217 op: ast::UnaryOpKind::Not,
13218 operand: exists,
13219 }))
13220 } else {
13221 exists
13222 };
13223 return self.compile_expr_ctx(&exists, ctx);
13224 }
13225 if f.module.as_deref().unwrap_or("std") == "std"
13228 && matches!(f.name.as_str(), "any" | "all")
13229 && f.kwargs.is_empty()
13230 && let [arg @ Expr::TypeIs { .. }] = f.args.as_slice()
13231 && let answers @ IrExpr::ArrayFromSelect(_) = self.compile_expr_ctx(arg, ctx)?
13232 {
13233 let (aggregate, over_nothing) = if f.name == "all" {
13234 ("bool_and", "true")
13235 } else {
13236 ("bool_or", "false")
13237 };
13238 return Ok(IrExpr::FunctionCall(IrFunctionCall {
13239 return_pg_type: None,
13240 schema: None,
13241 name: aggregate.to_string(),
13242 args: vec![answers],
13243 sql_template: Some(format!(
13244 "(SELECT coalesce({aggregate}(\"_unnested\".\"v\"), {over_nothing}) FROM unnest($1) AS \"_unnested\"(\"v\"))"
13245 )),
13246 }));
13247 }
13248 if f.module.as_deref().unwrap_or("std") == "std"
13253 && matches!(f.name.as_str(), "any" | "all")
13254 && f.kwargs.is_empty()
13255 && let [Expr::BinOp(comparison)] = f.args.as_slice()
13256 && let Expr::FunctionCall(unpack) = &comparison.left
13257 && unpack.module.as_deref().unwrap_or("std") == "std"
13258 && unpack.name == "array_unpack"
13259 && let [array] = unpack.args.as_slice()
13260 {
13261 let array = self.compile_expr_ctx(array, ctx)?;
13262 let element = "<unnested element>".to_string();
13263 self.inline_bindings
13264 .insert(element.clone(), IrExpr::RawSql("\"_unnested\".\"v\"".to_string()));
13265 let per_element = self.compile_expr_ctx(
13266 &Expr::BinOp(Box::new(ast::BinOp {
13267 left: Expr::Path(ast::Path::absolute(element.clone())),
13268 op: comparison.op.clone(),
13269 right: comparison.right.clone(),
13270 })),
13271 ctx,
13272 );
13273 self.inline_bindings.remove(&element);
13274 let (aggregate, over_nothing) = if f.name == "all" {
13275 ("bool_and", "true")
13276 } else {
13277 ("bool_or", "false")
13278 };
13279 return Ok(IrExpr::FunctionCall(IrFunctionCall {
13280 return_pg_type: None,
13281 schema: None,
13282 name: aggregate.to_string(),
13283 args: vec![per_element?, array],
13284 sql_template: Some(format!(
13285 "(SELECT coalesce({aggregate}($1), {over_nothing}) FROM unnest($2) AS \"_unnested\"(\"v\"))"
13286 )),
13287 }));
13288 }
13289 if let Some(args) = self.compile_named_call_args(f, ctx)? {
13290 return self.resolve_fn_call(f.module.as_deref(), &f.name, args);
13291 }
13292 let is_explicit_set = f.module.as_deref().unwrap_or("std") == "std"
13293 && matches!(f.name.as_str(), "any" | "all")
13294 && f.args.len() == 1;
13295 if is_explicit_set {
13296 self.explicit_set_depth += 1;
13297 }
13298 let args = f
13299 .args
13300 .iter()
13301 .map(|a| self.compile_expr_ctx(a, ctx))
13302 .collect::<Result<Vec<_>, _>>();
13303 if is_explicit_set {
13304 self.explicit_set_depth -= 1;
13305 }
13306 self.resolve_fn_call(f.module.as_deref(), &f.name, args?)
13307 }
13308
13309 Expr::UnaryOp(u) if u.op == ast::UnaryOpKind::Exists => self.compile_exists_ctx(&u.operand, ctx),
13310
13311 Expr::UnaryOp(u) if u.op == ast::UnaryOpKind::Distinct => {
13317 let operand = self.compile_expr_ctx(&u.operand, ctx)?;
13318 Ok(match operand {
13319 IrExpr::PathSubquery(mut ps) => {
13320 ps.distinct = true;
13321 IrExpr::PathSubquery(ps)
13322 }
13323 IrExpr::ArrayFromSelect(src) => IrExpr::ArrayFromSelect(Box::new(match *src {
13324 IrArraySource::Select(mut sel) => {
13325 sel.distinct = true;
13326 IrArraySource::Select(sel)
13327 }
13328 IrArraySource::PathSelect(mut ps) => {
13329 ps.distinct = true;
13330 IrArraySource::PathSelect(ps)
13331 }
13332 other => other,
13333 })),
13334 other => other,
13335 })
13336 }
13337
13338 Expr::UnaryOp(u) => {
13339 let operand = self.compile_expr_ctx(&u.operand, ctx)?;
13340 Ok(IrExpr::UnaryOp(Box::new(IrUnaryOp {
13341 op: u.op.clone(),
13342 operand,
13343 })))
13344 }
13345
13346 Expr::IfElse(ie) => {
13347 let condition = self.compile_expr_ctx(&ie.condition, ctx)?;
13348 let if_ = self.compile_expr_ctx(&ie.if_expr, ctx)?;
13349 let else_ = self.compile_expr_ctx(&ie.else_expr, ctx)?;
13350 Ok(IrExpr::IfElse(Box::new(IrIfElse { condition, if_, else_ })))
13351 }
13352
13353 Expr::Array(elems) => {
13354 let items = elems
13358 .iter()
13359 .map(|e| self.compile_expr_ctx(e, ctx).map(set_walk_as_scalar))
13360 .collect::<Result<Vec<_>, _>>()?;
13361 Ok(IrExpr::Array(items))
13362 }
13363
13364 Expr::NamedTuple(fields) => {
13365 let ir = fields
13366 .iter()
13367 .map(|(name, e)| Ok((name.clone(), self.compile_expr_ctx(e, ctx)?)))
13368 .collect::<Result<Vec<_>, PyQLError>>()?;
13369 Ok(IrExpr::NamedTuple {
13370 fields: ir,
13371 is_free_object: false,
13372 })
13373 }
13374
13375 Expr::Tuple(elems) => {
13376 let ir = elems
13377 .iter()
13378 .map(|e| self.compile_expr_ctx(e, ctx))
13379 .collect::<Result<Vec<_>, PyQLError>>()?;
13380 Ok(IrExpr::Tuple(ir))
13381 }
13382
13383 Expr::FieldAccess { expr: inner, field } => {
13384 if let Some(ir) = self.try_compile_walk_off_subquery(expr, ctx)? {
13387 return Ok(ir);
13388 }
13389 if let Expr::NamedTuple(fields) = inner.as_ref() {
13390 let (_, val) = fields.iter().find(|(k, _)| k == field).ok_or_else(|| {
13391 self.type_err(&format!("{field} is not a member of {}", named_tuple_type_str(fields)))
13392 })?;
13393 return self.compile_expr_ctx(val, ctx);
13394 }
13395 if let Some(ps) = self.compile_shape_field_select(expr, ctx)? {
13396 return Ok(IrExpr::ArrayFromSelect(Box::new(IrArraySource::PathSelect(Box::new(
13397 ps,
13398 )))));
13399 }
13400 let (base, fields) = Self::peel_field_access_chain(expr);
13405 if let Expr::SubQuery(stmt) = base {
13406 let stmt = stmt.as_ref().clone();
13407 return self.compile_subquery_expr(&stmt, &fields, ctx, &[]);
13408 }
13409 if let Expr::FunctionCall(fc) = base {
13412 let fc = fc.clone();
13413 if let Some(ir) = self.try_compile_fn_scalar_subquery(&fc, &fields, None, ctx)? {
13414 return Ok(ir);
13415 }
13416 }
13417 let ir = self.compile_expr_ctx(inner, ctx)?;
13418 Ok(Self::project_free_object_field(ir, field))
13419 }
13420
13421 Expr::TupleIndex { expr: inner, index } => {
13422 match inner.as_ref() {
13423 Expr::Tuple(elems) => {
13424 let elem = elems.get(*index).ok_or_else(|| {
13425 self.type_err(&format!(
13426 "{index} is not a member of {}",
13427 positional_tuple_type_str(elems)
13428 ))
13429 })?;
13430 self.compile_expr_ctx(elem, ctx)
13431 }
13432 Expr::NamedTuple(fields) => {
13433 let (_, val) = fields.get(*index).ok_or_else(|| {
13434 self.type_err(&format!("{index} is not a member of {}", named_tuple_type_str(fields)))
13435 })?;
13436 self.compile_expr_ctx(val, ctx)
13437 }
13438 _ => {
13444 if let Expr::TypeCast(tc) = inner.as_ref()
13445 && let Some(shape) = self.resolve_tuple_cast_shape(&tc.ty)
13446 && *index >= shape.members.len()
13447 {
13448 return Err(self.type_err(&format!(
13449 "{index} is not a member of {}",
13450 self.type_expr_to_display_str(&tc.ty)
13451 )));
13452 }
13453 let ir = self.compile_expr_ctx(inner, ctx)?;
13454 Ok(IrExpr::JsonbIndex {
13455 expr: Box::new(ir),
13456 index: *index,
13457 })
13458 }
13459 }
13460 }
13461
13462 Expr::Detached(inner) => {
13472 if ctx.is_some()
13473 && let Some(root) = self.find_path_root_in_expr(inner)
13474 {
13475 let synthetic = ast::SelectStmt {
13476 result: (**inner).clone(),
13477 filter: None,
13478 order_by: vec![],
13479 offset: None,
13480 limit: None,
13481 lock: None,
13482 };
13483 let ps = self.compile_expr_as_path_select(&synthetic, inner, &root, false)?;
13484 return Ok(IrExpr::PathSubquery(Box::new(ps)));
13485 }
13486 self.compile_expr_ctx(inner, None)
13487 }
13488
13489 Expr::Set(elems) if ctx.is_none() && elems.is_empty() => Ok(IrExpr::Null),
13495
13496 Expr::Set(elems) if ctx.is_none() => {
13497 let compiled: Result<Vec<_>, _> = elems.iter().map(|e| self.compile_expr_ctx(e, ctx)).collect();
13498 let mut compiled = compiled?;
13499 if compiled.len() == 1 {
13500 Ok(compiled.remove(0))
13501 } else {
13502 Err(self.type_err("multi-element set literal is not supported in free SELECT context"))
13503 }
13504 }
13505
13506 Expr::Shape(s) if s.expr.is_none() => {
13520 let fields = s
13521 .elements
13522 .iter()
13523 .map(|el| -> Result<(String, IrExpr), PyQLError> {
13524 let name = path_leaf(&el.path)?.to_string();
13525 let expr = el.compexpr.as_ref().ok_or_else(|| {
13526 self.type_err("free object field must have a value expression (':= expr')")
13527 })?;
13528 let compiled = match expr {
13529 Expr::SubQuery(stmt)
13530 if matches!(stmt.as_ref(), Stmt::Insert(_) | Stmt::Update(_) | Stmt::Delete(_)) =>
13531 {
13532 self.dml_as_value(stmt.as_ref())?
13533 }
13534 other => match self.free_object_link_field(other)? {
13535 Some(object) => object,
13536 None => self.compile_expr_ctx(other, ctx)?,
13537 },
13538 };
13539 Ok((name, compiled))
13540 })
13541 .collect::<Result<Vec<_>, _>>()?;
13542 Ok(IrExpr::NamedTuple {
13543 fields,
13544 is_free_object: true,
13545 })
13546 }
13547
13548 Expr::Shape(s) if matches!(&s.expr, Some(inner) if self.is_free_cte_ref(inner)) => {
13556 let Some(Expr::Path(root_path)) = &s.expr else {
13557 unreachable!()
13558 };
13559 let ast::PathStep::Name(root) = &root_path.steps[0] else {
13560 unreachable!()
13561 };
13562 let fields = s
13563 .elements
13564 .iter()
13565 .map(|el| -> Result<(String, IrExpr), PyQLError> {
13566 let name = path_leaf(&el.path)?.to_string();
13567 let expr = match &el.compexpr {
13568 Some(over) => self.compile_expr_ctx(over, ctx)?,
13569 None => match self.resolve_cte_field_chain(root, &[name.as_str()]) {
13570 Some(result) => result?,
13571 None => {
13572 return Err(self.type_err(&format!("free object '{root}' has no field '{name}'")));
13573 }
13574 },
13575 };
13576 Ok((name, expr))
13577 })
13578 .collect::<Result<Vec<_>, _>>()?;
13579 Ok(IrExpr::NamedTuple {
13580 fields,
13581 is_free_object: true,
13582 })
13583 }
13584
13585 Expr::Shape(sh)
13589 if ctx.is_some()
13590 && matches!(sh.expr.as_ref(), Some(Expr::Union(_, _)))
13591 && Self::union_of_relative_paths(sh.expr.as_ref().expect("checked")).is_some() =>
13592 {
13593 let (td, alias) = ctx.expect("checked by the guard");
13594 let operands =
13595 Self::union_of_relative_paths(sh.expr.as_ref().expect("checked")).expect("checked by the guard");
13596 let elements = sh.elements.clone();
13597 let multi = self.relative_paths_reach_many(td, &operands);
13598 let mut branches = Vec::with_capacity(operands.len());
13599 for path in operands {
13600 let mut steps = vec![ast::PathStep::Name(format!("{}::{}", td.module, td.name))];
13601 steps.extend(path.steps.iter().cloned());
13602 let rooted = ast::Path { steps, partial: false };
13603 let synthetic = ast::SelectStmt {
13604 result: Expr::Path(rooted.clone()),
13605 filter: None,
13606 order_by: vec![],
13607 offset: None,
13608 limit: None,
13609 lock: None,
13610 };
13611 let mut ps = self.compile_path_select(&synthetic, &rooted, &elements, false)?;
13612 Self::correlate_path_select(&mut ps, alias);
13613 branches.push(ps);
13614 }
13615 Ok(IrExpr::ObjectPathUnion {
13616 branches,
13617 limit: None,
13618 multi,
13619 })
13620 }
13621
13622 Expr::Shape(sh)
13626 if ctx.is_some() && sh.expr.as_ref().and_then(Self::coalesce_of_relative_paths).is_some() =>
13627 {
13628 let (td, alias) = ctx.expect("checked by the guard");
13629 let operands =
13630 Self::coalesce_of_relative_paths(sh.expr.as_ref().expect("checked")).expect("checked by the guard");
13631 let multi = self.relative_paths_reach_many(td, &operands);
13632 let branches = self.coalesce_path_branches(td, alias, &operands, &sh.elements)?;
13633 Ok(IrExpr::ObjectPathUnion {
13634 branches,
13635 limit: None,
13636 multi,
13637 })
13638 }
13639
13640 Expr::Shape(sh) if matches!(sh.expr.as_ref(), Some(Expr::SubQuery(_))) => {
13641 let sh = sh.clone();
13642 match self.shape_over_subquery(&sh)? {
13643 Some(ir) => Ok(ir),
13644 None => Err(PyQLError::Type(PyQLTypeError {
13645 message: "shapes and set literals are not valid in expression context".into(),
13646 position: Position { line: 0, col: 0 },
13647 })),
13648 }
13649 }
13650
13651 Expr::Shape(sh) if Self::shape_over_subquery_projection(sh).is_some() => {
13652 let (stmt, fields) = Self::shape_over_subquery_projection(sh).expect("checked by the guard");
13653 let stmt = stmt.clone();
13654 let elements = sh.elements.clone();
13655 self.compile_subquery_expr(&stmt, &fields, ctx, &elements)
13656 }
13657
13658 Expr::Set(items) if items.is_empty() => Ok(IrExpr::Null),
13663
13664 Expr::Shape(sh)
13665 if matches!(sh.expr.as_ref(), Some(Expr::Path(p))
13666 if !p.partial
13667 && matches!(p.steps.first(), Some(ast::PathStep::Name(n))
13668 if self.cte_object_type(n).is_some() || self.for_var_types.contains_key(n))) =>
13669 {
13670 let expr = expr.clone();
13671 match self.shape_over_binding(&expr)? {
13672 Some(ir) => Ok(ir),
13673 None => Err(PyQLError::Type(PyQLTypeError {
13674 message: "shapes and set literals are not valid in expression context".into(),
13675 position: Position { line: 0, col: 0 },
13676 })),
13677 }
13678 }
13679
13680 Expr::Shape(_) | Expr::Set(_) => Err(PyQLError::Type(PyQLTypeError {
13686 message: "shapes and set literals are not valid in expression context".into(),
13687 position: Position { line: 0, col: 0 },
13688 })),
13689
13690 Expr::SubQuery(stmt) => {
13698 let stmt = stmt.as_ref().clone();
13699 self.compile_subquery_expr(&stmt, &[], ctx, &[])
13700 }
13701
13702 Expr::Union(_, _) => Err(PyQLError::Type(PyQLTypeError {
13703 message: "union is not valid in expression context".into(),
13704 position: Position { line: 0, col: 0 },
13705 })),
13706
13707 Expr::Except(left, right) | Expr::Intersect(left, right) => {
13711 let op = if matches!(expr, Expr::Intersect(_, _)) {
13712 super::SetOpKind::Intersect
13713 } else {
13714 super::SetOpKind::Except
13715 };
13716 let left = self.compile_expr_ctx(left, ctx)?;
13717 let right = self.compile_expr_ctx(right, ctx)?;
13718 Ok(IrExpr::SetOp {
13719 op,
13720 left: Box::new(left),
13721 right: Box::new(right),
13722 mode: super::SetOpMode::Array,
13723 })
13724 }
13725
13726 Expr::TypeIs { expr, ty } => match ctx {
13732 Some((td, alias)) => self.compile_type_is(expr, ty, td, alias),
13733 None => match expr.as_ref() {
13734 Expr::Path(p) => self.compile_path_type_is(p, ty, None),
13735 _ => Err(self.type_err("'is' type check is not valid in free SELECT context")),
13736 },
13737 },
13738 }
13739 }
13740
13741 fn compile_expr(&mut self, expr: &Expr, td: &TypeDescriptor, alias: &str) -> Result<IrExpr, PyQLError> {
13742 self.compile_expr_ctx(expr, Some((td, alias)))
13743 }
13744
13745 fn compile_free_expr(&mut self, expr: &Expr) -> Result<IrExpr, PyQLError> {
13746 self.compile_expr_ctx(expr, None)
13747 }
13748
13749 fn compile_path_type_is(
13752 &mut self,
13753 path: &ast::Path,
13754 ty: &ast::TypeExpr,
13755 ctx: Option<(&TypeDescriptor, &str)>,
13756 ) -> Result<IrExpr, PyQLError> {
13757 let (ty_module, ty_name) = ty
13758 .as_named()
13759 .ok_or_else(|| self.type_err("cannot use IS with a tuple or array type"))?;
13760 let check_qname = match (ty_module, ctx) {
13761 (Some(module), _) => format!("{module}::{ty_name}"),
13762 (None, Some((td, _))) => format!("{}::{}", td.module, ty_name),
13763 (None, None) => ty_name.to_string(),
13764 };
13765 let check_td = self.resolve_type(&check_qname)?;
13766 let check_qname = format!("{}::{}", check_td.module, check_td.name);
13767 let mut type_path = path.clone();
13768 type_path.steps.push(ast::PathStep::Name("__type__".to_string()));
13769 let own_type = self.compile_expr_ctx(&Expr::Path(type_path), ctx)?;
13770 let implementors: Vec<String> = self
13771 .find_poly_implementors(&check_qname)
13772 .into_iter()
13773 .map(|implementor| implementor.type_name)
13774 .collect();
13775 let is_one_of = |own_type: IrExpr| {
13776 implementors
13777 .iter()
13778 .map(|implementor| {
13779 IrExpr::BinOp(Box::new(IrBinOp {
13780 left: own_type.clone(),
13781 op: ast::BinOpKind::Eq,
13782 right: IrExpr::Literal(IrLiteral::Str(implementor.clone())),
13783 }))
13784 })
13785 .reduce(|left, right| {
13786 IrExpr::BinOp(Box::new(IrBinOp {
13787 left,
13788 op: ast::BinOpKind::Or,
13789 right,
13790 }))
13791 })
13792 .unwrap_or(IrExpr::Literal(IrLiteral::Bool(false)))
13793 };
13794 Ok(match own_type {
13796 IrExpr::ArrayFromSelect(source) => match *source {
13797 IrArraySource::PathSelect(mut path_select) => {
13798 let IrPathResult::Scalar(element, cast) = path_select.result else {
13799 return Err(self.type_err("'__type__' is read as a value"));
13800 };
13801 path_select.result = IrPathResult::Scalar(is_one_of(element), cast);
13802 IrExpr::ArrayFromSelect(Box::new(IrArraySource::PathSelect(path_select)))
13803 }
13804 other => is_one_of(IrExpr::ArrayFromSelect(Box::new(other))),
13805 },
13806 single => is_one_of(single),
13807 })
13808 }
13809
13810 fn compile_type_is(
13811 &mut self,
13812 expr: &Expr,
13813 ty: &ast::TypeExpr,
13814 td: &TypeDescriptor,
13815 alias: &str,
13816 ) -> Result<IrExpr, PyQLError> {
13817 let self_qname = format!("{}::{}", td.module, td.name);
13818
13819 let reaches_other_objects = match expr {
13822 Expr::Path(p) if p.partial || p.steps.len() > 1 => {
13823 !matches!(p.steps.last(), Some(ast::PathStep::Name(n)) if n == "__type__")
13824 }
13825 Expr::Path(p) => matches!(
13826 p.steps.as_slice(),
13827 [ast::PathStep::Name(n)] if n != &td.name && n != &self_qname && self.resolve_type(n).is_err()
13828 && self.resolve_name_ref(n, true).is_some()
13829 ),
13830 _ => false,
13831 };
13832 if reaches_other_objects && let Expr::Path(p) = expr {
13833 return self.compile_path_type_is(p, ty, Some((td, alias)));
13834 }
13835
13836 let (source_qname, cross_scope) = match expr {
13839 Expr::Path(p) if !p.partial && p.steps.len() == 1 => {
13840 if let ast::PathStep::Name(n) = &p.steps[0] {
13841 if n == &td.name || n == &self_qname || self.scope_answers_to(n, &self_qname) {
13842 (self_qname.clone(), false)
13843 } else {
13844 match self.resolve_type(n) {
13846 Ok(other) => (format!("{}::{}", other.module, other.name), true),
13847 Err(_) => (self_qname.clone(), false),
13848 }
13849 }
13850 } else {
13851 (self_qname.clone(), false)
13852 }
13853 }
13854 _ => (self_qname.clone(), false),
13855 };
13856
13857 let (ty_module, ty_name) = ty
13858 .as_named()
13859 .ok_or_else(|| self.type_err("cannot use IS with a tuple or array type"))?;
13860 let check_module = ty_module.unwrap_or(td.module.as_str());
13861 let check_qname = format!("{}::{}", check_module, ty_name);
13862 self.resolve_type(&check_qname)?;
13863
13864 if !cross_scope {
13865 return Ok(self.type_check_bool_expr(&source_qname, &check_qname, td, alias));
13867 }
13868
13869 let src_alias = self.fresh_alias();
13872 let src_td = self.resolve_type(&source_qname)?;
13873 let source_table = src_td.table.clone();
13874 let (poly_implementors, poly_columns) = if self.is_polymorphic(src_td) {
13875 (
13876 self.find_poly_implementors(&source_qname),
13877 Self::poly_dml_columns(src_td),
13878 )
13879 } else {
13880 (vec![], vec![])
13881 };
13882 let bool_expr = self.type_check_bool_expr(&source_qname, &check_qname, src_td, &src_alias);
13883
13884 let source = IrSource {
13885 poly: None,
13886 type_name: source_qname,
13887 table: source_table,
13888 alias: src_alias,
13889 };
13890
13891 Ok(IrExpr::ArrayFromSelect(Box::new(IrArraySource::RawExpr {
13892 source,
13893 poly_implementors,
13894 poly_columns,
13895 expr: bool_expr,
13896 })))
13897 }
13898
13899 fn type_check_bool_expr(&self, source_qname: &str, check_qname: &str, td: &TypeDescriptor, alias: &str) -> IrExpr {
13901 if check_qname == source_qname || Self::is_or_implements(td, check_qname) {
13902 return IrExpr::Literal(IrLiteral::Bool(true));
13903 }
13904 if !self.is_polymorphic(td) {
13905 return IrExpr::Literal(IrLiteral::Bool(false));
13906 }
13907 self.find_poly_implementors(check_qname)
13910 .into_iter()
13911 .map(|implementor| {
13912 IrExpr::BinOp(Box::new(IrBinOp {
13913 left: IrExpr::ColumnRef {
13914 alias: alias.to_string(),
13915 column: "__type__".into(),
13916 pg_type: "text".into(),
13917 },
13918 op: crate::parse::ast::BinOpKind::Eq,
13919 right: IrExpr::Literal(IrLiteral::Str(implementor.type_name)),
13920 }))
13921 })
13922 .reduce(|left, right| {
13923 IrExpr::BinOp(Box::new(IrBinOp {
13924 left,
13925 op: crate::parse::ast::BinOpKind::Or,
13926 right,
13927 }))
13928 })
13929 .unwrap_or(IrExpr::Literal(IrLiteral::Bool(false)))
13930 }
13931
13932 fn resolve_name_ref(&mut self, name: &str, allow_fn_param: bool) -> Option<IrExpr> {
13946 if self.for_vars.contains_key(name) {
13947 return Some(self.for_var_ref(name));
13948 }
13949 if let Some(IrFreeExpr::FreeObject(_)) = self.cte_free_items.get(name) {
13960 return Some(IrExpr::NamedTuple {
13961 fields: vec![],
13962 is_free_object: true,
13963 });
13964 }
13965 if let Some(ir) = self.inline_bindings.get(name) {
13966 return Some(ir.clone());
13967 }
13968 if let Some(t) = self.cte_types.get(name) {
13969 let scalar = !t.contains("::");
13973 return Some(IrExpr::CteRef {
13974 name: self.cte_sql_name(name),
13975 scalar,
13976 pg_type: (scalar && !t.is_empty()).then(|| literal_sentinel_to_pg(t).to_string()),
13977 });
13978 }
13979 if allow_fn_param && let Some(pg_type) = self.fn_params.get(name) {
13980 return Some(IrExpr::FnParam {
13981 name: name.to_string(),
13982 pg_type: pg_type.clone(),
13983 });
13984 }
13985 None
13986 }
13987
13988 fn enclosing_anchor(&self, root: &str) -> Option<(String, String)> {
13995 let innermost = self.anchors.last()?;
13996 if !innermost.detached || !innermost.answers_to(root) {
13997 return None;
13998 }
13999 self.anchors
14000 .iter()
14001 .rev()
14002 .skip(1)
14003 .find(|a| a.answers_to(root))
14004 .map(|a| (a.qualified.clone(), a.alias.clone()))
14005 }
14006
14007 fn scope_answers_to(&self, root: &str, self_qname: &str) -> bool {
14011 self.anchors
14012 .last()
14013 .is_some_and(|anchor| anchor.qualified == self_qname && anchor.answers_to(root))
14014 }
14015
14016 fn outer_anchor(&self, root: &str) -> Option<(String, String)> {
14018 self.anchors
14019 .iter()
14020 .rev()
14021 .find(|a| a.answers_to(root))
14022 .map(|a| (a.qualified.clone(), a.alias.clone()))
14023 }
14024
14025 fn compile_path(&mut self, p: &ast::Path, td: &TypeDescriptor, alias: &str) -> Result<IrExpr, PyQLError> {
14026 if !p.partial {
14027 if p.steps.len() == 1
14028 && let ast::PathStep::Name(n) = &p.steps[0]
14029 {
14030 if let Some(ir) = self.resolve_name_ref(n, true) {
14031 return Ok(ir);
14032 }
14033 if n == "__type__" {
14035 return Ok(if self.is_polymorphic(td) {
14036 IrExpr::ColumnRef {
14037 alias: alias.to_string(),
14038 column: "__type__".to_string(),
14039 pg_type: "text".to_string(),
14040 }
14041 } else {
14042 IrExpr::Literal(IrLiteral::Str(format!("{}::{}", td.module, td.name)))
14043 });
14044 }
14045 }
14046 if p.steps.len() == 2
14048 && let [ast::PathStep::Name(type_ref), ast::PathStep::Name(variant)] = p.steps.as_slice()
14049 && self.resolve_enum(type_ref).is_some()
14050 {
14051 return self.compile_enum_access(type_ref, variant);
14052 }
14053 if let Some(resolved) = self.resolve_cte_path(p) {
14055 return resolved;
14056 }
14057 if p.steps.len() > 1
14073 && let ast::PathStep::Name(root) = &p.steps[0]
14074 && self
14075 .cte_types
14076 .get(root.as_str())
14077 .map(|t| t.contains("::"))
14078 .unwrap_or(false)
14079 {
14080 let full_path = ast::Path {
14081 steps: p.steps.clone(),
14082 partial: false,
14083 };
14084 let synthetic = ast::SelectStmt {
14085 result: Expr::Path(full_path.clone()),
14086 filter: None,
14087 order_by: vec![],
14088 offset: None,
14089 limit: None,
14090 lock: None,
14091 };
14092 let ps = self.compile_path_select(&synthetic, &full_path, &[], false)?;
14093 return Ok(IrExpr::PathSubquery(Box::new(ps)));
14094 }
14095 if p.steps.len() > 1
14109 && let ast::PathStep::Name(root) = &p.steps[0]
14110 && (root == "__new__" || root == "__old__")
14111 {
14112 if let Some((anchor_td, anchor_alias)) = self.special_anchors.get(root).cloned() {
14113 let relative = ast::Path {
14114 steps: p.steps[1..].to_vec(),
14115 partial: true,
14116 };
14117 return self.compile_path(&relative, anchor_td, &anchor_alias);
14118 }
14119 return Err(PyQLError::Resolution(PyQLResolutionError::UnknownField(
14120 PyQLUnknownFieldError {
14121 message: format!("{root} cannot be used in this expression"),
14122 position: Position { line: 0, col: 0 },
14123 },
14124 )));
14125 }
14126 if p.steps.len() > 1 && matches!(&p.steps[0], ast::PathStep::Name(root) if root == "__subject__") {
14129 let relative = ast::Path {
14130 steps: p.steps[1..].to_vec(),
14131 partial: true,
14132 };
14133 return self.compile_path(&relative, td, alias);
14134 }
14135 if p.steps.len() > 1
14139 && let ast::PathStep::Name(root) = &p.steps[0]
14140 {
14141 let qualified = format!("{}::{}", td.module, td.name);
14142 if *root == td.name || *root == qualified {
14143 let relative = ast::Path {
14144 steps: p.steps[1..].to_vec(),
14145 partial: true,
14146 };
14147 if let Some((outer_qualified, outer_alias)) = self.enclosing_anchor(root) {
14151 let outer_td = self.resolve_type(&outer_qualified)?.clone();
14152 return self.compile_path(&relative, &outer_td, &outer_alias);
14153 }
14154 return self.compile_path(&relative, td, alias);
14155 }
14156 if let Some((outer_qualified, outer_alias)) = self.outer_anchor(root) {
14161 let relative = ast::Path {
14162 steps: p.steps[1..].to_vec(),
14163 partial: true,
14164 };
14165 let outer_td = self.resolve_type(&outer_qualified)?.clone();
14166 return self.compile_path(&relative, &outer_td, &outer_alias);
14167 }
14168 }
14169 if let Some(ast::PathStep::Name(var)) = p.steps.first()
14174 && p.steps.len() > 1
14175 && self.for_var_types.contains_key(var)
14176 && !matches!(p.steps[1], ast::PathStep::TypeIntersection(_))
14177 {
14178 let synthetic = ast::SelectStmt {
14179 result: Expr::Path(p.clone()),
14180 filter: None,
14181 order_by: vec![],
14182 offset: None,
14183 limit: None,
14184 lock: None,
14185 };
14186 let ps = self.compile_path_select(&synthetic, p, &[], false)?;
14187 return Ok(IrExpr::PathSubquery(Box::new(ps)));
14188 }
14189 if let [
14197 ast::PathStep::Name(var),
14198 ast::PathStep::TypeIntersection(type_ref),
14199 rest @ ..,
14200 ] = p.steps.as_slice()
14201 && !rest.is_empty()
14202 && self.for_var_types.contains_key(var)
14203 {
14204 let type_name = match &type_ref.module {
14205 Some(m) => format!("{}::{}", m, type_ref.name),
14206 None => type_ref.name.clone(),
14207 };
14208 let narrowed = self.resolve_type(&type_name)?;
14209 let mut steps = vec![ast::PathStep::Name(format!("{}::{}", narrowed.module, narrowed.name))];
14210 steps.extend(rest.iter().cloned());
14211 let rooted = ast::Path { steps, partial: false };
14212 let synthetic = ast::SelectStmt {
14213 result: Expr::Path(rooted.clone()),
14214 filter: None,
14215 order_by: vec![],
14216 offset: None,
14217 limit: None,
14218 lock: None,
14219 };
14220 let mut ps = self.compile_path_select(&synthetic, &rooted, &[], false)?;
14221 let correlation = IrExpr::BinOp(Box::new(IrBinOp {
14222 left: IrExpr::ColumnRef {
14223 alias: ps.root.alias.clone(),
14224 column: "id".to_string(),
14225 pg_type: "uuid".to_string(),
14226 },
14227 op: ast::BinOpKind::Eq,
14228 right: self.for_var_ref(var),
14229 }));
14230 ps.filter = and_conditions(ps.filter, vec![correlation]);
14231 return Ok(IrExpr::PathSubquery(Box::new(ps)));
14232 }
14233 if let [ast::PathStep::Name(var), ast::PathStep::TypeIntersection(type_ref)] = p.steps.as_slice()
14234 && self.for_var_types.contains_key(var)
14235 {
14236 let type_name = match &type_ref.module {
14237 Some(m) => format!("{}::{}", m, type_ref.name),
14238 None => type_ref.name.clone(),
14239 };
14240 let narrowed = self.resolve_type(&type_name)?;
14241 let narrowed_alias = self.fresh_alias();
14242 let source = IrSource {
14243 poly: self.poly_fanout_for(&format!("{}::{}", narrowed.module, narrowed.name)),
14244 type_name: format!("{}::{}", narrowed.module, narrowed.name),
14245 table: narrowed.table.clone(),
14246 alias: narrowed_alias.clone(),
14247 };
14248 let filter = IrExpr::BinOp(Box::new(IrBinOp {
14249 left: IrExpr::ColumnRef {
14250 alias: narrowed_alias,
14251 column: "id".to_string(),
14252 pg_type: "uuid".to_string(),
14253 },
14254 op: ast::BinOpKind::Eq,
14255 right: self.for_var_ref(var),
14256 }));
14257 return Ok(IrExpr::Subquery(Box::new(IrSelect::schema_bound(
14258 source,
14259 Self::pk_returning(narrowed),
14260 Some(filter),
14261 ))));
14262 }
14263 return Err(PyQLError::Type(PyQLTypeError {
14264 message: "absolute paths are not valid in expression context; use .name".into(),
14265 position: Position { line: 0, col: 0 },
14266 }));
14267 }
14268
14269 if p.partial
14274 && let [ast::PathStep::LinkProp(prop_name)] = p.steps.as_slice()
14275 {
14276 return self.compile_link_prop_ref(prop_name);
14277 }
14278
14279 if p.partial && matches!(p.steps.first(), Some(ast::PathStep::TypeIntersection(_))) {
14281 return self.compile_type_intersection_expr(&p.steps, td, alias);
14282 }
14283
14284 if p.partial && matches!(p.steps.first(), Some(ast::PathStep::Backlink(_))) {
14289 return self.compile_partial_path_as_subquery(p, td, alias);
14290 }
14291 if p.partial
14296 && p.steps.len() > 1
14297 && let Some(ast::PathStep::Name(first)) = p.steps.first()
14298 && let Some(Expr::IfElse(ie)) = self
14299 .active_declared_pointers
14300 .iter()
14301 .find(|d| path_leaf(&d.path).is_ok_and(|name| name == first))
14302 .and_then(|d| d.compexpr.clone())
14303 {
14304 let extend = |branch: &Expr| match branch {
14305 Expr::Path(bp) => {
14306 let mut steps = bp.steps.clone();
14307 steps.extend(p.steps[1..].iter().cloned());
14308 Some(Expr::Path(ast::Path {
14309 steps,
14310 partial: bp.partial,
14311 }))
14312 }
14313 Expr::SubQuery(_) => p.steps[1..].iter().try_fold(branch.clone(), |expr, step| match step {
14316 ast::PathStep::Name(field) => Some(Expr::FieldAccess {
14317 expr: Box::new(expr),
14318 field: field.clone(),
14319 }),
14320 _ => None,
14321 }),
14322 Expr::Set(items) if items.is_empty() => Some(Expr::Set(vec![])),
14324 Expr::TypeCast(cast) if matches!(&cast.expr, Expr::Set(items) if items.is_empty()) => {
14325 Some(Expr::Set(vec![]))
14326 }
14327 _ => None,
14328 };
14329 if let (Some(if_expr), Some(else_expr)) = (extend(&ie.if_expr), extend(&ie.else_expr)) {
14330 let distributed = Expr::IfElse(Box::new(ast::IfElse {
14331 condition: ie.condition.clone(),
14332 if_expr,
14333 else_expr,
14334 }));
14335 return self.compile_expr(&distributed, td, alias);
14336 }
14337 }
14338
14339 let narrows_last = matches!(p.steps.last(), Some(ast::PathStep::TypeIntersection(_)));
14344
14345 if p.steps.len() == 2 && !narrows_last {
14346 return self.compile_path_2step(p, td, alias);
14347 }
14348
14349 if p.steps.len() != 1 {
14352 return self.compile_partial_path_as_subquery(p, td, alias);
14353 }
14354
14355 let pointer_name = match &p.steps[0] {
14356 ast::PathStep::Name(n) => n.as_str(),
14357 _ => {
14358 return Err(PyQLError::Type(PyQLTypeError {
14359 message: "type intersections are not valid in expression context".into(),
14360 position: Position { line: 0, col: 0 },
14361 }));
14362 }
14363 };
14364
14365 if pointer_name == "__type__" {
14368 return Ok(if self.is_polymorphic(td) {
14369 IrExpr::ColumnRef {
14370 alias: alias.to_string(),
14371 column: "__type__".to_string(),
14372 pg_type: "text".to_string(),
14373 }
14374 } else {
14375 IrExpr::Literal(IrLiteral::Str(format!("{}::{}", td.module, td.name)))
14376 });
14377 }
14378
14379 if let Some(prop) = Self::resolve_property(td, pointer_name) {
14380 return Ok(IrExpr::ColumnRef {
14381 alias: alias.to_string(),
14382 column: prop.name.clone(),
14383 pg_type: prop.pg_type.clone(),
14384 });
14385 }
14386
14387 if let Some(link) = Self::resolve_link(td, pointer_name) {
14388 if link.is_junction_backed() {
14389 return self.junction_target_id_expr(td, link, alias);
14390 }
14391 return Ok(IrExpr::ColumnRef {
14393 alias: alias.to_string(),
14394 column: format!("{}_id", link.name),
14395 pg_type: "uuid".to_string(),
14396 });
14397 }
14398
14399 if let Some(cd) = self.resolve_computed(td, pointer_name) {
14401 let expr_ast = crate::parse::parse_pointer_expr(&cd.expression).map_err(PyQLError::Syntax)?;
14402 return self.compile_expr(&expr_ast, td, alias);
14403 }
14404
14405 if Self::resolve_multilink(td, pointer_name).is_some() {
14411 return self.compile_partial_path_as_subquery(p, td, alias);
14412 }
14413
14414 if let Some(expr) = self
14419 .active_declared_pointers
14420 .iter()
14421 .find(|d| path_leaf(&d.path).is_ok_and(|n| n == pointer_name))
14422 .and_then(|d| d.compexpr.clone())
14423 {
14424 return self.compile_expr(&expr, td, alias);
14425 }
14426
14427 Err(self.field_err(pointer_name, &format!("{}::{}", td.module, td.name)))
14428 }
14429
14430 fn compile_free_path(&mut self, p: &ast::Path) -> Result<IrExpr, PyQLError> {
14435 if p.partial {
14436 if let Some((qualified, alias)) = self.anchors.last().map(|a| (a.qualified.clone(), a.alias.clone())) {
14440 let td = self.resolve_type(&qualified)?;
14441 return self.compile_path(p, td, &alias);
14442 }
14443 return Err(self.type_err(
14444 "property reference (.name) is not valid in free SELECT; \
14445 use a schema-bound SELECT instead",
14446 ));
14447 }
14448 if p.steps.len() == 2
14449 && let [ast::PathStep::Name(type_ref), ast::PathStep::Name(variant)] = p.steps.as_slice()
14450 && self.resolve_enum(type_ref).is_some()
14451 {
14452 return self.compile_enum_access(type_ref, variant);
14453 }
14454 if let Some(resolved) = self.resolve_cte_path(p) {
14457 return resolved;
14458 }
14459 if p.steps.len() == 1
14460 && let ast::PathStep::Name(n) = &p.steps[0]
14461 && let Some(ir) = self.resolve_name_ref(n, true)
14462 {
14463 return Ok(ir);
14464 }
14465 if !p.partial
14471 && p.steps.len() > 1
14472 && let Some(ast::PathStep::Name(root)) = p.steps.first()
14473 && self.cte_object_type(root).is_some()
14474 {
14475 let synthetic = ast::SelectStmt {
14476 result: Expr::Path(p.clone()),
14477 filter: None,
14478 order_by: vec![],
14479 offset: None,
14480 limit: None,
14481 lock: None,
14482 };
14483 let ps = self.compile_path_select(&synthetic, p, &[], false)?;
14484 let widens = ps.joins.iter().any(|join| !matches!(join, IrPathJoin::Single { .. }));
14489 let root_is_multi = self.multi_row_ctes.contains(root.as_str());
14490 let multi = matches!(ps.result, IrPathResult::Scalar(..)) && (widens || root_is_multi);
14491 return Ok(if multi {
14492 IrExpr::ArrayFromSelect(Box::new(IrArraySource::PathSelect(Box::new(ps))))
14493 } else {
14494 IrExpr::PathSubquery(Box::new(ps))
14495 });
14496 }
14497 Err(self.type_err("expression is not valid in free SELECT context"))
14498 }
14499
14500 fn compile_path_2step(&mut self, p: &ast::Path, td: &TypeDescriptor, alias: &str) -> Result<IrExpr, PyQLError> {
14501 let link_name = match &p.steps[0] {
14502 ast::PathStep::Name(n) => n.as_str(),
14503 _ => {
14504 return Err(PyQLError::Type(PyQLTypeError {
14505 message: "type intersections are not valid in expression context".into(),
14506 position: Position { line: 0, col: 0 },
14507 }));
14508 }
14509 };
14510 let pointer_name = match &p.steps[1] {
14511 ast::PathStep::Name(n) => n.as_str(),
14512 _ => {
14513 return Err(PyQLError::Type(PyQLTypeError {
14514 message: "type intersections are not valid in expression context".into(),
14515 position: Position { line: 0, col: 0 },
14516 }));
14517 }
14518 };
14519
14520 if let Some(link) = Self::resolve_link(td, link_name) {
14521 if pointer_name == "id" {
14522 if link.is_junction_backed() {
14523 return self.junction_target_id_expr(td, link, alias);
14524 }
14525 return Ok(IrExpr::ColumnRef {
14526 alias: alias.to_string(),
14527 column: format!("{}_id", link_name),
14528 pg_type: "uuid".to_string(),
14529 });
14530 }
14531 let target_td = self.resolve_type(&link.target)?;
14532 if let Some(prop) = Self::resolve_property(target_td, pointer_name) {
14533 let ft_alias = self.fresh_alias();
14534 let target_id_expr = if link.is_junction_backed() {
14535 self.junction_target_id_expr(td, link, alias)?
14536 } else {
14537 IrExpr::ColumnRef {
14538 alias: alias.to_string(),
14539 column: format!("{}_id", link_name),
14540 pg_type: "uuid".to_string(),
14541 }
14542 };
14543 return Ok(IrExpr::Subquery(Box::new(IrSelect::schema_bound(
14544 IrSource {
14545 poly: None,
14546 type_name: format!("{}::{}", target_td.module, target_td.name),
14547 table: target_td.table.clone(),
14548 alias: ft_alias.clone(),
14549 },
14550 vec![IrShapePointer::Scalar(IrScalarPointer {
14551 implicit_id: false,
14552 marker_offset: None,
14553 alias: prop.name.clone(),
14554 column: prop.name.clone(),
14555 pg_type: prop.pg_type.clone(),
14556 tuple_shape: self.resolve_property_tuple_shape(prop),
14557 })],
14558 Some(IrExpr::BinOp(Box::new(IrBinOp {
14559 left: IrExpr::ColumnRef {
14560 alias: ft_alias.clone(),
14561 column: "id".to_string(),
14562 pg_type: "uuid".to_string(),
14563 },
14564 op: ast::BinOpKind::Eq,
14565 right: target_id_expr,
14566 }))),
14567 ))));
14568 }
14569 return self.compile_partial_path_as_subquery(p, td, alias);
14572 }
14573
14574 if Self::resolve_multilink(td, link_name).is_some() {
14578 return self.compile_partial_path_as_subquery(p, td, alias);
14579 }
14580
14581 self.compile_partial_path_as_subquery(p, td, alias)
14588 }
14589
14590 fn try_backlink_exists(
14594 &mut self,
14595 b: &ast::BinOp,
14596 td: &TypeDescriptor,
14597 alias: &str,
14598 ) -> Result<Option<IrExpr>, PyQLError> {
14599 use ast::PathStep;
14600 fn is_backlink(p: &ast::Path) -> bool {
14601 p.partial && matches!(p.steps.first(), Some(PathStep::Backlink(_)))
14602 }
14603 let (path_steps, value_ast, flip) = if let Expr::Path(p) = &b.left {
14604 if is_backlink(p) {
14605 (p.steps.as_slice(), &b.right, false)
14606 } else {
14607 return Ok(None);
14608 }
14609 } else if let Expr::Path(p) = &b.right {
14610 if is_backlink(p) {
14611 (p.steps.as_slice(), &b.left, true)
14612 } else {
14613 return Ok(None);
14614 }
14615 } else {
14616 return Ok(None);
14617 };
14618 let value_expr = self.compile_expr(value_ast, td, alias)?;
14619 let current_qname = format!("{}::{}", td.module, td.name);
14620 let exists = self.compile_backlink_as_exists(
14621 path_steps,
14622 Some((b.op.clone(), value_expr, flip)),
14623 ¤t_qname,
14624 alias,
14625 )?;
14626 Ok(Some(exists))
14627 }
14628
14629 fn compile_backlink_as_exists(
14633 &mut self,
14634 steps: &[ast::PathStep],
14635 comparison: Option<(ast::BinOpKind, IrExpr, bool)>,
14636 current_qname: &str,
14637 alias: &str,
14638 ) -> Result<IrExpr, PyQLError> {
14639 use ast::PathStep;
14640
14641 let backlink_name = match steps.first() {
14642 Some(PathStep::Backlink(n)) => n.clone(),
14643 _ => return Err(self.type_err("internal: expected backlink step")),
14644 };
14645 let Some(PathStep::TypeIntersection(type_ref)) = steps.get(1) else {
14649 return self.backlink_exists_over_owners(
14652 None,
14653 &backlink_name,
14654 &steps[1..],
14655 comparison,
14656 current_qname,
14657 alias,
14658 );
14659 };
14660
14661 let type_name = match &type_ref.module {
14662 Some(m) => format!("{}::{}", m, type_ref.name),
14663 None => type_ref.name.clone(),
14664 };
14665 let target_td = self.resolve_type(&type_name)?;
14666 if self.declares_backlink(target_td, &backlink_name, current_qname) {
14667 return self.backlink_exists_for_owner(
14668 target_td,
14669 &backlink_name,
14670 &steps[2..],
14671 comparison,
14672 current_qname,
14673 alias,
14674 );
14675 }
14676 let narrow_to = format!("{}::{}", target_td.module, target_td.name);
14681 self.backlink_exists_over_owners(
14682 Some(&narrow_to),
14683 &backlink_name,
14684 &steps[2..],
14685 comparison,
14686 current_qname,
14687 alias,
14688 )
14689 }
14690
14691 fn backlink_owner(
14694 &self,
14695 owner_td: &'a TypeDescriptor,
14696 backlink_name: &str,
14697 current_qname: &str,
14698 ) -> Result<&'a TypeDescriptor, PyQLError> {
14699 Ok(if self.declares_backlink(owner_td, backlink_name, current_qname) {
14705 owner_td
14706 } else {
14707 let narrowed = format!("{}::{}", owner_td.module, owner_td.name);
14710 let declaring: Vec<&'a TypeDescriptor> = self
14711 .schema
14712 .types
14713 .iter()
14714 .filter(|t| !t.abstract_ && Self::is_or_implements(t, &narrowed))
14715 .filter(|t| self.declares_backlink(t, backlink_name, current_qname))
14716 .collect();
14717 let declaring = Self::without_inherited_owners(declaring);
14718 match declaring.as_slice() {
14719 [only] => only,
14720 [] => owner_td,
14721 several => {
14722 return Err(self.type_err(&format!(
14723 "'{backlink_name}' pointing to {current_qname} is declared by {} types under \
14724 {narrowed} ({}), so a backlink narrowed to it has no single source to read \
14725 — narrow to one of them instead",
14726 several.len(),
14727 several
14728 .iter()
14729 .map(|t| format!("{}::{}", t.module, t.name))
14730 .collect::<Vec<_>>()
14731 .join(", "),
14732 )));
14733 }
14734 }
14735 })
14736 }
14737
14738 fn backlink_is_single(&self, owner_td: &'a TypeDescriptor, backlink_name: &str, current_qname: &str) -> bool {
14741 let Ok(owner) = self.backlink_owner(owner_td, backlink_name, current_qname) else {
14742 return false;
14743 };
14744 owner
14745 .links
14746 .iter()
14747 .any(|l| l.name == backlink_name && l.is_exclusive && self.link_target_reaches(&l.target, current_qname))
14748 || owner.multilinks.iter().any(|ml| {
14749 ml.name == backlink_name && ml.is_exclusive && self.link_target_reaches(&ml.target, current_qname)
14750 })
14751 }
14752
14753 fn declares_backlink(&self, td: &TypeDescriptor, backlink_name: &str, current_qname: &str) -> bool {
14756 td.links
14757 .iter()
14758 .any(|l| l.name == backlink_name && self.link_target_reaches(&l.target, current_qname))
14759 || td
14760 .multilinks
14761 .iter()
14762 .any(|ml| ml.name == backlink_name && self.link_target_reaches(&ml.target, current_qname))
14763 }
14764
14765 #[allow(clippy::too_many_arguments)]
14769 fn without_inherited_owners(owners: Vec<&'a TypeDescriptor>) -> Vec<&'a TypeDescriptor> {
14772 let qnames: Vec<String> = owners.iter().map(|t| format!("{}::{}", t.module, t.name)).collect();
14773 owners
14774 .into_iter()
14775 .filter(|t| !t.bases.iter().any(|base| qnames.contains(base)))
14776 .collect()
14777 }
14778
14779 fn backlink_exists_over_owners(
14780 &mut self,
14781 narrow_to: Option<&str>,
14782 backlink_name: &str,
14783 rest: &[ast::PathStep],
14784 comparison: Option<(ast::BinOpKind, IrExpr, bool)>,
14785 current_qname: &str,
14786 alias: &str,
14787 ) -> Result<IrExpr, PyQLError> {
14788 let schema = self.schema;
14789 let owners: Vec<&'a TypeDescriptor> = schema
14790 .types
14791 .iter()
14792 .filter(|t| !t.abstract_)
14793 .filter(|t| narrow_to.is_none_or(|q| Self::is_or_implements(t, q)))
14794 .filter(|t| self.declares_backlink(t, backlink_name, current_qname))
14795 .collect();
14796 let owners = Self::without_inherited_owners(owners);
14797 if owners.is_empty() {
14798 return Err(self.type_err(&match narrow_to {
14799 Some(q) => format!("type {q} has no link or multi-link '{backlink_name}' pointing to {current_qname}"),
14800 None => format!("no type has a link or multi-link '{backlink_name}' pointing to {current_qname}"),
14801 }));
14802 }
14803 let mut combined: Option<IrExpr> = None;
14804 for owner_td in owners {
14805 let one = self.backlink_exists_for_owner(
14806 owner_td,
14807 backlink_name,
14808 rest,
14809 comparison.clone(),
14810 current_qname,
14811 alias,
14812 )?;
14813 combined = Some(match combined {
14814 None => one,
14815 Some(previous) => IrExpr::BinOp(Box::new(IrBinOp {
14816 left: previous,
14817 op: ast::BinOpKind::Or,
14818 right: one,
14819 })),
14820 });
14821 }
14822 Ok(combined.expect("owners is non-empty"))
14823 }
14824
14825 fn is_or_implements(td: &TypeDescriptor, qname: &str) -> bool {
14827 format!("{}::{}", td.module, td.name) == qname
14828 || td.interfaces.iter().any(|i| i == qname)
14829 || td.parents.iter().any(|p| p == qname)
14830 || td.bases.iter().any(|b| b == qname)
14831 }
14832
14833 fn has_subtypes(&self, td: &TypeDescriptor) -> bool {
14837 let qname = format!("{}::{}", td.module, td.name);
14838 !td.abstract_ && self.schema.types.iter().any(|t| t.bases.contains(&qname))
14839 }
14840
14841 fn backs_no_relation(td: &TypeDescriptor) -> bool {
14845 td.abstract_ && !td.materialized
14846 }
14847
14848 fn subtype_fanouts(&self) -> HashMap<(String, String), IrPolyFanout> {
14850 self.schema
14851 .types
14852 .iter()
14853 .filter(|t| self.has_subtypes(t) || Self::backs_no_relation(t))
14854 .filter_map(|t| {
14855 let fanout = self.poly_fanout_for(&format!("{}::{}", t.module, t.name))?;
14856 Some(((t.module.clone(), t.table.clone()), fanout))
14857 })
14858 .collect()
14859 }
14860
14861 fn is_polymorphic(&self, td: &TypeDescriptor) -> bool {
14866 td.abstract_ || self.has_subtypes(td)
14867 }
14868
14869 #[allow(clippy::too_many_arguments)]
14873 fn backlink_exists_for_owner(
14874 &mut self,
14875 target_td: &'a TypeDescriptor,
14876 backlink_name: &str,
14877 rest: &[ast::PathStep],
14878 comparison: Option<(ast::BinOpKind, IrExpr, bool)>,
14879 current_qname: &str,
14880 alias: &str,
14881 ) -> Result<IrExpr, PyQLError> {
14882 let backlink_name = backlink_name.to_string();
14883 let target_qname = format!("{}::{}", target_td.module, target_td.name);
14884 let target_table = target_td.table.clone();
14885 let t_alias = self.fresh_alias();
14886
14887 let join_cond = if let Some(l) = target_td
14896 .links
14897 .iter()
14898 .find(|l| l.name == backlink_name && self.link_target_reaches(&l.target, current_qname))
14899 {
14900 if l.is_junction_backed() {
14901 let (jt_table, jt_module, jt_owner_col, jt_current_col, _) = self.link_junction_info(target_td, l)?;
14905 let jt_alias = self.fresh_alias();
14906 IrExpr::UnaryOp(Box::new(IrUnaryOp {
14907 op: ast::UnaryOpKind::Exists,
14908 operand: IrExpr::Subquery(Box::new(IrSelect::schema_bound(
14909 IrSource {
14910 poly: None,
14911 type_name: format!("{}::__jt__", jt_module),
14912 table: jt_table,
14913 alias: jt_alias.clone(),
14914 },
14915 vec![],
14916 Some(IrExpr::BinOp(Box::new(IrBinOp {
14917 left: IrExpr::BinOp(Box::new(IrBinOp {
14918 left: IrExpr::ColumnRef {
14919 alias: jt_alias.clone(),
14920 column: jt_owner_col,
14921 pg_type: "uuid".to_string(),
14922 },
14923 op: ast::BinOpKind::Eq,
14924 right: IrExpr::ColumnRef {
14925 alias: t_alias.clone(),
14926 column: "id".to_string(),
14927 pg_type: "uuid".to_string(),
14928 },
14929 })),
14930 op: ast::BinOpKind::And,
14931 right: IrExpr::BinOp(Box::new(IrBinOp {
14932 left: IrExpr::ColumnRef {
14933 alias: jt_alias,
14934 column: jt_current_col,
14935 pg_type: "uuid".to_string(),
14936 },
14937 op: ast::BinOpKind::Eq,
14938 right: IrExpr::ColumnRef {
14939 alias: alias.to_string(),
14940 column: "id".to_string(),
14941 pg_type: "uuid".to_string(),
14942 },
14943 })),
14944 }))),
14945 ))),
14946 }))
14947 } else {
14948 let fk_col = format!("{}_id", backlink_name);
14949 IrExpr::BinOp(Box::new(IrBinOp {
14951 left: IrExpr::ColumnRef {
14952 alias: t_alias.clone(),
14953 column: fk_col,
14954 pg_type: "uuid".to_string(),
14955 },
14956 op: ast::BinOpKind::Eq,
14957 right: IrExpr::ColumnRef {
14958 alias: alias.to_string(),
14959 column: "id".to_string(),
14960 pg_type: "uuid".to_string(),
14961 },
14962 }))
14963 }
14964 } else if let Some(ml) = target_td
14965 .multilinks
14966 .iter()
14967 .find(|ml| ml.name == backlink_name && self.link_target_reaches(&ml.target, current_qname))
14968 .cloned()
14969 {
14970 let (jt_table, jt_module, _, _, _) = self.multilink_junction_info(target_td, &ml)?;
14975 let jt_alias = self.fresh_alias();
14976 IrExpr::UnaryOp(Box::new(IrUnaryOp {
14977 op: ast::UnaryOpKind::Exists,
14978 operand: IrExpr::Subquery(Box::new(IrSelect::schema_bound(
14979 IrSource {
14980 poly: None,
14981 type_name: format!("{}::__jt__", jt_module),
14982 table: jt_table,
14983 alias: jt_alias.clone(),
14984 },
14985 vec![],
14986 Some(IrExpr::BinOp(Box::new(IrBinOp {
14987 left: IrExpr::BinOp(Box::new(IrBinOp {
14988 left: IrExpr::ColumnRef {
14989 alias: jt_alias.clone(),
14990 column: "source".to_string(),
14991 pg_type: "uuid".to_string(),
14992 },
14993 op: ast::BinOpKind::Eq,
14994 right: IrExpr::ColumnRef {
14995 alias: t_alias.clone(),
14996 column: "id".to_string(),
14997 pg_type: "uuid".to_string(),
14998 },
14999 })),
15000 op: ast::BinOpKind::And,
15001 right: IrExpr::BinOp(Box::new(IrBinOp {
15002 left: IrExpr::ColumnRef {
15003 alias: jt_alias,
15004 column: "target".to_string(),
15005 pg_type: "uuid".to_string(),
15006 },
15007 op: ast::BinOpKind::Eq,
15008 right: IrExpr::ColumnRef {
15009 alias: alias.to_string(),
15010 column: "id".to_string(),
15011 pg_type: "uuid".to_string(),
15012 },
15013 })),
15014 }))),
15015 ))),
15016 }))
15017 } else {
15018 return Err(PyQLError::Type(PyQLTypeError {
15019 message: format!(
15020 "type {} has no link or multi-link '{}' pointing to {}",
15021 target_qname, backlink_name, current_qname,
15022 ),
15023 position: Position { line: 0, col: 0 },
15024 }));
15025 };
15026
15027 let tail_cond = self.compile_backlink_tail(rest, comparison, &target_qname, &t_alias)?;
15028
15029 let filter = match tail_cond {
15030 Some(tc) => IrExpr::BinOp(Box::new(IrBinOp {
15031 left: join_cond,
15032 op: ast::BinOpKind::And,
15033 right: tc,
15034 })),
15035 None => join_cond,
15036 };
15037
15038 Ok(IrExpr::UnaryOp(Box::new(IrUnaryOp {
15039 op: ast::UnaryOpKind::Exists,
15040 operand: IrExpr::Subquery(Box::new(IrSelect::schema_bound(
15041 IrSource {
15042 poly: None,
15043 type_name: target_qname,
15044 table: target_table,
15045 alias: t_alias,
15046 },
15047 vec![],
15048 Some(filter),
15049 ))),
15050 })))
15051 }
15052
15053 fn compile_backlink_tail(
15055 &mut self,
15056 steps: &[ast::PathStep],
15057 comparison: Option<(ast::BinOpKind, IrExpr, bool)>,
15058 target_qname: &str,
15059 t_alias: &str,
15060 ) -> Result<Option<IrExpr>, PyQLError> {
15061 use ast::PathStep;
15062
15063 if steps.is_empty() {
15068 return Ok(comparison.map(|(op, value, flip)| {
15069 let key = IrExpr::ColumnRef {
15070 alias: t_alias.to_string(),
15071 column: "id".to_string(),
15072 pg_type: "uuid".to_string(),
15073 };
15074 let (left, right) = if flip { (value, key) } else { (key, value) };
15075 IrExpr::BinOp(Box::new(IrBinOp { left, op, right }))
15076 }));
15077 }
15078
15079 if matches!(steps.first(), Some(PathStep::Backlink(_))) {
15081 let inner = self.compile_backlink_as_exists(steps, comparison, target_qname, t_alias)?;
15082 return Ok(Some(inner));
15083 }
15084
15085 let target_td = self.resolve_type(target_qname)?;
15086
15087 if let [PathStep::Name(pointer_name)] = steps {
15089 if let Some(prop) = Self::resolve_property(target_td, pointer_name) {
15090 let col = IrExpr::ColumnRef {
15091 alias: t_alias.to_string(),
15092 column: prop.name.clone(),
15093 pg_type: prop.pg_type.clone(),
15094 };
15095 return Ok(Some(Self::apply_comparison(col, comparison)));
15096 }
15097 if let Some(link) = Self::resolve_link(target_td, pointer_name) {
15098 let col = if link.is_junction_backed() {
15099 self.junction_target_id_expr(target_td, link, t_alias)?
15100 } else {
15101 IrExpr::ColumnRef {
15102 alias: t_alias.to_string(),
15103 column: format!("{}_id", link.name),
15104 pg_type: "uuid".to_string(),
15105 }
15106 };
15107 return Ok(Some(Self::apply_comparison(col, comparison)));
15108 }
15109 }
15113
15114 if let [PathStep::Name(link_name), PathStep::Name(prop_name)] = steps
15116 && let Some(link) = Self::resolve_link(target_td, link_name)
15117 {
15118 let link_target = link.target.clone();
15119 let target_id_expr = if link.is_junction_backed() {
15120 self.junction_target_id_expr(target_td, link, t_alias)?
15121 } else {
15122 IrExpr::ColumnRef {
15123 alias: t_alias.to_string(),
15124 column: format!("{}_id", link_name),
15125 pg_type: "uuid".to_string(),
15126 }
15127 };
15128 let link_target_td = self.resolve_type(&link_target)?;
15129 let link_target_qname = format!("{}::{}", link_target_td.module, link_target_td.name);
15130 let link_target_table = link_target_td.table.clone();
15131 if let Some(prop) = Self::resolve_property(link_target_td, prop_name) {
15132 let l_alias = self.fresh_alias();
15133 let id_cond = IrExpr::BinOp(Box::new(IrBinOp {
15134 left: IrExpr::ColumnRef {
15135 alias: l_alias.clone(),
15136 column: "id".to_string(),
15137 pg_type: "uuid".to_string(),
15138 },
15139 op: ast::BinOpKind::Eq,
15140 right: target_id_expr,
15141 }));
15142 let col = IrExpr::ColumnRef {
15143 alias: l_alias.clone(),
15144 column: prop.name.clone(),
15145 pg_type: prop.pg_type.clone(),
15146 };
15147 let prop_cond = Self::apply_comparison(col, comparison);
15148 let full = IrExpr::BinOp(Box::new(IrBinOp {
15149 left: id_cond,
15150 op: ast::BinOpKind::And,
15151 right: prop_cond,
15152 }));
15153 return Ok(Some(IrExpr::UnaryOp(Box::new(IrUnaryOp {
15154 op: ast::UnaryOpKind::Exists,
15155 operand: IrExpr::Subquery(Box::new(IrSelect::schema_bound(
15156 IrSource {
15157 poly: None,
15158 type_name: link_target_qname,
15159 table: link_target_table,
15160 alias: l_alias,
15161 },
15162 vec![],
15163 Some(full),
15164 ))),
15165 }))));
15166 }
15167 }
15168
15169 let tail = ast::Path {
15174 steps: steps.to_vec(),
15175 partial: true,
15176 };
15177 let walked = self.compile_path(&tail, target_td, t_alias)?;
15178 Ok(Some(Self::apply_comparison(walked, comparison)))
15179 }
15180
15181 fn apply_comparison(col: IrExpr, comparison: Option<(ast::BinOpKind, IrExpr, bool)>) -> IrExpr {
15183 match comparison {
15184 Some((op, val, flip)) => {
15185 let (l, r) = if flip { (val, col) } else { (col, val) };
15186 if matches!(op, ast::BinOpKind::Eq | ast::BinOpKind::Ne)
15190 && matches!(l, IrExpr::ArrayFromSelect(_)) != matches!(r, IrExpr::ArrayFromSelect(_))
15191 {
15192 let (value, set) = if matches!(r, IrExpr::ArrayFromSelect(_)) {
15193 (l, r)
15194 } else {
15195 (r, l)
15196 };
15197 let membership = IrExpr::BinOp(Box::new(IrBinOp {
15198 left: value,
15199 op: ast::BinOpKind::In,
15200 right: set,
15201 }));
15202 return if matches!(op, ast::BinOpKind::Ne) {
15203 IrExpr::UnaryOp(Box::new(IrUnaryOp {
15204 op: ast::UnaryOpKind::Not,
15205 operand: membership,
15206 }))
15207 } else {
15208 membership
15209 };
15210 }
15211 IrExpr::BinOp(Box::new(IrBinOp { left: l, op, right: r }))
15212 }
15213 None => ir_is_not_null(col),
15214 }
15215 }
15216
15217 fn try_multilink_exists(
15219 &mut self,
15220 b: &ast::BinOp,
15221 td: &TypeDescriptor,
15222 alias: &str,
15223 ) -> Result<Option<IrExpr>, PyQLError> {
15224 fn ml_first_name(steps: &[ast::PathStep]) -> Option<&str> {
15228 match steps.first()? {
15229 ast::PathStep::Name(n) => Some(n.as_str()),
15230 _ => None,
15231 }
15232 }
15233
15234 let (path_steps, value_ast, flip) = if let Expr::Path(p) = &b.left {
15235 if p.partial {
15236 if let Some(ln) = ml_first_name(&p.steps) {
15237 if Self::resolve_multilink(td, ln).is_some() {
15238 (p.steps.as_slice(), &b.right, false)
15239 } else {
15240 return Ok(None);
15241 }
15242 } else {
15243 return Ok(None);
15244 }
15245 } else {
15246 return Ok(None);
15247 }
15248 } else if let Expr::Path(p) = &b.right {
15249 if p.partial {
15250 if let Some(ln) = ml_first_name(&p.steps) {
15251 if Self::resolve_multilink(td, ln).is_some() {
15252 (p.steps.as_slice(), &b.left, true)
15253 } else {
15254 return Ok(None);
15255 }
15256 } else {
15257 return Ok(None);
15258 }
15259 } else {
15260 return Ok(None);
15261 }
15262 } else {
15263 return Ok(None);
15264 };
15265
15266 let value_expr = self.compile_expr(value_ast, td, alias)?;
15267 let ml_name = match &path_steps[0] {
15268 ast::PathStep::Name(n) => n.clone(),
15269 _ => return Ok(None),
15270 };
15271 let ml = Self::resolve_multilink(td, &ml_name).unwrap();
15272
15273 if self.explicit_set_depth == 0 {
15276 let mut pointer_path = String::new();
15279 for step in path_steps {
15280 match step {
15281 ast::PathStep::Name(n) => {
15282 pointer_path.push('.');
15283 pointer_path.push_str(n);
15284 }
15285 ast::PathStep::Backlink(n) => {
15286 pointer_path.push_str(".<");
15287 pointer_path.push_str(n);
15288 }
15289 ast::PathStep::LinkProp(n) => {
15290 pointer_path.push('@');
15291 pointer_path.push_str(n);
15292 }
15293 ast::PathStep::TypeIntersection(t) => {
15294 pointer_path.push_str("[is ");
15295 pointer_path.push_str(&t.qualified_name());
15296 pointer_path.push(']');
15297 }
15298 }
15299 }
15300 self.warnings.push(format!(
15301 "possibly more than one element returned by an expression in a FILTER clause \
15302 (multi-link '{pointer_path}'); wrap with any() to make intent explicit",
15303 ));
15304 }
15305
15306 let ml_target = ml.target.clone();
15308 let ml_through = ml.through.clone();
15309 let td_module = td.module.clone();
15310 let td_name = td.name.clone();
15311 let td_table = td.table.clone();
15312
15313 let tail_steps: Vec<ast::PathStep> = path_steps[1..].to_vec();
15315
15316 let jt_alias = self.fresh_alias();
15317
15318 let (jt_table, jt_module, jt_src_col, jt_tgt_col) = if let Some(through_qname) = &ml_through {
15320 let through_td = self.resolve_type(through_qname)?;
15321 if through_td.junction {
15322 (
15325 format!("{}.{}", td_table, ml_name),
15326 td_module.clone(),
15327 "source".to_string(),
15328 "target".to_string(),
15329 )
15330 } else {
15331 let source_qname = format!("{}::{}", td_module, td_name);
15332 let src_col = through_td
15333 .links
15334 .iter()
15335 .find(|l| l.target == source_qname)
15336 .ok_or_else(|| {
15337 PyQLError::Type(PyQLTypeError {
15338 message: format!("through type {through_qname} has no link to {source_qname}"),
15339 position: Position { line: 0, col: 0 },
15340 })
15341 })?
15342 .name
15343 .clone();
15344 let tgt_col = through_td
15345 .links
15346 .iter()
15347 .find(|l| l.target == ml_target && l.name != src_col)
15348 .or_else(|| through_td.links.iter().find(|l| l.target == ml_target))
15349 .ok_or_else(|| {
15350 PyQLError::Type(PyQLTypeError {
15351 message: format!("through type {through_qname} has no link to {ml_target}"),
15352 position: Position { line: 0, col: 0 },
15353 })
15354 })?
15355 .name
15356 .clone();
15357 (
15358 through_td.table.clone(),
15359 through_td.module.clone(),
15360 format!("{}_id", src_col),
15361 format!("{}_id", tgt_col),
15362 )
15363 }
15364 } else {
15365 (
15366 format!("{}.{}", td_table, ml_name),
15367 td_module.clone(),
15368 "source".to_string(),
15369 "target".to_string(),
15370 )
15371 };
15372
15373 let src_filter = IrExpr::BinOp(Box::new(IrBinOp {
15375 left: IrExpr::ColumnRef {
15376 alias: jt_alias.clone(),
15377 column: jt_src_col,
15378 pg_type: "uuid".to_string(),
15379 },
15380 op: ast::BinOpKind::Eq,
15381 right: IrExpr::ColumnRef {
15382 alias: alias.to_string(),
15383 column: "id".to_string(),
15384 pg_type: "uuid".to_string(),
15385 },
15386 }));
15387
15388 let tail_filter = self.compile_path_tail_filter(
15390 &tail_steps,
15391 b.op.clone(),
15392 value_expr,
15393 flip,
15394 &ml_target,
15395 &jt_alias,
15396 &jt_tgt_col,
15397 )?;
15398
15399 let full_filter = IrExpr::BinOp(Box::new(IrBinOp {
15400 left: src_filter,
15401 op: ast::BinOpKind::And,
15402 right: tail_filter,
15403 }));
15404
15405 let jt_source = IrSource {
15407 poly: None,
15408 type_name: format!("{}::__jt__", jt_module),
15409 table: jt_table,
15410 alias: jt_alias,
15411 };
15412 let inner = IrExpr::Subquery(Box::new(IrSelect::schema_bound(jt_source, vec![], Some(full_filter))));
15413
15414 Ok(Some(IrExpr::UnaryOp(Box::new(IrUnaryOp {
15415 op: ast::UnaryOpKind::Exists,
15416 operand: inner,
15417 }))))
15418 }
15419
15420 #[allow(clippy::too_many_arguments)]
15427 fn compile_path_tail_filter(
15428 &mut self,
15429 steps: &[ast::PathStep],
15430 op: ast::BinOpKind,
15431 value_expr: IrExpr,
15432 flip: bool,
15433 target_type: &str,
15434 jt_alias: &str,
15435 jt_tgt_col: &str,
15436 ) -> Result<IrExpr, PyQLError> {
15437 if steps.is_empty() {
15440 let col_ref = IrExpr::ColumnRef {
15441 alias: jt_alias.to_string(),
15442 column: jt_tgt_col.to_string(),
15443 pg_type: "uuid".to_string(),
15444 };
15445 let (left, right) = if flip {
15446 (value_expr, col_ref)
15447 } else {
15448 (col_ref, value_expr)
15449 };
15450 return Ok(IrExpr::BinOp(Box::new(IrBinOp { left, op, right })));
15451 }
15452
15453 if let Some(ast::PathStep::TypeIntersection(type_ref)) = steps.first() {
15459 let narrowed = self.resolve_type(&type_ref.qualified_name())?;
15460 let narrowed_qname = format!("{}::{}", narrowed.module, narrowed.name);
15461 return self.compile_path_tail_filter(
15462 &steps[1..],
15463 op,
15464 value_expr,
15465 flip,
15466 &narrowed_qname,
15467 jt_alias,
15468 jt_tgt_col,
15469 );
15470 }
15471
15472 let first_name = match steps.first() {
15473 Some(ast::PathStep::Name(n)) => n.clone(),
15474 _ => return Err(self.type_err("expected a property or link name in path")),
15475 };
15476
15477 let target_td = self.resolve_type(target_type)?;
15478 let target_table = target_td.table.clone();
15479
15480 if steps.len() == 1 {
15481 if first_name == "id" {
15483 let col_ref = IrExpr::ColumnRef {
15485 alias: jt_alias.to_string(),
15486 column: jt_tgt_col.to_string(),
15487 pg_type: "uuid".to_string(),
15488 };
15489 let (l, r) = if flip {
15490 (value_expr, col_ref)
15491 } else {
15492 (col_ref, value_expr)
15493 };
15494 return Ok(IrExpr::BinOp(Box::new(IrBinOp { left: l, op, right: r })));
15495 }
15496 let single_link = target_td
15502 .links
15503 .iter()
15504 .find(|l| l.name == first_name && !l.is_junction_backed());
15505 if single_link.is_none() && target_td.multilinks.iter().any(|l| l.name == first_name) {
15506 let target_display = target_type.replace("::", ".");
15507 return Err(PyQLError::Type(PyQLTypeError {
15508 message: format!(
15509 "operator '{op}' cannot be applied to operands of type '{target_display}' and the value type",
15510 op = op,
15511 ),
15512 position: Position { line: 0, col: 0 },
15513 }));
15514 }
15515 let (prop_name, prop_pg) = match single_link {
15516 Some(link) => (format!("{}_id", link.name), "uuid".to_string()),
15517 None => {
15518 let prop = target_td
15519 .properties
15520 .iter()
15521 .find(|p| p.name == first_name)
15522 .ok_or_else(|| self.field_err(&first_name, target_type))?;
15523 (prop.name.clone(), prop.pg_type.clone())
15524 }
15525 };
15526 let tgt_alias = self.fresh_alias();
15527 let id_filter = IrExpr::BinOp(Box::new(IrBinOp {
15529 left: IrExpr::ColumnRef {
15530 alias: tgt_alias.clone(),
15531 column: "id".to_string(),
15532 pg_type: "uuid".to_string(),
15533 },
15534 op: ast::BinOpKind::Eq,
15535 right: IrExpr::ColumnRef {
15536 alias: jt_alias.to_string(),
15537 column: jt_tgt_col.to_string(),
15538 pg_type: "uuid".to_string(),
15539 },
15540 }));
15541 let prop_col = IrExpr::ColumnRef {
15542 alias: tgt_alias.clone(),
15543 column: prop_name,
15544 pg_type: prop_pg,
15545 };
15546 let (pl, pr) = if flip {
15547 (value_expr, prop_col)
15548 } else {
15549 (prop_col, value_expr)
15550 };
15551 let prop_filter = IrExpr::BinOp(Box::new(IrBinOp {
15552 left: pl,
15553 op,
15554 right: pr,
15555 }));
15556 let full = IrExpr::BinOp(Box::new(IrBinOp {
15557 left: id_filter,
15558 op: ast::BinOpKind::And,
15559 right: prop_filter,
15560 }));
15561 let inner = IrExpr::Subquery(Box::new(IrSelect::schema_bound(
15562 IrSource {
15563 poly: None,
15564 type_name: target_type.to_string(),
15565 table: target_table,
15566 alias: tgt_alias,
15567 },
15568 vec![],
15569 Some(full),
15570 )));
15571 return Ok(IrExpr::UnaryOp(Box::new(IrUnaryOp {
15572 op: ast::UnaryOpKind::Exists,
15573 operand: inner,
15574 })));
15575 }
15576
15577 if target_td.multilinks.iter().any(|l| l.name == first_name) {
15579 return Err(self.type_err("nested multi-link traversal in comparison is not yet supported"));
15580 }
15581 let link = target_td
15582 .links
15583 .iter()
15584 .find(|l| l.name == first_name)
15585 .ok_or_else(|| self.field_err(&first_name, target_type))?;
15586 if link.is_junction_backed() {
15587 return Err(self.type_err(&format!(
15594 "filtering through a junction-backed single link ('{first_name}') nested inside \
15595 a multi-link path comparison is not yet supported — filter on '.{first_name}' \
15596 directly instead"
15597 )));
15598 }
15599 let next_target = link.target.clone();
15600 let fk_col = format!("{}_id", first_name);
15601 let tgt_alias = self.fresh_alias();
15602
15603 if steps.len() == 2
15605 && let Some(ast::PathStep::Name(n)) = steps.get(1)
15606 && n == "id"
15607 {
15608 let id_filter = IrExpr::BinOp(Box::new(IrBinOp {
15612 left: IrExpr::ColumnRef {
15613 alias: tgt_alias.clone(),
15614 column: "id".to_string(),
15615 pg_type: "uuid".to_string(),
15616 },
15617 op: ast::BinOpKind::Eq,
15618 right: IrExpr::ColumnRef {
15619 alias: jt_alias.to_string(),
15620 column: jt_tgt_col.to_string(),
15621 pg_type: "uuid".to_string(),
15622 },
15623 }));
15624 let fk_ref = IrExpr::ColumnRef {
15625 alias: tgt_alias.clone(),
15626 column: fk_col,
15627 pg_type: "uuid".to_string(),
15628 };
15629 let (fl, fr) = if flip {
15630 (value_expr, fk_ref)
15631 } else {
15632 (fk_ref, value_expr)
15633 };
15634 let fk_filter = IrExpr::BinOp(Box::new(IrBinOp {
15635 left: fl,
15636 op,
15637 right: fr,
15638 }));
15639 let full = IrExpr::BinOp(Box::new(IrBinOp {
15640 left: id_filter,
15641 op: ast::BinOpKind::And,
15642 right: fk_filter,
15643 }));
15644 let inner = IrExpr::Subquery(Box::new(IrSelect::schema_bound(
15645 IrSource {
15646 poly: None,
15647 type_name: target_type.to_string(),
15648 table: target_table,
15649 alias: tgt_alias,
15650 },
15651 vec![],
15652 Some(full),
15653 )));
15654 return Ok(IrExpr::UnaryOp(Box::new(IrUnaryOp {
15655 op: ast::UnaryOpKind::Exists,
15656 operand: inner,
15657 })));
15658 }
15659
15660 let id_filter = IrExpr::BinOp(Box::new(IrBinOp {
15663 left: IrExpr::ColumnRef {
15664 alias: tgt_alias.clone(),
15665 column: "id".to_string(),
15666 pg_type: "uuid".to_string(),
15667 },
15668 op: ast::BinOpKind::Eq,
15669 right: IrExpr::ColumnRef {
15670 alias: jt_alias.to_string(),
15671 column: jt_tgt_col.to_string(),
15672 pg_type: "uuid".to_string(),
15673 },
15674 }));
15675 let nested_filter =
15676 self.compile_path_tail_filter(&steps[1..], op, value_expr, flip, &next_target, &tgt_alias, &fk_col)?;
15677 let full = IrExpr::BinOp(Box::new(IrBinOp {
15678 left: id_filter,
15679 op: ast::BinOpKind::And,
15680 right: nested_filter,
15681 }));
15682 let inner = IrExpr::Subquery(Box::new(IrSelect::schema_bound(
15683 IrSource {
15684 poly: None,
15685 type_name: target_type.to_string(),
15686 table: target_table,
15687 alias: tgt_alias,
15688 },
15689 vec![],
15690 Some(full),
15691 )));
15692 Ok(IrExpr::UnaryOp(Box::new(IrUnaryOp {
15693 op: ast::UnaryOpKind::Exists,
15694 operand: inner,
15695 })))
15696 }
15697}
15698
15699#[derive(Clone)]
15703struct JunctionReadOverride {
15704 junction: String,
15705 targets: Option<String>,
15706}
15707
15708struct ConflictElse {
15712 sets: Vec<(String, IrExpr)>,
15713 predicate: Option<IrExpr>,
15714}
15715
15716impl<'a> Compiler<'a> {
15717 fn compile_conflict(
15719 &mut self,
15720 uc: &ast::UnlessConflict,
15721 td: &TypeDescriptor,
15722 ) -> Result<(IrConflict, Vec<IrMultiLinkMutation>), PyQLError> {
15723 let on = uc.on.as_ref().map(|e| self.compile_expr(e, td, "")).transpose()?;
15726 let mut appends = vec![];
15727 let mut do_update_where = None;
15728 let do_update = match uc.else_.as_ref() {
15729 Some(e) => {
15730 let resolved = self.compile_conflict_else(e, &mut appends)?;
15731 do_update_where = resolved.predicate;
15732 Some(resolved.sets)
15733 }
15734 None => None,
15735 };
15736 Ok((
15737 IrConflict {
15738 on,
15739 do_update,
15740 do_update_where,
15741 },
15742 appends,
15743 ))
15744 }
15745
15746 fn compile_conflict_else(
15761 &mut self,
15762 expr: &Expr,
15763 appends: &mut Vec<IrMultiLinkMutation>,
15764 ) -> Result<ConflictElse, PyQLError> {
15765 let Expr::SubQuery(stmt) = expr else {
15766 return Err(self.type_err("UNLESS CONFLICT ELSE must be an UPDATE expression, e.g. ELSE (UPDATE …)"));
15767 };
15768 if let Stmt::Select(sel) = stmt.as_ref()
15773 && sel.filter.is_none()
15774 && sel.limit.is_none()
15775 && sel.offset.is_none()
15776 && let Ok(type_name) = self.expr_as_type_name(&sel.result)
15777 && let Ok(sel_td) = self.resolve_type(&type_name)
15778 {
15779 let table = sel_td.table.clone();
15780 let pk = sel_td
15781 .properties
15782 .iter()
15783 .find(|p| p.is_pk)
15784 .ok_or_else(|| self.type_err(&format!("type '{type_name}' has no primary key to read back")))?;
15785 return Ok(ConflictElse {
15786 sets: vec![(
15787 pk.name.clone(),
15788 IrExpr::ColumnRef {
15789 alias: table,
15790 column: pk.name.clone(),
15791 pg_type: pk.pg_type.clone(),
15792 },
15793 )],
15794 predicate: None,
15795 });
15796 }
15797 let Stmt::Update(upd) = stmt.as_ref() else {
15798 return Err(self.type_err(
15799 "UNLESS CONFLICT ELSE must be an UPDATE that changes the conflicting row, or a \
15800 SELECT of its type to read it back unchanged",
15801 ));
15802 };
15803 let type_name = self.expr_as_type_name(&upd.subject)?;
15804 let upd_td = self.resolve_type(&type_name)?;
15805 let table = upd_td.table.clone();
15806 let do_update_where = upd
15814 .filter
15815 .as_ref()
15816 .map(|f| self.compile_expr(f, upd_td, &table))
15817 .transpose()?;
15818 let mut scalar_shape = vec![];
15825 for el in &upd.shape {
15826 let pointer_name = path_leaf(&el.path)?;
15827 let Some(ml) = Self::resolve_multilink(upd_td, pointer_name) else {
15828 scalar_shape.push(el.clone());
15829 continue;
15830 };
15831 if el.op == ShapeOp::Remove {
15832 return Err(self.type_err(&format!(
15833 "cannot use `-=` for multi-link '{pointer_name}' inside an UNLESS CONFLICT \
15834 ELSE clause; the rows to remove are not known until the conflict resolves"
15835 )));
15836 }
15837 let Some(value) = &el.compexpr else { continue };
15838 let (jt, module, src_col, tgt_col, through_td) = self.own_multilink_junction_info(upd_td, ml)?;
15839 let values = self.compile_multilink_values(value, upd_td, &table, through_td)?;
15840 appends.push(IrMultiLinkMutation {
15841 junction_table: jt,
15842 module,
15843 source_col: src_col,
15844 target_col: tgt_col,
15845 values,
15846 single: false,
15847 });
15848 }
15849 let sets = self.compile_assignments_for_update(&scalar_shape, upd_td, &table)?;
15850 Ok(ConflictElse {
15851 sets,
15852 predicate: do_update_where,
15853 })
15854 }
15855
15856 fn hoist_dml_as_cte(&mut self, stmt: &Stmt) -> Result<(String, String), PyQLError> {
15866 let type_name = self.dml_subject_type(stmt)?;
15867 let before = std::mem::take(&mut self.for_vars_read);
15868 let inner = self.compile_stmt(stmt);
15869 let read = std::mem::replace(&mut self.for_vars_read, before);
15870 let correlated_to = self
15871 .for_scope
15872 .iter()
15873 .rev()
15874 .find(|slot| read.contains(*slot))
15875 .map(|slot| format!("_for_{slot}"));
15876 self.for_vars_read.extend(read);
15877 let cte_name = self.fresh_nested_cte_name();
15878 self.hoisted_ctes.push(IrCteDef {
15879 name: cte_name.clone(),
15880 stmt: inner?,
15881 type_name: type_name.clone(),
15882 correlated_to,
15883 });
15884 Ok((cte_name, type_name))
15885 }
15886
15887 fn hoist_nested_dml(&mut self, stmt: &Stmt) -> Result<String, PyQLError> {
15888 let type_name = self.dml_subject_type(stmt)?;
15889 let inner = self.compile_stmt(stmt)?;
15890 let cte_name = self.fresh_nested_cte_name();
15891 self.pending_nested_ctes.push(IrCteDef {
15892 name: cte_name.clone(),
15893 stmt: inner,
15894 type_name,
15895 correlated_to: None,
15896 });
15897 Ok(cte_name)
15898 }
15899
15900 fn compile_link_subquery(&mut self, stmt: &Stmt) -> Result<IrExpr, PyQLError> {
15905 if matches!(stmt, Stmt::Insert(_) | Stmt::Update(_) | Stmt::Delete(_)) {
15909 let cte_name = self.hoist_nested_dml(stmt)?;
15910 return Ok(IrExpr::ColumnRef {
15911 alias: cte_name,
15912 column: "id".to_string(),
15913 pg_type: "uuid".to_string(),
15914 });
15915 }
15916 let Stmt::Select(sel) = stmt else {
15917 return Err(self.type_err(
15918 "only SELECT is valid as a link assignment value; \
15919 use SELECT (INSERT …) { id } to assign from a DML result",
15920 ));
15921 };
15922
15923 let nested_dml = match &sel.result {
15945 Expr::SubQuery(inner) if matches!(inner.as_ref(), Stmt::Insert(_) | Stmt::Update(_) | Stmt::Delete(_)) => {
15946 Some(inner.as_ref())
15947 }
15948 Expr::Shape(s) => match s.expr.as_ref() {
15949 Some(Expr::SubQuery(inner))
15950 if matches!(inner.as_ref(), Stmt::Insert(_) | Stmt::Update(_) | Stmt::Delete(_)) =>
15951 {
15952 Some(inner.as_ref())
15953 }
15954 _ => None,
15955 },
15956 _ => None,
15957 };
15958 if let Some(inner_stmt) = nested_dml {
15959 let cte_name = self.hoist_nested_dml(inner_stmt)?;
15960 return Ok(IrExpr::ColumnRef {
15961 alias: cte_name,
15962 column: "id".to_string(),
15963 pg_type: "uuid".to_string(),
15964 });
15965 }
15966
15967 let type_name = self.expr_as_type_name(&sel.result)?;
15968 let td = self.resolve_type(&type_name)?;
15969 let alias = self.fresh_alias();
15970
15971 let (filter, order_by, offset, limit) = self.compile_path_modifiers(sel, td, &alias)?;
15974 let mut select = IrSelect::schema_bound(
15975 IrSource {
15976 poly: None,
15977 type_name: format!("{}::{}", td.module, td.name),
15978 table: td.table.clone(),
15979 alias,
15980 },
15981 Self::pk_returning(td),
15982 filter,
15983 );
15984 select.order_by = order_by;
15985 select.offset = offset;
15986 select.limit = limit;
15987 Ok(IrExpr::Subquery(Box::new(select)))
15988 }
15989
15990 fn compile_sort_ctx(
15994 &mut self,
15995 s: &ast::SortExpr,
15996 ctx: Option<(&TypeDescriptor, &str)>,
15997 ) -> Result<IrSort, PyQLError> {
15998 Ok(IrSort {
15999 expr: self.compile_expr_ctx(&s.expr, ctx)?,
16000 direction: match s.direction {
16001 SortDirection::Asc => IrSortDir::Asc,
16002 SortDirection::Desc => IrSortDir::Desc,
16003 },
16004 nulls: match s.nones {
16005 NonesOrder::First => IrNulls::First,
16006 NonesOrder::Last => IrNulls::Last,
16007 },
16008 })
16009 }
16010
16011 fn compile_sort(&mut self, s: &ast::SortExpr, td: &TypeDescriptor, alias: &str) -> Result<IrSort, PyQLError> {
16012 self.compile_sort_ctx(s, Some((td, alias)))
16013 }
16014
16015 fn assert_message(
16023 &mut self,
16024 f: &ast::FunctionCall,
16025 ctx: Option<(&TypeDescriptor, &str)>,
16026 ) -> Result<Option<IrExpr>, PyQLError> {
16027 let mut message = None;
16028 for (name, value) in &f.kwargs {
16029 if name != "message" {
16030 return Err(self.type_err(&format!("function 'std::{}' has no parameter '{name}'", f.name)));
16031 }
16032 message = Some(self.compile_expr_ctx(value, ctx)?);
16033 }
16034 Ok(message)
16035 }
16036
16037 fn compile_named_call_args(
16043 &mut self,
16044 f: &ast::FunctionCall,
16045 ctx: Option<(&TypeDescriptor, &str)>,
16046 ) -> Result<Option<Vec<IrExpr>>, PyQLError> {
16047 use crate::stdlib::NamedDefault;
16048
16049 let ns = f.module.as_deref().unwrap_or("std");
16050 let overloads = crate::stdlib::lookup(ns, &f.name);
16051 let positional = |d: &crate::stdlib::FnDescriptor| d.params.iter().filter(|p| p.named_only.is_none()).count();
16052 let named = |d: &crate::stdlib::FnDescriptor| d.params.iter().filter(|p| p.named_only.is_some()).count();
16053 let takes_positionally = |d: &crate::stdlib::FnDescriptor| {
16056 if d.is_variadic() {
16057 f.args.len() + 1 >= positional(d)
16058 } else {
16059 positional(d) == f.args.len()
16060 }
16061 };
16062 if f.kwargs.is_empty() {
16063 if overloads.iter().any(|d| takes_positionally(d) && named(d) == 0) {
16064 return Ok(None);
16065 }
16066 let wrote_a_named_one_positionally =
16071 |d: &crate::stdlib::FnDescriptor| positional(d) < f.args.len() && f.args.len() <= d.params.len();
16072 if !overloads.iter().any(|d| takes_positionally(d))
16073 && let Some(param) = overloads
16074 .iter()
16075 .filter(|d| wrote_a_named_one_positionally(d))
16076 .flat_map(|d| d.params.iter())
16077 .find(|p| p.named_only.is_some())
16078 .map(|p| p.keyword())
16079 {
16080 return Err(self.type_err(&format!(
16081 "function '{ns}::{}' takes '{param}' as a named argument only — e.g. '{param} := …'",
16082 f.name
16083 )));
16084 }
16085 }
16086 let fits = |d: &&&crate::stdlib::FnDescriptor| {
16087 takes_positionally(d)
16088 && named(d) > 0
16089 && f.kwargs
16090 .iter()
16091 .all(|(name, _)| d.params.iter().any(|p| p.keyword() == name && p.named_only.is_some()))
16092 };
16093 let Some(desc) = overloads.iter().find(fits) else {
16094 if let Some((name, _)) = f.kwargs.first() {
16095 let message = if overloads.iter().all(|d| named(d) == 0) {
16096 format!(
16097 "function '{ns}::{}' does not take named arguments, got '{name}'",
16098 f.name
16099 )
16100 } else {
16101 format!("function '{ns}::{}' has no parameter '{name}'", f.name)
16102 };
16103 return Err(self.type_err(&message));
16104 }
16105 return Ok(None);
16106 };
16107 let mut args = f
16108 .args
16109 .iter()
16110 .map(|a| self.compile_expr_ctx(a, ctx))
16111 .collect::<Result<Vec<_>, _>>()?;
16112 for param in desc.params.iter().filter(|p| p.named_only.is_some()) {
16113 let arg = match (
16114 f.kwargs.iter().find(|(name, _)| name == param.keyword()),
16115 param.named_only,
16116 ) {
16117 (Some((_, value)), _) => self.compile_expr_ctx(value, ctx)?,
16118 (None, Some(NamedDefault::Required)) => {
16119 let keyword = param.keyword();
16120 return Err(self.type_err(&format!(
16121 "function '{ns}::{}' requires the named argument '{keyword}' — e.g. '{keyword} := …'",
16122 f.name
16123 )));
16124 }
16125 (None, Some(NamedDefault::Int(n))) => IrExpr::Literal(IrLiteral::Int(n)),
16126 (None, Some(NamedDefault::Bool(b))) => IrExpr::Literal(IrLiteral::Bool(b)),
16127 (None, Some(NamedDefault::Str(s))) => IrExpr::Literal(IrLiteral::Str(s.to_string())),
16128 (None, _) => IrExpr::Null,
16129 };
16130 args.push(arg);
16131 }
16132 Ok(Some(args))
16133 }
16134
16135 fn resolve_fn_call(&mut self, module: Option<&str>, name: &str, args: Vec<IrExpr>) -> Result<IrExpr, PyQLError> {
16136 use crate::stdlib::{ImplStrategy, lookup};
16137
16138 let ns = module.unwrap_or("std");
16139 if ns == "std" && matches!(name, "any" | "all") && args.len() == 1 && !is_array_expr(&args[0]) {
16144 return Ok(args.into_iter().next().expect("checked by the guard"));
16145 }
16146 let overloads = lookup(ns, name);
16147
16148 let by_arity: Vec<&crate::stdlib::FnDescriptor> = overloads
16156 .iter()
16157 .copied()
16158 .filter(|d| {
16159 if d.is_variadic() {
16160 args.len() + 1 >= d.params.len()
16161 } else {
16162 d.params.len() == args.len()
16163 }
16164 })
16165 .collect();
16166
16167 let exact = by_arity.iter().copied().find(|d| {
16173 params_for_args(d, args.len())
16174 .zip(&args)
16175 .any(|(p, a)| p.ty.scalar_pg_type().is_some() && infer_ir_type(a).is_some())
16176 && params_for_args(d, args.len()).zip(&args).all(|(p, a)| {
16177 match (p.ty.scalar_pg_type(), infer_ir_type(a)) {
16178 (Some(declared), Some(known)) => declared == known,
16179 _ => pylon_type_matches(a, &p.ty),
16180 }
16181 })
16182 });
16183 let best = exact.or_else(|| {
16184 by_arity.iter().copied().find(|d| {
16185 params_for_args(d, args.len())
16186 .zip(&args)
16187 .all(|(p, a)| pylon_type_matches(a, &p.ty))
16188 })
16189 });
16190
16191 let args_len = args.len();
16196 let args: Vec<IrExpr> = match best {
16197 Some(descriptor) => args
16198 .into_iter()
16199 .enumerate()
16200 .map(
16201 |(i, arg)| match params_for_args(descriptor, args_len).nth(i).map(|p| &p.ty) {
16202 Some(crate::stdlib::PylonType::Set(_)) | Some(crate::stdlib::PylonType::Array(_)) | None => arg,
16203 Some(_) => set_walk_as_scalar(arg),
16204 },
16205 )
16206 .collect(),
16207 None => args,
16208 };
16209 let args = match best {
16210 Some(descriptor) => pack_variadic_args(descriptor, args),
16211 None => args,
16212 };
16213
16214 let stdlib_return = best
16218 .map(|d| match &d.return_type {
16219 crate::stdlib::PylonType::Optional(inner) => inner.as_ref(),
16220 other => other,
16221 })
16222 .and_then(|ty| ty.scalar_pg_type())
16223 .map(str::to_string);
16224 let (schema, resolved_name, sql_template) = if let Some(desc) = best {
16225 match &desc.impl_strategy {
16226 ImplStrategy::SqlBuiltin(sql_name) => (None, sql_name.to_string(), None),
16227 ImplStrategy::SqlExpression(tmpl) => (None, name.to_string(), Some(tmpl.to_string())),
16228 ImplStrategy::PylonFunction(def) => (Some("_pylon".to_string()), def.name.to_string(), None),
16229 ImplStrategy::SqlOperator(op) if args.len() == 2 => {
16236 (None, name.to_string(), Some(format!("($1 {op} $2)")))
16237 }
16238 ImplStrategy::TranspilerIntrinsic(intrinsic) => {
16239 return self.compile_range_intrinsic(intrinsic, name, args);
16240 }
16241 _ => (module.map(str::to_string), name.to_string(), None),
16243 }
16244 } else {
16245 let candidates: Vec<&FunctionDescriptor> = self
16261 .schema
16262 .functions
16263 .iter()
16264 .filter(|f| {
16265 let module_matches = module.map(|m| m == f.module.as_str()).unwrap_or(true);
16266 module_matches && f.name == name && !f.return_is_object
16267 })
16268 .collect();
16269 let user_fn = candidates
16270 .iter()
16271 .find(|f| f.params.len() == args.len())
16272 .copied()
16273 .or_else(|| candidates.first().copied());
16274 if let Some(fd) = user_fn {
16275 if fd.params.len() != args.len() {
16276 return Err(self.type_err(&format!(
16277 "function '{}::{}' expects {} argument(s), got {}",
16278 fd.module,
16279 fd.name,
16280 fd.params.len(),
16281 args.len()
16282 )));
16283 }
16284 let cast_args = fd
16285 .params
16286 .iter()
16287 .zip(args)
16288 .map(|(p, a)| {
16289 IrExpr::TypeCast(Box::new(super::IrTypeCast {
16290 expr: a,
16291 pg_type: p.pg_type.clone(),
16292 tuple_shape: None,
16293 }))
16294 })
16295 .collect();
16296 let qualified = format!("{}::{}", fd.module, fd.name);
16297 let fn_module = fd.module.clone();
16298 let fn_name = fd.name.clone();
16299 let mut call_args: Vec<IrExpr> = cast_args;
16300 if let Some(globals) = self.globals_arg_for_call(&qualified)? {
16301 call_args.insert(0, globals);
16302 }
16303 let return_pg_type = (!fd.return_is_set).then(|| fd.return_pg_type.clone());
16308 return Ok(IrExpr::FunctionCall(super::IrFunctionCall {
16309 return_pg_type,
16310 schema: Some(fn_module),
16311 name: fn_name,
16312 args: call_args,
16313 sql_template: None,
16314 }));
16315 }
16316 if let Some(fd) = self.schema.functions.iter().find(|f| {
16323 let module_matches = module.map(|m| m == f.module.as_str()).unwrap_or(true);
16324 module_matches && f.name == name && f.return_is_object
16325 }) {
16326 return Err(self.type_err(&format!(
16327 "function '{}::{}' returns objects, so it can only be the subject of a \
16328 select (`select {}::{}(…) {{ … }}`), not part of a larger expression",
16329 fd.module, fd.name, fd.module, fd.name
16330 )));
16331 }
16332 let qualified = match module {
16333 Some(m) => format!("{m}::{name}"),
16334 None => name.to_string(),
16335 };
16336 if !overloads.is_empty() {
16341 return Err(self.type_err(&if by_arity.is_empty() {
16342 format!(
16343 "function '{qualified}' takes {}, got {}",
16344 describe_arities(&overloads),
16345 args.len()
16346 )
16347 } else {
16348 format!(
16349 "function '{qualified}' has no overload accepting ({}) — it accepts {}",
16350 describe_args(&args),
16351 describe_signatures(&by_arity),
16352 )
16353 }));
16354 }
16355 return Err(self.type_err(&format!(
16356 "function '{qualified}' does not exist{}",
16357 self.suggest_function_name(ns, name)
16358 )));
16359 };
16360
16361 Ok(IrExpr::FunctionCall(super::IrFunctionCall {
16362 return_pg_type: stdlib_return,
16363 schema,
16364 name: resolved_name,
16365 args,
16366 sql_template,
16367 }))
16368 }
16369
16370 fn compile_range_intrinsic(&self, intrinsic: &str, name: &str, args: Vec<IrExpr>) -> Result<IrExpr, PyQLError> {
16379 match intrinsic {
16380 "range" => {
16381 let bounds = args.len().saturating_sub(3);
16384 let point_ty = args[..bounds].iter().find_map(infer_ir_type).ok_or_else(|| {
16385 self.type_err(
16386 "range(): cannot infer the element type from either bound — \
16387 use an explicit cast, e.g. range(<int64>$lower, <int64>$upper)",
16388 )
16389 })?;
16390 let ctor = range_ctor_for_pg_type(point_ty).ok_or_else(|| {
16391 self.type_err(&format!(
16392 "range(): unsupported element type '{point_ty}' — PostgreSQL only has native \
16393 ranges over int64, decimal, datetime, cal::local_datetime, and cal::local_date"
16394 ))
16395 })?;
16396 let upper = match args.len() {
16400 4 => "NULL",
16401 5 => "$2",
16402 n => return Err(self.type_err(&format!("range(): unexpected argument count {n}"))),
16403 };
16404 let flags: Vec<Option<bool>> = args[args.len() - 3..].iter().map(bool_literal).collect();
16405 let sql_template = match flags[..] {
16409 [Some(_), Some(_), Some(true)] => format!("'empty'::{ctor}"),
16410 [Some(true), Some(false), Some(false)] => format!("{ctor}($1, {upper})"),
16411 [Some(inc_lower), Some(inc_upper), Some(false)] => format!(
16412 "{ctor}($1, {upper}, '{}{}')",
16413 if inc_lower { '[' } else { '(' },
16414 if inc_upper { ']' } else { ')' },
16415 ),
16416 _ => {
16417 let (lower_inc, upper_inc, empty) = match args.len() {
16418 4 => ("$2", "$3", "$4"),
16419 _ => ("$3", "$4", "$5"),
16420 };
16421 format!(
16422 "(CASE WHEN {empty} THEN 'empty'::{ctor} ELSE {ctor}($1, {upper}, \
16423 (CASE WHEN {lower_inc} THEN '[' ELSE '(' END) \
16424 || (CASE WHEN {upper_inc} THEN ']' ELSE ')' END)) END)"
16425 )
16426 }
16427 };
16428 Ok(IrExpr::FunctionCall(super::IrFunctionCall {
16439 return_pg_type: Some(ctor.to_string()),
16440 schema: None,
16441 name: ctor.to_string(),
16442 args,
16443 sql_template: Some(sql_template),
16444 }))
16445 }
16446 "multirange" => {
16447 let Some(IrExpr::Array(elems)) = args.first() else {
16448 return Err(self.type_err("multirange(): argument must be an array literal of ranges"));
16449 };
16450 let first_ctor = elems
16451 .first()
16452 .and_then(|e| match e {
16453 IrExpr::FunctionCall(fc) => Some(fc.name.as_str()),
16454 _ => None,
16455 })
16456 .ok_or_else(|| {
16457 self.type_err(
16458 "multirange(): cannot infer the element range type from an empty or non-range \
16459 array — pass at least one range(...) call, e.g. multirange([range(1, 3)])",
16460 )
16461 })?;
16462 let ctor = multirange_ctor_for_range_ctor(first_ctor).ok_or_else(|| {
16463 self.type_err(&format!("multirange(): unrecognized range constructor '{first_ctor}'"))
16464 })?;
16465 Ok(IrExpr::FunctionCall(super::IrFunctionCall {
16469 return_pg_type: Some(ctor.to_string()),
16470 schema: None,
16471 name: name.to_string(),
16472 args,
16473 sql_template: Some(format!("{ctor}(VARIADIC $1)")),
16474 }))
16475 }
16476 other => Err(self.type_err(&format!("internal error: unhandled TranspilerIntrinsic '{other}'"))),
16477 }
16478 }
16479
16480 fn compile_sequence_fn(&mut self, fc: &ast::FunctionCall) -> Result<IrExpr, PyQLError> {
16485 let (module, scalar_name) = self.resolve_sequence_scalar_arg(fc)?;
16486
16487 if fc.name == "sequence_next" {
16488 if fc.args.len() != 1 {
16489 return Err(self.type_err("sequence_next takes exactly 1 argument"));
16490 }
16491 let sql = format!("nextval('\"{}\".\"{}_seq\"')", module, scalar_name);
16492 return Ok(IrExpr::FunctionCall(super::IrFunctionCall {
16493 return_pg_type: None,
16494 schema: None,
16495 name: "nextval".into(),
16496 args: vec![],
16497 sql_template: Some(sql),
16498 }));
16499 }
16500
16501 match fc.args.len() {
16503 1 => {
16504 let sql = format!("setval('\"{}\".\"{}_seq\"', 1, false)", module, scalar_name);
16505 Ok(IrExpr::FunctionCall(super::IrFunctionCall {
16506 return_pg_type: None,
16507 schema: None,
16508 name: "setval".into(),
16509 args: vec![],
16510 sql_template: Some(sql),
16511 }))
16512 }
16513 2 => {
16514 let val = self.compile_free_expr(&fc.args[1])?;
16515 let sql = format!("setval('\"{}\".\"{}_seq\"', $1, true)", module, scalar_name);
16516 Ok(IrExpr::FunctionCall(super::IrFunctionCall {
16517 return_pg_type: None,
16518 schema: None,
16519 name: "setval".into(),
16520 args: vec![val],
16521 sql_template: Some(sql),
16522 }))
16523 }
16524 _ => Err(self.type_err("sequence_reset takes 1 or 2 arguments")),
16525 }
16526 }
16527
16528 fn resolve_sequence_scalar_arg(&self, fc: &ast::FunctionCall) -> Result<(String, String), PyQLError> {
16531 use crate::parse::ast::{Expr, Path, PathStep};
16532
16533 let arg = fc.args.first().ok_or_else(|| {
16534 self.type_err(&format!(
16535 "{}() requires a sequence scalar type as its first argument",
16536 fc.name
16537 ))
16538 })?;
16539
16540 let (arg_module, arg_name): (Option<&str>, &str) = match arg {
16543 Expr::Path(Path { steps, partial: false }) => match steps.as_slice() {
16544 [PathStep::Name(s)] => {
16545 if let Some((m, n)) = s.split_once("::") {
16546 (Some(m), n)
16547 } else {
16548 (None, s.as_str())
16549 }
16550 }
16551 _ => return Err(self.type_err(&format!(
16552 "{}(): first argument must be a sequence scalar type name (e.g. OrderNumber or default::OrderNumber)",
16553 fc.name
16554 ))),
16555 },
16556 _ => return Err(self.type_err(&format!(
16557 "{}(): first argument must be a sequence scalar type name (e.g. OrderNumber or default::OrderNumber)",
16558 fc.name
16559 ))),
16560 };
16561
16562 let scalar = self.schema.scalars.iter().find(|s| {
16563 s.is_sequence && s.name == arg_name && arg_module.map(|m| m == s.module.as_str()).unwrap_or(true)
16564 });
16565
16566 match scalar {
16567 Some(s) => Ok((s.module.clone(), s.name.clone())),
16568 None => Err(self.type_err(&format!(
16569 "{}(): '{}' is not a known sequence scalar type",
16570 fc.name, arg_name
16571 ))),
16572 }
16573 }
16574
16575 const NOTIFY_PAYLOAD_MAX_BYTES: usize = 8000;
16584
16585 fn notify_type_payload_err(&self, channel: &str, qname: &str) -> PyQLError {
16587 self.type_err(&format!(
16588 "notify(): payload for Channel '{channel}' (a '{qname}' object channel) must name an object of that \
16589 type — either a with-block binding, or __new__/__old__ inside a trigger handler"
16590 ))
16591 }
16592
16593 fn compile_notify(
16603 &mut self,
16604 fc: &ast::FunctionCall,
16605 ctx: Option<(&TypeDescriptor, &str)>,
16606 ) -> Result<IrExpr, PyQLError> {
16607 use crate::parse::ast::{Expr, Path, PathStep};
16608
16609 if fc.args.len() != 2 {
16610 return Err(self.type_err("notify() takes exactly 2 arguments: (Channel, payload)"));
16611 }
16612
16613 let (channel_module, channel_name): (Option<&str>, &str) = match &fc.args[0] {
16614 Expr::Path(Path { steps, partial: false }) => match steps.as_slice() {
16615 [PathStep::Name(s)] => {
16616 if let Some((m, n)) = s.split_once("::") {
16617 (Some(m), n)
16618 } else {
16619 (None, s.as_str())
16620 }
16621 }
16622 _ => {
16623 return Err(self.type_err(
16624 "notify(): first argument must be a Channel name (e.g. OrderEvents or orders::OrderEvents)",
16625 ));
16626 }
16627 },
16628 _ => {
16629 return Err(self.type_err(
16630 "notify(): first argument must be a Channel name (e.g. OrderEvents or orders::OrderEvents)",
16631 ));
16632 }
16633 };
16634 let full_channel_name = match channel_module {
16635 Some(m) => format!("{m}::{channel_name}"),
16636 None => channel_name.to_string(),
16637 };
16638 let channel = self
16639 .resolve_channel(&full_channel_name)
16640 .ok_or_else(|| self.type_err(&format!("notify(): '{channel_name}' is not a known Channel")))?;
16641 let wire_name = channel.wire_name.clone();
16642 let payload_arg = &fc.args[1];
16643
16644 let anchor_fallback: Option<(&'a TypeDescriptor, String)> = self.special_anchors.values().next().cloned();
16658 let ctx = ctx.or_else(|| anchor_fallback.as_ref().map(|(td, alias)| (*td, alias.as_str())));
16659
16660 let payload_ir = match &channel.payload {
16661 crate::schema::ChannelPayload::Type(qname) => {
16662 let Expr::Path(Path { steps, partial: false }) = payload_arg else {
16671 return Err(self.notify_type_payload_err(&full_channel_name, qname));
16672 };
16673 let [PathStep::Name(name)] = steps.as_slice() else {
16674 return Err(self.notify_type_payload_err(&full_channel_name, qname));
16675 };
16676
16677 if name == "__new__" || name == "__old__" {
16678 let (anchor_td, alias) = self.special_anchors.get(name.as_str()).cloned().ok_or_else(|| {
16679 self.type_err(&format!(
16680 "notify(): '{name}' cannot be used here — it's only bound inside a trigger handler"
16681 ))
16682 })?;
16683 let anchor_qname = format!("{}::{}", anchor_td.module, anchor_td.name);
16684 if &anchor_qname != qname {
16685 return Err(self.type_err(&format!(
16686 "notify(): Channel '{full_channel_name}' expects a payload of type '{qname}', got '{anchor_qname}'"
16687 )));
16688 }
16689 IrExpr::ColumnRef {
16690 alias,
16691 column: "id".to_string(),
16692 pg_type: "uuid".to_string(),
16693 }
16694 } else if let Some(cte_type) = self.cte_types.get(name.as_str()).cloned() {
16695 if &cte_type != qname {
16696 return Err(self.type_err(&format!(
16697 "notify(): Channel '{full_channel_name}' expects a payload of type '{qname}', got '{cte_type}'"
16698 )));
16699 }
16700 IrExpr::CteRef {
16703 name: name.clone(),
16704 scalar: false,
16705 pg_type: None,
16706 }
16707 } else {
16708 return Err(self.notify_type_payload_err(&full_channel_name, qname));
16709 }
16710 }
16711 crate::schema::ChannelPayload::Scalar(pg_type) => {
16712 let ir = self.compile_expr_ctx(payload_arg, ctx)?;
16713 if let Some(actual) = infer_ir_type(&ir)
16714 && !types_compatible(actual, pg_type)
16715 {
16716 return Err(self.type_err(&format!(
16717 "notify(): Channel '{full_channel_name}' expects a payload of type '{expected}', got '{actual_pyql}'",
16718 expected = pg_type_to_pyql(pg_type),
16719 actual_pyql = pg_type_to_pyql(actual),
16720 )));
16721 }
16722 ir
16723 }
16724 crate::schema::ChannelPayload::Object(declared_fields) => {
16725 let Expr::Shape(sh) = payload_arg else {
16726 return Err(self.type_err(&format!(
16727 "notify(): payload for Channel '{full_channel_name}' (an Object channel) must be a free \
16728 object literal, e.g. {{ {} }}",
16729 declared_fields
16730 .iter()
16731 .map(|(n, _)| format!("{n} := .."))
16732 .collect::<Vec<_>>()
16733 .join(", ")
16734 )));
16735 };
16736 if sh.expr.is_some() {
16737 return Err(self.type_err(&format!(
16738 "notify(): payload for Channel '{full_channel_name}' (an Object channel) must be a free \
16739 object literal, not a shape over a type"
16740 )));
16741 }
16742 let ir = self.compile_expr_ctx(payload_arg, ctx)?;
16743 let IrExpr::NamedTuple {
16744 fields,
16745 is_free_object: true,
16746 } = &ir
16747 else {
16748 return Err(self.type_err(&format!(
16749 "notify(): payload for Channel '{full_channel_name}' (an Object channel) must be a free object literal"
16750 )));
16751 };
16752 let declared_names: std::collections::HashSet<&str> =
16753 declared_fields.iter().map(|(n, _)| n.as_str()).collect();
16754 let actual_names: std::collections::HashSet<&str> = fields.iter().map(|(n, _)| n.as_str()).collect();
16755 if declared_names != actual_names {
16756 let mut expected: Vec<&str> = declared_names.iter().copied().collect();
16757 expected.sort();
16758 let mut actual: Vec<&str> = actual_names.iter().copied().collect();
16759 actual.sort();
16760 return Err(self.type_err(&format!(
16761 "notify(): payload fields for Channel '{full_channel_name}' don't match — expected {{{}}}, got {{{}}}",
16762 expected.join(", "), actual.join(", ")
16763 )));
16764 }
16765 for (name, expr) in fields {
16766 let Some((_, declared_pg_type)) = declared_fields.iter().find(|(n, _)| n == name) else {
16767 continue;
16768 };
16769 if let Some(actual) = infer_ir_type(expr)
16770 && !types_compatible(actual, declared_pg_type)
16771 {
16772 return Err(self.type_err(&format!(
16773 "notify(): field '{name}' of Channel '{full_channel_name}' expects type '{expected}', got '{actual_pyql}'",
16774 expected = pg_type_to_pyql(declared_pg_type),
16775 actual_pyql = pg_type_to_pyql(actual),
16776 )));
16777 }
16778 }
16779 ir
16780 }
16781 };
16782
16783 let floor = static_min_payload_bytes(payload_arg);
16784 if floor >= Self::NOTIFY_PAYLOAD_MAX_BYTES {
16785 return Err(self.type_err(&format!(
16786 "notify(): this payload is at least {floor} bytes, which is at or over PostgreSQL's {}-byte NOTIFY \
16787 payload limit — the notification would fail at runtime, aborting the transaction that sent it",
16788 Self::NOTIFY_PAYLOAD_MAX_BYTES
16789 )));
16790 }
16791
16792 let sql = format!("pg_notify('{}', ($1)::text)", wire_name.replace('\'', "''"));
16793 Ok(IrExpr::FunctionCall(super::IrFunctionCall {
16794 return_pg_type: None,
16795 schema: None,
16796 name: "pg_notify".to_string(),
16797 args: vec![payload_ir],
16798 sql_template: Some(sql),
16799 }))
16800 }
16801
16802 fn compile_notify_raw(
16806 &mut self,
16807 fc: &ast::FunctionCall,
16808 ctx: Option<(&TypeDescriptor, &str)>,
16809 ) -> Result<IrExpr, PyQLError> {
16810 if fc.args.len() != 2 {
16811 return Err(self.type_err("notify_raw() takes exactly 2 arguments: (channel_name, payload)"));
16812 }
16813 let channel_ir = self.compile_expr_ctx(&fc.args[0], ctx)?;
16814 let payload_ir = self.compile_expr_ctx(&fc.args[1], ctx)?;
16815
16816 let floor = static_min_payload_bytes(&fc.args[1]);
16817 if floor >= Self::NOTIFY_PAYLOAD_MAX_BYTES {
16818 return Err(self.type_err(&format!(
16819 "notify_raw(): this payload is at least {floor} bytes, which is at or over PostgreSQL's {}-byte \
16820 NOTIFY payload limit",
16821 Self::NOTIFY_PAYLOAD_MAX_BYTES
16822 )));
16823 }
16824
16825 Ok(IrExpr::FunctionCall(super::IrFunctionCall {
16826 return_pg_type: None,
16827 schema: None,
16828 name: "pg_notify".to_string(),
16829 args: vec![channel_ir, payload_ir],
16830 sql_template: None,
16831 }))
16832 }
16833
16834 fn globals_arg_for_call(&mut self, qualified: &str) -> Result<Option<IrExpr>, PyQLError> {
16843 if self.fns_needing_globals.is_none() {
16844 self.fns_needing_globals = Some(functions_needing_globals(self.schema).clone());
16845 }
16846 if !self.fns_needing_globals.as_ref().is_some_and(|s| s.contains(qualified)) {
16847 return Ok(None);
16848 }
16849 if self.in_fn_body {
16850 self.used_globals_arg = true;
16851 return Ok(Some(IrExpr::RawSql(GLOBALS_ARG.to_string())));
16852 }
16853 let session_globals: Vec<String> = self
16854 .schema
16855 .globals
16856 .iter()
16857 .filter(|g| g.computed_expr.is_none())
16858 .map(|g| format!("{}::{}", g.module, g.name))
16859 .collect();
16860 let mut args = Vec::with_capacity(session_globals.len() * 2);
16861 for name in session_globals {
16862 let value = self.compile_global(&name)?;
16863 args.push(IrExpr::Literal(IrLiteral::Str(name)));
16864 args.push(value);
16865 }
16866 Ok(Some(IrExpr::FunctionCall(super::IrFunctionCall {
16867 return_pg_type: None,
16868 schema: None,
16869 name: "jsonb_build_object".to_string(),
16870 args,
16871 sql_template: None,
16872 })))
16873 }
16874
16875 fn try_compile_fn_object_select(
16881 &mut self,
16882 fc: &ast::FunctionCall,
16883 elements: &[ast::ShapeElement],
16884 s: &ast::SelectStmt,
16885 distinct: bool,
16886 ) -> Result<Option<IrFunctionSelect>, PyQLError> {
16887 use crate::schema::FunctionDescriptor;
16888
16889 let fd: Option<&FunctionDescriptor> = self.schema.functions.iter().find(|f| {
16890 let module_matches = fc.module.as_deref().map(|m| m == f.module.as_str()).unwrap_or(true);
16891 module_matches && f.name == fc.name && f.return_is_object
16892 });
16893 let fd = match fd {
16894 Some(f) => f,
16895 None => return Ok(None),
16896 };
16897 if fd.params.len() != fc.args.len() {
16898 return Err(self.type_err(&format!(
16899 "function '{}::{}' expects {} argument(s), got {}",
16900 fd.module,
16901 fd.name,
16902 fd.params.len(),
16903 fc.args.len()
16904 )));
16905 }
16906
16907 let fn_module = fd.module.clone();
16908 let fn_name = fd.name.clone();
16909 let return_type_name = fd.return_pg_type.clone(); let polymorphic = fd.return_is_polymorphic;
16911
16912 let mut fn_args = fc
16913 .args
16914 .iter()
16915 .map(|a| self.compile_free_expr(a))
16916 .collect::<Result<Vec<_>, _>>()?;
16917 let qualified = format!("{}::{}", fn_module, fn_name);
16918 if let Some(globals) = self.globals_arg_for_call(&qualified)? {
16919 fn_args.insert(0, globals);
16920 }
16921
16922 let alias = self.fresh_alias();
16923
16924 let td = self.resolve_type(&return_type_name)?;
16926 let td = td.clone();
16927
16928 let (poly_implementors, poly_columns) = if polymorphic {
16929 self.collect_poly_info(&return_type_name)
16930 } else {
16931 (vec![], vec![])
16932 };
16933
16934 let td_module = td.module.clone();
16935 let shape = self.compile_shape(elements, &td, &alias, &td_module)?;
16937 let outer_declared = self.active_declared_pointers.clone();
16941 self.active_declared_pointers
16942 .extend(elements.iter().filter(|el| el.compexpr.is_some()).cloned());
16943 let modifiers = self.compile_path_modifiers(s, &td, &alias);
16944 self.active_declared_pointers = outer_declared;
16945 let (filter, order_by, offset, limit) = modifiers?;
16946
16947 Ok(Some(IrFunctionSelect {
16948 fn_module,
16949 fn_name,
16950 fn_args,
16951 alias,
16952 type_name: return_type_name,
16953 polymorphic,
16954 poly_implementors,
16955 poly_columns,
16956 shape,
16957 filter,
16958 order_by,
16959 offset,
16960 limit,
16961 distinct,
16962 }))
16963 }
16964
16965 fn read_nested_links_from_their_ctes(dml: &IrStmt, dml_cte: Option<&str>, shape: &mut [IrShapePointer]) {
16982 let (assignments, nested_ctes, appends) = match dml {
16983 IrStmt::Insert(ins) => (&ins.assignments, &ins.nested_ctes, &ins.multi_link_appends),
16984 IrStmt::Update(upd) => (&upd.assignments, &upd.nested_ctes, &upd.multi_link_appends),
16985 _ => return,
16986 };
16987 if let Some(dml_cte) = dml_cte {
16993 for pointer in shape.iter_mut() {
16994 let IrShapePointer::MultiLink(link) = pointer else {
16995 continue;
16996 };
16997 let IrMultiLinkJoin::Standard { junction_table, .. } = &mut link.join else {
16998 continue;
16999 };
17000 let Some(index) = appends.iter().position(|a| a.junction_table == *junction_table) else {
17001 continue;
17002 };
17003 *junction_table = format!("@cte:{dml_cte}__ml_add_{index}");
17004 if let IrMultiLinkValueSource::CteRef(target_cte) = &appends[index].values.source {
17005 for row in &mut link.subquery.rows {
17006 if let IrRowSource::Bound { source, .. } = row {
17007 source.table = format!("@cte:{target_cte}");
17008 source.poly = None;
17009 }
17010 }
17011 }
17012 }
17013 }
17014 if nested_ctes.is_empty() {
17015 return;
17016 }
17017 let from_cte: HashMap<&str, &str> = assignments
17018 .iter()
17019 .filter_map(|(column, expr)| match expr {
17020 IrExpr::ColumnRef { alias, column: c, .. }
17021 if c == "id" && nested_ctes.iter().any(|cte| cte.name == *alias) =>
17022 {
17023 Some((column.as_str(), alias.as_str()))
17024 }
17025 _ => None,
17026 })
17027 .collect();
17028 if from_cte.is_empty() {
17029 return;
17030 }
17031 for pointer in shape {
17032 let IrShapePointer::SingleLink(link) = pointer else {
17033 continue;
17034 };
17035 let IrSingleLinkCorrelation::Fk { fk_column, .. } = &link.correlation else {
17036 continue;
17037 };
17038 let Some(cte_name) = from_cte.get(fk_column.as_str()) else {
17039 continue;
17040 };
17041 for row in &mut link.subquery.rows {
17042 if let IrRowSource::Bound { source, .. } = row {
17043 source.table = format!("@cte:{cte_name}");
17044 source.poly = None;
17045 }
17046 }
17047 }
17048 }
17049
17050 fn poly_dml_columns(td: &TypeDescriptor) -> Vec<String> {
17051 td.properties
17052 .iter()
17053 .map(|p| p.name.clone())
17054 .chain(
17055 td.links
17056 .iter()
17057 .filter(|l| !l.is_junction_backed())
17058 .map(|l| format!("{}_id", l.name)),
17059 )
17060 .collect()
17061 }
17062
17063 fn link_target_select(
17073 &self,
17074 target_td: &TypeDescriptor,
17075 mut source: IrSource,
17076 shape: Vec<IrShapePointer>,
17077 ) -> IrSelect {
17078 source.poly = self.link_target_fanout(target_td);
17079 IrSelect::schema_bound(source, shape, None)
17080 }
17081
17082 fn link_target_fanout(&self, target_td: &TypeDescriptor) -> Option<IrPolyFanout> {
17085 self.poly_fanout_for(&format!("{}::{}", target_td.module, target_td.name))
17086 }
17087
17088 fn resolve_join_fanouts(&self, path_select: &mut IrPathSelect) {
17095 for join in &mut path_select.joins {
17096 let target = match join {
17097 IrPathJoin::Single { target, .. }
17098 | IrPathJoin::Multi { target, .. }
17099 | IrPathJoin::BacklinkSingle { target, .. }
17100 | IrPathJoin::BacklinkMulti { target, .. }
17101 | IrPathJoin::Function { target, .. }
17102 | IrPathJoin::Lateral { target, .. } => target,
17103 };
17104 if target.poly.is_none() && !target.table.starts_with("@cte:") {
17105 target.poly = self.poly_fanout_for(&target.type_name.clone());
17106 }
17107 }
17108 }
17109
17110 fn poly_fanout_for(&self, type_name: &str) -> Option<IrPolyFanout> {
17113 let td = self
17114 .schema
17115 .types
17116 .iter()
17117 .find(|t| format!("{}::{}", t.module, t.name) == type_name)?;
17118 if !td.abstract_ && !self.has_subtypes(td) {
17122 return None;
17123 }
17124 let (implementors, columns) = self.collect_poly_info(type_name);
17125 Some(IrPolyFanout { implementors, columns })
17126 }
17127
17128 fn collect_poly_info(&self, type_name: &str) -> (Vec<IrPolyImplementor>, Vec<String>) {
17130 let implementors = self.find_poly_implementors(type_name);
17131 let columns = if let Some(td) = self
17132 .schema
17133 .types
17134 .iter()
17135 .find(|t| format!("{}::{}", t.module, t.name) == type_name)
17136 {
17137 Self::poly_dml_columns(td)
17142 } else {
17143 vec![]
17144 };
17145 (implementors, columns)
17146 }
17147
17148 fn try_compile_vector_search(
17159 &mut self,
17160 fc: &ast::FunctionCall,
17161 elements: &[ast::ShapeElement],
17162 s: &ast::SelectStmt,
17163 ) -> Result<Option<IrVectorSearch>, PyQLError> {
17164 if fc.module.as_deref() != Some("vector") || fc.name != "search" {
17165 return Ok(None);
17166 }
17167
17168 let text_query_arg = fc.kwargs.iter().find(|(k, _)| k == "query").map(|(_, v)| v);
17170 let is_text_overload = text_query_arg.is_some();
17171
17172 if !is_text_overload && fc.args.len() < 2 {
17173 return Err(
17174 self.type_err("vector::search requires either a positional vector argument or `query := $text`")
17175 );
17176 }
17177
17178 let (type_qname, inner_filter_ast): (String, Option<ast::Expr>) = match &fc.args[0] {
17185 ast::Expr::Path(p) if !p.partial => {
17186 let name = p.steps.iter()
17187 .filter_map(|s| if let ast::PathStep::Name(n) = s { Some(n.as_str()) } else { None })
17188 .collect::<Vec<_>>().join("::");
17189 let td = self.resolve_type(&name)
17190 .map_err(|_| self.type_err(&format!("vector::search: '{}' is not a known type", name)))?;
17191 (format!("{}::{}", td.module, td.name), None)
17192 }
17193 ast::Expr::SubQuery(stmt) => {
17194 if let ast::Stmt::Select(inner_sel) = stmt.as_ref() {
17195 let inner_type_name = match &inner_sel.result {
17196 ast::Expr::Path(p) if !p.partial => {
17197 p.steps.iter()
17198 .filter_map(|s| if let ast::PathStep::Name(n) = s { Some(n.as_str()) } else { None })
17199 .collect::<Vec<_>>().join("::")
17200 }
17201 _ => return Err(self.type_err(
17202 "vector::search: subquery first argument must select a single type (e.g. select Product filter …)"
17203 )),
17204 };
17205 let td = self.resolve_type(&inner_type_name)
17206 .map_err(|_| self.type_err(&format!("vector::search: '{}' is not a known type", inner_type_name)))?;
17207 let qname = format!("{}::{}", td.module, td.name);
17208 (qname, inner_sel.filter.clone())
17209 } else {
17210 return Err(self.type_err("vector::search: subquery first argument must be a SELECT"));
17211 }
17212 }
17213 _ => return Err(self.type_err(
17214 "vector::search: first argument must be a type name or a filtered subquery (e.g. select Product filter …)"
17215 )),
17216 };
17217
17218 let index_name: Option<String> = fc.kwargs.iter().find(|(k, _)| k == "index_name").and_then(|(_, v)| {
17220 if let ast::Expr::Literal(ast::Literal::Str(s)) = v {
17221 Some(s.clone())
17222 } else {
17223 None
17224 }
17225 });
17226
17227 let td = self.resolve_type(&type_qname)?.clone();
17229 let vi = td
17230 .vector_indexes
17231 .iter()
17232 .find(|vi| vi.index_name.as_deref() == index_name.as_deref())
17233 .ok_or_else(|| {
17234 let key = index_name.as_deref().unwrap_or("<default>");
17235 self.type_err(&format!("type '{}' has no vector index '{}'", type_qname, key))
17236 })?;
17237
17238 let vector_col = vi.column_name();
17239 let distance_op = match vi.metric.as_str() {
17240 "euclidean" => "<->",
17241 "inner_product" => "<#>",
17242 _ => "<=>", };
17244
17245 let (
17247 query_expr,
17248 inference_query_param_name,
17249 inference_query_literal,
17250 inference_model,
17251 inference_type_name,
17252 inference_index_name,
17253 );
17254
17255 if is_text_overload {
17256 let vec_idx = self.param_index("__deferred_vec__");
17259 let vec_param = IrExpr::Param { index: vec_idx };
17260 let inner_cast = IrExpr::TypeCast(Box::new(IrTypeCast {
17262 expr: vec_param,
17263 pg_type: "float8[]".to_string(),
17264 tuple_shape: None,
17265 }));
17266 query_expr = IrExpr::TypeCast(Box::new(IrTypeCast {
17267 expr: inner_cast,
17268 pg_type: "vector".to_string(),
17269 tuple_shape: None,
17270 }));
17271 let query_arg = text_query_arg.unwrap();
17272 inference_query_param_name = Some(match query_arg {
17273 ast::Expr::Parameter(name) => name.clone(),
17274 _ => String::new(),
17275 });
17276 inference_query_literal = match query_arg {
17277 ast::Expr::Literal(ast::Literal::Str(s)) => Some(s.clone()),
17278 _ => None,
17279 };
17280 inference_model = Some(vi.model.clone());
17281 inference_type_name = Some(type_qname.clone());
17282 inference_index_name = Some(index_name.clone());
17283 } else {
17284 let raw_query_expr = self.compile_free_expr(&fc.args[1])?;
17286 query_expr = IrExpr::TypeCast(Box::new(IrTypeCast {
17287 expr: raw_query_expr,
17288 pg_type: "vector".to_string(),
17289 tuple_shape: None,
17290 }));
17291 inference_query_param_name = None;
17292 inference_query_literal = None;
17293 inference_model = None;
17294 inference_type_name = None;
17295 inference_index_name = None;
17296 }
17297
17298 let alias = self.fresh_alias();
17299 let source = IrSource {
17300 poly: None,
17301 type_name: type_qname.clone(),
17302 table: td.table.clone(),
17303 alias: alias.clone(),
17304 };
17305
17306 let pre_filter: Option<IrExpr> = match inner_filter_ast {
17309 Some(ref f) => Some(self.compile_expr(f, &td, &alias)?),
17310 None => None,
17311 };
17312
17313 let mut object_shape: Vec<IrShapePointer> = vec![];
17318 for el in elements {
17319 if el.splat.is_some() {
17320 continue;
17321 } let pointer_name = match el.path.steps.first() {
17323 Some(ast::PathStep::Name(n)) => n.as_str(),
17324 _ => continue,
17325 };
17326 match pointer_name {
17327 "distance" => { }
17328 "object" => {
17329 let sub_els = el.nested.as_deref().unwrap_or(&[]);
17330 object_shape = self.compile_shape(sub_els, &td, &alias, &td.module)?;
17331 }
17332 other => {
17333 return Err(self.type_err(&format!(
17334 "vector::search result has no pointer '{}'; valid pointers are 'object' and 'distance'",
17335 other
17336 )));
17337 }
17338 }
17339 }
17340
17341 let (outer_filter, order_by_distance, offset, limit) = self.compile_vs_modifiers(s)?;
17342
17343 let filter = match (pre_filter, outer_filter) {
17345 (Some(a), Some(b)) => Some(IrExpr::BinOp(Box::new(IrBinOp {
17346 left: a,
17347 op: ast::BinOpKind::And,
17348 right: b,
17349 }))),
17350 (Some(f), None) | (None, Some(f)) => Some(f),
17351 (None, None) => None,
17352 };
17353
17354 Ok(Some(IrVectorSearch {
17355 source,
17356 vector_col,
17357 distance_op,
17358 query_expr,
17359 object_shape,
17360 filter,
17361 order_by_distance,
17362 offset,
17363 limit,
17364 inference_query_param_name,
17365 inference_query_literal,
17366 inference_model,
17367 inference_type_name,
17368 inference_index_name,
17369 }))
17370 }
17371
17372 fn compile_vs_modifiers(&mut self, s: &ast::SelectStmt) -> Result<SearchModifiers, PyQLError> {
17375 let mut order_by_distance: Option<IrSortDir> = None;
17376 for sort in &s.order_by {
17377 let is_distance = matches!(&sort.expr,
17378 ast::Expr::Path(p) if p.partial && p.steps.len() == 1
17379 && matches!(&p.steps[0], ast::PathStep::Name(n) if n == "distance")
17380 );
17381 if is_distance {
17382 let dir = match sort.direction {
17383 ast::SortDirection::Desc => IrSortDir::Desc,
17384 ast::SortDirection::Asc => IrSortDir::Asc,
17385 };
17386 order_by_distance = Some(dir);
17387 } else {
17388 return Err(self.type_err("vector::search: only 'order by .distance' is supported as a sort key"));
17389 }
17390 }
17391
17392 let filter = match &s.filter {
17393 Some(f) => Some(self.compile_free_expr(f)?),
17394 None => None,
17395 };
17396 let offset = match &s.offset {
17397 Some(o) => Some(self.compile_free_expr(o)?),
17398 None => None,
17399 };
17400 let limit = match &s.limit {
17401 Some(l) => Some(self.compile_free_expr(l)?),
17402 None => None,
17403 };
17404
17405 Ok((filter, order_by_distance, offset, limit))
17406 }
17407
17408 fn try_compile_fts_search(
17413 &mut self,
17414 fc: &ast::FunctionCall,
17415 elements: &[ast::ShapeElement],
17416 s: &ast::SelectStmt,
17417 ) -> Result<Option<IrFtsSearch>, PyQLError> {
17418 if fc.module.as_deref() != Some("fts") || fc.name != "search" {
17419 return Ok(None);
17420 }
17421 if fc.args.len() < 2 {
17422 return Err(self.type_err("fts::search requires at least 2 arguments: (TypeName, $query)"));
17423 }
17424
17425 let type_qname = match &fc.args[0] {
17427 ast::Expr::Path(p) if !p.partial && p.steps.len() == 1 => {
17428 if let ast::PathStep::Name(n) = &p.steps[0] {
17429 let td = self
17430 .resolve_type(n)
17431 .map_err(|_| self.type_err(&format!("fts::search: '{}' is not a known type", n)))?;
17432 format!("{}::{}", td.module, td.name)
17433 } else {
17434 return Err(self.type_err("fts::search: first argument must be a type name"));
17435 }
17436 }
17437 _ => return Err(self.type_err("fts::search: first argument must be a bare type name")),
17438 };
17439
17440 let index_name: Option<String> = fc.kwargs.iter().find(|(k, _)| k == "index_name").and_then(|(_, v)| {
17442 if let ast::Expr::Literal(ast::Literal::Str(s)) = v {
17443 Some(s.clone())
17444 } else {
17445 None
17446 }
17447 });
17448
17449 let mode_str = fc
17450 .kwargs
17451 .iter()
17452 .find(|(k, _)| k == "mode")
17453 .and_then(|(_, v)| {
17454 if let ast::Expr::Literal(ast::Literal::Str(s)) = v {
17455 Some(s.as_str())
17456 } else {
17457 None
17458 }
17459 })
17460 .unwrap_or("BestFields");
17461
17462 let tsquery_fn: &'static str = match mode_str {
17463 "Phrase" => "phraseto_tsquery",
17464 _ => "websearch_to_tsquery", };
17466
17467 let td = self.resolve_type(&type_qname)?.clone();
17469 let si = td
17470 .search_indexes
17471 .iter()
17472 .find(|si| si.index_name.as_deref() == index_name.as_deref())
17473 .ok_or_else(|| {
17474 let key = index_name.as_deref().unwrap_or("<default>");
17475 self.type_err(&format!("type '{}' has no search index '{}'", type_qname, key))
17476 })?;
17477
17478 let backend = si.backend.clone();
17479 let search_col = si.column_name();
17480 let is_deferred = backend != SearchBackend::Postgres;
17481 let deferred_index_name = if is_deferred {
17482 Some(si.deferred_index_name(&td.module, &td.name))
17483 } else {
17484 None
17485 };
17486
17487 let query_expr;
17492 let deferred_query_param_name;
17493 let deferred_query_literal;
17494 let deferred_ids_param;
17495 let deferred_scores_param;
17496
17497 if is_deferred {
17498 let ids_idx = self.param_index("__deferred_ids__");
17499 let scores_idx = self.param_index("__deferred_scores__");
17500 deferred_ids_param = Some(ids_idx);
17501 deferred_scores_param = Some(scores_idx);
17502 deferred_query_param_name = match &fc.args[1] {
17503 ast::Expr::Parameter(name) => Some(name.clone()),
17504 _ => None,
17505 };
17506 deferred_query_literal = match &fc.args[1] {
17507 ast::Expr::Literal(ast::Literal::Str(s)) => Some(s.clone()),
17508 _ => None,
17509 };
17510 query_expr = IrExpr::Literal(crate::ir::IrLiteral::Str(String::new()));
17512 } else {
17513 query_expr = self.compile_free_expr(&fc.args[1])?;
17514 deferred_query_param_name = None;
17515 deferred_query_literal = None;
17516 deferred_ids_param = None;
17517 deferred_scores_param = None;
17518 }
17519
17520 let alias = self.fresh_alias();
17521 let source = IrSource {
17522 poly: None,
17523 type_name: type_qname.clone(),
17524 table: td.table.clone(),
17525 alias: alias.clone(),
17526 };
17527
17528 let mut object_shape: Vec<IrShapePointer> = vec![];
17530 for el in elements {
17531 if el.splat.is_some() {
17532 continue;
17533 }
17534 let pointer_name = match el.path.steps.first() {
17535 Some(ast::PathStep::Name(n)) => n.as_str(),
17536 _ => continue,
17537 };
17538 match pointer_name {
17539 "score" => { }
17540 "object" => {
17541 let sub_els = el.nested.as_deref().unwrap_or(&[]);
17542 object_shape = self.compile_shape(sub_els, &td, &alias, &td.module)?;
17543 }
17544 other => {
17545 return Err(self.type_err(&format!(
17546 "fts::search result has no pointer '{}'; valid pointers are 'object' and 'score'",
17547 other
17548 )));
17549 }
17550 }
17551 }
17552
17553 let (filter, order_by_rank, offset, limit) = self.compile_fts_modifiers(s)?;
17554
17555 Ok(Some(IrFtsSearch {
17556 source,
17557 backend,
17558 search_col,
17559 tsquery_fn,
17560 query_expr,
17561 object_shape,
17562 filter,
17563 order_by_rank,
17564 offset,
17565 limit,
17566 deferred_index_name,
17567 deferred_query_param_name,
17568 deferred_query_literal,
17569 deferred_ids_param,
17570 deferred_scores_param,
17571 }))
17572 }
17573
17574 fn compile_fts_modifiers(&mut self, s: &ast::SelectStmt) -> Result<SearchModifiers, PyQLError> {
17577 let mut order_by_rank: Option<IrSortDir> = None;
17578 for sort in &s.order_by {
17579 let is_rank = matches!(&sort.expr,
17580 ast::Expr::Path(p) if p.partial && p.steps.len() == 1
17581 && matches!(&p.steps[0], ast::PathStep::Name(n) if n == "score")
17582 );
17583 if is_rank {
17584 let dir = match sort.direction {
17585 ast::SortDirection::Desc => IrSortDir::Desc,
17586 ast::SortDirection::Asc => IrSortDir::Asc,
17587 };
17588 order_by_rank = Some(dir);
17589 } else {
17590 return Err(self.type_err("fts::search: only 'order by .score' is supported as a sort key"));
17591 }
17592 }
17593
17594 let filter = match &s.filter {
17595 Some(f) => Some(self.compile_free_expr(f)?),
17596 None => None,
17597 };
17598 let offset = match &s.offset {
17599 Some(o) => Some(self.compile_free_expr(o)?),
17600 None => None,
17601 };
17602 let limit = match &s.limit {
17603 Some(l) => Some(self.compile_free_expr(l)?),
17604 None => None,
17605 };
17606
17607 Ok((filter, order_by_rank, offset, limit))
17608 }
17609
17610 fn type_err(&self, msg: &str) -> PyQLError {
17613 PyQLError::Type(PyQLTypeError {
17614 message: msg.to_string(),
17615 position: Position { line: 0, col: 0 },
17616 })
17617 }
17618
17619 fn field_err(&self, field: &str, type_name: &str) -> PyQLError {
17620 if std::env::var("PYLON_DBG_FIELD_ERR").is_ok() {
17621 eprintln!(
17622 "DBG field_err {field} on {type_name}
17623{}",
17624 std::backtrace::Backtrace::force_capture()
17625 );
17626 }
17627 let suggestion = self
17628 .schema
17629 .types
17630 .iter()
17631 .find(|t| format!("{}::{}", t.module, t.name) == type_name)
17632 .and_then(|td| Self::suggest_pointer_name(td, field));
17633 let message = match suggestion {
17634 Some(s) => format!("object type '{type_name}' has no link or property '{field}'. Did you mean '{s}'?"),
17635 None => format!("object type '{type_name}' has no link or property '{field}'"),
17636 };
17637 PyQLError::Resolution(PyQLResolutionError::UnknownField(PyQLUnknownFieldError {
17638 message,
17639 position: Position { line: 0, col: 0 },
17640 }))
17641 }
17642
17643 fn suggest_function_name(&self, ns: &str, name: &str) -> String {
17659 const MIN_SIMILARITY: f64 = 0.7;
17660 if let Some(other) = ["std", "math", "cal", "sys"]
17661 .into_iter()
17662 .find(|o| *o != ns && !crate::stdlib::lookup(o, name).is_empty())
17663 {
17664 return format!(" — it lives in {other}, use {other}::{name}()");
17665 }
17666 crate::stdlib::registry()
17667 .iter()
17668 .filter(|d| d.namespace == ns)
17669 .map(|d| (format!("{ns}::{}", d.name), d.name))
17670 .chain(
17671 self.schema
17672 .functions
17673 .iter()
17674 .map(|f| (format!("{}::{}", f.module, f.name), f.name.as_str())),
17675 )
17676 .map(|(qualified, candidate)| (qualified, strsim::jaro_winkler(name, candidate)))
17677 .filter(|(_, score)| *score >= MIN_SIMILARITY)
17678 .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
17679 .map(|(qualified, _)| format!(" — did you mean {qualified}()?"))
17680 .unwrap_or_default()
17681 }
17682
17683 fn suggest_pointer_name(td: &TypeDescriptor, name: &str) -> Option<String> {
17694 const MIN_SIMILARITY: f64 = 0.7;
17695 td.properties
17696 .iter()
17697 .map(|p| p.name.as_str())
17698 .chain(td.links.iter().map(|l| l.name.as_str()))
17699 .chain(td.multilinks.iter().map(|m| m.name.as_str()))
17700 .chain(td.computed.iter().map(|c| c.name.as_str()))
17701 .map(|candidate| (candidate, strsim::jaro_winkler(name, candidate)))
17702 .filter(|(_, score)| *score >= MIN_SIMILARITY)
17703 .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
17704 .map(|(name, _)| name.to_string())
17705 }
17706
17707 fn pk_returning(td: &TypeDescriptor) -> Vec<IrShapePointer> {
17712 td.properties
17713 .iter()
17714 .filter(|p| p.is_pk)
17715 .map(|p| {
17716 IrShapePointer::Scalar(IrScalarPointer {
17717 implicit_id: false,
17718 marker_offset: None,
17719 alias: p.name.clone(),
17720 column: p.name.clone(),
17721 pg_type: p.pg_type.clone(),
17722 tuple_shape: None,
17723 })
17724 })
17725 .collect()
17726 }
17727}
17728
17729fn casts_to_json(ty: &ast::TypeExpr) -> bool {
17734 matches!(ty.as_named(), Some((module, "json")) if module.is_none_or(|m| m == "std"))
17735}
17736
17737fn has_any_link_props(vals: &IrMultiLinkValues) -> bool {
17740 if !vals.link_props.is_empty() {
17741 return true;
17742 }
17743 match &vals.source {
17744 IrMultiLinkValueSource::Union(a, b) => has_any_link_props(a) || has_any_link_props(b),
17745 _ => false,
17746 }
17747}
17748
17749fn is_empty_set_expr(expr: &Expr) -> bool {
17755 match expr {
17756 Expr::Set(elems) => elems.is_empty(),
17757 Expr::TypeCast(tc) => matches!(&tc.expr, Expr::Set(elems) if elems.is_empty()),
17758 _ => false,
17759 }
17760}
17761
17762fn path_leaf(p: &ast::Path) -> Result<&str, PyQLError> {
17764 match p.steps.as_slice() {
17765 [ast::PathStep::Name(n)] => Ok(n.as_str()),
17766 _ => Err(PyQLError::Type(PyQLTypeError {
17767 message: "expected a simple pointer name in shape element".into(),
17768 position: Position { line: 0, col: 0 },
17769 })),
17770 }
17771}
17772
17773fn type_expr_to_pg(ty: &ast::TypeExpr) -> Result<String, PyQLError> {
17775 let Some((module, bare_name)) = ty.as_named() else {
17776 return Err(PyQLError::Type(PyQLTypeError {
17780 message: "internal error: structural tuple/array type reached type_expr_to_pg".into(),
17781 position: Position { line: 0, col: 0 },
17782 }));
17783 };
17784
17785 if module == Some("pgvector") {
17787 return match bare_name {
17788 "vector" => Ok("vector".to_string()),
17789 other => Err(PyQLError::Type(PyQLTypeError {
17790 message: format!("unknown pgvector type '{other}'; valid types are: vector"),
17791 position: Position { line: 0, col: 0 },
17792 })),
17793 };
17794 }
17795
17796 if module == Some("postgis") {
17798 return match bare_name {
17799 "geometry" => Ok("geometry".to_string()),
17800 "geography" => Ok("geography".to_string()),
17801 "box2d" => Ok("box2d".to_string()),
17802 "box3d" => Ok("box3d".to_string()),
17803 other => Err(PyQLError::Type(PyQLTypeError {
17804 message: format!("unknown postgis type '{other}'; valid types are: geometry, geography, box2d, box3d"),
17805 position: Position { line: 0, col: 0 },
17806 })),
17807 };
17808 }
17809
17810 if module == Some("cal") {
17812 let pg = match bare_name {
17813 "local_datetime" => "timestamp",
17814 "local_date" => "date",
17815 "local_time" => "time",
17816 "relative_duration" | "date_duration" => "interval",
17817 other => {
17818 return Err(PyQLError::Type(PyQLTypeError {
17819 message: format!(
17820 "unknown cal type '{other}'; \
17821 valid types are: local_datetime, local_date, local_time, \
17822 relative_duration, date_duration"
17823 ),
17824 position: Position { line: 0, col: 0 },
17825 }));
17826 }
17827 };
17828 return Ok(pg.to_string());
17829 }
17830
17831 let name = match module {
17832 Some("std") | None => bare_name,
17833 Some(m) => {
17834 return Err(PyQLError::Type(PyQLTypeError {
17835 message: format!("unknown type '{}::{}'", m, bare_name),
17836 position: Position { line: 0, col: 0 },
17837 }));
17838 }
17839 };
17840 Ok(match name {
17841 "str" => "text",
17842 "int16" => "int2",
17843 "int32" => "int4",
17844 "int64" => "int8",
17845 "float32" => "float4",
17846 "float64" => "float8",
17847 "bool" => "boolean",
17848 "uuid" => "uuid",
17849 "bytes" => "bytea",
17850 "json" => "jsonb",
17851 "decimal" => "numeric",
17852 "bigint" => "numeric",
17855 "datetime" => "timestamptz",
17856 "date" => "date",
17857 "time" => "time",
17858 "duration" => "interval",
17859 other => {
17860 return Err(PyQLError::Type(PyQLTypeError {
17861 message: format!("unknown type '{other}'"),
17862 position: Position { line: 0, col: 0 },
17863 }));
17864 }
17865 }
17866 .to_string())
17867}
17868
17869fn ir_is_not_null(expr: IrExpr) -> IrExpr {
17873 IrExpr::FunctionCall(IrFunctionCall {
17874 return_pg_type: None,
17875 schema: None,
17876 name: String::new(),
17877 args: vec![expr],
17878 sql_template: Some("($1 IS NOT NULL)".to_string()),
17879 })
17880}
17881
17882fn describe_arities(overloads: &[&crate::stdlib::FnDescriptor]) -> String {
17886 let mut arities: Vec<usize> = overloads
17889 .iter()
17890 .map(|d| d.params.iter().filter(|p| p.named_only.is_none()).count())
17891 .collect();
17892 arities.sort_unstable();
17893 arities.dedup();
17894 if overloads.iter().any(|d| d.is_variadic()) {
17895 return format!("at least {} argument(s)", arities[0].saturating_sub(1));
17896 }
17897 let list = arities.iter().map(usize::to_string).collect::<Vec<_>>().join(" or ");
17898 format!("{list} argument(s)")
17899}
17900
17901fn describe_args(args: &[IrExpr]) -> String {
17904 args.iter()
17905 .map(|a| infer_ir_type(a).map_or("?", literal_sentinel_to_pg))
17906 .collect::<Vec<_>>()
17907 .join(", ")
17908}
17909
17910fn describe_signatures(overloads: &[&crate::stdlib::FnDescriptor]) -> String {
17913 overloads
17914 .iter()
17915 .map(|d| {
17916 let params = d
17917 .params
17918 .iter()
17919 .map(|p| match p.named_only {
17920 Some(_) => format!("{} := {}", p.keyword(), p.ty.pyql_name()),
17923 None => p.ty.pyql_name(),
17924 })
17925 .collect::<Vec<_>>()
17926 .join(", ");
17927 format!("({params})")
17928 })
17929 .collect::<Vec<_>>()
17930 .join(" or ")
17931}
17932
17933fn bool_literal(expr: &IrExpr) -> Option<bool> {
17936 match expr {
17937 IrExpr::Literal(IrLiteral::Bool(b)) => Some(*b),
17938 _ => None,
17939 }
17940}
17941
17942fn pack_variadic_args(d: &crate::stdlib::FnDescriptor, args: Vec<IrExpr>) -> Vec<IrExpr> {
17951 let named = d.named_count();
17952 let Some(variadic_at) = d.variadic_index().filter(|v| v + 1 < d.params.len()) else {
17953 return args;
17954 };
17955 let Some(absorbed) = args.len().checked_sub(variadic_at + named) else {
17956 return args;
17957 };
17958 let mut packed: Vec<IrExpr> = Vec::with_capacity(variadic_at + 1 + named);
17959 let mut rest = args.into_iter();
17960 packed.extend(rest.by_ref().take(variadic_at));
17961 packed.push(IrExpr::Array(rest.by_ref().take(absorbed).collect()));
17962 packed.extend(rest);
17963 packed
17964}
17965
17966fn params_for_args(d: &crate::stdlib::FnDescriptor, argc: usize) -> impl Iterator<Item = &crate::stdlib::Param> {
17979 let named = d.named_count();
17980 let variadic_at = d.is_variadic().then(|| d.variadic_index()).flatten();
17981 let absorbed = variadic_at.map_or(0, |v| argc.saturating_sub(v + named));
17982 (0..argc).filter_map(move |i| match variadic_at {
17983 Some(v) if i >= v && i < v + absorbed => d.params.get(v),
17984 Some(v) if i >= v => d.params.get(i + 1 - absorbed),
17985 _ => d.params.get(i),
17986 })
17987}
17988
17989fn pylon_type_matches(expr: &IrExpr, ty: &crate::stdlib::PylonType) -> bool {
17990 use crate::stdlib::PylonType as PT;
17991 match ty {
17992 PT::Any | PT::AnyOrderable | PT::AnyPoint => true,
17994 PT::Array(_) => is_array_expr(expr),
17995 PT::Range(_) => matches!(infer_ir_type(expr), Some(t) if t.ends_with("range") && !t.contains("multirange")),
18000 PT::Multirange(_) => matches!(infer_ir_type(expr), Some(t) if t.contains("multirange")),
18001 _ => match (ty.scalar_pg_type(), infer_ir_type(expr)) {
18015 (Some(declared), Some(known)) => types_compatible(known, declared),
18016 (Some(_), None) => false,
18017 (None, _) => true,
18018 },
18019 }
18020}
18021
18022fn expr_returns_set(expr: &Expr) -> bool {
18024 let Expr::FunctionCall(call) = expr else {
18025 return false;
18026 };
18027 crate::stdlib::lookup(call.module.as_deref().unwrap_or("std"), &call.name)
18028 .iter()
18029 .any(|d| d.returns_set())
18030}
18031
18032fn literal_sentinel_to_pg(t: &str) -> &str {
18033 match t {
18034 "__int_literal" => "int8",
18035 "__float_literal" => "float8",
18036 other => other,
18037 }
18038}
18039
18040fn is_array_expr(expr: &IrExpr) -> bool {
18041 match through_coalesce(expr) {
18042 IrExpr::Array(_) | IrExpr::ArrayFromSelect(_) => true,
18043 IrExpr::FunctionCall(f) if f.schema.is_none() => {
18046 let mut overloads = crate::stdlib::registry().iter().filter(|d| d.name == f.name).peekable();
18047 overloads.peek().is_some() && overloads.all(|d| matches!(d.return_type, crate::stdlib::PylonType::Array(_)))
18048 }
18049 other => matches!(infer_ir_type(other), Some(t) if t.ends_with("[]")),
18053 }
18054}
18055
18056fn static_min_payload_bytes(expr: &ast::Expr) -> usize {
18072 use crate::parse::ast::{BinOpKind, Expr, Literal};
18073 match expr {
18074 Expr::Literal(Literal::Str(s)) => s.len(),
18075 Expr::BinOp(op) if matches!(op.op, BinOpKind::Concat) => {
18076 static_min_payload_bytes(&op.left) + static_min_payload_bytes(&op.right)
18077 }
18078 Expr::Shape(sh) if sh.expr.is_none() => {
18079 let mut total = 2;
18083 for (i, el) in sh.elements.iter().enumerate() {
18084 if i > 0 {
18085 total += 1;
18086 }
18087 let name_len = match el.path.steps.last() {
18088 Some(crate::parse::ast::PathStep::Name(n)) => n.len(),
18089 _ => 0,
18090 };
18091 total += name_len + 3;
18092 if let Some(value) = &el.compexpr {
18093 total += static_min_payload_bytes(value);
18094 }
18095 }
18096 total
18097 }
18098 _ => 0,
18099 }
18100}
18101
18102fn expr_to_std_type(expr: &ast::Expr) -> &'static str {
18103 match expr {
18104 ast::Expr::Literal(ast::Literal::Str(_)) => "std::str",
18105 ast::Expr::Literal(ast::Literal::Int(_)) => "std::int64",
18106 ast::Expr::Literal(ast::Literal::Float(_)) => "std::float64",
18107 ast::Expr::Literal(ast::Literal::Bool(_)) => "std::bool",
18108 _ => "anytype",
18109 }
18110}
18111
18112fn named_tuple_type_str(fields: &[(String, ast::Expr)]) -> String {
18113 let inner = fields
18114 .iter()
18115 .map(|(k, v)| format!("{}: {}", k, expr_to_std_type(v)))
18116 .collect::<Vec<_>>()
18117 .join(", ");
18118 format!("tuple<{}>", inner)
18119}
18120
18121fn positional_tuple_type_str(elems: &[ast::Expr]) -> String {
18122 let inner = elems.iter().map(expr_to_std_type).collect::<Vec<_>>().join(", ");
18123 format!("tuple<{}>", inner)
18124}
18125
18126pub(crate) fn infer_ir_type(expr: &IrExpr) -> Option<&str> {
18127 match expr {
18128 IrExpr::ColumnRef { pg_type, .. } => Some(pg_type.as_str()),
18129 IrExpr::TypeCast(tc) => Some(tc.pg_type.as_str()),
18130 IrExpr::FnParam { pg_type, .. } => Some(pg_type.as_str()),
18131 IrExpr::Literal(lit) => Some(match lit {
18132 IrLiteral::Str(_) => "text",
18133 IrLiteral::Int(_) => "__int_literal",
18134 IrLiteral::Float(_) => "__float_literal",
18135 IrLiteral::Bool(_) => "boolean",
18136 }),
18137 IrExpr::EnumLiteral { pg_type, .. } => Some(pg_type.as_str()),
18138 IrExpr::NamedTuple { .. } => Some("jsonb"),
18139 IrExpr::JsonbField { .. } | IrExpr::JsonbIndex { .. } => Some("jsonb"),
18140 IrExpr::GlobalParam { pg_type, .. } => Some(pg_type.as_str()),
18141 IrExpr::CteRef { pg_type, .. } => pg_type.as_deref(),
18144 IrExpr::PathSubquery(ps) => match &ps.result {
18148 IrPathResult::Scalar(e, _) => infer_ir_type(e),
18149 IrPathResult::Object { .. } => None,
18150 },
18151 IrExpr::BinOp(b) if b.op == crate::parse::ast::BinOpKind::Concat => {
18154 infer_ir_type(&b.left).or_else(|| infer_ir_type(&b.right))
18155 }
18156 IrExpr::UnaryOp(u) if u.op == crate::parse::ast::UnaryOpKind::Distinct => infer_ir_type(&u.operand),
18157 IrExpr::UnaryOp(u) if u.op == crate::parse::ast::UnaryOpKind::Minus => infer_ir_type(&u.operand),
18160 IrExpr::UnaryOp(u)
18161 if matches!(
18162 u.op,
18163 crate::parse::ast::UnaryOpKind::Not | crate::parse::ast::UnaryOpKind::Exists
18164 ) =>
18165 {
18166 Some("boolean")
18167 }
18168 IrExpr::BinOp(b) if b.op == crate::parse::ast::BinOpKind::Coalesce => {
18174 infer_ir_type(&b.left).or_else(|| infer_ir_type(&b.right))
18175 }
18176 IrExpr::IfElse(ie) => infer_ir_type(&ie.if_).or_else(|| infer_ir_type(&ie.else_)),
18177 IrExpr::Slice { expr, .. } => infer_ir_type(expr),
18183 IrExpr::CteFieldRef { pg_type, .. } | IrExpr::ForVar { pg_type, .. } => pg_type.as_deref(),
18187 IrExpr::Subscript { expr, is_array, .. } => {
18188 let Some(base) = infer_ir_type(expr) else {
18192 return stdlib_array_element_type(expr);
18193 };
18194 if *is_array { base.strip_suffix("[]") } else { Some(base) }
18195 }
18196 IrExpr::BinOp(b) => arithmetic_result_type(&b.op, infer_ir_type(&b.left)?, infer_ir_type(&b.right)?),
18197 IrExpr::FunctionCall(f) if f.schema.is_none() && f.name == "coalesce" => f.args.iter().find_map(infer_ir_type),
18202 IrExpr::FunctionCall(f) if f.schema.is_none() && matches!(f.name.as_str(), "max" | "min" | "sum") => {
18203 aggregate_result_type(&f.name, infer_ir_type(f.args.first()?)?)
18204 }
18205 IrExpr::AggOverSet {
18206 fn_name, schema: None, ..
18207 } if fn_name == "count" => Some("int8"),
18208 IrExpr::AggOverSet {
18209 fn_name,
18210 schema: None,
18211 elems,
18212 } => aggregate_result_type(fn_name, infer_ir_type(elems.first()?)?),
18213 IrExpr::FunctionCall(f) => f.return_pg_type.as_deref(),
18218 _ => None,
18219 }
18220}
18221
18222fn stdlib_array_element_type(expr: &IrExpr) -> Option<&'static str> {
18227 let IrExpr::FunctionCall(f) = expr else {
18228 return None;
18229 };
18230 if f.schema.is_some() {
18231 return None;
18232 }
18233 let mut element: Option<&'static str> = None;
18234 for descriptor in crate::stdlib::registry().iter().filter(|d| d.name == f.name) {
18235 let crate::stdlib::PylonType::Array(inner) = &descriptor.return_type else {
18236 return None;
18237 };
18238 let scalar = inner.scalar_pg_type()?;
18239 if element.is_some_and(|seen| seen != scalar) {
18240 return None;
18241 }
18242 element = Some(scalar);
18243 }
18244 element
18245}
18246
18247fn temporal_result_type(op: &ast::BinOpKind, left: &str, right: &str) -> Option<&'static str> {
18253 use ast::BinOpKind::*;
18254 let instant = |t: &str| matches!(t, "timestamptz" | "timestamp" | "date" | "time");
18255 match (op, left, right) {
18256 (Sub, l, r) if instant(l) && instant(r) => Some("interval"),
18257 (Sub | Add, l, "interval") if instant(l) => Some(match l {
18258 "timestamptz" => "timestamptz",
18259 "timestamp" => "timestamp",
18260 "date" => "date",
18261 _ => "time",
18262 }),
18263 (Add, "interval", r) if instant(r) => Some(match r {
18264 "timestamptz" => "timestamptz",
18265 "timestamp" => "timestamp",
18266 "date" => "date",
18267 _ => "time",
18268 }),
18269 (Add | Sub, "interval", "interval") => Some("interval"),
18270 _ => None,
18271 }
18272}
18273
18274fn arithmetic_result_type(op: &ast::BinOpKind, left: &str, right: &str) -> Option<&'static str> {
18277 use ast::BinOpKind::*;
18278 if !matches!(op, Add | Sub | Mul | Div | FloorDiv | Mod | Pow) {
18279 return None;
18280 }
18281 let (left, right) = (literal_sentinel_to_pg(left), literal_sentinel_to_pg(right));
18282 if let Some(temporal) = temporal_result_type(op, left, right) {
18283 return Some(temporal);
18284 }
18285 let int = |t: &str| INT_TYPES.contains(&t);
18286 let float = |t: &str| FLOAT_TYPES.contains(&t);
18287 let numeric = |t: &str| NUMERIC_TYPES.contains(&t);
18288 if int(left) && int(right) {
18289 return Some(if matches!(op, Div | Pow) {
18290 "float8"
18291 } else if left == "int8" || right == "int8" {
18292 "int8"
18293 } else if left == "int4" || right == "int4" {
18294 "int4"
18295 } else {
18296 "int2"
18297 });
18298 }
18299 if (float(left) || int(left)) && (float(right) || int(right)) {
18300 return Some(if left == "float4" && right == "float4" {
18301 "float4"
18302 } else {
18303 "float8"
18304 });
18305 }
18306 if (numeric(left) || int(left)) && (numeric(right) || int(right)) {
18307 return Some("numeric");
18308 }
18309 None
18310}
18311
18312fn aggregate_result_type<'a>(name: &str, element: &'a str) -> Option<&'a str> {
18314 let element = literal_sentinel_to_pg(element);
18315 match name {
18316 "max" | "min" => Some(element),
18317 "sum" if INT_TYPES.contains(&element) => Some("int8"),
18318 "sum" if FLOAT_TYPES.contains(&element) || NUMERIC_TYPES.contains(&element) => Some(element),
18319 _ => None,
18320 }
18321}
18322
18323fn true_division_operand(op: &ast::BinOpKind, left: IrExpr, right: &IrExpr) -> IrExpr {
18326 let integer = |expr: &IrExpr| infer_ir_type(expr).is_some_and(|t| INT_TYPES.contains(&literal_sentinel_to_pg(t)));
18327 if *op != ast::BinOpKind::Div || !integer(&left) || !integer(right) {
18328 return left;
18329 }
18330 IrExpr::TypeCast(Box::new(IrTypeCast {
18331 expr: left,
18332 pg_type: "float8".to_string(),
18333 tuple_shape: None,
18334 }))
18335}
18336
18337fn range_ctor_for_pg_type(pg_type: &str) -> Option<&'static str> {
18344 match pg_type {
18345 "int2" | "int4" | "int8" | "__int_literal" => Some("int8range"),
18346 "numeric" | "__float_literal" => Some("numrange"),
18347 "timestamp" => Some("tsrange"),
18348 "timestamptz" => Some("tstzrange"),
18349 "date" => Some("daterange"),
18350 _ => None,
18351 }
18352}
18353
18354fn multirange_ctor_for_range_ctor(range_ctor: &str) -> Option<&'static str> {
18357 match range_ctor {
18358 "int8range" => Some("int8multirange"),
18359 "numrange" => Some("nummultirange"),
18360 "tsrange" => Some("tsmultirange"),
18361 "tstzrange" => Some("tstzmultirange"),
18362 "daterange" => Some("datemultirange"),
18363 _ => None,
18364 }
18365}
18366
18367const INT_TYPES: &[&str] = &["int2", "int4", "int8", "__int_literal"];
18368const FLOAT_TYPES: &[&str] = &["float4", "float8", "__float_literal"];
18369const NUMERIC_TYPES: &[&str] = &["numeric"];
18373
18374pub(crate) fn types_compatible(a: &str, b: &str) -> bool {
18384 if a == b {
18385 return true;
18386 }
18387 let a_int = INT_TYPES.contains(&a);
18388 let b_int = INT_TYPES.contains(&b);
18389 if a_int && b_int {
18390 return true;
18391 }
18392 let a_float = FLOAT_TYPES.contains(&a);
18393 let b_float = FLOAT_TYPES.contains(&b);
18394 if a_float && b_float {
18395 return true;
18396 }
18397 let a_numeric = NUMERIC_TYPES.contains(&a);
18398 let b_numeric = NUMERIC_TYPES.contains(&b);
18399 if a_numeric && b_numeric {
18400 return true;
18401 }
18402 (a_int && b_float) || (a_float && b_int) || (a_int && b_numeric) || (a_numeric && b_int)
18403}
18404
18405fn datetime_arithmetic_compatible(op: &ast::BinOpKind, a: &str, b: &str) -> bool {
18417 if !matches!(op, ast::BinOpKind::Add | ast::BinOpKind::Sub) {
18418 return false;
18419 }
18420 matches!(
18421 (a, b),
18422 ("timestamptz", "interval")
18423 | ("interval", "timestamptz")
18424 | ("timestamp", "interval")
18425 | ("interval", "timestamp")
18426 | ("date", "interval")
18427 | ("interval", "date")
18428 | ("time", "interval")
18429 | ("interval", "time")
18430 )
18431}
18432
18433fn collect_search_enqueue(td: &TypeDescriptor, type_name: &str, operation: &'static str) -> Vec<SearchEnqueueInfo> {
18438 td.search_indexes
18439 .iter()
18440 .filter(|si| si.backend == SearchBackend::OpenSearch || si.backend == SearchBackend::Meilisearch)
18441 .map(|si| SearchEnqueueInfo {
18442 type_name: type_name.to_string(),
18443 index_name: si.index_name.clone(),
18444 operation,
18445 backend: si.backend.clone(),
18446 })
18447 .collect()
18448}
18449
18450pub fn pg_type_to_pyql(pg: &str) -> &str {
18463 match pg {
18464 "text" | "varchar" => "std::str",
18465 "uuid" => "std::uuid",
18466 "int2" => "std::int16",
18467 "int4" => "std::int32",
18468 "int8" => "std::int64",
18469 "float4" => "std::float32",
18470 "float8" => "std::float64",
18471 "boolean" => "std::bool",
18472 "numeric" => "std::decimal",
18473 "timestamptz" => "std::datetime",
18474 "timestamp" => "cal::local_datetime",
18475 "date" => "cal::local_date",
18476 "time" => "cal::local_time",
18477 "interval" => "std::duration",
18478 "bytea" => "std::bytes",
18479 "jsonb" => "std::json",
18480 "__int_literal" => "std::int64",
18481 "__float_literal" => "std::float64",
18482 other => other,
18483 }
18484}