From 36b90c5eb25e9f45823d144a7c5e0c3d02d8aae5 Mon Sep 17 00:00:00 2001 From: Josh Pschorr Date: Wed, 9 Oct 2024 16:05:24 -0700 Subject: [PATCH] Add pretty-printing for `Value` (#503) * Move generic `pretty` extensions to `partiql-common` * Add pretty-printing for `Value` --- CHANGELOG.md | 1 + partiql-ast/src/pretty.rs | 371 +++++------------- partiql-common/src/lib.rs | 1 + partiql-common/src/pretty.rs | 300 ++++++++++++++ partiql-conformance-tests/Cargo.toml | 1 + partiql-conformance-tests/tests/mod.rs | 2 +- partiql-value/Cargo.toml | 2 + partiql-value/src/lib.rs | 2 + partiql-value/src/pretty.rs | 128 ++++++ partiql/Cargo.toml | 4 + partiql/tests/pretty.rs | 99 ++++- .../tests/snapshots/pretty__pretty_val.snap | 290 ++++++++++++++ 12 files changed, 921 insertions(+), 280 deletions(-) create mode 100644 partiql-common/src/pretty.rs create mode 100644 partiql-value/src/pretty.rs create mode 100644 partiql/tests/snapshots/pretty__pretty_val.snap diff --git a/CHANGELOG.md b/CHANGELOG.md index 589188ea..9008f486 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - *BREAKING* partiql-parser: Added a source location to `ParseError::UnexpectedEndOfInput` ### Added +- partiql-value: Pretty-printing of `Value` via `ToPretty` trait ### Removed diff --git a/partiql-ast/src/pretty.rs b/partiql-ast/src/pretty.rs index ad2dffe6..e7d6ba29 100644 --- a/partiql-ast/src/pretty.rs +++ b/partiql-ast/src/pretty.rs @@ -1,61 +1,10 @@ use crate::ast::*; -use pretty::{Arena, DocAllocator, DocBuilder, Pretty}; -use std::io; -use std::io::Write; -use std::string::FromUtf8Error; -use thiserror::Error; - -const MINOR_NEST_INDENT: isize = 2; -const SUBQUERY_INDENT: isize = 6; - -pub(crate) trait PrettyDoc { - fn pretty_doc<'b, D, A>(&'b self, arena: &'b D) -> DocBuilder<'b, D, A> - where - D: DocAllocator<'b, A>, - D::Doc: Clone, - A: Clone; -} - -#[derive(Debug, Error)] -#[non_exhaustive] -pub enum ToPrettyError { - #[error("IO error: `{0}`")] - IoError(#[from] std::io::Error), - - #[error("FromUtf8Error: `{0}`")] - FromUtf8Error(#[from] FromUtf8Error), -} - -type ToPrettyResult = Result; - -pub trait ToPretty { - /// Pretty-prints to a `String`. - fn to_pretty_string(&self, width: usize) -> ToPrettyResult { - let mut out = Vec::new(); - self.to_pretty(width, &mut out)?; - Ok(String::from_utf8(out)?) - } - - /// Pretty-prints to a `std::io::Write` object. - fn to_pretty(&self, width: usize, out: &mut W) -> ToPrettyResult<()> - where - W: ?Sized + io::Write; -} - -impl ToPretty for AstNode -where - T: PrettyDoc, -{ - fn to_pretty(&self, width: usize, out: &mut W) -> ToPrettyResult<()> - where - W: ?Sized + Write, - { - let arena = Arena::new(); - let DocBuilder(_, doc) = self.node.pretty_doc::<_, ()>(&arena); - Ok(doc.render(width, out)?) - } -} - +use partiql_common::pretty::{ + pretty_list, pretty_parenthesized_doc, pretty_prefixed_doc, pretty_seperated, + pretty_seperated_doc, pretty_seq, pretty_seq_doc, PrettyDoc, PRETTY_INDENT_MINOR_NEST, + PRETTY_INDENT_SUBORDINATE_CLAUSE_NEST, +}; +use pretty::{DocAllocator, DocBuilder}; impl PrettyDoc for AstNode where T: PrettyDoc, @@ -71,45 +20,6 @@ where } } -impl PrettyDoc for Box -where - T: PrettyDoc, -{ - #[inline] - fn pretty_doc<'b, D, A>(&'b self, arena: &'b D) -> DocBuilder<'b, D, A> - where - D: DocAllocator<'b, A>, - D::Doc: Clone, - A: Clone, - { - self.as_ref().pretty_doc(arena) - } -} - -impl PrettyDoc for str { - #[inline] - fn pretty_doc<'b, D, A>(&'b self, arena: &'b D) -> DocBuilder<'b, D, A> - where - D: DocAllocator<'b, A>, - D::Doc: Clone, - A: Clone, - { - arena.concat(["'", self, "'"]) - } -} - -impl PrettyDoc for rust_decimal::Decimal { - #[inline] - fn pretty_doc<'b, D, A>(&'b self, arena: &'b D) -> DocBuilder<'b, D, A> - where - D: DocAllocator<'b, A>, - D::Doc: Clone, - A: Clone, - { - arena.text(self.to_string()) - } -} - impl PrettyDoc for TopLevelQuery { fn pretty_doc<'b, D, A>(&'b self, arena: &'b D) -> DocBuilder<'b, D, A> where @@ -160,8 +70,8 @@ impl PrettyDoc for QuerySet { QuerySet::BagOp(op) => op.pretty_doc(arena), QuerySet::Select(sel) => sel.pretty_doc(arena), QuerySet::Expr(e) => e.pretty_doc(arena), - QuerySet::Values(v) => pretty_annotated_doc("VALUES", pretty_list(v, 0, arena), arena), - QuerySet::Table(t) => pretty_annotated_expr("TABLE", t, 0, arena), + QuerySet::Values(v) => pretty_prefixed_doc("VALUES", pretty_list(v, 0, arena), arena), + QuerySet::Table(t) => pretty_prefixed_expr("TABLE", t, 0, arena), } } } @@ -188,8 +98,8 @@ impl PrettyDoc for BagOpExpr { Some(SetQuantifier::Distinct) => op.append(" DISTINCT"), }; - let lhs = pretty_parenthesized_expr(&self.lhs, MINOR_NEST_INDENT, arena); - let rhs = pretty_parenthesized_expr(&self.rhs, MINOR_NEST_INDENT, arena); + let lhs = pretty_parenthesized_expr(&self.lhs, PRETTY_INDENT_MINOR_NEST, arena); + let rhs = pretty_parenthesized_expr(&self.rhs, PRETTY_INDENT_MINOR_NEST, arena); arena.intersperse([lhs, op, rhs], arena.hardline()).group() } @@ -294,9 +204,11 @@ impl PrettyDoc for ProjectionKind { { match self { ProjectionKind::ProjectStar => arena.text("SELECT *"), - ProjectionKind::ProjectList(l) => { - pretty_annotated_doc("SELECT", pretty_list(l, MINOR_NEST_INDENT, arena), arena) - } + ProjectionKind::ProjectList(l) => pretty_prefixed_doc( + "SELECT", + pretty_list(l, PRETTY_INDENT_MINOR_NEST, arena), + arena, + ), ProjectionKind::ProjectPivot(ProjectPivot { key, value }) => { let parts = [ value.pretty_doc(arena), @@ -304,10 +216,10 @@ impl PrettyDoc for ProjectionKind { key.pretty_doc(arena), ]; let decl = arena.intersperse(parts, arena.space()).group(); - pretty_annotated_doc("PIVOT", decl, arena) + pretty_prefixed_doc("PIVOT", decl, arena) } ProjectionKind::ProjectValue(ctor) => { - pretty_annotated_expr("SELECT VALUE", ctor, MINOR_NEST_INDENT, arena) + pretty_prefixed_expr("SELECT VALUE", ctor, PRETTY_INDENT_MINOR_NEST, arena) } } .group() @@ -349,9 +261,9 @@ impl PrettyDoc for Exclusion { D::Doc: Clone, A: Clone, { - pretty_annotated_doc( + pretty_prefixed_doc( "EXCLUDE", - pretty_list(&self.items, MINOR_NEST_INDENT, arena), + pretty_list(&self.items, PRETTY_INDENT_MINOR_NEST, arena), arena, ) } @@ -411,7 +323,7 @@ impl PrettyDoc for Expr { let inner = inner.pretty_doc(arena).group(); arena .text("(") - .append(inner.nest(SUBQUERY_INDENT)) + .append(inner.nest(PRETTY_INDENT_SUBORDINATE_CLAUSE_NEST)) .append(arena.text(")")) } Expr::Error => { @@ -574,8 +486,8 @@ impl PrettyDoc for BinOp { BinOpKind::Mod => (0, "%"), BinOpKind::Mul => (0, "*"), BinOpKind::Sub => (0, "-"), - BinOpKind::And => (MINOR_NEST_INDENT, "AND"), - BinOpKind::Or => (MINOR_NEST_INDENT, "OR"), + BinOpKind::And => (PRETTY_INDENT_MINOR_NEST, "AND"), + BinOpKind::Or => (PRETTY_INDENT_MINOR_NEST, "OR"), BinOpKind::Concat => (0, "||"), BinOpKind::Eq => (0, "="), BinOpKind::Gt => (0, ">"), @@ -721,7 +633,15 @@ impl PrettyDoc for SimpleCase { let search = expr.pretty_doc(arena); let branches = case_branches(arena, cases, default); - pretty_seq_doc(branches, "CASE", Some(search), "END", " ", arena) + pretty_seq_doc( + branches, + "CASE", + Some(search), + "END", + " ", + PRETTY_INDENT_MINOR_NEST, + arena, + ) } } @@ -735,36 +655,16 @@ impl PrettyDoc for SearchedCase { let SearchedCase { cases, default } = self; let branches = case_branches(arena, cases, default); - pretty_seq_doc(branches, "CASE", None, "END", " ", arena) - } -} - -fn case_branches<'b, D, A>( - arena: &'b D, - cases: &'b [ExprPair], - default: &'b Option>, -) -> impl Iterator> -where - D: DocAllocator<'b, A>, - D::Doc: Clone, - A: Clone + 'b, -{ - cases - .iter() - .map(|ExprPair { first, second }| { - let kw_when = arena.text("WHEN"); - let test = first.pretty_doc(arena); - let kw_then = arena.text("THEN"); - let then = second.pretty_doc(arena); - arena - .intersperse([kw_when, test, kw_then, then], arena.space()) - .group() - }) - .chain( - default - .iter() - .map(|d| arena.text("ELSE ").append(d.pretty_doc(arena)).group()), + pretty_seq_doc( + branches, + "CASE", + None, + "END", + " ", + PRETTY_INDENT_MINOR_NEST, + arena, ) + } } impl PrettyDoc for Struct { @@ -778,7 +678,7 @@ impl PrettyDoc for Struct { let x: &'b StructExprPair = std::mem::transmute(p); x }); - pretty_seq(wrapped, "{", "}", ",", arena) + pretty_seq(wrapped, "{", "}", ",", PRETTY_INDENT_MINOR_NEST, arena) } } @@ -806,7 +706,14 @@ impl PrettyDoc for Bag { D::Doc: Clone, A: Clone, { - pretty_seq(&self.values, "<<", ">>", ",", arena) + pretty_seq( + &self.values, + "<<", + ">>", + ",", + PRETTY_INDENT_MINOR_NEST, + arena, + ) } } @@ -817,7 +724,7 @@ impl PrettyDoc for List { D::Doc: Clone, A: Clone, { - pretty_seq(&self.values, "[", "]", ",", arena) + pretty_seq(&self.values, "[", "]", ",", PRETTY_INDENT_MINOR_NEST, arena) } } @@ -842,7 +749,7 @@ impl PrettyDoc for Call { let name = self.func_name.pretty_doc(arena); let list = pretty_list(&self.args, 0, arena); name.append(arena.text("(")) - .append(list.nest(MINOR_NEST_INDENT)) + .append(list.nest(PRETTY_INDENT_MINOR_NEST)) .append(arena.text(")")) } } @@ -857,7 +764,7 @@ impl PrettyDoc for CallAgg { let name = self.func_name.pretty_doc(arena); let list = pretty_list(&self.args, 0, arena); name.append(arena.text("(")) - .append(list.nest(MINOR_NEST_INDENT)) + .append(list.nest(PRETTY_INDENT_MINOR_NEST)) .append(arena.text(")")) } } @@ -919,7 +826,7 @@ impl PrettyDoc for FromClause { D::Doc: Clone, A: Clone, { - pretty_annotated_expr("FROM", &self.source, MINOR_NEST_INDENT, arena) + pretty_prefixed_expr("FROM", &self.source, PRETTY_INDENT_MINOR_NEST, arena) } } @@ -963,7 +870,7 @@ impl PrettyDoc for FromLet { let clause = match kind { FromLetKind::Scan => expr, - FromLetKind::Unpivot => pretty_annotated_doc("UNPIVOT", expr, arena), + FromLetKind::Unpivot => pretty_prefixed_doc("UNPIVOT", expr, arena), }; if aliases.is_empty() { @@ -1015,12 +922,12 @@ impl PrettyDoc for Join { .softline() .append(arena.text("ON")) .append(arena.softline()) - .append(on.pretty_doc(arena).nest(MINOR_NEST_INDENT)); + .append(on.pretty_doc(arena).nest(PRETTY_INDENT_MINOR_NEST)); join.append(pred) } JoinSpec::Using(using) => { let join = pretty_seperated(kw_join, arms, 0, arena); - let using = pretty_list(using, MINOR_NEST_INDENT, arena); + let using = pretty_list(using, PRETTY_INDENT_MINOR_NEST, arena); let pred = arena .softline() .append(arena.text("USING")) @@ -1052,7 +959,7 @@ impl PrettyDoc for WhereClause { D::Doc: Clone, A: Clone, { - pretty_annotated_expr("WHERE", &self.expr, MINOR_NEST_INDENT, arena) + pretty_prefixed_expr("WHERE", &self.expr, PRETTY_INDENT_MINOR_NEST, arena) } } @@ -1077,9 +984,11 @@ impl PrettyDoc for GroupByExpr { if !keys.is_empty() { doc = doc.append(arena.space()).append(arena.text("BY")).group(); - doc = doc - .append(arena.softline()) - .append(pretty_list(keys, MINOR_NEST_INDENT, arena)); + doc = doc.append(arena.softline()).append(pretty_list( + keys, + PRETTY_INDENT_MINOR_NEST, + arena, + )); } match group_as_alias { @@ -1112,7 +1021,7 @@ impl PrettyDoc for HavingClause { D::Doc: Clone, A: Clone, { - pretty_annotated_expr("HAVING", &self.expr, MINOR_NEST_INDENT, arena) + pretty_prefixed_expr("HAVING", &self.expr, PRETTY_INDENT_MINOR_NEST, arena) } } @@ -1126,9 +1035,9 @@ impl PrettyDoc for OrderByExpr { if self.sort_specs.is_empty() { arena.text("ORDER BY PRESERVE") } else { - pretty_annotated_doc( + pretty_prefixed_doc( "ORDER BY", - pretty_list(&self.sort_specs, MINOR_NEST_INDENT, arena), + pretty_list(&self.sort_specs, PRETTY_INDENT_MINOR_NEST, arena), arena, ) } @@ -1200,12 +1109,12 @@ impl PrettyDoc for LimitOffsetClause { let limit = self .limit .as_ref() - .map(|l| pretty_annotated_expr("LIMIT", l, MINOR_NEST_INDENT, arena)); + .map(|l| pretty_prefixed_expr("LIMIT", l, PRETTY_INDENT_MINOR_NEST, arena)); let offset = self .offset .as_ref() - .map(|o| pretty_annotated_expr("OFFSET", o, MINOR_NEST_INDENT, arena)); + .map(|o| pretty_prefixed_expr("OFFSET", o, PRETTY_INDENT_MINOR_NEST, arena)); match (limit, offset) { (None, None) => unreachable!(), @@ -1216,33 +1125,47 @@ impl PrettyDoc for LimitOffsetClause { } } -fn pretty_annotated_expr<'b, P, D, A>( - annot: &'static str, - expr: &'b P, - nest: isize, +fn case_branches<'b, D, A>( arena: &'b D, -) -> DocBuilder<'b, D, A> + cases: &'b [ExprPair], + default: &'b Option>, +) -> impl Iterator> where - P: PrettyDoc, D: DocAllocator<'b, A>, D::Doc: Clone, - A: Clone, + A: Clone + 'b, { - pretty_annotated_doc(annot, expr.pretty_doc(arena).nest(nest), arena) + cases + .iter() + .map(|ExprPair { first, second }| { + let kw_when = arena.text("WHEN"); + let test = first.pretty_doc(arena); + let kw_then = arena.text("THEN"); + let then = second.pretty_doc(arena); + arena + .intersperse([kw_when, test, kw_then, then], arena.space()) + .group() + }) + .chain( + default + .iter() + .map(|d| arena.text("ELSE ").append(d.pretty_doc(arena)).group()), + ) } -fn pretty_annotated_doc<'b, E, D, A>( +fn pretty_prefixed_expr<'b, P, D, A>( annot: &'static str, - doc: E, + expr: &'b P, + nest: isize, arena: &'b D, ) -> DocBuilder<'b, D, A> where - E: Pretty<'b, D, A>, + P: PrettyDoc, D: DocAllocator<'b, A>, D::Doc: Clone, A: Clone, { - arena.text(annot).append(arena.space()).append(doc).group() + pretty_prefixed_doc(annot, expr.pretty_doc(arena).nest(nest), arena) } fn pretty_parenthesized_expr<'b, P, D, A>( @@ -1258,114 +1181,6 @@ where { pretty_parenthesized_doc(expr.pretty_doc(arena).nest(nest), arena) } -fn pretty_parenthesized_doc<'b, E, D, A>(doc: E, arena: &'b D) -> DocBuilder<'b, D, A> -where - E: Pretty<'b, D, A>, - D: DocAllocator<'b, A>, - D::Doc: Clone, - A: Clone, -{ - arena.text("(").append(doc).append(arena.text(")")).group() -} - -fn pretty_seq_doc<'i, 'b, I, E, D, A>( - seq: I, - start: &'static str, - qualifier: Option, - end: &'static str, - sep: &'static str, - arena: &'b D, -) -> DocBuilder<'b, D, A> -where - E: Pretty<'b, D, A>, - I: IntoIterator, - D: DocAllocator<'b, A>, - D::Doc: Clone, - A: Clone, -{ - let start = arena.text(start); - let end = arena.text(end); - let sep = arena.text(sep).append(arena.line()); - let start = if let Some(qual) = qualifier { - start.append(arena.space()).append(qual) - } else { - start - }; - let body = arena - .line() - .append(arena.intersperse(seq, sep)) - .append(arena.line()) - .group(); - start - .append(body.nest(MINOR_NEST_INDENT)) - .append(end) - .group() -} - -fn pretty_seq<'i, 'b, I, P, D, A>( - list: I, - start: &'static str, - end: &'static str, - sep: &'static str, - arena: &'b D, -) -> DocBuilder<'b, D, A> -where - I: IntoIterator, - P: PrettyDoc + 'b, - D: DocAllocator<'b, A>, - D::Doc: Clone, - A: Clone, -{ - let seq = list.into_iter().map(|l| l.pretty_doc(arena)); - pretty_seq_doc(seq, start, None, end, sep, arena) -} - -fn pretty_list<'b, I, P, D, A>(list: I, nest: isize, arena: &'b D) -> DocBuilder<'b, D, A> -where - I: IntoIterator, - P: PrettyDoc + 'b, - D: DocAllocator<'b, A>, - D::Doc: Clone, - A: Clone, -{ - let sep = arena.text(",").append(arena.softline()); - pretty_seperated(sep, list, nest, arena) -} - -fn pretty_seperated<'b, I, E, P, D, A>( - sep: E, - list: I, - nest: isize, - arena: &'b D, -) -> DocBuilder<'b, D, A> -where - I: IntoIterator, - E: Pretty<'b, D, A>, - P: PrettyDoc + 'b, - D: DocAllocator<'b, A>, - D::Doc: Clone, - A: Clone, -{ - let list = list.into_iter().map(|l| l.pretty_doc(arena)); - pretty_seperated_doc(sep, list, nest, arena) -} - -fn pretty_seperated_doc<'b, I, E, D, A>( - sep: E, - list: I, - nest: isize, - arena: &'b D, -) -> DocBuilder<'b, D, A> -where - I: IntoIterator>, - E: Pretty<'b, D, A>, - D: DocAllocator<'b, A>, - D::Doc: Clone, - A: Clone, -{ - let sep = sep.pretty(arena); - arena.intersperse(list, sep).nest(nest).group() -} fn pretty_alias_helper<'b, D, A>( kw: &'static str, diff --git a/partiql-common/src/lib.rs b/partiql-common/src/lib.rs index 8238c394..42f73b30 100644 --- a/partiql-common/src/lib.rs +++ b/partiql-common/src/lib.rs @@ -4,6 +4,7 @@ pub static FN_VAR_ARG_MAX: usize = 10; pub mod metadata; pub mod node; +pub mod pretty; pub mod syntax; pub mod catalog; diff --git a/partiql-common/src/pretty.rs b/partiql-common/src/pretty.rs new file mode 100644 index 00000000..ecdf2f06 --- /dev/null +++ b/partiql-common/src/pretty.rs @@ -0,0 +1,300 @@ +use pretty::{Arena, DocAllocator, DocBuilder, Pretty}; +use std::io; +use std::io::Write; +use std::string::FromUtf8Error; +use thiserror::Error; + +pub const PRETTY_INDENT_MINOR_NEST: isize = 2; +pub const PRETTY_INDENT_SUBORDINATE_CLAUSE_NEST: isize = 6; + +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum ToPrettyError { + #[error("IO error: `{0}`")] + IoError(#[from] std::io::Error), + + #[error("FromUtf8Error: `{0}`")] + FromUtf8Error(#[from] FromUtf8Error), +} + +pub type ToPrettyResult = Result; + +pub trait ToPretty { + /// Pretty-prints to a `String`. + fn to_pretty_string(&self, width: usize) -> ToPrettyResult { + let mut out = Vec::new(); + self.to_pretty(width, &mut out)?; + Ok(String::from_utf8(out)?) + } + + /// Pretty-prints to a `std::io::Write` object. + fn to_pretty(&self, width: usize, out: &mut W) -> ToPrettyResult<()> + where + W: ?Sized + io::Write; +} + +impl ToPretty for T +where + T: PrettyDoc, +{ + fn to_pretty(&self, width: usize, out: &mut W) -> ToPrettyResult<()> + where + W: ?Sized + Write, + { + let arena = Arena::new(); + let DocBuilder(_, doc) = self.pretty_doc::<_, ()>(&arena); + Ok(doc.render(width, out)?) + } +} + +pub trait PrettyDoc { + fn pretty_doc<'b, D, A>(&'b self, arena: &'b D) -> DocBuilder<'b, D, A> + where + D: DocAllocator<'b, A>, + D::Doc: Clone, + A: Clone; +} + +impl PrettyDoc for &T +where + T: PrettyDoc, +{ + #[inline] + fn pretty_doc<'b, D, A>(&'b self, arena: &'b D) -> DocBuilder<'b, D, A> + where + D: DocAllocator<'b, A>, + D::Doc: Clone, + A: Clone, + { + (*self).pretty_doc(arena) + } +} + +impl PrettyDoc for Box +where + T: PrettyDoc, +{ + #[inline] + fn pretty_doc<'b, D, A>(&'b self, arena: &'b D) -> DocBuilder<'b, D, A> + where + D: DocAllocator<'b, A>, + D::Doc: Clone, + A: Clone, + { + self.as_ref().pretty_doc(arena) + } +} + +impl PrettyDoc for str { + #[inline] + fn pretty_doc<'b, D, A>(&'b self, arena: &'b D) -> DocBuilder<'b, D, A> + where + D: DocAllocator<'b, A>, + D::Doc: Clone, + A: Clone, + { + arena.concat(["'", self, "'"]) + } +} + +impl PrettyDoc for String { + #[inline] + fn pretty_doc<'b, D, A>(&'b self, arena: &'b D) -> DocBuilder<'b, D, A> + where + D: DocAllocator<'b, A>, + D::Doc: Clone, + A: Clone, + { + arena.concat(["'", self, "'"]) + } +} + +impl PrettyDoc for Vec { + #[inline] + fn pretty_doc<'b, D, A>(&'b self, arena: &'b D) -> DocBuilder<'b, D, A> + where + D: DocAllocator<'b, A>, + D::Doc: Clone, + A: Clone, + { + let y = String::from_utf8_lossy(self.as_slice()); + arena.text(y) + } +} + +impl PrettyDoc for rust_decimal::Decimal { + #[inline] + fn pretty_doc<'b, D, A>(&'b self, arena: &'b D) -> DocBuilder<'b, D, A> + where + D: DocAllocator<'b, A>, + D::Doc: Clone, + A: Clone, + { + arena.text(self.to_string()) + } +} + +#[inline] +pub fn pretty_prefixed_doc<'b, E, D, A>( + annot: &'static str, + doc: E, + arena: &'b D, +) -> DocBuilder<'b, D, A> +where + E: Pretty<'b, D, A>, + D: DocAllocator<'b, A>, + D::Doc: Clone, + A: Clone, +{ + arena.text(annot).append(arena.space()).append(doc).group() +} + +#[inline] +pub fn pretty_surrounded<'b, P, D, A>( + inner: &'b P, + start: &'static str, + end: &'static str, + arena: &'b D, +) -> DocBuilder<'b, D, A> +where + P: PrettyDoc + 'b, + D: DocAllocator<'b, A>, + D::Doc: Clone, + A: Clone, +{ + pretty_surrounded_doc(inner.pretty_doc(arena), start, end, arena) +} + +#[inline] +pub fn pretty_surrounded_doc<'b, E, D, A>( + doc: E, + start: &'static str, + end: &'static str, + arena: &'b D, +) -> DocBuilder<'b, D, A> +where + E: Pretty<'b, D, A>, + D: DocAllocator<'b, A>, + D::Doc: Clone, + A: Clone, +{ + arena + .text(start) + .append(doc) + .append(arena.text(end)) + .group() +} + +#[inline] +pub fn pretty_parenthesized_doc<'b, E, D, A>(doc: E, arena: &'b D) -> DocBuilder<'b, D, A> +where + E: Pretty<'b, D, A>, + D: DocAllocator<'b, A>, + D::Doc: Clone, + A: Clone, +{ + pretty_surrounded_doc(doc, "(", ")", arena) +} + +#[inline] +pub fn pretty_seq_doc<'i, 'b, I, E, D, A>( + seq: I, + start: &'static str, + qualifier: Option, + end: &'static str, + sep: &'static str, + nest: isize, + arena: &'b D, +) -> DocBuilder<'b, D, A> +where + E: Pretty<'b, D, A>, + I: IntoIterator, + D: DocAllocator<'b, A>, + D::Doc: Clone, + A: Clone, +{ + let start = arena.text(start); + let end = arena.text(end); + let sep = arena.text(sep).append(arena.line()); + let start = if let Some(qual) = qualifier { + start.append(arena.space()).append(qual) + } else { + start + }; + let body = arena + .line() + .append(arena.intersperse(seq, sep)) + .append(arena.line()) + .group(); + start.append(body.nest(nest)).append(end).group() +} + +#[inline] +pub fn pretty_seq<'i, 'b, I, P, D, A>( + list: I, + start: &'static str, + end: &'static str, + sep: &'static str, + nest: isize, + arena: &'b D, +) -> DocBuilder<'b, D, A> +where + I: IntoIterator, + P: PrettyDoc + 'b, + D: DocAllocator<'b, A>, + D::Doc: Clone, + A: Clone, +{ + let seq = list.into_iter().map(|l| l.pretty_doc(arena)); + pretty_seq_doc(seq, start, None, end, sep, nest, arena) +} + +#[inline] +pub fn pretty_list<'b, I, P, D, A>(list: I, nest: isize, arena: &'b D) -> DocBuilder<'b, D, A> +where + I: IntoIterator, + P: PrettyDoc + 'b, + D: DocAllocator<'b, A>, + D::Doc: Clone, + A: Clone, +{ + let sep = arena.text(",").append(arena.softline()); + pretty_seperated(sep, list, nest, arena) +} + +#[inline] +pub fn pretty_seperated<'b, I, E, P, D, A>( + sep: E, + list: I, + nest: isize, + arena: &'b D, +) -> DocBuilder<'b, D, A> +where + I: IntoIterator, + E: Pretty<'b, D, A>, + P: PrettyDoc + 'b, + D: DocAllocator<'b, A>, + D::Doc: Clone, + A: Clone, +{ + let list = list.into_iter().map(|l| l.pretty_doc(arena)); + pretty_seperated_doc(sep, list, nest, arena) +} + +#[inline] +pub fn pretty_seperated_doc<'b, I, E, D, A>( + sep: E, + list: I, + nest: isize, + arena: &'b D, +) -> DocBuilder<'b, D, A> +where + I: IntoIterator>, + E: Pretty<'b, D, A>, + D: DocAllocator<'b, A>, + D::Doc: Clone, + A: Clone, +{ + let sep = sep.pretty(arena); + arena.intersperse(list, sep).nest(nest).group() +} diff --git a/partiql-conformance-tests/Cargo.toml b/partiql-conformance-tests/Cargo.toml index 35d23508..d834e571 100644 --- a/partiql-conformance-tests/Cargo.toml +++ b/partiql-conformance-tests/Cargo.toml @@ -34,6 +34,7 @@ partiql-conformance-test-generator = { path = "../partiql-conformance-test-gener [dependencies] partiql-parser = { path = "../partiql-parser", version = "0.11.*" } partiql-catalog = { path = "../partiql-catalog", version = "0.11.*" } +partiql-common = { path = "../partiql-common", version = "0.11.*" } partiql-ast = { path = "../partiql-ast", version = "0.11.*" } partiql-ast-passes = { path = "../partiql-ast-passes", version = "0.11.*" } partiql-logical-planner = { path = "../partiql-logical-planner", version = "0.11.*" } diff --git a/partiql-conformance-tests/tests/mod.rs b/partiql-conformance-tests/tests/mod.rs index 651afe15..26fe2826 100644 --- a/partiql-conformance-tests/tests/mod.rs +++ b/partiql-conformance-tests/tests/mod.rs @@ -37,7 +37,7 @@ pub(crate) fn parse(statement: &str) -> ParserResult { #[cfg(feature = "test_pretty_print")] if let Ok(result) = &result { - use partiql_ast::pretty::ToPretty; + use partiql_common::pretty::ToPretty; let pretty = result.ast.to_pretty_string(80); if let Ok(pretty) = pretty { println!("{pretty}"); diff --git a/partiql-value/Cargo.toml b/partiql-value/Cargo.toml index 5b8ba5ff..df107a63 100644 --- a/partiql-value/Cargo.toml +++ b/partiql-value/Cargo.toml @@ -21,6 +21,7 @@ edition.workspace = true bench = false [dependencies] +partiql-common = { path = "../partiql-common", version = "0.11.*" } ordered-float = "4" itertools = "0.13" unicase = "2.7" @@ -28,6 +29,7 @@ rust_decimal = { version = "1.36.0", default-features = false, features = ["std" rust_decimal_macros = "1.36" time = { version = "0.3", features = ["macros"] } +pretty = "0.12" serde = { version = "1", features = ["derive"], optional = true } diff --git a/partiql-value/src/lib.rs b/partiql-value/src/lib.rs index 387d6e3d..e90ab87d 100644 --- a/partiql-value/src/lib.rs +++ b/partiql-value/src/lib.rs @@ -18,11 +18,13 @@ use rust_decimal::{Decimal as RustDecimal, Decimal}; mod bag; mod datetime; mod list; +mod pretty; mod tuple; pub use bag::*; pub use datetime::*; pub use list::*; +pub use pretty::*; pub use tuple::*; #[cfg(feature = "serde")] diff --git a/partiql-value/src/pretty.rs b/partiql-value/src/pretty.rs new file mode 100644 index 00000000..5767ca95 --- /dev/null +++ b/partiql-value/src/pretty.rs @@ -0,0 +1,128 @@ +use crate::{Bag, DateTime, List, Tuple, Value}; +use partiql_common::pretty::{ + pretty_prefixed_doc, pretty_seq, pretty_surrounded, PrettyDoc, PRETTY_INDENT_MINOR_NEST, +}; +use pretty::{DocAllocator, DocBuilder}; + +impl PrettyDoc for Value { + #[inline] + fn pretty_doc<'b, D, A>(&'b self, arena: &'b D) -> DocBuilder<'b, D, A> + where + D: DocAllocator<'b, A>, + D::Doc: Clone, + A: Clone, + { + match self { + Value::Null => arena.text("NULL"), + Value::Missing => arena.text("MISSING"), + Value::Boolean(inner) => arena.text(inner.to_string()), + Value::Integer(inner) => arena.text(inner.to_string()), + Value::Real(inner) => arena.text(inner.0.to_string()), + Value::Decimal(inner) => inner.pretty_doc(arena), + Value::String(inner) => inner.pretty_doc(arena), + Value::Blob(inner) => pretty_string(inner, arena), + Value::DateTime(inner) => inner.pretty_doc(arena), + Value::List(inner) => inner.pretty_doc(arena), + Value::Bag(inner) => inner.pretty_doc(arena), + Value::Tuple(inner) => inner.pretty_doc(arena), + } + } +} + +impl PrettyDoc for DateTime { + #[inline] + fn pretty_doc<'b, D, A>(&'b self, arena: &'b D) -> DocBuilder<'b, D, A> + where + D: DocAllocator<'b, A>, + D::Doc: Clone, + A: Clone, + { + match self { + DateTime::Date(d) => pretty_prefixed_doc("DATE", format!("'{d:?}'"), arena), + + DateTime::Time(t) => pretty_prefixed_doc("TIME", format!("'{t:?}'"), arena), + DateTime::TimeWithTz(t, tz) => { + pretty_prefixed_doc("TIME WITH TIME ZONE", format!("'{t:?} {tz:?}'"), arena) + } + DateTime::Timestamp(dt) => pretty_prefixed_doc("TIMESTAMP", format!("'{dt:?}'"), arena), + DateTime::TimestampWithTz(dt) => { + pretty_prefixed_doc("TIMESTAMP WITH TIME ZONE", format!("'{dt:?}'"), arena) + } + } + } +} + +impl PrettyDoc for List { + #[inline] + fn pretty_doc<'b, D, A>(&'b self, arena: &'b D) -> DocBuilder<'b, D, A> + where + D: DocAllocator<'b, A>, + D::Doc: Clone, + A: Clone, + { + pretty_seq(self.iter(), "[", "]", ",", PRETTY_INDENT_MINOR_NEST, arena) + } +} + +impl PrettyDoc for Bag { + #[inline] + fn pretty_doc<'b, D, A>(&'b self, arena: &'b D) -> DocBuilder<'b, D, A> + where + D: DocAllocator<'b, A>, + D::Doc: Clone, + A: Clone, + { + pretty_seq( + self.iter(), + "<<", + ">>", + ",", + PRETTY_INDENT_MINOR_NEST, + arena, + ) + } +} + +impl PrettyDoc for Tuple { + #[inline] + fn pretty_doc<'b, D, A>(&'b self, arena: &'b D) -> DocBuilder<'b, D, A> + where + D: DocAllocator<'b, A>, + D::Doc: Clone, + A: Clone, + { + let wrapped = self.pairs().map(|p| unsafe { + let x: &'b StructValuePair<'b> = std::mem::transmute(&p); + x + }); + pretty_seq(wrapped, "{", "}", ",", PRETTY_INDENT_MINOR_NEST, arena) + } +} + +pub struct StructValuePair<'a>((&'a String, &'a Value)); + +impl<'a> PrettyDoc for StructValuePair<'a> { + fn pretty_doc<'b, D, A>(&'b self, arena: &'b D) -> DocBuilder<'b, D, A> + where + D: DocAllocator<'b, A>, + D::Doc: Clone, + A: Clone, + { + let (k, v) = self.0; + let k = k.pretty_doc(arena); + let v = v.pretty_doc(arena); + let sep = arena.text(": "); + + k.append(sep).group().append(v).group() + } +} + +fn pretty_string<'b, P, D, A>(contents: &'b P, arena: &'b D) -> DocBuilder<'b, D, A> +where + P: PrettyDoc + 'b, + D: DocAllocator<'b, A>, + D::Doc: Clone, + A: Clone, +{ + pretty_surrounded(contents, "'", "'", arena) +} diff --git a/partiql/Cargo.toml b/partiql/Cargo.toml index fa411b3a..5cea77e3 100644 --- a/partiql/Cargo.toml +++ b/partiql/Cargo.toml @@ -26,6 +26,7 @@ bench = false partiql-parser = { path = "../partiql-parser" } partiql-ast = { path = "../partiql-ast" } partiql-ast-passes = { path = "../partiql-ast-passes" } +partiql-common = { path = "../partiql-common" } partiql-catalog = { path = "../partiql-catalog" } partiql-value = { path = "../partiql-value" } partiql-logical = { path = "../partiql-logical" } @@ -39,6 +40,9 @@ insta = "1.40.0" thiserror = "1.0" itertools = "0.13" +rust_decimal = { version = "1.25.0", default-features = false, features = ["std"] } +time = { version = "0.3", features = ["macros"] } + criterion = "0.5" rand = "0.8" diff --git a/partiql/tests/pretty.rs b/partiql/tests/pretty.rs index cbdc045f..e4f431f5 100644 --- a/partiql/tests/pretty.rs +++ b/partiql/tests/pretty.rs @@ -1,7 +1,10 @@ use itertools::Itertools; use partiql_ast::ast::{AstNode, TopLevelQuery}; -use partiql_ast::pretty::ToPretty; +use partiql_common::pretty::ToPretty; use partiql_parser::ParserResult; +use partiql_value::{bag, list, tuple, DateTime, Value}; +use rust_decimal::prelude::FromPrimitive; +use time::macros::{date, datetime, offset, time}; #[track_caller] #[inline] @@ -51,6 +54,100 @@ fn pretty_print_roundtrip_test(statement_ast: &AstNode) { assert_eq!(pretty, pretty2); } +#[track_caller] +#[inline] +fn pretty_print_value_test(name: &str, value: &Value) { + pretty_print_value_output_test(name, value); + pretty_print_value_roundtrip_test(value); +} + +#[track_caller] +fn pretty_print_value_output_test(name: &str, value: &Value) { + let doc = [180, 120, 80, 40, 30, 20, 10] + .into_iter() + .map(|w| { + let header = format!("{:->, + 'waldo': [ 1, 2, 'skip a few', 99, 100 ] +} + +------------------------------------------------------------------------------------------------------------------------ +{ + 'foo': true, + '-foo': false, + 'bar': 42, + 'baz': 3.14, + 'qux': 'string', + 'thud': NULL, + 'plugh': [ + 1, + 2, + 999.876, + 299800000, + DATE '2020-01-01', + TIME '1:02:03.004005006', + TIME WITH TIME ZONE '1:02:03.004005006 +00:00:00', + TIMESTAMP '2020-01-01 0:00:00.0', + TIMESTAMP WITH TIME ZONE '2020-01-01 0:00:00.0 +00:00:00', + 'abcdef', + MISSING + ], + 'xyzzy': << + { 'n': 1 }, + { 'n': 2 }, + { 'n': 3 }, + { 'n': 4 }, + { 'n': 5 }, + { 'n': 6 }, + { 'n': 7 }, + { 'n': 8 }, + { 'n': 9 }, + { 'n': 10 } + >>, + 'waldo': [ 1, 2, 'skip a few', 99, 100 ] +} + +-------------------------------------------------------------------------------- +{ + 'foo': true, + '-foo': false, + 'bar': 42, + 'baz': 3.14, + 'qux': 'string', + 'thud': NULL, + 'plugh': [ + 1, + 2, + 999.876, + 299800000, + DATE '2020-01-01', + TIME '1:02:03.004005006', + TIME WITH TIME ZONE '1:02:03.004005006 +00:00:00', + TIMESTAMP '2020-01-01 0:00:00.0', + TIMESTAMP WITH TIME ZONE '2020-01-01 0:00:00.0 +00:00:00', + 'abcdef', + MISSING + ], + 'xyzzy': << + { 'n': 1 }, + { 'n': 2 }, + { 'n': 3 }, + { 'n': 4 }, + { 'n': 5 }, + { 'n': 6 }, + { 'n': 7 }, + { 'n': 8 }, + { 'n': 9 }, + { 'n': 10 } + >>, + 'waldo': [ 1, 2, 'skip a few', 99, 100 ] +} + +---------------------------------------- +{ + 'foo': true, + '-foo': false, + 'bar': 42, + 'baz': 3.14, + 'qux': 'string', + 'thud': NULL, + 'plugh': [ + 1, + 2, + 999.876, + 299800000, + DATE '2020-01-01', + TIME '1:02:03.004005006', + TIME WITH TIME ZONE '1:02:03.004005006 +00:00:00', + TIMESTAMP '2020-01-01 0:00:00.0', + TIMESTAMP WITH TIME ZONE '2020-01-01 0:00:00.0 +00:00:00', + 'abcdef', + MISSING + ], + 'xyzzy': << + { 'n': 1 }, + { 'n': 2 }, + { 'n': 3 }, + { 'n': 4 }, + { 'n': 5 }, + { 'n': 6 }, + { 'n': 7 }, + { 'n': 8 }, + { 'n': 9 }, + { 'n': 10 } + >>, + 'waldo': [ + 1, + 2, + 'skip a few', + 99, + 100 + ] +} + +------------------------------ +{ + 'foo': true, + '-foo': false, + 'bar': 42, + 'baz': 3.14, + 'qux': 'string', + 'thud': NULL, + 'plugh': [ + 1, + 2, + 999.876, + 299800000, + DATE '2020-01-01', + TIME '1:02:03.004005006', + TIME WITH TIME ZONE '1:02:03.004005006 +00:00:00', + TIMESTAMP '2020-01-01 0:00:00.0', + TIMESTAMP WITH TIME ZONE '2020-01-01 0:00:00.0 +00:00:00', + 'abcdef', + MISSING + ], + 'xyzzy': << + { 'n': 1 }, + { 'n': 2 }, + { 'n': 3 }, + { 'n': 4 }, + { 'n': 5 }, + { 'n': 6 }, + { 'n': 7 }, + { 'n': 8 }, + { 'n': 9 }, + { 'n': 10 } + >>, + 'waldo': [ + 1, + 2, + 'skip a few', + 99, + 100 + ] +} + +-------------------- +{ + 'foo': true, + '-foo': false, + 'bar': 42, + 'baz': 3.14, + 'qux': 'string', + 'thud': NULL, + 'plugh': [ + 1, + 2, + 999.876, + 299800000, + DATE '2020-01-01', + TIME '1:02:03.004005006', + TIME WITH TIME ZONE '1:02:03.004005006 +00:00:00', + TIMESTAMP '2020-01-01 0:00:00.0', + TIMESTAMP WITH TIME ZONE '2020-01-01 0:00:00.0 +00:00:00', + 'abcdef', + MISSING + ], + 'xyzzy': << + { 'n': 1 }, + { 'n': 2 }, + { 'n': 3 }, + { 'n': 4 }, + { 'n': 5 }, + { 'n': 6 }, + { 'n': 7 }, + { 'n': 8 }, + { 'n': 9 }, + { 'n': 10 } + >>, + 'waldo': [ + 1, + 2, + 'skip a few', + 99, + 100 + ] +} + +---------- +{ + 'foo': true, + '-foo': false, + 'bar': 42, + 'baz': 3.14, + 'qux': 'string', + 'thud': NULL, + 'plugh': [ + 1, + 2, + 999.876, + 299800000, + DATE '2020-01-01', + TIME '1:02:03.004005006', + TIME WITH TIME ZONE '1:02:03.004005006 +00:00:00', + TIMESTAMP '2020-01-01 0:00:00.0', + TIMESTAMP WITH TIME ZONE '2020-01-01 0:00:00.0 +00:00:00', + 'abcdef', + MISSING + ], + 'xyzzy': << + { + 'n': 1 + }, + { + 'n': 2 + }, + { + 'n': 3 + }, + { + 'n': 4 + }, + { + 'n': 5 + }, + { + 'n': 6 + }, + { + 'n': 7 + }, + { + 'n': 8 + }, + { + 'n': 9 + }, + { + 'n': 10 + } + >>, + 'waldo': [ + 1, + 2, + 'skip a few', + 99, + 100 + ] +}