eventql_parser/
analysis.rs

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