Skip to main content

eventql_parser/
analysis.rs

1use std::{
2    borrow::Cow,
3    collections::{BTreeMap, HashSet, btree_map::Entry},
4    mem,
5};
6
7use case_insensitive_hashmap::CaseInsensitiveHashMap;
8use serde::{Serialize, ser::SerializeMap};
9use unicase::Ascii;
10
11use crate::{
12    App, Attrs, Binary, Expr, Field, FunArgs, Query, Raw, Source, SourceKind, Type, Value,
13    error::AnalysisError, token::Operator,
14};
15
16/// Represents the state of a query that has been statically analyzed.
17///
18/// This type is used as a marker to indicate that a query has successfully
19/// passed static analysis. It contains metadata about the query's type
20/// information and variable scope after type checking.
21///
22/// All variables in a typed query are guaranteed to be:
23/// - Properly declared and in scope
24/// - Type-safe with sound type assignments
25#[derive(Debug, Clone, Serialize)]
26pub struct Typed {
27    /// The inferred type of the query's projection (PROJECT INTO clause).
28    ///
29    /// This represents the shape and types of the data that will be
30    /// returned by the query.
31    pub project: Type,
32
33    /// The variable scope after static analysis.
34    ///
35    /// Contains all variables that were in scope during type checking,
36    /// including bindings from FROM clauses and their associated types.
37    #[serde(skip)]
38    pub scope: Scope,
39
40    /// Indicates if the query uses aggregate functions.
41    pub aggregate: bool,
42}
43
44/// Result type for static analysis operations.
45///
46/// This is a convenience type alias for `Result<A, AnalysisError>` used throughout
47/// the static analysis module.
48pub type AnalysisResult<A> = std::result::Result<A, AnalysisError>;
49
50/// Configuration options for static analysis.
51///
52/// This structure contains the type information needed to perform static analysis
53/// on EventQL queries, including the default scope with built-in functions and
54/// the type information for event records.
55pub struct AnalysisOptions {
56    /// The default scope containing built-in functions and their type signatures.
57    pub default_scope: Scope,
58    /// Type information for event records being queried.
59    pub event_type_info: Type,
60    /// Custom types that are not defined in the EventQL reference.
61    ///
62    /// This set allows users to register custom type names that can be used
63    /// in type conversion expressions (e.g., `field AS CustomType`). Custom
64    /// type names are case-insensitive.
65    ///
66    /// # Examples
67    ///
68    /// ```
69    /// use eventql_parser::prelude::AnalysisOptions;
70    ///
71    /// let options = AnalysisOptions::default()
72    ///     .add_custom_type("Foobar");
73    /// ```
74    pub custom_types: HashSet<Ascii<String>>,
75}
76
77impl AnalysisOptions {
78    /// Adds a custom type name to the analysis options.
79    ///
80    /// Custom types allow you to use type conversion syntax with types that are
81    /// not part of the standard EventQL type system. The type name is stored
82    /// case-insensitively.
83    ///
84    /// # Arguments
85    ///
86    /// * `value` - The custom type name to register
87    ///
88    /// # Returns
89    ///
90    /// Returns `self` to allow for method chaining.
91    ///
92    /// # Examples
93    ///
94    /// ```
95    /// use eventql_parser::prelude::AnalysisOptions;
96    ///
97    /// let options = AnalysisOptions::default()
98    ///     .add_custom_type("Timestamp")
99    ///     .add_custom_type("UUID");
100    /// ```
101    pub fn add_custom_type<'a>(mut self, value: impl Into<Cow<'a, str>>) -> Self {
102        match value.into() {
103            Cow::Borrowed(t) => self.custom_types.insert(Ascii::new(t.to_owned())),
104            Cow::Owned(t) => self.custom_types.insert(Ascii::new(t)),
105        };
106
107        self
108    }
109}
110
111impl Default for AnalysisOptions {
112    fn default() -> Self {
113        Self {
114            default_scope: Scope {
115                entries: CaseInsensitiveHashMap::from_iter([
116                    (
117                        "ABS",
118                        Type::App {
119                            args: vec![Type::Number].into(),
120                            result: Box::new(Type::Number),
121                            aggregate: false,
122                        },
123                    ),
124                    (
125                        "CEIL",
126                        Type::App {
127                            args: vec![Type::Number].into(),
128                            result: Box::new(Type::Number),
129                            aggregate: false,
130                        },
131                    ),
132                    (
133                        "FLOOR",
134                        Type::App {
135                            args: vec![Type::Number].into(),
136                            result: Box::new(Type::Number),
137                            aggregate: false,
138                        },
139                    ),
140                    (
141                        "ROUND",
142                        Type::App {
143                            args: vec![Type::Number].into(),
144                            result: Box::new(Type::Number),
145                            aggregate: false,
146                        },
147                    ),
148                    (
149                        "COS",
150                        Type::App {
151                            args: vec![Type::Number].into(),
152                            result: Box::new(Type::Number),
153                            aggregate: false,
154                        },
155                    ),
156                    (
157                        "EXP",
158                        Type::App {
159                            args: vec![Type::Number].into(),
160                            result: Box::new(Type::Number),
161                            aggregate: false,
162                        },
163                    ),
164                    (
165                        "POW",
166                        Type::App {
167                            args: vec![Type::Number, Type::Number].into(),
168                            result: Box::new(Type::Number),
169                            aggregate: false,
170                        },
171                    ),
172                    (
173                        "SQRT",
174                        Type::App {
175                            args: vec![Type::Number].into(),
176                            result: Box::new(Type::Number),
177                            aggregate: false,
178                        },
179                    ),
180                    (
181                        "RAND",
182                        Type::App {
183                            args: vec![].into(),
184                            result: Box::new(Type::Number),
185                            aggregate: false,
186                        },
187                    ),
188                    (
189                        "PI",
190                        Type::App {
191                            args: vec![Type::Number].into(),
192                            result: Box::new(Type::Number),
193                            aggregate: false,
194                        },
195                    ),
196                    (
197                        "LOWER",
198                        Type::App {
199                            args: vec![Type::String].into(),
200                            result: Box::new(Type::String),
201                            aggregate: false,
202                        },
203                    ),
204                    (
205                        "UPPER",
206                        Type::App {
207                            args: vec![Type::String].into(),
208                            result: Box::new(Type::String),
209                            aggregate: false,
210                        },
211                    ),
212                    (
213                        "TRIM",
214                        Type::App {
215                            args: vec![Type::String].into(),
216                            result: Box::new(Type::String),
217                            aggregate: false,
218                        },
219                    ),
220                    (
221                        "LTRIM",
222                        Type::App {
223                            args: vec![Type::String].into(),
224                            result: Box::new(Type::String),
225                            aggregate: false,
226                        },
227                    ),
228                    (
229                        "RTRIM",
230                        Type::App {
231                            args: vec![Type::String].into(),
232                            result: Box::new(Type::String),
233                            aggregate: false,
234                        },
235                    ),
236                    (
237                        "LEN",
238                        Type::App {
239                            args: vec![Type::String].into(),
240                            result: Box::new(Type::Number),
241                            aggregate: false,
242                        },
243                    ),
244                    (
245                        "INSTR",
246                        Type::App {
247                            args: vec![Type::String].into(),
248                            result: Box::new(Type::Number),
249                            aggregate: false,
250                        },
251                    ),
252                    (
253                        "SUBSTRING",
254                        Type::App {
255                            args: vec![Type::String, Type::Number, Type::Number].into(),
256                            result: Box::new(Type::String),
257                            aggregate: false,
258                        },
259                    ),
260                    (
261                        "REPLACE",
262                        Type::App {
263                            args: vec![Type::String, Type::String, Type::String].into(),
264                            result: Box::new(Type::String),
265                            aggregate: false,
266                        },
267                    ),
268                    (
269                        "STARTSWITH",
270                        Type::App {
271                            args: vec![Type::String, Type::String].into(),
272                            result: Box::new(Type::Bool),
273                            aggregate: false,
274                        },
275                    ),
276                    (
277                        "ENDSWITH",
278                        Type::App {
279                            args: vec![Type::String, Type::String].into(),
280                            result: Box::new(Type::Bool),
281                            aggregate: false,
282                        },
283                    ),
284                    (
285                        "NOW",
286                        Type::App {
287                            args: vec![].into(),
288                            result: Box::new(Type::DateTime),
289                            aggregate: false,
290                        },
291                    ),
292                    (
293                        "YEAR",
294                        Type::App {
295                            args: vec![Type::Date].into(),
296                            result: Box::new(Type::Number),
297                            aggregate: false,
298                        },
299                    ),
300                    (
301                        "MONTH",
302                        Type::App {
303                            args: vec![Type::Date].into(),
304                            result: Box::new(Type::Number),
305                            aggregate: false,
306                        },
307                    ),
308                    (
309                        "DAY",
310                        Type::App {
311                            args: vec![Type::Date].into(),
312                            result: Box::new(Type::Number),
313                            aggregate: false,
314                        },
315                    ),
316                    (
317                        "HOUR",
318                        Type::App {
319                            args: vec![Type::Time].into(),
320                            result: Box::new(Type::Number),
321                            aggregate: false,
322                        },
323                    ),
324                    (
325                        "MINUTE",
326                        Type::App {
327                            args: vec![Type::Time].into(),
328                            result: Box::new(Type::Number),
329                            aggregate: false,
330                        },
331                    ),
332                    (
333                        "SECOND",
334                        Type::App {
335                            args: vec![Type::Time].into(),
336                            result: Box::new(Type::Number),
337                            aggregate: false,
338                        },
339                    ),
340                    (
341                        "WEEKDAY",
342                        Type::App {
343                            args: vec![Type::Date].into(),
344                            result: Box::new(Type::Number),
345                            aggregate: false,
346                        },
347                    ),
348                    (
349                        "IF",
350                        Type::App {
351                            args: vec![Type::Bool, Type::Unspecified, Type::Unspecified].into(),
352                            result: Box::new(Type::Unspecified),
353                            aggregate: false,
354                        },
355                    ),
356                    (
357                        "COUNT",
358                        Type::App {
359                            args: FunArgs {
360                                values: vec![Type::Bool],
361                                needed: 0,
362                            },
363                            result: Box::new(Type::Number),
364                            aggregate: true,
365                        },
366                    ),
367                    (
368                        "SUM",
369                        Type::App {
370                            args: vec![Type::Number].into(),
371                            result: Box::new(Type::Number),
372                            aggregate: true,
373                        },
374                    ),
375                    (
376                        "AVG",
377                        Type::App {
378                            args: vec![Type::Number].into(),
379                            result: Box::new(Type::Number),
380                            aggregate: true,
381                        },
382                    ),
383                    (
384                        "MIN",
385                        Type::App {
386                            args: vec![Type::Number].into(),
387                            result: Box::new(Type::Number),
388                            aggregate: true,
389                        },
390                    ),
391                    (
392                        "MAX",
393                        Type::App {
394                            args: vec![Type::Number].into(),
395                            result: Box::new(Type::Number),
396                            aggregate: true,
397                        },
398                    ),
399                    (
400                        "MEDIAN",
401                        Type::App {
402                            args: vec![Type::Number].into(),
403                            result: Box::new(Type::Number),
404                            aggregate: true,
405                        },
406                    ),
407                    (
408                        "STDDEV",
409                        Type::App {
410                            args: vec![Type::Number].into(),
411                            result: Box::new(Type::Number),
412                            aggregate: true,
413                        },
414                    ),
415                    (
416                        "VARIANCE",
417                        Type::App {
418                            args: vec![Type::Number].into(),
419                            result: Box::new(Type::Number),
420                            aggregate: true,
421                        },
422                    ),
423                    (
424                        "UNIQUE",
425                        Type::App {
426                            args: vec![Type::Unspecified].into(),
427                            result: Box::new(Type::Unspecified),
428                            aggregate: true,
429                        },
430                    ),
431                ]),
432            },
433            event_type_info: Type::Record(BTreeMap::from([
434                ("specversion".to_owned(), Type::String),
435                ("id".to_owned(), Type::String),
436                ("time".to_owned(), Type::DateTime),
437                ("source".to_owned(), Type::String),
438                ("subject".to_owned(), Type::Subject),
439                ("type".to_owned(), Type::String),
440                ("datacontenttype".to_owned(), Type::String),
441                ("data".to_owned(), Type::Unspecified),
442                ("predecessorhash".to_owned(), Type::String),
443                ("hash".to_owned(), Type::String),
444                ("traceparent".to_owned(), Type::String),
445                ("tracestate".to_owned(), Type::String),
446                ("signature".to_owned(), Type::String),
447            ])),
448            custom_types: HashSet::default(),
449        }
450    }
451}
452
453/// Performs static analysis on an EventQL query.
454///
455/// This function takes a raw (untyped) query and performs type checking and
456/// variable scoping analysis. It validates that:
457/// - All variables are properly declared
458/// - Types match expected types in expressions and operations
459/// - Field accesses are valid for their record types
460/// - Function calls have the correct argument types
461/// - Aggregate functions are only used in PROJECT INTO clauses
462/// - Aggregate functions are not mixed with source-bound fields in projections
463/// - Aggregate function arguments are source-bound fields (not constants or function results)
464/// - Record literals are non-empty in projection contexts
465///
466/// # Arguments
467///
468/// * `options` - Configuration containing type information and default scope
469/// * `query` - The raw query to analyze
470///
471/// # Returns
472///
473/// Returns a typed query on success, or an `AnalysisError` if type checking fails.
474pub fn static_analysis(
475    options: &AnalysisOptions,
476    query: Query<Raw>,
477) -> AnalysisResult<Query<Typed>> {
478    let mut analysis = Analysis::new(options);
479
480    analysis.analyze_query(query)
481}
482
483/// Represents a variable scope during static analysis.
484///
485/// A scope tracks the variables and their types that are currently in scope
486/// during type checking. This is used to resolve variable references and
487/// ensure type correctness.
488#[derive(Default, Clone, Debug)]
489pub struct Scope {
490    /// Map of variable names to their types.
491    pub entries: CaseInsensitiveHashMap<Type>,
492}
493
494impl Serialize for Scope {
495    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
496    where
497        S: serde::Serializer,
498    {
499        let mut map = serializer.serialize_map(Some(self.entries.len()))?;
500
501        for (key, value) in self.entries.iter() {
502            map.serialize_entry(key.as_str(), value)?;
503        }
504
505        map.end()
506    }
507}
508
509impl Scope {
510    /// Checks if the scope contains no entries.
511    pub fn is_empty(&self) -> bool {
512        self.entries.is_empty()
513    }
514}
515
516#[derive(Default)]
517struct CheckContext {
518    use_agg_func: bool,
519    use_source_based: bool,
520}
521
522/// Context for controlling analysis behavior.
523///
524/// This struct allows you to configure how expressions are analyzed,
525/// such as whether aggregate functions are allowed in the current context.
526#[derive(Default)]
527pub struct AnalysisContext {
528    /// Controls whether aggregate functions (like COUNT, SUM, AVG) are allowed
529    /// in the current analysis context.
530    ///
531    /// Set to `true` to allow aggregate functions, `false` to reject them.
532    /// Defaults to `false`.
533    pub allow_agg_func: bool,
534
535    /// Indicates if the query uses aggregate functions.
536    pub use_agg_funcs: bool,
537}
538
539/// A type checker and static analyzer for EventQL expressions.
540///
541/// This struct maintains the analysis state including scopes and type information.
542/// It can be used to perform type checking on individual expressions or entire queries.
543pub struct Analysis<'a> {
544    /// The analysis options containing type information for functions and event types.
545    options: &'a AnalysisOptions,
546    /// Stack of previous scopes for nested scope handling.
547    prev_scopes: Vec<Scope>,
548    /// The current scope containing variable bindings and their types.
549    scope: Scope,
550}
551
552impl<'a> Analysis<'a> {
553    /// Creates a new analysis instance with the given options.
554    pub fn new(options: &'a AnalysisOptions) -> Self {
555        Self {
556            options,
557            prev_scopes: Default::default(),
558            scope: Scope::default(),
559        }
560    }
561
562    /// Returns a reference to the current scope.
563    ///
564    /// The scope contains variable bindings and their types for the current
565    /// analysis context. Note that this only includes local variable bindings
566    /// and does not include global definitions such as built-in functions
567    /// (e.g., `COUNT`, `NOW`) or event type information, which are stored
568    /// in the `AnalysisOptions`.
569    pub fn scope(&self) -> &Scope {
570        &self.scope
571    }
572
573    /// Returns a mutable reference to the current scope.
574    ///
575    /// This allows you to modify the scope by adding or removing variable bindings.
576    /// This is useful when you need to set up custom type environments before
577    /// analyzing expressions. Note that this only provides access to local variable
578    /// bindings; global definitions like built-in functions are managed through
579    /// `AnalysisOptions` and cannot be modified via the scope.
580    pub fn scope_mut(&mut self) -> &mut Scope {
581        &mut self.scope
582    }
583
584    fn enter_scope(&mut self) {
585        if self.scope.is_empty() {
586            return;
587        }
588
589        let prev = mem::take(&mut self.scope);
590        self.prev_scopes.push(prev);
591    }
592
593    fn exit_scope(&mut self) -> Scope {
594        if let Some(prev) = self.prev_scopes.pop() {
595            mem::replace(&mut self.scope, prev)
596        } else {
597            mem::take(&mut self.scope)
598        }
599    }
600
601    /// Performs static analysis on a parsed query.
602    ///
603    /// This method analyzes an entire EventQL query, performing type checking on all
604    /// clauses including sources, predicates, group by, order by, and projections.
605    /// It returns a typed version of the query with type information attached.
606    ///
607    /// # Arguments
608    ///
609    /// * `query` - A parsed query in its raw (untyped) form
610    ///
611    /// # Returns
612    ///
613    /// Returns a typed query with all type information resolved, or an error if
614    /// type checking fails for any part of the query.
615    ///
616    /// # Example
617    ///
618    /// ```rust
619    /// use eventql_parser::{parse_query, prelude::{Analysis, AnalysisOptions}};
620    ///
621    /// let query = parse_query("FROM e IN events WHERE [1,2,3] CONTAINS e.data.price PROJECT INTO e").unwrap();
622    ///
623    /// let options = AnalysisOptions::default();
624    /// let mut analysis = Analysis::new(&options);
625    ///
626    /// let typed_query = analysis.analyze_query(query);
627    /// assert!(typed_query.is_ok());
628    /// ```
629    pub fn analyze_query(&mut self, query: Query<Raw>) -> AnalysisResult<Query<Typed>> {
630        self.enter_scope();
631
632        let mut sources = Vec::with_capacity(query.sources.len());
633        let mut ctx = AnalysisContext::default();
634
635        for source in query.sources {
636            sources.push(self.analyze_source(source)?);
637        }
638
639        if let Some(expr) = &query.predicate {
640            self.analyze_expr(&mut ctx, expr, Type::Bool)?;
641        }
642
643        if let Some(group_by) = &query.group_by {
644            if !matches!(&group_by.expr.value, Value::Access(_)) {
645                return Err(AnalysisError::ExpectFieldLiteral(
646                    group_by.expr.attrs.pos.line,
647                    group_by.expr.attrs.pos.col,
648                ));
649            }
650
651            self.analyze_expr(&mut ctx, &group_by.expr, Type::Unspecified)?;
652
653            if let Some(expr) = &group_by.predicate {
654                self.analyze_expr(&mut ctx, expr, Type::Bool)?;
655            }
656        }
657
658        if let Some(order_by) = &query.order_by {
659            if !matches!(&order_by.expr.value, Value::Access(_)) {
660                return Err(AnalysisError::ExpectFieldLiteral(
661                    order_by.expr.attrs.pos.line,
662                    order_by.expr.attrs.pos.col,
663                ));
664            }
665            self.analyze_expr(&mut ctx, &order_by.expr, Type::Unspecified)?;
666        }
667
668        let project = self.analyze_projection(&mut ctx, &query.projection)?;
669        let scope = self.exit_scope();
670
671        Ok(Query {
672            attrs: query.attrs,
673            sources,
674            predicate: query.predicate,
675            group_by: query.group_by,
676            order_by: query.order_by,
677            limit: query.limit,
678            projection: query.projection,
679            distinct: query.distinct,
680            meta: Typed {
681                project,
682                scope,
683                aggregate: ctx.use_agg_funcs,
684            },
685        })
686    }
687
688    fn analyze_source(&mut self, source: Source<Raw>) -> AnalysisResult<Source<Typed>> {
689        let kind = self.analyze_source_kind(source.kind)?;
690        let tpe = match &kind {
691            SourceKind::Name(_) | SourceKind::Subject(_) => self.options.event_type_info.clone(),
692            SourceKind::Subquery(query) => self.projection_type(query),
693        };
694
695        if self
696            .scope
697            .entries
698            .insert(source.binding.name.clone(), tpe)
699            .is_some()
700        {
701            return Err(AnalysisError::BindingAlreadyExists(
702                source.binding.pos.line,
703                source.binding.pos.col,
704                source.binding.name,
705            ));
706        }
707
708        Ok(Source {
709            binding: source.binding,
710            kind,
711        })
712    }
713
714    fn analyze_source_kind(&mut self, kind: SourceKind<Raw>) -> AnalysisResult<SourceKind<Typed>> {
715        match kind {
716            SourceKind::Name(n) => Ok(SourceKind::Name(n)),
717            SourceKind::Subject(s) => Ok(SourceKind::Subject(s)),
718            SourceKind::Subquery(query) => {
719                let query = self.analyze_query(*query)?;
720                Ok(SourceKind::Subquery(Box::new(query)))
721            }
722        }
723    }
724
725    fn analyze_projection(
726        &mut self,
727        ctx: &mut AnalysisContext,
728        expr: &Expr,
729    ) -> AnalysisResult<Type> {
730        match &expr.value {
731            Value::Record(record) => {
732                if record.is_empty() {
733                    return Err(AnalysisError::EmptyRecord(
734                        expr.attrs.pos.line,
735                        expr.attrs.pos.col,
736                    ));
737                }
738
739                ctx.allow_agg_func = true;
740                let tpe = self.analyze_expr(ctx, expr, Type::Unspecified)?;
741                self.check_projection_on_record(&mut CheckContext::default(), record.as_slice())?;
742                Ok(tpe)
743            }
744
745            Value::App(app) => {
746                ctx.allow_agg_func = true;
747
748                let tpe = self.analyze_expr(ctx, expr, Type::Unspecified)?;
749
750                if ctx.use_agg_funcs {
751                    self.check_projection_on_field_expr(&mut CheckContext::default(), expr)?;
752                } else {
753                    self.reject_constant_func(&expr.attrs, app)?;
754                }
755
756                Ok(tpe)
757            }
758
759            Value::Id(id) => {
760                if let Some(tpe) = self.scope.entries.get(id.as_str()).cloned() {
761                    Ok(tpe)
762                } else {
763                    Err(AnalysisError::VariableUndeclared(
764                        expr.attrs.pos.line,
765                        expr.attrs.pos.col,
766                        id.clone(),
767                    ))
768                }
769            }
770
771            Value::Access(access) => {
772                let mut current = &access.target.value;
773
774                loop {
775                    match current {
776                        Value::Id(name) => {
777                            if !self.scope.entries.contains_key(name.as_str()) {
778                                return Err(AnalysisError::VariableUndeclared(
779                                    expr.attrs.pos.line,
780                                    expr.attrs.pos.col,
781                                    name.clone(),
782                                ));
783                            }
784
785                            break;
786                        }
787
788                        Value::Access(next) => current = &next.target.value,
789                        _ => unreachable!(),
790                    }
791                }
792
793                self.analyze_expr(ctx, expr, Type::Unspecified)
794            }
795
796            _ => Err(AnalysisError::ExpectRecordOrSourcedProperty(
797                expr.attrs.pos.line,
798                expr.attrs.pos.col,
799                self.project_type(&expr.value),
800            )),
801        }
802    }
803
804    fn check_projection_on_record(
805        &mut self,
806        ctx: &mut CheckContext,
807        record: &[Field],
808    ) -> AnalysisResult<()> {
809        for field in record {
810            self.check_projection_on_field(ctx, field)?;
811        }
812
813        Ok(())
814    }
815
816    fn check_projection_on_field(
817        &mut self,
818        ctx: &mut CheckContext,
819        field: &Field,
820    ) -> AnalysisResult<()> {
821        self.check_projection_on_field_expr(ctx, &field.value)
822    }
823
824    fn check_projection_on_field_expr(
825        &mut self,
826        ctx: &mut CheckContext,
827        expr: &Expr,
828    ) -> AnalysisResult<()> {
829        match &expr.value {
830            Value::Number(_) | Value::String(_) | Value::Bool(_) => Ok(()),
831
832            Value::Id(id) => {
833                if self.scope.entries.contains_key(id.as_str()) {
834                    if ctx.use_agg_func {
835                        return Err(AnalysisError::UnallowedAggFuncUsageWithSrcField(
836                            expr.attrs.pos.line,
837                            expr.attrs.pos.col,
838                        ));
839                    }
840
841                    ctx.use_source_based = true;
842                }
843
844                Ok(())
845            }
846
847            Value::Array(exprs) => {
848                for expr in exprs {
849                    self.check_projection_on_field_expr(ctx, expr)?;
850                }
851
852                Ok(())
853            }
854
855            Value::Record(fields) => {
856                for field in fields {
857                    self.check_projection_on_field(ctx, field)?;
858                }
859
860                Ok(())
861            }
862
863            Value::Access(access) => self.check_projection_on_field_expr(ctx, &access.target),
864
865            Value::App(app) => {
866                if let Some(Type::App { aggregate, .. }) =
867                    self.options.default_scope.entries.get(app.func.as_str())
868                {
869                    ctx.use_agg_func |= *aggregate;
870
871                    if ctx.use_agg_func && ctx.use_source_based {
872                        return Err(AnalysisError::UnallowedAggFuncUsageWithSrcField(
873                            expr.attrs.pos.line,
874                            expr.attrs.pos.col,
875                        ));
876                    }
877
878                    for arg in &app.args {
879                        if *aggregate {
880                            self.ensure_agg_param_is_source_bound(arg)?;
881                        }
882
883                        self.invalidate_agg_func_usage(arg)?;
884                    }
885                }
886
887                Ok(())
888            }
889
890            Value::Binary(binary) => {
891                self.check_projection_on_field_expr(ctx, &binary.lhs)?;
892                self.check_projection_on_field_expr(ctx, &binary.rhs)
893            }
894
895            Value::Unary(unary) => self.check_projection_on_field_expr(ctx, &unary.expr),
896            Value::Group(expr) => self.check_projection_on_field_expr(ctx, expr),
897        }
898    }
899
900    fn ensure_agg_param_is_source_bound(&mut self, expr: &Expr) -> AnalysisResult<()> {
901        match &expr.value {
902            Value::Id(id) if !self.options.default_scope.entries.contains_key(id.as_str()) => {
903                Ok(())
904            }
905            Value::Access(access) => self.ensure_agg_param_is_source_bound(&access.target),
906            Value::Binary(binary) => self.ensure_agg_binary_op_is_source_bound(&expr.attrs, binary),
907            Value::Unary(unary) => self.ensure_agg_param_is_source_bound(&unary.expr),
908
909            _ => Err(AnalysisError::ExpectSourceBoundProperty(
910                expr.attrs.pos.line,
911                expr.attrs.pos.col,
912            )),
913        }
914    }
915
916    fn ensure_agg_binary_op_is_source_bound(
917        &mut self,
918        attrs: &Attrs,
919        binary: &Binary,
920    ) -> AnalysisResult<()> {
921        if !self.ensure_agg_binary_op_branch_is_source_bound(&binary.lhs)
922            && !self.ensure_agg_binary_op_branch_is_source_bound(&binary.rhs)
923        {
924            return Err(AnalysisError::ExpectSourceBoundProperty(
925                attrs.pos.line,
926                attrs.pos.col,
927            ));
928        }
929
930        Ok(())
931    }
932
933    fn ensure_agg_binary_op_branch_is_source_bound(&mut self, expr: &Expr) -> bool {
934        match &expr.value {
935            Value::Id(id) => !self.options.default_scope.entries.contains_key(id.as_str()),
936            Value::Array(exprs) => {
937                if exprs.is_empty() {
938                    return false;
939                }
940
941                exprs
942                    .iter()
943                    .all(|expr| self.ensure_agg_binary_op_branch_is_source_bound(expr))
944            }
945            Value::Record(fields) => {
946                if fields.is_empty() {
947                    return false;
948                }
949
950                fields
951                    .iter()
952                    .all(|field| self.ensure_agg_binary_op_branch_is_source_bound(&field.value))
953            }
954            Value::Access(access) => {
955                self.ensure_agg_binary_op_branch_is_source_bound(&access.target)
956            }
957
958            Value::Binary(binary) => self
959                .ensure_agg_binary_op_is_source_bound(&expr.attrs, binary)
960                .is_ok(),
961            Value::Unary(unary) => self.ensure_agg_binary_op_branch_is_source_bound(&unary.expr),
962            Value::Group(expr) => self.ensure_agg_binary_op_branch_is_source_bound(expr),
963
964            Value::Number(_) | Value::String(_) | Value::Bool(_) | Value::App(_) => false,
965        }
966    }
967
968    fn invalidate_agg_func_usage(&mut self, expr: &Expr) -> AnalysisResult<()> {
969        match &expr.value {
970            Value::Number(_)
971            | Value::String(_)
972            | Value::Bool(_)
973            | Value::Id(_)
974            | Value::Access(_) => Ok(()),
975
976            Value::Array(exprs) => {
977                for expr in exprs {
978                    self.invalidate_agg_func_usage(expr)?;
979                }
980
981                Ok(())
982            }
983
984            Value::Record(fields) => {
985                for field in fields {
986                    self.invalidate_agg_func_usage(&field.value)?;
987                }
988
989                Ok(())
990            }
991
992            Value::App(app) => {
993                if let Some(Type::App { aggregate, .. }) =
994                    self.options.default_scope.entries.get(app.func.as_str())
995                    && *aggregate
996                {
997                    return Err(AnalysisError::WrongAggFunUsage(
998                        expr.attrs.pos.line,
999                        expr.attrs.pos.col,
1000                        app.func.clone(),
1001                    ));
1002                }
1003
1004                for arg in &app.args {
1005                    self.invalidate_agg_func_usage(arg)?;
1006                }
1007
1008                Ok(())
1009            }
1010
1011            Value::Binary(binary) => {
1012                self.invalidate_agg_func_usage(&binary.lhs)?;
1013                self.invalidate_agg_func_usage(&binary.rhs)
1014            }
1015
1016            Value::Unary(unary) => self.invalidate_agg_func_usage(&unary.expr),
1017            Value::Group(expr) => self.invalidate_agg_func_usage(expr),
1018        }
1019    }
1020
1021    fn reject_constant_func(&self, attrs: &Attrs, app: &App) -> AnalysisResult<()> {
1022        if app.args.is_empty() {
1023            return Err(AnalysisError::ConstantExprInProjectIntoClause(
1024                attrs.pos.line,
1025                attrs.pos.col,
1026            ));
1027        }
1028
1029        let mut errored = None;
1030        for arg in &app.args {
1031            if let Err(e) = self.reject_constant_expr(arg) {
1032                if errored.is_none() {
1033                    errored = Some(e);
1034                }
1035
1036                continue;
1037            }
1038
1039            // if at least one arg is sourced-bound is ok
1040            return Ok(());
1041        }
1042
1043        Err(errored.expect("to be defined at that point"))
1044    }
1045
1046    fn reject_constant_expr(&self, expr: &Expr) -> AnalysisResult<()> {
1047        match &expr.value {
1048            Value::Id(id) if self.scope.entries.contains_key(id.as_str()) => Ok(()),
1049
1050            Value::Array(exprs) => {
1051                let mut errored = None;
1052                for expr in exprs {
1053                    if let Err(e) = self.reject_constant_expr(expr) {
1054                        if errored.is_none() {
1055                            errored = Some(e);
1056                        }
1057
1058                        continue;
1059                    }
1060
1061                    // if at least one arg is sourced-bound is ok
1062                    return Ok(());
1063                }
1064
1065                Err(errored.expect("to be defined at that point"))
1066            }
1067
1068            Value::Record(fields) => {
1069                let mut errored = None;
1070                for field in fields {
1071                    if let Err(e) = self.reject_constant_expr(&field.value) {
1072                        if errored.is_none() {
1073                            errored = Some(e);
1074                        }
1075
1076                        continue;
1077                    }
1078
1079                    // if at least one arg is sourced-bound is ok
1080                    return Ok(());
1081                }
1082
1083                Err(errored.expect("to be defined at that point"))
1084            }
1085
1086            Value::Binary(binary) => self
1087                .reject_constant_expr(&binary.lhs)
1088                .or_else(|e| self.reject_constant_expr(&binary.rhs).map_err(|_| e)),
1089
1090            Value::Access(access) => self.reject_constant_expr(access.target.as_ref()),
1091            Value::App(app) => self.reject_constant_func(&expr.attrs, app),
1092            Value::Unary(unary) => self.reject_constant_expr(&unary.expr),
1093            Value::Group(expr) => self.reject_constant_expr(expr),
1094
1095            _ => Err(AnalysisError::ConstantExprInProjectIntoClause(
1096                expr.attrs.pos.line,
1097                expr.attrs.pos.col,
1098            )),
1099        }
1100    }
1101
1102    /// Analyzes an expression and checks it against an expected type.
1103    ///
1104    /// This method performs type checking on an expression, verifying that all operations
1105    /// are type-safe and that the expression's type is compatible with the expected type.
1106    ///
1107    /// # Arguments
1108    ///
1109    /// * `ctx` - The analysis context controlling analysis behavior
1110    /// * `expr` - The expression to analyze
1111    /// * `expect` - The expected type of the expression
1112    ///
1113    /// # Returns
1114    ///
1115    /// Returns the actual type of the expression after checking compatibility with the expected type,
1116    /// or an error if type checking fails.
1117    ///
1118    /// # Example
1119    ///
1120    /// ```rust
1121    /// use eventql_parser::prelude::{tokenize, Parser, Analysis, AnalysisContext, AnalysisOptions, Type};
1122    ///
1123    /// let tokens = tokenize("1 + 2").unwrap();
1124    /// let expr = Parser::new(tokens.as_slice()).parse_expr().unwrap();
1125    /// let options = AnalysisOptions::default();
1126    /// let mut analysis = Analysis::new(&options);
1127    ///
1128    /// let result = analysis.analyze_expr(&AnalysisContext::default(), &expr, Type::Number);
1129    /// assert!(result.is_ok());
1130    /// ```
1131    pub fn analyze_expr(
1132        &mut self,
1133        ctx: &mut AnalysisContext,
1134        expr: &Expr,
1135        mut expect: Type,
1136    ) -> AnalysisResult<Type> {
1137        match &expr.value {
1138            Value::Number(_) => expect.check(&expr.attrs, Type::Number),
1139            Value::String(_) => expect.check(&expr.attrs, Type::String),
1140            Value::Bool(_) => expect.check(&expr.attrs, Type::Bool),
1141
1142            Value::Id(id) => {
1143                if let Some(tpe) = self.options.default_scope.entries.get(id.as_str()) {
1144                    expect.check(&expr.attrs, tpe.clone())
1145                } else if let Some(tpe) = self.scope.entries.get_mut(id.as_str()) {
1146                    let tmp = mem::take(tpe);
1147                    *tpe = tmp.check(&expr.attrs, expect)?;
1148
1149                    Ok(tpe.clone())
1150                } else {
1151                    Err(AnalysisError::VariableUndeclared(
1152                        expr.attrs.pos.line,
1153                        expr.attrs.pos.col,
1154                        id.to_owned(),
1155                    ))
1156                }
1157            }
1158
1159            Value::Array(exprs) => {
1160                if matches!(expect, Type::Unspecified) {
1161                    for expr in exprs {
1162                        expect = self.analyze_expr(ctx, expr, expect)?;
1163                    }
1164
1165                    return Ok(Type::Array(Box::new(expect)));
1166                }
1167
1168                match expect {
1169                    Type::Array(mut expect) => {
1170                        for expr in exprs {
1171                            *expect = self.analyze_expr(ctx, expr, expect.as_ref().clone())?;
1172                        }
1173
1174                        Ok(Type::Array(expect))
1175                    }
1176
1177                    expect => Err(AnalysisError::TypeMismatch(
1178                        expr.attrs.pos.line,
1179                        expr.attrs.pos.col,
1180                        expect,
1181                        self.project_type(&expr.value),
1182                    )),
1183                }
1184            }
1185
1186            Value::Record(fields) => {
1187                if matches!(expect, Type::Unspecified) {
1188                    let mut record = BTreeMap::new();
1189
1190                    for field in fields {
1191                        record.insert(
1192                            field.name.clone(),
1193                            self.analyze_expr(ctx, &field.value, Type::Unspecified)?,
1194                        );
1195                    }
1196
1197                    return Ok(Type::Record(record));
1198                }
1199
1200                match expect {
1201                    Type::Record(mut types) if fields.len() == types.len() => {
1202                        for field in fields {
1203                            if let Some(tpe) = types.remove(field.name.as_str()) {
1204                                types.insert(
1205                                    field.name.clone(),
1206                                    self.analyze_expr(ctx, &field.value, tpe)?,
1207                                );
1208                            } else {
1209                                return Err(AnalysisError::FieldUndeclared(
1210                                    expr.attrs.pos.line,
1211                                    expr.attrs.pos.col,
1212                                    field.name.clone(),
1213                                ));
1214                            }
1215                        }
1216
1217                        Ok(Type::Record(types))
1218                    }
1219
1220                    expect => Err(AnalysisError::TypeMismatch(
1221                        expr.attrs.pos.line,
1222                        expr.attrs.pos.col,
1223                        expect,
1224                        self.project_type(&expr.value),
1225                    )),
1226                }
1227            }
1228
1229            this @ Value::Access(_) => Ok(self.analyze_access(&expr.attrs, this, expect)?),
1230
1231            Value::App(app) => {
1232                if let Some(tpe) = self.options.default_scope.entries.get(app.func.as_str())
1233                    && let Type::App {
1234                        args,
1235                        result,
1236                        aggregate,
1237                    } = tpe
1238                {
1239                    if !args.match_arg_count(app.args.len()) {
1240                        return Err(AnalysisError::FunWrongArgumentCount(
1241                            expr.attrs.pos.line,
1242                            expr.attrs.pos.col,
1243                            app.func.clone(),
1244                        ));
1245                    }
1246
1247                    if *aggregate && !ctx.allow_agg_func {
1248                        return Err(AnalysisError::WrongAggFunUsage(
1249                            expr.attrs.pos.line,
1250                            expr.attrs.pos.col,
1251                            app.func.clone(),
1252                        ));
1253                    }
1254
1255                    if *aggregate && ctx.allow_agg_func {
1256                        ctx.use_agg_funcs = true;
1257                    }
1258
1259                    for (arg, tpe) in app.args.iter().zip(args.values.iter().cloned()) {
1260                        self.analyze_expr(ctx, arg, tpe)?;
1261                    }
1262
1263                    if matches!(expect, Type::Unspecified) {
1264                        Ok(result.as_ref().clone())
1265                    } else {
1266                        expect.check(&expr.attrs, result.as_ref().clone())
1267                    }
1268                } else {
1269                    Err(AnalysisError::FuncUndeclared(
1270                        expr.attrs.pos.line,
1271                        expr.attrs.pos.col,
1272                        app.func.clone(),
1273                    ))
1274                }
1275            }
1276
1277            Value::Binary(binary) => match binary.operator {
1278                Operator::Add | Operator::Sub | Operator::Mul | Operator::Div => {
1279                    self.analyze_expr(ctx, &binary.lhs, Type::Number)?;
1280                    self.analyze_expr(ctx, &binary.rhs, Type::Number)?;
1281                    expect.check(&expr.attrs, Type::Number)
1282                }
1283
1284                Operator::Eq
1285                | Operator::Neq
1286                | Operator::Lt
1287                | Operator::Lte
1288                | Operator::Gt
1289                | Operator::Gte => {
1290                    let lhs_expect = self.analyze_expr(ctx, &binary.lhs, Type::Unspecified)?;
1291                    let rhs_expect = self.analyze_expr(ctx, &binary.rhs, lhs_expect.clone())?;
1292
1293                    // If the left side didn't have enough type information while the other did,
1294                    // we replay another typecheck pass on the left side if the right side was conclusive
1295                    if matches!(lhs_expect, Type::Unspecified)
1296                        && !matches!(rhs_expect, Type::Unspecified)
1297                    {
1298                        self.analyze_expr(ctx, &binary.lhs, rhs_expect)?;
1299                    }
1300
1301                    expect.check(&expr.attrs, Type::Bool)
1302                }
1303
1304                Operator::Contains => {
1305                    let lhs_expect = self.analyze_expr(
1306                        ctx,
1307                        &binary.lhs,
1308                        Type::Array(Box::new(Type::Unspecified)),
1309                    )?;
1310
1311                    let lhs_assumption = match lhs_expect {
1312                        Type::Array(inner) => *inner,
1313                        other => {
1314                            return Err(AnalysisError::ExpectArray(
1315                                expr.attrs.pos.line,
1316                                expr.attrs.pos.col,
1317                                other,
1318                            ));
1319                        }
1320                    };
1321
1322                    let rhs_expect = self.analyze_expr(ctx, &binary.rhs, lhs_assumption.clone())?;
1323
1324                    // If the left side didn't have enough type information while the other did,
1325                    // we replay another typecheck pass on the left side if the right side was conclusive
1326                    if matches!(lhs_assumption, Type::Unspecified)
1327                        && !matches!(rhs_expect, Type::Unspecified)
1328                    {
1329                        self.analyze_expr(ctx, &binary.lhs, Type::Array(Box::new(rhs_expect)))?;
1330                    }
1331
1332                    expect.check(&expr.attrs, Type::Bool)
1333                }
1334
1335                Operator::And | Operator::Or | Operator::Xor => {
1336                    self.analyze_expr(ctx, &binary.lhs, Type::Bool)?;
1337                    self.analyze_expr(ctx, &binary.rhs, Type::Bool)?;
1338
1339                    expect.check(&expr.attrs, Type::Bool)
1340                }
1341
1342                Operator::As => {
1343                    if let Value::Id(name) = &binary.rhs.value {
1344                        if let Some(tpe) = name_to_type(self.options, name) {
1345                            // NOTE - we could check if it's safe to convert the left branch to that type
1346                            return Ok(tpe);
1347                        } else {
1348                            return Err(AnalysisError::UnsupportedCustomType(
1349                                expr.attrs.pos.line,
1350                                expr.attrs.pos.col,
1351                                name.clone(),
1352                            ));
1353                        }
1354                    }
1355
1356                    unreachable!(
1357                        "we already made sure during parsing that we can only have an ID symbol at this point"
1358                    )
1359                }
1360
1361                Operator::Not => unreachable!(),
1362            },
1363
1364            Value::Unary(unary) => match unary.operator {
1365                Operator::Add | Operator::Sub => {
1366                    self.analyze_expr(ctx, &unary.expr, Type::Number)?;
1367                    expect.check(&expr.attrs, Type::Number)
1368                }
1369
1370                Operator::Not => {
1371                    self.analyze_expr(ctx, &unary.expr, Type::Bool)?;
1372                    expect.check(&expr.attrs, Type::Bool)
1373                }
1374
1375                _ => unreachable!(),
1376            },
1377
1378            Value::Group(expr) => Ok(self.analyze_expr(ctx, expr.as_ref(), expect)?),
1379        }
1380    }
1381
1382    fn analyze_access(
1383        &mut self,
1384        attrs: &Attrs,
1385        access: &Value,
1386        expect: Type,
1387    ) -> AnalysisResult<Type> {
1388        struct State<A, B> {
1389            depth: u8,
1390            /// When true means we are into dynamically type object.
1391            dynamic: bool,
1392            definition: Def<A, B>,
1393        }
1394
1395        impl<A, B> State<A, B> {
1396            fn new(definition: Def<A, B>) -> Self {
1397                Self {
1398                    depth: 0,
1399                    dynamic: false,
1400                    definition,
1401                }
1402            }
1403        }
1404
1405        enum Def<A, B> {
1406            User(A),
1407            System(B),
1408        }
1409
1410        fn go<'a>(
1411            scope: &'a mut Scope,
1412            sys: &'a AnalysisOptions,
1413            attrs: &'a Attrs,
1414            value: &'a Value,
1415        ) -> AnalysisResult<State<&'a mut Type, &'a Type>> {
1416            match value {
1417                Value::Id(id) => {
1418                    if let Some(tpe) = sys.default_scope.entries.get(id.as_str()) {
1419                        Ok(State::new(Def::System(tpe)))
1420                    } else if let Some(tpe) = scope.entries.get_mut(id.as_str()) {
1421                        Ok(State::new(Def::User(tpe)))
1422                    } else {
1423                        Err(AnalysisError::VariableUndeclared(
1424                            attrs.pos.line,
1425                            attrs.pos.col,
1426                            id.clone(),
1427                        ))
1428                    }
1429                }
1430                Value::Access(access) => {
1431                    let mut state = go(scope, sys, &access.target.attrs, &access.target.value)?;
1432
1433                    // TODO - we should consider make that field and depth configurable.
1434                    let is_data_field = state.depth == 0 && access.field == "data";
1435
1436                    // TODO - we should consider make that behavior configurable.
1437                    // the `data` property is where the JSON payload is located, which means
1438                    // we should be lax if a property is not defined yet.
1439                    if !state.dynamic && is_data_field {
1440                        state.dynamic = true;
1441                    }
1442
1443                    match state.definition {
1444                        Def::User(tpe) => {
1445                            if matches!(tpe, Type::Unspecified) && state.dynamic {
1446                                *tpe = Type::Record(BTreeMap::from([(
1447                                    access.field.clone(),
1448                                    Type::Unspecified,
1449                                )]));
1450                                return Ok(State {
1451                                    depth: state.depth + 1,
1452                                    definition: Def::User(
1453                                        tpe.as_record_or_panic_mut()
1454                                            .get_mut(access.field.as_str())
1455                                            .unwrap(),
1456                                    ),
1457                                    ..state
1458                                });
1459                            }
1460
1461                            if let Type::Record(fields) = tpe {
1462                                match fields.entry(access.field.clone()) {
1463                                    Entry::Vacant(entry) => {
1464                                        if state.dynamic || is_data_field {
1465                                            return Ok(State {
1466                                                depth: state.depth + 1,
1467                                                definition: Def::User(
1468                                                    entry.insert(Type::Unspecified),
1469                                                ),
1470                                                ..state
1471                                            });
1472                                        }
1473
1474                                        return Err(AnalysisError::FieldUndeclared(
1475                                            attrs.pos.line,
1476                                            attrs.pos.col,
1477                                            access.field.clone(),
1478                                        ));
1479                                    }
1480
1481                                    Entry::Occupied(entry) => {
1482                                        return Ok(State {
1483                                            depth: state.depth + 1,
1484                                            definition: Def::User(entry.into_mut()),
1485                                            ..state
1486                                        });
1487                                    }
1488                                }
1489                            }
1490
1491                            Err(AnalysisError::ExpectRecord(
1492                                attrs.pos.line,
1493                                attrs.pos.col,
1494                                tpe.clone(),
1495                            ))
1496                        }
1497
1498                        Def::System(tpe) => {
1499                            if matches!(tpe, Type::Unspecified) && state.dynamic {
1500                                return Ok(State {
1501                                    depth: state.depth + 1,
1502                                    definition: Def::System(&Type::Unspecified),
1503                                    ..state
1504                                });
1505                            }
1506
1507                            if let Type::Record(fields) = tpe {
1508                                if let Some(field) = fields.get(access.field.as_str()) {
1509                                    return Ok(State {
1510                                        depth: state.depth + 1,
1511                                        definition: Def::System(field),
1512                                        ..state
1513                                    });
1514                                }
1515
1516                                return Err(AnalysisError::FieldUndeclared(
1517                                    attrs.pos.line,
1518                                    attrs.pos.col,
1519                                    access.field.clone(),
1520                                ));
1521                            }
1522
1523                            Err(AnalysisError::ExpectRecord(
1524                                attrs.pos.line,
1525                                attrs.pos.col,
1526                                tpe.clone(),
1527                            ))
1528                        }
1529                    }
1530                }
1531                Value::Number(_)
1532                | Value::String(_)
1533                | Value::Bool(_)
1534                | Value::Array(_)
1535                | Value::Record(_)
1536                | Value::App(_)
1537                | Value::Binary(_)
1538                | Value::Unary(_)
1539                | Value::Group(_) => unreachable!(),
1540            }
1541        }
1542
1543        let state = go(&mut self.scope, self.options, attrs, access)?;
1544
1545        match state.definition {
1546            Def::User(tpe) => {
1547                let tmp = mem::take(tpe);
1548                *tpe = tmp.check(attrs, expect)?;
1549
1550                Ok(tpe.clone())
1551            }
1552
1553            Def::System(tpe) => tpe.clone().check(attrs, expect),
1554        }
1555    }
1556
1557    fn projection_type(&self, query: &Query<Typed>) -> Type {
1558        self.project_type(&query.projection.value)
1559    }
1560
1561    fn project_type(&self, value: &Value) -> Type {
1562        match value {
1563            Value::Number(_) => Type::Number,
1564            Value::String(_) => Type::String,
1565            Value::Bool(_) => Type::Bool,
1566            Value::Id(id) => {
1567                if let Some(tpe) = self.options.default_scope.entries.get(id.as_str()) {
1568                    tpe.clone()
1569                } else if let Some(tpe) = self.scope.entries.get(id.as_str()) {
1570                    tpe.clone()
1571                } else {
1572                    Type::Unspecified
1573                }
1574            }
1575            Value::Array(exprs) => {
1576                let mut project = Type::Unspecified;
1577
1578                for expr in exprs {
1579                    let tmp = self.project_type(&expr.value);
1580
1581                    if !matches!(tmp, Type::Unspecified) {
1582                        project = tmp;
1583                        break;
1584                    }
1585                }
1586
1587                Type::Array(Box::new(project))
1588            }
1589            Value::Record(fields) => Type::Record(
1590                fields
1591                    .iter()
1592                    .map(|field| (field.name.clone(), self.project_type(&field.value.value)))
1593                    .collect(),
1594            ),
1595            Value::Access(access) => {
1596                let tpe = self.project_type(&access.target.value);
1597                if let Type::Record(fields) = tpe {
1598                    fields
1599                        .get(access.field.as_str())
1600                        .cloned()
1601                        .unwrap_or_default()
1602                } else {
1603                    Type::Unspecified
1604                }
1605            }
1606            Value::App(app) => self
1607                .options
1608                .default_scope
1609                .entries
1610                .get(app.func.as_str())
1611                .cloned()
1612                .unwrap_or_default(),
1613            Value::Binary(binary) => match binary.operator {
1614                Operator::Add | Operator::Sub | Operator::Mul | Operator::Div => Type::Number,
1615                Operator::As => {
1616                    if let Value::Id(n) = &binary.rhs.as_ref().value
1617                        && let Some(tpe) = name_to_type(self.options, n.as_str())
1618                    {
1619                        tpe
1620                    } else {
1621                        Type::Unspecified
1622                    }
1623                }
1624                Operator::Eq
1625                | Operator::Neq
1626                | Operator::Lt
1627                | Operator::Lte
1628                | Operator::Gt
1629                | Operator::Gte
1630                | Operator::And
1631                | Operator::Or
1632                | Operator::Xor
1633                | Operator::Not
1634                | Operator::Contains => Type::Bool,
1635            },
1636            Value::Unary(unary) => match unary.operator {
1637                Operator::Add | Operator::Sub => Type::Number,
1638                Operator::Mul
1639                | Operator::Div
1640                | Operator::Eq
1641                | Operator::Neq
1642                | Operator::Lt
1643                | Operator::Lte
1644                | Operator::Gt
1645                | Operator::Gte
1646                | Operator::And
1647                | Operator::Or
1648                | Operator::Xor
1649                | Operator::Not
1650                | Operator::Contains
1651                | Operator::As => unreachable!(),
1652            },
1653            Value::Group(expr) => self.project_type(&expr.value),
1654        }
1655    }
1656}
1657
1658/// Converts a type name string to its corresponding [`Type`] variant.
1659///
1660/// This function performs case-insensitive matching for built-in type names and checks
1661/// against custom types defined in the analysis options.
1662///
1663/// # Returns
1664///
1665/// * `Some(Type)` - If the name matches a built-in type or custom type
1666/// * `None` - If the name doesn't match any known type
1667///
1668/// # Built-in Type Mappings
1669///
1670/// The following type names are recognized (case-insensitive):
1671/// - `"string"` → [`Type::String`]
1672/// - `"int"` or `"float64"` → [`Type::Number`]
1673/// - `"boolean"` → [`Type::Bool`]
1674/// - `"date"` → [`Type::Date`]
1675/// - `"time"` → [`Type::Time`]
1676/// - `"datetime"` → [`Type::DateTime`]
1677///
1678/// # Examples
1679///
1680/// ```ignore
1681/// let opts = AnalysisOptions::default();
1682/// assert_eq!(name_to_type(&opts, "String"), Some(Type::String));
1683/// assert_eq!(name_to_type(&opts, "INT"), Some(Type::Number));
1684/// assert_eq!(name_to_type(&opts, "unknown"), None);
1685/// ```
1686pub fn name_to_type(opts: &AnalysisOptions, name: &str) -> Option<Type> {
1687    if name.eq_ignore_ascii_case("string") {
1688        Some(Type::String)
1689    } else if name.eq_ignore_ascii_case("int") || name.eq_ignore_ascii_case("float64") {
1690        Some(Type::Number)
1691    } else if name.eq_ignore_ascii_case("boolean") {
1692        Some(Type::Bool)
1693    } else if name.eq_ignore_ascii_case("date") {
1694        Some(Type::Date)
1695    } else if name.eq_ignore_ascii_case("time") {
1696        Some(Type::Time)
1697    } else if name.eq_ignore_ascii_case("datetime") {
1698        Some(Type::DateTime)
1699    } else if opts.custom_types.contains(&Ascii::new(name.to_owned())) {
1700        // ^ Sad we have to allocate here for no reason
1701        Some(Type::Custom(name.to_owned()))
1702    } else {
1703        None
1704    }
1705}