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