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                ctx.allow_agg_func = true;
655                ctx.use_agg_funcs = true;
656
657                self.analyze_expr(&mut ctx, expr, Type::Bool)?;
658                if !self.expect_agg_expr(expr)? {
659                    return Err(AnalysisError::ExpectAggExpr(
660                        expr.attrs.pos.line,
661                        expr.attrs.pos.col,
662                    ));
663                }
664            }
665
666            ctx.allow_agg_func = true;
667            ctx.use_agg_funcs = true;
668        }
669
670        let project = self.analyze_projection(&mut ctx, &query.projection)?;
671
672        if let Some(order_by) = &query.order_by {
673            self.analyze_expr(&mut ctx, &order_by.expr, Type::Unspecified)?;
674
675            if query.group_by.is_none() && !matches!(&order_by.expr.value, Value::Access(_)) {
676                return Err(AnalysisError::ExpectFieldLiteral(
677                    order_by.expr.attrs.pos.line,
678                    order_by.expr.attrs.pos.col,
679                ));
680            } else if query.group_by.is_some() {
681                self.expect_agg_func(&order_by.expr)?;
682            }
683        }
684
685        let scope = self.exit_scope();
686
687        Ok(Query {
688            attrs: query.attrs,
689            sources,
690            predicate: query.predicate,
691            group_by: query.group_by,
692            order_by: query.order_by,
693            limit: query.limit,
694            projection: query.projection,
695            distinct: query.distinct,
696            meta: Typed {
697                project,
698                scope,
699                aggregate: ctx.use_agg_funcs,
700            },
701        })
702    }
703
704    fn analyze_source(&mut self, source: Source<Raw>) -> AnalysisResult<Source<Typed>> {
705        let kind = self.analyze_source_kind(source.kind)?;
706        let tpe = match &kind {
707            SourceKind::Name(_) | SourceKind::Subject(_) => self.options.event_type_info.clone(),
708            SourceKind::Subquery(query) => self.projection_type(query),
709        };
710
711        if self
712            .scope
713            .entries
714            .insert(source.binding.name.clone(), tpe)
715            .is_some()
716        {
717            return Err(AnalysisError::BindingAlreadyExists(
718                source.binding.pos.line,
719                source.binding.pos.col,
720                source.binding.name,
721            ));
722        }
723
724        Ok(Source {
725            binding: source.binding,
726            kind,
727        })
728    }
729
730    fn analyze_source_kind(&mut self, kind: SourceKind<Raw>) -> AnalysisResult<SourceKind<Typed>> {
731        match kind {
732            SourceKind::Name(n) => Ok(SourceKind::Name(n)),
733            SourceKind::Subject(s) => Ok(SourceKind::Subject(s)),
734            SourceKind::Subquery(query) => {
735                let query = self.analyze_query(*query)?;
736                Ok(SourceKind::Subquery(Box::new(query)))
737            }
738        }
739    }
740
741    fn analyze_projection(
742        &mut self,
743        ctx: &mut AnalysisContext,
744        expr: &Expr,
745    ) -> AnalysisResult<Type> {
746        match &expr.value {
747            Value::Record(record) => {
748                if record.is_empty() {
749                    return Err(AnalysisError::EmptyRecord(
750                        expr.attrs.pos.line,
751                        expr.attrs.pos.col,
752                    ));
753                }
754
755                ctx.allow_agg_func = true;
756                let tpe = self.analyze_expr(ctx, expr, Type::Unspecified)?;
757                let mut chk_ctx = CheckContext {
758                    use_agg_func: ctx.use_agg_funcs,
759                    ..Default::default()
760                };
761
762                self.check_projection_on_record(&mut chk_ctx, record.as_slice())?;
763                Ok(tpe)
764            }
765
766            Value::App(app) => {
767                ctx.allow_agg_func = true;
768
769                let tpe = self.analyze_expr(ctx, expr, Type::Unspecified)?;
770
771                if ctx.use_agg_funcs {
772                    let mut chk_ctx = CheckContext {
773                        use_agg_func: ctx.use_agg_funcs,
774                        ..Default::default()
775                    };
776
777                    self.check_projection_on_field_expr(&mut chk_ctx, expr)?;
778                } else {
779                    self.reject_constant_func(&expr.attrs, app)?;
780                }
781
782                Ok(tpe)
783            }
784
785            Value::Id(_) if ctx.use_agg_funcs => Err(AnalysisError::ExpectAggExpr(
786                expr.attrs.pos.line,
787                expr.attrs.pos.col,
788            )),
789
790            Value::Id(id) => {
791                if let Some(tpe) = self.scope.entries.get(id.as_str()).cloned() {
792                    Ok(tpe)
793                } else {
794                    Err(AnalysisError::VariableUndeclared(
795                        expr.attrs.pos.line,
796                        expr.attrs.pos.col,
797                        id.clone(),
798                    ))
799                }
800            }
801
802            Value::Access(_) if ctx.use_agg_funcs => Err(AnalysisError::ExpectAggExpr(
803                expr.attrs.pos.line,
804                expr.attrs.pos.col,
805            )),
806
807            Value::Access(access) => {
808                let mut current = &access.target.value;
809
810                loop {
811                    match current {
812                        Value::Id(name) => {
813                            if !self.scope.entries.contains_key(name.as_str()) {
814                                return Err(AnalysisError::VariableUndeclared(
815                                    expr.attrs.pos.line,
816                                    expr.attrs.pos.col,
817                                    name.clone(),
818                                ));
819                            }
820
821                            break;
822                        }
823
824                        Value::Access(next) => current = &next.target.value,
825                        _ => unreachable!(),
826                    }
827                }
828
829                self.analyze_expr(ctx, expr, Type::Unspecified)
830            }
831
832            _ => Err(AnalysisError::ExpectRecordOrSourcedProperty(
833                expr.attrs.pos.line,
834                expr.attrs.pos.col,
835                self.project_type(&expr.value),
836            )),
837        }
838    }
839
840    fn check_projection_on_record(
841        &mut self,
842        ctx: &mut CheckContext,
843        record: &[Field],
844    ) -> AnalysisResult<()> {
845        for field in record {
846            self.check_projection_on_field(ctx, field)?;
847        }
848
849        Ok(())
850    }
851
852    fn check_projection_on_field(
853        &mut self,
854        ctx: &mut CheckContext,
855        field: &Field,
856    ) -> AnalysisResult<()> {
857        self.check_projection_on_field_expr(ctx, &field.value)
858    }
859
860    fn check_projection_on_field_expr(
861        &mut self,
862        ctx: &mut CheckContext,
863        expr: &Expr,
864    ) -> AnalysisResult<()> {
865        match &expr.value {
866            Value::Number(_) | Value::String(_) | Value::Bool(_) => Ok(()),
867
868            Value::Id(id) => {
869                if self.scope.entries.contains_key(id.as_str()) {
870                    if ctx.use_agg_func {
871                        return Err(AnalysisError::UnallowedAggFuncUsageWithSrcField(
872                            expr.attrs.pos.line,
873                            expr.attrs.pos.col,
874                        ));
875                    }
876
877                    ctx.use_source_based = true;
878                }
879
880                Ok(())
881            }
882
883            Value::Array(exprs) => {
884                for expr in exprs {
885                    self.check_projection_on_field_expr(ctx, expr)?;
886                }
887
888                Ok(())
889            }
890
891            Value::Record(fields) => {
892                for field in fields {
893                    self.check_projection_on_field(ctx, field)?;
894                }
895
896                Ok(())
897            }
898
899            Value::Access(access) => self.check_projection_on_field_expr(ctx, &access.target),
900
901            Value::App(app) => {
902                if let Some(Type::App { aggregate, .. }) =
903                    self.options.default_scope.entries.get(app.func.as_str())
904                {
905                    ctx.use_agg_func |= *aggregate;
906
907                    if ctx.use_agg_func && ctx.use_source_based {
908                        return Err(AnalysisError::UnallowedAggFuncUsageWithSrcField(
909                            expr.attrs.pos.line,
910                            expr.attrs.pos.col,
911                        ));
912                    }
913
914                    if *aggregate {
915                        return self.expect_agg_func(expr);
916                    }
917
918                    for arg in &app.args {
919                        self.invalidate_agg_func_usage(arg)?;
920                    }
921                }
922
923                Ok(())
924            }
925
926            Value::Binary(binary) => {
927                self.check_projection_on_field_expr(ctx, &binary.lhs)?;
928                self.check_projection_on_field_expr(ctx, &binary.rhs)
929            }
930
931            Value::Unary(unary) => self.check_projection_on_field_expr(ctx, &unary.expr),
932            Value::Group(expr) => self.check_projection_on_field_expr(ctx, expr),
933        }
934    }
935
936    fn expect_agg_func(&self, expr: &Expr) -> AnalysisResult<()> {
937        if let Value::App(app) = &expr.value
938            && let Some(Type::App {
939                aggregate: true, ..
940            }) = self.options.default_scope.entries.get(app.func.as_str())
941        {
942            for arg in &app.args {
943                self.ensure_agg_param_is_source_bound(arg)?;
944                self.invalidate_agg_func_usage(arg)?;
945            }
946
947            return Ok(());
948        }
949
950        Err(AnalysisError::ExpectAggExpr(
951            expr.attrs.pos.line,
952            expr.attrs.pos.col,
953        ))
954    }
955
956    fn expect_agg_expr(&self, expr: &Expr) -> AnalysisResult<bool> {
957        match &expr.value {
958            Value::Id(id) => {
959                if self.scope.entries.contains_key(id.as_str()) {
960                    return Err(AnalysisError::UnallowedAggFuncUsageWithSrcField(
961                        expr.attrs.pos.line,
962                        expr.attrs.pos.col,
963                    ));
964                }
965
966                Ok(false)
967            }
968            Value::Group(expr) => self.expect_agg_expr(expr),
969            Value::Binary(binary) => {
970                let lhs = self.expect_agg_expr(&binary.lhs)?;
971                let rhs = self.expect_agg_expr(&binary.rhs)?;
972
973                if !lhs && !rhs {
974                    return Err(AnalysisError::ExpectAggExpr(
975                        expr.attrs.pos.line,
976                        expr.attrs.pos.col,
977                    ));
978                }
979
980                Ok(true)
981            }
982            Value::Unary(unary) => self.expect_agg_expr(unary.expr.as_ref()),
983            Value::App(_) => {
984                self.expect_agg_func(expr)?;
985                Ok(true)
986            }
987
988            _ => Ok(false),
989        }
990    }
991
992    fn ensure_agg_param_is_source_bound(&self, expr: &Expr) -> AnalysisResult<()> {
993        match &expr.value {
994            Value::Id(id) if !self.options.default_scope.entries.contains_key(id.as_str()) => {
995                Ok(())
996            }
997            Value::Access(access) => self.ensure_agg_param_is_source_bound(&access.target),
998            Value::Binary(binary) => self.ensure_agg_binary_op_is_source_bound(&expr.attrs, binary),
999            Value::Unary(unary) => self.ensure_agg_param_is_source_bound(&unary.expr),
1000
1001            _ => Err(AnalysisError::ExpectSourceBoundProperty(
1002                expr.attrs.pos.line,
1003                expr.attrs.pos.col,
1004            )),
1005        }
1006    }
1007
1008    fn ensure_agg_binary_op_is_source_bound(
1009        &self,
1010        attrs: &Attrs,
1011        binary: &Binary,
1012    ) -> AnalysisResult<()> {
1013        if !self.ensure_agg_binary_op_branch_is_source_bound(&binary.lhs)
1014            && !self.ensure_agg_binary_op_branch_is_source_bound(&binary.rhs)
1015        {
1016            return Err(AnalysisError::ExpectSourceBoundProperty(
1017                attrs.pos.line,
1018                attrs.pos.col,
1019            ));
1020        }
1021
1022        Ok(())
1023    }
1024
1025    fn ensure_agg_binary_op_branch_is_source_bound(&self, expr: &Expr) -> bool {
1026        match &expr.value {
1027            Value::Id(id) => !self.options.default_scope.entries.contains_key(id.as_str()),
1028            Value::Array(exprs) => {
1029                if exprs.is_empty() {
1030                    return false;
1031                }
1032
1033                exprs
1034                    .iter()
1035                    .all(|expr| self.ensure_agg_binary_op_branch_is_source_bound(expr))
1036            }
1037            Value::Record(fields) => {
1038                if fields.is_empty() {
1039                    return false;
1040                }
1041
1042                fields
1043                    .iter()
1044                    .all(|field| self.ensure_agg_binary_op_branch_is_source_bound(&field.value))
1045            }
1046            Value::Access(access) => {
1047                self.ensure_agg_binary_op_branch_is_source_bound(&access.target)
1048            }
1049
1050            Value::Binary(binary) => self
1051                .ensure_agg_binary_op_is_source_bound(&expr.attrs, binary)
1052                .is_ok(),
1053            Value::Unary(unary) => self.ensure_agg_binary_op_branch_is_source_bound(&unary.expr),
1054            Value::Group(expr) => self.ensure_agg_binary_op_branch_is_source_bound(expr),
1055
1056            Value::Number(_) | Value::String(_) | Value::Bool(_) | Value::App(_) => false,
1057        }
1058    }
1059
1060    fn invalidate_agg_func_usage(&self, expr: &Expr) -> AnalysisResult<()> {
1061        match &expr.value {
1062            Value::Number(_)
1063            | Value::String(_)
1064            | Value::Bool(_)
1065            | Value::Id(_)
1066            | Value::Access(_) => Ok(()),
1067
1068            Value::Array(exprs) => {
1069                for expr in exprs {
1070                    self.invalidate_agg_func_usage(expr)?;
1071                }
1072
1073                Ok(())
1074            }
1075
1076            Value::Record(fields) => {
1077                for field in fields {
1078                    self.invalidate_agg_func_usage(&field.value)?;
1079                }
1080
1081                Ok(())
1082            }
1083
1084            Value::App(app) => {
1085                if let Some(Type::App { aggregate, .. }) =
1086                    self.options.default_scope.entries.get(app.func.as_str())
1087                    && *aggregate
1088                {
1089                    return Err(AnalysisError::WrongAggFunUsage(
1090                        expr.attrs.pos.line,
1091                        expr.attrs.pos.col,
1092                        app.func.clone(),
1093                    ));
1094                }
1095
1096                for arg in &app.args {
1097                    self.invalidate_agg_func_usage(arg)?;
1098                }
1099
1100                Ok(())
1101            }
1102
1103            Value::Binary(binary) => {
1104                self.invalidate_agg_func_usage(&binary.lhs)?;
1105                self.invalidate_agg_func_usage(&binary.rhs)
1106            }
1107
1108            Value::Unary(unary) => self.invalidate_agg_func_usage(&unary.expr),
1109            Value::Group(expr) => self.invalidate_agg_func_usage(expr),
1110        }
1111    }
1112
1113    fn reject_constant_func(&self, attrs: &Attrs, app: &App) -> AnalysisResult<()> {
1114        if app.args.is_empty() {
1115            return Err(AnalysisError::ConstantExprInProjectIntoClause(
1116                attrs.pos.line,
1117                attrs.pos.col,
1118            ));
1119        }
1120
1121        let mut errored = None;
1122        for arg in &app.args {
1123            if let Err(e) = self.reject_constant_expr(arg) {
1124                if errored.is_none() {
1125                    errored = Some(e);
1126                }
1127
1128                continue;
1129            }
1130
1131            // if at least one arg is sourced-bound is ok
1132            return Ok(());
1133        }
1134
1135        Err(errored.expect("to be defined at that point"))
1136    }
1137
1138    fn reject_constant_expr(&self, expr: &Expr) -> AnalysisResult<()> {
1139        match &expr.value {
1140            Value::Id(id) if self.scope.entries.contains_key(id.as_str()) => Ok(()),
1141
1142            Value::Array(exprs) => {
1143                let mut errored = None;
1144                for expr in exprs {
1145                    if let Err(e) = self.reject_constant_expr(expr) {
1146                        if errored.is_none() {
1147                            errored = Some(e);
1148                        }
1149
1150                        continue;
1151                    }
1152
1153                    // if at least one arg is sourced-bound is ok
1154                    return Ok(());
1155                }
1156
1157                Err(errored.expect("to be defined at that point"))
1158            }
1159
1160            Value::Record(fields) => {
1161                let mut errored = None;
1162                for field in fields {
1163                    if let Err(e) = self.reject_constant_expr(&field.value) {
1164                        if errored.is_none() {
1165                            errored = Some(e);
1166                        }
1167
1168                        continue;
1169                    }
1170
1171                    // if at least one arg is sourced-bound is ok
1172                    return Ok(());
1173                }
1174
1175                Err(errored.expect("to be defined at that point"))
1176            }
1177
1178            Value::Binary(binary) => self
1179                .reject_constant_expr(&binary.lhs)
1180                .or_else(|e| self.reject_constant_expr(&binary.rhs).map_err(|_| e)),
1181
1182            Value::Access(access) => self.reject_constant_expr(access.target.as_ref()),
1183            Value::App(app) => self.reject_constant_func(&expr.attrs, app),
1184            Value::Unary(unary) => self.reject_constant_expr(&unary.expr),
1185            Value::Group(expr) => self.reject_constant_expr(expr),
1186
1187            _ => Err(AnalysisError::ConstantExprInProjectIntoClause(
1188                expr.attrs.pos.line,
1189                expr.attrs.pos.col,
1190            )),
1191        }
1192    }
1193
1194    /// Analyzes an expression and checks it against an expected type.
1195    ///
1196    /// This method performs type checking on an expression, verifying that all operations
1197    /// are type-safe and that the expression's type is compatible with the expected type.
1198    ///
1199    /// # Arguments
1200    ///
1201    /// * `ctx` - The analysis context controlling analysis behavior
1202    /// * `expr` - The expression to analyze
1203    /// * `expect` - The expected type of the expression
1204    ///
1205    /// # Returns
1206    ///
1207    /// Returns the actual type of the expression after checking compatibility with the expected type,
1208    /// or an error if type checking fails.
1209    ///
1210    /// # Example
1211    ///
1212    /// ```rust
1213    /// use eventql_parser::prelude::{tokenize, Parser, Analysis, AnalysisContext, AnalysisOptions, Type};
1214    ///
1215    /// let tokens = tokenize("1 + 2").unwrap();
1216    /// let expr = Parser::new(tokens.as_slice()).parse_expr().unwrap();
1217    /// let options = AnalysisOptions::default();
1218    /// let mut analysis = Analysis::new(&options);
1219    ///
1220    /// let result = analysis.analyze_expr(&mut AnalysisContext::default(), &expr, Type::Number);
1221    /// assert!(result.is_ok());
1222    /// ```
1223    pub fn analyze_expr(
1224        &mut self,
1225        ctx: &mut AnalysisContext,
1226        expr: &Expr,
1227        mut expect: Type,
1228    ) -> AnalysisResult<Type> {
1229        match &expr.value {
1230            Value::Number(_) => expect.check(&expr.attrs, Type::Number),
1231            Value::String(_) => expect.check(&expr.attrs, Type::String),
1232            Value::Bool(_) => expect.check(&expr.attrs, Type::Bool),
1233
1234            Value::Id(id) => {
1235                if let Some(tpe) = self.options.default_scope.entries.get(id.as_str()) {
1236                    expect.check(&expr.attrs, tpe.clone())
1237                } else if let Some(tpe) = self.scope.entries.get_mut(id.as_str()) {
1238                    let tmp = mem::take(tpe);
1239                    *tpe = tmp.check(&expr.attrs, expect)?;
1240
1241                    Ok(tpe.clone())
1242                } else {
1243                    Err(AnalysisError::VariableUndeclared(
1244                        expr.attrs.pos.line,
1245                        expr.attrs.pos.col,
1246                        id.to_owned(),
1247                    ))
1248                }
1249            }
1250
1251            Value::Array(exprs) => {
1252                if matches!(expect, Type::Unspecified) {
1253                    for expr in exprs {
1254                        expect = self.analyze_expr(ctx, expr, expect)?;
1255                    }
1256
1257                    return Ok(Type::Array(Box::new(expect)));
1258                }
1259
1260                match expect {
1261                    Type::Array(mut expect) => {
1262                        for expr in exprs {
1263                            *expect = self.analyze_expr(ctx, expr, expect.as_ref().clone())?;
1264                        }
1265
1266                        Ok(Type::Array(expect))
1267                    }
1268
1269                    expect => Err(AnalysisError::TypeMismatch(
1270                        expr.attrs.pos.line,
1271                        expr.attrs.pos.col,
1272                        expect,
1273                        self.project_type(&expr.value),
1274                    )),
1275                }
1276            }
1277
1278            Value::Record(fields) => {
1279                if matches!(expect, Type::Unspecified) {
1280                    let mut record = BTreeMap::new();
1281
1282                    for field in fields {
1283                        record.insert(
1284                            field.name.clone(),
1285                            self.analyze_expr(ctx, &field.value, Type::Unspecified)?,
1286                        );
1287                    }
1288
1289                    return Ok(Type::Record(record));
1290                }
1291
1292                match expect {
1293                    Type::Record(mut types) if fields.len() == types.len() => {
1294                        for field in fields {
1295                            if let Some(tpe) = types.remove(field.name.as_str()) {
1296                                types.insert(
1297                                    field.name.clone(),
1298                                    self.analyze_expr(ctx, &field.value, tpe)?,
1299                                );
1300                            } else {
1301                                return Err(AnalysisError::FieldUndeclared(
1302                                    expr.attrs.pos.line,
1303                                    expr.attrs.pos.col,
1304                                    field.name.clone(),
1305                                ));
1306                            }
1307                        }
1308
1309                        Ok(Type::Record(types))
1310                    }
1311
1312                    expect => Err(AnalysisError::TypeMismatch(
1313                        expr.attrs.pos.line,
1314                        expr.attrs.pos.col,
1315                        expect,
1316                        self.project_type(&expr.value),
1317                    )),
1318                }
1319            }
1320
1321            this @ Value::Access(_) => Ok(self.analyze_access(&expr.attrs, this, expect)?),
1322
1323            Value::App(app) => {
1324                if let Some(tpe) = self.options.default_scope.entries.get(app.func.as_str())
1325                    && let Type::App {
1326                        args,
1327                        result,
1328                        aggregate,
1329                    } = tpe
1330                {
1331                    if !args.match_arg_count(app.args.len()) {
1332                        return Err(AnalysisError::FunWrongArgumentCount(
1333                            expr.attrs.pos.line,
1334                            expr.attrs.pos.col,
1335                            app.func.clone(),
1336                        ));
1337                    }
1338
1339                    if *aggregate && !ctx.allow_agg_func {
1340                        return Err(AnalysisError::WrongAggFunUsage(
1341                            expr.attrs.pos.line,
1342                            expr.attrs.pos.col,
1343                            app.func.clone(),
1344                        ));
1345                    }
1346
1347                    if *aggregate && ctx.allow_agg_func {
1348                        ctx.use_agg_funcs = true;
1349                    }
1350
1351                    for (arg, tpe) in app.args.iter().zip(args.values.iter().cloned()) {
1352                        self.analyze_expr(ctx, arg, tpe)?;
1353                    }
1354
1355                    if matches!(expect, Type::Unspecified) {
1356                        Ok(result.as_ref().clone())
1357                    } else {
1358                        expect.check(&expr.attrs, result.as_ref().clone())
1359                    }
1360                } else {
1361                    Err(AnalysisError::FuncUndeclared(
1362                        expr.attrs.pos.line,
1363                        expr.attrs.pos.col,
1364                        app.func.clone(),
1365                    ))
1366                }
1367            }
1368
1369            Value::Binary(binary) => match binary.operator {
1370                Operator::Add | Operator::Sub | Operator::Mul | Operator::Div => {
1371                    self.analyze_expr(ctx, &binary.lhs, Type::Number)?;
1372                    self.analyze_expr(ctx, &binary.rhs, Type::Number)?;
1373                    expect.check(&expr.attrs, Type::Number)
1374                }
1375
1376                Operator::Eq
1377                | Operator::Neq
1378                | Operator::Lt
1379                | Operator::Lte
1380                | Operator::Gt
1381                | Operator::Gte => {
1382                    let lhs_expect = self.analyze_expr(ctx, &binary.lhs, Type::Unspecified)?;
1383                    let rhs_expect = self.analyze_expr(ctx, &binary.rhs, lhs_expect.clone())?;
1384
1385                    // If the left side didn't have enough type information while the other did,
1386                    // we replay another typecheck pass on the left side if the right side was conclusive
1387                    if matches!(lhs_expect, Type::Unspecified)
1388                        && !matches!(rhs_expect, Type::Unspecified)
1389                    {
1390                        self.analyze_expr(ctx, &binary.lhs, rhs_expect)?;
1391                    }
1392
1393                    expect.check(&expr.attrs, Type::Bool)
1394                }
1395
1396                Operator::Contains => {
1397                    let lhs_expect = self.analyze_expr(
1398                        ctx,
1399                        &binary.lhs,
1400                        Type::Array(Box::new(Type::Unspecified)),
1401                    )?;
1402
1403                    let lhs_assumption = match lhs_expect {
1404                        Type::Array(inner) => *inner,
1405                        other => {
1406                            return Err(AnalysisError::ExpectArray(
1407                                expr.attrs.pos.line,
1408                                expr.attrs.pos.col,
1409                                other,
1410                            ));
1411                        }
1412                    };
1413
1414                    let rhs_expect = self.analyze_expr(ctx, &binary.rhs, lhs_assumption.clone())?;
1415
1416                    // If the left side didn't have enough type information while the other did,
1417                    // we replay another typecheck pass on the left side if the right side was conclusive
1418                    if matches!(lhs_assumption, Type::Unspecified)
1419                        && !matches!(rhs_expect, Type::Unspecified)
1420                    {
1421                        self.analyze_expr(ctx, &binary.lhs, Type::Array(Box::new(rhs_expect)))?;
1422                    }
1423
1424                    expect.check(&expr.attrs, Type::Bool)
1425                }
1426
1427                Operator::And | Operator::Or | Operator::Xor => {
1428                    self.analyze_expr(ctx, &binary.lhs, Type::Bool)?;
1429                    self.analyze_expr(ctx, &binary.rhs, Type::Bool)?;
1430
1431                    expect.check(&expr.attrs, Type::Bool)
1432                }
1433
1434                Operator::As => {
1435                    if let Value::Id(name) = &binary.rhs.value {
1436                        if let Some(tpe) = name_to_type(self.options, name) {
1437                            // NOTE - we could check if it's safe to convert the left branch to that type
1438                            return Ok(tpe);
1439                        } else {
1440                            return Err(AnalysisError::UnsupportedCustomType(
1441                                expr.attrs.pos.line,
1442                                expr.attrs.pos.col,
1443                                name.clone(),
1444                            ));
1445                        }
1446                    }
1447
1448                    unreachable!(
1449                        "we already made sure during parsing that we can only have an ID symbol at this point"
1450                    )
1451                }
1452
1453                Operator::Not => unreachable!(),
1454            },
1455
1456            Value::Unary(unary) => match unary.operator {
1457                Operator::Add | Operator::Sub => {
1458                    self.analyze_expr(ctx, &unary.expr, Type::Number)?;
1459                    expect.check(&expr.attrs, Type::Number)
1460                }
1461
1462                Operator::Not => {
1463                    self.analyze_expr(ctx, &unary.expr, Type::Bool)?;
1464                    expect.check(&expr.attrs, Type::Bool)
1465                }
1466
1467                _ => unreachable!(),
1468            },
1469
1470            Value::Group(expr) => Ok(self.analyze_expr(ctx, expr.as_ref(), expect)?),
1471        }
1472    }
1473
1474    fn analyze_access(
1475        &mut self,
1476        attrs: &Attrs,
1477        access: &Value,
1478        expect: Type,
1479    ) -> AnalysisResult<Type> {
1480        struct State<A, B> {
1481            depth: u8,
1482            /// When true means we are into dynamically type object.
1483            dynamic: bool,
1484            definition: Def<A, B>,
1485        }
1486
1487        impl<A, B> State<A, B> {
1488            fn new(definition: Def<A, B>) -> Self {
1489                Self {
1490                    depth: 0,
1491                    dynamic: false,
1492                    definition,
1493                }
1494            }
1495        }
1496
1497        enum Def<A, B> {
1498            User(A),
1499            System(B),
1500        }
1501
1502        fn go<'a>(
1503            scope: &'a mut Scope,
1504            sys: &'a AnalysisOptions,
1505            attrs: &'a Attrs,
1506            value: &'a Value,
1507        ) -> AnalysisResult<State<&'a mut Type, &'a Type>> {
1508            match value {
1509                Value::Id(id) => {
1510                    if let Some(tpe) = sys.default_scope.entries.get(id.as_str()) {
1511                        Ok(State::new(Def::System(tpe)))
1512                    } else if let Some(tpe) = scope.entries.get_mut(id.as_str()) {
1513                        Ok(State::new(Def::User(tpe)))
1514                    } else {
1515                        Err(AnalysisError::VariableUndeclared(
1516                            attrs.pos.line,
1517                            attrs.pos.col,
1518                            id.clone(),
1519                        ))
1520                    }
1521                }
1522                Value::Access(access) => {
1523                    let mut state = go(scope, sys, &access.target.attrs, &access.target.value)?;
1524
1525                    // TODO - we should consider make that field and depth configurable.
1526                    let is_data_field = state.depth == 0 && access.field == "data";
1527
1528                    // TODO - we should consider make that behavior configurable.
1529                    // the `data` property is where the JSON payload is located, which means
1530                    // we should be lax if a property is not defined yet.
1531                    if !state.dynamic && is_data_field {
1532                        state.dynamic = true;
1533                    }
1534
1535                    match state.definition {
1536                        Def::User(tpe) => {
1537                            if matches!(tpe, Type::Unspecified) && state.dynamic {
1538                                *tpe = Type::Record(BTreeMap::from([(
1539                                    access.field.clone(),
1540                                    Type::Unspecified,
1541                                )]));
1542                                return Ok(State {
1543                                    depth: state.depth + 1,
1544                                    definition: Def::User(
1545                                        tpe.as_record_or_panic_mut()
1546                                            .get_mut(access.field.as_str())
1547                                            .unwrap(),
1548                                    ),
1549                                    ..state
1550                                });
1551                            }
1552
1553                            if let Type::Record(fields) = tpe {
1554                                match fields.entry(access.field.clone()) {
1555                                    Entry::Vacant(entry) => {
1556                                        if state.dynamic || is_data_field {
1557                                            return Ok(State {
1558                                                depth: state.depth + 1,
1559                                                definition: Def::User(
1560                                                    entry.insert(Type::Unspecified),
1561                                                ),
1562                                                ..state
1563                                            });
1564                                        }
1565
1566                                        return Err(AnalysisError::FieldUndeclared(
1567                                            attrs.pos.line,
1568                                            attrs.pos.col,
1569                                            access.field.clone(),
1570                                        ));
1571                                    }
1572
1573                                    Entry::Occupied(entry) => {
1574                                        return Ok(State {
1575                                            depth: state.depth + 1,
1576                                            definition: Def::User(entry.into_mut()),
1577                                            ..state
1578                                        });
1579                                    }
1580                                }
1581                            }
1582
1583                            Err(AnalysisError::ExpectRecord(
1584                                attrs.pos.line,
1585                                attrs.pos.col,
1586                                tpe.clone(),
1587                            ))
1588                        }
1589
1590                        Def::System(tpe) => {
1591                            if matches!(tpe, Type::Unspecified) && state.dynamic {
1592                                return Ok(State {
1593                                    depth: state.depth + 1,
1594                                    definition: Def::System(&Type::Unspecified),
1595                                    ..state
1596                                });
1597                            }
1598
1599                            if let Type::Record(fields) = tpe {
1600                                if let Some(field) = fields.get(access.field.as_str()) {
1601                                    return Ok(State {
1602                                        depth: state.depth + 1,
1603                                        definition: Def::System(field),
1604                                        ..state
1605                                    });
1606                                }
1607
1608                                return Err(AnalysisError::FieldUndeclared(
1609                                    attrs.pos.line,
1610                                    attrs.pos.col,
1611                                    access.field.clone(),
1612                                ));
1613                            }
1614
1615                            Err(AnalysisError::ExpectRecord(
1616                                attrs.pos.line,
1617                                attrs.pos.col,
1618                                tpe.clone(),
1619                            ))
1620                        }
1621                    }
1622                }
1623                Value::Number(_)
1624                | Value::String(_)
1625                | Value::Bool(_)
1626                | Value::Array(_)
1627                | Value::Record(_)
1628                | Value::App(_)
1629                | Value::Binary(_)
1630                | Value::Unary(_)
1631                | Value::Group(_) => unreachable!(),
1632            }
1633        }
1634
1635        let state = go(&mut self.scope, self.options, attrs, access)?;
1636
1637        match state.definition {
1638            Def::User(tpe) => {
1639                let tmp = mem::take(tpe);
1640                *tpe = tmp.check(attrs, expect)?;
1641
1642                Ok(tpe.clone())
1643            }
1644
1645            Def::System(tpe) => tpe.clone().check(attrs, expect),
1646        }
1647    }
1648
1649    fn projection_type(&self, query: &Query<Typed>) -> Type {
1650        self.project_type(&query.projection.value)
1651    }
1652
1653    fn project_type(&self, value: &Value) -> Type {
1654        match value {
1655            Value::Number(_) => Type::Number,
1656            Value::String(_) => Type::String,
1657            Value::Bool(_) => Type::Bool,
1658            Value::Id(id) => {
1659                if let Some(tpe) = self.options.default_scope.entries.get(id.as_str()) {
1660                    tpe.clone()
1661                } else if let Some(tpe) = self.scope.entries.get(id.as_str()) {
1662                    tpe.clone()
1663                } else {
1664                    Type::Unspecified
1665                }
1666            }
1667            Value::Array(exprs) => {
1668                let mut project = Type::Unspecified;
1669
1670                for expr in exprs {
1671                    let tmp = self.project_type(&expr.value);
1672
1673                    if !matches!(tmp, Type::Unspecified) {
1674                        project = tmp;
1675                        break;
1676                    }
1677                }
1678
1679                Type::Array(Box::new(project))
1680            }
1681            Value::Record(fields) => Type::Record(
1682                fields
1683                    .iter()
1684                    .map(|field| (field.name.clone(), self.project_type(&field.value.value)))
1685                    .collect(),
1686            ),
1687            Value::Access(access) => {
1688                let tpe = self.project_type(&access.target.value);
1689                if let Type::Record(fields) = tpe {
1690                    fields
1691                        .get(access.field.as_str())
1692                        .cloned()
1693                        .unwrap_or_default()
1694                } else {
1695                    Type::Unspecified
1696                }
1697            }
1698            Value::App(app) => self
1699                .options
1700                .default_scope
1701                .entries
1702                .get(app.func.as_str())
1703                .cloned()
1704                .unwrap_or_default(),
1705            Value::Binary(binary) => match binary.operator {
1706                Operator::Add | Operator::Sub | Operator::Mul | Operator::Div => Type::Number,
1707                Operator::As => {
1708                    if let Value::Id(n) = &binary.rhs.as_ref().value
1709                        && let Some(tpe) = name_to_type(self.options, n.as_str())
1710                    {
1711                        tpe
1712                    } else {
1713                        Type::Unspecified
1714                    }
1715                }
1716                Operator::Eq
1717                | Operator::Neq
1718                | Operator::Lt
1719                | Operator::Lte
1720                | Operator::Gt
1721                | Operator::Gte
1722                | Operator::And
1723                | Operator::Or
1724                | Operator::Xor
1725                | Operator::Not
1726                | Operator::Contains => Type::Bool,
1727            },
1728            Value::Unary(unary) => match unary.operator {
1729                Operator::Add | Operator::Sub => Type::Number,
1730                Operator::Mul
1731                | Operator::Div
1732                | Operator::Eq
1733                | Operator::Neq
1734                | Operator::Lt
1735                | Operator::Lte
1736                | Operator::Gt
1737                | Operator::Gte
1738                | Operator::And
1739                | Operator::Or
1740                | Operator::Xor
1741                | Operator::Not
1742                | Operator::Contains
1743                | Operator::As => unreachable!(),
1744            },
1745            Value::Group(expr) => self.project_type(&expr.value),
1746        }
1747    }
1748}
1749
1750/// Converts a type name string to its corresponding [`Type`] variant.
1751///
1752/// This function performs case-insensitive matching for built-in type names and checks
1753/// against custom types defined in the analysis options.
1754///
1755/// # Returns
1756///
1757/// * `Some(Type)` - If the name matches a built-in type or custom type
1758/// * `None` - If the name doesn't match any known type
1759///
1760/// # Built-in Type Mappings
1761///
1762/// The following type names are recognized (case-insensitive):
1763/// - `"string"` → [`Type::String`]
1764/// - `"int"` or `"float64"` → [`Type::Number`]
1765/// - `"boolean"` → [`Type::Bool`]
1766/// - `"date"` → [`Type::Date`]
1767/// - `"time"` → [`Type::Time`]
1768/// - `"datetime"` → [`Type::DateTime`]
1769///
1770/// # Examples
1771///
1772/// ```
1773/// use eventql_parser::Type;
1774/// use eventql_parser::prelude::{AnalysisOptions, name_to_type};
1775///
1776/// let opts = AnalysisOptions::default();
1777/// assert!(matches!(name_to_type(&opts, "String"), Some(Type::String)));
1778/// assert!(matches!(name_to_type(&opts, "INT"), Some(Type::Number)));
1779/// assert!(name_to_type(&opts, "unknown").is_none());
1780/// ```
1781pub fn name_to_type(opts: &AnalysisOptions, name: &str) -> Option<Type> {
1782    if name.eq_ignore_ascii_case("string") {
1783        Some(Type::String)
1784    } else if name.eq_ignore_ascii_case("int") || name.eq_ignore_ascii_case("float64") {
1785        Some(Type::Number)
1786    } else if name.eq_ignore_ascii_case("boolean") {
1787        Some(Type::Bool)
1788    } else if name.eq_ignore_ascii_case("date") {
1789        Some(Type::Date)
1790    } else if name.eq_ignore_ascii_case("time") {
1791        Some(Type::Time)
1792    } else if name.eq_ignore_ascii_case("datetime") {
1793        Some(Type::DateTime)
1794    } else if opts.custom_types.contains(&Ascii::new(name.to_owned())) {
1795        // ^ Sad we have to allocate here for no reason
1796        Some(Type::Custom(name.to_owned()))
1797    } else {
1798        None
1799    }
1800}