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;
33use crate::ir::text_search::{TsConfiguration, TsDictionary};
34
35pub fn parse_directory(root: &Path, ignores: &[glob::Pattern]) -> Result<Catalog, ParseError> {
45 parse_directory_with_locations(root, ignores).map(|(c, _)| c)
46}
47
48#[derive(Default)]
65struct ParseContext {
66 catalog: Catalog,
67 locations: HashMap<String, SourceLocation>,
68 pending_fks: Vec<builder::alter_table_stmt::PendingFk>,
69 pending_column_attrs: Vec<builder::alter_table_stmt::PendingColumnAttr>,
70 pending_owners: Vec<builder::alter_table_stmt::PendingOwner>,
71 pending_rls_toggles: Vec<builder::alter_table_stmt::PendingRlsToggle>,
72 pending_rel_options: Vec<builder::alter_table_stmt::PendingRelOptions>,
73 pending_tablespaces: Vec<builder::alter_table_stmt::PendingTablespace>,
74 pending_likes: Vec<builder::table_like::PendingLike>,
75 deferred_comments: Vec<(
76 pg_query::protobuf::CommentStmt,
77 SourceLocation,
78 Option<crate::identifier::Identifier>,
79 )>,
80 publications: BTreeMap<Identifier, Publication>,
81 subscriptions: BTreeMap<Identifier, Subscription>,
82 statistics: BTreeMap<QualifiedName, Statistic>,
83 event_triggers: BTreeMap<Identifier, EventTrigger>,
84 aggregates: Vec<Aggregate>,
85 casts: Vec<Cast>,
86 ts_dictionaries: Vec<TsDictionary>,
87 ts_configurations: Vec<TsConfiguration>,
88}
89
90pub fn parse_directory_with_locations(
97 root: &Path,
98 ignores: &[glob::Pattern],
99) -> Result<(Catalog, HashMap<String, SourceLocation>), ParseError> {
100 let mut files: Vec<PathBuf> = Vec::new();
101 for entry in walkdir::WalkDir::new(root).sort_by_file_name() {
102 let entry = entry.map_err(|e| ParseError::Io {
103 path: e.path().map(Path::to_path_buf).unwrap_or_default(),
104 source: e
105 .into_io_error()
106 .unwrap_or_else(|| std::io::Error::other("walkdir error")),
107 })?;
108 if !entry.file_type().is_file() {
109 continue;
110 }
111 let path = entry.into_path();
112 if path.extension().and_then(|e| e.to_str()) != Some("sql") {
113 continue;
114 }
115 if ignores.iter().any(|pat| pat.matches_path(&path)) {
116 continue;
117 }
118 files.push(path);
119 }
120
121 let mut ctx = ParseContext::default();
125
126 for path in files {
127 let contents = std::fs::read_to_string(&path).map_err(|e| ParseError::Io {
128 path: path.clone(),
129 source: e,
130 })?;
131 process_file(&mut ctx, &path, &contents)?;
132 }
133
134 let ParseContext {
137 mut catalog,
138 locations,
139 pending_fks,
140 pending_column_attrs,
141 pending_owners,
142 pending_rls_toggles,
143 pending_rel_options,
144 pending_tablespaces,
145 pending_likes,
146 deferred_comments,
147 publications,
148 subscriptions,
149 statistics,
150 event_triggers,
151 aggregates,
152 casts,
153 ts_dictionaries,
154 ts_configurations,
155 } = ctx;
156
157 catalog.statistics = statistics.into_values().collect();
161
162 builder::table_like::apply_pending_likes(&mut catalog, &pending_likes)?;
165
166 for (stmt, location, default_schema) in deferred_comments {
169 builder::comment_stmt::apply_comment(
170 &stmt,
171 &mut catalog,
172 default_schema.as_ref(),
173 &location,
174 )?;
175 }
176
177 builder::table_like::apply_pending_like_comments(&mut catalog, &pending_likes)?;
181
182 apply_pending_fks(&mut catalog, pending_fks)?;
184 apply_pending_column_attrs(&mut catalog, pending_column_attrs)?;
185 apply_pending_owners(&mut catalog, pending_owners)?;
186 apply_pending_rls_toggles(&mut catalog, pending_rls_toggles)?;
187 apply_pending_rel_options(&mut catalog, pending_rel_options)?;
188 apply_pending_tablespaces(&mut catalog, pending_tablespaces)?;
189
190 catalog.publications = publications.into_values().collect();
192
193 catalog.subscriptions = subscriptions.into_values().collect();
195
196 catalog.event_triggers = event_triggers.into_values().collect();
200
201 catalog.aggregates = aggregates;
203
204 catalog.casts = casts;
206
207 catalog.ts_dictionaries = ts_dictionaries;
209 catalog.ts_configurations = ts_configurations;
210
211 ast_resolution::resolve(&catalog, &locations).map_err(ParseError::AstResolution)?;
214
215 if !catalog.views.is_empty() || !catalog.materialized_views.is_empty() {
219 ast_canon::canonicalize_view_bodies(&mut catalog).map_err(ParseError::AstCanon)?;
220 }
221
222 ast_canon::promote_mv_index_parents(&mut catalog);
228
229 let canonical = catalog
230 .canonicalize()
231 .map_err(|e: IrError| translate_canonicalize_error(e, &locations))?;
232 Ok((canonical, locations))
233}
234
235fn apply_statistics_comment(
241 stmt: &pg_query::protobuf::CommentStmt,
242 statistics: &mut BTreeMap<QualifiedName, Statistic>,
243 location: &SourceLocation,
244) -> Result<(), ParseError> {
245 use pg_query::NodeEnum;
246
247 let obj = stmt
249 .object
250 .as_ref()
251 .and_then(|o| o.node.as_ref())
252 .ok_or_else(|| ParseError::Structural {
253 location: location.clone(),
254 message: "COMMENT ON STATISTICS missing object reference".into(),
255 })?;
256 let NodeEnum::List(list) = obj else {
257 return Err(ParseError::Structural {
258 location: location.clone(),
259 message: format!(
260 "COMMENT ON STATISTICS expected a List node, got {:?}",
261 std::mem::discriminant(obj)
262 ),
263 });
264 };
265 let qname = builder::shared::qname_from_string_list(&list.items, None, location)?;
270
271 let comment = if stmt.comment.is_empty() {
272 None
273 } else {
274 Some(stmt.comment.clone())
275 };
276
277 let statistic = statistics.get_mut(&qname).ok_or_else(|| {
278 ParseError::CommentOnStatisticBeforeCreate(qname.clone(), location.clone())
279 })?;
280 statistic.comment = comment;
281 Ok(())
282}
283
284fn apply_pending_fks(
286 catalog: &mut Catalog,
287 pending_fks: Vec<builder::alter_table_stmt::PendingFk>,
288) -> Result<(), ParseError> {
289 for pending in pending_fks {
290 let table = catalog
291 .tables
292 .iter_mut()
293 .find(|t| t.qname == pending.target)
294 .ok_or_else(|| ParseError::Structural {
295 location: SourceLocation::new(PathBuf::new(), 0, 0),
296 message: format!("ALTER TABLE referenced unknown table {}", pending.target),
297 })?;
298 table.constraints.push(pending.constraint);
299 }
300 Ok(())
301}
302
303fn apply_pending_column_attrs(
305 catalog: &mut Catalog,
306 pending: Vec<builder::alter_table_stmt::PendingColumnAttr>,
307) -> Result<(), ParseError> {
308 use builder::alter_table_stmt::PendingColumnAttrKind;
309 for attr in pending {
310 let table = catalog
311 .tables
312 .iter_mut()
313 .find(|t| t.qname == attr.target)
314 .ok_or_else(|| ParseError::Structural {
315 location: SourceLocation::new(PathBuf::new(), 0, 0),
316 message: format!(
317 "ALTER TABLE ALTER COLUMN referenced unknown table {}",
318 attr.target
319 ),
320 })?;
321 let col = table
322 .columns
323 .iter_mut()
324 .find(|c| c.name == attr.column)
325 .ok_or_else(|| ParseError::Structural {
326 location: SourceLocation::new(PathBuf::new(), 0, 0),
327 message: format!(
328 "ALTER TABLE ALTER COLUMN referenced unknown column {}.{}",
329 attr.target, attr.column
330 ),
331 })?;
332 match attr.kind {
333 PendingColumnAttrKind::Storage(s) => {
334 col.storage = Some(s);
335 }
336 PendingColumnAttrKind::Compression(c) => {
337 col.compression = c;
338 }
339 }
340 }
341 Ok(())
342}
343
344fn apply_pending_rel_options(
349 catalog: &mut Catalog,
350 pending: Vec<builder::alter_table_stmt::PendingRelOptions>,
351) -> Result<(), ParseError> {
352 let loc = SourceLocation::new(PathBuf::new(), 0, 0);
353 builder::alter_table_stmt::apply_pending_rel_options(catalog, pending, &loc)
354}
355
356fn apply_pending_tablespaces(
360 catalog: &mut Catalog,
361 pending: Vec<builder::alter_table_stmt::PendingTablespace>,
362) -> Result<(), ParseError> {
363 let loc = SourceLocation::new(PathBuf::new(), 0, 0);
364 builder::alter_table_stmt::apply_pending_tablespaces(catalog, pending, &loc)
365}
366
367fn apply_pending_rls_toggles(
371 catalog: &mut Catalog,
372 pending: Vec<builder::alter_table_stmt::PendingRlsToggle>,
373) -> Result<(), ParseError> {
374 let loc = SourceLocation::new(PathBuf::new(), 0, 0);
375 builder::alter_table_stmt::apply_pending_rls_toggles(catalog, pending, &loc)
376}
377
378fn apply_pending_owners(
382 catalog: &mut Catalog,
383 pending: Vec<builder::alter_table_stmt::PendingOwner>,
384) -> Result<(), ParseError> {
385 let loc = SourceLocation::new(PathBuf::new(), 0, 0);
386 for po in pending {
387 builder::alter_table_stmt::apply_pending_owners(catalog, vec![po], &loc)?;
388 }
389 Ok(())
390}
391
392#[allow(clippy::too_many_lines)]
395fn process_file(ctx: &mut ParseContext, path: &Path, contents: &str) -> Result<(), ParseError> {
396 let directives = directives::extract_file_directives(contents, path)?;
397 let parsed = pg_query::parse(contents).map_err(|e| ParseError::PgQuery {
398 location: SourceLocation::new(path.to_path_buf(), 1, 1),
399 message: e.to_string(),
400 })?;
401
402 let ParseContext {
406 catalog,
407 locations,
408 pending_fks,
409 pending_column_attrs,
410 pending_owners,
411 pending_rls_toggles,
412 pending_rel_options,
413 pending_tablespaces,
414 pending_likes,
415 deferred_comments,
416 publications,
417 subscriptions,
418 statistics,
419 event_triggers,
420 aggregates,
421 casts,
422 ts_dictionaries,
423 ts_configurations,
424 } = ctx;
425
426 for raw in parsed.protobuf.stmts {
427 let location = stmt_location(path, contents, raw.stmt_location);
428 let Some(node) = raw.stmt.and_then(|n| n.node) else {
429 continue;
430 };
431 let stmt = Statement::classify(node, location.clone())?;
432 match stmt {
433 Statement::CreateSchema(s) => {
434 let schema = builder::create_schema_stmt::build_schema(&s, &location)?;
435 let schema_qname = QualifiedName::new(schema.name.clone(), schema.name.clone()); if let Some(prior) = locations.get(&schema.name.to_string()) {
437 return Err(ParseError::DuplicateObject {
438 qname: schema.name.to_string(),
439 first: prior.clone(),
440 second: location,
441 });
442 }
443 locations.insert(schema.name.to_string(), location.clone());
444 catalog.schemas.push(schema);
445 let _ = schema_qname;
446 }
447 Statement::CreateTable(s) => {
448 let mut table =
449 builder::create_stmt::build_table(&s, directives.schema.as_ref(), &location)?;
450 pending_likes.extend(builder::table_like::extract_pending_likes(
451 &s,
452 &table.qname,
453 directives.schema.as_ref(),
454 &location,
455 )?);
456 let serial_seqs =
457 builder::desugar_serial::desugar_serials_in_table(&mut table, &location)?;
458 if let Some(prior) = locations.get(&table.qname.to_string()) {
459 return Err(ParseError::DuplicateObject {
460 qname: table.qname.to_string(),
461 first: prior.clone(),
462 second: location,
463 });
464 }
465 locations.insert(table.qname.to_string(), location.clone());
466 catalog.tables.push(table);
467 for seq in serial_seqs {
468 if let Some(prior) = locations.get(&seq.qname.to_string()) {
469 return Err(ParseError::DuplicateObject {
470 qname: seq.qname.to_string(),
471 first: prior.clone(),
472 second: location.clone(),
473 });
474 }
475 locations.insert(seq.qname.to_string(), location.clone());
476 catalog.sequences.push(seq);
477 }
478 }
479 Statement::CreateSequence(s) => {
480 let seq = builder::create_seq_stmt::build_sequence(
481 &s,
482 directives.schema.as_ref(),
483 &location,
484 )?;
485 if let Some(prior) = locations.get(&seq.qname.to_string()) {
486 return Err(ParseError::DuplicateObject {
487 qname: seq.qname.to_string(),
488 first: prior.clone(),
489 second: location,
490 });
491 }
492 locations.insert(seq.qname.to_string(), location.clone());
493 catalog.sequences.push(seq);
494 }
495 Statement::CreateIndex(s) => {
496 let idx =
497 builder::index_stmt::build_index(&s, directives.schema.as_ref(), &location)?;
498 if let Some(prior) = locations.get(&idx.qname.to_string()) {
499 return Err(ParseError::DuplicateObject {
500 qname: idx.qname.to_string(),
501 first: prior.clone(),
502 second: location,
503 });
504 }
505 locations.insert(idx.qname.to_string(), location.clone());
506 catalog.indexes.push(idx);
507 }
508 Statement::AlterTable(s) => {
509 let alter_out = builder::alter_table_stmt::build_alter_table(
510 &s,
511 directives.schema.as_ref(),
512 &location,
513 )?;
514 pending_fks.extend(alter_out.pending_fks);
515 pending_column_attrs.extend(alter_out.pending_column_attrs);
516 pending_owners.extend(alter_out.pending_owners);
517 pending_rls_toggles.extend(alter_out.pending_rls_toggles);
518 pending_rel_options.extend(alter_out.pending_rel_options);
519 pending_tablespaces.extend(alter_out.pending_tablespaces);
520 }
521 Statement::Comment(s) => {
522 use pg_query::protobuf::ObjectType;
523 let kind = ObjectType::try_from(s.objtype).unwrap_or(ObjectType::Undefined);
524 if matches!(kind, ObjectType::ObjectStatisticExt) {
525 apply_statistics_comment(&s, statistics, &location)?;
529 } else if matches!(kind, ObjectType::ObjectEventTrigger) {
530 builder::event_trigger_stmt::apply_event_trigger_comment(
533 &s,
534 &location,
535 event_triggers,
536 )?;
537 } else if matches!(kind, ObjectType::ObjectAggregate) {
538 builder::aggregate_stmt::apply_comment(
541 &s,
542 directives.schema.as_ref(),
543 &location,
544 aggregates,
545 )?;
546 } else if matches!(kind, ObjectType::ObjectCast) {
547 builder::cast_stmt::apply_comment(
550 &s,
551 directives.schema.as_ref(),
552 &location,
553 casts,
554 )?;
555 } else if matches!(kind, ObjectType::ObjectTsdictionary) {
556 builder::text_search_stmt::apply_dictionary_comment(
559 &s,
560 directives.schema.as_ref(),
561 &location,
562 ts_dictionaries,
563 )?;
564 } else if matches!(kind, ObjectType::ObjectTsconfiguration) {
565 builder::text_search_stmt::apply_configuration_comment(
567 &s,
568 directives.schema.as_ref(),
569 &location,
570 ts_configurations,
571 )?;
572 } else {
573 deferred_comments.push((s, location, directives.schema.clone()));
574 }
575 }
576 Statement::CreateView(s) => {
577 let view = builder::create_view_stmt::build_view(
578 &s,
579 directives.schema.as_ref(),
580 &location,
581 )?;
582 if let Some(prior) = locations.get(&view.qname.to_string()) {
583 return Err(ParseError::DuplicateObject {
584 qname: view.qname.to_string(),
585 first: prior.clone(),
586 second: location,
587 });
588 }
589 locations.insert(view.qname.to_string(), location.clone());
590 catalog.views.push(view);
591 }
592 Statement::CreateMaterializedView(s) => {
593 let mv = builder::create_materialized_view_stmt::build_materialized_view(
594 &s,
595 directives.schema.as_ref(),
596 &location,
597 )?;
598 if let Some(prior) = locations.get(&mv.qname.to_string()) {
599 return Err(ParseError::DuplicateObject {
600 qname: mv.qname.to_string(),
601 first: prior.clone(),
602 second: location,
603 });
604 }
605 locations.insert(mv.qname.to_string(), location.clone());
606 catalog.materialized_views.push(mv);
607 }
608 Statement::CreateEnum(s) => {
609 let ut = builder::create_enum_stmt::build_enum(
610 &s,
611 directives.schema.as_ref(),
612 &location,
613 )?;
614 if let Some(prior) = locations.get(&ut.qname.to_string()) {
615 return Err(ParseError::DuplicateObject {
616 qname: ut.qname.to_string(),
617 first: prior.clone(),
618 second: location,
619 });
620 }
621 locations.insert(ut.qname.to_string(), location.clone());
622 catalog.types.push(ut);
623 }
624 Statement::CreateDomain(s) => {
625 let ut = builder::create_domain_stmt::build_domain(
626 &s,
627 directives.schema.as_ref(),
628 &location,
629 )?;
630 if let Some(prior) = locations.get(&ut.qname.to_string()) {
631 return Err(ParseError::DuplicateObject {
632 qname: ut.qname.to_string(),
633 first: prior.clone(),
634 second: location,
635 });
636 }
637 locations.insert(ut.qname.to_string(), location.clone());
638 catalog.types.push(ut);
639 }
640 Statement::CreateCompositeType(s) => {
641 let ut = builder::create_composite_type_stmt::build_composite(
642 &s,
643 directives.schema.as_ref(),
644 &location,
645 )?;
646 if let Some(prior) = locations.get(&ut.qname.to_string()) {
647 return Err(ParseError::DuplicateObject {
648 qname: ut.qname.to_string(),
649 first: prior.clone(),
650 second: location,
651 });
652 }
653 locations.insert(ut.qname.to_string(), location.clone());
654 catalog.types.push(ut);
655 }
656 Statement::CreateRange(s) => {
657 let ut = builder::create_range_stmt::build_range(
658 &s,
659 directives.schema.as_ref(),
660 &location,
661 )?;
662 if let Some(prior) = locations.get(&ut.qname.to_string()) {
663 return Err(ParseError::DuplicateObject {
664 qname: ut.qname.to_string(),
665 first: prior.clone(),
666 second: location,
667 });
668 }
669 locations.insert(ut.qname.to_string(), location.clone());
670 catalog.types.push(ut);
671 }
672 Statement::CreateFunction(s) => {
673 let routine = builder::create_function_stmt::build_function_or_procedure(
674 &s,
675 directives.schema.as_ref(),
676 &location,
677 )?;
678 let builder::create_function_stmt::Routine::Function(f) = routine else {
679 return Err(ParseError::Structural {
680 location,
681 message: "internal error: expected Function from non-procedure stmt".into(),
682 });
683 };
684 let arg_sig = f
685 .args
686 .iter()
687 .filter(|a| {
688 matches!(
689 a.mode,
690 crate::ir::function::ArgMode::In
691 | crate::ir::function::ArgMode::InOut
692 | crate::ir::function::ArgMode::Variadic
693 )
694 })
695 .map(|a| a.ty.render_sql())
696 .collect::<Vec<_>>()
697 .join(",");
698 let key = format!("functions.{}({arg_sig})", f.qname);
699 if let Some(prior) = locations.get(&key) {
700 return Err(ParseError::DuplicateObject {
701 qname: key,
702 first: prior.clone(),
703 second: location,
704 });
705 }
706 locations.insert(key, location.clone());
707 catalog.functions.push(f);
708 }
709 Statement::CreateProcedure(s) => {
710 let routine = builder::create_function_stmt::build_function_or_procedure(
711 &s,
712 directives.schema.as_ref(),
713 &location,
714 )?;
715 let builder::create_function_stmt::Routine::Procedure(p) = routine else {
716 return Err(ParseError::Structural {
717 location,
718 message: "internal error: expected Procedure from procedure stmt".into(),
719 });
720 };
721 let key = format!("procedures.{}", p.qname);
727 if let Some(prior) = locations.get(&key) {
728 return Err(ParseError::DuplicateObject {
729 qname: key,
730 first: prior.clone(),
731 second: location,
732 });
733 }
734 locations.insert(key, location.clone());
735 catalog.procedures.push(p);
736 }
737 Statement::CreateExtension(s) => {
738 let ext = builder::create_extension_stmt::build_extension(&s, &location)?;
739 let key = format!("extensions.{}", ext.name);
740 if let Some(prior) = locations.get(&key) {
741 return Err(ParseError::DuplicateObject {
742 qname: key,
743 first: prior.clone(),
744 second: location,
745 });
746 }
747 locations.insert(key, location.clone());
748 catalog.extensions.push(ext);
749 }
750 Statement::CreateTrigger(s) => {
751 let trigger = builder::create_trigger_stmt::build_trigger(&s, &location)?;
752 let key = format!("triggers.{}", trigger.qname);
753 if let Some(prior) = locations.get(&key) {
754 return Err(ParseError::DuplicateObject {
755 qname: key,
756 first: prior.clone(),
757 second: location,
758 });
759 }
760 locations.insert(key, location.clone());
761 catalog.triggers.push(trigger);
762 }
763 Statement::AlterTableAttachPartition(s) => {
764 let attach = builder::alter_table_attach_partition::build_attach_partition(
765 &s,
766 directives.schema.as_ref(),
767 &location,
768 )?;
769 let child_table = catalog
770 .tables
771 .iter_mut()
772 .find(|t| t.qname == attach.child)
773 .ok_or_else(|| ParseError::Structural {
774 location: location.clone(),
775 message: format!(
776 "ATTACH PARTITION {} must follow its CREATE TABLE statement",
777 attach.child
778 ),
779 })?;
780 if child_table.partition_of.is_some() {
781 return Err(ParseError::Structural {
782 location,
783 message: format!("table {} already declared as a partition", attach.child),
784 });
785 }
786 child_table.partition_of = Some(attach.partition_of);
787 }
788 Statement::Grant(s) => {
789 builder::grants::apply(&s, catalog, &location)?;
790 }
791 Statement::AlterOwner(s) => {
792 use pg_query::protobuf::ObjectType;
793 let objtype = ObjectType::try_from(s.object_type).unwrap_or(ObjectType::Undefined);
794 if matches!(objtype, ObjectType::ObjectEventTrigger) {
795 builder::event_trigger_stmt::apply_event_trigger_owner(
799 &s,
800 &location,
801 event_triggers,
802 )?;
803 } else if matches!(objtype, ObjectType::ObjectAggregate) {
804 builder::aggregate_stmt::apply_owner(
807 &s,
808 directives.schema.as_ref(),
809 &location,
810 aggregates,
811 )?;
812 } else if matches!(objtype, ObjectType::ObjectTsdictionary) {
813 builder::text_search_stmt::apply_dictionary_owner(
815 &s,
816 directives.schema.as_ref(),
817 &location,
818 ts_dictionaries,
819 )?;
820 } else if matches!(objtype, ObjectType::ObjectTsconfiguration) {
821 builder::text_search_stmt::apply_configuration_owner(
823 &s,
824 directives.schema.as_ref(),
825 &location,
826 ts_configurations,
827 )?;
828 } else {
829 builder::owner_stmt::apply(&s, catalog, &location)?;
830 }
831 }
832 Statement::AlterDefaultPrivileges(s) => {
833 builder::default_privileges::apply(&s, catalog, &location)?;
834 }
835 Statement::CreatePolicy(s) => {
836 builder::policy_stmt::apply(&s, catalog, &location)?;
837 }
838 Statement::CreatePublication(s) => {
839 builder::publication_stmt::parse_create_publication(&s, location, publications)?;
840 }
841 Statement::AlterPublication(s) => {
842 builder::publication_stmt::parse_alter_publication(&s, location, publications)?;
843 }
844 Statement::CreateSubscription(s) => {
845 builder::subscription_stmt::parse_create_subscription(&s, location, subscriptions)?;
846 }
847 Statement::AlterSubscription(s) => {
848 builder::subscription_stmt::parse_alter_subscription(&s, location, subscriptions)?;
849 }
850 Statement::CreateStatistics(s) => {
851 builder::statistic_stmt::parse_create_statistics(&s, location, statistics)?;
852 }
853 Statement::AlterStatistics(s) => {
854 builder::statistic_stmt::parse_alter_statistics(&s, &location, statistics)?;
855 }
856 Statement::CreateCollation(s) => {
857 let coll = builder::create_collation_stmt::build_collation(
858 &s,
859 directives.schema.as_ref(),
860 &location,
861 )?;
862 if let Some(prior) = locations.get(&coll.qname.to_string()) {
863 return Err(ParseError::DuplicateObject {
864 qname: coll.qname.to_string(),
865 first: prior.clone(),
866 second: location,
867 });
868 }
869 locations.insert(coll.qname.to_string(), location.clone());
870 catalog.collations.push(coll);
871 }
872 Statement::CreateAggregate(s) => {
873 builder::aggregate_stmt::parse_create(
874 &s,
875 directives.schema.as_ref(),
876 &location,
877 aggregates,
878 )?;
879 }
880 Statement::CreateEventTrigger(s) => {
881 builder::event_trigger_stmt::parse_create_event_trigger(
882 &s,
883 directives.schema.as_ref(),
884 location,
885 event_triggers,
886 )?;
887 }
888 Statement::AlterEventTrigger(s) => {
889 builder::event_trigger_stmt::parse_alter_event_trigger(
890 &s,
891 location,
892 event_triggers,
893 )?;
894 }
895 Statement::CreateCast(s) => {
896 builder::cast_stmt::parse_create(&s, directives.schema.as_ref(), &location, casts)?;
897 }
898 Statement::CreateTsDictionary(s) => {
899 builder::text_search_stmt::parse_create_dictionary(
900 &s,
901 directives.schema.as_ref(),
902 &location,
903 ts_dictionaries,
904 )?;
905 }
906 Statement::CreateTsConfiguration(s) => {
907 builder::text_search_stmt::parse_create_configuration(
908 &s,
909 directives.schema.as_ref(),
910 &location,
911 ts_configurations,
912 )?;
913 }
914 Statement::AlterTsDictionary(s) => {
915 builder::text_search_stmt::apply_alter_dictionary(
916 &s,
917 directives.schema.as_ref(),
918 &location,
919 ts_dictionaries,
920 )?;
921 }
922 Statement::AlterTsConfiguration(s) => {
923 builder::text_search_stmt::apply_alter_configuration(
924 &s,
925 directives.schema.as_ref(),
926 &location,
927 ts_configurations,
928 )?;
929 }
930 }
931 }
932 Ok(())
933}
934
935fn stmt_location(path: &Path, contents: &str, byte_offset: i32) -> SourceLocation {
937 let offset = usize::try_from(byte_offset).unwrap_or(0);
938 let mut line = 1usize;
939 let mut col = 1usize;
940 for (i, c) in contents.char_indices() {
941 if i >= offset {
942 break;
943 }
944 if c == '\n' {
945 line += 1;
946 col = 1;
947 } else {
948 col += 1;
949 }
950 }
951 SourceLocation::new(path.to_path_buf(), line, col)
952}
953
954fn translate_canonicalize_error(
955 e: IrError,
956 locations: &HashMap<String, SourceLocation>,
957) -> ParseError {
958 if let IrError::InvalidIdentifier(msg) = &e {
959 if let Some(rest) = msg.strip_prefix("duplicate ")
961 && let Some((_, qname)) = rest.split_once(": ")
962 && let Some(loc) = locations.get(qname)
963 {
964 return ParseError::DuplicateObject {
965 qname: qname.to_string(),
966 first: loc.clone(),
967 second: loc.clone(),
968 };
969 }
970 }
971 let placeholder = SourceLocation::new(PathBuf::new(), 0, 0);
972 ParseError::Ir {
973 location: placeholder,
974 source: e,
975 }
976}
977
978#[cfg(test)]
980pub(crate) fn smoke_parse(sql: &str) -> Result<pg_query::ParseResult, pg_query::Error> {
981 pg_query::parse(sql)
982}
983
984#[cfg(test)]
985mod tests {
986 use super::*;
987
988 #[test]
989 fn pg_query_round_trips_a_create_table() {
990 let sql = "CREATE TABLE app.users (id integer);";
991 let result = smoke_parse(sql).expect("pg_query parses");
992 assert!(!result.protobuf.stmts.is_empty());
994 }
995
996 #[test]
997 fn pg_query_reports_syntax_errors() {
998 let sql = "CREATE TABLE !bad!;";
999 assert!(smoke_parse(sql).is_err());
1000 }
1001}