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