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