Skip to main content

pgevolve_core/parse/
mod.rs

1//! Source-side parser: SQL bytes → IR.
2//!
3//! This module accepts a directory of `CREATE`-style DDL files and produces a
4//! [`crate::ir::catalog::Catalog`]. Construction is I/O-free at the type level —
5//! the only I/O is performed by [`parse_directory`] on behalf of callers.
6
7pub mod ast_canon;
8mod ast_resolution;
9pub mod builder;
10pub mod cluster;
11pub mod directives;
12pub mod error;
13pub mod normalize_body;
14pub mod normalize_expr;
15pub mod statement;
16
17use std::collections::{BTreeMap, HashMap};
18use std::path::{Path, PathBuf};
19
20pub use directives::{FileDirectives, extract_file_directives};
21pub use error::{ParseError, SourceLocation};
22pub use statement::Statement;
23
24use crate::identifier::{Identifier, QualifiedName};
25use crate::ir::IrError;
26use crate::ir::catalog::Catalog;
27use crate::ir::publication::Publication;
28
29/// Parse every `*.sql` file under `root`, recursively, and produce a fully-
30/// populated [`Catalog`]. Files matching any pattern in `ignores` are skipped.
31///
32/// Walking is deterministic (paths are sorted before processing), and each
33/// statement classifies through the v0.1 whitelist; non-MVP DDL kinds raise
34/// [`ParseError::UnsupportedObjectKind`] with a phase-2 message.
35///
36/// After all files have been processed, the catalog is canonicalized (vec-
37/// sorted) and duplicate qnames raise [`ParseError::DuplicateObject`].
38pub fn parse_directory(root: &Path, ignores: &[glob::Pattern]) -> Result<Catalog, ParseError> {
39    parse_directory_with_locations(root, ignores).map(|(c, _)| c)
40}
41
42/// Like [`parse_directory`] but also returns the per-qname source-location
43/// map built during parsing. Used by the lint engine (Phase 10) to know which
44/// file each object was declared in.
45///
46/// The map keys are qname strings as rendered by `Display`: `"schema_name"`
47/// for schemas, `"schema.name"` for tables / indexes / sequences.
48pub fn parse_directory_with_locations(
49    root: &Path,
50    ignores: &[glob::Pattern],
51) -> Result<(Catalog, HashMap<String, SourceLocation>), ParseError> {
52    let mut files: Vec<PathBuf> = Vec::new();
53    for entry in walkdir::WalkDir::new(root).sort_by_file_name() {
54        let entry = entry.map_err(|e| ParseError::Io {
55            path: e.path().map(Path::to_path_buf).unwrap_or_default(),
56            source: e
57                .into_io_error()
58                .unwrap_or_else(|| std::io::Error::other("walkdir error")),
59        })?;
60        if !entry.file_type().is_file() {
61            continue;
62        }
63        let path = entry.into_path();
64        if path.extension().and_then(|e| e.to_str()) != Some("sql") {
65            continue;
66        }
67        if ignores.iter().any(|pat| pat.matches_path(&path)) {
68            continue;
69        }
70        files.push(path);
71    }
72
73    let mut catalog = Catalog::default();
74    let mut locations: HashMap<String, SourceLocation> = HashMap::new();
75    let mut pending_fks: Vec<builder::alter_table_stmt::PendingFk> = Vec::new();
76    let mut pending_column_attrs: Vec<builder::alter_table_stmt::PendingColumnAttr> = Vec::new();
77    let mut pending_owners: Vec<builder::alter_table_stmt::PendingOwner> = Vec::new();
78    let mut pending_rls_toggles: Vec<builder::alter_table_stmt::PendingRlsToggle> = Vec::new();
79    let mut pending_rel_options: Vec<builder::alter_table_stmt::PendingRelOptions> = Vec::new();
80    let mut deferred_comments: Vec<(
81        pg_query::protobuf::CommentStmt,
82        SourceLocation,
83        Option<crate::identifier::Identifier>,
84    )> = Vec::new();
85    // Publications are accumulated in insertion order (keyed by name) and
86    // folded: CREATE ... WITH (...) then subsequent ALTER ... ADD/DROP/SET
87    // all land in the same record.
88    let mut publications: BTreeMap<Identifier, Publication> = BTreeMap::new();
89
90    for path in files {
91        let contents = std::fs::read_to_string(&path).map_err(|e| ParseError::Io {
92            path: path.clone(),
93            source: e,
94        })?;
95        process_file(
96            &path,
97            &contents,
98            &mut catalog,
99            &mut locations,
100            &mut pending_fks,
101            &mut pending_column_attrs,
102            &mut pending_owners,
103            &mut pending_rls_toggles,
104            &mut pending_rel_options,
105            &mut deferred_comments,
106            &mut publications,
107        )?;
108    }
109
110    // Apply deferred comments (the underlying object may be defined in a later
111    // file).
112    for (stmt, location, default_schema) in deferred_comments {
113        builder::comment_stmt::apply_comment(
114            &stmt,
115            &mut catalog,
116            default_schema.as_ref(),
117            &location,
118        )?;
119    }
120
121    // Merge pending FKs and column-attribute updates from ALTER TABLE statements.
122    apply_pending_fks(&mut catalog, pending_fks)?;
123    apply_pending_column_attrs(&mut catalog, pending_column_attrs)?;
124    apply_pending_owners(&mut catalog, pending_owners)?;
125    apply_pending_rls_toggles(&mut catalog, pending_rls_toggles)?;
126    apply_pending_rel_options(&mut catalog, pending_rel_options)?;
127
128    // Flush the publications accumulator into the catalog.
129    catalog.publications = publications.into_values().collect();
130
131    // AST resolution pass: validate that all structural references (FKs,
132    // sequence defaults) resolve against the declared IR, before any DB touch.
133    ast_resolution::resolve(&catalog, &locations).map_err(ParseError::AstResolution)?;
134
135    // AST canonicalization pass: fill body_canonical, body_dependencies, and
136    // (when needed) columns for all views and materialized views. Skipped when
137    // the catalog has no views, so v0.1 fixtures pay no overhead.
138    if !catalog.views.is_empty() || !catalog.materialized_views.is_empty() {
139        ast_canon::canonicalize_view_bodies(&mut catalog).map_err(ParseError::AstCanon)?;
140    }
141
142    // MV index parent promotion: source-side `CREATE INDEX ON mv_name (...)` is
143    // initially parsed as `IndexParent::Table` because the parser doesn't know
144    // whether the relation is a table or an MV. Now that both the indexes and
145    // the MVs are in the catalog, promote any `IndexParent::Table(q)` where `q`
146    // is actually a materialized view.
147    ast_canon::promote_mv_index_parents(&mut catalog);
148
149    let canonical = catalog
150        .canonicalize()
151        .map_err(|e: IrError| translate_canonicalize_error(e, &locations))?;
152    Ok((canonical, locations))
153}
154
155/// Merge accumulated pending FKs onto their target tables.
156fn apply_pending_fks(
157    catalog: &mut Catalog,
158    pending_fks: Vec<builder::alter_table_stmt::PendingFk>,
159) -> Result<(), ParseError> {
160    for pending in pending_fks {
161        let table = catalog
162            .tables
163            .iter_mut()
164            .find(|t| t.qname == pending.target)
165            .ok_or_else(|| ParseError::Structural {
166                location: SourceLocation::new(PathBuf::new(), 0, 0),
167                message: format!("ALTER TABLE referenced unknown table {}", pending.target),
168            })?;
169        table.constraints.push(pending.constraint);
170    }
171    Ok(())
172}
173
174/// Apply accumulated `ALTER COLUMN SET STORAGE / SET COMPRESSION` updates.
175fn apply_pending_column_attrs(
176    catalog: &mut Catalog,
177    pending: Vec<builder::alter_table_stmt::PendingColumnAttr>,
178) -> Result<(), ParseError> {
179    use builder::alter_table_stmt::PendingColumnAttrKind;
180    for attr in pending {
181        let table = catalog
182            .tables
183            .iter_mut()
184            .find(|t| t.qname == attr.target)
185            .ok_or_else(|| ParseError::Structural {
186                location: SourceLocation::new(PathBuf::new(), 0, 0),
187                message: format!(
188                    "ALTER TABLE ALTER COLUMN referenced unknown table {}",
189                    attr.target
190                ),
191            })?;
192        let col = table
193            .columns
194            .iter_mut()
195            .find(|c| c.name == attr.column)
196            .ok_or_else(|| ParseError::Structural {
197                location: SourceLocation::new(PathBuf::new(), 0, 0),
198                message: format!(
199                    "ALTER TABLE ALTER COLUMN referenced unknown column {}.{}",
200                    attr.target, attr.column
201                ),
202            })?;
203        match attr.kind {
204            PendingColumnAttrKind::Storage(s) => {
205                col.storage = Some(s);
206            }
207            PendingColumnAttrKind::Compression(c) => {
208                col.compression = c;
209            }
210        }
211    }
212    Ok(())
213}
214
215/// Apply accumulated `ALTER TABLE/MATERIALIZED VIEW ... SET (...)` reloption
216/// updates to the catalog.
217///
218/// Called after all tables and materialized views are built.
219fn apply_pending_rel_options(
220    catalog: &mut Catalog,
221    pending: Vec<builder::alter_table_stmt::PendingRelOptions>,
222) -> Result<(), ParseError> {
223    let loc = SourceLocation::new(PathBuf::new(), 0, 0);
224    builder::alter_table_stmt::apply_pending_rel_options(catalog, pending, &loc)
225}
226
227/// Apply accumulated RLS mode toggles from ALTER TABLE statements.
228///
229/// Called after all tables are built so that the tables exist in the catalog.
230fn apply_pending_rls_toggles(
231    catalog: &mut Catalog,
232    pending: Vec<builder::alter_table_stmt::PendingRlsToggle>,
233) -> Result<(), ParseError> {
234    let loc = SourceLocation::new(PathBuf::new(), 0, 0);
235    builder::alter_table_stmt::apply_pending_rls_toggles(catalog, pending, &loc)
236}
237
238/// Apply accumulated `ALTER TABLE ... OWNER TO` ownership assignments.
239///
240/// Called after all tables, views, and materialized views are built.
241fn apply_pending_owners(
242    catalog: &mut Catalog,
243    pending: Vec<builder::alter_table_stmt::PendingOwner>,
244) -> Result<(), ParseError> {
245    let loc = SourceLocation::new(PathBuf::new(), 0, 0);
246    for po in pending {
247        builder::alter_table_stmt::apply_pending_owners(catalog, vec![po], &loc)?;
248    }
249    Ok(())
250}
251
252#[allow(clippy::too_many_lines, clippy::too_many_arguments)]
253fn process_file(
254    path: &Path,
255    contents: &str,
256    catalog: &mut Catalog,
257    locations: &mut HashMap<String, SourceLocation>,
258    pending_fks: &mut Vec<builder::alter_table_stmt::PendingFk>,
259    pending_column_attrs: &mut Vec<builder::alter_table_stmt::PendingColumnAttr>,
260    pending_owners: &mut Vec<builder::alter_table_stmt::PendingOwner>,
261    pending_rls_toggles: &mut Vec<builder::alter_table_stmt::PendingRlsToggle>,
262    pending_rel_options: &mut Vec<builder::alter_table_stmt::PendingRelOptions>,
263    deferred_comments: &mut Vec<(
264        pg_query::protobuf::CommentStmt,
265        SourceLocation,
266        Option<crate::identifier::Identifier>,
267    )>,
268    publications: &mut BTreeMap<Identifier, Publication>,
269) -> Result<(), ParseError> {
270    let directives = directives::extract_file_directives(contents, path)?;
271    let parsed = pg_query::parse(contents).map_err(|e| ParseError::PgQuery {
272        location: SourceLocation::new(path.to_path_buf(), 1, 1),
273        message: e.to_string(),
274    })?;
275
276    for raw in parsed.protobuf.stmts {
277        let location = stmt_location(path, contents, raw.stmt_location);
278        let Some(node) = raw.stmt.and_then(|n| n.node) else {
279            continue;
280        };
281        let stmt = Statement::classify(node, location.clone())?;
282        match stmt {
283            Statement::CreateSchema(s) => {
284                let schema = builder::create_schema_stmt::build_schema(&s, &location)?;
285                let schema_qname = QualifiedName::new(schema.name.clone(), schema.name.clone()); // schema has no parent; track by name
286                if let Some(prior) = locations.get(&schema.name.to_string()) {
287                    return Err(ParseError::DuplicateObject {
288                        qname: schema.name.to_string(),
289                        first: prior.clone(),
290                        second: location,
291                    });
292                }
293                locations.insert(schema.name.to_string(), location.clone());
294                catalog.schemas.push(schema);
295                let _ = schema_qname;
296            }
297            Statement::CreateTable(s) => {
298                let mut table =
299                    builder::create_stmt::build_table(&s, directives.schema.as_ref(), &location)?;
300                let serial_seqs =
301                    builder::desugar_serial::desugar_serials_in_table(&mut table, &location)?;
302                if let Some(prior) = locations.get(&table.qname.to_string()) {
303                    return Err(ParseError::DuplicateObject {
304                        qname: table.qname.to_string(),
305                        first: prior.clone(),
306                        second: location,
307                    });
308                }
309                locations.insert(table.qname.to_string(), location.clone());
310                catalog.tables.push(table);
311                for seq in serial_seqs {
312                    if let Some(prior) = locations.get(&seq.qname.to_string()) {
313                        return Err(ParseError::DuplicateObject {
314                            qname: seq.qname.to_string(),
315                            first: prior.clone(),
316                            second: location.clone(),
317                        });
318                    }
319                    locations.insert(seq.qname.to_string(), location.clone());
320                    catalog.sequences.push(seq);
321                }
322            }
323            Statement::CreateSequence(s) => {
324                let seq = builder::create_seq_stmt::build_sequence(
325                    &s,
326                    directives.schema.as_ref(),
327                    &location,
328                )?;
329                if let Some(prior) = locations.get(&seq.qname.to_string()) {
330                    return Err(ParseError::DuplicateObject {
331                        qname: seq.qname.to_string(),
332                        first: prior.clone(),
333                        second: location,
334                    });
335                }
336                locations.insert(seq.qname.to_string(), location.clone());
337                catalog.sequences.push(seq);
338            }
339            Statement::CreateIndex(s) => {
340                let idx =
341                    builder::index_stmt::build_index(&s, directives.schema.as_ref(), &location)?;
342                if let Some(prior) = locations.get(&idx.qname.to_string()) {
343                    return Err(ParseError::DuplicateObject {
344                        qname: idx.qname.to_string(),
345                        first: prior.clone(),
346                        second: location,
347                    });
348                }
349                locations.insert(idx.qname.to_string(), location.clone());
350                catalog.indexes.push(idx);
351            }
352            Statement::AlterTable(s) => {
353                let alter_out = builder::alter_table_stmt::build_alter_table(
354                    &s,
355                    directives.schema.as_ref(),
356                    &location,
357                )?;
358                pending_fks.extend(alter_out.pending_fks);
359                pending_column_attrs.extend(alter_out.pending_column_attrs);
360                pending_owners.extend(alter_out.pending_owners);
361                pending_rls_toggles.extend(alter_out.pending_rls_toggles);
362                pending_rel_options.extend(alter_out.pending_rel_options);
363            }
364            Statement::Comment(s) => {
365                deferred_comments.push((s, location, directives.schema.clone()));
366            }
367            Statement::CreateView(s) => {
368                let view = builder::create_view_stmt::build_view(
369                    &s,
370                    directives.schema.as_ref(),
371                    &location,
372                )?;
373                if let Some(prior) = locations.get(&view.qname.to_string()) {
374                    return Err(ParseError::DuplicateObject {
375                        qname: view.qname.to_string(),
376                        first: prior.clone(),
377                        second: location,
378                    });
379                }
380                locations.insert(view.qname.to_string(), location.clone());
381                catalog.views.push(view);
382            }
383            Statement::CreateMaterializedView(s) => {
384                let mv = builder::create_materialized_view_stmt::build_materialized_view(
385                    &s,
386                    directives.schema.as_ref(),
387                    &location,
388                )?;
389                if let Some(prior) = locations.get(&mv.qname.to_string()) {
390                    return Err(ParseError::DuplicateObject {
391                        qname: mv.qname.to_string(),
392                        first: prior.clone(),
393                        second: location,
394                    });
395                }
396                locations.insert(mv.qname.to_string(), location.clone());
397                catalog.materialized_views.push(mv);
398            }
399            Statement::CreateEnum(s) => {
400                let ut = builder::create_enum_stmt::build_enum(
401                    &s,
402                    directives.schema.as_ref(),
403                    &location,
404                )?;
405                if let Some(prior) = locations.get(&ut.qname.to_string()) {
406                    return Err(ParseError::DuplicateObject {
407                        qname: ut.qname.to_string(),
408                        first: prior.clone(),
409                        second: location,
410                    });
411                }
412                locations.insert(ut.qname.to_string(), location.clone());
413                catalog.types.push(ut);
414            }
415            Statement::CreateDomain(s) => {
416                let ut = builder::create_domain_stmt::build_domain(
417                    &s,
418                    directives.schema.as_ref(),
419                    &location,
420                )?;
421                if let Some(prior) = locations.get(&ut.qname.to_string()) {
422                    return Err(ParseError::DuplicateObject {
423                        qname: ut.qname.to_string(),
424                        first: prior.clone(),
425                        second: location,
426                    });
427                }
428                locations.insert(ut.qname.to_string(), location.clone());
429                catalog.types.push(ut);
430            }
431            Statement::CreateCompositeType(s) => {
432                let ut = builder::create_composite_type_stmt::build_composite(
433                    &s,
434                    directives.schema.as_ref(),
435                    &location,
436                )?;
437                if let Some(prior) = locations.get(&ut.qname.to_string()) {
438                    return Err(ParseError::DuplicateObject {
439                        qname: ut.qname.to_string(),
440                        first: prior.clone(),
441                        second: location,
442                    });
443                }
444                locations.insert(ut.qname.to_string(), location.clone());
445                catalog.types.push(ut);
446            }
447            Statement::CreateFunction(s) => {
448                let routine = builder::create_function_stmt::build_function_or_procedure(
449                    &s,
450                    directives.schema.as_ref(),
451                    &location,
452                )?;
453                let builder::create_function_stmt::Routine::Function(f) = routine else {
454                    return Err(ParseError::Structural {
455                        location,
456                        message: "internal error: expected Function from non-procedure stmt".into(),
457                    });
458                };
459                let arg_sig = f
460                    .args
461                    .iter()
462                    .filter(|a| {
463                        matches!(
464                            a.mode,
465                            crate::ir::function::ArgMode::In
466                                | crate::ir::function::ArgMode::InOut
467                                | crate::ir::function::ArgMode::Variadic
468                        )
469                    })
470                    .map(|a| a.ty.render_sql())
471                    .collect::<Vec<_>>()
472                    .join(",");
473                let key = format!("functions.{}({arg_sig})", f.qname);
474                if let Some(prior) = locations.get(&key) {
475                    return Err(ParseError::DuplicateObject {
476                        qname: key,
477                        first: prior.clone(),
478                        second: location,
479                    });
480                }
481                locations.insert(key, location.clone());
482                catalog.functions.push(f);
483            }
484            Statement::CreateProcedure(s) => {
485                let routine = builder::create_function_stmt::build_function_or_procedure(
486                    &s,
487                    directives.schema.as_ref(),
488                    &location,
489                )?;
490                let builder::create_function_stmt::Routine::Procedure(p) = routine else {
491                    return Err(ParseError::Structural {
492                        location,
493                        message: "internal error: expected Procedure from procedure stmt".into(),
494                    });
495                };
496                // Procedure identity is qname-only per arch Decision 2 — PG
497                // allows procedure overloading at the catalog level, but
498                // pgevolve v0.2 deliberately restricts procedures to a single
499                // signature per qname. Two procedures with the same qname
500                // (even with different arg types) collide.
501                let key = format!("procedures.{}", p.qname);
502                if let Some(prior) = locations.get(&key) {
503                    return Err(ParseError::DuplicateObject {
504                        qname: key,
505                        first: prior.clone(),
506                        second: location,
507                    });
508                }
509                locations.insert(key, location.clone());
510                catalog.procedures.push(p);
511            }
512            Statement::CreateExtension(s) => {
513                let ext = builder::create_extension_stmt::build_extension(&s, &location)?;
514                let key = format!("extensions.{}", ext.name);
515                if let Some(prior) = locations.get(&key) {
516                    return Err(ParseError::DuplicateObject {
517                        qname: key,
518                        first: prior.clone(),
519                        second: location,
520                    });
521                }
522                locations.insert(key, location.clone());
523                catalog.extensions.push(ext);
524            }
525            Statement::CreateTrigger(s) => {
526                let trigger = builder::create_trigger_stmt::build_trigger(&s, &location)?;
527                let key = format!("triggers.{}", trigger.qname);
528                if let Some(prior) = locations.get(&key) {
529                    return Err(ParseError::DuplicateObject {
530                        qname: key,
531                        first: prior.clone(),
532                        second: location,
533                    });
534                }
535                locations.insert(key, location.clone());
536                catalog.triggers.push(trigger);
537            }
538            Statement::AlterTableAttachPartition(s) => {
539                let attach = builder::alter_table_attach_partition::build_attach_partition(
540                    &s,
541                    directives.schema.as_ref(),
542                    &location,
543                )?;
544                let child_table = catalog
545                    .tables
546                    .iter_mut()
547                    .find(|t| t.qname == attach.child)
548                    .ok_or_else(|| ParseError::Structural {
549                        location: location.clone(),
550                        message: format!(
551                            "ATTACH PARTITION {} must follow its CREATE TABLE statement",
552                            attach.child
553                        ),
554                    })?;
555                if child_table.partition_of.is_some() {
556                    return Err(ParseError::Structural {
557                        location,
558                        message: format!("table {} already declared as a partition", attach.child),
559                    });
560                }
561                child_table.partition_of = Some(attach.partition_of);
562            }
563            Statement::Grant(s) => {
564                builder::grants::apply(&s, catalog, &location)?;
565            }
566            Statement::AlterOwner(s) => {
567                builder::owner_stmt::apply(&s, catalog, &location)?;
568            }
569            Statement::AlterDefaultPrivileges(s) => {
570                builder::default_privileges::apply(&s, catalog, &location)?;
571            }
572            Statement::CreatePolicy(s) => {
573                builder::policy_stmt::apply(&s, catalog, &location)?;
574            }
575            Statement::CreatePublication(s) => {
576                builder::publication_stmt::parse_create_publication(&s, location, publications)?;
577            }
578            Statement::AlterPublication(s) => {
579                builder::publication_stmt::parse_alter_publication(&s, location, publications)?;
580            }
581        }
582    }
583    Ok(())
584}
585
586/// Convert a `pg_query` byte offset into a 1-based line/column.
587fn stmt_location(path: &Path, contents: &str, byte_offset: i32) -> SourceLocation {
588    let offset = usize::try_from(byte_offset).unwrap_or(0);
589    let mut line = 1usize;
590    let mut col = 1usize;
591    for (i, c) in contents.char_indices() {
592        if i >= offset {
593            break;
594        }
595        if c == '\n' {
596            line += 1;
597            col = 1;
598        } else {
599            col += 1;
600        }
601    }
602    SourceLocation::new(path.to_path_buf(), line, col)
603}
604
605fn translate_canonicalize_error(
606    e: IrError,
607    locations: &HashMap<String, SourceLocation>,
608) -> ParseError {
609    if let IrError::InvalidIdentifier(msg) = &e {
610        // Format is "duplicate <kind>: <qname>" (see `Catalog::canonicalize`).
611        if let Some(rest) = msg.strip_prefix("duplicate ")
612            && let Some((_, qname)) = rest.split_once(": ")
613            && let Some(loc) = locations.get(qname)
614        {
615            return ParseError::DuplicateObject {
616                qname: qname.to_string(),
617                first: loc.clone(),
618                second: loc.clone(),
619            };
620        }
621    }
622    let placeholder = SourceLocation::new(PathBuf::new(), 0, 0);
623    ParseError::Ir {
624        location: placeholder,
625        source: e,
626    }
627}
628
629/// Smoke test: parse a single statement string with `pg_query`.
630#[cfg(test)]
631pub(crate) fn smoke_parse(sql: &str) -> Result<pg_query::ParseResult, pg_query::Error> {
632    pg_query::parse(sql)
633}
634
635#[cfg(test)]
636mod tests {
637    use super::*;
638
639    #[test]
640    fn pg_query_round_trips_a_create_table() {
641        let sql = "CREATE TABLE app.users (id integer);";
642        let result = smoke_parse(sql).expect("pg_query parses");
643        // Smoke check: the parse tree contains at least one statement.
644        assert!(!result.protobuf.stmts.is_empty());
645    }
646
647    #[test]
648    fn pg_query_reports_syntax_errors() {
649        let sql = "CREATE TABLE !bad!;";
650        assert!(smoke_parse(sql).is_err());
651    }
652}