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 SelectStatement {
128    pub projection: Vec<SelectProjection>,
129    pub distinct: bool,
130    /// `None` for a FROM-less SELECT (e.g. `SELECT version();`): the
131    /// projection is evaluated exactly once against an implicit row with no
132    /// column bindings, so any column reference in it is rejected the same
133    /// way an unresolvable column against a real table would be.
134    pub from: Option<SqlTableName>,
135    pub from_alias: Option<String>,
136    pub joins: Vec<SqlJoin>,
137    pub selection: Option<SqlPredicate>,
138    pub group_by: Vec<SqlColumnRef>,
139    pub having: Option<SqlPredicate>,
140    pub order_by: Vec<SqlOrderItem>,
141    pub limit: Option<SqlBound>,
142    pub offset: Option<SqlBound>,
143    pub lock_strength: Option<SqlLockStrength>,
144}
145
146impl SelectStatement {
147    /// The base table, for every code path that only ever runs on a real
148    /// table (catalog resolution, access-path selection, join planning, row
149    /// scanning). FROM-less selects never reach these paths — they're
150    /// handled by a separate, earlier branch — so `None` here is a caller
151    /// bug, not a user-facing state.
152    pub fn from_table(&self) -> &SqlTableName {
153        self.from
154            .as_ref()
155            .expect("from_table called on a FROM-less SELECT; it must take the fromless path")
156    }
157}
158
159#[derive(Debug, Clone, Copy, PartialEq, Eq)]
160pub enum SqlLockStrength {
161    Share,
162    Update,
163}
164
165#[derive(Debug, Clone, PartialEq, Eq)]
166pub struct InsertStatement {
167    pub table: SqlTableName,
168    pub columns: Vec<String>,
169    pub rows: Vec<Vec<SqlValue>>,
170    pub on_conflict: Option<SqlOnConflict>,
171    pub returning: Vec<SqlColumnRef>,
172}
173
174#[derive(Debug, Clone, PartialEq, Eq)]
175pub struct UpdateStatement {
176    pub table: SqlTableName,
177    pub alias: Option<String>,
178    pub assignments: Vec<SqlAssignment>,
179    pub selection: Option<SqlPredicate>,
180}
181
182#[derive(Debug, Clone, PartialEq, Eq)]
183pub struct DeleteStatement {
184    pub table: SqlTableName,
185    pub alias: Option<String>,
186    pub selection: Option<SqlPredicate>,
187}
188
189#[derive(Debug, Clone, PartialEq, Eq)]
190pub struct SqlAssignment {
191    pub column: String,
192    pub value: SqlAssignmentValue,
193}
194
195#[derive(Debug, Clone, PartialEq, Eq)]
196pub enum SqlAssignmentValue {
197    Value(SqlValue),
198    Column(SqlColumnRef),
199    Arithmetic {
200        left: SqlArithmeticOperand,
201        operator: SqlArithmeticOperator,
202        right: SqlArithmeticOperand,
203    },
204}
205
206#[derive(Debug, Clone, PartialEq, Eq)]
207pub enum SqlArithmeticOperand {
208    Value(SqlValue),
209    Column(SqlColumnRef),
210}
211
212#[derive(Debug, Clone, Copy, PartialEq, Eq)]
213pub enum SqlArithmeticOperator {
214    Add,
215    Subtract,
216}
217
218#[derive(Debug, Clone, PartialEq, Eq)]
219pub struct SqlOnConflict {
220    pub columns: Vec<String>,
221    pub action: SqlConflictAction,
222}
223
224#[derive(Debug, Clone, PartialEq, Eq)]
225pub enum SqlConflictAction {
226    DoNothing,
227    DoUpdate(Vec<SqlAssignment>),
228}
229
230#[derive(Debug, Clone, PartialEq, Eq)]
231pub struct CreateTableStatement {
232    pub table: SqlTableName,
233    pub if_not_exists: bool,
234    pub columns: Vec<SqlColumnDefinition>,
235    pub constraints: Vec<SqlTableConstraint>,
236    pub storage: SqlTableStorage,
237}
238
239#[derive(Debug, Clone, PartialEq, Eq, Default)]
240pub enum SqlTableStorage {
241    #[default]
242    RowPage,
243    StrictAppend {
244        partition_key: Vec<String>,
245        order_key: Vec<String>,
246        generated_order: SqlGeneratedOrder,
247    },
248}
249
250#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
251pub enum SqlGeneratedOrder {
252    #[default]
253    CallerProvided,
254    CommitSequence,
255}
256
257#[derive(Debug, Clone, PartialEq, Eq)]
258pub struct SqlColumnDefinition {
259    pub name: String,
260    pub data_type: SqlDataType,
261    pub nullable: bool,
262    pub default: Option<SqlColumnDefault>,
263    pub primary_key: bool,
264    pub unique: bool,
265    pub references: Option<SqlForeignKeyReference>,
266}
267
268#[derive(Debug, Clone, PartialEq, Eq)]
269pub enum SqlColumnDefault {
270    Literal(SqlValue),
271    UuidV7,
272}
273
274#[derive(Debug, Clone, Copy, PartialEq, Eq)]
275pub enum SqlDataType {
276    Boolean,
277    BigInt,
278    DoublePrecision,
279    Text,
280    Bytea,
281    Uuid,
282}
283
284impl SqlDataType {
285    pub const fn logical_type(self) -> LogicalType {
286        match self {
287            Self::Boolean => LogicalType::Boolean,
288            Self::BigInt => LogicalType::Int64,
289            Self::DoublePrecision => LogicalType::Float64,
290            Self::Text => LogicalType::Text,
291            Self::Bytea => LogicalType::Binary,
292            Self::Uuid => LogicalType::Uuid,
293        }
294    }
295}
296
297#[derive(Debug, Clone, PartialEq, Eq)]
298pub enum SqlTableConstraint {
299    PrimaryKey(Vec<String>),
300    Unique(Vec<String>),
301    ForeignKey {
302        columns: Vec<String>,
303        reference: SqlForeignKeyReference,
304    },
305}
306
307#[derive(Debug, Clone, PartialEq, Eq)]
308pub struct SqlForeignKeyReference {
309    pub table: SqlTableName,
310    pub columns: Vec<String>,
311    pub on_delete: SqlReferentialAction,
312    pub on_update: SqlReferentialAction,
313}
314
315#[derive(Debug, Clone, Copy, PartialEq, Eq)]
316pub enum SqlReferentialAction {
317    NoAction,
318    Restrict,
319    Cascade,
320    SetNull,
321}
322
323#[derive(Debug, Clone, PartialEq, Eq)]
324pub struct CreateIndexStatement {
325    pub name: String,
326    pub table: SqlTableName,
327    pub columns: Vec<SqlIndexColumn>,
328    pub unique: bool,
329    pub if_not_exists: bool,
330}
331
332#[derive(Debug, Clone, PartialEq, Eq)]
333pub struct AlterTableAddColumnStatement {
334    pub table: SqlTableName,
335    pub if_not_exists: bool,
336    pub column: SqlColumnDefinition,
337}
338
339#[derive(Debug, Clone, Copy, PartialEq, Eq)]
340pub enum SqlBound {
341    Literal(u64),
342    Parameter(usize),
343}
344
345#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
346pub struct SqlTableName {
347    pub schema: Option<String>,
348    pub name: String,
349}
350
351#[derive(Debug, Clone, PartialEq, Eq)]
352pub enum SelectProjection {
353    Wildcard,
354    Expression {
355        expression: SqlExpression,
356        alias: Option<String>,
357    },
358}
359
360#[derive(Debug, Clone, PartialEq, Eq)]
361pub struct SqlJoin {
362    pub kind: SqlJoinKind,
363    pub table: SqlTableName,
364    pub alias: Option<String>,
365    pub on: SqlPredicate,
366    /// First input visible to ON, with the SELECT base relation at ordinal zero.
367    /// The current join's right input is the inclusive end of this scope.
368    pub on_scope_start: usize,
369}
370
371#[derive(Debug, Clone, Copy, PartialEq, Eq)]
372pub enum SqlJoinKind {
373    Inner,
374    Left,
375}
376
377/// Matches SQL LIKE patterns with locale-independent Unicode default case folding for ILIKE.
378pub fn sql_like_matches(
379    value: &str,
380    pattern: &str,
381    escape: SqlLikeEscape,
382    case_insensitive: bool,
383) -> Result<bool> {
384    let tokens = tokenize_like_pattern(pattern, escape, case_insensitive)?;
385    let value = value.chars().collect::<Vec<_>>();
386
387    let mut value_index = 0;
388    let mut pattern_index = 0;
389    let mut wildcard_index = None;
390    let mut wildcard_value_index = 0;
391    while value_index < value.len() {
392        let matched = match tokens.get(pattern_index) {
393            Some(LikeToken::Literal(expected)) => {
394                if let Some(consumed) =
395                    match_like_literal(&value[value_index..], expected, case_insensitive)
396                {
397                    value_index += consumed;
398                    pattern_index += 1;
399                    true
400                } else {
401                    false
402                }
403            }
404            Some(LikeToken::One) => {
405                value_index += 1;
406                pattern_index += 1;
407                true
408            }
409            Some(LikeToken::Many) => {
410                wildcard_index = Some(pattern_index);
411                wildcard_value_index = value_index;
412                pattern_index += 1;
413                true
414            }
415            None => false,
416        };
417        if matched {
418            continue;
419        }
420        let Some(wildcard_index) = wildcard_index else {
421            return Ok(false);
422        };
423        wildcard_value_index += 1;
424        value_index = wildcard_value_index;
425        pattern_index = wildcard_index + 1;
426    }
427    while matches!(tokens.get(pattern_index), Some(LikeToken::Many)) {
428        pattern_index += 1;
429    }
430    Ok(pattern_index == tokens.len())
431}
432
433#[derive(Debug, Clone)]
434enum LikeToken {
435    Literal(String),
436    One,
437    Many,
438}
439
440fn tokenize_like_pattern(
441    pattern: &str,
442    escape: SqlLikeEscape,
443    case_insensitive: bool,
444) -> Result<Vec<LikeToken>> {
445    let escape = match escape {
446        SqlLikeEscape::Character(character) => Some(character),
447        SqlLikeEscape::Disabled => None,
448    };
449    let mut tokens = Vec::with_capacity(pattern.chars().count());
450    let mut characters = pattern.chars();
451    while let Some(character) = characters.next() {
452        if Some(character) == escape {
453            let escaped = characters.next().ok_or_else(|| {
454                HawDBError::Semantic("LIKE pattern ends with its escape character".to_string())
455            })?;
456            push_like_literal(&mut tokens, escaped, case_insensitive);
457            continue;
458        }
459        match character {
460            '%' if !matches!(tokens.last(), Some(LikeToken::Many)) => tokens.push(LikeToken::Many),
461            '%' => {}
462            '_' => tokens.push(LikeToken::One),
463            literal => push_like_literal(&mut tokens, literal, case_insensitive),
464        }
465    }
466    Ok(tokens)
467}
468
469fn push_like_literal(tokens: &mut Vec<LikeToken>, literal: char, case_insensitive: bool) {
470    let literal = if case_insensitive {
471        std::iter::once(literal).default_case_fold().collect()
472    } else {
473        literal.to_string()
474    };
475    if let Some(LikeToken::Literal(previous)) = tokens.last_mut() {
476        previous.push_str(&literal);
477    } else {
478        tokens.push(LikeToken::Literal(literal));
479    }
480}
481
482fn match_like_literal(value: &[char], expected: &str, case_insensitive: bool) -> Option<usize> {
483    let mut actual = String::new();
484    for (index, character) in value.iter().copied().enumerate() {
485        if case_insensitive {
486            actual.extend(std::iter::once(character).default_case_fold());
487        } else {
488            actual.push(character);
489        }
490        if !expected.starts_with(&actual) {
491            return None;
492        }
493        if actual == expected {
494            return Some(index + 1);
495        }
496    }
497    None
498}
499
500#[cfg(test)]
501mod tests {
502    use super::*;
503
504    #[test]
505    fn postgres_data_types_use_shared_logical_types() {
506        assert_eq!(SqlDataType::Boolean.logical_type(), LogicalType::Boolean);
507        assert_eq!(SqlDataType::BigInt.logical_type(), LogicalType::Int64);
508        assert_eq!(
509            SqlDataType::DoublePrecision.logical_type(),
510            LogicalType::Float64
511        );
512        assert_eq!(SqlDataType::Text.logical_type(), LogicalType::Text);
513        assert_eq!(SqlDataType::Bytea.logical_type(), LogicalType::Binary);
514    }
515}