Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion bin/wasm-build
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand All @@ -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
9 changes: 9 additions & 0 deletions misc/wasm/src/sql-pretty-wasm/README.md
Original file line number Diff line number Diff line change
@@ -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
```
108 changes: 108 additions & 0 deletions misc/wasm/src/sql-pretty-wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,92 @@ use wasm_bindgen::prelude::*;
static ALLOCATOR: LockedAllocator<FreeListAllocator> =
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;
}
Comment on lines +30 to +34
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this legit? The types aren't auto-generated?

"#;

#[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<Option<f64>, 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<usize, JsError> {
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<isize, JsError> {
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<mz_sql_pretty::FormatMode, JsError> {
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<mz_sql_pretty::PrettyConfig, JsError> {
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
Expand All @@ -32,6 +118,16 @@ pub fn pretty_str(query: &str, width: usize) -> Result<String, JsError> {
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<String, JsError> {
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
Expand All @@ -43,3 +139,15 @@ pub fn pretty_strs(queries: &str, width: usize) -> Result<Vec<String>, 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<Vec<String>, JsError> {
Ok(mz_sql_pretty::pretty_strs(queries, pretty_config(config)?)
.map_err(|e| JsError::new(&e.to_string()))?
.into_iter()
.collect())
}
55 changes: 55 additions & 0 deletions misc/wasm/src/sql-pretty-wasm/test.mjs
Original file line number Diff line number Diff line change
@@ -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");
4 changes: 2 additions & 2 deletions src/expr/src/scalar/func.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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()))?;
Expand Down
Loading
Loading