diff --git a/crates/pgls_pretty_print/src/emitter.rs b/crates/pgls_pretty_print/src/emitter.rs index 368c79b23..a854411c2 100644 --- a/crates/pgls_pretty_print/src/emitter.rs +++ b/crates/pgls_pretty_print/src/emitter.rs @@ -25,6 +25,12 @@ pub enum LayoutEvent { #[derive(Debug, Default)] pub struct EventEmitter { pub events: Vec, + /// When set, [`EventEmitter::token`] drops `SEMICOLON` tokens. + /// + /// Used when emitting complete statements as sub-elements of another + /// statement (e.g. the `schema_elts` of a `CREATE SCHEMA`), where the + /// child statement's terminating `;` must not be emitted. + pub no_semicolon: bool, } impl EventEmitter { @@ -33,6 +39,9 @@ impl EventEmitter { } pub fn token(&mut self, token: TokenKind) { + if self.no_semicolon && token == TokenKind::SEMICOLON { + return; + } self.events.push(LayoutEvent::Token(token)); } diff --git a/crates/pgls_pretty_print/src/nodes/create_schema_stmt.rs b/crates/pgls_pretty_print/src/nodes/create_schema_stmt.rs index 356b3e50f..9393e1e27 100644 --- a/crates/pgls_pretty_print/src/nodes/create_schema_stmt.rs +++ b/crates/pgls_pretty_print/src/nodes/create_schema_stmt.rs @@ -6,6 +6,7 @@ use crate::{ }; use super::node_list::emit_space_separated_list; +use crate::emitter::LineType; pub(super) fn emit_create_schema_stmt(e: &mut EventEmitter, n: &CreateSchemaStmt) { e.group_start(GroupKind::CreateSchemaStmt); @@ -36,10 +37,18 @@ pub(super) fn emit_create_schema_stmt(e: &mut EventEmitter, n: &CreateSchemaStmt super::emit_role_spec(e, authrole); } - // Schema elements (nested CREATE statements) + // Schema elements (nested CREATE statements). + // + // Per the PostgreSQL grammar (`OptSchemaEltList: OptSchemaEltList schema_stmt`), + // elements are space-separated and the child statements must not carry their + // own terminating `;` — only the outer `CREATE SCHEMA` does. if !n.schema_elts.is_empty() { - e.space(); + // Soft line so the renderer can wrap when the schema name and the + // first element don't fit on one line. + e.line(LineType::SoftOrSpace); + e.no_semicolon = true; emit_space_separated_list(e, &n.schema_elts, super::emit_node); + e.no_semicolon = false; } e.token(TokenKind::SEMICOLON); diff --git a/crates/pgls_pretty_print/tests/create_schema_elts.rs b/crates/pgls_pretty_print/tests/create_schema_elts.rs new file mode 100644 index 000000000..ea96a9efa --- /dev/null +++ b/crates/pgls_pretty_print/tests/create_schema_elts.rs @@ -0,0 +1,40 @@ +use pgls_pretty_print::{ + emitter::EventEmitter, + nodes::emit_node_enum, + normalize::normalize_ast, + renderer::{RenderConfig, Renderer}, +}; + +/// `CREATE SCHEMA` takes a list of `schema_element`s. Per the PostgreSQL +/// grammar (`OptSchemaEltList: OptSchemaEltList schema_stmt`) the elements are +/// space-separated and must not carry their own terminating `;` — only the +/// outer statement does. +/// +/// Regression test: the emitter used to emit each child statement's `;`, +/// producing invalid SQL like `create schema s1 create table a (f1 int);;`. +#[test] +fn create_schema_with_elements_emits_valid_sql() { + let content = "CREATE SCHEMA s1 CREATE TABLE a (f1 int) CREATE TABLE b (f2 int);"; + + let parsed = pgls_query::parse(content).expect("Failed to parse SQL"); + let mut ast = parsed.into_root().expect("No root node found"); + + let mut emitter = EventEmitter::new(); + emit_node_enum(&ast, &mut emitter); + + let mut output = String::new(); + let config = RenderConfig { + max_line_length: 80, + ..Default::default() + }; + let mut renderer = Renderer::new(&mut output, config); + renderer.render(emitter.events).expect("Failed to render"); + + let parsed_output = pgls_query::parse(&output).expect("Failed to parse formatted SQL"); + let mut parsed_ast = parsed_output.into_root().expect("No root node found"); + + normalize_ast(&mut parsed_ast); + normalize_ast(&mut ast); + + assert_eq!(ast, parsed_ast); +}