Skip to main content

qusql_type/
schema.rs

1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License at
4//
5// http://www.apache.org/licenses/LICENSE-2.0
6//
7// Unless required by applicable law or agreed to in writing, software
8// distributed under the License is distributed on an "AS IS" BASIS,
9// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10// See the License for the specific language governing permissions and
11// limitations under the License.
12
13//! Parse and evaluate SQL schema definitions into a typed representation
14//! used for statement type-checking.
15//!
16//! Supports DDL statements (CREATE/ALTER/DROP for tables, views, functions,
17//! procedures, indices, and types) across MySQL/MariaDB, PostgreSQL/PostGIS,
18//! and SQLite dialects. Includes a limited schema-level evaluator that can
19//! interpret PL/pgSQL function bodies, DO blocks, IF/ELSE control flow,
20//! INSERT/DELETE/TRUNCATE for in-memory row tracking, and expressions
21//! (EXISTS, COALESCE, aggregates). PostgreSQL/PostGIS built-in schemas
22//! (e.g. `spatial_ref_sys`, `geometry_columns`) are injected automatically.
23//!
24//! ```
25//! use qusql_type::{schema::parse_schemas, TypeOptions, SQLDialect, Issues};
26//! let schemas = "
27//!     -- Table structure for table `events`
28//!     DROP TABLE IF EXISTS `events`;
29//!     CREATE TABLE `events` (
30//!       `id` bigint(20) NOT NULL,
31//!       `user` int(11) NOT NULL,
32//!       `event_key` int(11) NOT NULL,
33//!       `time` datetime NOT NULL
34//!     ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
35//!
36//!     -- Table structure for table `events_keys`
37//!     DROP TABLE IF EXISTS `event_keys`;
38//!     CREATE TABLE `event_keys` (
39//!       `id` int(11) NOT NULL,
40//!       `name` text NOT NULL
41//!     ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
42//!
43//!     -- Stand-in structure for view `events_view`
44//!     -- (See below for the actual view)
45//!     DROP VIEW IF EXISTS `events_view`;
46//!     CREATE TABLE `events_view` (
47//!         `id` int(11),
48//!         `user` int(11) NOT NULL,
49//!         `event_key` text NOT NULL,
50//!         `time` datetime NOT NULL
51//!     );
52//!
53//!     -- Indexes for table `events`
54//!     ALTER TABLE `events`
55//!       ADD PRIMARY KEY (`id`),
56//!       ADD KEY `time` (`time`),
57//!       ADD KEY `event_key` (`event_key`);
58//!
59//!     -- Indexes for table `event_keys`
60//!     ALTER TABLE `event_keys`
61//!       ADD PRIMARY KEY (`id`);
62//!
63//!     -- Constraints for table `events`
64//!     ALTER TABLE `events`
65//!       ADD CONSTRAINT `event_key` FOREIGN KEY (`event_key`) REFERENCES `event_keys` (`id`);
66//!
67//!     -- Structure for view `events_view`
68//!     DROP TABLE IF EXISTS `events_view`;
69//!     DROP VIEW IF EXISTS `events_view`;
70//!     CREATE ALGORITHM=UNDEFINED DEFINER=`phpmyadmin`@`localhost`
71//!         SQL SECURITY DEFINER VIEW `events_view` AS
72//!         SELECT
73//!             `events`.`id` AS `id`,
74//!             `events`.`user` AS `user`,
75//!             `event_keys`.`name` AS `event_key`,
76//!             `events`.`time` AS `time`
77//!         FROM `events`, `event_keys`
78//!         WHERE `events`.`event_key` = `event_keys`.`id`;
79//!     ";
80//!
81//! let mut issues = Issues::new(schemas);
82//! let schemas = parse_schemas(schemas,
83//!     &mut issues,
84//!     &TypeOptions::new().dialect(SQLDialect::MariaDB));
85//!
86//! assert!(issues.is_ok());
87//!
88//! for (key, schema) in schemas.schemas {
89//!     println!("{}: {schema:?}", key.table_name())
90//! }
91//! ```
92
93use crate::{
94    Type, TypeOptions,
95    type_::{BaseType, FullType},
96    type_statement,
97    typer::resolve_table_name,
98};
99use alloc::{
100    borrow::Cow,
101    boxed::Box,
102    collections::{BTreeMap, BTreeSet},
103    rc::Rc,
104    sync::Arc,
105    vec::Vec,
106};
107use qusql_parse::{
108    AddColumn, AddIndex, AlterColumn, DataType, DataTypeProperty, DropColumn, Expression,
109    FunctionParam, Identifier, IdentifierPart, Issues, ModifyColumn, OptSpanned, QualifiedName,
110    Span, Spanned, Statement, parse_statements,
111};
112
113/// A column in a schema
114#[derive(Debug, Clone)]
115pub struct Column<'a> {
116    pub identifier: Identifier<'a>,
117    /// Type of the column
118    pub type_: FullType<'a>,
119    /// True if the column is auto_increment
120    pub auto_increment: bool,
121    pub default: bool,
122    pub as_: Option<Expression<'a>>,
123    pub generated: bool,
124}
125
126/// Schema representing a table or view
127#[derive(Debug)]
128pub struct Schema<'a> {
129    /// Span of identifier
130    pub identifier_span: Span,
131    /// List of columns
132    pub columns: Vec<Column<'a>>,
133    /// True if this is a view instead of a table
134    pub view: bool,
135}
136
137impl<'a> Schema<'a> {
138    pub fn get_column(&self, identifier: &str) -> Option<&Column<'a>> {
139        self.columns
140            .iter()
141            .find(|&column| column.identifier.value == identifier)
142    }
143    pub fn get_column_mut(&mut self, identifier: &str) -> Option<&mut Column<'a>> {
144        self.columns
145            .iter_mut()
146            .find(|column| column.identifier.value == identifier)
147    }
148}
149
150/// A stored procedure definition
151#[derive(Debug, Clone)]
152pub struct ProcedureDef<'a> {
153    pub name: Identifier<'a>,
154    pub params: Vec<FunctionParam<'a>>,
155    pub span: Span,
156    /// Statements extracted from the procedure body (if the body was a BEGIN...END block)
157    pub body: Option<Vec<Statement<'a>>>,
158}
159
160/// Parsed body of a stored function, with an offset for mapping spans
161/// back to the outer source file.
162#[derive(Debug)]
163pub struct FunctionDefBody<'a> {
164    /// Parsed statements from the function body
165    pub statements: Vec<Statement<'a>>,
166    /// The body source string (borrowed from the outer source)
167    pub src: &'a str,
168}
169
170/// A stored function definition
171#[derive(Debug)]
172pub struct FunctionDef<'a> {
173    pub name: Identifier<'a>,
174    pub params: Vec<FunctionParam<'a>>,
175    pub return_type: DataType<'a>,
176    pub span: Span,
177    /// Parsed body, present when the function was defined with a
178    /// dollar-quoted (non-escaped) AS body string.
179    pub body: Option<FunctionDefBody<'a>>,
180}
181
182#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
183pub struct IndexKey<'a> {
184    /// Schema name (PostgreSQL only); `None` for MySQL/MariaDB/SQLite.
185    pub schema: Option<Identifier<'a>>,
186    pub table: Option<Identifier<'a>>,
187    pub index: Identifier<'a>,
188}
189
190/// A user-defined type registered via `CREATE TYPE`
191#[derive(Debug)]
192pub enum TypeDef<'a> {
193    /// A PostgreSQL enum type
194    Enum {
195        values: Arc<Vec<Cow<'a, str>>>,
196        span: Span,
197    },
198}
199
200/// A schema-qualified or unqualified table/view identifier used as a `Schemas.schemas` map key.
201///
202/// In PostgreSQL mode every name is stored as `Qualified`; unqualified names use the
203/// implicit `"public"` schema.  In MySQL / MariaDB / SQLite mode every name is `Unqualified`.
204#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
205pub enum QualifiedIdentifier<'a> {
206    /// Unqualified table name (MySQL / MariaDB / SQLite)
207    Unqualified(Identifier<'a>),
208    /// Schema-qualified table name, e.g. `"public"."tablename"` (PostgreSQL)
209    Qualified(Identifier<'a>, Identifier<'a>),
210}
211
212impl<'a> core::fmt::Display for QualifiedIdentifier<'a> {
213    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
214        match self {
215            QualifiedIdentifier::Unqualified(id) => write!(f, "{}", id.value),
216            QualifiedIdentifier::Qualified(schema, table) => {
217                write!(f, "{}.{}", schema.value, table.value)
218            }
219        }
220    }
221}
222
223impl<'a> QualifiedIdentifier<'a> {
224    /// The table (rightmost) identifier.
225    pub fn table_name(&self) -> &Identifier<'a> {
226        match self {
227            QualifiedIdentifier::Unqualified(id) | QualifiedIdentifier::Qualified(_, id) => id,
228        }
229    }
230    /// The schema (leftmost) identifier, if any.
231    pub fn schema_name(&self) -> Option<&Identifier<'a>> {
232        match self {
233            QualifiedIdentifier::Unqualified(_) => None,
234            QualifiedIdentifier::Qualified(s, _) => Some(s),
235        }
236    }
237}
238
239/// Abstracts over search-path representations so callers don't need to allocate a
240/// `Vec<&str>` just to invoke the lookup helpers.
241///
242/// Implemented for `[&'s str]` (covering `&'static [&'static str]` from the typer) and
243/// `[Identifier<'a>]` (so `SchemaCtx` can pass `self.search_path` directly).
244pub trait SearchPath<'a>: Copy {
245    fn iter_schemas(self) -> impl Iterator<Item = &'a str>;
246}
247
248impl<'b> SearchPath<'b> for &[&'b str] {
249    fn iter_schemas(self) -> impl Iterator<Item = &'b str> {
250        self.iter().copied()
251    }
252}
253
254impl<'b> SearchPath<'b> for &[Identifier<'b>] {
255    fn iter_schemas(self) -> impl Iterator<Item = &'b str> {
256        self.iter().map(|id| id.value)
257    }
258}
259
260impl<'a> SearchPath<'a> for () {
261    fn iter_schemas(self) -> impl Iterator<Item = &'a str> {
262        core::iter::empty()
263    }
264}
265
266/// Like [`lookup_name`] but returns both the canonical map key and the value.
267/// Useful when the resolved schema is needed (e.g. to build an `IndexKey`).
268pub fn lookup_name_key<'a, 'b, T, S: SearchPath<'a>>(
269    map: &'b BTreeMap<QualifiedIdentifier<'a>, T>,
270    name: &QualifiedIdentifier<'a>,
271    search_path: S,
272) -> Option<(&'b QualifiedIdentifier<'a>, &'b T)> {
273    if let Some(kv) = map.get_key_value(name) {
274        return Some(kv);
275    }
276    if let QualifiedIdentifier::Unqualified(table) = name {
277        for schema in search_path.iter_schemas() {
278            let qualified =
279                QualifiedIdentifier::Qualified(Identifier::new(schema, 0..0), table.clone());
280            if let Some(kv) = map.get_key_value(&qualified) {
281                return Some(kv);
282            }
283        }
284    }
285    None
286}
287
288/// Look up a name in a schema-qualified map, with optional search-path fallback.
289///
290/// For an exact (qualified) hit the answer is immediate.  When `name` is
291/// `Unqualified` and the key in `map` is `Qualified`, each entry in
292/// `search_path` is tried in order so that e.g. a bare table reference is
293/// resolved against the PostgreSQL `search_path`.  Pass an empty slice for
294/// MySQL / MariaDB / SQLite.
295pub fn lookup_name<'a, 'b, T, S: SearchPath<'a>>(
296    map: &'b BTreeMap<QualifiedIdentifier<'a>, T>,
297    name: &QualifiedIdentifier<'a>,
298    search_path: S,
299) -> Option<&'b T> {
300    lookup_name_key(map, name, search_path).map(|(_, v)| v)
301}
302
303/// Mutable variant of [`lookup_name`]: returns a mutable reference to the map value.
304pub fn lookup_name_mut<'a, 'b, T, S: SearchPath<'a>>(
305    map: &'b mut BTreeMap<QualifiedIdentifier<'a>, T>,
306    name: &QualifiedIdentifier<'a>,
307    search_path: S,
308) -> Option<&'b mut T> {
309    if map.contains_key(name) {
310        return map.get_mut(name);
311    }
312    if let QualifiedIdentifier::Unqualified(table) = name {
313        for schema in search_path.iter_schemas() {
314            let qualified =
315                QualifiedIdentifier::Qualified(Identifier::new(schema, 0..0), table.clone());
316            if map.contains_key(&qualified) {
317                return map.get_mut(&qualified);
318            }
319        }
320    }
321    None
322}
323
324/// A description of tables, views, procedures, and functions in a schema definition file.
325#[derive(Debug, Default)]
326pub struct Schemas<'a> {
327    /// Map from qualified table/view name to its schema definition.
328    ///
329    /// PostgreSQL: all keys are `QualifiedIdentifier::Qualified`; unqualified names are
330    /// stored under `"public"`.  MySQL/MariaDB/SQLite: all keys are `QualifiedIdentifier::Unqualified`.
331    pub schemas: BTreeMap<QualifiedIdentifier<'a>, Schema<'a>>,
332    /// Set of schema names registered via `CREATE SCHEMA` (PostgreSQL only).
333    pub schema_names: BTreeSet<Identifier<'a>>,
334    /// Map from qualified name to stored procedure.
335    pub procedures: BTreeMap<QualifiedIdentifier<'a>, ProcedureDef<'a>>,
336    /// Map from qualified name to stored function.
337    pub functions: BTreeMap<QualifiedIdentifier<'a>, FunctionDef<'a>>,
338    /// Map from (table, index) to location.
339    pub indices: BTreeMap<IndexKey<'a>, Span>,
340    /// Map from qualified type name to type definition (e.g. enums created with `CREATE TYPE … AS ENUM`).
341    pub types: BTreeMap<QualifiedIdentifier<'a>, TypeDef<'a>>,
342    /// Map of sequence names registered via `CREATE SEQUENCE` (PostgreSQL only).
343    /// The value is `()` — only existence is tracked.
344    pub sequences: BTreeMap<QualifiedIdentifier<'a>, ()>,
345}
346
347/// Try to parse a borrowed string as SQL statements.
348/// Returns the parsed body if the string is a non-escaped borrow from `src`,
349/// or None if the string is escaped (Cow::Owned).
350fn try_parse_body<'a>(
351    src: &'a str,
352    body_str: &qusql_parse::SString<'a>,
353    issues: &mut Issues<'a>,
354    options: &qusql_parse::ParseOptions,
355) -> Option<FunctionDefBody<'a>> {
356    let Cow::Borrowed(borrowed) = &body_str.value else {
357        return None;
358    };
359    let span_offset = borrowed.as_ptr() as usize - src.as_ptr() as usize;
360    let body_options = options.clone().function_body(true).span_offset(span_offset);
361    let statements = parse_statements(borrowed, issues, &body_options);
362    Some(FunctionDefBody {
363        statements,
364        src: borrowed,
365    })
366}
367
368/// Map a PostgreSQL range/multirange subtype to its canonical element `Type`, preserving
369/// the concrete integer width (`I32` for `int4range`, `I64` for `int8range`, ...) so that
370/// e.g. `int4range` and `int8range` are not conflated.
371pub(crate) fn range_subtype_to_type<'a>(sub: &qusql_parse::RangeSubtype) -> Type<'a> {
372    use qusql_parse::RangeSubtype;
373    match sub {
374        RangeSubtype::Int4 => Type::I32,
375        RangeSubtype::Int8 => Type::I64,
376        RangeSubtype::Num => BaseType::Float.into(),
377        RangeSubtype::Ts => BaseType::DateTime.into(),
378        RangeSubtype::Tstz => BaseType::TimeStamp.into(),
379        RangeSubtype::Date => BaseType::Date.into(),
380    }
381}
382
383fn type_kind_from_parse<'a, S: SearchPath<'a>>(
384    type_: qusql_parse::Type<'a>,
385    unsigned: bool,
386    is_sqlite: bool,
387    is_postgresql: bool,
388    types: Option<&BTreeMap<QualifiedIdentifier<'a>, TypeDef<'a>>>,
389    issues: &mut Issues<'a>,
390    search_path: S,
391) -> Type<'a> {
392    match type_ {
393        qusql_parse::Type::TinyInt(v) => {
394            if !unsigned && matches!(v, Some((1, _))) {
395                BaseType::Bool.into()
396            } else if unsigned {
397                Type::U8
398            } else {
399                Type::I8
400            }
401        }
402        qusql_parse::Type::SmallInt(_) => {
403            if unsigned {
404                Type::U16
405            } else {
406                Type::I16
407            }
408        }
409        qusql_parse::Type::MediumInt(_) => {
410            if unsigned {
411                Type::U24
412            } else {
413                Type::I24
414            }
415        }
416        qusql_parse::Type::Int(_) => {
417            if unsigned {
418                Type::U32
419            } else {
420                Type::I32
421            }
422        }
423        qusql_parse::Type::BigInt(_) => {
424            if unsigned {
425                Type::U64
426            } else {
427                Type::I64
428            }
429        }
430        qusql_parse::Type::Char(_) => BaseType::String.into(),
431        qusql_parse::Type::VarChar(_) => BaseType::String.into(),
432        qusql_parse::Type::TinyText(_) => BaseType::String.into(),
433        qusql_parse::Type::MediumText(_) => BaseType::String.into(),
434        qusql_parse::Type::Text(_) => BaseType::String.into(),
435        qusql_parse::Type::LongText(_) => BaseType::String.into(),
436        qusql_parse::Type::Enum(e) => {
437            Type::Enum(Arc::new(e.into_iter().map(|s| s.value).collect()))
438        }
439        qusql_parse::Type::Set(s) => Type::Set(Arc::new(s.into_iter().map(|s| s.value).collect())),
440        qusql_parse::Type::Float(_) => Type::F32,
441        qusql_parse::Type::Double(_) => Type::F64,
442        qusql_parse::Type::DateTime(_) => BaseType::DateTime.into(),
443        qusql_parse::Type::Timestamp(_) => BaseType::TimeStamp.into(),
444        qusql_parse::Type::Time(_) => BaseType::Time.into(),
445        qusql_parse::Type::TinyBlob(_) => BaseType::Bytes.into(),
446        qusql_parse::Type::MediumBlob(_) => BaseType::Bytes.into(),
447        qusql_parse::Type::Date => BaseType::Date.into(),
448        qusql_parse::Type::Blob(_) => BaseType::Bytes.into(),
449        qusql_parse::Type::LongBlob(_) => BaseType::Bytes.into(),
450        qusql_parse::Type::VarBinary(_) => BaseType::Bytes.into(),
451        qusql_parse::Type::Binary(_) => BaseType::Bytes.into(),
452        qusql_parse::Type::Boolean => BaseType::Bool.into(),
453        qusql_parse::Type::Integer(_) => {
454            if is_sqlite {
455                BaseType::Integer.into()
456            } else {
457                Type::I32
458            }
459        }
460        qusql_parse::Type::Float8 => BaseType::Float.into(),
461        qusql_parse::Type::Numeric(ref v) => {
462            let span = v.as_ref().map(|(_, _, s)| s.clone()).unwrap_or(0..0);
463            issues.err("NUMERIC type is not yet supported", &span);
464            BaseType::Float.into()
465        }
466        qusql_parse::Type::Decimal(ref v) => {
467            let span = v.as_ref().map(|(_, _, s)| s.clone()).unwrap_or(0..0);
468            issues.err("DECIMAL type is not yet supported", &span);
469            BaseType::Float.into()
470        }
471        qusql_parse::Type::Timestamptz => BaseType::TimeStamp.into(),
472        qusql_parse::Type::Json | qusql_parse::Type::Jsonb => {
473            if is_postgresql {
474                // In PostgreSQL `json`/`jsonb` are distinct wire types; decoding
475                // them as text fails, so surface them as JSON.
476                Type::JSON
477            } else {
478                BaseType::String.into()
479            }
480        }
481        qusql_parse::Type::Bit(_, _) => BaseType::Bytes.into(),
482        qusql_parse::Type::VarBit(_) => BaseType::Bytes.into(),
483        qusql_parse::Type::Bytea => BaseType::Bytes.into(),
484        qusql_parse::Type::Named(qname) => {
485            // Look up user-defined types (e.g. enums created with CREATE TYPE ... AS ENUM).
486            if let Some(types) = types {
487                let key = match qname.prefix.as_slice() {
488                    [] => QualifiedIdentifier::Unqualified(qname.identifier.clone()),
489                    [(schema, _)] if is_postgresql => {
490                        QualifiedIdentifier::Qualified(schema.clone(), qname.identifier.clone())
491                    }
492                    _ => return BaseType::String.into(),
493                };
494                match lookup_name(types, &key, search_path) {
495                    Some(TypeDef::Enum { values, .. }) => Type::Enum(values.clone()),
496                    _ => BaseType::String.into(),
497                }
498            } else {
499                BaseType::String.into()
500            }
501        }
502        qusql_parse::Type::Inet4 => BaseType::String.into(),
503        qusql_parse::Type::Inet6 => BaseType::String.into(),
504        qusql_parse::Type::InetAddr => BaseType::String.into(),
505        qusql_parse::Type::Cidr => BaseType::String.into(),
506        qusql_parse::Type::Macaddr => BaseType::String.into(),
507        qusql_parse::Type::Macaddr8 => BaseType::String.into(),
508        qusql_parse::Type::Array(inner, _) => Type::Array(Box::new(type_kind_from_parse(
509            *inner,
510            unsigned,
511            is_sqlite,
512            is_postgresql,
513            types,
514            issues,
515            search_path,
516        ))),
517        qusql_parse::Type::Table(ref span, _) => {
518            issues.err("TABLE type is not yet supported", span);
519            BaseType::String.into()
520        }
521        qusql_parse::Type::Serial => Type::I32,
522        qusql_parse::Type::SmallSerial => Type::I16,
523        qusql_parse::Type::BigSerial => Type::I64,
524        qusql_parse::Type::Money => BaseType::Float.into(),
525        qusql_parse::Type::Timetz(_) => BaseType::Time.into(),
526        qusql_parse::Type::Interval(_) => BaseType::TimeInterval.into(),
527        qusql_parse::Type::TsQuery => BaseType::String.into(),
528        qusql_parse::Type::TsVector => BaseType::String.into(),
529        qusql_parse::Type::Uuid => BaseType::Uuid.into(),
530        qusql_parse::Type::Xml => BaseType::String.into(),
531        qusql_parse::Type::Range(sub) => Type::Range(Box::new(range_subtype_to_type(&sub))),
532        qusql_parse::Type::MultiRange(sub) => {
533            Type::MultiRange(Box::new(range_subtype_to_type(&sub)))
534        }
535        qusql_parse::Type::Point
536        | qusql_parse::Type::Line
537        | qusql_parse::Type::Lseg
538        | qusql_parse::Type::Box
539        | qusql_parse::Type::Path
540        | qusql_parse::Type::Polygon
541        | qusql_parse::Type::Circle => Type::Geometry,
542    }
543}
544
545pub(crate) fn parse_column<'a, S: SearchPath<'a>>(
546    data_type: DataType<'a>,
547    identifier: Identifier<'a>,
548    _issues: &mut Issues<'a>,
549    options: Option<&TypeOptions>,
550    types: Option<&BTreeMap<QualifiedIdentifier<'a>, TypeDef<'a>>>,
551    search_path: S,
552) -> Column<'a> {
553    let mut not_null = false;
554    let mut unsigned = false;
555    let mut auto_increment = false;
556    let mut default = false;
557    let mut as_ = None;
558    let mut generated = false;
559    let mut primary_key = false;
560    let is_sqlite = options
561        .map(|v| v.parse_options.get_dialect().is_sqlite())
562        .unwrap_or_default();
563    let is_postgresql = options
564        .map(|v| v.parse_options.get_dialect().is_postgresql())
565        .unwrap_or_default();
566    for p in data_type.properties {
567        match p {
568            DataTypeProperty::Signed(_) => unsigned = false,
569            DataTypeProperty::Unsigned(_) => unsigned = true,
570            DataTypeProperty::Null(_) => not_null = false,
571            DataTypeProperty::NotNull(_) => not_null = true,
572            DataTypeProperty::AutoIncrement(_) => auto_increment = true,
573            DataTypeProperty::As((_, e)) => as_ = Some(e),
574            DataTypeProperty::Default(_) => default = true,
575            DataTypeProperty::GeneratedAlways(_) => generated = true,
576            DataTypeProperty::GeneratedAlwaysAsExpr { .. } => generated = true,
577            DataTypeProperty::PrimaryKey(_) => primary_key = true,
578            _ => {}
579        }
580    }
581    // SQLite INTEGER PRIMARY KEY is an alias for rowid (auto-increment)
582    if is_sqlite && primary_key && matches!(data_type.type_, qusql_parse::Type::Integer(_)) {
583        auto_increment = true;
584    }
585    // PRIMARY KEY implies NOT NULL
586    if primary_key {
587        not_null = true;
588    }
589    let type_ = type_kind_from_parse(
590        data_type.type_,
591        unsigned,
592        is_sqlite,
593        is_postgresql,
594        types,
595        _issues,
596        search_path,
597    );
598    Column {
599        identifier,
600        type_: FullType {
601            t: type_,
602            not_null,
603            list_hack: false,
604        },
605        auto_increment,
606        as_,
607        default,
608        generated,
609    }
610}
611
612/// A runtime SQL value produced during schema evaluation.
613#[derive(Clone, Debug, PartialEq)]
614enum SqlValue<'a> {
615    Null,
616    Bool(bool),
617    Integer(i64),
618    /// A text slice directly from the SQL source - span arithmetic still works.
619    SourceText(&'a str),
620    /// A computed / owned text value.
621    OwnedText(alloc::string::String),
622}
623
624impl<'a> SqlValue<'a> {
625    fn as_source_text(&self) -> Option<&'a str> {
626        if let SqlValue::SourceText(s) = self {
627            Some(s)
628        } else {
629            None
630        }
631    }
632
633    fn is_truthy(&self) -> bool {
634        match self {
635            SqlValue::Bool(b) => *b,
636            SqlValue::Integer(i) => *i != 0,
637            SqlValue::Null => false,
638            SqlValue::SourceText(_) | SqlValue::OwnedText(_) => true,
639        }
640    }
641
642    fn sql_eq(&self, other: &SqlValue<'a>) -> Option<bool> {
643        match (self, other) {
644            (SqlValue::Null, _) | (_, SqlValue::Null) => None,
645            (SqlValue::Bool(a), SqlValue::Bool(b)) => Some(a == b),
646            (SqlValue::Integer(a), SqlValue::Integer(b)) => Some(a == b),
647            (SqlValue::SourceText(a), SqlValue::SourceText(b)) => Some(a == b),
648            (SqlValue::SourceText(a), SqlValue::OwnedText(b)) => Some(*a == b.as_str()),
649            (SqlValue::OwnedText(a), SqlValue::SourceText(b)) => Some(a.as_str() == *b),
650            (SqlValue::OwnedText(a), SqlValue::OwnedText(b)) => Some(a == b),
651            _ => None,
652        }
653    }
654
655    fn sql_lte(&self, other: &SqlValue<'a>) -> Option<bool> {
656        match (self, other) {
657            (SqlValue::Null, _) | (_, SqlValue::Null) => None,
658            (SqlValue::Integer(a), SqlValue::Integer(b)) => Some(a <= b),
659            _ => None,
660        }
661    }
662}
663
664/// A single row of evaluated column values.
665type Row<'a> = Rc<Vec<(&'a str, SqlValue<'a>)>>;
666
667/// Processing context for schema evaluation: holds mutable schema state, issue
668/// sink, the source text for span-offset calculations, and parse/type options.
669struct SchemaCtx<'a, 'b> {
670    schemas: &'b mut Schemas<'a>,
671    issues: &'b mut Issues<'a>,
672    /// The source text slice that all spans inside `issues` refer to.
673    src: &'a str,
674    options: &'b TypeOptions,
675    /// Active function argument bindings: parameter name -> SQL value.
676    /// Set when evaluating a known function body.
677    bindings: BTreeMap<&'a str, SqlValue<'a>>,
678    /// In-memory row store for tables populated during schema evaluation.
679    /// Keyed by the canonical `QualifiedIdentifier` so that tables with the same
680    /// bare name in different schemas are stored and retrieved independently.
681    rows: BTreeMap<QualifiedIdentifier<'a>, Vec<Row<'a>>>,
682    /// Table rows made available to aggregate functions during eval_condition.
683    /// Temporarily swapped via core::mem::take so eval functions can take &mut self.
684    current_table_rows: Vec<Row<'a>>,
685    /// The row currently being evaluated (e.g. during WHERE clause filtering).
686    /// Set by eval_select_matching_rows around each row's eval call.
687    current_row: Option<Row<'a>>,
688    /// The return value of the most recently executed RETURN statement.
689    /// Set by the Return arm in process_statement; consumed by eval_function_expr.
690    return_value: Option<SqlValue<'a>>,
691    /// Current PostgreSQL search path for unqualified name resolution.
692    /// Updated by `SET [LOCAL] search_path TO ...`.
693    /// For MySQL/MariaDB/SQLite this is always empty.
694    search_path: Vec<Identifier<'a>>,
695}
696
697impl<'a, 'b> SchemaCtx<'a, 'b> {
698    fn new(
699        schemas: &'b mut Schemas<'a>,
700        issues: &'b mut Issues<'a>,
701        src: &'a str,
702        options: &'b TypeOptions,
703    ) -> Self {
704        Self {
705            schemas,
706            issues,
707            src,
708            options,
709            bindings: Default::default(),
710            rows: Default::default(),
711            current_table_rows: Default::default(),
712            current_row: Default::default(),
713            return_value: None,
714            search_path: if options.parse_options.get_dialect().is_postgresql() {
715                alloc::vec![Identifier::new("public", 0..0)]
716            } else {
717                Vec::new()
718            },
719        }
720    }
721
722    /// Build a `QualifiedIdentifier` key using the current dialect convention.
723    /// PostgreSQL: unqualified names get the `"public"` schema prefix.
724    /// MySQL/MariaDB/SQLite: all names are `Unqualified`.
725    fn make_table_key(
726        &self,
727        schema: Option<Identifier<'a>>,
728        table: Identifier<'a>,
729    ) -> QualifiedIdentifier<'a> {
730        if self.options.parse_options.get_dialect().is_postgresql() {
731            QualifiedIdentifier::Qualified(
732                schema.unwrap_or_else(|| Identifier::new("public", 0..0)),
733                table,
734            )
735        } else {
736            QualifiedIdentifier::Unqualified(table)
737        }
738    }
739
740    /// Parse a `QualifiedName` into a `QualifiedIdentifier` key, respecting dialect rules.
741    /// Returns `None` and emits an error if the qualification is invalid for the dialect.
742    fn parse_qname(&mut self, qname: &QualifiedName<'a>) -> Option<QualifiedIdentifier<'a>> {
743        let is_pg = self.options.parse_options.get_dialect().is_postgresql();
744        match qname.prefix.as_slice() {
745            [] => Some(self.make_table_key(None, qname.identifier.clone())),
746            [(schema, _)] if is_pg => {
747                Some(self.make_table_key(Some(schema.clone()), qname.identifier.clone()))
748            }
749            _ => {
750                let msg = if is_pg {
751                    "Expected at most schema.table qualified name"
752                } else {
753                    "Schema-qualified names are not supported in MySQL"
754                };
755                self.issues.err(msg, &qname.prefix.opt_span().unwrap());
756                None
757            }
758        }
759    }
760
761    /// Like [`parse_qname`] but keeps unqualified names as `Unqualified` rather than
762    /// pre-qualifying them with `"public"`.  Use this for lookups where `lookup_name`
763    /// + the search path (passed as `self.search_path.as_slice()`) should resolve the schema.
764    fn parse_qname_for_lookup(
765        &mut self,
766        qname: &QualifiedName<'a>,
767    ) -> Option<QualifiedIdentifier<'a>> {
768        let is_pg = self.options.parse_options.get_dialect().is_postgresql();
769        match qname.prefix.as_slice() {
770            [] => Some(QualifiedIdentifier::Unqualified(qname.identifier.clone())),
771            [(schema, _)] if is_pg => Some(QualifiedIdentifier::Qualified(
772                schema.clone(),
773                qname.identifier.clone(),
774            )),
775            _ => {
776                let msg = if is_pg {
777                    "Expected at most schema.table qualified name"
778                } else {
779                    "Schema-qualified names are not supported in MySQL"
780                };
781                self.issues.err(msg, &qname.prefix.opt_span().unwrap());
782                None
783            }
784        }
785    }
786
787    /// Process a list of top-level schema statements.  Each statement is
788    /// independent: errors in one do not stop processing of the next.
789    fn process_top_level_statements(&mut self, statements: Vec<qusql_parse::Statement<'a>>) {
790        for statement in statements {
791            let _ = self.process_statement(statement);
792        }
793    }
794
795    /// Process a list of statements in a block or function body, stopping at
796    /// the first `Err` (error or `RETURN`).
797    fn process_statements(
798        &mut self,
799        statements: Vec<qusql_parse::Statement<'a>>,
800    ) -> Result<(), ()> {
801        for statement in statements {
802            self.process_statement(statement)?;
803        }
804        Ok(())
805    }
806
807    fn process_statement(&mut self, statement: qusql_parse::Statement<'a>) -> Result<(), ()> {
808        match statement {
809            qusql_parse::Statement::CreateTable(t) => {
810                self.process_create_table(*t);
811                Ok(())
812            }
813            qusql_parse::Statement::CreateView(v) => {
814                self.process_create_view(*v);
815                Ok(())
816            }
817            qusql_parse::Statement::CreateFunction(f) => {
818                self.process_create_function(*f);
819                Ok(())
820            }
821            qusql_parse::Statement::CreateProcedure(p) => {
822                self.process_create_procedure(*p);
823                Ok(())
824            }
825            qusql_parse::Statement::CreateIndex(ci) => {
826                self.process_create_index(*ci);
827                Ok(())
828            }
829            qusql_parse::Statement::CreateTrigger(_) => Ok(()),
830            qusql_parse::Statement::CreateTypeEnum(s) => {
831                self.process_create_type_enum(*s);
832                Ok(())
833            }
834            qusql_parse::Statement::AlterTable(a) => {
835                self.process_alter_table(*a);
836                Ok(())
837            }
838            qusql_parse::Statement::AlterSequence(a) => {
839                self.process_alter_sequence(*a);
840                Ok(())
841            }
842            qusql_parse::Statement::DropTable(t) => {
843                self.process_drop_table(*t);
844                Ok(())
845            }
846            qusql_parse::Statement::DropView(v) => {
847                self.process_drop_view(*v);
848                Ok(())
849            }
850            qusql_parse::Statement::DropFunction(f) => {
851                self.process_drop_function(*f);
852                Ok(())
853            }
854            qusql_parse::Statement::DropProcedure(p) => {
855                self.process_drop_procedure(*p);
856                Ok(())
857            }
858            qusql_parse::Statement::DropIndex(ci) => {
859                self.process_drop_index(*ci);
860                Ok(())
861            }
862            qusql_parse::Statement::CreateSchema(s) => {
863                self.process_create_schema(*s);
864                Ok(())
865            }
866            qusql_parse::Statement::DropDatabase(s) => {
867                self.issues.err("not implemented", &s);
868                Err(())
869            }
870            qusql_parse::Statement::DropSchema(s) => {
871                if self.options.parse_options.get_dialect().is_postgresql() {
872                    self.process_drop_schema(*s);
873                } else {
874                    self.issues.err("not implemented", &s);
875                }
876                Ok(())
877            }
878            qusql_parse::Statement::DropServer(s) => {
879                self.issues.err("not implemented", &s);
880                Err(())
881            }
882            qusql_parse::Statement::DropTrigger(_) => Ok(()),
883            qusql_parse::Statement::DropType(s) => {
884                self.process_drop_type(*s);
885                Ok(())
886            }
887            // Control-flow: recurse into all reachable branches.
888            qusql_parse::Statement::Do(d) => self.process_do(*d),
889            qusql_parse::Statement::Block(b) => self.process_statements(b.statements),
890            qusql_parse::Statement::If(i) => self.process_if(*i),
891            // SELECT: may call a known function whose body we can evaluate.
892            qusql_parse::Statement::Select(s) => self.process_select(*s),
893            // DML: track row insertions so conditions like EXISTS(...) can be evaluated.
894            qusql_parse::Statement::InsertReplace(i) => self.process_insert(*i),
895            // Transaction control: we assume all transactions commit.
896            qusql_parse::Statement::Commit(_) => Ok(()),
897            qusql_parse::Statement::Begin(_) => Ok(()),
898            // Statements with no schema effect.
899            qusql_parse::Statement::Grant(_) => Ok(()),
900            qusql_parse::Statement::CommentOn(_) => Ok(()),
901            qusql_parse::Statement::Analyze(_) => Ok(()),
902            qusql_parse::Statement::CreateSequence(s) => {
903                self.process_create_sequence(*s);
904                Ok(())
905            }
906            qusql_parse::Statement::DropSequence(s) => {
907                self.process_drop_sequence(*s);
908                Ok(())
909            }
910            // Variable / cursor plumbing — update search_path if relevant, otherwise ignore.
911            qusql_parse::Statement::Set(s) => {
912                self.process_set(*s);
913                Ok(())
914            }
915            // Assign and Perform may call known functions with schema effects.
916            qusql_parse::Statement::Assign(a) => {
917                for se in a.value.select_exprs {
918                    self.process_expression(se.expr)?;
919                }
920                Ok(())
921            }
922            qusql_parse::Statement::Perform(p) => self.process_expression(p.expr),
923            qusql_parse::Statement::DeclareVariable(d) => {
924                // Evaluate the DEFAULT expression for its side effects (may call user-defined functions).
925                if let Some((_, select)) = d.default {
926                    self.eval_condition(&select)?;
927                }
928                Ok(())
929            }
930            // DeclareHandler bodies only run on error; we model the happy path, so skip them.
931            qusql_parse::Statement::DeclareHandler(_) => Ok(()),
932            qusql_parse::Statement::ExecuteFunction(s) => {
933                self.issues.err("not implemented", &s);
934                Err(())
935            }
936            // RAISE EXCEPTION aborts execution; anything else is a log/notice with no schema effect.
937            qusql_parse::Statement::Raise(r) => {
938                if matches!(r.level, Some(qusql_parse::RaiseLevel::Exception(_))) {
939                    Err(())
940                } else {
941                    Ok(())
942                }
943            }
944            qusql_parse::Statement::Return(r) => {
945                self.return_value = self.eval_expr(&r.expr).ok();
946                Err(())
947            }
948            qusql_parse::Statement::PlpgsqlExecute(e) => self.process_plpgsql_execute(*e),
949            qusql_parse::Statement::Update(u) => self.process_update(*u),
950            qusql_parse::Statement::Delete(d) => self.process_delete(*d),
951            qusql_parse::Statement::AlterType(a) => {
952                self.process_alter_type(*a);
953                Ok(())
954            }
955            qusql_parse::Statement::TruncateTable(t) => {
956                self.process_truncate_table(*t);
957                Ok(())
958            }
959            qusql_parse::Statement::RenameTable(r) => {
960                self.process_rename_table(*r);
961                Ok(())
962            }
963            qusql_parse::Statement::Call(c) => self.process_call(*c),
964            s => {
965                self.issues.err(
966                    alloc::format!("Unsupported statement {s:?} in schema definition"),
967                    &s,
968                );
969                Err(())
970            }
971        }
972    }
973
974    fn process_create_sequence(&mut self, s: qusql_parse::CreateSequence<'a>) {
975        let Some(key) = self.parse_qname(&s.name) else {
976            return;
977        };
978        if s.if_not_exists.is_some()
979            && lookup_name(&self.schemas.sequences, &key, self.search_path.as_slice()).is_some()
980        {
981            return;
982        }
983        self.schemas.sequences.insert(key, ());
984    }
985
986    fn process_drop_sequence(&mut self, s: qusql_parse::DropSequence<'a>) {
987        for name in s.sequences {
988            let Some(key) = self.parse_qname(&name) else {
989                continue;
990            };
991            if s.if_exists.is_none()
992                && lookup_name(&self.schemas.sequences, &key, self.search_path.as_slice()).is_none()
993            {
994                self.issues.err(
995                    alloc::format!("Unknown sequence `{}`", name.identifier.value),
996                    &name.identifier,
997                );
998            }
999            self.schemas.sequences.remove(&key);
1000        }
1001    }
1002
1003    fn process_create_table(&mut self, t: qusql_parse::CreateTable<'a>) {
1004        let mut replace = false;
1005        let Some(key) = self.parse_qname(&t.identifier) else {
1006            return;
1007        };
1008        let id = key.table_name().clone();
1009        let identifier_span = id.span.clone();
1010        let mut schema = Schema {
1011            view: false,
1012            identifier_span,
1013            columns: Default::default(),
1014        };
1015        for o in t.create_options {
1016            match o {
1017                qusql_parse::CreateOption::OrReplace(_) => replace = true,
1018                qusql_parse::CreateOption::Temporary { temporary_span, .. } => {
1019                    self.issues.err("Not supported", &temporary_span);
1020                }
1021                qusql_parse::CreateOption::Materialized(s) => {
1022                    self.issues.err("Not supported", &s);
1023                }
1024                qusql_parse::CreateOption::Concurrently(s) => {
1025                    self.issues.err("Not supported", &s);
1026                }
1027                qusql_parse::CreateOption::Unique(s) => {
1028                    self.issues.err("Not supported", &s);
1029                }
1030                _ => {}
1031            }
1032        }
1033        for d in t.create_definitions {
1034            match d {
1035                qusql_parse::CreateDefinition::ColumnDefinition {
1036                    identifier,
1037                    data_type,
1038                } => {
1039                    let column = parse_column(
1040                        data_type,
1041                        identifier.clone(),
1042                        self.issues,
1043                        Some(self.options),
1044                        Some(&self.schemas.types),
1045                        self.search_path.as_slice(),
1046                    );
1047                    if let Some(oc) = schema.get_column(column.identifier.value) {
1048                        self.issues
1049                            .err("Column already defined", &identifier)
1050                            .frag("Defined here", &oc.identifier);
1051                    } else {
1052                        schema.columns.push(column);
1053                    }
1054                }
1055                qusql_parse::CreateDefinition::IndexDefinition {
1056                    index_type,
1057                    index_name,
1058                    cols,
1059                    ..
1060                } => {
1061                    // Validate that every column referenced by the index exists.
1062                    for col in &cols {
1063                        if let qusql_parse::IndexColExpr::Column(cname) = &col.expr
1064                            && schema.get_column(cname.value).is_none()
1065                        {
1066                            self.issues
1067                                .err("No such column in table", col)
1068                                .frag("Table defined here", &schema.identifier_span);
1069                        }
1070                    }
1071                    // PRIMARY KEY implies NOT NULL on each listed column.
1072                    if matches!(index_type, qusql_parse::IndexType::Primary(_)) {
1073                        for col in &cols {
1074                            if let qusql_parse::IndexColExpr::Column(cname) = &col.expr
1075                                && let Some(c) = schema.get_column_mut(cname.value)
1076                            {
1077                                c.type_.not_null = true;
1078                            }
1079                        }
1080                    }
1081                    // Register named indices.
1082                    if let Some(name) = index_name {
1083                        let ident = if self.options.parse_options.get_dialect().is_postgresql() {
1084                            IndexKey {
1085                                schema: key.schema_name().cloned(),
1086                                table: None,
1087                                index: name.clone(),
1088                            }
1089                        } else {
1090                            IndexKey {
1091                                schema: None,
1092                                table: Some(id.clone()),
1093                                index: name.clone(),
1094                            }
1095                        };
1096                        let span = name.span();
1097                        if let Some(old) = self.schemas.indices.insert(ident, span) {
1098                            self.issues
1099                                .err("Multiple indices with the same identifier", &name)
1100                                .frag("Already defined here", &old);
1101                        }
1102                    }
1103                }
1104                qusql_parse::CreateDefinition::ForeignKeyDefinition { .. } => {}
1105                qusql_parse::CreateDefinition::CheckConstraintDefinition { .. } => {}
1106                qusql_parse::CreateDefinition::LikeTable { source_table, .. } => {
1107                    let source_key = match self.parse_qname_for_lookup(&source_table) {
1108                        Some(k) => k,
1109                        None => continue,
1110                    };
1111                    if let Some(src) = lookup_name(
1112                        &self.schemas.schemas,
1113                        &source_key,
1114                        self.search_path.as_slice(),
1115                    ) {
1116                        let cols: Vec<Column<'a>> = src.columns.to_vec();
1117                        for col in cols {
1118                            if schema.get_column(col.identifier.value).is_none() {
1119                                schema.columns.push(col);
1120                            }
1121                        }
1122                    } else {
1123                        self.issues.err("Table not found", &source_table);
1124                    }
1125                }
1126            }
1127        }
1128        match self.schemas.schemas.entry(key.clone()) {
1129            alloc::collections::btree_map::Entry::Occupied(mut e) => {
1130                if replace {
1131                    e.insert(schema);
1132                } else if t.if_not_exists.is_none() {
1133                    self.issues
1134                        .err("Table already defined", &t.identifier)
1135                        .frag("Defined here", &e.get().identifier_span);
1136                }
1137            }
1138            alloc::collections::btree_map::Entry::Vacant(e) => {
1139                e.insert(schema);
1140            }
1141        }
1142    }
1143
1144    fn process_create_view(&mut self, v: qusql_parse::CreateView<'a>) {
1145        let mut replace = false;
1146        let mut schema = Schema {
1147            view: true,
1148            identifier_span: v.name.span(),
1149            columns: Default::default(),
1150        };
1151        for o in v.create_options {
1152            match o {
1153                qusql_parse::CreateOption::OrReplace(_) => replace = true,
1154                qusql_parse::CreateOption::Temporary { temporary_span, .. } => {
1155                    self.issues.err("Not supported", &temporary_span);
1156                }
1157                qusql_parse::CreateOption::Materialized(s) => {
1158                    self.issues.err("Not supported", &s);
1159                }
1160                qusql_parse::CreateOption::Concurrently(s) => {
1161                    self.issues.err("Not supported", &s);
1162                }
1163                qusql_parse::CreateOption::Unique(s) => {
1164                    self.issues.err("Not supported", &s);
1165                }
1166                _ => {}
1167            }
1168        }
1169        {
1170            let mut typer: crate::typer::Typer<'a, '_> = crate::typer::Typer {
1171                schemas: self.schemas,
1172                issues: self.issues,
1173                reference_types: Vec::new(),
1174                outer_reference_types: Vec::new(),
1175                arg_types: Default::default(),
1176                options: self.options,
1177                with_schemas: Default::default(),
1178            };
1179            let t = type_statement::type_statement(&mut typer, &v.select);
1180            let s = if let type_statement::InnerStatementType::Select(s) = t {
1181                s
1182            } else {
1183                self.issues.err("Not supported", &v.select.span());
1184                return;
1185            };
1186            for column in s.columns {
1187                let Some(name) = column.name else {
1188                    self.issues.err(
1189                        "View column has no name; add an alias with AS",
1190                        &v.select.span(),
1191                    );
1192                    continue;
1193                };
1194                schema.columns.push(Column {
1195                    identifier: name,
1196                    type_: column.type_,
1197                    auto_increment: false,
1198                    default: false,
1199                    as_: None,
1200                    generated: false,
1201                });
1202            }
1203        }
1204        let view_key = match self.parse_qname(&v.name) {
1205            Some(k) => k,
1206            None => return,
1207        };
1208        match self.schemas.schemas.entry(view_key) {
1209            alloc::collections::btree_map::Entry::Occupied(mut e) => {
1210                if replace {
1211                    e.insert(schema);
1212                } else if v.if_not_exists.is_none() {
1213                    self.issues
1214                        .err("View already defined", &v.name)
1215                        .frag("Defined here", &e.get().identifier_span);
1216                }
1217            }
1218            alloc::collections::btree_map::Entry::Vacant(e) => {
1219                e.insert(schema);
1220            }
1221        }
1222    }
1223
1224    fn process_create_function(&mut self, f: qusql_parse::CreateFunction<'a>) {
1225        let mut replace = false;
1226        for o in &f.create_options {
1227            if matches!(o, qusql_parse::CreateOption::OrReplace(_)) {
1228                replace = true;
1229            }
1230        }
1231        let body = f
1232            .body
1233            .as_ref()
1234            .and_then(|b| b.strings.first())
1235            .and_then(|s| try_parse_body(self.src, s, self.issues, &self.options.parse_options));
1236        let Some(key) = self.parse_qname(&f.name) else {
1237            return;
1238        };
1239        let def = FunctionDef {
1240            name: f.name.identifier.clone(),
1241            params: f.params,
1242            return_type: f.return_type,
1243            span: f.create_span.join_span(&f.function_span),
1244            body,
1245        };
1246        match self.schemas.functions.entry(key) {
1247            alloc::collections::btree_map::Entry::Occupied(mut e) => {
1248                if replace {
1249                    e.insert(def);
1250                } else if f.if_not_exists.is_none() {
1251                    self.issues
1252                        .err("Function already defined", &f.name.identifier)
1253                        .frag("Defined here", &e.get().span);
1254                }
1255            }
1256            alloc::collections::btree_map::Entry::Vacant(e) => {
1257                e.insert(def);
1258            }
1259        }
1260    }
1261
1262    fn process_create_procedure(&mut self, p: qusql_parse::CreateProcedure<'a>) {
1263        let mut replace = false;
1264        for o in &p.create_options {
1265            if matches!(o, qusql_parse::CreateOption::OrReplace(_)) {
1266                replace = true;
1267            }
1268        }
1269        let key = self.make_table_key(None, p.name.clone());
1270        let body = p.body.map(|stmt| match stmt {
1271            qusql_parse::Statement::Block(b) => b.statements,
1272            other => alloc::vec![other],
1273        });
1274        let def = ProcedureDef {
1275            name: p.name.clone(),
1276            params: p.params,
1277            span: p.create_span.join_span(&p.procedure_span),
1278            body,
1279        };
1280        match self.schemas.procedures.entry(key) {
1281            alloc::collections::btree_map::Entry::Occupied(mut e) => {
1282                if replace {
1283                    e.insert(def);
1284                } else if p.if_not_exists.is_none() {
1285                    self.issues
1286                        .err("Procedure already defined", &p.name)
1287                        .frag("Defined here", &e.get().span);
1288                }
1289            }
1290            alloc::collections::btree_map::Entry::Vacant(e) => {
1291                e.insert(def);
1292            }
1293        }
1294    }
1295
1296    fn process_call(&mut self, c: qusql_parse::Call<'a>) -> Result<(), ()> {
1297        let Some(key) = self.parse_qname_for_lookup(&c.name) else {
1298            return Ok(());
1299        };
1300        // Look up the procedure and clone its body statements.
1301        let body = lookup_name(&self.schemas.procedures, &key, self.search_path.as_slice())
1302            .and_then(|p| p.body.clone());
1303        let Some(statements) = body else {
1304            // Unknown or body-less procedure — no schema effect.
1305            return Ok(());
1306        };
1307        // Ignore Ok/Err from the body: a Err() just means "stopped early" (RAISE EXCEPTION, RETURN, etc.)
1308        let _ = self.process_statements(statements);
1309        Ok(())
1310    }
1311
1312    fn process_set(&mut self, s: qusql_parse::Set<'a>) {
1313        if !self.options.parse_options.get_dialect().is_postgresql() {
1314            return;
1315        }
1316        for (var, exprs) in s.values {
1317            let qusql_parse::SetVariable::Named(qname) = var else {
1318                continue;
1319            };
1320            // Match `search_path` (possibly qualified as `pg_catalog.search_path` etc.)
1321            if !qname.identifier.value.eq_ignore_ascii_case("search_path") {
1322                continue;
1323            }
1324            // Each expression is either a bare identifier (schema name) or a string literal.
1325            let mut new_path: Vec<Identifier<'a>> = Vec::new();
1326            for expr in exprs {
1327                match expr {
1328                    Expression::Identifier(id) => {
1329                        if let Some(part) = id.parts.last()
1330                            && let qusql_parse::IdentifierPart::Name(name) = part
1331                        {
1332                            new_path.push(name.clone());
1333                        }
1334                    }
1335                    Expression::String(s) => {
1336                        // quoted schema name — wrap it as a synthetic Identifier
1337                        if let alloc::borrow::Cow::Borrowed(b) = &s.value {
1338                            new_path.push(Identifier::new(b, 0..0));
1339                        } else {
1340                            // escaped/owned string — skip (uncommon in search_path)
1341                        }
1342                    }
1343                    _ => {}
1344                }
1345            }
1346            self.search_path = new_path;
1347        }
1348    }
1349
1350    fn process_create_index(&mut self, ci: qusql_parse::CreateIndex<'a>) {
1351        let Some(lookup_key) = self.parse_qname_for_lookup(&ci.table_name) else {
1352            return;
1353        };
1354        let t = lookup_key.table_name().clone();
1355        // Look up the table to validate columns and get the resolved schema for the IndexKey.
1356        let resolved_schema = match lookup_name_key(
1357            &self.schemas.schemas,
1358            &lookup_key,
1359            self.search_path.as_slice(),
1360        ) {
1361            Some((resolved_key, table)) => {
1362                let schema = resolved_key.schema_name().cloned();
1363                for col in &ci.column_names {
1364                    if let qusql_parse::IndexColExpr::Column(name) = &col.expr
1365                        && table.get_column(name.value).is_none()
1366                    {
1367                        self.issues
1368                            .err("No such column in table", col)
1369                            .frag("Table defined here", &table.identifier_span);
1370                    }
1371                }
1372                schema
1373            }
1374            None => {
1375                self.issues.err("No such table", &ci.table_name);
1376                None
1377            }
1378        };
1379        let index_name = match &ci.index_name {
1380            Some(name) => name.clone(),
1381            None => return,
1382        };
1383        let ident = if self.options.parse_options.get_dialect().is_postgresql() {
1384            IndexKey {
1385                schema: resolved_schema,
1386                table: None,
1387                index: index_name.clone(),
1388            }
1389        } else {
1390            IndexKey {
1391                schema: None,
1392                table: Some(t.clone()),
1393                index: index_name.clone(),
1394            }
1395        };
1396        if let Some(old) = self.schemas.indices.insert(ident, ci.span())
1397            && ci.if_not_exists.is_none()
1398        {
1399            self.issues
1400                .err("Multiple indices with the same identifier", &ci)
1401                .frag("Already defined here", &old);
1402        }
1403    }
1404
1405    fn process_create_type_enum(&mut self, s: qusql_parse::CreateTypeEnum<'a>) {
1406        let Some(key) = self.parse_qname(&s.name) else {
1407            return;
1408        };
1409        let mut replace = false;
1410        for o in &s.create_options {
1411            if matches!(o, qusql_parse::CreateOption::OrReplace(_)) {
1412                replace = true;
1413            }
1414        }
1415        let values = Arc::new(s.values.into_iter().map(|v| v.value).collect());
1416        let typedef = TypeDef::Enum {
1417            values,
1418            span: s.as_enum_span,
1419        };
1420        match self.schemas.types.entry(key) {
1421            alloc::collections::btree_map::Entry::Occupied(mut e) => {
1422                if replace {
1423                    e.insert(typedef);
1424                }
1425                // Otherwise silently skip - SQL uses EXCEPTION WHEN duplicate_object to handle re-runs
1426            }
1427            alloc::collections::btree_map::Entry::Vacant(e) => {
1428                e.insert(typedef);
1429            }
1430        }
1431    }
1432
1433    fn process_drop_type(&mut self, s: qusql_parse::DropType<'a>) {
1434        let if_exists = s.if_exists;
1435        for name in s.names {
1436            let Some(key) = self.parse_qname(&name) else {
1437                continue;
1438            };
1439            if self.schemas.types.remove(&key).is_none() && if_exists.is_none() {
1440                self.issues.err("Type not found", &name);
1441            }
1442        }
1443    }
1444
1445    fn process_alter_table(&mut self, a: qusql_parse::AlterTable<'a>) {
1446        let key = match self.parse_qname(&a.table) {
1447            Some(k) => k,
1448            None => return,
1449        };
1450        let e = match self.schemas.schemas.entry(key) {
1451            alloc::collections::btree_map::Entry::Occupied(e) => {
1452                let e = e.into_mut();
1453                if e.view {
1454                    self.issues.err("Cannot alter view", &a.table);
1455                    return;
1456                }
1457                e
1458            }
1459            alloc::collections::btree_map::Entry::Vacant(_) => {
1460                if a.if_exists.is_none() {
1461                    self.issues.err("Table not found", &a.table);
1462                }
1463                return;
1464            }
1465        };
1466        for s in a.alter_specifications {
1467            process_alter_specification(
1468                s,
1469                e,
1470                &a.table,
1471                self.issues,
1472                &mut self.schemas.indices,
1473                self.options,
1474                &self.schemas.types,
1475                self.search_path.as_slice(),
1476            );
1477        }
1478    }
1479
1480    fn process_alter_sequence(&mut self, a: qusql_parse::AlterSequence<'a>) {
1481        // Sequence option changes (increment, restart, cache, ...) have no effect on
1482        // the parts of the schema we track; only existence and the qualified name matter.
1483        // Keep the name unqualified so the search path can resolve it, and use the
1484        // canonical key from the map for the removal/renaming below.
1485        let lookup_key = match self.parse_qname_for_lookup(&a.name) {
1486            Some(k) => k,
1487            None => return,
1488        };
1489        let key = match lookup_name_key(
1490            &self.schemas.sequences,
1491            &lookup_key,
1492            self.search_path.as_slice(),
1493        ) {
1494            Some((resolved_key, _)) => resolved_key.clone(),
1495            None => {
1496                if a.if_exists.is_none() {
1497                    self.issues.err(
1498                        alloc::format!("Unknown sequence `{}`", a.name.identifier.value),
1499                        &a.name,
1500                    );
1501                }
1502                return;
1503            }
1504        };
1505        match a.action {
1506            qusql_parse::AlterSequenceAction::RenameTo { new_name, .. } => {
1507                let new_key = self.make_table_key(key.schema_name().cloned(), new_name);
1508                if self.schemas.sequences.contains_key(&new_key) {
1509                    self.issues
1510                        .err("Sequence already exists", new_key.table_name());
1511                    return;
1512                }
1513                if let Some(entry) = self.schemas.sequences.remove(&key) {
1514                    self.schemas.sequences.insert(new_key, entry);
1515                }
1516            }
1517            qusql_parse::AlterSequenceAction::SetSchema { new_schema, .. } => {
1518                let new_key = self.make_table_key(Some(new_schema), key.table_name().clone());
1519                if new_key != key && self.schemas.sequences.contains_key(&new_key) {
1520                    self.issues
1521                        .err("Sequence already exists in target schema", &a.name);
1522                    return;
1523                }
1524                if let Some(entry) = self.schemas.sequences.remove(&key) {
1525                    self.schemas.sequences.insert(new_key, entry);
1526                }
1527            }
1528            // OWNER TO and option changes have no tracked schema effect.
1529            qusql_parse::AlterSequenceAction::OwnerTo { .. }
1530            | qusql_parse::AlterSequenceAction::Options(_) => {}
1531        }
1532    }
1533}
1534
1535#[allow(clippy::too_many_lines)]
1536#[allow(clippy::too_many_arguments)]
1537fn process_alter_specification<'a, S: SearchPath<'a>>(
1538    s: qusql_parse::AlterSpecification<'a>,
1539    e: &mut Schema<'a>,
1540    table_ref: &qusql_parse::QualifiedName<'a>,
1541    issues: &mut Issues<'a>,
1542    indices: &mut alloc::collections::BTreeMap<IndexKey<'a>, Span>,
1543    options: &TypeOptions,
1544    types: &BTreeMap<QualifiedIdentifier<'a>, TypeDef<'a>>,
1545    search_path: S,
1546) {
1547    match s {
1548        qusql_parse::AlterSpecification::AddIndex(AddIndex {
1549            if_not_exists,
1550            name,
1551            cols,
1552            index_type,
1553            ..
1554        }) => {
1555            for col in &cols {
1556                if let qusql_parse::IndexColExpr::Column(cname) = &col.expr
1557                    && e.get_column(cname.value).is_none()
1558                {
1559                    issues
1560                        .err("No such column in table", col)
1561                        .frag("Table defined here", table_ref);
1562                }
1563            }
1564            // PRIMARY KEY implies NOT NULL on each listed column.
1565            if matches!(index_type, qusql_parse::IndexType::Primary(_)) {
1566                for col in &cols {
1567                    if let qusql_parse::IndexColExpr::Column(cname) = &col.expr
1568                        && let Some(c) = e.get_column_mut(cname.value)
1569                    {
1570                        c.type_.not_null = true;
1571                    }
1572                }
1573            }
1574            if let Some(name) = &name {
1575                let ident = if options.parse_options.get_dialect().is_postgresql() {
1576                    let (schema, _) = resolve_table_name(issues, options, table_ref);
1577                    IndexKey {
1578                        schema: Some(
1579                            schema
1580                                .cloned()
1581                                .unwrap_or_else(|| Identifier::new("public", 0..0)),
1582                        ),
1583                        table: None,
1584                        index: name.clone(),
1585                    }
1586                } else {
1587                    IndexKey {
1588                        schema: None,
1589                        table: Some(resolve_table_name(issues, options, table_ref).1.clone()),
1590                        index: name.clone(),
1591                    }
1592                };
1593                if let Some(old) = indices.insert(ident, name.span())
1594                    && if_not_exists.is_none()
1595                {
1596                    issues
1597                        .err("Multiple indices with the same identifier", &name.span())
1598                        .frag("Already defined here", &old);
1599                }
1600            }
1601        }
1602        qusql_parse::AlterSpecification::AddForeignKey { .. } => {}
1603        qusql_parse::AlterSpecification::Modify(ModifyColumn {
1604            if_exists,
1605            col,
1606            definition,
1607            ..
1608        }) => match e.get_column_mut(col.value) {
1609            Some(c) => {
1610                let new_col = parse_column(
1611                    definition,
1612                    c.identifier.clone(),
1613                    issues,
1614                    Some(options),
1615                    Some(types),
1616                    search_path,
1617                );
1618                *c = new_col;
1619            }
1620            None if if_exists.is_none() => {
1621                issues
1622                    .err("No such column in table", &col)
1623                    .frag("Table defined here", &e.identifier_span);
1624            }
1625            None => {}
1626        },
1627        qusql_parse::AlterSpecification::AddColumn(AddColumn {
1628            identifier,
1629            data_type,
1630            if_not_exists_span,
1631            ..
1632        }) => {
1633            if e.get_column(identifier.value).is_some() {
1634                if if_not_exists_span.is_none() {
1635                    issues
1636                        .err("Column already exists in table", &identifier)
1637                        .frag("Table defined here", &e.identifier_span);
1638                }
1639            } else {
1640                e.columns.push(parse_column(
1641                    data_type,
1642                    identifier,
1643                    issues,
1644                    Some(options),
1645                    Some(types),
1646                    search_path,
1647                ));
1648            }
1649        }
1650        qusql_parse::AlterSpecification::OwnerTo { .. } => {}
1651        qusql_parse::AlterSpecification::DropColumn(DropColumn {
1652            column, if_exists, ..
1653        }) => {
1654            let cnt = e.columns.len();
1655            e.columns.retain(|c| c.identifier != column);
1656            if cnt == e.columns.len() && if_exists.is_none() {
1657                issues
1658                    .err("No such column in table", &column)
1659                    .frag("Table defined here", &e.identifier_span);
1660            }
1661        }
1662        qusql_parse::AlterSpecification::AlterColumn(AlterColumn {
1663            column,
1664            alter_column_action,
1665            ..
1666        }) => match e.get_column_mut(column.value) {
1667            Some(c) => match alter_column_action {
1668                qusql_parse::AlterColumnAction::SetDefault { .. } => c.default = true,
1669                qusql_parse::AlterColumnAction::DropDefault { .. } => c.default = false,
1670                qusql_parse::AlterColumnAction::Type { type_, .. } => {
1671                    *c = parse_column(
1672                        type_,
1673                        column,
1674                        issues,
1675                        Some(options),
1676                        Some(types),
1677                        search_path,
1678                    );
1679                }
1680                qusql_parse::AlterColumnAction::SetNotNull { .. } => c.type_.not_null = true,
1681                qusql_parse::AlterColumnAction::DropNotNull { .. } => c.type_.not_null = false,
1682                a @ qusql_parse::AlterColumnAction::AddGenerated { .. } => {
1683                    issues.err("not implemented", &a);
1684                }
1685            },
1686            None => {
1687                issues
1688                    .err("No such column in table", &column)
1689                    .frag("Table defined here", &e.identifier_span);
1690            }
1691        },
1692        qusql_parse::AlterSpecification::DropIndex(drop_idx) => {
1693            let is_postgresql = options.parse_options.get_dialect().is_postgresql();
1694            let key = if is_postgresql {
1695                let (schema, _) = resolve_table_name(issues, options, table_ref);
1696                IndexKey {
1697                    schema: Some(
1698                        schema
1699                            .cloned()
1700                            .unwrap_or_else(|| Identifier::new("public", 0..0)),
1701                    ),
1702                    table: None,
1703                    index: drop_idx.name.clone(),
1704                }
1705            } else {
1706                IndexKey {
1707                    schema: None,
1708                    table: Some(resolve_table_name(issues, options, table_ref).1.clone()),
1709                    index: drop_idx.name.clone(),
1710                }
1711            };
1712            if indices.remove(&key).is_none() {
1713                issues.err("No such index to drop", &drop_idx.name);
1714            }
1715        }
1716        qusql_parse::AlterSpecification::RenameColumn(qusql_parse::RenameColumn {
1717            old_col_name,
1718            new_col_name,
1719            ..
1720        }) => match e.get_column_mut(old_col_name.value) {
1721            Some(c) => c.identifier = new_col_name,
1722            None => {
1723                issues
1724                    .err("No such column in table", &old_col_name)
1725                    .frag("Table defined here", &e.identifier_span);
1726            }
1727        },
1728        qusql_parse::AlterSpecification::RenameIndex(qusql_parse::RenameIndex {
1729            old_index_name,
1730            new_index_name,
1731            ..
1732        }) => {
1733            let is_postgresql = options.parse_options.get_dialect().is_postgresql();
1734            let (schema_ref, table_id_ref) = resolve_table_name(issues, options, table_ref);
1735            let schema_id = schema_ref
1736                .cloned()
1737                .unwrap_or_else(|| Identifier::new("public", 0..0));
1738            let table_id = table_id_ref.clone();
1739            let old_key = if is_postgresql {
1740                IndexKey {
1741                    schema: Some(schema_id.clone()),
1742                    table: None,
1743                    index: old_index_name.clone(),
1744                }
1745            } else {
1746                IndexKey {
1747                    schema: None,
1748                    table: Some(table_id.clone()),
1749                    index: old_index_name.clone(),
1750                }
1751            };
1752            match indices.remove(&old_key) {
1753                Some(span) => {
1754                    let new_key = if is_postgresql {
1755                        IndexKey {
1756                            schema: Some(schema_id),
1757                            table: None,
1758                            index: new_index_name,
1759                        }
1760                    } else {
1761                        IndexKey {
1762                            schema: None,
1763                            table: Some(table_id),
1764                            index: new_index_name,
1765                        }
1766                    };
1767                    indices.insert(new_key, span);
1768                }
1769                None => {
1770                    issues.err("No such index to rename", &old_index_name);
1771                }
1772            }
1773        }
1774        // We do not track constraints, so RENAME CONSTRAINT is a no-op.
1775        qusql_parse::AlterSpecification::RenameConstraint(_) => {}
1776        // RENAME TO inside ALTER TABLE requires renaming the table's map key, which is
1777        // not accessible here.  The standalone RENAME TABLE statement (handled by
1778        // process_rename_table) should be preferred; this variant is left as a no-op.
1779        qusql_parse::AlterSpecification::RenameTo(_) => {}
1780        qusql_parse::AlterSpecification::Change(qusql_parse::Change {
1781            column,
1782            new_column,
1783            definition,
1784            ..
1785        }) => match e.get_column_mut(column.value) {
1786            Some(c) => {
1787                *c = parse_column(
1788                    definition,
1789                    new_column,
1790                    issues,
1791                    Some(options),
1792                    Some(types),
1793                    search_path,
1794                );
1795            }
1796            None => {
1797                issues
1798                    .err("No such column in table", &column)
1799                    .frag("Table defined here", &e.identifier_span);
1800            }
1801        },
1802        qusql_parse::AlterSpecification::Lock { .. }
1803        | qusql_parse::AlterSpecification::DropForeignKey { .. }
1804        | qusql_parse::AlterSpecification::DropPrimaryKey { .. }
1805        | qusql_parse::AlterSpecification::Algorithm { .. }
1806        | qusql_parse::AlterSpecification::AutoIncrement { .. }
1807        | qusql_parse::AlterSpecification::ReplicaIdentity(_)
1808        | qusql_parse::AlterSpecification::ValidateConstraint(_)
1809        | qusql_parse::AlterSpecification::AddTableConstraint(_)
1810        | qusql_parse::AlterSpecification::DisableTrigger(_)
1811        | qusql_parse::AlterSpecification::EnableTrigger(_)
1812        | qusql_parse::AlterSpecification::DisableRule(_)
1813        | qusql_parse::AlterSpecification::EnableRule(_)
1814        | qusql_parse::AlterSpecification::DisableRowLevelSecurity(_)
1815        | qusql_parse::AlterSpecification::EnableRowLevelSecurity(_)
1816        | qusql_parse::AlterSpecification::ForceRowLevelSecurity(_)
1817        | qusql_parse::AlterSpecification::NoForceRowLevelSecurity(_) => {}
1818    }
1819}
1820
1821impl<'a, 'b> SchemaCtx<'a, 'b> {
1822    fn process_drop_table(&mut self, t: qusql_parse::DropTable<'a>) {
1823        for i in t.tables {
1824            let key = match self.parse_qname(&i) {
1825                Some(k) => k,
1826                None => continue,
1827            };
1828            match self.schemas.schemas.entry(key) {
1829                alloc::collections::btree_map::Entry::Occupied(e) => {
1830                    if e.get().view {
1831                        self.issues
1832                            .err("Name defines a view not a table", &i)
1833                            .frag("View defined here", &e.get().identifier_span);
1834                    } else {
1835                        e.remove();
1836                    }
1837                }
1838                alloc::collections::btree_map::Entry::Vacant(_) => {
1839                    if t.if_exists.is_none() {
1840                        self.issues
1841                            .err("A table with this name does not exist to drop", &i);
1842                    }
1843                }
1844            }
1845        }
1846    }
1847
1848    fn process_drop_view(&mut self, v: qusql_parse::DropView<'a>) {
1849        for i in v.views {
1850            let key = match self.parse_qname(&i) {
1851                Some(k) => k,
1852                None => continue,
1853            };
1854            match self.schemas.schemas.entry(key) {
1855                alloc::collections::btree_map::Entry::Occupied(e) => {
1856                    if !e.get().view {
1857                        self.issues
1858                            .err("Name defines a table not a view", &i)
1859                            .frag("Table defined here", &e.get().identifier_span);
1860                    } else {
1861                        e.remove();
1862                    }
1863                }
1864                alloc::collections::btree_map::Entry::Vacant(_) => {
1865                    if v.if_exists.is_none() {
1866                        self.issues
1867                            .err("A view with this name does not exist to drop", &i);
1868                    }
1869                }
1870            }
1871        }
1872    }
1873
1874    fn process_drop_function(&mut self, f: qusql_parse::DropFunction<'a>) {
1875        for (func_name, _args) in &f.functions {
1876            let Some(key) = self.parse_qname(func_name) else {
1877                continue;
1878            };
1879            if self.schemas.functions.remove(&key).is_none() && f.if_exists.is_none() {
1880                self.issues.err(
1881                    "A function with this name does not exist to drop",
1882                    func_name,
1883                );
1884            }
1885        }
1886    }
1887
1888    fn process_drop_procedure(&mut self, p: qusql_parse::DropProcedure<'a>) {
1889        let Some(key) = self.parse_qname(&p.procedure) else {
1890            return;
1891        };
1892        if self.schemas.procedures.remove(&key).is_none() && p.if_exists.is_none() {
1893            self.issues.err(
1894                "A procedure with this name does not exist to drop",
1895                &p.procedure,
1896            );
1897        }
1898    }
1899
1900    fn process_drop_index(&mut self, ci: qusql_parse::DropIndex<'a>) {
1901        let is_pg = self.options.parse_options.get_dialect().is_postgresql();
1902        let key = if is_pg {
1903            let schema = ci
1904                .on
1905                .as_ref()
1906                .and_then(|(_, t)| t.prefix.first().map(|(s, _)| s.clone()))
1907                .unwrap_or_else(|| Identifier::new("public", 0..0));
1908            IndexKey {
1909                schema: Some(schema),
1910                table: None,
1911                index: ci.index_name.clone(),
1912            }
1913        } else {
1914            IndexKey {
1915                schema: None,
1916                table: ci.on.as_ref().map(|(_, t)| t.identifier.clone()),
1917                index: ci.index_name.clone(),
1918            }
1919        };
1920        if self.schemas.indices.remove(&key).is_none() && ci.if_exists.is_none() {
1921            self.issues.err("No such index", &ci);
1922        }
1923    }
1924    /// DO $$ ... $$: re-parse the dollar-quoted body and recurse.
1925    fn process_do(&mut self, d: qusql_parse::Do<'a>) -> Result<(), ()> {
1926        match d.body {
1927            qusql_parse::DoBody::Statements(stmts) => self.process_statements(stmts)?,
1928            qusql_parse::DoBody::String(s, _) => {
1929                let span_offset = s.as_ptr() as usize - self.src.as_ptr() as usize;
1930                let body_opts = self
1931                    .options
1932                    .parse_options
1933                    .clone()
1934                    .function_body(true)
1935                    .span_offset(span_offset);
1936                let stmts = parse_statements(s, self.issues, &body_opts);
1937                self.process_statements(stmts)?;
1938            }
1939        }
1940        Ok(())
1941    }
1942
1943    /// IF ... THEN / ELSEIF / ELSE: recurse into all branches.
1944    fn process_if(&mut self, i: qusql_parse::If<'a>) -> Result<(), ()> {
1945        for cond in i.conditions {
1946            if self.eval_condition(&cond.search_condition)? {
1947                return self.process_statements(cond.then);
1948            }
1949        }
1950        if let Some((_, stmts)) = i.else_ {
1951            return self.process_statements(stmts);
1952        }
1953        Ok(())
1954    }
1955
1956    /// SELECT used at the top level of a schema file must be a bare list of
1957    /// function calls with no FROM / WHERE / LIMIT / etc.  Each expression is
1958    /// dispatched to `process_expression`.
1959    fn process_select(&mut self, s: qusql_parse::Select<'a>) -> Result<(), ()> {
1960        // Reject anything that looks like a real query.
1961        if let Some(span) = &s.from_span {
1962            self.issues
1963                .err("SELECT with FROM is not supported at schema level", span);
1964            return Err(());
1965        }
1966        if let Some((_, span)) = &s.where_ {
1967            self.issues
1968                .err("SELECT with WHERE is not supported at schema level", span);
1969            return Err(());
1970        }
1971        if let Some((span, _)) = &s.group_by {
1972            self.issues.err(
1973                "SELECT with GROUP BY is not supported at schema level",
1974                span,
1975            );
1976            return Err(());
1977        }
1978        if let Some((_, span)) = &s.having {
1979            self.issues
1980                .err("SELECT with HAVING is not supported at schema level", span);
1981            return Err(());
1982        }
1983        if let Some((span, _, _)) = &s.limit {
1984            self.issues
1985                .err("SELECT with LIMIT is not supported at schema level", span);
1986            return Err(());
1987        }
1988        if let Some((span, _)) = &s.order_by {
1989            self.issues.err(
1990                "SELECT with ORDER BY is not supported at schema level",
1991                span,
1992            );
1993            return Err(());
1994        }
1995        for se in s.select_exprs {
1996            self.process_expression(se.expr)?;
1997        }
1998        Ok(())
1999    }
2000
2001    fn process_expression(&mut self, expr: Expression<'a>) -> Result<(), ()> {
2002        self.eval_expr(&expr).map(|_| ())
2003    }
2004
2005    fn process_update(&mut self, u: qusql_parse::Update<'a>) -> Result<(), ()> {
2006        // We cannot evaluate SET expressions. Error out if any target table has
2007        // tracked rows whose state would be changed; if there are no tracked
2008        // rows the update has no effect on our model.
2009        let span = u.update_span;
2010        for tref in u.tables {
2011            if let qusql_parse::TableReference::Table { identifier, .. } = tref {
2012                let Some(key) = self.parse_qname(&identifier) else {
2013                    continue;
2014                };
2015                if lookup_name(&self.rows, &key, ()).is_some_and(|r| !r.is_empty()) {
2016                    self.issues.err(
2017                        "UPDATE on a table with tracked rows is not supported in schema evaluator",
2018                        &span,
2019                    );
2020                    return Err(());
2021                }
2022            }
2023        }
2024        Ok(())
2025    }
2026
2027    fn process_delete(&mut self, d: qusql_parse::Delete<'a>) -> Result<(), ()> {
2028        let qusql_parse::Delete {
2029            tables,
2030            using,
2031            where_,
2032            order_by,
2033            limit,
2034            ..
2035        } = d;
2036
2037        // USING / ORDER BY / LIMIT are not supported: error if any target table
2038        // has tracked rows that would be affected.
2039        let has_unsupported = !using.is_empty() || order_by.is_some() || limit.is_some();
2040        if has_unsupported {
2041            for table in &tables {
2042                let Some(key) = self.parse_qname(table) else {
2043                    continue;
2044                };
2045                if lookup_name(&self.rows, &key, ()).is_some_and(|r| !r.is_empty()) {
2046                    self.issues.err(
2047                        "DELETE with USING/ORDER BY/LIMIT on a table with tracked rows \
2048                         is not supported in schema evaluator",
2049                        table,
2050                    );
2051                    return Err(());
2052                }
2053            }
2054        }
2055
2056        if let Some((where_expr, _)) = where_ {
2057            // Evaluate the WHERE for each tracked row; keep rows that do NOT match.
2058            for table in &tables {
2059                let Some(key) = self.parse_qname(table) else {
2060                    continue;
2061                };
2062                let Some(source_rows) = self.rows.get(&key) else {
2063                    continue;
2064                };
2065                let source_rows = source_rows.clone();
2066                let mut new_rows = Vec::new();
2067                for row in source_rows {
2068                    let saved = self.current_row.replace(row.clone());
2069                    let matches = self.eval_expr(&where_expr).map(|v| v.is_truthy());
2070                    self.current_row = saved;
2071                    match matches {
2072                        Ok(true) => {} // row is deleted
2073                        Ok(false) => new_rows.push(row),
2074                        Err(()) => return Err(()),
2075                    }
2076                }
2077                self.rows.insert(key, new_rows);
2078            }
2079        } else {
2080            // No WHERE - all rows in every target table are deleted.
2081            for table in tables {
2082                let Some(key) = self.parse_qname(&table) else {
2083                    continue;
2084                };
2085                self.rows.remove(&key);
2086            }
2087        }
2088        Ok(())
2089    }
2090
2091    fn process_alter_type(&mut self, a: qusql_parse::AlterType<'a>) {
2092        let Some(key) = self.parse_qname(&a.name) else {
2093            return;
2094        };
2095        match a.action {
2096            qusql_parse::AlterTypeAction::AddValue {
2097                if_not_exists_span,
2098                new_enum_value,
2099                ..
2100            } => {
2101                let Some(TypeDef::Enum { values, .. }) = self.schemas.types.get_mut(&key) else {
2102                    self.issues.err("Type not found", &a.name);
2103                    return;
2104                };
2105                let new_val: Cow<'a, str> = new_enum_value.value;
2106                if values.contains(&new_val) {
2107                    if if_not_exists_span.is_none() {
2108                        self.issues.err("Enum value already exists", &a.name);
2109                    }
2110                    // IF NOT EXISTS: silently skip
2111                } else {
2112                    Arc::make_mut(values).push(new_val);
2113                }
2114            }
2115            qusql_parse::AlterTypeAction::RenameTo { new_name, .. } => {
2116                let new_key = self.make_table_key(key.schema_name().cloned(), new_name);
2117                if let Some(typedef) = self.schemas.types.remove(&key) {
2118                    self.schemas.types.insert(new_key, typedef);
2119                } else {
2120                    self.issues.err("Type not found", &a.name);
2121                }
2122            }
2123            qusql_parse::AlterTypeAction::RenameValue {
2124                existing_enum_value,
2125                new_enum_value,
2126                ..
2127            } => {
2128                let Some(TypeDef::Enum { values, .. }) = self.schemas.types.get_mut(&key) else {
2129                    self.issues.err("Type not found", &a.name);
2130                    return;
2131                };
2132                let old_val: Cow<'a, str> = existing_enum_value.value;
2133                let new_val: Cow<'a, str> = new_enum_value.value;
2134                if let Some(entry) = Arc::make_mut(values).iter_mut().find(|v| **v == old_val) {
2135                    *entry = new_val;
2136                } else {
2137                    self.issues.err("Enum value not found", &a.name);
2138                }
2139            }
2140            // Other ALTER TYPE actions (OWNER TO, SET SCHEMA, etc.) have no effect on
2141            // the parts of the schema we track.
2142            _ => {}
2143        }
2144    }
2145
2146    fn process_truncate_table(&mut self, t: qusql_parse::TruncateTable<'a>) {
2147        for spec in t.tables {
2148            let Some(key) = self.parse_qname(&spec.table_name) else {
2149                continue;
2150            };
2151            self.rows.remove(&key);
2152        }
2153    }
2154
2155    fn process_create_schema(&mut self, s: qusql_parse::CreateSchema<'a>) {
2156        if !self.options.parse_options.get_dialect().is_postgresql() {
2157            self.issues.err(
2158                "CREATE SCHEMA is not supported in MySQL; use CREATE DATABASE",
2159                &s,
2160            );
2161            return;
2162        }
2163        let Some(name) = s.name else {
2164            // AUTHORIZATION-only CREATE SCHEMA — nothing to register
2165            return;
2166        };
2167        match self.schemas.schema_names.get(&name) {
2168            Some(_) if s.if_not_exists.is_none() => {
2169                self.issues.err("Schema already exists", &name);
2170            }
2171            _ => {
2172                self.schemas.schema_names.insert(name);
2173            }
2174        }
2175    }
2176
2177    fn process_drop_schema(&mut self, s: qusql_parse::DropSchema<'a>) {
2178        if !self.schemas.schema_names.remove(&s.schema) && s.if_exists.is_none() {
2179            self.issues.err("Schema does not exist", &s.schema);
2180        }
2181        // Remove all tables/views registered under this schema.
2182        self.schemas
2183            .schemas
2184            .retain(|k, _| k.schema_name().map(|n| n == &s.schema) != Some(true));
2185        // Remove all functions in this schema.
2186        self.schemas
2187            .functions
2188            .retain(|k, _| k.schema_name().map(|n| n == &s.schema) != Some(true));
2189        // Remove all procedures in this schema.
2190        self.schemas
2191            .procedures
2192            .retain(|k, _| k.schema_name().map(|n| n == &s.schema) != Some(true));
2193        // Remove all types in this schema.
2194        self.schemas
2195            .types
2196            .retain(|k, _| k.schema_name().map(|n| n == &s.schema) != Some(true));
2197        // Remove all indices associated with this schema.
2198        self.schemas
2199            .indices
2200            .retain(|k, _| k.schema.as_ref() != Some(&s.schema));
2201        // Remove all sequences in this schema.
2202        self.schemas
2203            .sequences
2204            .retain(|k, _| k.schema_name().map(|n| n == &s.schema) != Some(true));
2205        // Remove in-memory rows for tables that belonged to this schema.
2206        self.rows
2207            .retain(|k, _| k.schema_name().map(|n| n == &s.schema) != Some(true));
2208    }
2209
2210    fn process_rename_table(&mut self, r: qusql_parse::RenameTable<'a>) {
2211        for pair in r.table_to_tables {
2212            let old_key = match self.parse_qname(&pair.table) {
2213                Some(k) => k,
2214                None => continue,
2215            };
2216            let new_key = match self.parse_qname(&pair.new_table) {
2217                Some(k) => k,
2218                None => continue,
2219            };
2220            // Rename in schemas map.
2221            if let Some(schema) = self.schemas.schemas.remove(&old_key) {
2222                self.schemas.schemas.insert(new_key.clone(), schema);
2223            } else {
2224                self.issues.err("Table not found", &pair.table);
2225            }
2226            // Rename tracked rows if present.
2227            if let Some(rows) = self.rows.remove(&old_key) {
2228                self.rows.insert(new_key, rows);
2229            }
2230        }
2231    }
2232
2233    fn process_insert(&mut self, i: qusql_parse::InsertReplace<'a>) -> Result<(), ()> {
2234        let Some(table_key) = self.parse_qname(&i.table) else {
2235            return Ok(());
2236        };
2237        let col_names: Vec<&'a str> = i.columns.iter().map(|c| c.value).collect();
2238
2239        if let Some(set) = i.set {
2240            // INSERT ... SET col = expr, ...: evaluate as a single-row VALUES insert.
2241            let mut row: Vec<(&'a str, SqlValue<'a>)> = Vec::new();
2242            for pair in set.pairs {
2243                if let Ok(val) = self.eval_expr(&pair.value) {
2244                    row.push((pair.column.value, val));
2245                }
2246            }
2247            self.rows.entry(table_key).or_default().push(Rc::new(row));
2248            return Ok(());
2249        }
2250
2251        if let Some((_, value_rows)) = i.values {
2252            // INSERT ... VALUES (...)
2253            for row_exprs in value_rows {
2254                let mut row: Vec<(&'a str, SqlValue<'a>)> = Vec::new();
2255                for (col, expr) in col_names.iter().zip(row_exprs.iter()) {
2256                    if let Ok(val) = self.eval_expr(expr) {
2257                        row.push((col, val));
2258                    }
2259                }
2260                self.rows
2261                    .entry(table_key.clone())
2262                    .or_default()
2263                    .push(Rc::new(row));
2264            }
2265            return Ok(());
2266        }
2267
2268        let Some(select_stmt) = i.select else {
2269            return Ok(());
2270        };
2271        // Clone select expressions before eval borrows `self`.
2272        // Only available for a plain SELECT; compound queries (UNION etc.) produce no
2273        // named expressions to project, so we track rows without column values.
2274        let select_exprs: Vec<_> = if let qusql_parse::Statement::Select(s) = &select_stmt {
2275            s.select_exprs.iter().map(|se| se.expr.clone()).collect()
2276        } else {
2277            Vec::new()
2278        };
2279        let source_rows = self.eval_statement_rows(&select_stmt)?;
2280        let mut built_rows = Vec::with_capacity(source_rows.len());
2281        for source_row in source_rows {
2282            let saved_row = self.current_row.replace(source_row);
2283            let mut row: Vec<(&'a str, SqlValue<'a>)> = Vec::new();
2284            for (col, expr) in col_names.iter().zip(select_exprs.iter()) {
2285                if let Ok(val) = self.eval_expr(expr) {
2286                    row.push((col, val));
2287                }
2288            }
2289            self.current_row = saved_row;
2290            built_rows.push(Rc::new(row));
2291        }
2292        self.rows.entry(table_key).or_default().extend(built_rows);
2293        Ok(())
2294    }
2295
2296    /// Evaluate an expression to a `SqlValue`.
2297    /// Reads the current row from `self.current_row` (set by eval_select_matching_rows).
2298    /// Aggregate functions read rows from `self.current_table_rows` (set by eval_condition).
2299    /// Returns `Err(())` for expression types not yet handled by the evaluator.
2300    fn eval_expr(&mut self, expr: &Expression<'a>) -> Result<SqlValue<'a>, ()> {
2301        match expr {
2302            Expression::Null(_) => Ok(SqlValue::Null),
2303            Expression::Bool(b) => Ok(SqlValue::Bool(b.value)),
2304            Expression::Integer(i) => Ok(SqlValue::Integer(i.value as i64)),
2305            Expression::String(s) => Ok(match &s.value {
2306                Cow::Borrowed(b) => SqlValue::SourceText(b),
2307                Cow::Owned(o) => SqlValue::OwnedText(o.clone()),
2308            }),
2309            Expression::Identifier(id) => {
2310                if let [IdentifierPart::Name(name)] = id.parts.as_slice() {
2311                    self.bindings
2312                        .get(name.value)
2313                        .cloned()
2314                        .or_else(|| {
2315                            self.current_row.as_ref().and_then(|r| {
2316                                r.iter()
2317                                    .find(|(k, _)| *k == name.value)
2318                                    .map(|(_, v)| v.clone())
2319                            })
2320                        })
2321                        .ok_or(())
2322                } else {
2323                    Err(())
2324                }
2325            }
2326            Expression::Exists(e) => Ok(SqlValue::Bool(self.eval_exists(&e.subquery)?)),
2327            Expression::Unary(u) => match &u.op {
2328                qusql_parse::UnaryOperator::Not(_) | qusql_parse::UnaryOperator::LogicalNot(_) => {
2329                    Ok(SqlValue::Bool(!self.eval_expr(&u.operand)?.is_truthy()))
2330                }
2331                qusql_parse::UnaryOperator::Minus(_) => match self.eval_expr(&u.operand)? {
2332                    SqlValue::Integer(i) => Ok(SqlValue::Integer(-i)),
2333                    _ => Err(()),
2334                },
2335                _ => Err(()),
2336            },
2337            Expression::Function(f) => self.eval_function_expr(f),
2338            Expression::AggregateFunction(f) => self.eval_aggregate(f),
2339            Expression::Binary(b) => self.eval_binary_expr(b),
2340            _ => {
2341                self.issues
2342                    .err("Unimplemented expression in schema evaluator", expr);
2343                Err(())
2344            }
2345        }
2346    }
2347
2348    fn eval_exists(&mut self, stmt: &qusql_parse::Statement<'a>) -> Result<bool, ()> {
2349        Ok(!self.eval_statement_rows(stmt)?.is_empty())
2350    }
2351
2352    /// Resolve the rows for a SELECT's FROM clause.
2353    /// Returns empty if there is no FROM clause.
2354    /// Errors (with a message) for:
2355    ///   - joins or non-table FROM references (e.g. subqueries in FROM)
2356    ///   - qualified table names (e.g. `information_schema.columns`)
2357    ///   - unqualified table names not known to the schema evaluator
2358    fn resolve_from_rows(&mut self, s: &qusql_parse::Select<'a>) -> Result<Vec<Row<'a>>, ()> {
2359        use qusql_parse::TableReference;
2360        let Some(refs) = s.table_references.as_deref() else {
2361            return Ok(Vec::new());
2362        };
2363        let [TableReference::Table { identifier, .. }] = refs else {
2364            self.issues.err(
2365                "FROM clause with joins or subqueries is not supported in schema evaluator",
2366                &refs[0],
2367            );
2368            return Err(());
2369        };
2370
2371        let table_key: QualifiedIdentifier = match identifier.prefix.as_slice() {
2372            [] => QualifiedIdentifier::Unqualified(identifier.identifier.clone()),
2373            [(schema, _)]
2374                if schema.value.eq_ignore_ascii_case("information_schema")
2375                    && identifier.identifier.value.eq_ignore_ascii_case("columns") =>
2376            {
2377                // Synthesize information_schema.columns from the current schema state.
2378                let rows = self
2379                    .schemas
2380                    .schemas
2381                    .iter()
2382                    .flat_map(|(key, schema)| {
2383                        schema.columns.iter().map(move |col| {
2384                            Rc::new(alloc::vec![
2385                                ("table_name", SqlValue::SourceText(key.table_name().value)),
2386                                ("column_name", SqlValue::SourceText(col.identifier.value)),
2387                            ])
2388                        })
2389                    })
2390                    .collect();
2391                return Ok(rows);
2392            }
2393            [(schema, _)] => {
2394                QualifiedIdentifier::Qualified(schema.clone(), identifier.identifier.clone())
2395            }
2396            _ => {
2397                self.issues.err(
2398                    "Qualified table name in FROM clause is not supported in schema evaluator",
2399                    identifier,
2400                );
2401                return Err(());
2402            }
2403        };
2404
2405        let Some((resolved_key, _)) = lookup_name_key(
2406            &self.schemas.schemas,
2407            &table_key,
2408            self.search_path.as_slice(),
2409        ) else {
2410            self.issues.err(
2411                alloc::format!("Unknown table `{table_key}` referenced in schema evaluator"),
2412                identifier,
2413            );
2414            return Err(());
2415        };
2416        Ok(self.rows.get(resolved_key).cloned().unwrap_or_default())
2417    }
2418
2419    /// Evaluate any SELECT-like statement (plain SELECT or UNION/INTERSECT/EXCEPT compound
2420    /// query) to a list of result rows.
2421    fn eval_statement_rows(
2422        &mut self,
2423        stmt: &qusql_parse::Statement<'a>,
2424    ) -> Result<Vec<Row<'a>>, ()> {
2425        match stmt {
2426            qusql_parse::Statement::Select(s) => self.eval_select_matching_rows(s),
2427            qusql_parse::Statement::CompoundQuery(cq) => self.eval_compound_query_rows(cq),
2428            _ => {
2429                self.issues
2430                    .err("Unsupported statement kind in INSERT ... SELECT", stmt);
2431                Err(())
2432            }
2433        }
2434    }
2435
2436    /// Evaluate a UNION / INTERSECT / EXCEPT compound query to rows.
2437    /// Only UNION (ALL or deduplicated) is supported; INTERSECT and EXCEPT error out.
2438    fn eval_compound_query_rows(
2439        &mut self,
2440        cq: &qusql_parse::CompoundQuery<'a>,
2441    ) -> Result<Vec<Row<'a>>, ()> {
2442        use qusql_parse::CompoundOperator;
2443        let mut result = self.eval_statement_rows(&cq.left)?;
2444        for branch in &cq.with {
2445            match branch.operator {
2446                CompoundOperator::Union => {
2447                    let branch_rows = self.eval_statement_rows(&branch.statement)?;
2448                    result.extend(branch_rows);
2449                }
2450                CompoundOperator::Intersect | CompoundOperator::Except => {
2451                    self.issues.err(
2452                        "INTERSECT / EXCEPT is not supported in schema evaluator",
2453                        &branch.operator_span,
2454                    );
2455                    return Err(());
2456                }
2457            }
2458        }
2459        Ok(result)
2460    }
2461
2462    /// Return the rows from the single table in a SELECT that satisfy the WHERE clause.
2463    fn eval_select_matching_rows(
2464        &mut self,
2465        s: &qusql_parse::Select<'a>,
2466    ) -> Result<Vec<Row<'a>>, ()> {
2467        let source_rows = self.resolve_from_rows(s)?;
2468        let where_expr: Option<Expression<'a>> = s.where_.as_ref().map(|(e, _)| e.clone());
2469        let mut result = Vec::new();
2470        for row in source_rows {
2471            let saved_row = self.current_row.replace(row.clone());
2472            let eval_result = match &where_expr {
2473                Some(expr) => self.eval_expr(expr).map(|v| v.is_truthy()),
2474                None => Ok(true),
2475            };
2476            self.current_row = saved_row;
2477            if eval_result? {
2478                result.push(row);
2479            }
2480        }
2481        Ok(result)
2482    }
2483
2484    fn eval_function_expr(
2485        &mut self,
2486        f: &qusql_parse::FunctionCallExpression<'a>,
2487    ) -> Result<SqlValue<'a>, ()> {
2488        use qusql_parse::Function;
2489        match &f.function {
2490            Function::Other(parts) if !parts.is_empty() => {
2491                let fn_ident = parts.last().unwrap().clone();
2492                let schema_ident = (parts.len() >= 2).then(|| parts[parts.len() - 2].clone());
2493                let key = match schema_ident {
2494                    Some(s) => QualifiedIdentifier::Qualified(s, fn_ident.clone()),
2495                    None => QualifiedIdentifier::Unqualified(fn_ident.clone()),
2496                };
2497                let func_info =
2498                    lookup_name(&self.schemas.functions, &key, self.search_path.as_slice())
2499                        .and_then(|func| {
2500                            func.body
2501                                .as_ref()
2502                                .map(|b| (func.params.clone(), b.statements.clone()))
2503                        });
2504                let Some((params, statements)) = func_info else {
2505                    self.issues.err(
2506                        alloc::format!(
2507                            "Unknown function or function has no evaluable body: {}",
2508                            parts
2509                                .iter()
2510                                .map(|p| p.value)
2511                                .collect::<alloc::vec::Vec<_>>()
2512                                .join(".")
2513                        ),
2514                        f,
2515                    );
2516                    return Err(());
2517                };
2518                let mut bindings = BTreeMap::new();
2519                for (param, arg) in params.iter().zip(f.args.iter()) {
2520                    let Some(name) = &param.name else { continue };
2521                    if let Ok(value) = self.eval_expr(arg) {
2522                        bindings.insert(name.value, value);
2523                    }
2524                }
2525                let old_bindings = core::mem::replace(&mut self.bindings, bindings);
2526                let old_return = self.return_value.take();
2527                let _ = self.process_statements(statements);
2528                let ret = self.return_value.take().unwrap_or(SqlValue::Null);
2529                self.return_value = old_return;
2530                self.bindings = old_bindings;
2531                Ok(ret)
2532            }
2533            Function::Coalesce => {
2534                // Clone args to avoid borrow conflict between &f and &mut self.
2535                let args: Vec<_> = f.args.clone();
2536                for arg in &args {
2537                    let v = self.eval_expr(arg)?;
2538                    if v != SqlValue::Null {
2539                        return Ok(v);
2540                    }
2541                }
2542                Ok(SqlValue::Null)
2543            }
2544            Function::Exists => {
2545                let Some(Expression::Subquery(sq)) = f.args.first() else {
2546                    self.issues.err("EXISTS without subquery argument", f);
2547                    return Err(());
2548                };
2549                let qusql_parse::Statement::Select(s) = &sq.expression else {
2550                    self.issues.err("EXISTS argument is not a SELECT", f);
2551                    return Err(());
2552                };
2553                let s = s.clone();
2554                Ok(SqlValue::Bool(
2555                    !self.eval_select_matching_rows(&s)?.is_empty(),
2556                ))
2557            }
2558            Function::Nextval => {
2559                let arg = f.args.first().ok_or(())?;
2560                let seq_str = match self.eval_expr(arg)? {
2561                    SqlValue::SourceText(s) => alloc::borrow::Cow::Borrowed(s),
2562                    SqlValue::OwnedText(s) => alloc::borrow::Cow::Owned(s),
2563                    _ => {
2564                        self.issues.err("nextval: argument must be a string", f);
2565                        return Err(());
2566                    }
2567                };
2568                // Parse "schema.sequence" or "sequence" from the string argument and
2569                // check that the sequence was registered via CREATE SEQUENCE.
2570                let seq_name = seq_str.as_ref();
2571                let key = if let Some((schema, seq)) = seq_name.split_once('.') {
2572                    QualifiedIdentifier::Qualified(
2573                        Identifier::new(schema, 0..0),
2574                        Identifier::new(seq, 0..0),
2575                    )
2576                } else {
2577                    QualifiedIdentifier::Unqualified(Identifier::new(seq_name, 0..0))
2578                };
2579                let found = lookup_name(&self.schemas.sequences, &key, self.search_path.as_slice())
2580                    .is_some();
2581                if !found {
2582                    self.issues
2583                        .err(alloc::format!("nextval: unknown sequence `{seq_name}`"), f);
2584                    return Err(());
2585                }
2586                Ok(SqlValue::Integer(1))
2587            }
2588            _ => {
2589                self.issues
2590                    .err("Unimplemented function in schema evaluator", f);
2591                Err(())
2592            }
2593        }
2594    }
2595
2596    fn eval_aggregate(
2597        &mut self,
2598        f: &qusql_parse::AggregateFunctionCallExpression<'a>,
2599    ) -> Result<SqlValue<'a>, ()> {
2600        use qusql_parse::Function;
2601        match &f.function {
2602            Function::Max => {
2603                let col_expr = f.args.first().ok_or(())?.clone();
2604                // Take rows out so we can call &mut self methods during iteration.
2605                let rows = core::mem::take(&mut self.current_table_rows);
2606                let mut max: Option<SqlValue<'a>> = None;
2607                for r in &rows {
2608                    // Set current_row so column references in the expression resolve correctly.
2609                    let saved = self.current_row.replace(r.clone());
2610                    // Skip rows where evaluation fails (NULL semantics for aggregates).
2611                    let v = self.eval_expr(&col_expr);
2612                    self.current_row = saved;
2613                    if let Ok(v) = v
2614                        && v != SqlValue::Null
2615                    {
2616                        max = Some(match max {
2617                            None => v,
2618                            Some(SqlValue::Integer(m)) => {
2619                                if let SqlValue::Integer(n) = &v {
2620                                    SqlValue::Integer(m.max(*n))
2621                                } else {
2622                                    v
2623                                }
2624                            }
2625                            Some(existing) => existing,
2626                        });
2627                    }
2628                }
2629                self.current_table_rows = rows;
2630                Ok(max.unwrap_or(SqlValue::Null))
2631            }
2632            Function::Count => {
2633                let rows = core::mem::take(&mut self.current_table_rows);
2634                let is_star = matches!(
2635                    f.args.first(),
2636                    Some(Expression::Identifier(ie))
2637                        if matches!(ie.parts.as_slice(), [IdentifierPart::Star(_)])
2638                );
2639                let count = if f.args.is_empty() || is_star {
2640                    // COUNT(*) or COUNT() - count all rows
2641                    rows.len() as i64
2642                } else {
2643                    let col_expr = f.args.first().unwrap().clone();
2644                    let mut n = 0i64;
2645                    for r in &rows {
2646                        let saved = self.current_row.replace(r.clone());
2647                        let v = self.eval_expr(&col_expr);
2648                        self.current_row = saved;
2649                        if matches!(v, Ok(v) if v != SqlValue::Null) {
2650                            n += 1;
2651                        }
2652                    }
2653                    n
2654                };
2655                self.current_table_rows = rows;
2656                Ok(SqlValue::Integer(count))
2657            }
2658            _ => {
2659                self.issues
2660                    .err("Unimplemented aggregate function in schema evaluator", f);
2661                Err(())
2662            }
2663        }
2664    }
2665
2666    fn eval_binary_expr(
2667        &mut self,
2668        b: &qusql_parse::BinaryExpression<'a>,
2669    ) -> Result<SqlValue<'a>, ()> {
2670        use qusql_parse::BinaryOperator;
2671        let lhs = self.eval_expr(&b.lhs)?;
2672        // Short-circuit AND/OR before evaluating rhs.
2673        match &b.op {
2674            BinaryOperator::And(_) => {
2675                if !lhs.is_truthy() {
2676                    return Ok(SqlValue::Bool(false));
2677                }
2678                return Ok(SqlValue::Bool(self.eval_expr(&b.rhs)?.is_truthy()));
2679            }
2680            BinaryOperator::Or(_) => {
2681                if lhs.is_truthy() {
2682                    return Ok(SqlValue::Bool(true));
2683                }
2684                return Ok(SqlValue::Bool(self.eval_expr(&b.rhs)?.is_truthy()));
2685            }
2686            _ => {}
2687        }
2688        let rhs = self.eval_expr(&b.rhs)?;
2689        // NULL comparisons propagate NULL (not an error).
2690        Ok(match &b.op {
2691            BinaryOperator::Eq(_) => lhs.sql_eq(&rhs).map_or(SqlValue::Null, SqlValue::Bool),
2692            BinaryOperator::Neq(_) => lhs
2693                .sql_eq(&rhs)
2694                .map_or(SqlValue::Null, |v| SqlValue::Bool(!v)),
2695            BinaryOperator::LtEq(_) => lhs.sql_lte(&rhs).map_or(SqlValue::Null, SqlValue::Bool),
2696            BinaryOperator::Lt(_) => rhs
2697                .sql_lte(&lhs)
2698                .map_or(SqlValue::Null, |v| SqlValue::Bool(!v)),
2699            BinaryOperator::GtEq(_) => rhs.sql_lte(&lhs).map_or(SqlValue::Null, SqlValue::Bool),
2700            BinaryOperator::Gt(_) => lhs
2701                .sql_lte(&rhs)
2702                .map_or(SqlValue::Null, |v| SqlValue::Bool(!v)),
2703            _ => {
2704                self.issues
2705                    .err("Unimplemented binary operator in schema evaluator", b);
2706                return Err(());
2707            }
2708        })
2709    }
2710
2711    /// Evaluate the search condition of an IF branch as a boolean.
2712    /// Sets `self.current_table_rows` from the FROM clause (if any) so that
2713    /// aggregate expressions in the condition can read the table rows.
2714    /// Returns `Err(())` if the condition uses constructs the evaluator does not handle.
2715    fn eval_condition(&mut self, s: &qusql_parse::Select<'a>) -> Result<bool, ()> {
2716        let expr = s.select_exprs.first().map(|se| se.expr.clone()).ok_or(())?;
2717
2718        // Load the FROM table's rows into current_table_rows for aggregate evaluation.
2719        let table_rows = self.resolve_from_rows(s)?;
2720        let saved = core::mem::replace(&mut self.current_table_rows, table_rows);
2721        let result = self.eval_expr(&expr);
2722        self.current_table_rows = saved;
2723
2724        Ok(result?.is_truthy())
2725    }
2726
2727    fn process_plpgsql_execute(&mut self, e: qusql_parse::PlpgsqlExecute<'a>) -> Result<(), ()> {
2728        let sql = self.resolve_expr_to_bound_string(&e.command);
2729        let Some(sql) = sql else {
2730            self.issues.err(
2731                "EXECUTE argument could not be resolved to a known SQL string",
2732                &e,
2733            );
2734            return Err(());
2735        };
2736        let span_offset = sql.as_ptr() as usize - self.src.as_ptr() as usize;
2737        let opts = self.options.parse_options.clone().span_offset(span_offset);
2738        let stmts = parse_statements(sql, self.issues, &opts);
2739        let _ = self.process_statements(stmts);
2740        Ok(())
2741    }
2742
2743    /// Try to resolve an expression to a `&'a str` from the current bindings.
2744    /// Only succeeds for bare identifier expressions that name a bound parameter
2745    /// whose value is a borrow from the original source text.
2746    fn resolve_expr_to_bound_string(&self, expr: &Expression<'a>) -> Option<&'a str> {
2747        let Expression::Identifier(ident) = expr else {
2748            return None;
2749        };
2750        let [IdentifierPart::Name(name)] = ident.parts.as_slice() else {
2751            return None;
2752        };
2753        self.bindings
2754            .get(name.value)
2755            .and_then(|v| v.as_source_text())
2756    }
2757}
2758
2759/// Parse a schema definition and return a terse description
2760///
2761/// Errors and warnings are added to issues. The schema is successfully
2762/// parsed if no errors are added to issues.
2763/// Built-in table/view definitions that are automatically available in
2764/// PostgreSQL and PostGIS databases.  These are injected into every
2765/// `Schemas` produced by [`parse_schemas`] when the dialect is
2766/// [`SQLDialect::PostgreSQL`] or [`SQLDialect::PostGIS`].
2767const POSTGRESQL_BUILTIN_SQL: &str = "
2768CREATE TABLE spatial_ref_sys (
2769    srid INTEGER NOT NULL,
2770    auth_name VARCHAR(256),
2771    auth_srid INTEGER,
2772    srtext VARCHAR(2048),
2773    proj4text VARCHAR(2048)
2774);
2775CREATE VIEW geometry_columns AS (
2776    SELECT '' AS f_table_catalog, '' AS f_table_schema, '' AS f_table_name,
2777           '' AS f_geometry_column, 0 AS coord_dimension, 0 AS srid, '' AS type
2778);
2779CREATE VIEW geography_columns AS (
2780    SELECT '' AS f_table_catalog, '' AS f_table_schema, '' AS f_table_name,
2781           '' AS f_geography_column, 0 AS coord_dimension, 0 AS srid, '' AS type
2782);
2783";
2784
2785pub fn parse_schemas<'a>(
2786    src: &'a str,
2787    issues: &mut Issues<'a>,
2788    options: &TypeOptions,
2789) -> Schemas<'a> {
2790    let statements = parse_statements(src, issues, &options.parse_options);
2791
2792    let mut schemas = Schemas {
2793        schemas: Default::default(),
2794        schema_names: Default::default(),
2795        procedures: Default::default(),
2796        functions: Default::default(),
2797        indices: Default::default(),
2798        types: Default::default(),
2799        sequences: Default::default(),
2800    };
2801
2802    SchemaCtx::new(&mut schemas, issues, src, options).process_top_level_statements(statements);
2803
2804    let dummy_schemas = Schemas::default();
2805
2806    let mut typer = crate::typer::Typer {
2807        schemas: &dummy_schemas,
2808        issues,
2809        reference_types: Vec::new(),
2810        outer_reference_types: Vec::new(),
2811        arg_types: Default::default(),
2812        options,
2813        with_schemas: Default::default(),
2814    };
2815
2816    // Compute nullity of generated columns
2817    for (key, schema) in &mut schemas.schemas {
2818        if schema.columns.iter().all(|v| v.as_.is_none()) {
2819            continue;
2820        }
2821        typer.reference_types.clear();
2822        let mut columns = Vec::new();
2823        for c in &schema.columns {
2824            columns.push((c.identifier.clone(), c.type_.clone()));
2825        }
2826        typer.reference_types.push(crate::typer::ReferenceType {
2827            name: Some(key.table_name().clone()),
2828            span: schema.identifier_span.clone(),
2829            columns,
2830        });
2831        for c in &mut schema.columns {
2832            if let Some(as_) = &c.as_ {
2833                let full_type = crate::type_expression::type_expression(
2834                    &mut typer,
2835                    as_,
2836                    crate::type_expression::ExpressionFlags::default(),
2837                    BaseType::Any,
2838                );
2839                c.type_.not_null = full_type.not_null;
2840            }
2841        }
2842    }
2843
2844    // Inject dialect-specific built-in schemas so that system tables like
2845    // `spatial_ref_sys` are always resolvable without requiring the user to
2846    // declare them in their schema file.
2847    let dialect = options.parse_options.get_dialect();
2848    if dialect.is_postgresql() {
2849        // Coerce 'static to &'a str — sound because 'static: 'a.
2850        let builtin_src: &'a str = POSTGRESQL_BUILTIN_SQL;
2851        let builtin_options = TypeOptions::new().dialect(dialect);
2852        let builtin_stmts = parse_statements(
2853            builtin_src,
2854            &mut Issues::new(builtin_src),
2855            &builtin_options.parse_options,
2856        );
2857        let mut builtin_schemas: Schemas<'a> = Schemas::default();
2858        SchemaCtx::new(
2859            &mut builtin_schemas,
2860            &mut Issues::new(builtin_src),
2861            builtin_src,
2862            &builtin_options,
2863        )
2864        .process_top_level_statements(builtin_stmts);
2865        // User-defined tables take priority; only add entries not already present.
2866        for (k, v) in builtin_schemas.schemas {
2867            schemas.schemas.entry(k).or_insert(v);
2868        }
2869    }
2870
2871    schemas
2872}