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    },
134
135    /// `CREATE STREAM` for a named streaming pipeline.
136    CreateStream {
137        /// Stream name.
138        name: ObjectName,
139        /// Backing query.
140        query: Box<StreamingStatement>,
141        /// Optional output strategy.
142        emit_clause: Option<EmitClause>,
143        /// Replace an existing stream.
144        or_replace: bool,
145        /// Ignore an existing stream.
146        if_not_exists: bool,
147        /// Raw query text between `AS` and `EMIT`.
148        query_sql: String,
149        /// `RETAIN_HISTORY` cap in bytes from the trailing `WITH (...)`.
150        retention_bytes: Option<u64>,
151    },
152
153    /// `DROP STREAM`.
154    DropStream {
155        /// Stream to drop.
156        name: ObjectName,
157        /// Ignore a missing stream.
158        if_exists: bool,
159        /// Also drop dependent objects.
160        cascade: bool,
161    },
162
163    /// `ALTER SOURCE`.
164    AlterSource {
165        /// Source to alter.
166        name: ObjectName,
167        /// Requested alteration.
168        operation: AlterSourceOperation,
169    },
170
171    /// `INSERT INTO` a streaming source or table.
172    InsertInto {
173        /// Target source or table.
174        table_name: ObjectName,
175        /// Empty when the statement does not name columns.
176        columns: Vec<Ident>,
177        /// Rows to insert.
178        values: Vec<Vec<Expr>>,
179    },
180
181    /// `CREATE LOOKUP TABLE`.
182    CreateLookupTable(Box<super::lookup_table::CreateLookupTableStatement>),
183
184    /// `DROP LOOKUP TABLE`.
185    DropLookupTable {
186        /// Lookup table to drop.
187        name: ObjectName,
188        /// Ignore a missing table.
189        if_exists: bool,
190    },
191
192    /// Trigger an immediate checkpoint.
193    Checkpoint,
194
195    /// `RESTORE FROM CHECKPOINT <id>`.
196    RestoreCheckpoint {
197        /// Durable checkpoint identifier.
198        checkpoint_id: u64,
199    },
200
201    /// `SUBSCRIBE <name> [WHERE ...] [WITH (...)]`.
202    Subscribe(Box<SubscribeStatement>),
203
204    /// `DECLARE <name> [NO SCROLL] CURSOR [WITHOUT HOLD] FOR SUBSCRIBE …`
205    ///
206    /// Forward-only cursor over a SUBSCRIBE, scoped to the current SimpleQuery
207    /// connection. SCROLL, BINARY, WITH HOLD, INSENSITIVE, and ASENSITIVE are
208    /// rejected at parse time.
209    DeclareCursorForSubscribe {
210        /// Cursor identifier.
211        name: Ident,
212        /// `NO SCROLL` was explicit in the source. We never emit SCROLL.
213        no_scroll: bool,
214        /// The SUBSCRIBE body the cursor wraps.
215        subscribe: Box<SubscribeStatement>,
216    },
217}
218
219/// `SUBSCRIBE <name> [AS OF EPOCH n] [WHERE <fragment>] [WITH (...)]`.
220#[derive(Debug, Clone, PartialEq)]
221pub struct SubscribeStatement {
222    /// Target stream or materialized view.
223    pub name: ObjectName,
224    /// Raw WHERE fragment, compiled by the engine against the target schema.
225    pub filter_sql: Option<String>,
226    /// `AS OF EPOCH n`: replay everything emitted strictly after barrier `n`.
227    pub as_of_epoch: Option<u64>,
228    /// Reserved `WITH` options.
229    pub options: HashMap<String, String>,
230}
231
232/// Operations for ALTER SOURCE statements.
233#[derive(Debug, Clone, PartialEq)]
234pub enum AlterSourceOperation {
235    /// Add a new column: `ALTER SOURCE name ADD COLUMN col_name data_type`
236    AddColumn {
237        /// Column to add.
238        column_def: ColumnDef,
239    },
240    /// Set source properties: `ALTER SOURCE name SET ('key' = 'value', ...)`
241    SetProperties {
242        /// Properties to replace.
243        properties: HashMap<String, String>,
244    },
245}
246
247/// Format specification for serialization (e.g., FORMAT JSON, FORMAT AVRO).
248#[derive(Debug, Clone, PartialEq)]
249pub struct FormatSpec {
250    /// For example, `JSON`, `AVRO`, or `PROTOBUF`.
251    pub format_type: String,
252    /// Additional format options (from WITH clause after FORMAT).
253    pub options: HashMap<String, String>,
254}
255
256/// Parsed `CREATE SOURCE` statement.
257#[derive(Debug, Clone, PartialEq)]
258pub struct CreateSourceStatement {
259    /// Source name.
260    pub name: ObjectName,
261    /// Declared columns.
262    pub columns: Vec<ColumnDef>,
263    /// Primary-key columns, empty when no key is declared.
264    pub primary_key: Vec<Ident>,
265    /// Optional event-time watermark.
266    pub watermark: Option<WatermarkDef>,
267    /// Source runtime options from the trailing `WITH` clause.
268    pub with_options: HashMap<String, String>,
269    /// Replace an existing source.
270    pub or_replace: bool,
271    /// Ignore an existing source.
272    pub if_not_exists: bool,
273    /// Connector type (e.g., "KAFKA") from `FROM KAFKA (...)` syntax
274    pub connector_type: Option<String>,
275    /// Connector-specific options (from `FROM KAFKA (...)`)
276    pub connector_options: HashMap<String, String>,
277    /// Format specification (e.g., `FORMAT JSON`)
278    pub format: Option<FormatSpec>,
279}
280
281/// Parsed `CREATE SINK` statement.
282#[derive(Debug, Clone, PartialEq)]
283pub struct CreateSinkStatement {
284    /// Sink name.
285    pub name: ObjectName,
286    /// Sink input.
287    pub from: SinkFrom,
288    /// Replace an existing sink.
289    pub or_replace: bool,
290    /// Ignore an existing sink.
291    pub if_not_exists: bool,
292    /// Optional row filter.
293    pub filter: Option<Expr>,
294    /// Connector type (e.g., "KAFKA") from `INTO KAFKA (...)` syntax
295    pub connector_type: Option<String>,
296    /// Connector-specific options (from `INTO KAFKA (...)`)
297    pub connector_options: HashMap<String, String>,
298    /// Format specification (e.g., `FORMAT JSON`)
299    pub format: Option<FormatSpec>,
300}
301
302/// Input selected by a sink.
303#[derive(Debug, Clone, PartialEq)]
304pub enum SinkFrom {
305    /// A named table or source.
306    Table(ObjectName),
307    /// A query result.
308    Query(Box<StreamingStatement>),
309}
310
311/// Parsed watermark definition.
312#[derive(Debug, Clone, PartialEq)]
313pub struct WatermarkDef {
314    /// Event-time column.
315    pub column: Ident,
316    /// Watermark expression (e.g., column - INTERVAL '5' SECOND).
317    /// `None` when `WATERMARK FOR col` is used without `AS expr`,
318    /// meaning watermark advances via `source.watermark()` with zero delay.
319    pub expression: Option<Expr>,
320}
321
322/// SQL configuration for events that arrive after their window closes.
323#[derive(Debug, Clone, PartialEq, Default)]
324pub struct LateDataClause {
325    /// For example, `INTERVAL '1' HOUR`.
326    pub allowed_lateness: Option<Box<Expr>>,
327    /// Destination for late events.
328    pub side_output: Option<String>,
329}
330
331impl LateDataClause {
332    /// Uses the given lateness without a side output.
333    #[must_use]
334    pub fn with_allowed_lateness(lateness: Expr) -> Self {
335        Self {
336            allowed_lateness: Some(Box::new(lateness)),
337            side_output: None,
338        }
339    }
340
341    /// Uses the given lateness and side output.
342    #[must_use]
343    pub fn with_side_output(lateness: Expr, side_output: String) -> Self {
344        Self {
345            allowed_lateness: Some(Box::new(lateness)),
346            side_output: Some(side_output),
347        }
348    }
349
350    /// Uses the default lateness with a side output.
351    #[must_use]
352    pub fn side_output_only(side_output: String) -> Self {
353        Self {
354            allowed_lateness: None,
355            side_output: Some(side_output),
356        }
357    }
358
359    /// # Errors
360    ///
361    /// Returns `ParseError::WindowError` if the interval cannot be parsed.
362    pub fn to_allowed_lateness(&self) -> Result<Duration, ParseError> {
363        match &self.allowed_lateness {
364            Some(expr) => WindowRewriter::parse_interval_to_duration(expr),
365            None => Ok(Duration::ZERO),
366        }
367    }
368
369    #[must_use]
370    /// Whether late events have a side-output destination.
371    pub fn has_side_output(&self) -> bool {
372        self.side_output.is_some()
373    }
374
375    #[must_use]
376    /// The configured side-output destination.
377    pub fn get_side_output(&self) -> Option<&str> {
378        self.side_output.as_deref()
379    }
380}
381
382/// Runtime output strategy for a streaming operator.
383#[derive(Debug, Clone, PartialEq)]
384pub enum EmitStrategy {
385    /// Emit when the watermark passes a window end.
386    OnWatermark,
387    /// Emit only when a window closes.
388    OnWindowClose,
389    /// Emit at a fixed interval.
390    Periodic(Duration),
391    /// Emit after every state change.
392    OnUpdate,
393    /// Emit Z-set changelog records.
394    Changelog,
395    /// Suppress intermediate results.
396    FinalOnly,
397}
398
399/// Parsed `EMIT` strategy.
400#[derive(Debug, Clone, PartialEq)]
401pub enum EmitClause {
402    /// Emit when the watermark passes the window end.
403    AfterWatermark,
404
405    /// Emit only when the window closes; this is distinct from `AfterWatermark`.
406    OnWindowClose,
407
408    /// Emit intermediate results periodically and final results on watermark.
409    Periodically {
410        /// Parsed interval expression.
411        interval: Box<Expr>,
412    },
413
414    /// Emit after every state change.
415    OnUpdate,
416
417    /// Emit Z-set changelog weights, including retraction pairs for updates.
418    Changes,
419
420    /// Emit only finalized results and drop data arriving after window close.
421    Final,
422}
423
424impl std::fmt::Display for EmitClause {
425    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
426        match self {
427            EmitClause::AfterWatermark => write!(f, "EMIT AFTER WATERMARK"),
428            EmitClause::OnWindowClose => write!(f, "EMIT ON WINDOW CLOSE"),
429            EmitClause::Periodically { interval } => write!(f, "EMIT EVERY {interval}"),
430            EmitClause::OnUpdate => write!(f, "EMIT ON UPDATE"),
431            EmitClause::Changes => write!(f, "EMIT CHANGES"),
432            EmitClause::Final => write!(f, "EMIT FINAL"),
433        }
434    }
435}
436
437impl EmitClause {
438    /// # Errors
439    ///
440    /// Returns `ParseError::WindowError` if the periodic interval cannot be parsed.
441    pub fn to_emit_strategy(&self) -> Result<EmitStrategy, ParseError> {
442        match self {
443            EmitClause::AfterWatermark => Ok(EmitStrategy::OnWatermark),
444            EmitClause::OnWindowClose => Ok(EmitStrategy::OnWindowClose),
445            EmitClause::Periodically { interval } => {
446                let duration = WindowRewriter::parse_interval_to_duration(interval)?;
447                Ok(EmitStrategy::Periodic(duration))
448            }
449            EmitClause::OnUpdate => Ok(EmitStrategy::OnUpdate),
450            EmitClause::Changes => Ok(EmitStrategy::Changelog),
451            EmitClause::Final => Ok(EmitStrategy::FinalOnly),
452        }
453    }
454
455    /// Whether the strategy can emit retractions.
456    #[must_use]
457    pub fn requires_changelog(&self) -> bool {
458        matches!(self, EmitClause::Changes | EmitClause::OnUpdate)
459    }
460
461    /// Whether the strategy produces no retractions.
462    #[must_use]
463    pub fn is_append_only(&self) -> bool {
464        matches!(
465            self,
466            EmitClause::OnWindowClose | EmitClause::Final | EmitClause::AfterWatermark
467        )
468    }
469
470    /// Whether output depends on source watermark advancement.
471    ///
472    /// `OnWindowClose`, `Final`, and `AfterWatermark` all depend on watermark
473    /// advancement to trigger window closure. Without a watermark, timers will
474    /// never fire and windows will never close.
475    #[must_use]
476    pub fn requires_watermark(&self) -> bool {
477        matches!(
478            self,
479            EmitClause::OnWindowClose | EmitClause::Final | EmitClause::AfterWatermark
480        )
481    }
482}
483
484/// Parsed streaming window function.
485#[derive(Debug, Clone, PartialEq)]
486pub enum WindowFunction {
487    /// `TUMBLE(column, interval [, offset])`.
488    Tumble {
489        /// Event-time expression.
490        time_column: Box<Expr>,
491        /// Window size.
492        interval: Box<Expr>,
493        /// Optional timezone-alignment offset.
494        offset: Option<Box<Expr>>,
495    },
496    /// `HOP(column, slide, size [, offset])`.
497    Hop {
498        /// Event-time expression.
499        time_column: Box<Expr>,
500        /// How often a new window begins.
501        slide_interval: Box<Expr>,
502        /// Window size.
503        window_interval: Box<Expr>,
504        /// Optional timezone-alignment offset.
505        offset: Option<Box<Expr>>,
506    },
507    /// `SESSION(column, gap)`.
508    Session {
509        /// Event-time expression.
510        time_column: Box<Expr>,
511        /// Maximum gap between events in one session.
512        gap_interval: Box<Expr>,
513    },
514    /// `CUMULATE(column, step, size)`.
515    Cumulate {
516        /// Event-time expression.
517        time_column: Box<Expr>,
518        /// Window growth increment.
519        step_interval: Box<Expr>,
520        /// Maximum window size.
521        max_size_interval: Box<Expr>,
522    },
523}
524
525#[cfg(test)]
526mod tests;