1pub 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::aggregate::Aggregate;
27use crate::ir::cast::Cast;
28use crate::ir::catalog::Catalog;
29use crate::ir::event_trigger::EventTrigger;
30use crate::ir::publication::Publication;
31use crate::ir::statistic::Statistic;
32use crate::ir::subscription::Subscription;
33
34pub fn parse_directory(root: &Path, ignores: &[glob::Pattern]) -> Result<Catalog, ParseError> {
44 parse_directory_with_locations(root, ignores).map(|(c, _)| c)
45}
46
47pub fn parse_directory_with_locations(
54 root: &Path,
55 ignores: &[glob::Pattern],
56) -> Result<(Catalog, HashMap<String, SourceLocation>), ParseError> {
57 let mut files: Vec<PathBuf> = Vec::new();
58 for entry in walkdir::WalkDir::new(root).sort_by_file_name() {
59 let entry = entry.map_err(|e| ParseError::Io {
60 path: e.path().map(Path::to_path_buf).unwrap_or_default(),
61 source: e
62 .into_io_error()
63 .unwrap_or_else(|| std::io::Error::other("walkdir error")),
64 })?;
65 if !entry.file_type().is_file() {
66 continue;
67 }
68 let path = entry.into_path();
69 if path.extension().and_then(|e| e.to_str()) != Some("sql") {
70 continue;
71 }
72 if ignores.iter().any(|pat| pat.matches_path(&path)) {
73 continue;
74 }
75 files.push(path);
76 }
77
78 let mut catalog = Catalog::default();
79 let mut locations: HashMap<String, SourceLocation> = HashMap::new();
80 let mut pending_fks: Vec<builder::alter_table_stmt::PendingFk> = Vec::new();
81 let mut pending_column_attrs: Vec<builder::alter_table_stmt::PendingColumnAttr> = Vec::new();
82 let mut pending_owners: Vec<builder::alter_table_stmt::PendingOwner> = Vec::new();
83 let mut pending_rls_toggles: Vec<builder::alter_table_stmt::PendingRlsToggle> = Vec::new();
84 let mut pending_rel_options: Vec<builder::alter_table_stmt::PendingRelOptions> = Vec::new();
85 let mut deferred_comments: Vec<(
86 pg_query::protobuf::CommentStmt,
87 SourceLocation,
88 Option<crate::identifier::Identifier>,
89 )> = Vec::new();
90 let mut publications: BTreeMap<Identifier, Publication> = BTreeMap::new();
94 let mut subscriptions: BTreeMap<Identifier, Subscription> = BTreeMap::new();
96 let mut statistics: BTreeMap<QualifiedName, Statistic> = BTreeMap::new();
98 let mut event_triggers: BTreeMap<Identifier, EventTrigger> = BTreeMap::new();
101 let mut aggregates: Vec<Aggregate> = Vec::new();
105 let mut casts: Vec<Cast> = Vec::new();
108
109 for path in files {
110 let contents = std::fs::read_to_string(&path).map_err(|e| ParseError::Io {
111 path: path.clone(),
112 source: e,
113 })?;
114 process_file(
115 &path,
116 &contents,
117 &mut catalog,
118 &mut locations,
119 &mut pending_fks,
120 &mut pending_column_attrs,
121 &mut pending_owners,
122 &mut pending_rls_toggles,
123 &mut pending_rel_options,
124 &mut deferred_comments,
125 &mut publications,
126 &mut subscriptions,
127 &mut statistics,
128 &mut event_triggers,
129 &mut aggregates,
130 &mut casts,
131 )?;
132 }
133
134 for (stmt, location, default_schema) in deferred_comments {
137 builder::comment_stmt::apply_comment(
138 &stmt,
139 &mut catalog,
140 default_schema.as_ref(),
141 &location,
142 )?;
143 }
144
145 apply_pending_fks(&mut catalog, pending_fks)?;
147 apply_pending_column_attrs(&mut catalog, pending_column_attrs)?;
148 apply_pending_owners(&mut catalog, pending_owners)?;
149 apply_pending_rls_toggles(&mut catalog, pending_rls_toggles)?;
150 apply_pending_rel_options(&mut catalog, pending_rel_options)?;
151
152 catalog.publications = publications.into_values().collect();
154
155 catalog.subscriptions = subscriptions.into_values().collect();
157
158 catalog.statistics = statistics.into_values().collect();
160
161 catalog.event_triggers = event_triggers.into_values().collect();
163
164 catalog.aggregates = aggregates;
166
167 catalog.casts = casts;
169
170 ast_resolution::resolve(&catalog, &locations).map_err(ParseError::AstResolution)?;
173
174 if !catalog.views.is_empty() || !catalog.materialized_views.is_empty() {
178 ast_canon::canonicalize_view_bodies(&mut catalog).map_err(ParseError::AstCanon)?;
179 }
180
181 ast_canon::promote_mv_index_parents(&mut catalog);
187
188 let canonical = catalog
189 .canonicalize()
190 .map_err(|e: IrError| translate_canonicalize_error(e, &locations))?;
191 Ok((canonical, locations))
192}
193
194fn apply_statistics_comment(
200 stmt: &pg_query::protobuf::CommentStmt,
201 statistics: &mut BTreeMap<QualifiedName, Statistic>,
202 location: &SourceLocation,
203) -> Result<(), ParseError> {
204 use pg_query::NodeEnum;
205
206 let obj = stmt
208 .object
209 .as_ref()
210 .and_then(|o| o.node.as_ref())
211 .ok_or_else(|| ParseError::Structural {
212 location: location.clone(),
213 message: "COMMENT ON STATISTICS missing object reference".into(),
214 })?;
215 let NodeEnum::List(list) = obj else {
216 return Err(ParseError::Structural {
217 location: location.clone(),
218 message: format!(
219 "COMMENT ON STATISTICS expected a List node, got {:?}",
220 std::mem::discriminant(obj)
221 ),
222 });
223 };
224 let parts: Vec<String> = list
225 .items
226 .iter()
227 .filter_map(|n| {
228 if let Some(NodeEnum::String(s)) = n.node.as_ref() {
229 Some(s.sval.clone())
230 } else {
231 None
232 }
233 })
234 .collect();
235 let qname = match parts.as_slice() {
236 [schema, name] => QualifiedName::new(
237 builder::shared::ident(schema, location)?,
238 builder::shared::ident(name, location)?,
239 ),
240 [_name] => {
241 return Err(ParseError::UnqualifiedName {
242 location: location.clone(),
243 });
244 }
245 _ => {
246 return Err(ParseError::Structural {
247 location: location.clone(),
248 message: format!(
249 "COMMENT ON STATISTICS expected 1-2 qualified components, got {parts:?}"
250 ),
251 });
252 }
253 };
254
255 let comment = if stmt.comment.is_empty() {
256 None
257 } else {
258 Some(stmt.comment.clone())
259 };
260
261 let statistic = statistics.get_mut(&qname).ok_or_else(|| {
262 ParseError::CommentOnStatisticBeforeCreate(qname.clone(), location.clone())
263 })?;
264 statistic.comment = comment;
265 Ok(())
266}
267
268fn apply_pending_fks(
270 catalog: &mut Catalog,
271 pending_fks: Vec<builder::alter_table_stmt::PendingFk>,
272) -> Result<(), ParseError> {
273 for pending in pending_fks {
274 let table = catalog
275 .tables
276 .iter_mut()
277 .find(|t| t.qname == pending.target)
278 .ok_or_else(|| ParseError::Structural {
279 location: SourceLocation::new(PathBuf::new(), 0, 0),
280 message: format!("ALTER TABLE referenced unknown table {}", pending.target),
281 })?;
282 table.constraints.push(pending.constraint);
283 }
284 Ok(())
285}
286
287fn apply_pending_column_attrs(
289 catalog: &mut Catalog,
290 pending: Vec<builder::alter_table_stmt::PendingColumnAttr>,
291) -> Result<(), ParseError> {
292 use builder::alter_table_stmt::PendingColumnAttrKind;
293 for attr in pending {
294 let table = catalog
295 .tables
296 .iter_mut()
297 .find(|t| t.qname == attr.target)
298 .ok_or_else(|| ParseError::Structural {
299 location: SourceLocation::new(PathBuf::new(), 0, 0),
300 message: format!(
301 "ALTER TABLE ALTER COLUMN referenced unknown table {}",
302 attr.target
303 ),
304 })?;
305 let col = table
306 .columns
307 .iter_mut()
308 .find(|c| c.name == attr.column)
309 .ok_or_else(|| ParseError::Structural {
310 location: SourceLocation::new(PathBuf::new(), 0, 0),
311 message: format!(
312 "ALTER TABLE ALTER COLUMN referenced unknown column {}.{}",
313 attr.target, attr.column
314 ),
315 })?;
316 match attr.kind {
317 PendingColumnAttrKind::Storage(s) => {
318 col.storage = Some(s);
319 }
320 PendingColumnAttrKind::Compression(c) => {
321 col.compression = c;
322 }
323 }
324 }
325 Ok(())
326}
327
328fn apply_pending_rel_options(
333 catalog: &mut Catalog,
334 pending: Vec<builder::alter_table_stmt::PendingRelOptions>,
335) -> Result<(), ParseError> {
336 let loc = SourceLocation::new(PathBuf::new(), 0, 0);
337 builder::alter_table_stmt::apply_pending_rel_options(catalog, pending, &loc)
338}
339
340fn apply_pending_rls_toggles(
344 catalog: &mut Catalog,
345 pending: Vec<builder::alter_table_stmt::PendingRlsToggle>,
346) -> Result<(), ParseError> {
347 let loc = SourceLocation::new(PathBuf::new(), 0, 0);
348 builder::alter_table_stmt::apply_pending_rls_toggles(catalog, pending, &loc)
349}
350
351fn apply_pending_owners(
355 catalog: &mut Catalog,
356 pending: Vec<builder::alter_table_stmt::PendingOwner>,
357) -> Result<(), ParseError> {
358 let loc = SourceLocation::new(PathBuf::new(), 0, 0);
359 for po in pending {
360 builder::alter_table_stmt::apply_pending_owners(catalog, vec![po], &loc)?;
361 }
362 Ok(())
363}
364
365#[allow(clippy::too_many_lines, clippy::too_many_arguments)]
366fn process_file(
367 path: &Path,
368 contents: &str,
369 catalog: &mut Catalog,
370 locations: &mut HashMap<String, SourceLocation>,
371 pending_fks: &mut Vec<builder::alter_table_stmt::PendingFk>,
372 pending_column_attrs: &mut Vec<builder::alter_table_stmt::PendingColumnAttr>,
373 pending_owners: &mut Vec<builder::alter_table_stmt::PendingOwner>,
374 pending_rls_toggles: &mut Vec<builder::alter_table_stmt::PendingRlsToggle>,
375 pending_rel_options: &mut Vec<builder::alter_table_stmt::PendingRelOptions>,
376 deferred_comments: &mut Vec<(
377 pg_query::protobuf::CommentStmt,
378 SourceLocation,
379 Option<crate::identifier::Identifier>,
380 )>,
381 publications: &mut BTreeMap<Identifier, Publication>,
382 subscriptions: &mut BTreeMap<Identifier, Subscription>,
383 statistics: &mut BTreeMap<QualifiedName, Statistic>,
384 event_triggers: &mut BTreeMap<Identifier, EventTrigger>,
385 aggregates: &mut Vec<Aggregate>,
386 casts: &mut Vec<Cast>,
387) -> Result<(), ParseError> {
388 let directives = directives::extract_file_directives(contents, path)?;
389 let parsed = pg_query::parse(contents).map_err(|e| ParseError::PgQuery {
390 location: SourceLocation::new(path.to_path_buf(), 1, 1),
391 message: e.to_string(),
392 })?;
393
394 for raw in parsed.protobuf.stmts {
395 let location = stmt_location(path, contents, raw.stmt_location);
396 let Some(node) = raw.stmt.and_then(|n| n.node) else {
397 continue;
398 };
399 let stmt = Statement::classify(node, location.clone())?;
400 match stmt {
401 Statement::CreateSchema(s) => {
402 let schema = builder::create_schema_stmt::build_schema(&s, &location)?;
403 let schema_qname = QualifiedName::new(schema.name.clone(), schema.name.clone()); if let Some(prior) = locations.get(&schema.name.to_string()) {
405 return Err(ParseError::DuplicateObject {
406 qname: schema.name.to_string(),
407 first: prior.clone(),
408 second: location,
409 });
410 }
411 locations.insert(schema.name.to_string(), location.clone());
412 catalog.schemas.push(schema);
413 let _ = schema_qname;
414 }
415 Statement::CreateTable(s) => {
416 let mut table =
417 builder::create_stmt::build_table(&s, directives.schema.as_ref(), &location)?;
418 let serial_seqs =
419 builder::desugar_serial::desugar_serials_in_table(&mut table, &location)?;
420 if let Some(prior) = locations.get(&table.qname.to_string()) {
421 return Err(ParseError::DuplicateObject {
422 qname: table.qname.to_string(),
423 first: prior.clone(),
424 second: location,
425 });
426 }
427 locations.insert(table.qname.to_string(), location.clone());
428 catalog.tables.push(table);
429 for seq in serial_seqs {
430 if let Some(prior) = locations.get(&seq.qname.to_string()) {
431 return Err(ParseError::DuplicateObject {
432 qname: seq.qname.to_string(),
433 first: prior.clone(),
434 second: location.clone(),
435 });
436 }
437 locations.insert(seq.qname.to_string(), location.clone());
438 catalog.sequences.push(seq);
439 }
440 }
441 Statement::CreateSequence(s) => {
442 let seq = builder::create_seq_stmt::build_sequence(
443 &s,
444 directives.schema.as_ref(),
445 &location,
446 )?;
447 if let Some(prior) = locations.get(&seq.qname.to_string()) {
448 return Err(ParseError::DuplicateObject {
449 qname: seq.qname.to_string(),
450 first: prior.clone(),
451 second: location,
452 });
453 }
454 locations.insert(seq.qname.to_string(), location.clone());
455 catalog.sequences.push(seq);
456 }
457 Statement::CreateIndex(s) => {
458 let idx =
459 builder::index_stmt::build_index(&s, directives.schema.as_ref(), &location)?;
460 if let Some(prior) = locations.get(&idx.qname.to_string()) {
461 return Err(ParseError::DuplicateObject {
462 qname: idx.qname.to_string(),
463 first: prior.clone(),
464 second: location,
465 });
466 }
467 locations.insert(idx.qname.to_string(), location.clone());
468 catalog.indexes.push(idx);
469 }
470 Statement::AlterTable(s) => {
471 let alter_out = builder::alter_table_stmt::build_alter_table(
472 &s,
473 directives.schema.as_ref(),
474 &location,
475 )?;
476 pending_fks.extend(alter_out.pending_fks);
477 pending_column_attrs.extend(alter_out.pending_column_attrs);
478 pending_owners.extend(alter_out.pending_owners);
479 pending_rls_toggles.extend(alter_out.pending_rls_toggles);
480 pending_rel_options.extend(alter_out.pending_rel_options);
481 }
482 Statement::Comment(s) => {
483 use pg_query::protobuf::ObjectType;
484 let kind = ObjectType::try_from(s.objtype).unwrap_or(ObjectType::Undefined);
485 if matches!(kind, ObjectType::ObjectStatisticExt) {
486 apply_statistics_comment(&s, statistics, &location)?;
490 } else if matches!(kind, ObjectType::ObjectEventTrigger) {
491 builder::event_trigger_stmt::apply_event_trigger_comment(
494 &s,
495 &location,
496 event_triggers,
497 )?;
498 } else if matches!(kind, ObjectType::ObjectAggregate) {
499 builder::aggregate_stmt::apply_comment(
502 &s,
503 directives.schema.as_ref(),
504 &location,
505 aggregates,
506 )?;
507 } else if matches!(kind, ObjectType::ObjectCast) {
508 builder::cast_stmt::apply_comment(
511 &s,
512 directives.schema.as_ref(),
513 &location,
514 casts,
515 )?;
516 } else {
517 deferred_comments.push((s, location, directives.schema.clone()));
518 }
519 }
520 Statement::CreateView(s) => {
521 let view = builder::create_view_stmt::build_view(
522 &s,
523 directives.schema.as_ref(),
524 &location,
525 )?;
526 if let Some(prior) = locations.get(&view.qname.to_string()) {
527 return Err(ParseError::DuplicateObject {
528 qname: view.qname.to_string(),
529 first: prior.clone(),
530 second: location,
531 });
532 }
533 locations.insert(view.qname.to_string(), location.clone());
534 catalog.views.push(view);
535 }
536 Statement::CreateMaterializedView(s) => {
537 let mv = builder::create_materialized_view_stmt::build_materialized_view(
538 &s,
539 directives.schema.as_ref(),
540 &location,
541 )?;
542 if let Some(prior) = locations.get(&mv.qname.to_string()) {
543 return Err(ParseError::DuplicateObject {
544 qname: mv.qname.to_string(),
545 first: prior.clone(),
546 second: location,
547 });
548 }
549 locations.insert(mv.qname.to_string(), location.clone());
550 catalog.materialized_views.push(mv);
551 }
552 Statement::CreateEnum(s) => {
553 let ut = builder::create_enum_stmt::build_enum(
554 &s,
555 directives.schema.as_ref(),
556 &location,
557 )?;
558 if let Some(prior) = locations.get(&ut.qname.to_string()) {
559 return Err(ParseError::DuplicateObject {
560 qname: ut.qname.to_string(),
561 first: prior.clone(),
562 second: location,
563 });
564 }
565 locations.insert(ut.qname.to_string(), location.clone());
566 catalog.types.push(ut);
567 }
568 Statement::CreateDomain(s) => {
569 let ut = builder::create_domain_stmt::build_domain(
570 &s,
571 directives.schema.as_ref(),
572 &location,
573 )?;
574 if let Some(prior) = locations.get(&ut.qname.to_string()) {
575 return Err(ParseError::DuplicateObject {
576 qname: ut.qname.to_string(),
577 first: prior.clone(),
578 second: location,
579 });
580 }
581 locations.insert(ut.qname.to_string(), location.clone());
582 catalog.types.push(ut);
583 }
584 Statement::CreateCompositeType(s) => {
585 let ut = builder::create_composite_type_stmt::build_composite(
586 &s,
587 directives.schema.as_ref(),
588 &location,
589 )?;
590 if let Some(prior) = locations.get(&ut.qname.to_string()) {
591 return Err(ParseError::DuplicateObject {
592 qname: ut.qname.to_string(),
593 first: prior.clone(),
594 second: location,
595 });
596 }
597 locations.insert(ut.qname.to_string(), location.clone());
598 catalog.types.push(ut);
599 }
600 Statement::CreateRange(s) => {
601 let ut = builder::create_range_stmt::build_range(
602 &s,
603 directives.schema.as_ref(),
604 &location,
605 )?;
606 if let Some(prior) = locations.get(&ut.qname.to_string()) {
607 return Err(ParseError::DuplicateObject {
608 qname: ut.qname.to_string(),
609 first: prior.clone(),
610 second: location,
611 });
612 }
613 locations.insert(ut.qname.to_string(), location.clone());
614 catalog.types.push(ut);
615 }
616 Statement::CreateFunction(s) => {
617 let routine = builder::create_function_stmt::build_function_or_procedure(
618 &s,
619 directives.schema.as_ref(),
620 &location,
621 )?;
622 let builder::create_function_stmt::Routine::Function(f) = routine else {
623 return Err(ParseError::Structural {
624 location,
625 message: "internal error: expected Function from non-procedure stmt".into(),
626 });
627 };
628 let arg_sig = f
629 .args
630 .iter()
631 .filter(|a| {
632 matches!(
633 a.mode,
634 crate::ir::function::ArgMode::In
635 | crate::ir::function::ArgMode::InOut
636 | crate::ir::function::ArgMode::Variadic
637 )
638 })
639 .map(|a| a.ty.render_sql())
640 .collect::<Vec<_>>()
641 .join(",");
642 let key = format!("functions.{}({arg_sig})", f.qname);
643 if let Some(prior) = locations.get(&key) {
644 return Err(ParseError::DuplicateObject {
645 qname: key,
646 first: prior.clone(),
647 second: location,
648 });
649 }
650 locations.insert(key, location.clone());
651 catalog.functions.push(f);
652 }
653 Statement::CreateProcedure(s) => {
654 let routine = builder::create_function_stmt::build_function_or_procedure(
655 &s,
656 directives.schema.as_ref(),
657 &location,
658 )?;
659 let builder::create_function_stmt::Routine::Procedure(p) = routine else {
660 return Err(ParseError::Structural {
661 location,
662 message: "internal error: expected Procedure from procedure stmt".into(),
663 });
664 };
665 let key = format!("procedures.{}", p.qname);
671 if let Some(prior) = locations.get(&key) {
672 return Err(ParseError::DuplicateObject {
673 qname: key,
674 first: prior.clone(),
675 second: location,
676 });
677 }
678 locations.insert(key, location.clone());
679 catalog.procedures.push(p);
680 }
681 Statement::CreateExtension(s) => {
682 let ext = builder::create_extension_stmt::build_extension(&s, &location)?;
683 let key = format!("extensions.{}", ext.name);
684 if let Some(prior) = locations.get(&key) {
685 return Err(ParseError::DuplicateObject {
686 qname: key,
687 first: prior.clone(),
688 second: location,
689 });
690 }
691 locations.insert(key, location.clone());
692 catalog.extensions.push(ext);
693 }
694 Statement::CreateTrigger(s) => {
695 let trigger = builder::create_trigger_stmt::build_trigger(&s, &location)?;
696 let key = format!("triggers.{}", trigger.qname);
697 if let Some(prior) = locations.get(&key) {
698 return Err(ParseError::DuplicateObject {
699 qname: key,
700 first: prior.clone(),
701 second: location,
702 });
703 }
704 locations.insert(key, location.clone());
705 catalog.triggers.push(trigger);
706 }
707 Statement::AlterTableAttachPartition(s) => {
708 let attach = builder::alter_table_attach_partition::build_attach_partition(
709 &s,
710 directives.schema.as_ref(),
711 &location,
712 )?;
713 let child_table = catalog
714 .tables
715 .iter_mut()
716 .find(|t| t.qname == attach.child)
717 .ok_or_else(|| ParseError::Structural {
718 location: location.clone(),
719 message: format!(
720 "ATTACH PARTITION {} must follow its CREATE TABLE statement",
721 attach.child
722 ),
723 })?;
724 if child_table.partition_of.is_some() {
725 return Err(ParseError::Structural {
726 location,
727 message: format!("table {} already declared as a partition", attach.child),
728 });
729 }
730 child_table.partition_of = Some(attach.partition_of);
731 }
732 Statement::Grant(s) => {
733 builder::grants::apply(&s, catalog, &location)?;
734 }
735 Statement::AlterOwner(s) => {
736 use pg_query::protobuf::ObjectType;
737 let objtype = ObjectType::try_from(s.object_type).unwrap_or(ObjectType::Undefined);
738 if matches!(objtype, ObjectType::ObjectEventTrigger) {
739 builder::event_trigger_stmt::apply_event_trigger_owner(
743 &s,
744 &location,
745 event_triggers,
746 )?;
747 } else if matches!(objtype, ObjectType::ObjectAggregate) {
748 builder::aggregate_stmt::apply_owner(
751 &s,
752 directives.schema.as_ref(),
753 &location,
754 aggregates,
755 )?;
756 } else {
757 builder::owner_stmt::apply(&s, catalog, &location)?;
758 }
759 }
760 Statement::AlterDefaultPrivileges(s) => {
761 builder::default_privileges::apply(&s, catalog, &location)?;
762 }
763 Statement::CreatePolicy(s) => {
764 builder::policy_stmt::apply(&s, catalog, &location)?;
765 }
766 Statement::CreatePublication(s) => {
767 builder::publication_stmt::parse_create_publication(&s, location, publications)?;
768 }
769 Statement::AlterPublication(s) => {
770 builder::publication_stmt::parse_alter_publication(&s, location, publications)?;
771 }
772 Statement::CreateSubscription(s) => {
773 builder::subscription_stmt::parse_create_subscription(&s, location, subscriptions)?;
774 }
775 Statement::AlterSubscription(s) => {
776 builder::subscription_stmt::parse_alter_subscription(&s, location, subscriptions)?;
777 }
778 Statement::CreateStatistics(s) => {
779 builder::statistic_stmt::parse_create_statistics(&s, location, statistics)?;
780 }
781 Statement::AlterStatistics(s) => {
782 builder::statistic_stmt::parse_alter_statistics(&s, &location, statistics)?;
783 }
784 Statement::CreateCollation(s) => {
785 let coll = builder::create_collation_stmt::build_collation(
786 &s,
787 directives.schema.as_ref(),
788 &location,
789 )?;
790 if let Some(prior) = locations.get(&coll.qname.to_string()) {
791 return Err(ParseError::DuplicateObject {
792 qname: coll.qname.to_string(),
793 first: prior.clone(),
794 second: location,
795 });
796 }
797 locations.insert(coll.qname.to_string(), location.clone());
798 catalog.collations.push(coll);
799 }
800 Statement::CreateAggregate(s) => {
801 builder::aggregate_stmt::parse_create(
802 &s,
803 directives.schema.as_ref(),
804 &location,
805 aggregates,
806 )?;
807 }
808 Statement::CreateEventTrigger(s) => {
809 builder::event_trigger_stmt::parse_create_event_trigger(
810 &s,
811 directives.schema.as_ref(),
812 location,
813 event_triggers,
814 )?;
815 }
816 Statement::AlterEventTrigger(s) => {
817 builder::event_trigger_stmt::parse_alter_event_trigger(
818 &s,
819 location,
820 event_triggers,
821 )?;
822 }
823 Statement::CreateCast(s) => {
824 builder::cast_stmt::parse_create(&s, directives.schema.as_ref(), &location, casts)?;
825 }
826 }
827 }
828 Ok(())
829}
830
831fn stmt_location(path: &Path, contents: &str, byte_offset: i32) -> SourceLocation {
833 let offset = usize::try_from(byte_offset).unwrap_or(0);
834 let mut line = 1usize;
835 let mut col = 1usize;
836 for (i, c) in contents.char_indices() {
837 if i >= offset {
838 break;
839 }
840 if c == '\n' {
841 line += 1;
842 col = 1;
843 } else {
844 col += 1;
845 }
846 }
847 SourceLocation::new(path.to_path_buf(), line, col)
848}
849
850fn translate_canonicalize_error(
851 e: IrError,
852 locations: &HashMap<String, SourceLocation>,
853) -> ParseError {
854 if let IrError::InvalidIdentifier(msg) = &e {
855 if let Some(rest) = msg.strip_prefix("duplicate ")
857 && let Some((_, qname)) = rest.split_once(": ")
858 && let Some(loc) = locations.get(qname)
859 {
860 return ParseError::DuplicateObject {
861 qname: qname.to_string(),
862 first: loc.clone(),
863 second: loc.clone(),
864 };
865 }
866 }
867 let placeholder = SourceLocation::new(PathBuf::new(), 0, 0);
868 ParseError::Ir {
869 location: placeholder,
870 source: e,
871 }
872}
873
874#[cfg(test)]
876pub(crate) fn smoke_parse(sql: &str) -> Result<pg_query::ParseResult, pg_query::Error> {
877 pg_query::parse(sql)
878}
879
880#[cfg(test)]
881mod tests {
882 use super::*;
883
884 #[test]
885 fn pg_query_round_trips_a_create_table() {
886 let sql = "CREATE TABLE app.users (id integer);";
887 let result = smoke_parse(sql).expect("pg_query parses");
888 assert!(!result.protobuf.stmts.is_empty());
890 }
891
892 #[test]
893 fn pg_query_reports_syntax_errors() {
894 let sql = "CREATE TABLE !bad!;";
895 assert!(smoke_parse(sql).is_err());
896 }
897}