diff --git a/bin/wasm-build b/bin/wasm-build index 8a30ab46b292d..b374da47ba61b 100755 --- a/bin/wasm-build +++ b/bin/wasm-build @@ -29,7 +29,7 @@ Builds a WASM32 target and NPM metadata for the specified crate fi crate=$1 -exec "$(dirname "$0")"/xcompile \ +"$(dirname "$0")"/xcompile \ tool \ --no-name-target-prefix \ wasm-pack \ @@ -39,3 +39,9 @@ exec "$(dirname "$0")"/xcompile \ "$crate" \ -- \ --no-default-features + +test_script="$crate/test.mjs" +if [[ -f "$test_script" ]] +then + node --experimental-wasm-modules "$test_script" +fi diff --git a/misc/wasm/src/sql-pretty-wasm/README.md b/misc/wasm/src/sql-pretty-wasm/README.md index 854b894a6f9d8..bcc33ff6becec 100644 --- a/misc/wasm/src/sql-pretty-wasm/README.md +++ b/misc/wasm/src/sql-pretty-wasm/README.md @@ -1,3 +1,12 @@ # Materialize SQL pretty printer Pretty print a SQL string. + +## Test + +After building the package with `bin/wasm-build misc/wasm/src/sql-pretty-wasm`, +run: + +```shell +node --experimental-wasm-modules misc/wasm/src/sql-pretty-wasm/test.mjs +``` diff --git a/misc/wasm/src/sql-pretty-wasm/src/lib.rs b/misc/wasm/src/sql-pretty-wasm/src/lib.rs index 8d69335cfd4f9..6296a9462a619 100644 --- a/misc/wasm/src/sql-pretty-wasm/src/lib.rs +++ b/misc/wasm/src/sql-pretty-wasm/src/lib.rs @@ -23,6 +23,92 @@ use wasm_bindgen::prelude::*; static ALLOCATOR: LockedAllocator = LockedAllocator::new(FreeListAllocator::new()); +#[wasm_bindgen(typescript_custom_section)] +const TS_TYPES: &str = r#" +export type PrettyFormatMode = "simple" | "simpleRedacted" | "stable"; + +export interface PrettyConfig { + width?: number; + indent?: number; + formatMode?: PrettyFormatMode; +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "PrettyConfig")] + pub type JsPrettyConfig; + + #[wasm_bindgen(method, getter, structural)] + fn width(this: &JsPrettyConfig) -> JsValue; + + #[wasm_bindgen(method, getter, structural)] + fn indent(this: &JsPrettyConfig) -> JsValue; + + #[wasm_bindgen(method, getter, structural, js_name = formatMode)] + fn format_mode(this: &JsPrettyConfig) -> JsValue; +} + +fn number(value: JsValue, name: &str) -> Result, JsError> { + if value.is_undefined() || value.is_null() { + return Ok(None); + } + + let value = value + .as_f64() + .ok_or_else(|| JsError::new(&format!("{name} must be a number")))?; + if value.is_finite() && value.fract() == 0.0 { + Ok(Some(value)) + } else { + Err(JsError::new(&format!("{name} must be an integer"))) + } +} + +fn width(value: JsValue) -> Result { + match number(value, "width")? { + None => Ok(mz_sql_pretty::DEFAULT_WIDTH), + Some(width) if width >= 0.0 && width <= usize::MAX as f64 => Ok(width as usize), + Some(_) => Err(JsError::new("width is out of range")), + } +} + +fn indent(value: JsValue) -> Result { + match number(value, "indent")? { + None => Ok(mz_sql_pretty::DEFAULT_INDENT), + Some(indent) if indent >= 0.0 && indent <= isize::MAX as f64 => Ok(indent as isize), + Some(_) => Err(JsError::new("indent is out of range")), + } +} + +fn format_mode(format_mode: JsValue) -> Result { + if format_mode.is_undefined() || format_mode.is_null() { + return Ok(mz_sql_pretty::FormatMode::Simple); + } + + let Some(format_mode) = format_mode.as_string() else { + return Err(JsError::new("formatMode must be a string")); + }; + + match format_mode.as_str() { + "simple" | "Simple" => Ok(mz_sql_pretty::FormatMode::Simple), + "simpleRedacted" | "SimpleRedacted" => Ok(mz_sql_pretty::FormatMode::SimpleRedacted), + "stable" | "Stable" => Ok(mz_sql_pretty::FormatMode::Stable), + _ => Err(JsError::new(&format!("invalid formatMode: {format_mode}"))), + } +} + +fn pretty_config(config: &JsPrettyConfig) -> Result { + let width = width(config.width())?; + let indent = indent(config.indent())?; + let format_mode = format_mode(config.format_mode())?; + + Ok(mz_sql_pretty::PrettyConfig { + width, + indent, + format_mode, + }) +} + /// Pretty prints one SQL query. /// /// Returns the pretty-printed query at the specified maximum target width. Returns an error if the @@ -32,6 +118,16 @@ pub fn pretty_str(query: &str, width: usize) -> Result { mz_sql_pretty::pretty_str_simple(query, width).map_err(|e| JsError::new(&e.to_string())) } +/// Pretty prints one SQL query with the specified config. +/// +/// Returns the pretty-printed query at the specified maximum target width, indent width, and format +/// mode. Returns an error if the SQL query is not parseable or if the config is invalid. +#[wasm_bindgen(js_name = prettyStrConfig)] +pub fn pretty_str_config(query: &str, config: &JsPrettyConfig) -> Result { + mz_sql_pretty::pretty_str(query, pretty_config(config)?) + .map_err(|e| JsError::new(&e.to_string())) +} + /// Pretty prints many SQL queries. /// /// Returns the list of pretty-printed queries at the specified maximum target width. Returns an @@ -43,3 +139,15 @@ pub fn pretty_strs(queries: &str, width: usize) -> Result, JsError> .into_iter() .collect()) } + +/// Pretty prints many SQL queries with the specified config. +/// +/// Returns the list of pretty-printed queries at the specified maximum target width, indent width, +/// and format mode. Returns an error if any SQL query is not parseable or if the config is invalid. +#[wasm_bindgen(js_name = prettyStrsConfig)] +pub fn pretty_strs_config(queries: &str, config: &JsPrettyConfig) -> Result, JsError> { + Ok(mz_sql_pretty::pretty_strs(queries, pretty_config(config)?) + .map_err(|e| JsError::new(&e.to_string()))? + .into_iter() + .collect()) +} diff --git a/misc/wasm/src/sql-pretty-wasm/test.mjs b/misc/wasm/src/sql-pretty-wasm/test.mjs new file mode 100644 index 0000000000000..9a8264c908200 --- /dev/null +++ b/misc/wasm/src/sql-pretty-wasm/test.mjs @@ -0,0 +1,55 @@ +#!/usr/bin/env node + +import assert from "node:assert/strict"; + +if (!process.execArgv.includes("--experimental-wasm-modules")) { + console.error( + "Run with: node --experimental-wasm-modules misc/wasm/src/sql-pretty-wasm/test.mjs", + ); + process.exit(1); +} + +const { prettyStrConfig, prettyStrsConfig } = await import( + "./pkg/mz_sql_pretty_wasm.js" +); + +function assertError(fn, message) { + assert.throws(fn, (error) => error instanceof Error && error.message === message); +} + +assert.equal( + prettyStrConfig("CREATE TABLE t (a int, b int)", { width: 1, indent: 2 }), + "CREATE TABLE\n t\n (\n a int4,\n b int4\n );", +); + +assert.deepEqual( + prettyStrsConfig("SELECT 1; SELECT 2", { width: 100, indent: 2 }), + ["SELECT 1;", "SELECT 2;"], +); + +assertError( + () => prettyStrConfig("SELECT 1", { indent: -1 }), + "indent is out of range", +); +assertError( + () => prettyStrConfig("SELECT 1", { indent: 1.5 }), + "indent must be an integer", +); +assertError( + () => prettyStrConfig("SELECT 1", { indent: "2" }), + "indent must be a number", +); +assertError( + () => prettyStrConfig("SELECT 1", { width: "100" }), + "width must be a number", +); +assertError( + () => prettyStrConfig("SELECT 1", { formatMode: "bogus" }), + "invalid formatMode: bogus", +); +assertError( + () => prettyStrConfig("SELECT 1", { formatMode: 1 }), + "formatMode must be a string", +); + +console.log("sql-pretty-wasm test passed"); diff --git a/src/expr/src/scalar/func.rs b/src/expr/src/scalar/func.rs index 28d691db7cfba..0def421e38380 100644 --- a/src/expr/src/scalar/func.rs +++ b/src/expr/src/scalar/func.rs @@ -46,7 +46,7 @@ use mz_repr::{ ArrayRustType, Datum, DatumList, DatumMap, ExcludeNull, FromDatum, InputDatumType, Row, RowArena, SqlScalarType, strconv, }; -use mz_sql_parser::ast::display::{AstDisplay, FormatMode}; +use mz_sql_parser::ast::display::AstDisplay; use mz_sql_pretty::{PrettyConfig, pretty_str}; use num::traits::CheckedNeg; @@ -2346,7 +2346,7 @@ fn pretty_sql<'a>(sql: &str, width: i32, temp_storage: &'a RowArena) -> Result<& sql, PrettyConfig { width, - format_mode: FormatMode::Simple, + ..Default::default() }, ) .map_err(|e| EvalError::PrettyError(e.to_string().into()))?; diff --git a/src/sql-pretty/src/doc.rs b/src/sql-pretty/src/doc.rs index de647b88194e2..94b334724bec3 100644 --- a/src/sql-pretty/src/doc.rs +++ b/src/sql-pretty/src/doc.rs @@ -14,11 +14,8 @@ use mz_sql_parser::ast::display::{AstDisplay, escape_single_quote_string}; use mz_sql_parser::ast::*; use pretty::{Doc, RcDoc}; -use crate::util::{ - bracket, bracket_doc, comma_separate, comma_separated, intersperse_line_nest, nest, - nest_comma_separate, nest_title, title_comma_separate, -}; -use crate::{Pretty, TAB}; +use crate::Pretty; +use crate::util::{comma_separate, comma_separated}; impl Pretty { // Use when we don't know what to do. @@ -56,36 +53,33 @@ impl Pretty { names.extend(v.col_names.iter().map(|name| self.doc_display_pass(name))); names.extend(v.key_constraint.iter().map(|kc| self.doc_display_pass(kc))); if !names.is_empty() { - doc = nest(doc, bracket("(", comma_separated(names), ")")); + doc = self.nest(doc, self.bracket("(", comma_separated(names), ")")); } - docs.push(nest_title(title, doc)); + docs.push(self.nest_title(title, doc)); if let Some(cluster) = &v.in_cluster { - docs.push(nest_title("IN CLUSTER", self.doc_display_pass(cluster))); + docs.push(self.nest_title("IN CLUSTER", self.doc_display_pass(cluster))); } - docs.push(nest_title("FROM", self.doc_display_pass(&v.connection))); + docs.push(self.nest_title("FROM", self.doc_display_pass(&v.connection))); if let Some(format) = &v.format { docs.push(self.doc_format_specifier(format)); } if !v.include_metadata.is_empty() { - docs.push(nest_title( + docs.push(self.nest_title( "INCLUDE", comma_separate(|im| self.doc_display_pass(im), &v.include_metadata), )); } if let Some(envelope) = &v.envelope { - docs.push(nest_title("ENVELOPE", self.doc_display_pass(envelope))); + docs.push(self.nest_title("ENVELOPE", self.doc_display_pass(envelope))); } if let Some(references) = &v.external_references { docs.push(self.doc_external_references(references)); } if let Some(progress) = &v.progress_subsource { - docs.push(nest_title( - "EXPOSE PROGRESS AS", - self.doc_display_pass(progress), - )); + docs.push(self.nest_title("EXPOSE PROGRESS AS", self.doc_display_pass(progress))); } if !v.with_options.is_empty() { - docs.push(bracket( + docs.push(self.bracket( "WITH (", comma_separate(|wo| self.doc_display_pass(wo), &v.with_options), ")", @@ -109,20 +103,17 @@ impl Pretty { if v.if_not_exists { title.push_str(" IF NOT EXISTS"); } - docs.push(nest_title(title, self.doc_display_pass(&v.name))); + docs.push(self.nest_title(title, self.doc_display_pass(&v.name))); // IN CLUSTER (only for sources, not tables) if !v.is_table { if let Some(cluster) = &v.in_cluster { - docs.push(nest_title("IN CLUSTER", self.doc_display_pass(cluster))); + docs.push(self.nest_title("IN CLUSTER", self.doc_display_pass(cluster))); } } docs.push(RcDoc::text("FROM WEBHOOK")); - docs.push(nest_title( - "BODY FORMAT", - self.doc_display_pass(&v.body_format), - )); + docs.push(self.nest_title("BODY FORMAT", self.doc_display_pass(&v.body_format))); if !v.include_headers.mappings.is_empty() || v.include_headers.column.is_some() { let mut header_docs = Vec::new(); @@ -137,7 +128,7 @@ impl Pretty { if filters.is_empty() { header_docs.push(RcDoc::text("INCLUDE HEADERS")); } else { - header_docs.push(bracket( + header_docs.push(self.bracket( "INCLUDE HEADERS (", comma_separate(|f| self.doc_display_pass(f), filters), ")", @@ -177,14 +168,14 @@ impl Pretty { } if !with_items.is_empty() { - inner.push(bracket("WITH (", comma_separated(with_items), ")")); + inner.push(self.bracket("WITH (", comma_separated(with_items), ")")); inner.push(RcDoc::line()); } } inner.push(self.doc_display_pass(&v.using)); - bracket_doc( + self.bracket_doc( RcDoc::text("CHECK ("), RcDoc::concat(inner), RcDoc::text(")"), @@ -213,15 +204,15 @@ impl Pretty { col_items.extend(v.columns.iter().map(|c| self.doc_display_pass(c))); col_items.extend(v.constraints.iter().map(|c| self.doc_display_pass(c))); - let table_def = nest( + let table_def = self.nest( self.doc_display_pass(&v.name), - bracket("(", comma_separated(col_items), ")"), + self.bracket("(", comma_separated(col_items), ")"), ); - docs.push(nest_title(title, table_def)); + docs.push(self.nest_title(title, table_def)); // WITH options if !v.with_options.is_empty() { - docs.push(bracket( + docs.push(self.bracket( "WITH (", comma_separate(|wo| self.doc_display_pass(wo), &v.with_options), ")", @@ -266,18 +257,18 @@ impl Pretty { items.extend(v.constraints.iter().map(|c| self.doc_display_pass(c))); if !items.is_empty() { - table_def = nest(table_def, bracket("(", comma_separated(items), ")")); + table_def = self.nest(table_def, self.bracket("(", comma_separated(items), ")")); } } - docs.push(nest_title(title, table_def)); + docs.push(self.nest_title(title, table_def)); // FROM SOURCE - let mut from_source = nest_title("FROM SOURCE", self.doc_display_pass(&v.source)); + let mut from_source = self.nest_title("FROM SOURCE", self.doc_display_pass(&v.source)); if let Some(reference) = &v.external_reference { - from_source = nest( + from_source = self.nest( from_source, - bracket("(REFERENCE = ", self.doc_display_pass(reference), ")"), + self.bracket("(REFERENCE = ", self.doc_display_pass(reference), ")"), ); } docs.push(from_source); @@ -287,18 +278,18 @@ impl Pretty { } if !v.include_metadata.is_empty() { - docs.push(nest_title( + docs.push(self.nest_title( "INCLUDE", comma_separate(|im| self.doc_display_pass(im), &v.include_metadata), )); } if let Some(envelope) = &v.envelope { - docs.push(nest_title("ENVELOPE", self.doc_display_pass(envelope))); + docs.push(self.nest_title("ENVELOPE", self.doc_display_pass(envelope))); } if !v.with_options.is_empty() { - docs.push(bracket( + docs.push(self.bracket( "WITH (", comma_separate(|wo| self.doc_display_pass(wo), &v.with_options), ")", @@ -318,14 +309,14 @@ impl Pretty { if v.if_not_exists { title.push_str(" IF NOT EXISTS"); } - docs.push(nest_title(title, self.doc_display_pass(&v.name))); + docs.push(self.nest_title(title, self.doc_display_pass(&v.name))); - let connection_with_values = nest( + let connection_with_values = self.nest( RcDoc::concat([ RcDoc::text("TO "), self.doc_display_pass(&v.connection_type), ]), - bracket( + self.bracket( "(", comma_separate(|val| self.doc_display_pass(val), &v.values), ")", @@ -334,7 +325,7 @@ impl Pretty { docs.push(connection_with_values); if !v.with_options.is_empty() { - docs.push(bracket( + docs.push(self.bracket( "WITH (", comma_separate(|wo| self.doc_display_pass(wo), &v.with_options), ")", @@ -357,32 +348,32 @@ impl Pretty { } if let Some(name) = &v.name { - docs.push(nest_title(title, self.doc_display_pass(name))); + docs.push(self.nest_title(title, self.doc_display_pass(name))); } else { docs.push(RcDoc::text(title)); } if let Some(cluster) = &v.in_cluster { - docs.push(nest_title("IN CLUSTER", self.doc_display_pass(cluster))); + docs.push(self.nest_title("IN CLUSTER", self.doc_display_pass(cluster))); } - docs.push(nest_title("FROM", self.doc_display_pass(&v.from))); - docs.push(nest_title("INTO", self.doc_display_pass(&v.connection))); + docs.push(self.nest_title("FROM", self.doc_display_pass(&v.from))); + docs.push(self.nest_title("INTO", self.doc_display_pass(&v.connection))); if let Some(format) = &v.format { docs.push(self.doc_format_specifier(format)); } if let Some(envelope) = &v.envelope { - docs.push(nest_title("ENVELOPE", self.doc_display_pass(envelope))); + docs.push(self.nest_title("ENVELOPE", self.doc_display_pass(envelope))); } if let Some(mode) = &v.mode { - docs.push(nest_title("MODE", self.doc_display_pass(mode))); + docs.push(self.nest_title("MODE", self.doc_display_pass(mode))); } if !v.with_options.is_empty() { - docs.push(bracket( + docs.push(self.bracket( "WITH (", comma_separate(|wo| self.doc_display_pass(wo), &v.with_options), ")", @@ -409,20 +400,20 @@ impl Pretty { col_items.extend(v.columns.iter().map(|c| self.doc_display_pass(c))); col_items.extend(v.constraints.iter().map(|c| self.doc_display_pass(c))); - let table_def = nest( + let table_def = self.nest( self.doc_display_pass(&v.name), - bracket("(", comma_separated(col_items), ")"), + self.bracket("(", comma_separated(col_items), ")"), ); - docs.push(nest_title(title, table_def)); + docs.push(self.nest_title(title, table_def)); // OF SOURCE if let Some(of_source) = &v.of_source { - docs.push(nest_title("OF SOURCE", self.doc_display_pass(of_source))); + docs.push(self.nest_title("OF SOURCE", self.doc_display_pass(of_source))); } // WITH options if !v.with_options.is_empty() { - docs.push(bracket( + docs.push(self.bracket( "WITH (", comma_separate(|wo| self.doc_display_pass(wo), &v.with_options), ")", @@ -439,11 +430,11 @@ impl Pretty { let mut docs = Vec::new(); // CREATE CLUSTER name - docs.push(nest_title("CREATE CLUSTER", self.doc_display_pass(&v.name))); + docs.push(self.nest_title("CREATE CLUSTER", self.doc_display_pass(&v.name))); // OPTIONS (...) if !v.options.is_empty() { - docs.push(bracket( + docs.push(self.bracket( "(", comma_separate(|o| self.doc_display_pass(o), &v.options), ")", @@ -452,7 +443,7 @@ impl Pretty { // FEATURES (...) if !v.features.is_empty() { - docs.push(bracket( + docs.push(self.bracket( "FEATURES (", comma_separate(|f| self.doc_display_pass(f), &v.features), ")", @@ -474,10 +465,10 @@ impl Pretty { RcDoc::text("."), self.doc_display_pass(&v.definition.name), ]); - docs.push(nest_title("CREATE CLUSTER REPLICA", replica_name)); + docs.push(self.nest_title("CREATE CLUSTER REPLICA", replica_name)); // OPTIONS (...) - docs.push(bracket( + docs.push(self.bracket( "(", comma_separate(|o| self.doc_display_pass(o), &v.definition.options), ")", @@ -492,9 +483,9 @@ impl Pretty { ) -> RcDoc<'a> { let docs = vec![ // CREATE NETWORK POLICY name - nest_title("CREATE NETWORK POLICY", self.doc_display_pass(&v.name)), + self.nest_title("CREATE NETWORK POLICY", self.doc_display_pass(&v.name)), // OPTIONS (...) - bracket( + self.bracket( "(", comma_separate(|o| self.doc_display_pass(o), &v.options), ")", @@ -521,21 +512,21 @@ impl Pretty { } if let Some(name) = &v.name { - docs.push(nest_title(title, self.doc_display_pass(name))); + docs.push(self.nest_title(title, self.doc_display_pass(name))); } else { docs.push(RcDoc::text(title)); } // IN CLUSTER if let Some(cluster) = &v.in_cluster { - docs.push(nest_title("IN CLUSTER", self.doc_display_pass(cluster))); + docs.push(self.nest_title("IN CLUSTER", self.doc_display_pass(cluster))); } // ON table_name [(key_parts)] let on_clause = if let Some(key_parts) = &v.key_parts { - nest( + self.nest( self.doc_display_pass(&v.on_name), - bracket( + self.bracket( "(", comma_separate(|k| self.doc_display_pass(k), key_parts), ")", @@ -544,11 +535,11 @@ impl Pretty { } else { self.doc_display_pass(&v.on_name) }; - docs.push(nest_title("ON", on_clause)); + docs.push(self.nest_title("ON", on_clause)); // WITH options if !v.with_options.is_empty() { - docs.push(bracket( + docs.push(self.bracket( "WITH (", comma_separate(|wo| self.doc_display_pass(wo), &v.with_options), ")", @@ -560,11 +551,13 @@ impl Pretty { fn doc_format_specifier(&self, v: &FormatSpecifier) -> RcDoc<'_> { match v { - FormatSpecifier::Bare(format) => nest_title("FORMAT", self.doc_display_pass(format)), + FormatSpecifier::Bare(format) => { + self.nest_title("FORMAT", self.doc_display_pass(format)) + } FormatSpecifier::KeyValue { key, value } => { let docs = vec![ - nest_title("KEY FORMAT", self.doc_display_pass(key)), - nest_title("VALUE FORMAT", self.doc_display_pass(value)), + self.nest_title("KEY FORMAT", self.doc_display_pass(key)), + self.nest_title("VALUE FORMAT", self.doc_display_pass(value)), ]; RcDoc::intersperse(docs, Doc::line()).group() } @@ -573,12 +566,12 @@ impl Pretty { fn doc_external_references<'a>(&'a self, v: &'a ExternalReferences) -> RcDoc<'a> { match v { - ExternalReferences::SubsetTables(subsources) => bracket( + ExternalReferences::SubsetTables(subsources) => self.bracket( "FOR TABLES (", comma_separate(|s| self.doc_display_pass(s), subsources), ")", ), - ExternalReferences::SubsetSchemas(schemas) => bracket( + ExternalReferences::SubsetSchemas(schemas) => self.bracket( "FOR SCHEMAS (", comma_separate(|s| self.doc_display_pass(s), schemas), ")", @@ -592,8 +585,8 @@ impl Pretty { CopyRelation::Named { name, columns } => { let mut relation = self.doc_display_pass(name); if !columns.is_empty() { - relation = bracket_doc( - nest(relation, RcDoc::text("(")), + relation = self.bracket_doc( + self.nest(relation, RcDoc::text("(")), comma_separate(|c| self.doc_display_pass(c), columns), RcDoc::text(")"), RcDoc::line_(), @@ -601,8 +594,12 @@ impl Pretty { } RcDoc::concat([RcDoc::text("COPY "), relation]) } - CopyRelation::Select(query) => bracket("COPY (", self.doc_select_statement(query), ")"), - CopyRelation::Subscribe(query) => bracket("COPY (", self.doc_subscribe(query), ")"), + CopyRelation::Select(query) => { + self.bracket("COPY (", self.doc_select_statement(query), ")") + } + CopyRelation::Subscribe(query) => { + self.bracket("COPY (", self.doc_subscribe(query), ")") + } }; let mut docs = vec![ relation, @@ -613,7 +610,7 @@ impl Pretty { ]), ]; if !v.options.is_empty() { - docs.push(bracket( + docs.push(self.bracket( "WITH (", comma_separate(|o| self.doc_display_pass(o), &v.options), ")", @@ -627,12 +624,16 @@ impl Pretty { v: &'a SubscribeStatement, ) -> RcDoc<'a> { let doc = match &v.relation { - SubscribeRelation::Name(name) => nest_title("SUBSCRIBE", self.doc_display_pass(name)), - SubscribeRelation::Query(query) => bracket("SUBSCRIBE (", self.doc_query(query), ")"), + SubscribeRelation::Name(name) => { + self.nest_title("SUBSCRIBE", self.doc_display_pass(name)) + } + SubscribeRelation::Query(query) => { + self.bracket("SUBSCRIBE (", self.doc_query(query), ")") + } }; let mut docs = vec![doc]; if !v.options.is_empty() { - docs.push(bracket( + docs.push(self.bracket( "WITH (", comma_separate(|o| self.doc_display_pass(o), &v.options), ")", @@ -642,25 +643,25 @@ impl Pretty { docs.push(self.doc_as_of(as_of)); } if let Some(up_to) = &v.up_to { - docs.push(nest_title("UP TO", self.doc_expr(up_to))); + docs.push(self.nest_title("UP TO", self.doc_expr(up_to))); } match &v.output { SubscribeOutput::Diffs => {} SubscribeOutput::WithinTimestampOrderBy { order_by } => { - docs.push(nest_title( + docs.push(self.nest_title( "WITHIN TIMESTAMP ORDER BY ", comma_separate(|o| self.doc_order_by_expr(o), order_by), )); } SubscribeOutput::EnvelopeUpsert { key_columns } => { - docs.push(bracket( + docs.push(self.bracket( "ENVELOPE UPSERT (KEY (", comma_separate(|kc| self.doc_display_pass(kc), key_columns), "))", )); } SubscribeOutput::EnvelopeDebezium { key_columns } => { - docs.push(bracket( + docs.push(self.bracket( "ENVELOPE DEBEZIUM (KEY (", comma_separate(|kc| self.doc_display_pass(kc), key_columns), "))", @@ -675,7 +676,7 @@ impl Pretty { AsOf::At(expr) => ("AS OF", expr), AsOf::AtLeast(expr) => ("AS OF AT LEAST", expr), }; - nest_title(title, self.doc_expr(expr)) + self.nest_title(title, self.doc_expr(expr)) } pub(crate) fn doc_create_view<'a, T: AstInfo>( @@ -698,7 +699,7 @@ impl Pretty { }, ))); docs.push(self.doc_view_definition(&v.definition)); - intersperse_line_nest(docs) + self.intersperse_line_nest(docs) } pub(crate) fn doc_create_materialized_view<'a, T: AstInfo>( @@ -726,7 +727,7 @@ impl Pretty { v.name, ))); if !v.columns.is_empty() { - docs.push(bracket( + docs.push(self.bracket( "(", comma_separate(|c| self.doc_display_pass(c), &v.columns), ")", @@ -761,19 +762,19 @@ impl Pretty { (None, None) => {} } if !v.with_options.is_empty() { - docs.push(bracket( + docs.push(self.bracket( "WITH (", comma_separate(|wo| self.doc_display_pass(wo), &v.with_options), ")", )); } - docs.push(nest_title("AS", self.doc_query(&v.query))); + docs.push(self.nest_title("AS", self.doc_query(&v.query))); // `AS OF` is internal syntax that follows the query; the generic AstDisplay // emits it, so we must too, otherwise it is silently dropped. if let Some(time) = &v.as_of { docs.push(RcDoc::text(format!("AS OF {time}"))); } - intersperse_line_nest(docs) + self.intersperse_line_nest(docs) } pub(crate) fn doc_create_role<'a>(&'a self, v: &'a CreateRoleStatement) -> RcDoc<'a> { @@ -784,7 +785,7 @@ impl Pretty { for option in &v.options { docs.push(self.doc_role_attribute(option)); } - intersperse_line_nest(docs) + self.intersperse_line_nest(docs) } pub(crate) fn doc_alter_role<'a, T: AstInfo>( @@ -805,7 +806,7 @@ impl Pretty { // AstDisplay is already lossless here. AlterRoleOption::Variable(var) => docs.push(self.doc_display_pass(var)), } - intersperse_line_nest(docs) + self.intersperse_line_nest(docs) } /// Like the generic AstDisplay for `RoleAttribute`, but preserves the `PASSWORD` @@ -825,13 +826,13 @@ impl Pretty { fn doc_view_definition<'a, T: AstInfo>(&'a self, v: &'a ViewDefinition) -> RcDoc<'a> { let mut docs = vec![RcDoc::text(v.name.to_string())]; if !v.columns.is_empty() { - docs.push(bracket( + docs.push(self.bracket( "(", comma_separate(|c| self.doc_display_pass(c), &v.columns), ")", )); } - docs.push(nest_title("AS", self.doc_query(&v.query))); + docs.push(self.nest_title("AS", self.doc_query(&v.query))); RcDoc::intersperse(docs, Doc::line()).group() } @@ -841,7 +842,7 @@ impl Pretty { v.table_name.to_ast_string_simple() ))]; if !v.columns.is_empty() { - first.push(bracket( + first.push(self.bracket( "(", comma_separate(|c| self.doc_display_pass(c), &v.columns), ")", @@ -851,11 +852,11 @@ impl Pretty { InsertSource::Query(query) => self.doc_query(query), InsertSource::DefaultValues => self.doc_display(&v.source, "insert source"), }; - let mut doc = intersperse_line_nest([intersperse_line_nest(first), sources]); + let mut doc = self.intersperse_line_nest([self.intersperse_line_nest(first), sources]); if !v.returning.is_empty() { - doc = nest( + doc = self.nest( doc, - nest_title( + self.nest_title( "RETURNING", comma_separate(|r| self.doc_display_pass(r), &v.returning), ), @@ -870,25 +871,25 @@ impl Pretty { ) -> RcDoc<'a> { let mut doc = self.doc_query(&v.query); if let Some(as_of) = &v.as_of { - doc = intersperse_line_nest([doc, self.doc_as_of(as_of)]); + doc = self.intersperse_line_nest([doc, self.doc_as_of(as_of)]); } doc.group() } fn doc_order_by<'a, T: AstInfo>(&'a self, v: &'a [OrderByExpr]) -> RcDoc<'a> { - title_comma_separate("ORDER BY", |o| self.doc_order_by_expr(o), v) + self.title_comma_separate("ORDER BY", |o| self.doc_order_by_expr(o), v) } fn doc_order_by_expr<'a, T: AstInfo>(&'a self, v: &'a OrderByExpr) -> RcDoc<'a> { let doc = self.doc_expr(&v.expr); let doc = match v.asc { - Some(true) => nest(doc, RcDoc::text("ASC")), - Some(false) => nest(doc, RcDoc::text("DESC")), + Some(true) => self.nest(doc, RcDoc::text("ASC")), + Some(false) => self.nest(doc, RcDoc::text("DESC")), None => doc, }; match v.nulls_last { - Some(true) => nest(doc, RcDoc::text("NULLS LAST")), - Some(false) => nest(doc, RcDoc::text("NULLS FIRST")), + Some(true) => self.nest(doc, RcDoc::text("NULLS LAST")), + Some(false) => self.nest(doc, RcDoc::text("NULLS FIRST")), None => doc, } } @@ -898,21 +899,21 @@ impl Pretty { if !v.ctes.is_empty() { match &v.ctes { CteBlock::Simple(ctes) => { - docs.push(title_comma_separate("WITH", |cte| self.doc_cte(cte), ctes)) + docs.push(self.title_comma_separate("WITH", |cte| self.doc_cte(cte), ctes)) } CteBlock::MutuallyRecursive(mutrec) => { let mut doc = RcDoc::text("WITH MUTUALLY RECURSIVE"); if !mutrec.options.is_empty() { - doc = nest( + doc = self.nest( doc, - bracket( + self.bracket( "(", comma_separate(|o| self.doc_display_pass(o), &mutrec.options), ")", ), ); } - docs.push(nest( + docs.push(self.nest( doc, comma_separate(|c| self.doc_mutually_recursive(c), &mutrec.ctes), )); @@ -925,7 +926,9 @@ impl Pretty { } let offset = if let Some(offset) = &v.offset { - vec![RcDoc::concat([nest_title("OFFSET", self.doc_expr(offset))])] + vec![RcDoc::concat([ + self.nest_title("OFFSET", self.doc_expr(offset)) + ])] } else { vec![] }; @@ -939,7 +942,7 @@ impl Pretty { RcDoc::text(" ROWS WITH TIES"), ])); } else { - docs.push(nest_title("LIMIT", self.doc_expr(&limit.quantity))); + docs.push(self.nest_title("LIMIT", self.doc_expr(&limit.quantity))); docs.extend(offset); } } else { @@ -953,21 +956,21 @@ impl Pretty { RcDoc::concat([ RcDoc::text(format!("{} AS", v.alias)), RcDoc::line(), - bracket("(", self.doc_query(&v.query), ")"), + self.bracket("(", self.doc_query(&v.query), ")"), ]) } fn doc_mutually_recursive<'a, T: AstInfo>(&'a self, v: &'a CteMutRec) -> RcDoc<'a> { let mut docs = Vec::new(); if !v.columns.is_empty() { - docs.push(bracket( + docs.push(self.bracket( "(", comma_separate(|c| self.doc_display_pass(c), &v.columns), ")", )); } - docs.push(bracket("AS (", self.doc_query(&v.query), ")")); - nest( + docs.push(self.bracket("AS (", self.doc_query(&v.query), ")")); + self.nest( self.doc_display_pass(&v.name), RcDoc::intersperse(docs, Doc::line()).group(), ) @@ -976,7 +979,7 @@ impl Pretty { fn doc_set_expr<'a, T: AstInfo>(&'a self, v: &'a SetExpr) -> RcDoc<'a> { match v { SetExpr::Select(v) => self.doc_select(v), - SetExpr::Query(v) => bracket("(", self.doc_query(v), ")"), + SetExpr::Query(v) => self.bracket("(", self.doc_query(v), ")"), SetExpr::SetOperation { op, all, @@ -992,13 +995,13 @@ impl Pretty { RcDoc::line(), self.doc_set_expr(right), ]) - .nest(TAB) + .nest(self.config.indent) .group(), ]) } SetExpr::Values(v) => self.doc_values(v), SetExpr::Show(v) => self.doc_display(v, "SHOW"), - SetExpr::Table(v) => nest(RcDoc::text("TABLE"), self.doc_display_pass(v)), + SetExpr::Table(v) => self.nest(RcDoc::text("TABLE"), self.doc_display_pass(v)), } .group() } @@ -1006,9 +1009,9 @@ impl Pretty { fn doc_values<'a, T: AstInfo>(&'a self, v: &'a Values) -> RcDoc<'a> { let rows = v.0.iter() - .map(|row| bracket("(", comma_separate(|v| self.doc_expr(v), row), ")")); + .map(|row| self.bracket("(", comma_separate(|v| self.doc_expr(v), row), ")")); RcDoc::concat([RcDoc::text("VALUES"), RcDoc::line(), comma_separated(rows)]) - .nest(TAB) + .nest(self.config.indent) .group() } @@ -1017,7 +1020,7 @@ impl Pretty { for j in &v.joins { docs.push(self.doc_join(j)); } - intersperse_line_nest(docs) + self.intersperse_line_nest(docs) } fn doc_join<'a, T: AstInfo>(&'a self, v: &'a Join) -> RcDoc<'a> { @@ -1029,21 +1032,21 @@ impl Pretty { JoinOperator::CrossJoin => return self.doc_display(v, "join operator"), }; let constraint = match constraint { - JoinConstraint::On(expr) => nest_title("ON", self.doc_expr(expr)), + JoinConstraint::On(expr) => self.nest_title("ON", self.doc_expr(expr)), JoinConstraint::Using { columns, alias } => { - let mut doc = bracket( + let mut doc = self.bracket( "USING(", comma_separate(|c| self.doc_display_pass(c), columns), ")", ); if let Some(alias) = alias { - doc = nest(doc, nest_title("AS", self.doc_display_pass(alias))); + doc = self.nest(doc, self.nest_title("AS", self.doc_display_pass(alias))); } doc } JoinConstraint::Natural => return self.doc_display(v, "join constraint"), }; - intersperse_line_nest([ + self.intersperse_line_nest([ RcDoc::text(name), self.doc_table_factor(&v.relation), constraint, @@ -1058,23 +1061,23 @@ impl Pretty { alias, } => { let prefix = if *lateral { "LATERAL (" } else { "(" }; - let mut docs = vec![bracket(prefix, self.doc_query(subquery), ")")]; + let mut docs = vec![self.bracket(prefix, self.doc_query(subquery), ")")]; if let Some(alias) = alias { docs.push(RcDoc::text(format!("AS {}", alias))); } - intersperse_line_nest(docs) + self.intersperse_line_nest(docs) } TableFactor::NestedJoin { join, alias } => { - let mut doc = bracket("(", self.doc_table_with_joins(join), ")"); + let mut doc = self.bracket("(", self.doc_table_with_joins(join), ")"); if let Some(alias) = alias { - doc = nest(doc, RcDoc::text(format!("AS {}", alias))); + doc = self.nest(doc, RcDoc::text(format!("AS {}", alias))); } doc } TableFactor::Table { name, alias } => { let mut doc = self.doc_display_pass(name); if let Some(alias) = alias { - doc = nest(doc, RcDoc::text(format!("AS {}", alias))); + doc = self.nest(doc, RcDoc::text(format!("AS {}", alias))); } doc } @@ -1085,7 +1088,7 @@ impl Pretty { fn doc_distinct<'a, T: AstInfo>(&'a self, v: &'a Distinct) -> RcDoc<'a> { match v { Distinct::EntireRow => RcDoc::text("DISTINCT"), - Distinct::On(cols) => bracket( + Distinct::On(cols) => self.bracket( "DISTINCT ON (", comma_separate(|c| self.doc_expr(c), cols), ")", @@ -1097,38 +1100,26 @@ impl Pretty { let mut docs = vec![]; let mut select = RcDoc::text("SELECT"); if let Some(distinct) = &v.distinct { - select = nest(select, self.doc_distinct(distinct)); + select = self.nest(select, self.doc_distinct(distinct)); } - docs.push(nest_comma_separate( - select, - |s| self.doc_select_item(s), - &v.projection, - )); + docs.push(self.nest_comma_separate(select, |s| self.doc_select_item(s), &v.projection)); if !v.from.is_empty() { - docs.push(title_comma_separate( - "FROM", - |t| self.doc_table_with_joins(t), - &v.from, - )); + docs.push(self.title_comma_separate("FROM", |t| self.doc_table_with_joins(t), &v.from)); } if let Some(selection) = &v.selection { - docs.push(nest_title("WHERE", self.doc_expr(selection))); + docs.push(self.nest_title("WHERE", self.doc_expr(selection))); } if !v.group_by.is_empty() { - docs.push(title_comma_separate( - "GROUP BY", - |e| self.doc_expr(e), - &v.group_by, - )); + docs.push(self.title_comma_separate("GROUP BY", |e| self.doc_expr(e), &v.group_by)); } if let Some(having) = &v.having { - docs.push(nest_title("HAVING", self.doc_expr(having))); + docs.push(self.nest_title("HAVING", self.doc_expr(having))); } if let Some(qualify) = &v.qualify { - docs.push(nest_title("QUALIFY", self.doc_expr(qualify))); + docs.push(self.nest_title("QUALIFY", self.doc_expr(qualify))); } if !v.options.is_empty() { - docs.push(bracket( + docs.push(self.bracket( "OPTIONS (", comma_separate(|o| self.doc_display_pass(o), &v.options), ")", @@ -1142,7 +1133,7 @@ impl Pretty { SelectItem::Expr { expr, alias } => { let mut doc = self.doc_expr(expr); if let Some(alias) = alias { - doc = nest( + doc = self.nest( doc, RcDoc::concat([RcDoc::text("AS "), self.doc_display_pass(alias)]), ); @@ -1161,7 +1152,7 @@ impl Pretty { self.doc_expr(expr1), RcDoc::line(), RcDoc::text(format!("{} ", op)), - self.doc_expr(expr2).nest(TAB), + self.doc_expr(expr2).nest(self.config.indent), ]) } else { RcDoc::concat([RcDoc::text(format!("{} ", op)), self.doc_expr(expr1)]) @@ -1178,15 +1169,15 @@ impl Pretty { docs.push(self.doc_expr(operand)); } for (c, r) in conditions.iter().zip_eq(results) { - let when = nest_title("WHEN", self.doc_expr(c)); - let then = nest_title("THEN", self.doc_expr(r)); - docs.push(nest(when, then)); + let when = self.nest_title("WHEN", self.doc_expr(c)); + let then = self.nest_title("THEN", self.doc_expr(r)); + docs.push(self.nest(when, then)); } if let Some(else_result) = else_result { - docs.push(nest_title("ELSE", self.doc_expr(else_result))); + docs.push(self.nest_title("ELSE", self.doc_expr(else_result))); } - let doc = intersperse_line_nest(docs); - bracket_doc(RcDoc::text("CASE"), doc, RcDoc::text("END"), RcDoc::line()) + let doc = self.intersperse_line_nest(docs); + self.bracket_doc(RcDoc::text("CASE"), doc, RcDoc::text("END"), RcDoc::line()) } Expr::Cast { expr, data_type } => { let doc = self.doc_expr(expr); @@ -1195,32 +1186,32 @@ impl Pretty { RcDoc::text(format!("::{}", data_type.to_ast_string_simple())), ]) } - Expr::Nested(ast) => bracket("(", self.doc_expr(ast), ")"), + Expr::Nested(ast) => self.bracket("(", self.doc_expr(ast), ")"), Expr::Function(fun) => self.doc_function(fun), - Expr::Subquery(ast) => bracket("(", self.doc_query(ast), ")"), + Expr::Subquery(ast) => self.bracket("(", self.doc_query(ast), ")"), Expr::Identifier(_) | Expr::Value(_) | Expr::QualifiedWildcard(_) | Expr::WildcardAccess(_) | Expr::FieldAccess { .. } => self.doc_display_pass(v), - Expr::And { left, right } => bracket_doc( + Expr::And { left, right } => self.bracket_doc( self.doc_expr(left), RcDoc::text("AND"), self.doc_expr(right), RcDoc::line(), ), - Expr::Or { left, right } => bracket_doc( + Expr::Or { left, right } => self.bracket_doc( self.doc_expr(left), RcDoc::text("OR"), self.doc_expr(right), RcDoc::line(), ), - Expr::Exists(s) => bracket("EXISTS (", self.doc_query(s), ")"), + Expr::Exists(s) => self.bracket("EXISTS (", self.doc_query(s), ")"), Expr::IsExpr { expr, negated, construct, - } => bracket_doc( + } => self.bracket_doc( self.doc_expr(expr), RcDoc::text(if *negated { "IS NOT" } else { "IS" }), self.doc_display_pass(construct), @@ -1273,24 +1264,26 @@ impl Pretty { RcDoc::line(), ), Expr::Row { exprs } => { - bracket("ROW(", comma_separate(|e| self.doc_expr(e), exprs), ")") + self.bracket("ROW(", comma_separate(|e| self.doc_expr(e), exprs), ")") } - Expr::NullIf { l_expr, r_expr } => bracket( + Expr::NullIf { l_expr, r_expr } => self.bracket( "NULLIF (", comma_separate(|e| self.doc_expr(e), [&**l_expr, &**r_expr]), ")", ), - Expr::HomogenizingFunction { function, exprs } => bracket( + Expr::HomogenizingFunction { function, exprs } => self.bracket( format!("{function}("), comma_separate(|e| self.doc_expr(e), exprs), ")", ), - Expr::ArraySubquery(s) => bracket("ARRAY(", self.doc_query(s), ")"), - Expr::ListSubquery(s) => bracket("LIST(", self.doc_query(s), ")"), + Expr::ArraySubquery(s) => self.bracket("ARRAY(", self.doc_query(s), ")"), + Expr::ListSubquery(s) => self.bracket("LIST(", self.doc_query(s), ")"), Expr::Array(exprs) => { - bracket("ARRAY[", comma_separate(|e| self.doc_expr(e), exprs), "]") + self.bracket("ARRAY[", comma_separate(|e| self.doc_expr(e), exprs), "]") + } + Expr::List(exprs) => { + self.bracket("LIST[", comma_separate(|e| self.doc_expr(e), exprs), "]") } - Expr::List(exprs) => bracket("LIST[", comma_separate(|e| self.doc_expr(e), exprs), "]"), _ => self.doc_display(v, "expr variant"), } .group() @@ -1320,7 +1313,7 @@ impl Pretty { v.name.to_ast_string_simple(), if v.distinct { "DISTINCT " } else { "" } ); - bracket(name, comma_separate(|e| self.doc_expr(e), args), ")") + self.bracket(name, comma_separate(|e| self.doc_expr(e), args), ")") } } } diff --git a/src/sql-pretty/src/lib.rs b/src/sql-pretty/src/lib.rs index ed7b37c543106..a819d39093752 100644 --- a/src/sql-pretty/src/lib.rs +++ b/src/sql-pretty/src/lib.rs @@ -10,22 +10,34 @@ mod doc; mod util; -use mz_sql_parser::ast::display::FormatMode; use mz_sql_parser::ast::*; use mz_sql_parser::parser::{ParserStatementError, parse_statements}; use pretty::RcDoc; use thiserror::Error; +pub use mz_sql_parser::ast::display::FormatMode; + pub const DEFAULT_WIDTH: usize = 100; -const TAB: isize = 4; +pub const DEFAULT_INDENT: isize = 4; #[derive(Clone, Copy)] pub struct PrettyConfig { pub width: usize, + pub indent: isize, pub format_mode: FormatMode, } +impl Default for PrettyConfig { + fn default() -> Self { + Self { + width: DEFAULT_WIDTH, + indent: DEFAULT_INDENT, + format_mode: FormatMode::Simple, + } + } +} + /// Pretty prints a statement at a width. pub fn to_pretty(stmt: &Statement, config: PrettyConfig) -> String { format!("{};", Pretty { config }.to_doc(stmt).pretty(config.width)) @@ -52,7 +64,7 @@ pub fn pretty_strs_simple(str: &str, width: usize) -> Result, Error> str, PrettyConfig { width, - format_mode: FormatMode::Simple, + ..Default::default() }, ) } @@ -63,7 +75,7 @@ pub fn pretty_str_simple(str: &str, width: usize) -> Result { str, PrettyConfig { width, - format_mode: FormatMode::Simple, + ..Default::default() }, ) } diff --git a/src/sql-pretty/src/util.rs b/src/sql-pretty/src/util.rs index cda422fc9234b..6a2b326c42ab9 100644 --- a/src/sql-pretty/src/util.rs +++ b/src/sql-pretty/src/util.rs @@ -11,49 +11,89 @@ use pretty::{Doc, RcDoc}; -use crate::TAB; +use crate::Pretty; -pub(crate) fn intersperse_line_nest<'a, I>(v: I) -> RcDoc<'a> -where - I: IntoIterator>, -{ - RcDoc::intersperse(v, Doc::line()).nest(TAB).group() -} +impl Pretty { + pub(crate) fn intersperse_line_nest<'a, I>(&self, v: I) -> RcDoc<'a> + where + I: IntoIterator>, + { + RcDoc::intersperse(v, Doc::line()) + .nest(self.config.indent) + .group() + } -pub(crate) fn nest<'a>(title: RcDoc<'a>, v: RcDoc<'a>) -> RcDoc<'a> { - intersperse_line_nest([title, v]) -} + pub(crate) fn nest<'a>(&self, title: RcDoc<'a>, v: RcDoc<'a>) -> RcDoc<'a> { + self.intersperse_line_nest([title, v]) + } -pub(crate) fn nest_title(title: S, v: RcDoc) -> RcDoc -where - S: Into, -{ - nest(RcDoc::text(title.into()), v) -} + pub(crate) fn nest_title<'a, S>(&self, title: S, v: RcDoc<'a>) -> RcDoc<'a> + where + S: Into, + { + self.nest(RcDoc::text(title.into()), v) + } -pub(crate) fn title_comma_separate<'a, F, T, S>(title: S, f: F, v: &'a [T]) -> RcDoc<'a, ()> -where - F: Fn(&'a T) -> RcDoc<'a>, - S: Into, -{ - let title = RcDoc::text(title.into()); - if v.is_empty() { - title - } else { - nest_comma_separate(title, f, v) + pub(crate) fn title_comma_separate<'a, F, T, S>( + &self, + title: S, + f: F, + v: &'a [T], + ) -> RcDoc<'a, ()> + where + F: Fn(&'a T) -> RcDoc<'a>, + S: Into, + { + let title = RcDoc::text(title.into()); + if v.is_empty() { + title + } else { + self.nest_comma_separate(title, f, v) + } } -} -pub(crate) fn nest_comma_separate<'a, F, T: 'a, I>( - title: RcDoc<'a, ()>, - f: F, - v: I, -) -> RcDoc<'a, ()> -where - F: Fn(&'a T) -> RcDoc<'a>, - I: IntoIterator, -{ - nest(title, comma_separate(f, v)) + pub(crate) fn nest_comma_separate<'a, F, T: 'a, I>( + &self, + title: RcDoc<'a, ()>, + f: F, + v: I, + ) -> RcDoc<'a, ()> + where + F: Fn(&'a T) -> RcDoc<'a>, + I: IntoIterator, + { + self.nest(title, comma_separate(f, v)) + } + + pub(crate) fn bracket<'a, A: Into, B: Into>( + &self, + left: A, + d: RcDoc<'a>, + right: B, + ) -> RcDoc<'a> { + self.bracket_doc( + RcDoc::text(left.into()), + d, + RcDoc::text(right.into()), + RcDoc::line_(), + ) + } + + pub(crate) fn bracket_doc<'a>( + &self, + left: RcDoc<'a>, + d: RcDoc<'a>, + right: RcDoc<'a>, + line: RcDoc<'a>, + ) -> RcDoc<'a> { + RcDoc::concat([ + left, + RcDoc::concat([line.clone(), d]).nest(self.config.indent), + line, + right, + ]) + .group() + } } pub(crate) fn comma_separate<'a, F, T: 'a, I>(f: F, v: I) -> RcDoc<'a, ()> @@ -71,27 +111,3 @@ where { RcDoc::intersperse(v, RcDoc::concat([RcDoc::text(","), RcDoc::line()])).group() } - -pub(crate) fn bracket, B: Into>(left: A, d: RcDoc, right: B) -> RcDoc { - bracket_doc( - RcDoc::text(left.into()), - d, - RcDoc::text(right.into()), - RcDoc::line_(), - ) -} - -pub(crate) fn bracket_doc<'a>( - left: RcDoc<'a>, - d: RcDoc<'a>, - right: RcDoc<'a>, - line: RcDoc<'a>, -) -> RcDoc<'a> { - RcDoc::concat([ - left, - RcDoc::concat([line.clone(), d]).nest(TAB), - line, - right, - ]) - .group() -} diff --git a/src/sql-pretty/tests/parser.rs b/src/sql-pretty/tests/parser.rs index 5331e800ebaed..2cdb123f7520f 100644 --- a/src/sql-pretty/tests/parser.rs +++ b/src/sql-pretty/tests/parser.rs @@ -10,10 +10,10 @@ #![cfg(not(target_arch = "wasm32"))] use datadriven::walk; -use mz_sql_parser::ast::display::{AstDisplay, FormatMode}; +use mz_sql_parser::ast::display::AstDisplay; use mz_sql_parser::datadriven_testcase; use mz_sql_parser::parser::{parse_expr, parse_statements}; -use mz_sql_pretty::{Pretty, PrettyConfig, to_pretty}; +use mz_sql_pretty::{Pretty, PrettyConfig, pretty_str, to_pretty}; // Use the parser's datadriven tests to get a comprehensive set of SQL statements. Assert they all // generate identical ASTs when pretty printed. Output the same output as the parser so datadriven @@ -48,7 +48,7 @@ fn verify_pretty_expr(expr: &str) { Pretty { config: PrettyConfig { width: n, - format_mode: FormatMode::Simple + ..Default::default() } } .doc_expr(&original) @@ -61,7 +61,7 @@ fn verify_pretty_expr(expr: &str) { Pretty { config: PrettyConfig { width: n, - format_mode: FormatMode::Simple + ..Default::default() } } .doc_expr(&prettied) @@ -90,7 +90,7 @@ fn verify_pretty_statement(stmt: &str) { &original.ast, PrettyConfig { width, - format_mode: FormatMode::Simple, + ..Default::default() }, ); let prettied = parse_statements(&pretty1) @@ -102,7 +102,7 @@ fn verify_pretty_statement(stmt: &str) { &prettied.ast, PrettyConfig { width, - format_mode: FormatMode::Simple, + ..Default::default() }, ); assert_eq!(pretty1, pretty2); @@ -115,3 +115,21 @@ fn verify_pretty_statement(stmt: &str) { // idents can contain newlines so that's not always possible. } } + +#[mz_ore::test] +fn test_indent_config() { + let pretty = pretty_str( + "CREATE TABLE t (a int, b int)", + PrettyConfig { + width: 1, + indent: 2, + ..Default::default() + }, + ) + .expect("valid SQL"); + + assert_eq!( + pretty, + "CREATE TABLE\n t\n (\n a int4,\n b int4\n );" + ); +} diff --git a/src/sql/src/plan/statement/show.rs b/src/sql/src/plan/statement/show.rs index 0e83e900245aa..8817684305fea 100644 --- a/src/sql/src/plan/statement/show.rs +++ b/src/sql/src/plan/statement/show.rs @@ -1271,12 +1271,12 @@ fn humanize_sql_for_show_create( Ok(mz_sql_pretty::to_pretty( &resolved, PrettyConfig { - width: mz_sql_pretty::DEFAULT_WIDTH, format_mode: if redacted { FormatMode::SimpleRedacted } else { FormatMode::Simple }, + ..Default::default() }, )) }