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