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::publication::Publication;
28use crate::ir::subscription::Subscription;
29
30pub fn parse_directory(root: &Path, ignores: &[glob::Pattern]) -> Result<Catalog, ParseError> {
40 parse_directory_with_locations(root, ignores).map(|(c, _)| c)
41}
42
43pub fn parse_directory_with_locations(
50 root: &Path,
51 ignores: &[glob::Pattern],
52) -> Result<(Catalog, HashMap<String, SourceLocation>), ParseError> {
53 let mut files: Vec<PathBuf> = Vec::new();
54 for entry in walkdir::WalkDir::new(root).sort_by_file_name() {
55 let entry = entry.map_err(|e| ParseError::Io {
56 path: e.path().map(Path::to_path_buf).unwrap_or_default(),
57 source: e
58 .into_io_error()
59 .unwrap_or_else(|| std::io::Error::other("walkdir error")),
60 })?;
61 if !entry.file_type().is_file() {
62 continue;
63 }
64 let path = entry.into_path();
65 if path.extension().and_then(|e| e.to_str()) != Some("sql") {
66 continue;
67 }
68 if ignores.iter().any(|pat| pat.matches_path(&path)) {
69 continue;
70 }
71 files.push(path);
72 }
73
74 let mut catalog = Catalog::default();
75 let mut locations: HashMap<String, SourceLocation> = HashMap::new();
76 let mut pending_fks: Vec<builder::alter_table_stmt::PendingFk> = Vec::new();
77 let mut pending_column_attrs: Vec<builder::alter_table_stmt::PendingColumnAttr> = Vec::new();
78 let mut pending_owners: Vec<builder::alter_table_stmt::PendingOwner> = Vec::new();
79 let mut pending_rls_toggles: Vec<builder::alter_table_stmt::PendingRlsToggle> = Vec::new();
80 let mut pending_rel_options: Vec<builder::alter_table_stmt::PendingRelOptions> = Vec::new();
81 let mut deferred_comments: Vec<(
82 pg_query::protobuf::CommentStmt,
83 SourceLocation,
84 Option<crate::identifier::Identifier>,
85 )> = Vec::new();
86 let mut publications: BTreeMap<Identifier, Publication> = BTreeMap::new();
90 let mut subscriptions: BTreeMap<Identifier, Subscription> = BTreeMap::new();
92
93 for path in files {
94 let contents = std::fs::read_to_string(&path).map_err(|e| ParseError::Io {
95 path: path.clone(),
96 source: e,
97 })?;
98 process_file(
99 &path,
100 &contents,
101 &mut catalog,
102 &mut locations,
103 &mut pending_fks,
104 &mut pending_column_attrs,
105 &mut pending_owners,
106 &mut pending_rls_toggles,
107 &mut pending_rel_options,
108 &mut deferred_comments,
109 &mut publications,
110 &mut subscriptions,
111 )?;
112 }
113
114 for (stmt, location, default_schema) in deferred_comments {
117 builder::comment_stmt::apply_comment(
118 &stmt,
119 &mut catalog,
120 default_schema.as_ref(),
121 &location,
122 )?;
123 }
124
125 apply_pending_fks(&mut catalog, pending_fks)?;
127 apply_pending_column_attrs(&mut catalog, pending_column_attrs)?;
128 apply_pending_owners(&mut catalog, pending_owners)?;
129 apply_pending_rls_toggles(&mut catalog, pending_rls_toggles)?;
130 apply_pending_rel_options(&mut catalog, pending_rel_options)?;
131
132 catalog.publications = publications.into_values().collect();
134
135 catalog.subscriptions = subscriptions.into_values().collect();
137
138 ast_resolution::resolve(&catalog, &locations).map_err(ParseError::AstResolution)?;
141
142 if !catalog.views.is_empty() || !catalog.materialized_views.is_empty() {
146 ast_canon::canonicalize_view_bodies(&mut catalog).map_err(ParseError::AstCanon)?;
147 }
148
149 ast_canon::promote_mv_index_parents(&mut catalog);
155
156 let canonical = catalog
157 .canonicalize()
158 .map_err(|e: IrError| translate_canonicalize_error(e, &locations))?;
159 Ok((canonical, locations))
160}
161
162fn apply_pending_fks(
164 catalog: &mut Catalog,
165 pending_fks: Vec<builder::alter_table_stmt::PendingFk>,
166) -> Result<(), ParseError> {
167 for pending in pending_fks {
168 let table = catalog
169 .tables
170 .iter_mut()
171 .find(|t| t.qname == pending.target)
172 .ok_or_else(|| ParseError::Structural {
173 location: SourceLocation::new(PathBuf::new(), 0, 0),
174 message: format!("ALTER TABLE referenced unknown table {}", pending.target),
175 })?;
176 table.constraints.push(pending.constraint);
177 }
178 Ok(())
179}
180
181fn apply_pending_column_attrs(
183 catalog: &mut Catalog,
184 pending: Vec<builder::alter_table_stmt::PendingColumnAttr>,
185) -> Result<(), ParseError> {
186 use builder::alter_table_stmt::PendingColumnAttrKind;
187 for attr in pending {
188 let table = catalog
189 .tables
190 .iter_mut()
191 .find(|t| t.qname == attr.target)
192 .ok_or_else(|| ParseError::Structural {
193 location: SourceLocation::new(PathBuf::new(), 0, 0),
194 message: format!(
195 "ALTER TABLE ALTER COLUMN referenced unknown table {}",
196 attr.target
197 ),
198 })?;
199 let col = table
200 .columns
201 .iter_mut()
202 .find(|c| c.name == attr.column)
203 .ok_or_else(|| ParseError::Structural {
204 location: SourceLocation::new(PathBuf::new(), 0, 0),
205 message: format!(
206 "ALTER TABLE ALTER COLUMN referenced unknown column {}.{}",
207 attr.target, attr.column
208 ),
209 })?;
210 match attr.kind {
211 PendingColumnAttrKind::Storage(s) => {
212 col.storage = Some(s);
213 }
214 PendingColumnAttrKind::Compression(c) => {
215 col.compression = c;
216 }
217 }
218 }
219 Ok(())
220}
221
222fn apply_pending_rel_options(
227 catalog: &mut Catalog,
228 pending: Vec<builder::alter_table_stmt::PendingRelOptions>,
229) -> Result<(), ParseError> {
230 let loc = SourceLocation::new(PathBuf::new(), 0, 0);
231 builder::alter_table_stmt::apply_pending_rel_options(catalog, pending, &loc)
232}
233
234fn apply_pending_rls_toggles(
238 catalog: &mut Catalog,
239 pending: Vec<builder::alter_table_stmt::PendingRlsToggle>,
240) -> Result<(), ParseError> {
241 let loc = SourceLocation::new(PathBuf::new(), 0, 0);
242 builder::alter_table_stmt::apply_pending_rls_toggles(catalog, pending, &loc)
243}
244
245fn apply_pending_owners(
249 catalog: &mut Catalog,
250 pending: Vec<builder::alter_table_stmt::PendingOwner>,
251) -> Result<(), ParseError> {
252 let loc = SourceLocation::new(PathBuf::new(), 0, 0);
253 for po in pending {
254 builder::alter_table_stmt::apply_pending_owners(catalog, vec![po], &loc)?;
255 }
256 Ok(())
257}
258
259#[allow(clippy::too_many_lines, clippy::too_many_arguments)]
260fn process_file(
261 path: &Path,
262 contents: &str,
263 catalog: &mut Catalog,
264 locations: &mut HashMap<String, SourceLocation>,
265 pending_fks: &mut Vec<builder::alter_table_stmt::PendingFk>,
266 pending_column_attrs: &mut Vec<builder::alter_table_stmt::PendingColumnAttr>,
267 pending_owners: &mut Vec<builder::alter_table_stmt::PendingOwner>,
268 pending_rls_toggles: &mut Vec<builder::alter_table_stmt::PendingRlsToggle>,
269 pending_rel_options: &mut Vec<builder::alter_table_stmt::PendingRelOptions>,
270 deferred_comments: &mut Vec<(
271 pg_query::protobuf::CommentStmt,
272 SourceLocation,
273 Option<crate::identifier::Identifier>,
274 )>,
275 publications: &mut BTreeMap<Identifier, Publication>,
276 subscriptions: &mut BTreeMap<Identifier, Subscription>,
277) -> Result<(), ParseError> {
278 let directives = directives::extract_file_directives(contents, path)?;
279 let parsed = pg_query::parse(contents).map_err(|e| ParseError::PgQuery {
280 location: SourceLocation::new(path.to_path_buf(), 1, 1),
281 message: e.to_string(),
282 })?;
283
284 for raw in parsed.protobuf.stmts {
285 let location = stmt_location(path, contents, raw.stmt_location);
286 let Some(node) = raw.stmt.and_then(|n| n.node) else {
287 continue;
288 };
289 let stmt = Statement::classify(node, location.clone())?;
290 match stmt {
291 Statement::CreateSchema(s) => {
292 let schema = builder::create_schema_stmt::build_schema(&s, &location)?;
293 let schema_qname = QualifiedName::new(schema.name.clone(), schema.name.clone()); if let Some(prior) = locations.get(&schema.name.to_string()) {
295 return Err(ParseError::DuplicateObject {
296 qname: schema.name.to_string(),
297 first: prior.clone(),
298 second: location,
299 });
300 }
301 locations.insert(schema.name.to_string(), location.clone());
302 catalog.schemas.push(schema);
303 let _ = schema_qname;
304 }
305 Statement::CreateTable(s) => {
306 let mut table =
307 builder::create_stmt::build_table(&s, directives.schema.as_ref(), &location)?;
308 let serial_seqs =
309 builder::desugar_serial::desugar_serials_in_table(&mut table, &location)?;
310 if let Some(prior) = locations.get(&table.qname.to_string()) {
311 return Err(ParseError::DuplicateObject {
312 qname: table.qname.to_string(),
313 first: prior.clone(),
314 second: location,
315 });
316 }
317 locations.insert(table.qname.to_string(), location.clone());
318 catalog.tables.push(table);
319 for seq in serial_seqs {
320 if let Some(prior) = locations.get(&seq.qname.to_string()) {
321 return Err(ParseError::DuplicateObject {
322 qname: seq.qname.to_string(),
323 first: prior.clone(),
324 second: location.clone(),
325 });
326 }
327 locations.insert(seq.qname.to_string(), location.clone());
328 catalog.sequences.push(seq);
329 }
330 }
331 Statement::CreateSequence(s) => {
332 let seq = builder::create_seq_stmt::build_sequence(
333 &s,
334 directives.schema.as_ref(),
335 &location,
336 )?;
337 if let Some(prior) = locations.get(&seq.qname.to_string()) {
338 return Err(ParseError::DuplicateObject {
339 qname: seq.qname.to_string(),
340 first: prior.clone(),
341 second: location,
342 });
343 }
344 locations.insert(seq.qname.to_string(), location.clone());
345 catalog.sequences.push(seq);
346 }
347 Statement::CreateIndex(s) => {
348 let idx =
349 builder::index_stmt::build_index(&s, directives.schema.as_ref(), &location)?;
350 if let Some(prior) = locations.get(&idx.qname.to_string()) {
351 return Err(ParseError::DuplicateObject {
352 qname: idx.qname.to_string(),
353 first: prior.clone(),
354 second: location,
355 });
356 }
357 locations.insert(idx.qname.to_string(), location.clone());
358 catalog.indexes.push(idx);
359 }
360 Statement::AlterTable(s) => {
361 let alter_out = builder::alter_table_stmt::build_alter_table(
362 &s,
363 directives.schema.as_ref(),
364 &location,
365 )?;
366 pending_fks.extend(alter_out.pending_fks);
367 pending_column_attrs.extend(alter_out.pending_column_attrs);
368 pending_owners.extend(alter_out.pending_owners);
369 pending_rls_toggles.extend(alter_out.pending_rls_toggles);
370 pending_rel_options.extend(alter_out.pending_rel_options);
371 }
372 Statement::Comment(s) => {
373 deferred_comments.push((s, location, directives.schema.clone()));
374 }
375 Statement::CreateView(s) => {
376 let view = builder::create_view_stmt::build_view(
377 &s,
378 directives.schema.as_ref(),
379 &location,
380 )?;
381 if let Some(prior) = locations.get(&view.qname.to_string()) {
382 return Err(ParseError::DuplicateObject {
383 qname: view.qname.to_string(),
384 first: prior.clone(),
385 second: location,
386 });
387 }
388 locations.insert(view.qname.to_string(), location.clone());
389 catalog.views.push(view);
390 }
391 Statement::CreateMaterializedView(s) => {
392 let mv = builder::create_materialized_view_stmt::build_materialized_view(
393 &s,
394 directives.schema.as_ref(),
395 &location,
396 )?;
397 if let Some(prior) = locations.get(&mv.qname.to_string()) {
398 return Err(ParseError::DuplicateObject {
399 qname: mv.qname.to_string(),
400 first: prior.clone(),
401 second: location,
402 });
403 }
404 locations.insert(mv.qname.to_string(), location.clone());
405 catalog.materialized_views.push(mv);
406 }
407 Statement::CreateEnum(s) => {
408 let ut = builder::create_enum_stmt::build_enum(
409 &s,
410 directives.schema.as_ref(),
411 &location,
412 )?;
413 if let Some(prior) = locations.get(&ut.qname.to_string()) {
414 return Err(ParseError::DuplicateObject {
415 qname: ut.qname.to_string(),
416 first: prior.clone(),
417 second: location,
418 });
419 }
420 locations.insert(ut.qname.to_string(), location.clone());
421 catalog.types.push(ut);
422 }
423 Statement::CreateDomain(s) => {
424 let ut = builder::create_domain_stmt::build_domain(
425 &s,
426 directives.schema.as_ref(),
427 &location,
428 )?;
429 if let Some(prior) = locations.get(&ut.qname.to_string()) {
430 return Err(ParseError::DuplicateObject {
431 qname: ut.qname.to_string(),
432 first: prior.clone(),
433 second: location,
434 });
435 }
436 locations.insert(ut.qname.to_string(), location.clone());
437 catalog.types.push(ut);
438 }
439 Statement::CreateCompositeType(s) => {
440 let ut = builder::create_composite_type_stmt::build_composite(
441 &s,
442 directives.schema.as_ref(),
443 &location,
444 )?;
445 if let Some(prior) = locations.get(&ut.qname.to_string()) {
446 return Err(ParseError::DuplicateObject {
447 qname: ut.qname.to_string(),
448 first: prior.clone(),
449 second: location,
450 });
451 }
452 locations.insert(ut.qname.to_string(), location.clone());
453 catalog.types.push(ut);
454 }
455 Statement::CreateFunction(s) => {
456 let routine = builder::create_function_stmt::build_function_or_procedure(
457 &s,
458 directives.schema.as_ref(),
459 &location,
460 )?;
461 let builder::create_function_stmt::Routine::Function(f) = routine else {
462 return Err(ParseError::Structural {
463 location,
464 message: "internal error: expected Function from non-procedure stmt".into(),
465 });
466 };
467 let arg_sig = f
468 .args
469 .iter()
470 .filter(|a| {
471 matches!(
472 a.mode,
473 crate::ir::function::ArgMode::In
474 | crate::ir::function::ArgMode::InOut
475 | crate::ir::function::ArgMode::Variadic
476 )
477 })
478 .map(|a| a.ty.render_sql())
479 .collect::<Vec<_>>()
480 .join(",");
481 let key = format!("functions.{}({arg_sig})", f.qname);
482 if let Some(prior) = locations.get(&key) {
483 return Err(ParseError::DuplicateObject {
484 qname: key,
485 first: prior.clone(),
486 second: location,
487 });
488 }
489 locations.insert(key, location.clone());
490 catalog.functions.push(f);
491 }
492 Statement::CreateProcedure(s) => {
493 let routine = builder::create_function_stmt::build_function_or_procedure(
494 &s,
495 directives.schema.as_ref(),
496 &location,
497 )?;
498 let builder::create_function_stmt::Routine::Procedure(p) = routine else {
499 return Err(ParseError::Structural {
500 location,
501 message: "internal error: expected Procedure from procedure stmt".into(),
502 });
503 };
504 let key = format!("procedures.{}", p.qname);
510 if let Some(prior) = locations.get(&key) {
511 return Err(ParseError::DuplicateObject {
512 qname: key,
513 first: prior.clone(),
514 second: location,
515 });
516 }
517 locations.insert(key, location.clone());
518 catalog.procedures.push(p);
519 }
520 Statement::CreateExtension(s) => {
521 let ext = builder::create_extension_stmt::build_extension(&s, &location)?;
522 let key = format!("extensions.{}", ext.name);
523 if let Some(prior) = locations.get(&key) {
524 return Err(ParseError::DuplicateObject {
525 qname: key,
526 first: prior.clone(),
527 second: location,
528 });
529 }
530 locations.insert(key, location.clone());
531 catalog.extensions.push(ext);
532 }
533 Statement::CreateTrigger(s) => {
534 let trigger = builder::create_trigger_stmt::build_trigger(&s, &location)?;
535 let key = format!("triggers.{}", trigger.qname);
536 if let Some(prior) = locations.get(&key) {
537 return Err(ParseError::DuplicateObject {
538 qname: key,
539 first: prior.clone(),
540 second: location,
541 });
542 }
543 locations.insert(key, location.clone());
544 catalog.triggers.push(trigger);
545 }
546 Statement::AlterTableAttachPartition(s) => {
547 let attach = builder::alter_table_attach_partition::build_attach_partition(
548 &s,
549 directives.schema.as_ref(),
550 &location,
551 )?;
552 let child_table = catalog
553 .tables
554 .iter_mut()
555 .find(|t| t.qname == attach.child)
556 .ok_or_else(|| ParseError::Structural {
557 location: location.clone(),
558 message: format!(
559 "ATTACH PARTITION {} must follow its CREATE TABLE statement",
560 attach.child
561 ),
562 })?;
563 if child_table.partition_of.is_some() {
564 return Err(ParseError::Structural {
565 location,
566 message: format!("table {} already declared as a partition", attach.child),
567 });
568 }
569 child_table.partition_of = Some(attach.partition_of);
570 }
571 Statement::Grant(s) => {
572 builder::grants::apply(&s, catalog, &location)?;
573 }
574 Statement::AlterOwner(s) => {
575 builder::owner_stmt::apply(&s, catalog, &location)?;
576 }
577 Statement::AlterDefaultPrivileges(s) => {
578 builder::default_privileges::apply(&s, catalog, &location)?;
579 }
580 Statement::CreatePolicy(s) => {
581 builder::policy_stmt::apply(&s, catalog, &location)?;
582 }
583 Statement::CreatePublication(s) => {
584 builder::publication_stmt::parse_create_publication(&s, location, publications)?;
585 }
586 Statement::AlterPublication(s) => {
587 builder::publication_stmt::parse_alter_publication(&s, location, publications)?;
588 }
589 Statement::CreateSubscription(s) => {
590 builder::subscription_stmt::parse_create_subscription(&s, location, subscriptions)?;
591 }
592 Statement::AlterSubscription(s) => {
593 builder::subscription_stmt::parse_alter_subscription(&s, location, subscriptions)?;
594 }
595 }
596 }
597 Ok(())
598}
599
600fn stmt_location(path: &Path, contents: &str, byte_offset: i32) -> SourceLocation {
602 let offset = usize::try_from(byte_offset).unwrap_or(0);
603 let mut line = 1usize;
604 let mut col = 1usize;
605 for (i, c) in contents.char_indices() {
606 if i >= offset {
607 break;
608 }
609 if c == '\n' {
610 line += 1;
611 col = 1;
612 } else {
613 col += 1;
614 }
615 }
616 SourceLocation::new(path.to_path_buf(), line, col)
617}
618
619fn translate_canonicalize_error(
620 e: IrError,
621 locations: &HashMap<String, SourceLocation>,
622) -> ParseError {
623 if let IrError::InvalidIdentifier(msg) = &e {
624 if let Some(rest) = msg.strip_prefix("duplicate ")
626 && let Some((_, qname)) = rest.split_once(": ")
627 && let Some(loc) = locations.get(qname)
628 {
629 return ParseError::DuplicateObject {
630 qname: qname.to_string(),
631 first: loc.clone(),
632 second: loc.clone(),
633 };
634 }
635 }
636 let placeholder = SourceLocation::new(PathBuf::new(), 0, 0);
637 ParseError::Ir {
638 location: placeholder,
639 source: e,
640 }
641}
642
643#[cfg(test)]
645pub(crate) fn smoke_parse(sql: &str) -> Result<pg_query::ParseResult, pg_query::Error> {
646 pg_query::parse(sql)
647}
648
649#[cfg(test)]
650mod tests {
651 use super::*;
652
653 #[test]
654 fn pg_query_round_trips_a_create_table() {
655 let sql = "CREATE TABLE app.users (id integer);";
656 let result = smoke_parse(sql).expect("pg_query parses");
657 assert!(!result.protobuf.stmts.is_empty());
659 }
660
661 #[test]
662 fn pg_query_reports_syntax_errors() {
663 let sql = "CREATE TABLE !bad!;";
664 assert!(smoke_parse(sql).is_err());
665 }
666}