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//! Facility for parsing SQL schemas into a terse format that can be used
14//! for typing statements.
15//!
16//! ```
17//! use qusql_type::{schema::parse_schemas, TypeOptions, SQLDialect, Issues};
18//! let schemas = "
19//!     -- Table structure for table `events`
20//!     DROP TABLE IF EXISTS `events`;
21//!     CREATE TABLE `events` (
22//!       `id` bigint(20) NOT NULL,
23//!       `user` int(11) NOT NULL,
24//!       `event_key` int(11) NOT NULL,
25//!       `time` datetime NOT NULL
26//!     ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
27//!
28//!     -- Table structure for table `events_keys`
29//!     DROP TABLE IF EXISTS `event_keys`;
30//!     CREATE TABLE `event_keys` (
31//!       `id` int(11) NOT NULL,
32//!       `name` text NOT NULL
33//!     ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
34//!
35//!     -- Stand-in structure for view `events_view`
36//!     -- (See below for the actual view)
37//!     DROP VIEW IF EXISTS `events_view`;
38//!     CREATE TABLE `events_view` (
39//!         `id` int(11),
40//!         `user` int(11) NOT NULL,
41//!         `event_key` text NOT NULL,
42//!         `time` datetime NOT NULL
43//!     );
44//!
45//!     -- Indexes for table `events`
46//!     ALTER TABLE `events`
47//!       ADD PRIMARY KEY (`id`),
48//!       ADD KEY `time` (`time`),
49//!       ADD KEY `event_key` (`event_key`);
50//!
51//!     -- Indexes for table `event_keys`
52//!     ALTER TABLE `event_keys`
53//!       ADD PRIMARY KEY (`id`);
54//!
55//!     -- Constraints for table `events`
56//!     ALTER TABLE `events`
57//!       ADD CONSTRAINT `event_key` FOREIGN KEY (`event_key`) REFERENCES `event_keys` (`id`);
58//!
59//!     -- Structure for view `events_view`
60//!     DROP TABLE IF EXISTS `events_view`;
61//!     DROP VIEW IF EXISTS `events_view`;
62//!     CREATE ALGORITHM=UNDEFINED DEFINER=`phpmyadmin`@`localhost`
63//!         SQL SECURITY DEFINER VIEW `events_view` AS
64//!         SELECT
65//!             `events`.`id` AS `id`,
66//!             `events`.`user` AS `user`,
67//!             `event_keys`.`name` AS `event_key`,
68//!             `events`.`time` AS `time`
69//!         FROM `events`, `event_keys`
70//!         WHERE `events`.`event_key` = `event_keys`.`id`;
71//!     ";
72//!
73//! let mut issues = Issues::new(schemas);
74//! let schemas = parse_schemas(schemas,
75//!     &mut issues,
76//!     &TypeOptions::new().dialect(SQLDialect::MariaDB));
77//!
78//! assert!(issues.is_ok());
79//!
80//! for (name, schema) in schemas.schemas {
81//!     println!("{name}: {schema:?}")
82//! }
83//! ```
84
85use crate::{
86    Type, TypeOptions,
87    type_::{BaseType, FullType},
88    type_statement,
89    typer::unqualified_name,
90};
91use alloc::{collections::BTreeMap, sync::Arc, vec::Vec};
92use qusql_parse::{
93    AddColumn, AddIndex, AlterColumn, DataType, DropColumn, Expression, Identifier, Issues,
94    ModifyColumn, Span, Spanned, parse_statements,
95};
96
97/// A column in a schema
98#[derive(Debug)]
99pub struct Column<'a> {
100    pub identifier: Identifier<'a>,
101    /// Type of the column
102    pub type_: FullType<'a>,
103    /// True if the column is auto_increment
104    pub auto_increment: bool,
105    pub default: bool,
106    pub as_: Option<Expression<'a>>,
107    pub generated: bool,
108}
109
110/// Schema representing a table or view
111#[derive(Debug)]
112pub struct Schema<'a> {
113    /// Span of identifier
114    pub identifier_span: Span,
115    /// List of columns
116    pub columns: Vec<Column<'a>>,
117    /// True if this is a view instead of a table
118    pub view: bool,
119}
120
121impl<'a> Schema<'a> {
122    pub fn get_column(&self, identifier: &str) -> Option<&Column<'a>> {
123        self.columns
124            .iter()
125            .find(|&column| column.identifier.value == identifier)
126    }
127    pub fn get_column_mut(&mut self, identifier: &str) -> Option<&mut Column<'a>> {
128        self.columns
129            .iter_mut()
130            .find(|column| column.identifier.value == identifier)
131    }
132}
133
134/// A procedure
135#[derive(Debug)]
136pub struct Procedure {}
137
138/// A function
139#[derive(Debug)]
140pub struct Functions {}
141
142#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
143pub struct IndexKey<'a> {
144    pub table: Option<Identifier<'a>>,
145    pub index: Identifier<'a>,
146}
147
148/// A description of tables, view, procedures and function in a schemas definition file
149#[derive(Debug, Default)]
150pub struct Schemas<'a> {
151    /// Map from name to Tables or views
152    pub schemas: BTreeMap<Identifier<'a>, Schema<'a>>,
153    /// Map from name to procedure
154    pub procedures: BTreeMap<Identifier<'a>, Procedure>,
155    /// Map from name to function
156    pub functions: BTreeMap<Identifier<'a>, Functions>,
157    /// Map from (table, index) to location
158    pub indices: BTreeMap<IndexKey<'a>, Span>,
159}
160
161pub(crate) fn parse_column<'a>(
162    data_type: DataType<'a>,
163    identifier: Identifier<'a>,
164    _issues: &mut Issues<'a>,
165    options: Option<&TypeOptions>,
166) -> Column<'a> {
167    let mut not_null = false;
168    let mut unsigned = false;
169    let mut auto_increment = false;
170    let mut default = false;
171    let mut _as = None;
172    let mut generated = false;
173    let mut primary_key = false;
174    let is_sqlite = options
175        .map(|v| v.parse_options.get_dialect().is_sqlite())
176        .unwrap_or_default();
177    for p in data_type.properties {
178        match p {
179            qusql_parse::DataTypeProperty::Signed(_) => unsigned = false,
180            qusql_parse::DataTypeProperty::Unsigned(_) => unsigned = true,
181            qusql_parse::DataTypeProperty::Null(_) => not_null = false,
182            qusql_parse::DataTypeProperty::NotNull(_) => not_null = true,
183            qusql_parse::DataTypeProperty::AutoIncrement(_) => auto_increment = true,
184            qusql_parse::DataTypeProperty::As((_, e)) => _as = Some(e),
185            qusql_parse::DataTypeProperty::Default(_) => default = true,
186            qusql_parse::DataTypeProperty::GeneratedAlways(_) => generated = true,
187            qusql_parse::DataTypeProperty::PrimaryKey(_) => primary_key = true,
188            _ => {}
189        }
190    }
191    let type_ = match data_type.type_ {
192        qusql_parse::Type::TinyInt(v) => {
193            if !unsigned && matches!(v, Some((1, _))) {
194                BaseType::Bool.into()
195            } else if unsigned {
196                Type::U8
197            } else {
198                Type::I8
199            }
200        }
201        qusql_parse::Type::SmallInt(_) => {
202            if unsigned {
203                Type::U16
204            } else {
205                Type::I16
206            }
207        }
208        qusql_parse::Type::MediumInt(_) => {
209            if unsigned {
210                Type::U24
211            } else {
212                Type::I24
213            }
214        }
215        qusql_parse::Type::Int(_) => {
216            if unsigned {
217                Type::U32
218            } else {
219                Type::I32
220            }
221        }
222        qusql_parse::Type::BigInt(_) => {
223            if unsigned {
224                Type::U64
225            } else {
226                Type::I64
227            }
228        }
229        qusql_parse::Type::Char(_) => BaseType::String.into(),
230        qusql_parse::Type::VarChar(_) => BaseType::String.into(),
231        qusql_parse::Type::TinyText(_) => BaseType::String.into(),
232        qusql_parse::Type::MediumText(_) => BaseType::String.into(),
233        qusql_parse::Type::Text(_) => BaseType::String.into(),
234        qusql_parse::Type::LongText(_) => BaseType::String.into(),
235        qusql_parse::Type::Enum(e) => {
236            Type::Enum(Arc::new(e.into_iter().map(|s| s.value).collect()))
237        }
238        qusql_parse::Type::Set(s) => Type::Set(Arc::new(s.into_iter().map(|s| s.value).collect())),
239        qusql_parse::Type::Float(_) => Type::F32,
240        qusql_parse::Type::Double(_) => Type::F64,
241        qusql_parse::Type::DateTime(_) => BaseType::DateTime.into(),
242        qusql_parse::Type::Timestamp(_) => BaseType::TimeStamp.into(),
243        qusql_parse::Type::Time(_) => BaseType::Time.into(),
244        qusql_parse::Type::TinyBlob(_) => BaseType::Bytes.into(),
245        qusql_parse::Type::MediumBlob(_) => BaseType::Bytes.into(),
246        qusql_parse::Type::Date => BaseType::Date.into(),
247        qusql_parse::Type::Blob(_) => BaseType::Bytes.into(),
248        qusql_parse::Type::LongBlob(_) => BaseType::Bytes.into(),
249        qusql_parse::Type::VarBinary(_) => BaseType::Bytes.into(),
250        qusql_parse::Type::Binary(_) => BaseType::Bytes.into(),
251        qusql_parse::Type::Boolean => BaseType::Bool.into(),
252        qusql_parse::Type::Integer(_) => {
253            if is_sqlite && primary_key {
254                auto_increment = true;
255            }
256            BaseType::Integer.into()
257        }
258        qusql_parse::Type::Float8 => BaseType::Float.into(),
259        qusql_parse::Type::Numeric(_) => todo!("Numeric"),
260        qusql_parse::Type::Decimal(_) => todo!("Decimal"),
261        qusql_parse::Type::Timestamptz => BaseType::TimeStamp.into(),
262        qusql_parse::Type::Json => BaseType::String.into(),
263        qusql_parse::Type::Jsonb => BaseType::String.into(),
264        qusql_parse::Type::Bit(_, _) => BaseType::Bytes.into(),
265        qusql_parse::Type::VarBit(_) => BaseType::Bytes.into(),
266        qusql_parse::Type::Bytea => BaseType::Bytes.into(),
267        qusql_parse::Type::Named(_) => BaseType::String.into(), // TODO lookup name??
268        qusql_parse::Type::Inet4 => BaseType::String.into(),
269        qusql_parse::Type::Inet6 => BaseType::String.into(),
270        qusql_parse::Type::InetAddr => BaseType::String.into(),
271        qusql_parse::Type::Cidr => BaseType::String.into(),
272        qusql_parse::Type::Macaddr => BaseType::String.into(),
273        qusql_parse::Type::Macaddr8 => BaseType::String.into(),
274        qusql_parse::Type::Array(_, _) => todo!("Array type not yet implemented"),
275        qusql_parse::Type::Table(_, _) => todo!("Table type not yet implemented"),
276        qusql_parse::Type::Serial
277        | qusql_parse::Type::SmallSerial
278        | qusql_parse::Type::BigSerial => BaseType::Integer.into(),
279        qusql_parse::Type::Money => BaseType::Float.into(),
280        qusql_parse::Type::Timetz(_) => BaseType::Time.into(),
281        qusql_parse::Type::Interval(_) => BaseType::TimeInterval.into(),
282        qusql_parse::Type::TsQuery => BaseType::String.into(),
283        qusql_parse::Type::TsVector => BaseType::String.into(),
284        qusql_parse::Type::Uuid => BaseType::String.into(),
285        qusql_parse::Type::Xml => BaseType::String.into(),
286        qusql_parse::Type::Range(_) => BaseType::Bytes.into(),
287        qusql_parse::Type::MultiRange(_) => BaseType::Bytes.into(),
288        qusql_parse::Type::Point
289        | qusql_parse::Type::Line
290        | qusql_parse::Type::Lseg
291        | qusql_parse::Type::Box
292        | qusql_parse::Type::Path
293        | qusql_parse::Type::Polygon
294        | qusql_parse::Type::Circle => BaseType::Bytes.into(),
295    };
296
297    Column {
298        identifier,
299        type_: FullType {
300            t: type_,
301            not_null,
302            list_hack: false,
303        },
304        auto_increment,
305        as_: _as,
306        default,
307        generated,
308    }
309}
310
311/// Parse a schema definition and return a terse description
312///
313/// Errors and warnings are added to issues. The schema is successfully
314/// parsed if no errors are added to issues.
315///
316/// The schema definition in srs should be a sequence of the following
317/// statements:
318/// - Drop table
319/// - Drop function
320/// - Drop view
321/// - Drop procedure
322/// - Create table
323/// - Create function
324/// - Create view
325/// - Create procedure
326/// - Alter table
327pub fn parse_schemas<'a>(
328    src: &'a str,
329    issues: &mut Issues<'a>,
330    options: &TypeOptions,
331) -> Schemas<'a> {
332    let statements = parse_statements(src, issues, &options.parse_options);
333
334    let mut schemas = Schemas {
335        schemas: Default::default(),
336        procedures: Default::default(),
337        functions: Default::default(),
338        indices: Default::default(),
339    };
340
341    for statement in statements {
342        match statement {
343            qusql_parse::Statement::CreateTable(t) => {
344                let mut replace = false;
345
346                let id = unqualified_name(issues, &t.identifier);
347
348                let mut schema = Schema {
349                    view: false,
350                    identifier_span: id.span.clone(),
351                    columns: Default::default(),
352                };
353
354                for o in t.create_options {
355                    match o {
356                        qusql_parse::CreateOption::OrReplace(_) => {
357                            replace = true;
358                        }
359                        qusql_parse::CreateOption::Temporary { temporary_span, .. } => {
360                            issues.err("Not supported", &temporary_span);
361                        }
362                        qusql_parse::CreateOption::Materialized(s) => {
363                            issues.err("Not supported", &s);
364                        }
365                        qusql_parse::CreateOption::Concurrently(s) => {
366                            issues.err("Not supported", &s);
367                        }
368                        qusql_parse::CreateOption::Unique(s) => {
369                            issues.err("Not supported", &s);
370                        }
371                        qusql_parse::CreateOption::Algorithm(_, _) => {}
372                        qusql_parse::CreateOption::Definer { .. } => {}
373                        qusql_parse::CreateOption::SqlSecurityDefiner(_, _) => {}
374                        qusql_parse::CreateOption::SqlSecurityUser(_, _) => {}
375                        qusql_parse::CreateOption::SqlSecurityInvoker(_, _) => {}
376                    }
377                }
378                // TODO: do we care about table options
379                for d in t.create_definitions {
380                    match d {
381                        qusql_parse::CreateDefinition::ColumnDefinition {
382                            identifier,
383                            data_type,
384                        } => {
385                            let column =
386                                parse_column(data_type, identifier.clone(), issues, Some(options));
387                            if let Some(oc) = schema.get_column(column.identifier.value) {
388                                issues
389                                    .err("Column already defined", &identifier)
390                                    .frag("Defined here", &oc.identifier);
391                            } else {
392                                schema.columns.push(column);
393                            }
394                        }
395                        qusql_parse::CreateDefinition::IndexDefinition { .. } => {}
396                        qusql_parse::CreateDefinition::ForeignKeyDefinition { .. } => {}
397                        qusql_parse::CreateDefinition::CheckConstraintDefinition { .. } => {}
398                    }
399                }
400                match schemas.schemas.entry(id.clone()) {
401                    alloc::collections::btree_map::Entry::Occupied(mut e) => {
402                        if replace {
403                            e.insert(schema);
404                        } else if t.if_not_exists.is_none() {
405                            issues
406                                .err("Table already defined", &t.identifier)
407                                .frag("Defined here", &e.get().identifier_span);
408                        }
409                    }
410                    alloc::collections::btree_map::Entry::Vacant(e) => {
411                        e.insert(schema);
412                    }
413                }
414            }
415            qusql_parse::Statement::CreateView(v) => {
416                let mut replace = false;
417                let mut schema = Schema {
418                    view: true,
419                    identifier_span: v.name.span(),
420                    columns: Default::default(),
421                };
422                for o in v.create_options {
423                    match o {
424                        qusql_parse::CreateOption::OrReplace(_) => {
425                            replace = true;
426                        }
427                        qusql_parse::CreateOption::Temporary { temporary_span, .. } => {
428                            issues.err("Not supported", &temporary_span);
429                        }
430                        qusql_parse::CreateOption::Materialized(s) => {
431                            issues.err("Not supported", &s);
432                        }
433                        qusql_parse::CreateOption::Concurrently(s) => {
434                            issues.err("Not supported", &s);
435                        }
436                        qusql_parse::CreateOption::Unique(s) => {
437                            issues.err("Not supported", &s);
438                        }
439                        qusql_parse::CreateOption::Algorithm(_, _) => {}
440                        qusql_parse::CreateOption::Definer { .. } => {}
441                        qusql_parse::CreateOption::SqlSecurityDefiner(_, _) => {}
442                        qusql_parse::CreateOption::SqlSecurityUser(_, _) => {}
443                        qusql_parse::CreateOption::SqlSecurityInvoker(_, _) => {}
444                    }
445                }
446
447                {
448                    let mut typer: crate::typer::Typer<'a, '_> = crate::typer::Typer {
449                        schemas: &schemas,
450                        issues,
451                        reference_types: Vec::new(),
452                        arg_types: Default::default(),
453                        options,
454                        with_schemas: Default::default(),
455                    };
456
457                    let t = type_statement::type_statement(&mut typer, &v.select);
458                    let s = if let type_statement::InnerStatementType::Select(s) = t {
459                        s
460                    } else {
461                        issues.err("Not supported", &v.select.span());
462                        continue;
463                    };
464
465                    for column in s.columns {
466                        //let column: crate::SelectTypeColumn<'a> = column;
467                        let name = column.name.unwrap();
468
469                        schema.columns.push(Column {
470                            identifier: name,
471                            type_: column.type_,
472                            auto_increment: false,
473                            default: false,
474                            as_: None,
475                            generated: false,
476                        });
477                    }
478                }
479
480                match schemas
481                    .schemas
482                    .entry(unqualified_name(issues, &v.name).clone())
483                {
484                    alloc::collections::btree_map::Entry::Occupied(mut e) => {
485                        if replace {
486                            e.insert(schema);
487                        } else if v.if_not_exists.is_none() {
488                            issues
489                                .err("View already defined", &v.name)
490                                .frag("Defined here", &e.get().identifier_span);
491                        }
492                    }
493                    alloc::collections::btree_map::Entry::Vacant(e) => {
494                        e.insert(schema);
495                    }
496                }
497            }
498            qusql_parse::Statement::CreateTrigger(_) => {}
499            // qusql_parse::Statement::CreateFunction(_) => todo!(),
500            // qusql_parse::Statement::Select(_) => todo!(),
501            // qusql_parse::Statement::Delete(_) => todo!(),
502            // qusql_parse::Statement::Insert(_) => todo!(),
503            // qusql_parse::Statement::Update(_) => todo!(),
504            qusql_parse::Statement::DropTable(t) => {
505                for i in t.tables {
506                    match schemas.schemas.entry(unqualified_name(issues, &i).clone()) {
507                        alloc::collections::btree_map::Entry::Occupied(e) => {
508                            if e.get().view {
509                                issues
510                                    .err("Name defines a view not a table", &i)
511                                    .frag("View defined here", &e.get().identifier_span);
512                            } else {
513                                e.remove();
514                            }
515                        }
516                        alloc::collections::btree_map::Entry::Vacant(_) => {
517                            if t.if_exists.is_none() {
518                                issues.err("A table with this name does not exist to drop", &i);
519                            }
520                        }
521                    }
522                }
523            }
524            qusql_parse::Statement::DropFunction(f) => {
525                for (func_name, _args) in &f.functions {
526                    match schemas
527                        .functions
528                        .entry(unqualified_name(issues, func_name).clone())
529                    {
530                        alloc::collections::btree_map::Entry::Occupied(e) => {
531                            e.remove();
532                        }
533                        alloc::collections::btree_map::Entry::Vacant(_) => {
534                            if f.if_exists.is_none() {
535                                issues.err(
536                                    "A function with this name does not exist to drop",
537                                    func_name,
538                                );
539                            }
540                        }
541                    }
542                }
543            }
544            qusql_parse::Statement::DropProcedure(p) => {
545                match schemas
546                    .procedures
547                    .entry(unqualified_name(issues, &p.procedure).clone())
548                {
549                    alloc::collections::btree_map::Entry::Occupied(e) => {
550                        e.remove();
551                    }
552                    alloc::collections::btree_map::Entry::Vacant(_) => {
553                        if p.if_exists.is_none() {
554                            issues.err(
555                                "A procedure with this name does not exist to drop",
556                                &p.procedure,
557                            );
558                        }
559                    }
560                }
561            }
562            //qusql_parse::Statement::DropEvent(_) => todo!(),
563            qusql_parse::Statement::DropDatabase(_) => {}
564            qusql_parse::Statement::DropServer(_) => {}
565            qusql_parse::Statement::DropTrigger(_) => {}
566            qusql_parse::Statement::DropView(v) => {
567                for i in v.views {
568                    match schemas.schemas.entry(unqualified_name(issues, &i).clone()) {
569                        alloc::collections::btree_map::Entry::Occupied(e) => {
570                            if !e.get().view {
571                                issues
572                                    .err("Name defines a table not a view", &i)
573                                    .frag("Table defined here", &e.get().identifier_span);
574                            } else {
575                                e.remove();
576                            }
577                        }
578                        alloc::collections::btree_map::Entry::Vacant(_) => {
579                            if v.if_exists.is_none() {
580                                issues.err("A view with this name does not exist to drop", &i);
581                            }
582                        }
583                    }
584                }
585            }
586            qusql_parse::Statement::Set(_) => {}
587            qusql_parse::Statement::AlterTable(a) => {
588                let e = match schemas
589                    .schemas
590                    .entry(unqualified_name(issues, &a.table).clone())
591                {
592                    alloc::collections::btree_map::Entry::Occupied(e) => {
593                        let e = e.into_mut();
594                        if e.view {
595                            issues.err("Cannot alter view", &a.table);
596                            continue;
597                        }
598                        e
599                    }
600                    alloc::collections::btree_map::Entry::Vacant(_) => {
601                        if a.if_exists.is_none() {
602                            issues.err("Table not found", &a.table);
603                        }
604                        continue;
605                    }
606                };
607                for s in a.alter_specifications {
608                    match s {
609                        qusql_parse::AlterSpecification::AddIndex(AddIndex {
610                            if_not_exists,
611                            name,
612                            cols,
613                            ..
614                        }) => {
615                            for col in &cols {
616                                // Only validate regular column names, skip functional index expressions
617                                if let qusql_parse::IndexColExpr::Column(name) = &col.expr
618                                    && e.get_column(name.value).is_none()
619                                {
620                                    issues
621                                        .err("No such column in table", col)
622                                        .frag("Table defined here", &a.table);
623                                }
624                            }
625
626                            if let Some(name) = &name {
627                                let ident = if options.parse_options.get_dialect().is_postgresql() {
628                                    IndexKey {
629                                        table: None,
630                                        index: name.clone(),
631                                    }
632                                } else {
633                                    IndexKey {
634                                        table: Some(unqualified_name(issues, &a.table).clone()),
635                                        index: name.clone(),
636                                    }
637                                };
638
639                                if let Some(old) = schemas.indices.insert(ident, name.span())
640                                    && if_not_exists.is_none()
641                                {
642                                    issues
643                                        .err(
644                                            "Multiple indeces with the same identifier",
645                                            &name.span(),
646                                        )
647                                        .frag("Already defined here", &old);
648                                }
649                            }
650                        }
651                        qusql_parse::AlterSpecification::AddForeignKey { .. } => {}
652                        qusql_parse::AlterSpecification::Modify(ModifyColumn {
653                            if_exists,
654                            col,
655                            definition,
656                            ..
657                        }) => {
658                            let c = match e.get_column_mut(col.value) {
659                                Some(v) => v,
660                                None => {
661                                    if if_exists.is_none() {
662                                        issues
663                                            .err("No such column in table", &col)
664                                            .frag("Table defined here", &e.identifier_span);
665                                    }
666                                    continue;
667                                }
668                            };
669                            *c = parse_column(
670                                definition,
671                                c.identifier.clone(),
672                                issues,
673                                Some(options),
674                            );
675                        }
676                        qusql_parse::AlterSpecification::AddColumn(AddColumn {
677                            identifier,
678                            data_type,
679                            ..
680                        }) => {
681                            e.columns.push(parse_column(
682                                data_type,
683                                identifier,
684                                issues,
685                                Some(options),
686                            ));
687                        }
688                        qusql_parse::AlterSpecification::OwnerTo { .. } => {}
689                        qusql_parse::AlterSpecification::DropColumn(DropColumn {
690                            column, ..
691                        }) => {
692                            let cnt = e.columns.len();
693                            e.columns.retain(|c| c.identifier != column);
694                            if cnt == e.columns.len() {
695                                issues
696                                    .err("No such column in table", &column)
697                                    .frag("Table defined here", &e.identifier_span);
698                            }
699                        }
700                        qusql_parse::AlterSpecification::AlterColumn(AlterColumn {
701                            column,
702                            alter_column_action,
703                            ..
704                        }) => {
705                            let c = match e.get_column_mut(column.value) {
706                                Some(v) => v,
707                                None => {
708                                    issues
709                                        .err("No such column in table", &column)
710                                        .frag("Table defined here", &e.identifier_span);
711                                    continue;
712                                }
713                            };
714                            match alter_column_action {
715                                qusql_parse::AlterColumnAction::SetDefault { .. } => (),
716                                qusql_parse::AlterColumnAction::DropDefault { .. } => (),
717                                qusql_parse::AlterColumnAction::Type { type_, .. } => {
718                                    *c = parse_column(type_, column, issues, Some(options))
719                                }
720                                qusql_parse::AlterColumnAction::SetNotNull { .. } => {
721                                    c.type_.not_null = true
722                                }
723                                qusql_parse::AlterColumnAction::DropNotNull { .. } => {
724                                    c.type_.not_null = false
725                                }
726                                a @ qusql_parse::AlterColumnAction::AddGenerated { .. } => {
727                                    issues.err("not implemented", &a);
728                                }
729                            }
730                        }
731                        s @ qusql_parse::AlterSpecification::Lock { .. } => {
732                            issues.err(
733                                alloc::format!("Unsupported statement {s:?} in schema definition"),
734                                &s,
735                            );
736                        }
737                        s @ qusql_parse::AlterSpecification::DropIndex { .. } => {
738                            issues.err(
739                                alloc::format!("Unsupported statement {s:?} in schema definition"),
740                                &s,
741                            );
742                        }
743                        s @ qusql_parse::AlterSpecification::DropForeignKey { .. } => {
744                            issues.err(
745                                alloc::format!("Unsupported statement {s:?} in schema definition"),
746                                &s,
747                            );
748                        }
749                        s @ qusql_parse::AlterSpecification::DropPrimaryKey { .. } => {
750                            issues.err(
751                                alloc::format!("Unsupported statement {s:?} in schema definition"),
752                                &s,
753                            );
754                        }
755                        s @ qusql_parse::AlterSpecification::RenameColumn { .. } => {
756                            issues.err(
757                                alloc::format!("Unsupported statement {s:?} in schema definition"),
758                                &s,
759                            );
760                        }
761                        s @ qusql_parse::AlterSpecification::RenameIndex { .. } => {
762                            issues.err(
763                                alloc::format!("Unsupported statement {s:?} in schema definition"),
764                                &s,
765                            );
766                        }
767                        s @ qusql_parse::AlterSpecification::RenameConstraint { .. } => {
768                            issues.err(
769                                alloc::format!("Unsupported statement {s:?} in schema definition"),
770                                &s,
771                            );
772                        }
773                        s @ qusql_parse::AlterSpecification::RenameTo { .. } => {
774                            issues.err(
775                                alloc::format!("Unsupported statement {s:?} in schema definition"),
776                                &s,
777                            );
778                        }
779                        s @ qusql_parse::AlterSpecification::Algorithm { .. } => {
780                            issues.err(
781                                alloc::format!("Unsupported statement {s:?} in schema definition"),
782                                &s,
783                            );
784                        }
785                        s @ qusql_parse::AlterSpecification::AutoIncrement { .. } => {
786                            issues.err(
787                                alloc::format!("Unsupported statement {s:?} in schema definition"),
788                                &s,
789                            );
790                        }
791                        s @ qusql_parse::AlterSpecification::Change { .. } => {
792                            issues.err(
793                                alloc::format!("Unsupported statement {s:?} in schema definition"),
794                                &s,
795                            );
796                        }
797                        s @ qusql_parse::AlterSpecification::ReplicaIdentity(_) => {
798                            issues.err("Not supported", &s);
799                        }
800                        s @ qusql_parse::AlterSpecification::ValidateConstraint(_) => {
801                            issues.err("Not supported", &s);
802                        }
803                        s @ qusql_parse::AlterSpecification::AddTableConstraint(_) => {
804                            issues.err("Not supported", &s);
805                        }
806                        s @ qusql_parse::AlterSpecification::DisableTrigger(_) => {
807                            issues.err("Not supported", &s);
808                        }
809                        s @ qusql_parse::AlterSpecification::EnableTrigger(_) => {
810                            issues.err("Not supported", &s);
811                        }
812                        s @ qusql_parse::AlterSpecification::DisableRule(_) => {
813                            issues.err("Not supported", &s);
814                        }
815                        s @ qusql_parse::AlterSpecification::EnableRule(_) => {
816                            issues.err("Not supported", &s);
817                        }
818                        s @ qusql_parse::AlterSpecification::DisableRowLevelSecurity(_) => {
819                            issues.err("Not supported", &s);
820                        }
821                        s @ qusql_parse::AlterSpecification::EnableRowLevelSecurity(_) => {
822                            issues.err("Not supported", &s);
823                        }
824                        s @ qusql_parse::AlterSpecification::ForceRowLevelSecurity(_) => {
825                            issues.err("Not supported", &s);
826                        }
827                        s @ qusql_parse::AlterSpecification::NoForceRowLevelSecurity(_) => {
828                            issues.err("Not supported", &s);
829                        }
830                    }
831                }
832            }
833            qusql_parse::Statement::Do(_) => {
834                //todo!()
835            }
836            // qusql_parse::Statement::Block(_) => todo!(),
837            // qusql_parse::Statement::If(_) => todo!(),
838            // qusql_parse::Statement::Invalid => todo!(),
839            // qusql_parse::Statement::Union(_) => todo!(),
840            // qusql_parse::Statement::Replace(_) => todo!(),
841            // qusql_parse::Statement::Case(_) => todo!(),
842            qusql_parse::Statement::CreateIndex(ci) => {
843                let t = unqualified_name(issues, &ci.table_name);
844
845                if let Some(table) = schemas.schemas.get(t) {
846                    for col in &ci.column_names {
847                        // Only validate regular column names, skip functional index expressions
848                        if let qusql_parse::IndexColExpr::Column(name) = &col.expr
849                            && table.get_column(name.value).is_none()
850                        {
851                            issues
852                                .err("No such column in table", col)
853                                .frag("Table defined here", &table.identifier_span);
854                        }
855                    }
856                    // TODO type where_
857                } else {
858                    issues.err("No such table", &ci.table_name);
859                }
860
861                // Skip unnamed indexes (PostgreSQL allows CREATE INDEX without a name)
862                let index_name = match &ci.index_name {
863                    Some(name) => name.clone(),
864                    None => continue,
865                };
866
867                let ident = if options.parse_options.get_dialect().is_postgresql() {
868                    IndexKey {
869                        table: None,
870                        index: index_name.clone(),
871                    }
872                } else {
873                    IndexKey {
874                        table: Some(t.clone()),
875                        index: index_name.clone(),
876                    }
877                };
878
879                if let Some(old) = schemas.indices.insert(ident, ci.span())
880                    && ci.if_not_exists.is_none()
881                {
882                    issues
883                        .err("Multiple indeces with the same identifier", &ci)
884                        .frag("Already defined here", &old);
885                }
886            }
887            qusql_parse::Statement::DropIndex(ci) => {
888                let key = IndexKey {
889                    table: ci.on.as_ref().map(|(_, t)| t.identifier.clone()),
890                    index: ci.index_name.clone(),
891                };
892                if schemas.indices.remove(&key).is_none() && ci.if_exists.is_none() {
893                    issues.err("No such index", &ci);
894                }
895            }
896            qusql_parse::Statement::Commit(_) => (),
897            qusql_parse::Statement::Begin(_) => (),
898            qusql_parse::Statement::CreateFunction(_) => (),
899            qusql_parse::Statement::CreateProcedure(_) => (),
900            qusql_parse::Statement::Call(_) => (),
901            qusql_parse::Statement::DeclareVariable(_) => (),
902            qusql_parse::Statement::DeclareCursorMariaDb(_) => (),
903            qusql_parse::Statement::DeclareHandler(_) => (),
904            qusql_parse::Statement::OpenCursor(_) => (),
905            qusql_parse::Statement::CloseCursor(_) => (),
906            qusql_parse::Statement::FetchCursor(_) => (),
907            qusql_parse::Statement::Leave(_) => (),
908            qusql_parse::Statement::Iterate(_) => (),
909            qusql_parse::Statement::Loop(_) => (),
910            qusql_parse::Statement::While(_) => (),
911            qusql_parse::Statement::Repeat(_) => (),
912            s => {
913                issues.err(
914                    alloc::format!("Unsupported statement {s:?} in schema definition"),
915                    &s,
916                );
917            }
918        }
919    }
920
921    let dummy_schemas = Schemas::default();
922
923    let mut typer = crate::typer::Typer {
924        schemas: &dummy_schemas,
925        issues,
926        reference_types: Vec::new(),
927        arg_types: Default::default(),
928        options,
929        with_schemas: Default::default(),
930    };
931
932    // Compute nullity of generated columns
933    for (name, schema) in &mut schemas.schemas {
934        if schema.columns.iter().all(|v| v.as_.is_none()) {
935            continue;
936        }
937        typer.reference_types.clear();
938        let mut columns = Vec::new();
939        for c in &schema.columns {
940            columns.push((c.identifier.clone(), c.type_.clone()));
941        }
942        typer.reference_types.push(crate::typer::ReferenceType {
943            name: Some(name.clone()),
944            span: schema.identifier_span.clone(),
945            columns,
946        });
947        for c in &mut schema.columns {
948            if let Some(as_) = &c.as_ {
949                let full_type = crate::type_expression::type_expression(
950                    &mut typer,
951                    as_,
952                    crate::type_expression::ExpressionFlags::default(),
953                    BaseType::Any,
954                );
955                c.type_.not_null = full_type.not_null;
956            }
957        }
958    }
959    schemas
960}