Skip to main content

hawdb_sql/
ast.rs

1// Copyright 2026 Nowledge
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15pub use hawdb_expression::sql::{
16    Expr, ExprKind, SqlColumnRef, SqlComparisonOp, SqlExpression, SqlFunctionArgument,
17    SqlIndexColumn, SqlLikeEscape, SqlNullOrder, SqlOrderDirection, SqlOrderItem, SqlPredicate,
18    SqlSourceLocation, SqlSourceSpan, SqlValue,
19};
20
21use caseless::Caseless;
22use hawdb_core::{HawDBError, LogicalType, Result};
23
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum SqlStatement {
26    Explain(SqlExplainStatement),
27    Select(SelectStatement),
28    Insert(InsertStatement),
29    Update(UpdateStatement),
30    Delete(DeleteStatement),
31    CreateTable(CreateTableStatement),
32    CreateIndex(CreateIndexStatement),
33    CreateMaterializedView(CreateMaterializedViewStatement),
34    DropMaterializedView(DropMaterializedViewStatement),
35    AlterTableAddColumn(AlterTableAddColumnStatement),
36    /// Project-scoped branch lifecycle commands. These are deliberately
37    /// distinct from relational DDL: their executor operates on the durable
38    /// branch catalog rather than a selected branch's schema.
39    Branch(BranchSqlStatement),
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct CreateMaterializedViewStatement {
44    pub name: SqlTableName,
45    pub if_not_exists: bool,
46    pub query: Box<SelectStatement>,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct DropMaterializedViewStatement {
51    pub name: SqlTableName,
52    pub if_exists: bool,
53}
54
55/// A branch lifecycle command parsed by the PostgreSQL entrypoint.
56///
57/// Keeping this as an AST family makes branch operations visible to the query
58/// runtime and prevents a host integration from recognizing command strings
59/// itself. Lifecycle and selection commands dispatch outside ordinary implicit
60/// data transactions through the storage-owned admission/publication kernel.
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub enum BranchSqlStatement {
63    ShowBranches(ShowBranchesStatement),
64    ShowBranch(ShowBranchStatement),
65    ShowCurrentBranch,
66    UseBranch(BranchSqlSelector),
67    CreateBranch(CreateBranchStatement),
68    DropBranch(DropBranchStatement),
69}
70
71impl BranchSqlStatement {
72    pub const fn changes_context_or_catalog(&self) -> bool {
73        matches!(
74            self,
75            Self::UseBranch(_) | Self::CreateBranch(_) | Self::DropBranch(_)
76        )
77    }
78
79    pub const fn command(&self) -> &'static str {
80        match self {
81            Self::ShowBranches(_) => "SHOW BRANCHES",
82            Self::ShowBranch(_) => "SHOW BRANCH",
83            Self::ShowCurrentBranch => "SHOW CURRENT BRANCH",
84            Self::UseBranch(_) => "USE BRANCH",
85            Self::CreateBranch(_) => "CREATE BRANCH",
86            Self::DropBranch(_) => "DROP BRANCH",
87        }
88    }
89}
90
91#[derive(Debug, Clone, PartialEq, Eq)]
92pub struct CreateBranchStatement {
93    pub name: Option<BranchSqlValue>,
94    pub source: BranchSqlSelector,
95    pub expected_source_revision: SqlBound,
96    pub request_key: BranchSqlValue,
97    pub owner: Option<BranchSqlValue>,
98}
99
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct DropBranchStatement {
102    pub id: BranchSqlValue,
103    pub expected_metadata_revision: SqlBound,
104}
105
106/// A bounded page over the durable branch catalog.
107#[derive(Debug, Clone, PartialEq, Eq)]
108pub struct ShowBranchesStatement {
109    pub limit: SqlBound,
110    pub offset: Option<SqlBound>,
111}
112
113#[derive(Debug, Clone, PartialEq, Eq)]
114pub struct ShowBranchStatement {
115    pub selector: BranchSqlSelector,
116}
117
118/// A typed selector avoids treating UUID-looking names as identifiers. The
119/// runtime validates `Id` values as UUIDs only after parameter binding.
120#[derive(Debug, Clone, PartialEq, Eq)]
121pub enum BranchSqlSelector {
122    Name(BranchSqlValue),
123    Id(BranchSqlValue),
124}
125
126/// A branch name, UUID, or opaque selector parameter. Branch commands accept
127/// only string literals or PostgreSQL positional parameters, never SQL
128/// expression evaluation or interpolated text.
129#[derive(Debug, Clone, PartialEq, Eq)]
130pub enum BranchSqlValue {
131    Literal(String),
132    Parameter(usize),
133}
134
135#[derive(Debug, Clone, PartialEq, Eq)]
136pub struct SqlExplainStatement {
137    pub analyze: bool,
138    pub statement: Box<SqlStatement>,
139}
140
141#[derive(Debug, Clone, PartialEq, Eq)]
142pub struct SqlVectorIndexSearch {
143    pub index: SqlValue,
144    pub embedding: SqlValue,
145    pub mode: hawdb_core::VectorIndexQueryMode,
146    pub candidates: Option<SqlValue>,
147    pub max_work: Option<SqlValue>,
148    pub top_k: SqlValue,
149}
150
151#[derive(Debug, Clone, PartialEq, Eq)]
152pub struct SelectStatement {
153    pub vector_index: Option<Box<SqlVectorIndexSearch>>,
154    pub projection: Vec<SelectProjection>,
155    pub distinct: bool,
156    /// `None` for a FROM-less SELECT (e.g. `SELECT version();`): the
157    /// projection is evaluated exactly once against an implicit row with no
158    /// column bindings, so any column reference in it is rejected the same
159    /// way an unresolvable column against a real table would be.
160    pub from: Option<SqlTableName>,
161    pub from_alias: Option<String>,
162    pub joins: Vec<SqlJoin>,
163    pub selection: Option<SqlPredicate>,
164    pub group_by: Vec<SqlColumnRef>,
165    pub having: Option<SqlPredicate>,
166    pub order_by: Vec<SqlOrderItem>,
167    pub limit: Option<SqlBound>,
168    pub offset: Option<SqlBound>,
169    pub lock_strength: Option<SqlLockStrength>,
170}
171
172impl SelectStatement {
173    /// The base table, for every code path that only ever runs on a real
174    /// table (catalog resolution, access-path selection, join planning, row
175    /// scanning). FROM-less selects never reach these paths — they're
176    /// handled by a separate, earlier branch — so `None` here is a caller
177    /// bug, not a user-facing state.
178    pub fn from_table(&self) -> &SqlTableName {
179        self.from
180            .as_ref()
181            .expect("from_table called on a FROM-less SELECT; it must take the fromless path")
182    }
183}
184
185#[derive(Debug, Clone, Copy, PartialEq, Eq)]
186pub enum SqlLockStrength {
187    Share,
188    Update,
189}
190
191#[derive(Debug, Clone, PartialEq, Eq)]
192pub struct InsertStatement {
193    pub table: SqlTableName,
194    pub columns: Vec<String>,
195    pub rows: Vec<Vec<SqlValue>>,
196    pub on_conflict: Option<SqlOnConflict>,
197    pub returning: Vec<SqlColumnRef>,
198}
199
200#[derive(Debug, Clone, PartialEq, Eq)]
201pub struct UpdateStatement {
202    pub table: SqlTableName,
203    pub alias: Option<String>,
204    pub assignments: Vec<SqlAssignment>,
205    pub selection: Option<SqlPredicate>,
206}
207
208#[derive(Debug, Clone, PartialEq, Eq)]
209pub struct DeleteStatement {
210    pub table: SqlTableName,
211    pub alias: Option<String>,
212    pub selection: Option<SqlPredicate>,
213}
214
215#[derive(Debug, Clone, PartialEq, Eq)]
216pub struct SqlAssignment {
217    pub column: String,
218    pub value: SqlAssignmentValue,
219}
220
221#[derive(Debug, Clone, PartialEq, Eq)]
222pub enum SqlAssignmentValue {
223    Expression(Expr),
224    Value(SqlValue),
225    Column(SqlColumnRef),
226    Arithmetic {
227        left: SqlArithmeticOperand,
228        operator: SqlArithmeticOperator,
229        right: SqlArithmeticOperand,
230    },
231}
232
233#[derive(Debug, Clone, PartialEq, Eq)]
234pub enum SqlArithmeticOperand {
235    Value(SqlValue),
236    Column(SqlColumnRef),
237}
238
239#[derive(Debug, Clone, Copy, PartialEq, Eq)]
240pub enum SqlArithmeticOperator {
241    Add,
242    Subtract,
243}
244
245#[derive(Debug, Clone, PartialEq, Eq)]
246pub struct SqlOnConflict {
247    pub columns: Vec<String>,
248    pub action: SqlConflictAction,
249}
250
251#[derive(Debug, Clone, PartialEq, Eq)]
252pub enum SqlConflictAction {
253    DoNothing,
254    DoUpdate(Vec<SqlAssignment>),
255}
256
257#[derive(Debug, Clone, PartialEq, Eq)]
258pub struct CreateTableStatement {
259    pub table: SqlTableName,
260    pub if_not_exists: bool,
261    pub columns: Vec<SqlColumnDefinition>,
262    pub constraints: Vec<SqlTableConstraint>,
263    pub storage: SqlTableStorage,
264}
265
266#[derive(Debug, Clone, PartialEq, Eq, Default)]
267pub enum SqlTableStorage {
268    #[default]
269    RowPage,
270    StrictAppend {
271        partition_key: Vec<String>,
272        order_key: Vec<String>,
273        generated_order: SqlGeneratedOrder,
274    },
275}
276
277#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
278pub enum SqlGeneratedOrder {
279    #[default]
280    CallerProvided,
281    CommitSequence,
282}
283
284#[derive(Debug, Clone, PartialEq, Eq)]
285pub struct SqlColumnDefinition {
286    pub name: String,
287    pub data_type: SqlDataType,
288    pub nullable: bool,
289    pub default: Option<SqlColumnDefault>,
290    pub primary_key: bool,
291    pub unique: bool,
292    pub references: Option<SqlForeignKeyReference>,
293}
294
295#[derive(Debug, Clone, PartialEq, Eq)]
296pub enum SqlColumnDefault {
297    Literal(SqlValue),
298    UuidV7,
299    /// `CURRENT_TIMESTAMP[(p)]`, `now()` or `LOCALTIMESTAMP[(p)]`, evaluated
300    /// for each inserted row from the transaction-start clock.
301    CurrentTimestamp(hawdb_core::sql_timestamp::TimestampType),
302}
303
304#[derive(Debug, Clone, Copy, PartialEq, Eq)]
305pub enum SqlDataType {
306    Boolean,
307    BigInt,
308    DoublePrecision,
309    Decimal(hawdb_core::DecimalType),
310    Text,
311    Bytea,
312    Uuid,
313    Vector(hawdb_core::VectorDimension),
314    /// `TIMESTAMP [ (p) ] [ WITH[OUT] TIME ZONE ]` and `TIMESTAMPTZ [ (p) ]`.
315    Timestamp(hawdb_core::sql_timestamp::TimestampType),
316}
317
318impl SqlDataType {
319    pub const fn logical_type(self) -> LogicalType {
320        match self {
321            Self::Boolean => LogicalType::Boolean,
322            Self::BigInt => LogicalType::Int64,
323            Self::DoublePrecision => LogicalType::Float64,
324            Self::Decimal(_) => LogicalType::Decimal,
325            Self::Text => LogicalType::Text,
326            Self::Bytea => LogicalType::Binary,
327            Self::Uuid => LogicalType::Uuid,
328            Self::Vector(dimension) => LogicalType::Vector(dimension),
329            Self::Timestamp(kind) if kind.zoned => LogicalType::DateTime,
330            Self::Timestamp(_) => LogicalType::LocalDateTime,
331        }
332    }
333}
334
335#[derive(Debug, Clone, PartialEq, Eq)]
336pub enum SqlTableConstraint {
337    PrimaryKey(Vec<String>),
338    Unique(Vec<String>),
339    ForeignKey {
340        columns: Vec<String>,
341        reference: SqlForeignKeyReference,
342    },
343}
344
345#[derive(Debug, Clone, PartialEq, Eq)]
346pub struct SqlForeignKeyReference {
347    pub table: SqlTableName,
348    pub columns: Vec<String>,
349    pub on_delete: SqlReferentialAction,
350    pub on_update: SqlReferentialAction,
351}
352
353#[derive(Debug, Clone, Copy, PartialEq, Eq)]
354pub enum SqlReferentialAction {
355    NoAction,
356    Restrict,
357    Cascade,
358    SetNull,
359}
360
361#[derive(Debug, Clone, PartialEq, Eq)]
362pub struct CreateIndexStatement {
363    pub name: String,
364    pub table: SqlTableName,
365    pub columns: Vec<SqlIndexColumn>,
366    pub unique: bool,
367    pub if_not_exists: bool,
368}
369
370#[derive(Debug, Clone, PartialEq, Eq)]
371pub struct AlterTableAddColumnStatement {
372    pub table: SqlTableName,
373    pub if_not_exists: bool,
374    pub column: SqlColumnDefinition,
375}
376
377#[derive(Debug, Clone, Copy, PartialEq, Eq)]
378pub enum SqlBound {
379    Literal(u64),
380    Parameter(usize),
381}
382
383#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
384pub struct SqlTableName {
385    pub schema: Option<String>,
386    pub name: String,
387}
388
389#[derive(Debug, Clone, PartialEq, Eq)]
390pub enum SelectProjection {
391    Wildcard,
392    Expression {
393        expression: SqlExpression,
394        alias: Option<String>,
395    },
396}
397
398#[derive(Debug, Clone, PartialEq, Eq)]
399pub struct SqlJoin {
400    pub kind: SqlJoinKind,
401    pub table: SqlTableName,
402    pub alias: Option<String>,
403    pub on: SqlPredicate,
404    /// First input visible to ON, with the SELECT base relation at ordinal zero.
405    /// The current join's right input is the inclusive end of this scope.
406    pub on_scope_start: usize,
407}
408
409#[derive(Debug, Clone, Copy, PartialEq, Eq)]
410pub enum SqlJoinKind {
411    Inner,
412    Left,
413}
414
415/// Matches SQL LIKE patterns with locale-independent Unicode default case folding for ILIKE.
416pub fn sql_like_matches(
417    value: &str,
418    pattern: &str,
419    escape: SqlLikeEscape,
420    case_insensitive: bool,
421) -> Result<bool> {
422    let layout = like_layout(pattern, escape, case_insensitive)?;
423    let value_chars = value.chars().count();
424    layout.scratch_bytes(value_chars)?;
425    let (tokens, literals) = tokenize_like_pattern(pattern, escape, case_insensitive, &layout)?;
426    let mut characters = Vec::with_capacity(value_chars);
427    characters.extend(value.chars());
428    let value = characters;
429
430    let mut value_index = 0;
431    let mut pattern_index = 0;
432    let mut wildcard_index = None;
433    let mut wildcard_value_index = 0;
434    while value_index < value.len() {
435        let matched = match tokens.get(pattern_index) {
436            Some(LikeToken::Literal(expected)) => {
437                if let Some(consumed) = match_like_literal(
438                    &value[value_index..],
439                    &literals[expected.clone()],
440                    case_insensitive,
441                ) {
442                    value_index += consumed;
443                    pattern_index += 1;
444                    true
445                } else {
446                    false
447                }
448            }
449            Some(LikeToken::One) => {
450                value_index += 1;
451                pattern_index += 1;
452                true
453            }
454            Some(LikeToken::Many) => {
455                wildcard_index = Some(pattern_index);
456                wildcard_value_index = value_index;
457                pattern_index += 1;
458                true
459            }
460            None => false,
461        };
462        if matched {
463            continue;
464        }
465        let Some(wildcard_index) = wildcard_index else {
466            return Ok(false);
467        };
468        wildcard_value_index += 1;
469        value_index = wildcard_value_index;
470        pattern_index = wildcard_index + 1;
471    }
472    while matches!(tokens.get(pattern_index), Some(LikeToken::Many)) {
473        pattern_index += 1;
474    }
475    Ok(pattern_index == tokens.len())
476}
477
478/// Requested heap bytes used by the LIKE matcher, before allocating its buffers.
479/// Escape parsing and Unicode folding share the matcher's exact layout pass.
480#[doc(hidden)]
481pub fn sql_like_scratch_bytes(
482    value: &str,
483    pattern: &str,
484    escape: SqlLikeEscape,
485    case_insensitive: bool,
486) -> Result<usize> {
487    like_layout(pattern, escape, case_insensitive)?.scratch_bytes(value.chars().count())
488}
489
490struct LikeLayout {
491    tokens: usize,
492    literal_bytes: usize,
493}
494
495impl LikeLayout {
496    fn scratch_bytes(&self, value_chars: usize) -> Result<usize> {
497        self.tokens
498            .checked_mul(std::mem::size_of::<LikeToken>())
499            .and_then(|bytes| bytes.checked_add(self.literal_bytes))
500            .and_then(|bytes| {
501                value_chars
502                    .checked_mul(std::mem::size_of::<char>())
503                    .and_then(|value| bytes.checked_add(value))
504            })
505            .ok_or_else(like_size_overflow)
506    }
507}
508
509#[derive(Debug, Clone)]
510enum LikeToken {
511    Literal(std::ops::Range<usize>),
512    One,
513    Many,
514}
515
516#[derive(Clone, Copy)]
517enum LikePart {
518    Literal(char),
519    One,
520    Many,
521}
522
523fn like_size_overflow() -> HawDBError {
524    HawDBError::Execution("LIKE scratch length overflow".into())
525}
526
527fn visit_like_pattern(
528    pattern: &str,
529    escape: SqlLikeEscape,
530    mut visit: impl FnMut(LikePart) -> Result<()>,
531) -> Result<()> {
532    let escape = match escape {
533        SqlLikeEscape::Character(character) => Some(character),
534        SqlLikeEscape::Disabled => None,
535    };
536    let mut characters = pattern.chars();
537    while let Some(character) = characters.next() {
538        let part = if Some(character) == escape {
539            LikePart::Literal(characters.next().ok_or_else(|| {
540                HawDBError::Semantic("LIKE pattern ends with its escape character".to_string())
541            })?)
542        } else {
543            match character {
544                '%' => LikePart::Many,
545                '_' => LikePart::One,
546                literal => LikePart::Literal(literal),
547            }
548        };
549        visit(part)?;
550    }
551    Ok(())
552}
553
554fn visit_like_literal(
555    literal: char,
556    case_insensitive: bool,
557    mut visit: impl FnMut(char) -> Result<()>,
558) -> Result<()> {
559    if case_insensitive {
560        for character in std::iter::once(literal).default_case_fold() {
561            visit(character)?;
562        }
563        Ok(())
564    } else {
565        visit(literal)
566    }
567}
568
569fn like_layout(pattern: &str, escape: SqlLikeEscape, case_insensitive: bool) -> Result<LikeLayout> {
570    let mut layout = LikeLayout {
571        tokens: 0,
572        literal_bytes: 0,
573    };
574    let mut previous = None;
575    visit_like_pattern(pattern, escape, |part| {
576        let merged = matches!(
577            (previous, part),
578            (Some(LikePart::Many), LikePart::Many)
579                | (Some(LikePart::Literal(_)), LikePart::Literal(_))
580        );
581        if !merged {
582            layout.tokens = layout
583                .tokens
584                .checked_add(1)
585                .ok_or_else(like_size_overflow)?;
586        }
587        if let LikePart::Literal(literal) = part {
588            visit_like_literal(literal, case_insensitive, |character| {
589                layout.literal_bytes = layout
590                    .literal_bytes
591                    .checked_add(character.len_utf8())
592                    .ok_or_else(like_size_overflow)?;
593                Ok(())
594            })?;
595        }
596        previous = Some(part);
597        Ok(())
598    })?;
599    Ok(layout)
600}
601
602fn tokenize_like_pattern(
603    pattern: &str,
604    escape: SqlLikeEscape,
605    case_insensitive: bool,
606    layout: &LikeLayout,
607) -> Result<(Vec<LikeToken>, String)> {
608    let mut tokens = Vec::with_capacity(layout.tokens);
609    let mut literals = String::with_capacity(layout.literal_bytes);
610    visit_like_pattern(pattern, escape, |part| {
611        match part {
612            LikePart::Literal(literal) => {
613                let start = literals.len();
614                visit_like_literal(literal, case_insensitive, |character| {
615                    literals.push(character);
616                    Ok(())
617                })?;
618                if let Some(LikeToken::Literal(range)) = tokens.last_mut() {
619                    range.end = literals.len();
620                } else {
621                    tokens.push(LikeToken::Literal(start..literals.len()));
622                }
623            }
624            LikePart::Many if !matches!(tokens.last(), Some(LikeToken::Many)) => {
625                tokens.push(LikeToken::Many)
626            }
627            LikePart::Many => {}
628            LikePart::One => tokens.push(LikeToken::One),
629        }
630        Ok(())
631    })?;
632    debug_assert_eq!(tokens.len(), layout.tokens);
633    debug_assert_eq!(literals.len(), layout.literal_bytes);
634    Ok((tokens, literals))
635}
636
637fn match_like_literal(value: &[char], expected: &str, case_insensitive: bool) -> Option<usize> {
638    let mut expected = expected.chars().peekable();
639    for (index, character) in value.iter().copied().enumerate() {
640        if case_insensitive {
641            for folded in std::iter::once(character).default_case_fold() {
642                if expected.next() != Some(folded) {
643                    return None;
644                }
645            }
646        } else if expected.next() != Some(character) {
647            return None;
648        }
649        // Wildcards and literal boundaries consume complete source characters,
650        // even when one character folds into several code points.
651        if expected.peek().is_none() {
652            return Some(index + 1);
653        }
654    }
655    None
656}
657
658#[cfg(test)]
659mod tests {
660    use super::*;
661
662    #[test]
663    fn postgres_data_types_use_shared_logical_types() {
664        assert_eq!(SqlDataType::Boolean.logical_type(), LogicalType::Boolean);
665        assert_eq!(SqlDataType::BigInt.logical_type(), LogicalType::Int64);
666        assert_eq!(
667            SqlDataType::DoublePrecision.logical_type(),
668            LogicalType::Float64
669        );
670        assert_eq!(SqlDataType::Text.logical_type(), LogicalType::Text);
671        assert_eq!(SqlDataType::Bytea.logical_type(), LogicalType::Binary);
672    }
673}