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