From 8d7231f0de4ba3b39e688f730137446d7e3c8d82 Mon Sep 17 00:00:00 2001 From: YoEight Date: Tue, 17 Feb 2026 15:13:26 -0500 Subject: [PATCH] feat: support defining type per data source --- Cargo.toml | 2 +- src/ast.rs | 2 +- src/lib.rs | 100 ++++++++++++++++++++++++++++++++++------- src/tests/analysis.rs | 2 +- src/typing/analysis.rs | 22 +++++++-- 5 files changed, 105 insertions(+), 23 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4083f91..a6aab6f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "eventql-parser" -version = "0.1.12" +version = "0.1.13" authors = ["Yorick Laupa "] description = "EventQL Lexer and Parser" homepage = "https://github.com/YoEight/eventql-parser" diff --git a/src/ast.rs b/src/ast.rs index e11f567..a049f19 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -297,7 +297,7 @@ pub enum Order { /// In `GROUP BY e.age HAVING age > 123`, this would be represented as: /// - `expr`: expression for `e.age` /// - `predicate`: `age > 123` -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Copy, Serialize)] pub struct GroupBy { /// Expression to group by pub expr: ExprRef, diff --git a/src/lib.rs b/src/lib.rs index edf10ae..39143c2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -71,33 +71,78 @@ impl<'a, const N: usize> From<&'a [Type; N]> for FunArgsBuilder<'a> { } } -/// Builder for configuring event type information on a [`SessionBuilder`]. +/// Builder for configuring type information on a [`SessionBuilder`]. /// -/// Obtained by calling [`SessionBuilder::declare_event_type`]. Use [`record`](EventTypeBuilder::record) -/// to define a record-shaped event type or [`custom`](EventTypeBuilder::custom) for a named custom type. +/// Obtained by calling [`SessionBuilder::declare_type`]. Use [`define_record`](EventTypeBuilder::define_record) +/// to define a record-shaped type or [`custom`](EventTypeBuilder::custom) for a named custom type. +/// Call [`done`](EventTypeBuilder::done) to return to the [`SessionBuilder`]. pub struct EventTypeBuilder { parent: SessionBuilder, } impl EventTypeBuilder { /// Starts building a record-shaped event type with named fields. - pub fn record(self) -> EventTypeRecordBuilder { + pub fn define_record(self) -> EventTypeRecordBuilder { EventTypeRecordBuilder { inner: self, props: Default::default(), } } - /// Declares a custom (non-record) event type by name. - pub fn custom(self, _name: &str) -> SessionBuilder { - todo!("deal with custom type later") + /// Sets the default event type used when no data source-specific type is found. + pub fn default_event_type(mut self, tpe: Type) -> Self { + self.parent.options.default_event_type = tpe; + self + } + + /// Registers a type for a specific named data source. + /// + /// Queries targeting `data_source` will use `tpe` for type checking instead of the default event type. + /// Data source names are case-insensitive. + pub fn data_source(mut self, data_source: &str, tpe: Type) -> Self { + let data_source = self.parent.arena.strings.alloc_no_case(data_source); + + self.parent.options.data_sources.insert(data_source, tpe); + + self + } + + /// Declares a custom type by name. + pub fn custom(mut self, name: &str) -> Self { + let name = self.parent.arena.strings.alloc_no_case(name); + self.parent.options.custom_types.insert(name); + + self + } + + /// Declares a custom event type by name for as default event type. + pub fn custom_default_event_type(mut self, name: &str) -> Self { + let name = self.parent.arena.strings.alloc_no_case(name); + self.parent.options.custom_types.insert(name); + + self.parent.options.default_event_type = Type::Custom(name); + self + } + + /// Declares a custom event type by name for a data source. + pub fn custom_for_data_source(mut self, name: &str, data_source: &str) -> Self { + let name = self.parent.arena.strings.alloc_no_case(name); + self.parent.options.custom_types.insert(name); + + self.data_source(data_source, Type::Custom(name)) + } + + /// Finalizes type configuration and returns the [`SessionBuilder`]. + pub fn done(self) -> SessionBuilder { + self.parent } } /// Builder for defining the fields of a record-shaped event type. /// -/// Obtained by calling [`EventTypeBuilder::record`]. Add fields with [`prop`](EventTypeRecordBuilder::prop) -/// and finalize with [`build`](EventTypeRecordBuilder::build) to return to the [`SessionBuilder`]. +/// Obtained by calling [`EventTypeBuilder::define_record`]. Add fields with [`prop`](EventTypeRecordBuilder::prop) +/// and finalize with [`as_default_event_type`](EventTypeRecordBuilder::as_default_event_type) or +/// [`for_data_source`](EventTypeRecordBuilder::for_data_source) to return to the [`EventTypeBuilder`]. pub struct EventTypeRecordBuilder { inner: EventTypeBuilder, props: FxHashMap, @@ -135,10 +180,28 @@ impl EventTypeRecordBuilder { } /// Finalizes the event record type and returns the [`SessionBuilder`]. - pub fn build(mut self) -> SessionBuilder { + pub fn as_default_event_type(mut self) -> EventTypeBuilder { let ptr = self.inner.parent.arena.types.alloc_record(self.props); - self.inner.parent.options.event_type_info = Type::Record(ptr); - self.inner.parent + self.inner.parent.options.default_event_type = Type::Record(ptr); + self.inner + } + + /// Finalizes the record type and registers it for a specific named data source. + /// + /// Queries targeting `data_source` will use this record type for type checking. + /// Data source names are case-insensitive. Returns the [`EventTypeBuilder`] to allow + /// chaining further type declarations. + pub fn for_data_source(mut self, data_source: &str) -> EventTypeBuilder { + let data_source = self.inner.parent.arena.strings.alloc_no_case(data_source); + let ptr = self.inner.parent.arena.types.alloc_record(self.props); + + self.inner + .parent + .options + .data_sources + .insert(data_source, Type::Record(ptr)); + + self.inner } } @@ -286,7 +349,7 @@ impl SessionBuilder { /// * `tpe` - The `Type` representing the structure of event records. pub fn declare_event_type_when(mut self, test: bool, tpe: Type) -> Self { if test { - self.options.event_type_info = tpe; + self.options.default_event_type = tpe; } self @@ -300,7 +363,7 @@ impl SessionBuilder { /// # Arguments /// /// * `tpe` - The `Type` representing the structure of event records. - pub fn declare_event_type(self) -> EventTypeBuilder { + pub fn declare_type(self) -> EventTypeBuilder { EventTypeBuilder { parent: self } } @@ -401,8 +464,10 @@ impl SessionBuilder { .declare_agg_func("stddev", &[Type::Number], Type::Number) .declare_agg_func("variance", &[Type::Number], Type::Number) .declare_agg_func("unique", &[Type::Unspecified], Type::Unspecified) - .declare_event_type() - .record() + .declare_type() + .data_source("eventtypes", Type::String) + .data_source("subjects", Type::String) + .define_record() .prop("specversion", Type::String) .prop("id", Type::String) .prop("time", Type::DateTime) @@ -416,7 +481,8 @@ impl SessionBuilder { .prop("traceparent", Type::String) .prop("tracestate", Type::String) .prop("signature", Type::String) - .build() + .as_default_event_type() + .done() } /// Builds the `Session` object with the configured analysis options. diff --git a/src/tests/analysis.rs b/src/tests/analysis.rs index 67ada9e..85e03b5 100644 --- a/src/tests/analysis.rs +++ b/src/tests/analysis.rs @@ -204,7 +204,7 @@ fn test_typecheck_datetime_contravariance_1() { .parse_expr() .unwrap(); - let event_type = session.options.event_type_info; + let event_type = session.options.default_event_type; let mut analysis = session.analysis(); analysis.test_declare("e", event_type); diff --git a/src/typing/analysis.rs b/src/typing/analysis.rs index e859f5c..1ba5d59 100644 --- a/src/typing/analysis.rs +++ b/src/typing/analysis.rs @@ -53,13 +53,20 @@ pub struct AnalysisOptions { /// The default scope containing built-in functions and their type signatures. pub default_scope: Scope, /// Type information for event records being queried. - pub event_type_info: Type, + pub default_event_type: Type, /// Custom types that are not defined in the EventQL reference. /// /// This set allows users to register custom type names that can be used /// in type conversion expressions (e.g., `field AS CustomType`). Custom /// type names are case-insensitive. pub custom_types: HashSet, + + /// Per-data-source type overrides. + /// + /// When a query targets a named data source, this map is checked first. If a match is + /// found, the associated type is used instead of [`default_event_type`](AnalysisOptions::default_event_type). + /// Keys are case-insensitive data source names. + pub data_sources: FxHashMap, } /// Represents a variable scope during static analysis. @@ -307,9 +314,18 @@ impl<'a> Analysis<'a> { fn analyze_source(&mut self, source: Source) -> AnalysisResult> { let kind = self.analyze_source_kind(source.kind)?; let tpe = match &kind { - SourceKind::Name(_) | SourceKind::Subject(_) => { - self.arena.types.alloc_type(self.options.event_type_info) + SourceKind::Name(name) => { + let tpe = if let Some(tpe) = self.options.data_sources.get(name).copied() { + tpe + } else { + self.options.default_event_type + }; + + self.arena.types.alloc_type(tpe) } + + SourceKind::Subject(_) => self.arena.types.alloc_type(self.options.default_event_type), + SourceKind::Subquery(query) => self.projection_type(query), };