diff --git a/CHANGELOG.md b/CHANGELOG.md index d04b324e..47576046 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Changed +- *BREAKING:* partiql-ast: changed modeling of `BagOpExpr` `setq` field to be an `Option` +- *BREAKING:* partiql-ast: changed modeling of `GroupByExpr` `strategy` field to be an `Option` +- *BREAKING:* partiql-ast: changed modeling of `PathStep` to split `PathExpr` to `PathIndex` (e.g., `[2]`) and `PathProject` (e.g., `.a`) +- *BREAKING:* partiql-ast: changed modeling of `PathStep` to rename `PathWildcard` to `PathForEach` (for `[*]`) ### Added +- partiql-ast: Pretty-printing of AST via `ToPretty` trait ### Fixed +- Minor documentation issues ## [0.8.0] ### Changed diff --git a/extension/partiql-extension-visualize/src/ast_to_dot.rs b/extension/partiql-extension-visualize/src/ast_to_dot.rs index 3d80a3fe..3c641eb9 100644 --- a/extension/partiql-extension-visualize/src/ast_to_dot.rs +++ b/extension/partiql-extension-visualize/src/ast_to_dot.rs @@ -631,8 +631,9 @@ impl ToDot for AstToDot { impl ToDot for AstToDot { fn to_dot(&mut self, out: &mut Scope<'_, '_>, ast: &ast::PathStep) -> Targets { match &ast { - ast::PathStep::PathExpr(e) => self.to_dot(out, e), - ast::PathStep::PathWildCard => vec![out.node_auto_labelled("*").id()], + ast::PathStep::PathProject(e) => self.to_dot(out, e), + ast::PathStep::PathIndex(e) => self.to_dot(out, e), + ast::PathStep::PathForEach => vec![out.node_auto_labelled("*").id()], ast::PathStep::PathUnpivot => vec![out.node_auto_labelled("Unpivot").id()], } } diff --git a/partiql-ast-passes/src/name_resolver.rs b/partiql-ast-passes/src/name_resolver.rs index 5ce5b09c..e6994668 100644 --- a/partiql-ast-passes/src/name_resolver.rs +++ b/partiql-ast-passes/src/name_resolver.rs @@ -514,7 +514,8 @@ fn infer_alias(expr: &ast::Expr) -> Option { match expr { ast::Expr::VarRef(ast::AstNode { node, .. }) => Some(node.name.clone()), ast::Expr::Path(ast::AstNode { node, .. }) => match node.steps.last() { - Some(ast::PathStep::PathExpr(expr)) => infer_alias(&expr.index), + Some(ast::PathStep::PathProject(expr)) => infer_alias(&expr.index), + Some(ast::PathStep::PathIndex(expr)) => infer_alias(&expr.index), _ => None, }, _ => None, diff --git a/partiql-ast/Cargo.toml b/partiql-ast/Cargo.toml index 7e0828f7..a9f1624f 100644 --- a/partiql-ast/Cargo.toml +++ b/partiql-ast/Cargo.toml @@ -9,8 +9,8 @@ readme = "../README.md" keywords = ["sql", "ast", "query", "compilers", "interpreters"] categories = ["database", "compilers", "ast-implementations"] exclude = [ - "**/.git/**", - "**/.github/**", + "**/.git/**", + "**/.github/**", ] version.workspace = true edition.workspace = true @@ -23,16 +23,19 @@ bench = false indexmap = { version = "1.9", default-features = false } rust_decimal = { version = "1.25.0", default-features = false, features = ["std"] } serde = { version = "1.*", features = ["derive"], optional = true } +pretty = "0.12" +thiserror = "1.0" [dev-dependencies] +partiql-parser = { path = "../partiql-parser", version = "0.8" } [features] default = [] serde = [ - "dep:serde", - "rust_decimal/serde-with-str", - "rust_decimal/serde", - "indexmap/serde", + "dep:serde", + "rust_decimal/serde-with-str", + "rust_decimal/serde", + "indexmap/serde", ] [dependencies.partiql-ast-macros] diff --git a/partiql-ast/src/ast.rs b/partiql-ast/src/ast.rs index 7b712a4e..5b9e7d94 100644 --- a/partiql-ast/src/ast.rs +++ b/partiql-ast/src/ast.rs @@ -283,7 +283,7 @@ pub struct BagOpExpr { #[visit(skip)] pub bag_op: BagOperator, #[visit(skip)] - pub setq: SetQuantifier, + pub setq: Option, pub lhs: Box>, pub rhs: Box>, } @@ -644,7 +644,7 @@ pub enum CallArg { PositionalType(Type), /// named argument to a function call (e.g., the `"from" : 2` in `substring(a, "from":2)` Named(CallArgNamed), - /// E.g. `AS: VARCHAR` in `CAST('abc' AS VARCHAR` + /// E.g. `AS: VARCHAR` in `CAST('abc' AS VARCHAR)` NamedType(CallArgNamedType), } @@ -676,9 +676,10 @@ pub struct Path { #[derive(Visit, Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum PathStep { - PathExpr(PathExpr), + PathProject(PathExpr), + PathIndex(PathExpr), #[visit(skip)] - PathWildCard, + PathForEach, #[visit(skip)] PathUnpivot, } @@ -789,7 +790,7 @@ pub enum JoinSpec { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct GroupByExpr { #[visit(skip)] - pub strategy: GroupingStrategy, + pub strategy: Option, pub keys: Vec>, #[visit(skip)] pub group_as_alias: Option, diff --git a/partiql-ast/src/lib.rs b/partiql-ast/src/lib.rs index 6fb8ca12..30f517da 100644 --- a/partiql-ast/src/lib.rs +++ b/partiql-ast/src/lib.rs @@ -8,4 +8,7 @@ //! This API is currently unstable and subject to change. pub mod ast; + +pub mod pretty; + pub mod visit; diff --git a/partiql-ast/src/pretty.rs b/partiql-ast/src/pretty.rs new file mode 100644 index 00000000..9b812966 --- /dev/null +++ b/partiql-ast/src/pretty.rs @@ -0,0 +1,1362 @@ +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)?) + } +} + +impl PrettyDoc for AstNode +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.node.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 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 + D: DocAllocator<'b, A>, + D::Doc: Clone, + A: Clone, + { + if self.with.is_some() { + todo!("WITH Clause") + } + self.query.pretty_doc(arena) + } +} + +impl PrettyDoc for Query { + 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 Query { + set, + order_by, + limit_offset, + } = self; + + let clauses = [ + Some(set.pretty_doc(arena)), + order_by.as_ref().map(|inner| inner.pretty_doc(arena)), + limit_offset.as_ref().map(|inner| inner.pretty_doc(arena)), + ] + .into_iter() + .flatten(); + + arena.intersperse(clauses, arena.softline()).group() + } +} + +impl PrettyDoc for QuerySet { + 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 { + 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), + } + } +} + +impl PrettyDoc for BagOpExpr { + 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 op = match self.bag_op { + BagOperator::Union => "UNION", + BagOperator::Except => "EXCEPT", + BagOperator::Intersect => "INTERSECT", + BagOperator::OuterUnion => "OUTER UNION", + BagOperator::OuterExcept => "OUTER EXCEPT", + BagOperator::OuterIntersect => "OUTER INTERSECT", + }; + let op = arena.text(op); + let op = match self.setq { + None => op, + Some(SetQuantifier::All) => op.append(" ALL"), + 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); + + arena.intersperse([lhs, op, rhs], arena.hardline()).group() + } +} + +impl PrettyDoc for QueryTable { + 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.table_name.pretty_doc(arena) + } +} + +impl PrettyDoc for Select { + 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 Select { + project, + from, + from_let, + where_clause, + group_by, + having, + } = self; + let clauses = [ + Some(project.pretty_doc(arena).group()), + from.as_ref().map(|inner| inner.pretty_doc(arena).group()), + from_let + .as_ref() + .map(|inner| inner.pretty_doc(arena).group()), + where_clause + .as_ref() + .map(|inner| inner.pretty_doc(arena).group()), + group_by + .as_ref() + .map(|inner| inner.pretty_doc(arena).group()), + having.as_ref().map(|inner| inner.pretty_doc(arena).group()), + ] + .into_iter() + .flatten(); + + arena.intersperse(clauses, arena.softline()).group() + } +} + +impl PrettyDoc for Projection { + 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, + { + if self.setq.is_some() { + todo!("project SetQuantifier") + } + self.kind.pretty_doc(arena) + } +} + +impl PrettyDoc for ProjectionKind { + 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 { + ProjectionKind::ProjectStar => arena.text("SELECT *"), + ProjectionKind::ProjectList(l) => { + pretty_annotated_doc("SELECT", pretty_list(l, MINOR_NEST_INDENT, arena), arena) + } + ProjectionKind::ProjectPivot(ProjectPivot { key, value }) => { + let parts = [ + key.pretty_doc(arena), + arena.text("AT"), + value.pretty_doc(arena), + ]; + let decl = arena.intersperse(parts, arena.space()).group(); + pretty_annotated_doc("PIVOT", decl, arena) + } + ProjectionKind::ProjectValue(ctor) => { + pretty_annotated_expr("SELECT VALUE", ctor, MINOR_NEST_INDENT, arena) + } + } + .group() + } +} + +impl PrettyDoc for ProjectItem { + 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 { + ProjectItem::ProjectAll(_) => { + todo!("ProjectItem::ProjectAll; remove this?") + } + ProjectItem::ProjectExpr(e) => e.pretty_doc(arena), + } + } +} + +impl PrettyDoc for ProjectExpr { + 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_source_as_alias(&self.expr, self.as_alias.as_ref(), arena) + .unwrap_or_else(|| self.expr.pretty_doc(arena)) + } +} + +impl PrettyDoc for Expr { + 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 { + Expr::Lit(inner) => inner.pretty_doc(arena), + Expr::VarRef(inner) => inner.pretty_doc(arena), + Expr::BinOp(inner) => inner.pretty_doc(arena), + Expr::UniOp(inner) => inner.pretty_doc(arena), + Expr::Like(inner) => inner.pretty_doc(arena), + Expr::Between(inner) => inner.pretty_doc(arena), + Expr::In(inner) => inner.pretty_doc(arena), + Expr::Case(inner) => inner.pretty_doc(arena), + Expr::Struct(inner) => inner.pretty_doc(arena), + Expr::Bag(inner) => inner.pretty_doc(arena), + Expr::List(inner) => inner.pretty_doc(arena), + Expr::Sexp(inner) => inner.pretty_doc(arena), + Expr::Path(inner) => inner.pretty_doc(arena), + Expr::Call(inner) => inner.pretty_doc(arena), + + Expr::CallAgg(inner) => inner.pretty_doc(arena), + + Expr::Query(inner) => { + let inner = inner.pretty_doc(arena).group(); + arena + .text("(") + .append(inner.nest(SUBQUERY_INDENT)) + .append(arena.text(")")) + } + Expr::Error => { + unreachable!(); + } + } + } +} + +impl PrettyDoc for Path { + 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 Path { root, steps } = self; + let mut path = root.pretty_doc(arena); + for step in steps { + path = path.append(match step { + PathStep::PathProject(e) => arena.text(".").append(e.index.pretty_doc(arena)), + PathStep::PathIndex(e) => arena + .text("[") + .append(e.index.pretty_doc(arena)) + .append(arena.text("]")), + PathStep::PathForEach => arena.text("[*]"), + PathStep::PathUnpivot => arena.text(".*"), + }); + } + + path + } +} + +impl PrettyDoc for VarRef { + 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 name = self.name.pretty_doc(arena); + match self.qualifier { + ScopeQualifier::Unqualified => name, + ScopeQualifier::Qualified => arena.text("@").append(name).group(), + } + } +} + +impl PrettyDoc for Lit { + 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 { + Lit::Null => arena.text("NULL"), + Lit::Missing => arena.text("MISSING"), + Lit::Int8Lit(inner) => arena.text(inner.to_string()), + Lit::Int16Lit(inner) => arena.text(inner.to_string()), + Lit::Int32Lit(inner) => arena.text(inner.to_string()), + Lit::Int64Lit(inner) => arena.text(inner.to_string()), + Lit::DecimalLit(inner) => inner.pretty_doc(arena), + Lit::NumericLit(inner) => inner.pretty_doc(arena), + Lit::RealLit(inner) => arena.text(inner.to_string()), + Lit::FloatLit(inner) => arena.text(inner.to_string()), + Lit::DoubleLit(inner) => arena.text(inner.to_string()), + Lit::BoolLit(inner) => arena.text(inner.to_string()), + Lit::IonStringLit(inner) => inner.pretty_doc(arena), + Lit::CharStringLit(inner) => inner.pretty_doc(arena), + Lit::NationalCharStringLit(inner) => inner.pretty_doc(arena), + Lit::BitStringLit(inner) => inner.pretty_doc(arena), + Lit::HexStringLit(inner) => inner.pretty_doc(arena), + Lit::StructLit(inner) => inner.pretty_doc(arena), + Lit::BagLit(inner) => inner.pretty_doc(arena), + Lit::ListLit(inner) => inner.pretty_doc(arena), + Lit::TypedLit(s, ty) => { + let ty = ty.pretty_doc(arena); + let s = s.pretty_doc(arena); + pretty_seperated_doc(arena.space(), [ty, s], 0, arena) + } + } + } +} + +impl PrettyDoc for Type { + 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 { + Type::CustomType(cty) => cty.pretty_doc(arena), + _ => { + todo!("Non-custom type type") + } + } + } +} + +impl PrettyDoc for CustomType { + 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_seperated(arena.space(), &self.parts, 0, arena) + } +} + +impl PrettyDoc for CustomTypePart { + 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 { + CustomTypePart::Name(sym) => sym.pretty_doc(arena), + CustomTypePart::Parameterized(sym, param) => { + let sym = sym.pretty_doc(arena); + let list = pretty_list(param, 0, arena); + let list = pretty_parenthesized_doc(list, arena); + sym.append(list) + } + } + } +} + +impl PrettyDoc for CustomTypeParam { + 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 { + CustomTypeParam::Lit(l) => l.pretty_doc(arena), + CustomTypeParam::Type(ty) => ty.pretty_doc(arena), + } + } +} + +impl PrettyDoc for BinOp { + 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 BinOp { kind, lhs, rhs } = self; + let (nest, sym) = match kind { + BinOpKind::Add => (0, "+"), + BinOpKind::Div => (0, "/"), + BinOpKind::Exp => (0, "^"), + BinOpKind::Mod => (0, "%"), + BinOpKind::Mul => (0, "*"), + BinOpKind::Sub => (0, "-"), + BinOpKind::And => (MINOR_NEST_INDENT, "AND"), + BinOpKind::Or => (MINOR_NEST_INDENT, "OR"), + BinOpKind::Concat => (0, "||"), + BinOpKind::Eq => (0, "="), + BinOpKind::Gt => (0, ">"), + BinOpKind::Gte => (0, ">="), + BinOpKind::Lt => (0, "<"), + BinOpKind::Lte => (0, "<="), + BinOpKind::Ne => (0, "<>"), + BinOpKind::Is => (0, "IS"), + }; + let op = arena.text(sym); + let lhs = lhs.pretty_doc(arena).nest(nest); + let rhs = rhs.pretty_doc(arena).nest(nest); + let sep = arena.space(); + let expr = arena.intersperse([lhs, op, rhs], sep).group(); + let paren_expr = [arena.text("("), expr, arena.text(")")]; + arena.concat(paren_expr).group() + } +} + +impl PrettyDoc for UniOp { + 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, + { + // TODO NOT LIKE, NOT IN, NOT BETWEEN? + let UniOp { kind, expr } = self; + let (sym, paren) = match kind { + UniOpKind::Pos => ("+", false), + UniOpKind::Neg => ("-", false), + UniOpKind::Not => ("NOT ", true), + }; + let op = arena.text(sym); + let expr = expr.pretty_doc(arena); + if paren { + let open = arena.text("("); + let close = arena.text(")"); + arena.concat([op, open, expr, close]).group() + } else { + arena.concat([op, expr]).group() + } + } +} + +impl PrettyDoc for Like { + 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 Like { + value, + pattern, + escape, + } = self; + + let sep = arena.space(); + let value = value.pretty_doc(arena); + let kw_like = arena.text("LIKE"); + let pattern = pattern.pretty_doc(arena); + if let Some(escape) = escape { + let kw_esc = arena.text("ESCAPE"); + let escape = escape.pretty_doc(arena); + arena.intersperse([value, kw_like, pattern, kw_esc, escape], sep) + } else { + arena.intersperse([value, kw_like, pattern], sep) + } + .group() + } +} + +impl PrettyDoc for Between { + 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 Between { value, from, to } = self; + + let value = value.pretty_doc(arena); + let kw_b = arena.text("BETWEEN"); + let kw_a = arena.text("AND"); + let from = from.pretty_doc(arena); + let to = to.pretty_doc(arena); + let sep = arena.space(); + let expr = arena + .intersperse([value, kw_b, from, kw_a, to], sep) + .group(); + expr.group() + } +} + +impl PrettyDoc for In { + 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 In { lhs, rhs } = self; + + let kw_in = arena.text("IN"); + let lhs = lhs.pretty_doc(arena); + let rhs = rhs.pretty_doc(arena); + let sep = arena.space(); + let expr = arena.intersperse([lhs, kw_in, rhs], sep).group(); + expr.group() + } +} + +impl PrettyDoc for Case { + 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 { + Case::SimpleCase(inner) => inner.pretty_doc(arena), + Case::SearchedCase(inner) => inner.pretty_doc(arena), + } + } +} + +impl PrettyDoc for SimpleCase { + 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 SimpleCase { + expr, + cases, + default, + } = self; + + let kw_case = arena.text("CASE"); + let search = expr.pretty_doc(arena); + let branches = 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() + }); + let branches = arena + .intersperse(branches, arena.softline()) + .group() + .nest(MINOR_NEST_INDENT); + let default = default + .as_ref() + .map(|d| arena.text("ELSE ").append(d.pretty_doc(arena))); + + if let Some(default) = default { + arena.intersperse([kw_case, search, branches, default], arena.softline()) + } else { + arena.intersperse([kw_case, search, branches], arena.softline()) + } + } +} + +impl PrettyDoc for SearchedCase { + 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 SearchedCase { cases, default } = self; + + let kw_case = arena.text("CASE"); + let branches = 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() + }); + let branches = arena + .intersperse(branches, arena.softline()) + .group() + .nest(MINOR_NEST_INDENT); + let default = default + .as_ref() + .map(|d| arena.text("ELSE ").append(d.pretty_doc(arena))); + + if let Some(default) = default { + arena.intersperse([kw_case, branches, default], arena.softline()) + } else { + arena.intersperse([kw_case, branches], arena.softline()) + } + } +} + +impl PrettyDoc for Struct { + 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.fields.iter().map(|p| unsafe { + let x: &'b StructExprPair = std::mem::transmute(p); + x + }); + pretty_seq(wrapped, "{", "}", ",", arena) + } +} + +pub struct StructExprPair(pub ExprPair); + +impl PrettyDoc for StructExprPair { + 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 = self.0.first.pretty_doc(arena); + let v = self.0.second.pretty_doc(arena); + let sep = arena.text(": "); + + k.append(sep).group().append(v).group() + } +} + +impl PrettyDoc for Bag { + 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.values, "<<", ">>", ",", arena) + } +} + +impl PrettyDoc for List { + 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.values, "[", "]", ",", arena) + } +} + +impl PrettyDoc for Sexp { + 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, + { + todo!("remove s-expr from ast?"); + } +} + +impl PrettyDoc for Call { + 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 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(arena.text(")")) + } +} + +impl PrettyDoc for CallAgg { + 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 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(arena.text(")")) + } +} + +impl PrettyDoc for CallArg { + 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 { + CallArg::Star() => arena.text("*"), + CallArg::Positional(arg) => arg.pretty_doc(arena), + CallArg::PositionalType(_) => { + todo!("CallArg::PositionalType") + } + CallArg::Named(arg) => arg.pretty_doc(arena), + CallArg::NamedType(_) => { + todo!("CallArg::NamedType") + } + } + } +} + +impl PrettyDoc for CallArgNamed { + 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 CallArgNamed { name, value } = self; + let name = name.pretty_doc(arena); + let value = value.pretty_doc(arena); + pretty_seperated_doc(":", [name, value], 0, arena) + } +} + +impl PrettyDoc for SymbolPrimitive { + 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 sym = arena.text(self.value.as_str()); + match self.case { + CaseSensitivity::CaseSensitive => arena.text("\"").append(sym).append(arena.text("\"")), + CaseSensitivity::CaseInsensitive => sym, + } + } +} + +impl PrettyDoc for FromClause { + 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_annotated_expr("FROM", &self.source, MINOR_NEST_INDENT, arena) + } +} + +impl PrettyDoc for FromSource { + 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 { + FromSource::FromLet(fl) => fl.pretty_doc(arena), + FromSource::Join(join) => join.pretty_doc(arena), + } + } +} + +impl PrettyDoc for FromLet { + 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 FromLet { + expr, + kind, + as_alias, + at_alias, + by_alias, + } = self; + + let expr = expr.pretty_doc(arena); + let as_alias = pretty_as_alias(as_alias.as_ref(), arena); + let at_alias = pretty_at_alias(at_alias.as_ref(), arena); + let by_alias = pretty_by_alias(by_alias.as_ref(), arena); + let aliases: Vec<_> = [as_alias, at_alias, by_alias] + .into_iter() + .flatten() + .collect(); + + let clause = match kind { + FromLetKind::Scan => expr, + FromLetKind::Unpivot => pretty_annotated_doc("UNPIVOT", expr, arena), + }; + + if aliases.is_empty() { + clause + } else { + clause.append(arena.concat(aliases).group()) + } + .group() + } +} + +impl PrettyDoc for Join { + 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 Join { + kind, + left, + right, + predicate, + } = self; + + let arms = [left.as_ref(), right.as_ref()]; + let kw_join = match kind { + JoinKind::Cross => " CROSS JOIN ", + JoinKind::Inner => " INNER JOIN ", + JoinKind::Left => " LEFT JOIN ", + JoinKind::Right => " RIGHT JOIN ", + JoinKind::Full => " FULL JOIN ", + }; + + match (kind, predicate) { + (JoinKind::Cross, Some(_)) => { + todo!("CROSS JOIN with predicate") + } + (JoinKind::Cross, None) => pretty_list(arms, 0, arena), + (_, None) => pretty_seperated(kw_join, arms, 0, arena), + (_, Some(pred)) => match &pred.node { + JoinSpec::Natural => { + let kw = arena.text(" NATURAL").append(kw_join); + pretty_seperated(kw, arms, 0, arena) + } + JoinSpec::On(on) => { + let join = pretty_seperated(kw_join, arms, 0, arena); + let pred = arena + .softline() + .append(arena.text("ON")) + .append(arena.softline()) + .append(on.pretty_doc(arena).nest(MINOR_NEST_INDENT)); + 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 pred = arena + .softline() + .append(arena.text("USING")) + .append(arena.softline()) + .append(using); + join.append(pred) + } + }, + } + .group() + } +} + +impl PrettyDoc for Let { + 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, + { + todo!("LET") + } +} + +impl PrettyDoc for WhereClause { + 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_annotated_expr("WHERE", &self.expr, MINOR_NEST_INDENT, arena) + } +} + +impl PrettyDoc for GroupByExpr { + 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 GroupByExpr { + strategy, + keys, + group_as_alias, + } = self; + + let mut doc = match strategy { + None => arena.text("GROUP"), + Some(GroupingStrategy::GroupFull) => arena.text("GROUP ALL"), + Some(GroupingStrategy::GroupPartial) => arena.text("GROUP PARTIAL"), + }; + + 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)); + } + + match group_as_alias { + None => doc, + Some(gas) => { + let gas = pretty_source_as_alias("GROUP", Some(gas), arena); + doc.append(gas) + } + } + .group() + } +} + +impl PrettyDoc for GroupKey { + 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_source_as_alias(&self.expr, self.as_alias.as_ref(), arena) + .unwrap_or_else(|| self.expr.pretty_doc(arena)) + } +} + +impl PrettyDoc for HavingClause { + 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_annotated_expr("HAVING", &self.expr, MINOR_NEST_INDENT, arena) + } +} + +impl PrettyDoc for OrderByExpr { + 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, + { + if self.sort_specs.is_empty() { + arena.text("ORDER BY PRESERVE") + } else { + pretty_annotated_doc( + "ORDER BY", + pretty_list(&self.sort_specs, MINOR_NEST_INDENT, arena), + arena, + ) + } + .group() + } +} + +impl PrettyDoc for SortSpec { + 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 SortSpec { + expr, + ordering_spec, + null_ordering_spec, + } = self; + let mut doc = expr.pretty_doc(arena); + if let Some(os) = ordering_spec { + let os = arena.space().append(os.pretty_doc(arena)).group(); + doc = doc.append(os) + }; + if let Some(nos) = null_ordering_spec { + let nos = arena.space().append(nos.pretty_doc(arena)).group(); + doc = doc.append(nos) + }; + + doc.group() + } +} + +impl PrettyDoc for OrderingSpec { + 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(match self { + OrderingSpec::Asc => "ASC", + OrderingSpec::Desc => "DESC", + }) + } +} + +impl PrettyDoc for NullOrderingSpec { + 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(match self { + NullOrderingSpec::First => "NULLS FIRST", + NullOrderingSpec::Last => "NULLS LAST", + }) + } +} + +impl PrettyDoc for LimitOffsetClause { + 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 limit = self + .limit + .as_ref() + .map(|l| pretty_annotated_expr("LIMIT", l, MINOR_NEST_INDENT, arena)); + + let offset = self + .offset + .as_ref() + .map(|o| pretty_annotated_expr("OFFSET", o, MINOR_NEST_INDENT, arena)); + + match (limit, offset) { + (None, None) => unreachable!(), + (Some(limit), None) => limit, + (None, Some(offset)) => offset, + (Some(limit), Some(offset)) => limit.append(arena.softline()).append(offset), + } + } +} + +fn pretty_annotated_expr<'b, P, D, A>( + annot: &'static str, + expr: &'b P, + nest: isize, + arena: &'b D, +) -> DocBuilder<'b, D, A> +where + P: PrettyDoc, + D: DocAllocator<'b, A>, + D::Doc: Clone, + A: Clone, +{ + pretty_annotated_doc(annot, expr.pretty_doc(arena).nest(nest), arena) +} + +fn pretty_annotated_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() +} + +fn pretty_parenthesized_expr<'b, P, D, A>( + expr: &'b P, + nest: isize, + arena: &'b D, +) -> DocBuilder<'b, D, A> +where + P: PrettyDoc, + D: DocAllocator<'b, A>, + D::Doc: Clone, + A: Clone, +{ + 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(arena.space()) + .append(doc) + .append(arena.space()) + .append(arena.text(")")) + .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 start = arena.text(start); + let end = arena.text(end); + let sep = arena.text(sep).append(arena.line()); + let seq = list.into_iter().map(|l| l.pretty_doc(arena)); + let body = arena.line().append(arena.intersperse(seq, sep)).group(); + start + .append(body.nest(MINOR_NEST_INDENT)) + .append(arena.line()) + .append(end) + .group() +} + +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, + sym: Option<&'b SymbolPrimitive>, + arena: &'b D, +) -> Option> +where + D: DocAllocator<'b, A>, + D::Doc: Clone, + A: Clone, +{ + sym.map(|sym| { + arena + .space() + .append(arena.text(kw)) + .append(arena.space()) + .append(sym.pretty_doc(arena)) + .group() + }) +} + +fn pretty_source_as_alias<'b, S, D, A>( + source: &'b S, + as_alias: Option<&'b SymbolPrimitive>, + arena: &'b D, +) -> Option> +where + S: PrettyDoc + ?Sized, + D: DocAllocator<'b, A>, + D::Doc: Clone, + A: Clone, +{ + pretty_as_alias(as_alias, arena).map(|alias| { + let expr = source.pretty_doc(arena); + arena.concat([expr, alias]).group() + }) +} + +fn pretty_as_alias<'b, D, A>( + sym: Option<&'b SymbolPrimitive>, + arena: &'b D, +) -> Option> +where + D: DocAllocator<'b, A>, + D::Doc: Clone, + A: Clone, +{ + pretty_alias_helper("AS", sym, arena) +} + +fn pretty_at_alias<'b, D, A>( + sym: Option<&'b SymbolPrimitive>, + arena: &'b D, +) -> Option> +where + D: DocAllocator<'b, A>, + D::Doc: Clone, + A: Clone, +{ + pretty_alias_helper("AT", sym, arena) +} + +fn pretty_by_alias<'b, D, A>( + sym: Option<&'b SymbolPrimitive>, + arena: &'b D, +) -> Option> +where + D: DocAllocator<'b, A>, + D::Doc: Clone, + A: Clone, +{ + pretty_alias_helper("BY", sym, arena) +} diff --git a/partiql-ast/tests/common.rs b/partiql-ast/tests/common.rs index 60a4158b..ec30d1e2 100644 --- a/partiql-ast/tests/common.rs +++ b/partiql-ast/tests/common.rs @@ -1,3 +1,142 @@ +use partiql_ast::pretty::ToPretty; +use partiql_parser::ParserResult; + pub fn setup() { // setup test code goes here } + +#[track_caller] +#[inline] +fn parse(statement: &str) -> ParserResult<'_> { + partiql_parser::Parser::default().parse(statement) +} + +#[track_caller] +#[inline] +fn pretty_print_test(statement: &str) { + let res = parse(statement); + assert!(res.is_ok()); + let res = res.unwrap(); + // TODO https://github.com/partiql/partiql-lang-rust/issues/473 + for w in [180, 120, 80, 40, 30, 20, 10] { + println!("{:-> group by foo order by n desc", + ); +} + +#[test] +fn pretty2() { + pretty_print_test( + "select foo,bar, baz,thud,grunt, count(1) as n from + (SELECT * FROM table1) + where (bar between 3 and 25 AND baz NOT LIKE 'example%') OR foo.a.b[*] IS MISSING + group by foo + order by n desc", + ); +} + +#[test] +fn pretty_having_limit_offset() { + pretty_print_test("SELECT a FROM foo GROUP BY a HAVING a > 2 ORDER BY a LIMIT 1 OFFSET 1"); +} + +#[test] +fn pretty_select_value_unpivot() { + pretty_print_test("SELECT VALUE foo FROM (SELECT VALUE v FROM UNPIVOT e AS v) AS foo"); +} + +#[test] +fn pretty_select_value_tuple_ctor() { + pretty_print_test("SELECT VALUE {'a':v.a, 'b':v.b} FROM [{'a':1, 'b':1}, {'a':2, 'b':2}] AS v"); +} + +#[test] +fn pretty_from_comma() { + pretty_print_test("SELECT a, b FROM T1, T2"); +} + +#[test] +fn pretty_expr_in() { + pretty_print_test("(a, b) IN ((1, 2), (3, 4))"); +} + +#[test] +fn pretty_setop() { + pretty_print_test( + "(SELECT a1 FROM b1 ORDER BY c1 LIMIT d1 OFFSET e1) + UNION + (SELECT a2 FROM b2 ORDER BY c2 LIMIT d2 OFFSET e2) + ORDER BY c3 LIMIT d3 OFFSET e3", + ); +} + +#[test] +fn pretty_bagop() { + pretty_print_test( + " + ( + (SELECT a1 FROM b1 ORDER BY c1 LIMIT d1 OFFSET e1) + UNION DISTINCT + (SELECT a2 FROM b2 ORDER BY c2 LIMIT d2 OFFSET e2) + ) + OUTER UNION ALL + (SELECT a3 FROM b3 ORDER BY c3 LIMIT d3 OFFSET e3) + ORDER BY c4 LIMIT d4 OFFSET e4", + ); +} + +#[test] +fn pretty_join() { + pretty_print_test( + " + SELECT t1.id AS id, t1.val AS val1, t2.val AS val2 + FROM table1 AS t1 JOIN table1_null_row AS t2 ON t1.id = t2.id", + ); +} + +#[test] +fn pretty_kw_fns() { + pretty_print_test("trim(trailing from 'test')"); + pretty_print_test("POSITION('abc' IN 'abcdefg')"); + pretty_print_test("substring('test', 100, 50)"); + pretty_print_test("substring('test', 100)"); +} + +#[test] +fn pretty_typed_lits() { + pretty_print_test("TIME WITH TIME ZONE '23:59:59.1234567890+18:00'"); + pretty_print_test("TIME (3) WITH TIME ZONE '12:59:31'"); +} + +#[test] +fn pretty_case() { + pretty_print_test("SELECT VALUE CASE WHEN x + 1 < i THEN '< ONE' WHEN x + 1 = f THEN 'TWO' WHEN (x + 1 > d) AND (x + 1 < 100) THEN '>= THREE < 100' ELSE '?' END FROM << -1.0000, i, f, d, 100e0, null, missing >> AS x"); + pretty_print_test("SELECT VALUE CASE x + 1 WHEN NULL THEN 'shouldnt be null' WHEN MISSING THEN 'shouldnt be missing' WHEN i THEN 'ONE' WHEN f THEN 'TWO' WHEN d THEN 'THREE' END FROM << i, f, d, null, missing >> AS x"); +} + +#[test] +fn pretty_pivot() { + pretty_print_test( + " + PIVOT foo.a AT foo.b + FROM <<{'a': 1, 'b':'I'}, {'a': 2, 'b':'II'}, {'a': 3, 'b':'III'}>> AS foo + ORDER BY a + LIMIT 1 OFFSET 1 + ", + ); +} diff --git a/partiql-conformance-tests/Cargo.toml b/partiql-conformance-tests/Cargo.toml index 19b8a338..612406d0 100644 --- a/partiql-conformance-tests/Cargo.toml +++ b/partiql-conformance-tests/Cargo.toml @@ -28,7 +28,7 @@ required-features = ["report_tool"] bench = false [build-dependencies] -miette = { version ="5.*", features = ["fancy"] } +miette = { version = "5.*", features = ["fancy"] } partiql-conformance-test-generator = { path = "../partiql-conformance-test-generator", version = "0.8.*" } [dependencies] @@ -40,7 +40,7 @@ partiql-logical-planner = { path = "../partiql-logical-planner", version = "0.8. partiql-logical = { path = "../partiql-logical", version = "0.8.*" } partiql-value = { path = "../partiql-value", version = "0.8.*" } partiql-eval = { path = "../partiql-eval", version = "0.8.*" } -partiql-extension-ion = {path = "../extension/partiql-extension-ion", version = "0.8.*" } +partiql-extension-ion = { path = "../extension/partiql-extension-ion", version = "0.8.*" } ion-rs = "0.18" @@ -57,11 +57,12 @@ serde_json = { version = "1.*", optional = true } [features] default = ["base"] base = ["syntax", "semantics", "strict", "permissive"] -syntax=[] -semantics=[] -strict=[] -permissive=[] -experimental=[] -conformance_test=[] +syntax = [] +semantics = [] +strict = [] +permissive = [] +experimental = [] +conformance_test = [] report_tool = ["serde"] +test_pretty_print = [] serde = ["dep:serde", "dep:serde_json"] diff --git a/partiql-conformance-tests/tests/mod.rs b/partiql-conformance-tests/tests/mod.rs index 15c276af..74e1d507 100644 --- a/partiql-conformance-tests/tests/mod.rs +++ b/partiql-conformance-tests/tests/mod.rs @@ -33,7 +33,20 @@ impl From for eval::plan::EvaluationMode { #[track_caller] #[inline] pub(crate) fn parse(statement: &str) -> ParserResult { - partiql_parser::Parser::default().parse(statement) + let result = partiql_parser::Parser::default().parse(statement); + + #[cfg(feature = "test_pretty_print")] + if let Ok(result) = &result { + use partiql_ast::pretty::ToPretty; + let pretty = result.ast.to_pretty_string(80); + if let Ok(pretty) = pretty { + println!("{pretty}"); + } else { + panic!("failed pretty print"); + } + } + + result } #[track_caller] diff --git a/partiql-logical-planner/src/lower.rs b/partiql-logical-planner/src/lower.rs index d2fe5203..6499ace6 100644 --- a/partiql-logical-planner/src/lower.rs +++ b/partiql-logical-planner/src/lower.rs @@ -771,8 +771,9 @@ impl<'a, 'ast> Visitor<'ast> for AstToLogical<'a> { BagOperator::OuterIntersect => logical::BagOperator::OuterIntersect, }; let setq = match bag_op.node.setq { - SetQuantifier::All => logical::SetQuantifier::All, - SetQuantifier::Distinct => logical::SetQuantifier::Distinct, + Some(SetQuantifier::All) => logical::SetQuantifier::All, + Some(SetQuantifier::Distinct) => logical::SetQuantifier::Distinct, + None => logical::SetQuantifier::Distinct, }; let id = self.plan.add_operator(BindingsOp::BagOp(BagOp { @@ -845,14 +846,14 @@ impl<'a, 'ast> Visitor<'ast> for AstToLogical<'a> { Traverse::Continue } - fn exit_projection(&mut self, _projection: &'ast Projection) -> Traverse { + fn exit_projection(&mut self, projection: &'ast Projection) -> Traverse { let benv = self.exit_benv(); eq_or_fault!(self, benv.len(), 0, "benv.len() != 0"); let env = self.exit_env(); eq_or_fault!(self, env.len(), 0, "env.len() != 0"); - if let Some(SetQuantifier::Distinct) = _projection.setq { + if let Some(SetQuantifier::Distinct) = projection.setq { let id = self.plan.add_operator(BindingsOp::Distinct); self.current_clauses_mut().distinct.replace(id); } @@ -1382,8 +1383,8 @@ impl<'a, 'ast> Visitor<'ast> for AstToLogical<'a> { Traverse::Continue } - fn enter_path_step(&mut self, _path_step: &'ast PathStep) -> Traverse { - if let PathStep::PathExpr(expr) = _path_step { + fn enter_path_step(&mut self, path_step: &'ast PathStep) -> Traverse { + if let PathStep::PathIndex(expr) | PathStep::PathProject(expr) = path_step { self.enter_env(); match *(expr.index) { Expr::VarRef(_) => { @@ -1401,9 +1402,9 @@ impl<'a, 'ast> Visitor<'ast> for AstToLogical<'a> { Traverse::Continue } - fn exit_path_step(&mut self, _path_step: &'ast PathStep) -> Traverse { - let step = match _path_step { - PathStep::PathExpr(_s) => { + fn exit_path_step(&mut self, path_step: &'ast PathStep) -> Traverse { + let step = match path_step { + PathStep::PathProject(_s) | PathStep::PathIndex(_s) => { let mut env = self.exit_env(); eq_or_fault!(self, env.len(), 1, "env.len() != 1"); @@ -1425,8 +1426,8 @@ impl<'a, 'ast> Visitor<'ast> for AstToLogical<'a> { } } } - PathStep::PathWildCard => { - not_yet_implemented_fault!(self, "PathStep::PathWildCard".to_string()); + PathStep::PathForEach => { + not_yet_implemented_fault!(self, "PathStep::PathForEach".to_string()); } PathStep::PathUnpivot => { not_yet_implemented_fault!(self, "PathStep::PathUnpivot".to_string()); @@ -1627,8 +1628,9 @@ impl<'a, 'ast> Visitor<'ast> for AstToLogical<'a> { .map(|SymbolPrimitive { value, case: _ }| value.clone()); let strategy = match _group_by_expr.strategy { - GroupingStrategy::GroupFull => logical::GroupingStrategy::GroupFull, - GroupingStrategy::GroupPartial => logical::GroupingStrategy::GroupPartial, + None => logical::GroupingStrategy::GroupFull, + Some(GroupingStrategy::GroupFull) => logical::GroupingStrategy::GroupFull, + Some(GroupingStrategy::GroupPartial) => logical::GroupingStrategy::GroupPartial, }; // What follows is an approach to implement section 11.2.1 of the PartiQL spec diff --git a/partiql-parser/src/parse/partiql.lalrpop b/partiql-parser/src/parse/partiql.lalrpop index 65b350e9..d5b59d04 100644 --- a/partiql-parser/src/parse/partiql.lalrpop +++ b/partiql-parser/src/parse/partiql.lalrpop @@ -102,7 +102,7 @@ WithCycleClause : () = { // - all set operations are left-associative and are thus expressed as left-self-recursive rules QuerySet: ast::AstNode = { - => { + => { let lhs = strip_query(lhs); let rhs = strip_query_set(rhs, state, lo, hi); let bag_expr = state.node(ast::BagOpExpr { @@ -223,31 +223,26 @@ FwsClauses: ast::AstNode = { SelectClause: ast::AstNode = { "SELECT" "*" => state.node(ast::Projection { kind: ast::ProjectionKind::ProjectStar, - setq: Some(strategy) + setq: strategy, }, lo..hi), "SELECT" > => state.node(ast::Projection { kind: ast::ProjectionKind::ProjectList(project_items), - setq: Some(strategy), + setq: strategy, }, lo..hi), "SELECT" "VALUE" => state.node(ast::Projection { kind: ast::ProjectionKind::ProjectValue(value), - setq: Some(strategy), + setq: strategy, }, lo..hi), "PIVOT" "AT" => state.node(ast::Projection { kind: ast::ProjectionKind::ProjectPivot(ast::ProjectPivot { key, value }), - setq: None + setq: None, }, lo..hi), } #[inline] -SetQuantifierStrategy: ast::SetQuantifier = { - "ALL" => ast::SetQuantifier::All, - => { - match distinct { - Some(_) => ast::SetQuantifier::Distinct, - None => ast::SetQuantifier::All, - } - } +SetQuantifierStrategy: Option = { + "ALL" => Some(ast::SetQuantifier::All), + => distinct.map(|_|ast::SetQuantifier::Distinct) } #[inline] @@ -436,14 +431,9 @@ GroupClause: Box> = { } } #[inline] -GroupStrategy: ast::GroupingStrategy = { - "ALL" => ast::GroupingStrategy::GroupFull, - => { - match partial { - Some(_) => ast::GroupingStrategy::GroupPartial, - None => ast::GroupingStrategy::GroupFull, - } - } +GroupStrategy: Option = { + "ALL" => Some(ast::GroupingStrategy::GroupFull), + => partial.map(|_| ast::GroupingStrategy::GroupPartial), } #[inline] GroupByKeys: Vec> = { @@ -842,7 +832,6 @@ ExprPrecedence02: Synth = { PathExpr: ast::Path = { => { - let step = ast::PathStep::PathWildCard; ast::Path { root: Box::new(l.data), steps: s @@ -1109,16 +1098,16 @@ PathSteps: Vec = { } PathStep: ast::PathStep = { "." => { - ast::PathStep::PathExpr( ast::PathExpr{ index: Box::new(v) }) + ast::PathStep::PathProject( ast::PathExpr{ index: Box::new(v) }) }, "[" "*" "]" => { - ast::PathStep::PathWildCard + ast::PathStep::PathForEach }, "." "*" => { ast::PathStep::PathUnpivot }, "[" "]" => { - ast::PathStep::PathExpr( ast::PathExpr{ index: Box::new(*expr) }) + ast::PathStep::PathIndex( ast::PathExpr{ index: Box::new(*expr) }) }, }