1use std::collections::{HashMap, HashSet};
2use std::sync::Arc;
3
4use arrow_schema::{DataType, Field, Schema, SchemaRef};
5
6use crate::catalog::Catalog;
7use crate::error::{NanoError, Result};
8use crate::types::{Direction, PropType, ScalarType};
9
10use super::ast::*;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum BindingKind {
14 Node,
15 Edge,
16}
17
18#[derive(Debug, Clone)]
19pub struct BoundVariable {
20 pub var_name: String,
21 pub type_name: String,
22 pub kind: BindingKind,
23}
24
25#[derive(Debug, Clone)]
26pub struct TypeContext {
27 pub bindings: HashMap<String, BoundVariable>,
28 pub aliases: HashMap<String, ResolvedType>,
29 pub traversals: Vec<ResolvedTraversal>,
30}
31
32#[derive(Debug, Clone)]
33pub struct ResolvedTraversal {
34 pub src: String,
35 pub dst: String,
36 pub edge_type: String,
37 pub direction: Direction,
38 pub min_hops: u32,
39 pub max_hops: Option<u32>,
40}
41
42#[derive(Debug, Clone, PartialEq)]
43pub enum ResolvedType {
44 Scalar(PropType),
45 Node(String),
46 Aggregate,
47}
48
49impl ResolvedType {
50 fn display_name(&self) -> String {
51 match self {
52 Self::Scalar(prop) => prop.display_name(),
53 Self::Node(type_name) => format!("node `{}`", type_name),
54 Self::Aggregate => "aggregate".to_string(),
55 }
56 }
57}
58
59#[derive(Debug, Clone)]
60pub struct MutationTypeContext {
61 pub target_types: Vec<String>,
62}
63
64#[derive(Debug, Clone)]
65pub enum CheckedQuery {
66 Read(TypeContext),
67 Mutation(MutationTypeContext),
68}
69
70pub fn typecheck_query_decl(catalog: &Catalog, query: &QueryDecl) -> Result<CheckedQuery> {
71 if !query.mutations.is_empty() {
72 let mut target_types = Vec::with_capacity(query.mutations.len());
73 for mutation in &query.mutations {
74 let target_type = typecheck_mutation(catalog, mutation, &query.params)?;
75 target_types.push(target_type);
76 }
77 Ok(CheckedQuery::Mutation(MutationTypeContext { target_types }))
78 } else {
79 Ok(CheckedQuery::Read(typecheck_read_query(catalog, query)?))
80 }
81}
82
83pub fn typecheck_query(catalog: &Catalog, query: &QueryDecl) -> Result<TypeContext> {
84 if !query.mutations.is_empty() {
85 return Err(NanoError::Type(
86 "mutation query cannot be typechecked with read-query API".to_string(),
87 ));
88 }
89 typecheck_read_query(catalog, query)
90}
91
92pub fn infer_query_result_schema(
93 catalog: &Catalog,
94 query: &QueryDecl,
95 ctx: &TypeContext,
96) -> Result<SchemaRef> {
97 let params = parse_declared_param_types(&query.params)?;
98 let mut fields = Vec::with_capacity(query.return_clause.len());
99
100 for projection in &query.return_clause {
101 let field = infer_projection_field(
102 catalog,
103 &projection.expr,
104 projection.alias.as_deref(),
105 ctx,
106 ¶ms,
107 )?;
108 fields.push(field);
109 }
110
111 Ok(Arc::new(Schema::new(fields)))
112}
113
114fn parse_declared_param_types(params: &[Param]) -> Result<HashMap<String, PropType>> {
115 let mut out = HashMap::with_capacity(params.len());
116 for p in params {
117 if p.name == NOW_PARAM_NAME {
118 return Err(NanoError::Type(format!(
119 "parameter name `${}` is reserved for runtime timestamp injection",
120 NOW_PARAM_NAME
121 )));
122 }
123 let prop_type =
124 PropType::from_param_type_name(&p.type_name, p.nullable).ok_or_else(|| {
125 NanoError::Type(format!(
126 "unknown parameter type `{}` for `${}`",
127 p.type_name, p.name
128 ))
129 })?;
130 out.insert(p.name.clone(), prop_type);
131 }
132 Ok(out)
133}
134
135fn typecheck_read_query(catalog: &Catalog, query: &QueryDecl) -> Result<TypeContext> {
136 let mut ctx = TypeContext {
137 bindings: HashMap::new(),
138 aliases: HashMap::new(),
139 traversals: Vec::new(),
140 };
141 let mut alias_exprs: HashMap<String, &Expr> = HashMap::new();
142
143 let params = parse_declared_param_types(&query.params)?;
144
145 typecheck_clauses(catalog, &query.match_clause, &mut ctx, ¶ms, false)?;
147
148 for proj in &query.return_clause {
150 let resolved = resolve_expr_type(catalog, &proj.expr, &ctx, ¶ms)?;
151 if let Some(alias) = &proj.alias {
152 ctx.aliases.insert(alias.clone(), resolved);
153 alias_exprs.insert(alias.clone(), &proj.expr);
154 }
155 }
156
157 for ord in &query.order_clause {
159 resolve_expr_type(catalog, &ord.expr, &ctx, ¶ms)?;
160 }
161
162 let has_standalone_nearest = query
163 .order_clause
164 .iter()
165 .any(|ord| expr_contains_standalone_nearest_with_aliases(&ord.expr, &alias_exprs));
166 let has_rrf = query
167 .order_clause
168 .iter()
169 .any(|ord| expr_contains_rrf_with_aliases(&ord.expr, &alias_exprs));
170 if has_rrf && query.limit.is_none() {
171 return Err(NanoError::Type(
172 "T21: rrf ordering requires a limit clause".to_string(),
173 ));
174 }
175 if has_standalone_nearest && query.limit.is_none() {
176 return Err(NanoError::Type(
177 "T17: nearest ordering requires a limit clause".to_string(),
178 ));
179 }
180 if has_standalone_nearest
181 && query
182 .order_clause
183 .iter()
184 .any(|ord| matches!(ord.expr, Expr::AliasRef(_)))
185 {
186 return Err(NanoError::Type(
187 "T18: alias-based ordering is not supported together with nearest in phase 1"
188 .to_string(),
189 ));
190 }
191
192 let has_agg = query
195 .return_clause
196 .iter()
197 .any(|p| matches!(p.expr, Expr::Aggregate { .. }));
198 if has_agg {
199 for proj in &query.return_clause {
200 if !matches!(proj.expr, Expr::Aggregate { .. }) {
201 match &proj.expr {
202 Expr::PropAccess { .. } | Expr::Variable(_) => {}
203 _ => {
204 return Err(NanoError::Type(
205 "T9: non-aggregate expressions in an aggregate query must be \
206 property accesses or variables"
207 .to_string(),
208 ));
209 }
210 }
211 }
212 }
213 }
214
215 Ok(ctx)
216}
217
218fn typecheck_mutation(catalog: &Catalog, mutation: &Mutation, params: &[Param]) -> Result<String> {
219 let param_types = parse_declared_param_types(params)?;
220
221 match mutation {
222 Mutation::Insert(insert) => {
223 if insert.assignments.is_empty() {
224 return Err(NanoError::Type(
225 "T10: insert mutation requires at least one assignment".to_string(),
226 ));
227 }
228
229 ensure_no_duplicate_assignment_names(&insert.assignments)?;
230
231 if let Some(node_type) = catalog.node_types.get(&insert.type_name) {
232 for assignment in &insert.assignments {
233 let prop_type =
234 node_type
235 .properties
236 .get(&assignment.property)
237 .ok_or_else(|| {
238 NanoError::Type(format!(
239 "T11: type `{}` has no property `{}`",
240 insert.type_name, assignment.property
241 ))
242 })?;
243 check_match_value_type(
244 &assignment.value,
245 ¶m_types,
246 prop_type,
247 &assignment.property,
248 )?;
249 }
250
251 let assigned_props: HashSet<&str> = insert
252 .assignments
253 .iter()
254 .map(|assignment| assignment.property.as_str())
255 .collect();
256 for (prop_name, prop_type) in &node_type.properties {
257 if prop_type.nullable {
258 continue;
259 }
260 if assigned_props.contains(prop_name.as_str()) {
261 continue;
262 }
263
264 if let Some(source_prop) = node_type.embed_sources.get(prop_name) {
265 if assigned_props.contains(source_prop.as_str()) {
266 continue;
267 }
268 return Err(NanoError::Type(format!(
269 "T12: insert for `{}` must provide non-nullable property `{}` or @embed source `{}`",
270 insert.type_name, prop_name, source_prop
271 )));
272 }
273
274 return Err(NanoError::Type(format!(
275 "T12: insert for `{}` must provide non-nullable property `{}`",
276 insert.type_name, prop_name
277 )));
278 }
279 return Ok(insert.type_name.clone());
280 }
281
282 if let Some(edge_type) = catalog.edge_types.get(&insert.type_name) {
283 let mut has_from = false;
284 let mut has_to = false;
285
286 for assignment in &insert.assignments {
287 match assignment.property.as_str() {
288 "from" => {
289 has_from = true;
290 check_match_value_type(
291 &assignment.value,
292 ¶m_types,
293 &PropType::scalar(ScalarType::String, false),
294 "from",
295 )?;
296 }
297 "to" => {
298 has_to = true;
299 check_match_value_type(
300 &assignment.value,
301 ¶m_types,
302 &PropType::scalar(ScalarType::String, false),
303 "to",
304 )?;
305 }
306 _ => {
307 let prop_type = edge_type
308 .properties
309 .get(&assignment.property)
310 .ok_or_else(|| {
311 NanoError::Type(format!(
312 "T11: type `{}` has no property `{}`",
313 insert.type_name, assignment.property
314 ))
315 })?;
316 check_match_value_type(
317 &assignment.value,
318 ¶m_types,
319 prop_type,
320 &assignment.property,
321 )?;
322 }
323 }
324 }
325
326 if !has_from {
327 return Err(NanoError::Type(format!(
328 "T12: insert for `{}` must provide required endpoint `from`",
329 insert.type_name
330 )));
331 }
332 if !has_to {
333 return Err(NanoError::Type(format!(
334 "T12: insert for `{}` must provide required endpoint `to`",
335 insert.type_name
336 )));
337 }
338
339 for (prop_name, prop_type) in &edge_type.properties {
340 if prop_type.nullable {
341 continue;
342 }
343 if !insert.assignments.iter().any(|a| &a.property == prop_name) {
344 return Err(NanoError::Type(format!(
345 "T12: insert for `{}` must provide non-nullable property `{}`",
346 insert.type_name, prop_name
347 )));
348 }
349 }
350 return Ok(insert.type_name.clone());
351 }
352
353 Err(NanoError::Type(format!(
354 "T10: unknown node/edge type `{}`",
355 insert.type_name
356 )))
357 }
358 Mutation::Update(update) => {
359 let node_type = if let Some(node_type) = catalog.node_types.get(&update.type_name) {
360 node_type
361 } else if catalog.edge_types.contains_key(&update.type_name) {
362 return Err(NanoError::Type(format!(
363 "T16: update mutation for edge type `{}` is not supported",
364 update.type_name
365 )));
366 } else {
367 return Err(NanoError::Type(format!(
368 "T10: unknown node/edge type `{}`",
369 update.type_name
370 )));
371 };
372
373 if update.assignments.is_empty() {
374 return Err(NanoError::Type(
375 "T10: update mutation requires at least one assignment".to_string(),
376 ));
377 }
378 ensure_no_duplicate_assignment_names(&update.assignments)?;
379
380 for assignment in &update.assignments {
381 let prop_type =
382 node_type
383 .properties
384 .get(&assignment.property)
385 .ok_or_else(|| {
386 NanoError::Type(format!(
387 "T11: type `{}` has no property `{}`",
388 update.type_name, assignment.property
389 ))
390 })?;
391 check_match_value_type(
392 &assignment.value,
393 ¶m_types,
394 prop_type,
395 &assignment.property,
396 )?;
397 }
398
399 typecheck_mutation_predicate(
400 &update.type_name,
401 &update.predicate,
402 node_type,
403 ¶m_types,
404 )?;
405 Ok(update.type_name.clone())
406 }
407 Mutation::Delete(delete) => {
408 if let Some(node_type) = catalog.node_types.get(&delete.type_name) {
409 typecheck_mutation_predicate(
410 &delete.type_name,
411 &delete.predicate,
412 node_type,
413 ¶m_types,
414 )?;
415 Ok(delete.type_name.clone())
416 } else if let Some(edge_type) = catalog.edge_types.get(&delete.type_name) {
417 typecheck_edge_mutation_predicate(
418 &delete.type_name,
419 &delete.predicate,
420 edge_type,
421 ¶m_types,
422 )?;
423 Ok(delete.type_name.clone())
424 } else {
425 Err(NanoError::Type(format!(
426 "T10: unknown node/edge type `{}`",
427 delete.type_name
428 )))
429 }
430 }
431 }
432}
433
434fn ensure_no_duplicate_assignment_names(assignments: &[MutationAssignment]) -> Result<()> {
435 let mut seen = std::collections::HashSet::new();
436 for assignment in assignments {
437 if !seen.insert(&assignment.property) {
438 return Err(NanoError::Type(format!(
439 "T13: duplicate assignment for property `{}`",
440 assignment.property
441 )));
442 }
443 }
444 Ok(())
445}
446
447fn typecheck_mutation_predicate(
448 type_name: &str,
449 predicate: &MutationPredicate,
450 node_type: &crate::catalog::NodeType,
451 param_types: &HashMap<String, PropType>,
452) -> Result<()> {
453 let prop_type = node_type
454 .properties
455 .get(&predicate.property)
456 .ok_or_else(|| {
457 NanoError::Type(format!(
458 "T11: type `{}` has no property `{}`",
459 type_name, predicate.property
460 ))
461 })?;
462 if matches!(prop_type.scalar, ScalarType::Blob) {
463 return Err(NanoError::Type(format!(
464 "T11: blob property `{}` cannot be used in WHERE predicates",
465 predicate.property
466 )));
467 }
468 check_match_value_type(
469 &predicate.value,
470 param_types,
471 prop_type,
472 &predicate.property,
473 )?;
474 Ok(())
475}
476
477fn typecheck_edge_mutation_predicate(
478 type_name: &str,
479 predicate: &MutationPredicate,
480 edge_type: &crate::catalog::EdgeType,
481 param_types: &HashMap<String, PropType>,
482) -> Result<()> {
483 if predicate.property == "from" || predicate.property == "to" {
484 return check_match_value_type(
485 &predicate.value,
486 param_types,
487 &PropType::scalar(ScalarType::String, false),
488 &predicate.property,
489 );
490 }
491
492 let prop_type = edge_type
493 .properties
494 .get(&predicate.property)
495 .ok_or_else(|| {
496 NanoError::Type(format!(
497 "T11: type `{}` has no property `{}`",
498 type_name, predicate.property
499 ))
500 })?;
501 check_match_value_type(
502 &predicate.value,
503 param_types,
504 prop_type,
505 &predicate.property,
506 )?;
507 Ok(())
508}
509
510fn check_match_value_type(
511 value: &MatchValue,
512 params: &HashMap<String, PropType>,
513 expected: &PropType,
514 property: &str,
515) -> Result<()> {
516 match value {
517 MatchValue::Literal(lit) => check_literal_type(lit, expected, property),
518 MatchValue::Variable(v) => {
519 let Some(actual) = params.get(v) else {
520 return Err(NanoError::Type(format!(
521 "T14: mutation variable `${}` must be a declared query parameter",
522 v
523 )));
524 };
525 let compatible = types_compatible(actual, expected)
527 || (matches!(expected.scalar, ScalarType::Blob)
528 && matches!(actual.scalar, ScalarType::String)
529 && !actual.list);
530 if !compatible {
531 return Err(NanoError::Type(format!(
532 "T7: cannot assign/compare {} with {} for property `{}`",
533 actual.display_name(),
534 expected.display_name(),
535 property
536 )));
537 }
538 Ok(())
539 }
540 MatchValue::Now => check_now_match_value_type(expected, property),
541 }
542}
543
544fn check_now_match_value_type(expected: &PropType, property: &str) -> Result<()> {
545 if expected.list || expected.scalar != ScalarType::DateTime {
546 return Err(NanoError::Type(format!(
547 "T7: cannot assign/compare DateTime with {} for property `{}`",
548 expected.display_name(),
549 property
550 )));
551 }
552 Ok(())
553}
554
555fn typecheck_clauses(
556 catalog: &Catalog,
557 clauses: &[Clause],
558 ctx: &mut TypeContext,
559 params: &HashMap<String, PropType>,
560 _in_negation: bool,
561) -> Result<()> {
562 for clause in clauses {
563 match clause {
564 Clause::Binding(b) => typecheck_binding(catalog, b, ctx, params)?,
565 Clause::Traversal(t) => typecheck_traversal(catalog, t, ctx)?,
566 Clause::Filter(f) => typecheck_filter(catalog, f, ctx, params)?,
567 Clause::Negation(inner) => {
568 let outer_vars: Vec<String> = ctx.bindings.keys().cloned().collect();
570
571 let mut inner_ctx = ctx.clone();
573 typecheck_clauses(catalog, inner, &mut inner_ctx, params, true)?;
574
575 let mut has_outer = false;
577 for clause in inner {
578 match clause {
579 Clause::Traversal(t) => {
580 if outer_vars.contains(&t.src) || outer_vars.contains(&t.dst) {
581 has_outer = true;
582 }
583 }
584 Clause::Filter(f) => {
585 if expr_references_any(&f.left, &outer_vars)
586 || expr_references_any(&f.right, &outer_vars)
587 {
588 has_outer = true;
589 }
590 }
591 Clause::Binding(b) => {
592 if outer_vars.contains(&b.variable) {
593 has_outer = true;
594 }
595 }
596 _ => {}
597 }
598 }
599 if !has_outer {
600 return Err(NanoError::Type(
601 "T9: negation block must reference at least one outer-bound variable"
602 .to_string(),
603 ));
604 }
605 }
606 }
607 }
608 Ok(())
609}
610
611fn typecheck_binding(
612 catalog: &Catalog,
613 binding: &Binding,
614 ctx: &mut TypeContext,
615 params: &HashMap<String, PropType>,
616) -> Result<()> {
617 if !catalog.node_types.contains_key(&binding.type_name) {
619 return Err(NanoError::Type(format!(
620 "T1: unknown node type `{}`",
621 binding.type_name
622 )));
623 }
624
625 let node_type = &catalog.node_types[&binding.type_name];
626
627 for pm in &binding.prop_matches {
629 let prop = node_type.properties.get(&pm.prop_name).ok_or_else(|| {
630 NanoError::Type(format!(
631 "T2: type `{}` has no property `{}`",
632 binding.type_name, pm.prop_name
633 ))
634 })?;
635
636 if matches!(prop.scalar, ScalarType::Blob) {
637 return Err(NanoError::Type(format!(
638 "T3: blob property `{}.{}` cannot be used in match patterns",
639 binding.type_name, pm.prop_name
640 )));
641 }
642
643 match &pm.value {
645 MatchValue::Literal(lit) => {
646 check_binding_literal_type(lit, prop, &pm.prop_name)?;
647 }
648 MatchValue::Variable(v) => {
649 if let Some(actual) = params.get(v) {
650 check_binding_variable_type(actual, prop, &pm.prop_name)?;
651 }
652 }
653 MatchValue::Now => check_now_match_value_type(prop, &pm.prop_name)?,
654 }
655 }
656
657 if let Some(existing) = ctx.bindings.get(&binding.variable)
659 && existing.type_name != binding.type_name
660 {
661 return Err(NanoError::Type(format!(
662 "variable `${}` already bound to type `{}`, cannot rebind to `{}`",
663 binding.variable, existing.type_name, binding.type_name
664 )));
665 }
666
667 ctx.bindings.insert(
668 binding.variable.clone(),
669 BoundVariable {
670 var_name: binding.variable.clone(),
671 type_name: binding.type_name.clone(),
672 kind: BindingKind::Node,
673 },
674 );
675
676 Ok(())
677}
678
679fn check_binding_literal_type(lit: &Literal, expected: &PropType, property: &str) -> Result<()> {
680 if expected.list {
681 let lit_type = literal_type(lit)?;
682 if lit_type.list {
683 return Err(NanoError::Type(format!(
684 "T3: list equality is not supported for property `{}`; use a scalar value to match list membership",
685 property
686 )));
687 }
688
689 let expected_member = PropType::scalar(expected.scalar, expected.nullable);
690 if !types_compatible(&lit_type, &expected_member) {
691 return Err(NanoError::Type(format!(
692 "T3: property `{}` has type {} but membership match got {}",
693 property,
694 expected.display_name(),
695 lit_type.display_name()
696 )));
697 }
698 return Ok(());
699 }
700
701 check_literal_type(lit, expected, property)
702}
703
704fn check_binding_variable_type(
705 actual: &PropType,
706 expected: &PropType,
707 property: &str,
708) -> Result<()> {
709 if expected.list {
710 if actual.list {
711 return Err(NanoError::Type(format!(
712 "T7: list equality is not supported for property `{}`; use a scalar parameter for membership matching",
713 property
714 )));
715 }
716
717 let expected_member = PropType::scalar(expected.scalar, expected.nullable);
718 if !types_compatible(actual, &expected_member) {
719 return Err(NanoError::Type(format!(
720 "T7: cannot compare {} membership against {} for property `{}`",
721 actual.display_name(),
722 expected.display_name(),
723 property
724 )));
725 }
726 return Ok(());
727 }
728
729 if !types_compatible(actual, expected) {
730 return Err(NanoError::Type(format!(
731 "T7: cannot assign/compare {} with {} for property `{}`",
732 actual.display_name(),
733 expected.display_name(),
734 property
735 )));
736 }
737 Ok(())
738}
739
740fn typecheck_traversal(
741 catalog: &Catalog,
742 traversal: &Traversal,
743 ctx: &mut TypeContext,
744) -> Result<()> {
745 let edge = catalog
747 .lookup_edge_by_name(&traversal.edge_name)
748 .ok_or_else(|| {
749 NanoError::Type(format!("T4: unknown edge type `{}`", traversal.edge_name))
750 })?;
751
752 if traversal.min_hops == 0 {
753 return Err(NanoError::Type(
754 "T15: traversal min hop bound must be >= 1".to_string(),
755 ));
756 }
757 if let Some(max_hops) = traversal.max_hops {
758 if max_hops < traversal.min_hops {
759 return Err(NanoError::Type(format!(
760 "T15: invalid traversal bounds {{{},{}}}; max must be >= min",
761 traversal.min_hops, max_hops
762 )));
763 }
764 } else {
765 return Err(NanoError::Type(
766 "T15: unbounded traversal is disabled; use bounded traversal {min,max}".to_string(),
767 ));
768 }
769
770 let src_bound = ctx.bindings.get(&traversal.src);
772 let dst_bound = ctx.bindings.get(&traversal.dst);
773
774 let direction;
775
776 if let Some(src_bv) = src_bound {
777 if src_bv.type_name == edge.from_type {
779 direction = Direction::Out;
780 bind_traversal_endpoint(ctx, &traversal.dst, &edge.to_type, edge)?;
782 } else if src_bv.type_name == edge.to_type {
783 direction = Direction::In;
784 bind_traversal_endpoint(ctx, &traversal.dst, &edge.from_type, edge)?;
786 } else {
787 return Err(NanoError::Type(format!(
788 "T5: variable `${}` has type `{}`, which is not an endpoint of edge `{}: {} -> {}`",
789 traversal.src, src_bv.type_name, edge.name, edge.from_type, edge.to_type
790 )));
791 }
792 } else if let Some(dst_bv) = dst_bound {
793 if dst_bv.type_name == edge.to_type {
795 direction = Direction::Out;
796 bind_traversal_endpoint(ctx, &traversal.src, &edge.from_type, edge)?;
797 } else if dst_bv.type_name == edge.from_type {
798 direction = Direction::In;
799 bind_traversal_endpoint(ctx, &traversal.src, &edge.to_type, edge)?;
800 } else {
801 return Err(NanoError::Type(format!(
802 "T5: variable `${}` has type `{}`, which is not an endpoint of edge `{}: {} -> {}`",
803 traversal.dst, dst_bv.type_name, edge.name, edge.from_type, edge.to_type
804 )));
805 }
806 } else {
807 direction = Direction::Out;
809 bind_traversal_endpoint(ctx, &traversal.src, &edge.from_type, edge)?;
810 bind_traversal_endpoint(ctx, &traversal.dst, &edge.to_type, edge)?;
811 }
812
813 ctx.traversals.push(ResolvedTraversal {
814 src: traversal.src.clone(),
815 dst: traversal.dst.clone(),
816 edge_type: edge.name.clone(),
817 direction,
818 min_hops: traversal.min_hops,
819 max_hops: traversal.max_hops,
820 });
821
822 Ok(())
823}
824
825fn bind_traversal_endpoint(
826 ctx: &mut TypeContext,
827 var: &str,
828 expected_type: &str,
829 edge: &crate::catalog::EdgeType,
830) -> Result<()> {
831 if var == "_" {
832 return Ok(()); }
834 if let Some(existing) = ctx.bindings.get(var) {
835 if existing.type_name != expected_type {
836 return Err(NanoError::Type(format!(
837 "T5: variable `${}` has type `{}` but edge `{}` expects `{}`",
838 var, existing.type_name, edge.name, expected_type
839 )));
840 }
841 } else {
842 ctx.bindings.insert(
843 var.to_string(),
844 BoundVariable {
845 var_name: var.to_string(),
846 type_name: expected_type.to_string(),
847 kind: BindingKind::Node,
848 },
849 );
850 }
851 Ok(())
852}
853
854fn typecheck_filter(
855 catalog: &Catalog,
856 filter: &Filter,
857 ctx: &TypeContext,
858 params: &HashMap<String, PropType>,
859) -> Result<()> {
860 let left_type = resolve_expr_type(catalog, &filter.left, ctx, params)?;
861 let right_type = resolve_expr_type(catalog, &filter.right, ctx, params)?;
862
863 if let (ResolvedType::Scalar(l), ResolvedType::Scalar(r)) = (&left_type, &right_type) {
864 if filter.op == CompOp::Contains {
865 if !l.list {
866 return Err(NanoError::Type(format!(
867 "T7: contains requires a list property on the left, got {}",
868 l.display_name()
869 )));
870 }
871 if r.list {
872 return Err(NanoError::Type(
873 "T7: contains requires a scalar right operand".to_string(),
874 ));
875 }
876 if matches!(l.scalar, ScalarType::Vector(_))
877 || matches!(r.scalar, ScalarType::Vector(_))
878 {
879 return Err(NanoError::Type(
880 "T7: vector membership filters are not supported".to_string(),
881 ));
882 }
883
884 let expected_member = PropType::scalar(l.scalar, l.nullable);
885 if !types_compatible(&expected_member, r) {
886 return Err(NanoError::Type(format!(
887 "T7: cannot test membership of {} in {}",
888 r.display_name(),
889 l.display_name()
890 )));
891 }
892 return Ok(());
893 }
894
895 if l.list || r.list {
897 return Err(NanoError::Type(
898 "T7: list comparisons in filters are not supported; use `contains` for list membership".to_string(),
899 ));
900 }
901 if matches!(l.scalar, ScalarType::Vector(_)) || matches!(r.scalar, ScalarType::Vector(_)) {
902 return Err(NanoError::Type(
903 "T7: vector comparisons in filters are not supported".to_string(),
904 ));
905 }
906 if matches!(l.scalar, ScalarType::Blob) || matches!(r.scalar, ScalarType::Blob) {
907 return Err(NanoError::Type(
908 "T7: blob comparisons in filters are not supported".to_string(),
909 ));
910 }
911 if !types_compatible(l, r) {
912 return Err(NanoError::Type(format!(
913 "T7: cannot compare {} with {}",
914 l.display_name(),
915 r.display_name()
916 )));
917 }
918 } else {
919 return Err(NanoError::Type(format!(
920 "T7: filter comparisons require scalar operands, got {} and {}",
921 left_type.display_name(),
922 right_type.display_name()
923 )));
924 }
925
926 Ok(())
927}
928
929fn resolve_expr_type(
930 catalog: &Catalog,
931 expr: &Expr,
932 ctx: &TypeContext,
933 params: &HashMap<String, PropType>,
934) -> Result<ResolvedType> {
935 match expr {
936 Expr::Now => Ok(ResolvedType::Scalar(PropType::scalar(
937 ScalarType::DateTime,
938 false,
939 ))),
940 Expr::PropAccess { variable, property } => {
941 let bv = ctx.bindings.get(variable).ok_or_else(|| {
943 NanoError::Type(format!("T6: variable `${}` is not bound", variable))
944 })?;
945
946 let node_type = catalog.node_types.get(&bv.type_name).ok_or_else(|| {
947 NanoError::Type(format!("T6: type `{}` not found in catalog", bv.type_name))
948 })?;
949
950 let prop = node_type.properties.get(property).ok_or_else(|| {
951 NanoError::Type(format!(
952 "T6: type `{}` has no property `{}`",
953 bv.type_name, property
954 ))
955 })?;
956
957 Ok(ResolvedType::Scalar(prop.clone()))
958 }
959 Expr::Nearest {
960 variable,
961 property,
962 query,
963 } => {
964 let node_binding = ctx.bindings.get(variable).ok_or_else(|| {
965 NanoError::Type(format!("T15: variable `${}` is not bound", variable))
966 })?;
967 let node_type = catalog
968 .node_types
969 .get(&node_binding.type_name)
970 .ok_or_else(|| {
971 NanoError::Type(format!(
972 "T15: type `{}` not found in catalog",
973 node_binding.type_name
974 ))
975 })?;
976 let prop_type = node_type.properties.get(property).ok_or_else(|| {
977 NanoError::Type(format!(
978 "T15: type `{}` has no property `{}`",
979 node_binding.type_name, property
980 ))
981 })?;
982 let vector_dim = match prop_type.scalar {
983 ScalarType::Vector(dim) => dim,
984 _ => {
985 return Err(NanoError::Type(format!(
986 "T15: nearest requires a Vector property, got {}.{}: {}",
987 node_binding.type_name,
988 property,
989 prop_type.display_name()
990 )));
991 }
992 };
993 if prop_type.list {
994 return Err(NanoError::Type(
995 "T15: nearest does not support list-wrapped vectors".to_string(),
996 ));
997 }
998
999 if let Expr::Literal(lit) = query.as_ref()
1000 && let Some(dim) = numeric_vector_literal_dim(lit)
1001 {
1002 if dim != vector_dim {
1003 return Err(NanoError::Type(format!(
1004 "T15: nearest vector dimension mismatch: property is Vector({}), query literal has {} elements",
1005 vector_dim, dim
1006 )));
1007 }
1008 return Ok(ResolvedType::Scalar(PropType::scalar(
1009 ScalarType::F64,
1010 false,
1011 )));
1012 }
1013
1014 let query_type = resolve_expr_type(catalog, query, ctx, params)?;
1015 match query_type {
1016 ResolvedType::Scalar(s) if matches!(s.scalar, ScalarType::Vector(_)) && !s.list => {
1017 let qdim = match s.scalar {
1018 ScalarType::Vector(dim) => dim,
1019 _ => unreachable!(),
1020 };
1021 if qdim != vector_dim {
1022 return Err(NanoError::Type(format!(
1023 "T15: nearest vector dimension mismatch: property is Vector({}), query is Vector({})",
1024 vector_dim, qdim
1025 )));
1026 }
1027 }
1028 ResolvedType::Scalar(s) if s.scalar == ScalarType::String && !s.list => {
1029 }
1031 ResolvedType::Scalar(s) => {
1032 return Err(NanoError::Type(format!(
1033 "T15: nearest query must be Vector({}) or String, got {}",
1034 vector_dim,
1035 s.display_name()
1036 )));
1037 }
1038 _ => {
1039 return Err(NanoError::Type(
1040 "T15: nearest query must be a scalar expression".to_string(),
1041 ));
1042 }
1043 }
1044
1045 Ok(ResolvedType::Scalar(PropType::scalar(
1046 ScalarType::F64,
1047 false,
1048 )))
1049 }
1050 Expr::Search { field, query } => {
1051 let field_type = resolve_expr_type(catalog, field, ctx, params)?;
1052 match field_type {
1053 ResolvedType::Scalar(s) if s.scalar == ScalarType::String && !s.list => {}
1054 ResolvedType::Scalar(s) => {
1055 return Err(NanoError::Type(format!(
1056 "T19: search field must be String, got {}",
1057 s.display_name()
1058 )));
1059 }
1060 _ => {
1061 return Err(NanoError::Type(
1062 "T19: search field must be a scalar String expression".to_string(),
1063 ));
1064 }
1065 }
1066
1067 let query_type = resolve_expr_type(catalog, query, ctx, params)?;
1068 match query_type {
1069 ResolvedType::Scalar(s) if s.scalar == ScalarType::String && !s.list => {}
1070 ResolvedType::Scalar(s) => {
1071 return Err(NanoError::Type(format!(
1072 "T19: search query must be String, got {}",
1073 s.display_name()
1074 )));
1075 }
1076 _ => {
1077 return Err(NanoError::Type(
1078 "T19: search query must be a scalar String expression".to_string(),
1079 ));
1080 }
1081 }
1082
1083 Ok(ResolvedType::Scalar(PropType::scalar(
1084 ScalarType::Bool,
1085 false,
1086 )))
1087 }
1088 Expr::Fuzzy {
1089 field,
1090 query,
1091 max_edits,
1092 } => {
1093 let field_type = resolve_expr_type(catalog, field, ctx, params)?;
1094 match field_type {
1095 ResolvedType::Scalar(s) if s.scalar == ScalarType::String && !s.list => {}
1096 ResolvedType::Scalar(s) => {
1097 return Err(NanoError::Type(format!(
1098 "T19: fuzzy field must be String, got {}",
1099 s.display_name()
1100 )));
1101 }
1102 _ => {
1103 return Err(NanoError::Type(
1104 "T19: fuzzy field must be a scalar String expression".to_string(),
1105 ));
1106 }
1107 }
1108
1109 let query_type = resolve_expr_type(catalog, query, ctx, params)?;
1110 match query_type {
1111 ResolvedType::Scalar(s) if s.scalar == ScalarType::String && !s.list => {}
1112 ResolvedType::Scalar(s) => {
1113 return Err(NanoError::Type(format!(
1114 "T19: fuzzy query must be String, got {}",
1115 s.display_name()
1116 )));
1117 }
1118 _ => {
1119 return Err(NanoError::Type(
1120 "T19: fuzzy query must be a scalar String expression".to_string(),
1121 ));
1122 }
1123 }
1124
1125 if let Some(max_edits_expr) = max_edits {
1126 let max_edits_type = resolve_expr_type(catalog, max_edits_expr, ctx, params)?;
1127 match max_edits_type {
1128 ResolvedType::Scalar(s)
1129 if !s.list
1130 && matches!(
1131 s.scalar,
1132 ScalarType::I32
1133 | ScalarType::I64
1134 | ScalarType::U32
1135 | ScalarType::U64
1136 ) => {}
1137 ResolvedType::Scalar(s) => {
1138 return Err(NanoError::Type(format!(
1139 "T19: fuzzy max_edits must be an integer scalar, got {}",
1140 s.display_name()
1141 )));
1142 }
1143 _ => {
1144 return Err(NanoError::Type(
1145 "T19: fuzzy max_edits must be an integer scalar expression".to_string(),
1146 ));
1147 }
1148 }
1149 }
1150
1151 Ok(ResolvedType::Scalar(PropType::scalar(
1152 ScalarType::Bool,
1153 false,
1154 )))
1155 }
1156 Expr::MatchText { field, query } => {
1157 let field_type = resolve_expr_type(catalog, field, ctx, params)?;
1158 match field_type {
1159 ResolvedType::Scalar(s) if s.scalar == ScalarType::String && !s.list => {}
1160 ResolvedType::Scalar(s) => {
1161 return Err(NanoError::Type(format!(
1162 "T20: match_text field must be String, got {}",
1163 s.display_name()
1164 )));
1165 }
1166 _ => {
1167 return Err(NanoError::Type(
1168 "T20: match_text field must be a scalar String expression".to_string(),
1169 ));
1170 }
1171 }
1172
1173 let query_type = resolve_expr_type(catalog, query, ctx, params)?;
1174 match query_type {
1175 ResolvedType::Scalar(s) if s.scalar == ScalarType::String && !s.list => {}
1176 ResolvedType::Scalar(s) => {
1177 return Err(NanoError::Type(format!(
1178 "T20: match_text query must be String, got {}",
1179 s.display_name()
1180 )));
1181 }
1182 _ => {
1183 return Err(NanoError::Type(
1184 "T20: match_text query must be a scalar String expression".to_string(),
1185 ));
1186 }
1187 }
1188
1189 Ok(ResolvedType::Scalar(PropType::scalar(
1190 ScalarType::Bool,
1191 false,
1192 )))
1193 }
1194 Expr::Bm25 { field, query } => {
1195 let field_type = resolve_expr_type(catalog, field, ctx, params)?;
1196 match field_type {
1197 ResolvedType::Scalar(s) if s.scalar == ScalarType::String && !s.list => {}
1198 ResolvedType::Scalar(s) => {
1199 return Err(NanoError::Type(format!(
1200 "T20: bm25 field must be String, got {}",
1201 s.display_name()
1202 )));
1203 }
1204 _ => {
1205 return Err(NanoError::Type(
1206 "T20: bm25 field must be a scalar String expression".to_string(),
1207 ));
1208 }
1209 }
1210
1211 let query_type = resolve_expr_type(catalog, query, ctx, params)?;
1212 match query_type {
1213 ResolvedType::Scalar(s) if s.scalar == ScalarType::String && !s.list => {}
1214 ResolvedType::Scalar(s) => {
1215 return Err(NanoError::Type(format!(
1216 "T20: bm25 query must be String, got {}",
1217 s.display_name()
1218 )));
1219 }
1220 _ => {
1221 return Err(NanoError::Type(
1222 "T20: bm25 query must be a scalar String expression".to_string(),
1223 ));
1224 }
1225 }
1226
1227 Ok(ResolvedType::Scalar(PropType::scalar(
1228 ScalarType::F64,
1229 false,
1230 )))
1231 }
1232 Expr::Rrf {
1233 primary,
1234 secondary,
1235 k,
1236 } => {
1237 if !matches!(primary.as_ref(), Expr::Nearest { .. } | Expr::Bm25 { .. }) {
1238 return Err(NanoError::Type(
1239 "T21: rrf primary expression must be nearest(...) or bm25(...)".to_string(),
1240 ));
1241 }
1242 if !matches!(secondary.as_ref(), Expr::Nearest { .. } | Expr::Bm25 { .. }) {
1243 return Err(NanoError::Type(
1244 "T21: rrf secondary expression must be nearest(...) or bm25(...)".to_string(),
1245 ));
1246 }
1247
1248 let primary_ty = resolve_expr_type(catalog, primary, ctx, params)?;
1249 let secondary_ty = resolve_expr_type(catalog, secondary, ctx, params)?;
1250
1251 for ty in [primary_ty, secondary_ty] {
1252 match ty {
1253 ResolvedType::Scalar(s) if s.scalar == ScalarType::F64 && !s.list => {}
1254 ResolvedType::Scalar(s) => {
1255 return Err(NanoError::Type(format!(
1256 "T21: rrf rank expressions must evaluate to F64, got {}",
1257 s.display_name()
1258 )));
1259 }
1260 _ => {
1261 return Err(NanoError::Type(
1262 "T21: rrf rank expressions must be scalar numeric expressions"
1263 .to_string(),
1264 ));
1265 }
1266 }
1267 }
1268
1269 if let Some(k_expr) = k {
1270 let k_type = resolve_expr_type(catalog, k_expr, ctx, params)?;
1271 match k_type {
1272 ResolvedType::Scalar(s)
1273 if !s.list
1274 && matches!(
1275 s.scalar,
1276 ScalarType::I32
1277 | ScalarType::I64
1278 | ScalarType::U32
1279 | ScalarType::U64
1280 ) => {}
1281 ResolvedType::Scalar(s) => {
1282 return Err(NanoError::Type(format!(
1283 "T21: rrf k must be an integer scalar, got {}",
1284 s.display_name()
1285 )));
1286 }
1287 _ => {
1288 return Err(NanoError::Type(
1289 "T21: rrf k must be an integer scalar expression".to_string(),
1290 ));
1291 }
1292 }
1293 if let Expr::Literal(Literal::Integer(v)) = k_expr.as_ref()
1294 && *v <= 0
1295 {
1296 return Err(NanoError::Type(
1297 "T21: rrf k must be greater than 0".to_string(),
1298 ));
1299 }
1300 }
1301
1302 Ok(ResolvedType::Scalar(PropType::scalar(
1303 ScalarType::F64,
1304 false,
1305 )))
1306 }
1307 Expr::Variable(name) => {
1308 if let Some(prop_type) = params.get(name) {
1310 Ok(ResolvedType::Scalar(prop_type.clone()))
1311 } else if let Some(bv) = ctx.bindings.get(name) {
1312 Ok(ResolvedType::Node(bv.type_name.clone()))
1313 } else {
1314 Err(NanoError::Type(format!(
1315 "variable `${}` is not bound",
1316 name
1317 )))
1318 }
1319 }
1320 Expr::Literal(lit) => Ok(ResolvedType::Scalar(literal_type(lit)?)),
1321 Expr::Aggregate { func, arg } => {
1322 let arg_type = resolve_expr_type(catalog, arg, ctx, params)?;
1323
1324 match func {
1326 AggFunc::Sum | AggFunc::Avg => {
1327 if let ResolvedType::Scalar(s) = &arg_type
1328 && (s.list || !s.scalar.is_numeric())
1329 {
1330 return Err(NanoError::Type(format!(
1331 "T8: {} requires numeric type, got {}",
1332 func,
1333 s.display_name()
1334 )));
1335 }
1336 }
1337 AggFunc::Min | AggFunc::Max => {
1338 if let ResolvedType::Scalar(s) = &arg_type
1339 && (s.list || (!s.scalar.is_numeric() && s.scalar != ScalarType::String))
1340 {
1341 return Err(NanoError::Type(format!(
1342 "T8: {} requires numeric or string type, got {}",
1343 func,
1344 s.display_name()
1345 )));
1346 }
1347 }
1348 _ => {} }
1350
1351 Ok(ResolvedType::Aggregate)
1352 }
1353 Expr::AliasRef(name) => {
1354 if let Some(resolved) = ctx.aliases.get(name) {
1356 Ok(resolved.clone())
1357 } else {
1358 Ok(ResolvedType::Aggregate)
1360 }
1361 }
1362 }
1363}
1364
1365fn infer_projection_field(
1366 catalog: &Catalog,
1367 expr: &Expr,
1368 alias: Option<&str>,
1369 ctx: &TypeContext,
1370 params: &HashMap<String, PropType>,
1371) -> Result<Field> {
1372 let name = projection_name(expr, alias);
1373 match expr {
1374 Expr::Aggregate { func, arg } => {
1375 let (data_type, nullable) = match func {
1376 AggFunc::Count => (DataType::Int64, true),
1377 AggFunc::Avg | AggFunc::Sum => (DataType::Float64, true),
1378 AggFunc::Min | AggFunc::Max => {
1379 let resolved = resolve_expr_type(catalog, arg, ctx, params)?;
1380 let (data_type, _) = resolved_type_to_field_shape(catalog, &resolved)?;
1381 (data_type, true)
1382 }
1383 };
1384 Ok(Field::new(name, data_type, nullable))
1385 }
1386 _ => {
1387 let resolved = resolve_expr_type(catalog, expr, ctx, params)?;
1388 let (data_type, nullable) = resolved_type_to_field_shape(catalog, &resolved)?;
1389 Ok(Field::new(name, data_type, nullable))
1390 }
1391 }
1392}
1393
1394fn projection_name(expr: &Expr, alias: Option<&str>) -> String {
1395 if let Some(alias) = alias {
1396 return alias.to_string();
1397 }
1398
1399 match expr {
1400 Expr::Now => "now".to_string(),
1401 Expr::PropAccess { property, .. } => property.clone(),
1402 Expr::Variable(variable) => variable.clone(),
1403 Expr::Literal(_) => "literal".to_string(),
1404 Expr::Nearest { .. } => "nearest".to_string(),
1405 Expr::Search { .. } => "search".to_string(),
1406 Expr::Fuzzy { .. } => "fuzzy".to_string(),
1407 Expr::MatchText { .. } => "match_text".to_string(),
1408 Expr::Bm25 { .. } => "bm25".to_string(),
1409 Expr::Rrf { .. } => "rrf".to_string(),
1410 Expr::Aggregate { func, .. } => func.to_string(),
1411 Expr::AliasRef(name) => name.clone(),
1412 }
1413}
1414
1415fn resolved_type_to_field_shape(
1416 catalog: &Catalog,
1417 resolved: &ResolvedType,
1418) -> Result<(DataType, bool)> {
1419 match resolved {
1420 ResolvedType::Scalar(prop_type) => Ok((prop_type.to_arrow(), prop_type.nullable)),
1421 ResolvedType::Node(type_name) => {
1422 let node_type = catalog.node_types.get(type_name).ok_or_else(|| {
1423 NanoError::Type(format!("type `{}` not found in catalog", type_name))
1424 })?;
1425 let fields: Vec<Field> = node_type
1426 .arrow_schema
1427 .fields()
1428 .iter()
1429 .map(|field| field.as_ref().clone())
1430 .collect();
1431 Ok((DataType::Struct(fields.into()), false))
1432 }
1433 ResolvedType::Aggregate => Ok((DataType::Int64, true)),
1434 }
1435}
1436
1437fn literal_type(lit: &Literal) -> Result<PropType> {
1438 match lit {
1439 Literal::Null => Ok(PropType::scalar(ScalarType::String, true)),
1441 Literal::String(_) => Ok(PropType::scalar(ScalarType::String, false)),
1442 Literal::Integer(_) => Ok(PropType::scalar(ScalarType::I64, false)),
1443 Literal::Float(_) => Ok(PropType::scalar(ScalarType::F64, false)),
1444 Literal::Bool(_) => Ok(PropType::scalar(ScalarType::Bool, false)),
1445 Literal::Date(_) => Ok(PropType::scalar(ScalarType::Date, false)),
1446 Literal::DateTime(_) => Ok(PropType::scalar(ScalarType::DateTime, false)),
1447 Literal::List(items) => {
1448 if items.is_empty() {
1449 return Ok(PropType::list_of(ScalarType::String, false));
1450 }
1451 let first = literal_type(&items[0])?;
1452 if first.list {
1453 return Err(NanoError::Type(
1454 "nested list literals are not supported".to_string(),
1455 ));
1456 }
1457 for item in items.iter().skip(1) {
1458 let item_type = literal_type(item)?;
1459 if item_type.list || !types_compatible(&first, &item_type) {
1460 return Err(NanoError::Type(
1461 "list literal elements must share a compatible scalar type".to_string(),
1462 ));
1463 }
1464 }
1465 Ok(PropType::list_of(first.scalar, false))
1466 }
1467 }
1468}
1469
1470fn check_literal_type(lit: &Literal, expected: &PropType, prop_name: &str) -> Result<()> {
1471 if matches!(lit, Literal::Null) {
1473 return if expected.nullable {
1474 Ok(())
1475 } else {
1476 Err(NanoError::Type(format!(
1477 "T3: property `{}` is non-nullable but got null",
1478 prop_name
1479 )))
1480 };
1481 }
1482
1483 if !expected.list
1484 && let ScalarType::Vector(expected_dim) = expected.scalar
1485 && let Some(actual_dim) = numeric_vector_literal_dim(lit)
1486 {
1487 if actual_dim == expected_dim {
1488 return Ok(());
1489 }
1490 return Err(NanoError::Type(format!(
1491 "T3: property `{}` has type Vector({}) but got vector literal with {} elements",
1492 prop_name, expected_dim, actual_dim
1493 )));
1494 }
1495
1496 let lit_type = literal_type(lit)?;
1497 if !types_compatible(&lit_type, expected) {
1498 return Err(NanoError::Type(format!(
1499 "T3: property `{}` has type {} but got {}",
1500 prop_name,
1501 expected.display_name(),
1502 lit_type.display_name()
1503 )));
1504 }
1505 if expected.is_enum() {
1506 let allowed = expected.enum_values.as_ref().cloned().unwrap_or_default();
1507 match lit {
1508 Literal::String(v) => {
1509 if !allowed.contains(v) {
1510 return Err(NanoError::Type(format!(
1511 "T3: property `{}` expects one of [{}], got '{}'",
1512 prop_name,
1513 allowed.join(", "),
1514 v
1515 )));
1516 }
1517 }
1518 Literal::List(items) if expected.list => {
1519 for item in items {
1520 match item {
1521 Literal::String(v) if allowed.contains(v) => {}
1522 Literal::String(v) => {
1523 return Err(NanoError::Type(format!(
1524 "T3: property `{}` expects one of [{}], got '{}'",
1525 prop_name,
1526 allowed.join(", "),
1527 v
1528 )));
1529 }
1530 _ => {}
1531 }
1532 }
1533 }
1534 _ => {}
1535 }
1536 }
1537 Ok(())
1538}
1539
1540fn types_compatible(a: &PropType, b: &PropType) -> bool {
1541 if a.list != b.list {
1542 return false;
1543 }
1544 if a.scalar == b.scalar {
1545 return true;
1546 }
1547 if a.scalar.is_numeric() && b.scalar.is_numeric() {
1549 return true;
1550 }
1551 false
1552}
1553
1554fn numeric_vector_literal_dim(lit: &Literal) -> Option<u32> {
1555 let items = match lit {
1556 Literal::List(items) => items,
1557 _ => return None,
1558 };
1559 if items.is_empty() {
1560 return None;
1561 }
1562 if items
1563 .iter()
1564 .all(|v| matches!(v, Literal::Integer(_) | Literal::Float(_)))
1565 {
1566 Some(items.len() as u32)
1567 } else {
1568 None
1569 }
1570}
1571
1572fn expr_references_any(expr: &Expr, vars: &[String]) -> bool {
1573 match expr {
1574 Expr::PropAccess { variable, .. } => vars.contains(variable),
1575 Expr::Nearest {
1576 variable, query, ..
1577 } => vars.contains(variable) || expr_references_any(query, vars),
1578 Expr::Search { field, query } => {
1579 expr_references_any(field, vars) || expr_references_any(query, vars)
1580 }
1581 Expr::Fuzzy {
1582 field,
1583 query,
1584 max_edits,
1585 } => {
1586 expr_references_any(field, vars)
1587 || expr_references_any(query, vars)
1588 || max_edits
1589 .as_deref()
1590 .is_some_and(|m| expr_references_any(m, vars))
1591 }
1592 Expr::MatchText { field, query } => {
1593 expr_references_any(field, vars) || expr_references_any(query, vars)
1594 }
1595 Expr::Bm25 { field, query } => {
1596 expr_references_any(field, vars) || expr_references_any(query, vars)
1597 }
1598 Expr::Rrf {
1599 primary,
1600 secondary,
1601 k,
1602 } => {
1603 expr_references_any(primary, vars)
1604 || expr_references_any(secondary, vars)
1605 || k.as_deref()
1606 .is_some_and(|expr| expr_references_any(expr, vars))
1607 }
1608 Expr::Variable(v) => vars.contains(v),
1609 Expr::Aggregate { arg, .. } => expr_references_any(arg, vars),
1610 _ => false,
1611 }
1612}
1613
1614fn expr_contains_standalone_nearest_with_aliases(
1615 expr: &Expr,
1616 alias_exprs: &HashMap<String, &Expr>,
1617) -> bool {
1618 expr_contains_standalone_nearest_inner(expr, alias_exprs, &mut HashSet::new())
1619}
1620
1621fn expr_contains_standalone_nearest_inner(
1622 expr: &Expr,
1623 alias_exprs: &HashMap<String, &Expr>,
1624 seen_aliases: &mut HashSet<String>,
1625) -> bool {
1626 match expr {
1627 Expr::Nearest { .. } => true,
1628 Expr::Aggregate { arg, .. } => {
1629 expr_contains_standalone_nearest_inner(arg, alias_exprs, seen_aliases)
1630 }
1631 Expr::Search { field, query }
1632 | Expr::MatchText { field, query }
1633 | Expr::Bm25 { field, query } => {
1634 expr_contains_standalone_nearest_inner(field, alias_exprs, seen_aliases)
1635 || expr_contains_standalone_nearest_inner(query, alias_exprs, seen_aliases)
1636 }
1637 Expr::Fuzzy {
1638 field,
1639 query,
1640 max_edits,
1641 } => {
1642 expr_contains_standalone_nearest_inner(field, alias_exprs, seen_aliases)
1643 || expr_contains_standalone_nearest_inner(query, alias_exprs, seen_aliases)
1644 || max_edits.as_deref().is_some_and(|expr| {
1645 expr_contains_standalone_nearest_inner(expr, alias_exprs, seen_aliases)
1646 })
1647 }
1648 Expr::AliasRef(name) => {
1649 if !seen_aliases.insert(name.clone()) {
1650 return false;
1651 }
1652 let found = alias_exprs.get(name).is_some_and(|expr| {
1653 expr_contains_standalone_nearest_inner(expr, alias_exprs, seen_aliases)
1654 });
1655 seen_aliases.remove(name);
1656 found
1657 }
1658 Expr::Rrf { .. } => false,
1660 _ => false,
1661 }
1662}
1663
1664fn expr_contains_rrf_with_aliases(expr: &Expr, alias_exprs: &HashMap<String, &Expr>) -> bool {
1665 expr_contains_rrf_inner(expr, alias_exprs, &mut HashSet::new())
1666}
1667
1668fn expr_contains_rrf_inner(
1669 expr: &Expr,
1670 alias_exprs: &HashMap<String, &Expr>,
1671 seen_aliases: &mut HashSet<String>,
1672) -> bool {
1673 match expr {
1674 Expr::Rrf { .. } => true,
1675 Expr::Aggregate { arg, .. } => expr_contains_rrf_inner(arg, alias_exprs, seen_aliases),
1676 Expr::Search { field, query }
1677 | Expr::MatchText { field, query }
1678 | Expr::Bm25 { field, query } => {
1679 expr_contains_rrf_inner(field, alias_exprs, seen_aliases)
1680 || expr_contains_rrf_inner(query, alias_exprs, seen_aliases)
1681 }
1682 Expr::Fuzzy {
1683 field,
1684 query,
1685 max_edits,
1686 } => {
1687 expr_contains_rrf_inner(field, alias_exprs, seen_aliases)
1688 || expr_contains_rrf_inner(query, alias_exprs, seen_aliases)
1689 || max_edits
1690 .as_deref()
1691 .is_some_and(|expr| expr_contains_rrf_inner(expr, alias_exprs, seen_aliases))
1692 }
1693 Expr::AliasRef(name) => {
1694 if !seen_aliases.insert(name.clone()) {
1695 return false;
1696 }
1697 let found = alias_exprs
1698 .get(name)
1699 .is_some_and(|expr| expr_contains_rrf_inner(expr, alias_exprs, seen_aliases));
1700 seen_aliases.remove(name);
1701 found
1702 }
1703 _ => false,
1704 }
1705}
1706
1707#[cfg(test)]
1708#[path = "typecheck_tests.rs"]
1709mod tests;