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