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