Skip to main content

laminar_sql/parser/statements/
mod.rs

1//! AST types for streaming SQL extensions and their runtime conversions.
2
3#[allow(clippy::disallowed_types)] // cold path: SQL parsing
4use std::collections::HashMap;
5use std::time::Duration;
6
7use sqlparser::ast::{ColumnDef, Expr, Ident, ObjectName, Statement};
8
9use super::join_parser::JoinAnalysis;
10use super::window_rewriter::WindowRewriter;
11use super::ParseError;
12
13/// Supported `SHOW` commands.
14#[derive(Debug, Clone, PartialEq)]
15pub enum ShowCommand {
16    /// List registered sources.
17    Sources,
18    /// List registered sinks.
19    Sinks,
20    /// List continuous queries.
21    Queries,
22    /// List materialized views.
23    MaterializedViews,
24    /// List named streams.
25    Streams,
26    /// List reference tables.
27    Tables,
28    /// Show checkpoint state.
29    CheckpointStatus,
30    /// Reconstruct source DDL.
31    CreateSource {
32        /// Source to reconstruct.
33        name: ObjectName,
34    },
35    /// Reconstruct sink DDL.
36    CreateSink {
37        /// Sink to reconstruct.
38        name: ObjectName,
39    },
40}
41
42/// SQL statements understood by the streaming parser.
43#[derive(Debug, Clone, PartialEq)]
44pub enum StreamingStatement {
45    /// A standard SQL statement without streaming syntax.
46    Standard(Box<sqlparser::ast::Statement>),
47
48    /// A multi-horizon temporal probe normalized to the canonical AS-OF query shape.
49    TemporalProbeQuery {
50        /// AST used for projection and non-join analysis.
51        statement: Box<Statement>,
52        /// Parsed temporal-join contract.
53        analysis: Box<JoinAnalysis>,
54    },
55
56    /// `CREATE SOURCE`.
57    CreateSource(Box<CreateSourceStatement>),
58    /// `CREATE SINK`.
59    CreateSink(Box<CreateSinkStatement>),
60    /// `CREATE CONTINUOUS QUERY`.
61    CreateContinuousQuery {
62        /// Query name.
63        name: ObjectName,
64        /// Streaming query body.
65        query: Box<StreamingStatement>,
66        /// Optional output strategy.
67        emit_clause: Option<EmitClause>,
68    },
69
70    /// `DROP SOURCE`.
71    DropSource {
72        /// Source to drop.
73        name: ObjectName,
74        /// Ignore a missing source.
75        if_exists: bool,
76        /// Also drop dependent streams and materialized views.
77        cascade: bool,
78    },
79
80    /// `DROP SINK`.
81    DropSink {
82        /// Sink to drop.
83        name: ObjectName,
84        /// Ignore a missing sink.
85        if_exists: bool,
86        /// Also drop dependent objects.
87        cascade: bool,
88    },
89
90    /// `DROP MATERIALIZED VIEW`.
91    DropMaterializedView {
92        /// View to drop.
93        name: ObjectName,
94        /// Ignore a missing view.
95        if_exists: bool,
96        /// Also drop dependent objects.
97        cascade: bool,
98    },
99
100    /// A `SHOW` command.
101    Show(ShowCommand),
102
103    /// `DESCRIBE` a streaming object.
104    Describe {
105        /// Object to describe.
106        name: ObjectName,
107        /// Include extended details.
108        extended: bool,
109    },
110
111    /// `EXPLAIN [ANALYZE]` a streaming query plan.
112    Explain {
113        /// Statement to explain.
114        statement: Box<StreamingStatement>,
115        /// Execute the query and collect metrics.
116        analyze: bool,
117    },
118
119    /// `CREATE MATERIALIZED VIEW`.
120    CreateMaterializedView {
121        /// View name.
122        name: ObjectName,
123        /// Backing query.
124        query: Box<StreamingStatement>,
125        /// Optional output strategy.
126        emit_clause: Option<EmitClause>,
127        /// Replace an existing view.
128        or_replace: bool,
129        /// Ignore an existing view.
130        if_not_exists: bool,
131        /// Raw query text between `AS` and `EMIT`.
132        query_sql: String,
133        /// Declared retention from the trailing `WITH (...)`.
134        retention: Option<crate::parser::MaterializedViewRetention>,
135        /// Declared freshness from the trailing `WITH (...)`: the most a committed read of the
136        /// view may lag.
137        freshness: Option<std::time::Duration>,
138    },
139
140    /// `CREATE STREAM` for a named streaming pipeline.
141    CreateStream {
142        /// Stream name.
143        name: ObjectName,
144        /// Backing query.
145        query: Box<StreamingStatement>,
146        /// Optional output strategy.
147        emit_clause: Option<EmitClause>,
148        /// Replace an existing stream.
149        or_replace: bool,
150        /// Ignore an existing stream.
151        if_not_exists: bool,
152        /// Raw query text between `AS` and `EMIT`.
153        query_sql: String,
154        /// `RETAIN_HISTORY` cap in bytes from the trailing `WITH (...)`.
155        retention_bytes: Option<u64>,
156    },
157
158    /// `DROP STREAM`.
159    DropStream {
160        /// Stream to drop.
161        name: ObjectName,
162        /// Ignore a missing stream.
163        if_exists: bool,
164        /// Also drop dependent objects.
165        cascade: bool,
166    },
167
168    /// `ALTER SOURCE`.
169    AlterSource {
170        /// Source to alter.
171        name: ObjectName,
172        /// Requested alteration.
173        operation: AlterSourceOperation,
174    },
175
176    /// `INSERT INTO` a streaming source or table.
177    InsertInto {
178        /// Target source or table.
179        table_name: ObjectName,
180        /// Empty when the statement does not name columns.
181        columns: Vec<Ident>,
182        /// Rows to insert.
183        values: Vec<Vec<Expr>>,
184    },
185
186    /// `CREATE LOOKUP TABLE`.
187    CreateLookupTable(Box<super::lookup_table::CreateLookupTableStatement>),
188
189    /// `DROP LOOKUP TABLE`.
190    DropLookupTable {
191        /// Lookup table to drop.
192        name: ObjectName,
193        /// Ignore a missing table.
194        if_exists: bool,
195    },
196
197    /// Trigger an immediate checkpoint.
198    Checkpoint,
199
200    /// `RESTORE FROM CHECKPOINT <id>`.
201    RestoreCheckpoint {
202        /// Durable checkpoint identifier.
203        checkpoint_id: u64,
204    },
205
206    /// `SUBSCRIBE <name> [WHERE ...] [WITH (...)]`.
207    Subscribe(Box<SubscribeStatement>),
208
209    /// `DECLARE <name> [NO SCROLL] CURSOR [WITHOUT HOLD] FOR SUBSCRIBE …`
210    ///
211    /// Forward-only cursor over a SUBSCRIBE, scoped to the current SimpleQuery
212    /// connection. SCROLL, BINARY, WITH HOLD, INSENSITIVE, and ASENSITIVE are
213    /// rejected at parse time.
214    DeclareCursorForSubscribe {
215        /// Cursor identifier.
216        name: Ident,
217        /// `NO SCROLL` was explicit in the source. We never emit SCROLL.
218        no_scroll: bool,
219        /// The SUBSCRIBE body the cursor wraps.
220        subscribe: Box<SubscribeStatement>,
221    },
222}
223
224/// `SUBSCRIBE <name> [AS OF EPOCH n] [WHERE <fragment>] [WITH (...)]`.
225#[derive(Debug, Clone, PartialEq)]
226pub struct SubscribeStatement {
227    /// Target stream or materialized view.
228    pub name: ObjectName,
229    /// Raw WHERE fragment, compiled by the engine against the target schema.
230    pub filter_sql: Option<String>,
231    /// `AS OF EPOCH n`: replay everything emitted strictly after barrier `n`.
232    pub as_of_epoch: Option<u64>,
233    /// Reserved `WITH` options.
234    pub options: HashMap<String, String>,
235}
236
237/// Operations for ALTER SOURCE statements.
238#[derive(Debug, Clone, PartialEq)]
239pub enum AlterSourceOperation {
240    /// Add a new column: `ALTER SOURCE name ADD COLUMN col_name data_type`
241    AddColumn {
242        /// Column to add.
243        column_def: ColumnDef,
244    },
245    /// Set source properties: `ALTER SOURCE name SET ('key' = 'value', ...)`
246    SetProperties {
247        /// Properties to replace.
248        properties: HashMap<String, String>,
249    },
250}
251
252/// Format specification for serialization (e.g., FORMAT JSON, FORMAT AVRO).
253#[derive(Debug, Clone, PartialEq)]
254pub struct FormatSpec {
255    /// For example, `JSON`, `AVRO`, or `PROTOBUF`.
256    pub format_type: String,
257    /// Additional format options (from WITH clause after FORMAT).
258    pub options: HashMap<String, String>,
259}
260
261/// Parsed `CREATE SOURCE` statement.
262#[derive(Debug, Clone, PartialEq)]
263pub struct CreateSourceStatement {
264    /// Source name.
265    pub name: ObjectName,
266    /// Declared columns.
267    pub columns: Vec<ColumnDef>,
268    /// Primary-key columns, empty when no key is declared.
269    pub primary_key: Vec<Ident>,
270    /// Optional event-time watermark.
271    pub watermark: Option<WatermarkDef>,
272    /// Source runtime options from the trailing `WITH` clause.
273    pub with_options: HashMap<String, String>,
274    /// Replace an existing source.
275    pub or_replace: bool,
276    /// Ignore an existing source.
277    pub if_not_exists: bool,
278    /// Connector type (e.g., "KAFKA") from `FROM KAFKA (...)` syntax
279    pub connector_type: Option<String>,
280    /// Connector-specific options (from `FROM KAFKA (...)`)
281    pub connector_options: HashMap<String, String>,
282    /// Format specification (e.g., `FORMAT JSON`)
283    pub format: Option<FormatSpec>,
284}
285
286/// Parsed `CREATE SINK` statement.
287#[derive(Debug, Clone, PartialEq)]
288pub struct CreateSinkStatement {
289    /// Sink name.
290    pub name: ObjectName,
291    /// Sink input.
292    pub from: SinkFrom,
293    /// Replace an existing sink.
294    pub or_replace: bool,
295    /// Ignore an existing sink.
296    pub if_not_exists: bool,
297    /// Optional row filter.
298    pub filter: Option<Expr>,
299    /// Connector type (e.g., "KAFKA") from `INTO KAFKA (...)` syntax
300    pub connector_type: Option<String>,
301    /// Connector-specific options (from `INTO KAFKA (...)`)
302    pub connector_options: HashMap<String, String>,
303    /// Format specification (e.g., `FORMAT JSON`)
304    pub format: Option<FormatSpec>,
305}
306
307/// Input selected by a sink.
308#[derive(Debug, Clone, PartialEq)]
309pub enum SinkFrom {
310    /// A named table or source.
311    Table(ObjectName),
312    /// A query result.
313    Query(Box<StreamingStatement>),
314}
315
316/// Parsed watermark definition.
317#[derive(Debug, Clone, PartialEq)]
318pub struct WatermarkDef {
319    /// Event-time column.
320    pub column: Ident,
321    /// Watermark expression (e.g., column - INTERVAL '5' SECOND).
322    /// `None` when `WATERMARK FOR col` is used without `AS expr`,
323    /// meaning watermark advances via `source.watermark()` with zero delay.
324    pub expression: Option<Expr>,
325}
326
327/// SQL configuration for events that arrive after their window closes.
328#[derive(Debug, Clone, PartialEq, Default)]
329pub struct LateDataClause {
330    /// For example, `INTERVAL '1' HOUR`.
331    pub allowed_lateness: Option<Box<Expr>>,
332    /// Destination for late events.
333    pub side_output: Option<String>,
334}
335
336impl LateDataClause {
337    /// Uses the given lateness without a side output.
338    #[must_use]
339    pub fn with_allowed_lateness(lateness: Expr) -> Self {
340        Self {
341            allowed_lateness: Some(Box::new(lateness)),
342            side_output: None,
343        }
344    }
345
346    /// Uses the given lateness and side output.
347    #[must_use]
348    pub fn with_side_output(lateness: Expr, side_output: String) -> Self {
349        Self {
350            allowed_lateness: Some(Box::new(lateness)),
351            side_output: Some(side_output),
352        }
353    }
354
355    /// Uses the default lateness with a side output.
356    #[must_use]
357    pub fn side_output_only(side_output: String) -> Self {
358        Self {
359            allowed_lateness: None,
360            side_output: Some(side_output),
361        }
362    }
363
364    /// # Errors
365    ///
366    /// Returns `ParseError::WindowError` if the interval cannot be parsed.
367    pub fn to_allowed_lateness(&self) -> Result<Duration, ParseError> {
368        match &self.allowed_lateness {
369            Some(expr) => WindowRewriter::parse_interval_to_duration(expr),
370            None => Ok(Duration::ZERO),
371        }
372    }
373
374    #[must_use]
375    /// Whether late events have a side-output destination.
376    pub fn has_side_output(&self) -> bool {
377        self.side_output.is_some()
378    }
379
380    #[must_use]
381    /// The configured side-output destination.
382    pub fn get_side_output(&self) -> Option<&str> {
383        self.side_output.as_deref()
384    }
385}
386
387/// Runtime output strategy for a streaming operator.
388#[derive(Debug, Clone, PartialEq)]
389pub enum EmitStrategy {
390    /// Emit when the watermark passes a window end.
391    OnWatermark,
392    /// Emit only when a window closes.
393    OnWindowClose,
394    /// Emit at a fixed interval.
395    Periodic(Duration),
396    /// Emit after every state change.
397    OnUpdate,
398    /// Emit Z-set changelog records.
399    Changelog,
400    /// Suppress intermediate results.
401    FinalOnly,
402}
403
404/// Parsed `EMIT` strategy.
405#[derive(Debug, Clone, PartialEq)]
406pub enum EmitClause {
407    /// Emit when the watermark passes the window end.
408    AfterWatermark,
409
410    /// Emit only when the window closes; this is distinct from `AfterWatermark`.
411    OnWindowClose,
412
413    /// Emit intermediate results periodically and final results on watermark.
414    Periodically {
415        /// Parsed interval expression.
416        interval: Box<Expr>,
417    },
418
419    /// Emit after every state change.
420    OnUpdate,
421
422    /// Emit Z-set changelog weights, including retraction pairs for updates.
423    Changes,
424
425    /// Emit only finalized results and drop data arriving after window close.
426    Final,
427}
428
429impl std::fmt::Display for EmitClause {
430    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
431        match self {
432            EmitClause::AfterWatermark => write!(f, "EMIT AFTER WATERMARK"),
433            EmitClause::OnWindowClose => write!(f, "EMIT ON WINDOW CLOSE"),
434            EmitClause::Periodically { interval } => write!(f, "EMIT EVERY {interval}"),
435            EmitClause::OnUpdate => write!(f, "EMIT ON UPDATE"),
436            EmitClause::Changes => write!(f, "EMIT CHANGES"),
437            EmitClause::Final => write!(f, "EMIT FINAL"),
438        }
439    }
440}
441
442impl EmitClause {
443    /// # Errors
444    ///
445    /// Returns `ParseError::WindowError` if the periodic interval cannot be parsed.
446    pub fn to_emit_strategy(&self) -> Result<EmitStrategy, ParseError> {
447        match self {
448            EmitClause::AfterWatermark => Ok(EmitStrategy::OnWatermark),
449            EmitClause::OnWindowClose => Ok(EmitStrategy::OnWindowClose),
450            EmitClause::Periodically { interval } => {
451                let duration = WindowRewriter::parse_interval_to_duration(interval)?;
452                Ok(EmitStrategy::Periodic(duration))
453            }
454            EmitClause::OnUpdate => Ok(EmitStrategy::OnUpdate),
455            EmitClause::Changes => Ok(EmitStrategy::Changelog),
456            EmitClause::Final => Ok(EmitStrategy::FinalOnly),
457        }
458    }
459
460    /// Whether the strategy can emit retractions.
461    #[must_use]
462    pub fn requires_changelog(&self) -> bool {
463        matches!(self, EmitClause::Changes | EmitClause::OnUpdate)
464    }
465
466    /// Whether the strategy produces no retractions.
467    #[must_use]
468    pub fn is_append_only(&self) -> bool {
469        matches!(
470            self,
471            EmitClause::OnWindowClose | EmitClause::Final | EmitClause::AfterWatermark
472        )
473    }
474
475    /// Whether output depends on source watermark advancement.
476    ///
477    /// `OnWindowClose`, `Final`, and `AfterWatermark` all depend on watermark
478    /// advancement to trigger window closure. Without a watermark, timers will
479    /// never fire and windows will never close.
480    #[must_use]
481    pub fn requires_watermark(&self) -> bool {
482        matches!(
483            self,
484            EmitClause::OnWindowClose | EmitClause::Final | EmitClause::AfterWatermark
485        )
486    }
487}
488
489/// Parsed streaming window function.
490#[derive(Debug, Clone, PartialEq)]
491pub enum WindowFunction {
492    /// `TUMBLE(column, interval [, offset])`.
493    Tumble {
494        /// Event-time expression.
495        time_column: Box<Expr>,
496        /// Window size.
497        interval: Box<Expr>,
498        /// Optional timezone-alignment offset.
499        offset: Option<Box<Expr>>,
500    },
501    /// `HOP(column, slide, size [, offset])`.
502    Hop {
503        /// Event-time expression.
504        time_column: Box<Expr>,
505        /// How often a new window begins.
506        slide_interval: Box<Expr>,
507        /// Window size.
508        window_interval: Box<Expr>,
509        /// Optional timezone-alignment offset.
510        offset: Option<Box<Expr>>,
511    },
512    /// `SESSION(column, gap)`.
513    Session {
514        /// Event-time expression.
515        time_column: Box<Expr>,
516        /// Maximum gap between events in one session.
517        gap_interval: Box<Expr>,
518    },
519    /// `CUMULATE(column, step, size)`.
520    Cumulate {
521        /// Event-time expression.
522        time_column: Box<Expr>,
523        /// Window growth increment.
524        step_interval: Box<Expr>,
525        /// Maximum window size.
526        max_size_interval: Box<Expr>,
527    },
528}
529
530#[cfg(test)]
531mod tests;