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