From 711d28e764d291f22b56edfdbe9c6635fd255305 Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Sun, 23 Aug 2026 17:29:07 -0400 Subject: [PATCH 01/17] fmt: support named function args --- crates/squawk_fmt/src/fmt.rs | 214 ++++++++++++++- crates/squawk_fmt/tests/after/from.snap | 6 + .../squawk_fmt/tests/after/select_expr.snap | 6 + crates/squawk_fmt/tests/before/from.sql | 7 + .../squawk_fmt/tests/before/select_expr.sql | 6 + .../src/generated/syntax_kind.rs | 12 +- crates/squawk_parser/src/grammar.rs | 20 +- .../tests/data/ok/select_funcs.sql | 3 + .../snapshots/tests__select_funcs_ok.snap | 35 +++ .../squawk_syntax/src/ast/generated/nodes.rs | 258 ++---------------- crates/squawk_syntax/src/postgresql.ungram | 32 +-- crates/xtask/src/codegen.rs | 11 + 12 files changed, 328 insertions(+), 282 deletions(-) diff --git a/crates/squawk_fmt/src/fmt.rs b/crates/squawk_fmt/src/fmt.rs index bfdd5c2b..f839cf10 100644 --- a/crates/squawk_fmt/src/fmt.rs +++ b/crates/squawk_fmt/src/fmt.rs @@ -387,12 +387,90 @@ fn build_from_alias<'a>(alias: Option) -> Doc<'a> { .append(leading_comments(name.syntax())) .append(build_name(name.syntax())); } - if alias.columns().is_some() { - todo!("columns in from aliases are not supported yet") + if let Some(columns) = alias.columns() { + doc = doc.append(build_from_alias_columns(columns)); } doc } +fn build_from_alias_columns<'a>(columns: ast::FromAliasColumns) -> Doc<'a> { + match columns { + ast::FromAliasColumns::ColumnList(list) => { + let items = list + .column_names() + .map(|name| { + leading_comments(name.syntax()) + .append(build_name(name.syntax())) + .append(trailing_comments(name.syntax())) + }) + .collect(); + comments_before(list.syntax().clone()).append(build_from_alias_column_list( + list.l_paren_token(), + items, + list.r_paren_token(), + )) + } + ast::FromAliasColumns::ColumnDefList(list) => { + let items = list + .column_defs() + .map(|column| { + let mut doc = leading_comments(column.syntax()); + if let Some(name) = column.name() { + doc = doc.append(build_name(name.syntax())); + } + if let Some(ty) = column.ty() { + doc = doc + .append(Doc::space()) + .append(leading_comments(ty.syntax())) + .append(build_type(ty)); + } + if let Some(collate) = column.collate() { + doc = doc + .append(Doc::space()) + .append(leading_comments(collate.syntax())) + .append(build_collate_expr(collate)); + } + doc.append(trailing_comments(column.syntax())) + }) + .collect(); + comments_before(list.syntax().clone()).append(build_from_alias_column_list( + list.l_paren_token(), + items, + list.r_paren_token(), + )) + } + } +} + +fn build_from_alias_column_list<'a>( + l_paren: Option, + items: Vec>, + r_paren: Option, +) -> Doc<'a> { + let mut doc = Doc::nil(); + if let Some(l_paren) = l_paren { + doc = doc.append(comments_before(l_paren)); + } + doc = doc.append(Doc::text("(")); + if items.is_empty() { + if let Some(r_paren) = r_paren { + doc = doc.append(comments_before(r_paren)); + } + } else { + doc = doc.append( + Doc::list( + Itertools::intersperse( + items.into_iter(), + Doc::text(",").append(Doc::line_or_space()), + ) + .collect(), + ) + .nest(2), + ); + } + doc.append(Doc::text(")")).group() +} + fn build_group_by_list<'a>(list: ast::GroupByList) -> Doc<'a> { leading_comments(list.syntax()).append(build_group_bys(list.group_bys())) } @@ -827,19 +905,130 @@ fn build_call_arg_list<'a>(arg_list: ast::ArgList) -> Doc<'a> { } fn build_call_arg<'a>(arg: ast::Arg) -> Doc<'a> { - if let Some(_named_arg) = arg.named_arg() { - todo!("named function arguments are not supported yet") + let mut doc = if let Some(named_arg) = arg.named_arg() { + build_named_call_arg(named_arg) + } else { + let mut doc = Doc::nil(); + if arg.variadic_token().is_some() { + doc = doc.append(Doc::text("variadic")).append(Doc::space()); + } + if let Some(expr) = arg.expr() { + doc = doc + .append(leading_comments(expr.syntax())) + .append(build_expr(expr)); + } + doc + }; + if let Some(order_by_clause) = arg.order_by_clause() { + doc = doc + .append(Doc::space()) + .append(leading_comments(order_by_clause.syntax())) + .append(build_order_by_clause(order_by_clause)); + } + doc +} + +fn build_order_by_clause<'a>(clause: ast::OrderByClause) -> Doc<'a> { + let mut doc = Doc::text("order").append(Doc::space()); + if let Some(by_token) = clause.by_token() { + doc = doc.append(leading_comments_token(&by_token)); + } + doc = doc.append(Doc::text("by")); + + if let Some(list) = clause.sort_by_list() { + let items = list + .sort_bys() + .map(|sort_by| leading_comments(sort_by.syntax()).append(build_sort_by(sort_by))); + doc = doc + .append(Doc::space()) + .append(leading_comments(list.syntax())) + .append(Doc::list( + Itertools::intersperse(items, Doc::text(",").append(Doc::space())).collect(), + )); + } + doc +} + +fn build_sort_by<'a>(sort_by: ast::SortBy) -> Doc<'a> { + let mut doc = Doc::nil(); + if let Some(expr) = sort_by.expr() { + doc = doc + .append(leading_comments(expr.syntax())) + .append(build_expr(expr)); + } + + if let Some(order) = sort_by.sort_order() { + doc = doc + .append(Doc::space()) + .append(leading_comments(order.syntax())) + .append(match order { + ast::SortOrder::SortAsc(_) => Doc::text("asc"), + ast::SortOrder::SortDesc(_) => Doc::text("desc"), + ast::SortOrder::SortUsing(using) => { + let mut doc = Doc::text("using"); + if let Some(operator_call) = using.operator_call() { + doc = doc + .append(Doc::space()) + .append(leading_comments(operator_call.syntax())) + .append(build_operator_call(&operator_call)); + } else if let Some(op) = using.op() { + doc = doc + .append(Doc::space()) + .append(leading_comments(op.syntax())) + .append(build_operator(&op)); + } + doc + } + }); } - if let Some(_order_by_clause) = arg.order_by_clause() { - todo!("order by clauses in function arguments are not supported yet") + + if let Some(nulls_order) = sort_by.nulls_order() { + doc = doc + .append(Doc::space()) + .append(leading_comments(nulls_order.syntax())) + .append(Doc::text("nulls")) + .append(Doc::space()); + let suffix = match nulls_order { + ast::NullsOrder::NullsFirst(first) => first + .first_token() + .map(|token| leading_comments_token(&token)) + .unwrap_or_else(Doc::nil) + .append(Doc::text("first")), + ast::NullsOrder::NullsLast(last) => last + .last_token() + .map(|token| leading_comments_token(&token)) + .unwrap_or_else(Doc::nil) + .append(Doc::text("last")), + }; + doc = doc.append(suffix); } + doc.append(trailing_comments(sort_by.syntax())) +} + +fn build_named_call_arg<'a>(arg: ast::NamedArg) -> Doc<'a> { let mut doc = Doc::nil(); - if arg.variadic_token().is_some() { - doc = doc.append(Doc::text("variadic")).append(Doc::space()); + if let Some(name) = arg.name() { + doc = doc + .append(leading_comments(name.syntax())) + .append(build_name(name.syntax())); } + + if let Some(fat_arrow) = arg.fat_arrow_token() { + doc = doc + .append(Doc::space()) + .append(leading_comments_token(&fat_arrow)) + .append(Doc::text("=>")); + } else if let Some(colon_eq) = arg.colon_eq_token() { + doc = doc + .append(Doc::space()) + .append(leading_comments_token(&colon_eq)) + .append(Doc::text(":=")); + } + if let Some(expr) = arg.expr() { doc = doc + .append(Doc::space()) .append(leading_comments(expr.syntax())) .append(build_expr(expr)); } @@ -981,12 +1170,17 @@ fn build_cast_expr<'a>(cast_expr: ast::CastExpr) -> Doc<'a> { } fn build_collate_expr<'a>(collate: ast::Collate) -> Doc<'a> { - let mut doc = collate.expr().map(build_expr).unwrap_or_else(Doc::nil); + let expr = collate.expr(); + let has_expr = expr.is_some(); + let mut doc = expr.map(build_expr).unwrap_or_else(Doc::nil); if let Some(collate_token) = collate.collate_token() { doc = doc.append(comments_before(collate_token)); } - doc = doc.append(Doc::space()).append(Doc::text("collate")); + if has_expr { + doc = doc.append(Doc::space()); + } + doc = doc.append(Doc::text("collate")); if let Some(collation) = collate.collation_ref() { doc = doc diff --git a/crates/squawk_fmt/tests/after/from.snap b/crates/squawk_fmt/tests/after/from.snap index 38b71d64..b6c255c4 100644 --- a/crates/squawk_fmt/tests/after/from.snap +++ b/crates/squawk_fmt/tests/after/from.snap @@ -4,6 +4,12 @@ input_file: crates/squawk_fmt/tests/before/from.sql --- select * from foo; select * from public.foo as f, bar b; +select * from foo as f(id, display_name); +select * from foo f(id int, display_name text collate "C"); +select + * +from foo /* before alias */ as /* before alias name */ f /* before open paren */(/* after open paren */ id /* before comma */, + /* after comma */ display_name /* before close paren */); select * from users tablesample bernoulli(10) repeatable(42); select * diff --git a/crates/squawk_fmt/tests/after/select_expr.snap b/crates/squawk_fmt/tests/after/select_expr.snap index d690b6b1..bf8c6809 100644 --- a/crates/squawk_fmt/tests/after/select_expr.snap +++ b/crates/squawk_fmt/tests/after/select_expr.snap @@ -60,6 +60,12 @@ select foo(/* before distinct */ distinct /* after distinct */ 1), foo(variadic xs), foo(/* before variadic */ variadic /* after variadic */ xs), + foo(a => 1, b := 2), + foo(/* before name */ a /* before arrow */ => /* before value */ 1 /* before comma */, /* before arg */ b /* before assign */ := /* before value 2 */ 2), + array_agg(x order by y desc nulls last, z asc), + foo(a => x order by y), + array_agg(x order by y using > nulls first), + array_agg(x /* before order */ order /* before by */ by /* before first */ y /* before desc */ desc /* before nulls */ nulls /* before last */ last /* before comma */, /* before second */ z /* before asc */ asc), public.foo(1), foo /* before opening paren */(/* before first arg */ 1 /* before comma */, /* before second arg */ 2 /* before closing paren */), -- case expr diff --git a/crates/squawk_fmt/tests/before/from.sql b/crates/squawk_fmt/tests/before/from.sql index a13b4de4..338400d8 100644 --- a/crates/squawk_fmt/tests/before/from.sql +++ b/crates/squawk_fmt/tests/before/from.sql @@ -1,5 +1,12 @@ select * from foo; select * from public.foo as f, bar b; +select * from foo as f (id, display_name); +select * from foo f (id int, display_name text collate "C"); +select * from foo + /* before alias */ as /* before alias name */ f /* before open paren */ ( + /* after open paren */ id /* before comma */, + /* after comma */ display_name /* before close paren */ + ); select * from users tablesample bernoulli(10) repeatable (42); select * /* before from */ from diff --git a/crates/squawk_fmt/tests/before/select_expr.sql b/crates/squawk_fmt/tests/before/select_expr.sql index 96ee9506..eedef5c9 100644 --- a/crates/squawk_fmt/tests/before/select_expr.sql +++ b/crates/squawk_fmt/tests/before/select_expr.sql @@ -56,6 +56,12 @@ select foo ( /* before distinct */ DISTINCT /* after distinct */ 1 ), foo ( VARIADIC xs ), foo ( /* before variadic */ VARIADIC /* after variadic */ xs ), + foo(a => 1, b := 2), + foo(/* before name */ a /* before arrow */ => /* before value */ 1 /* before comma */, /* before arg */ b /* before assign */ := /* before value 2 */ 2), + array_agg(x order by y desc nulls last, z asc), + foo(a => x order by y), + array_agg(x order by y using > nulls first), + array_agg(x /* before order */ order /* before by */ by /* before first */ y /* before desc */ desc /* before nulls */ nulls /* before last */ last /* before comma */, /* before second */ z /* before asc */ asc), public.foo(1), foo /* before opening paren */ ( /* before first arg */ 1 /* before comma */, /* before second arg */ 2 /* before closing paren */ ), -- case expr diff --git a/crates/squawk_parser/src/generated/syntax_kind.rs b/crates/squawk_parser/src/generated/syntax_kind.rs index 208ad5e2..845fca62 100644 --- a/crates/squawk_parser/src/generated/syntax_kind.rs +++ b/crates/squawk_parser/src/generated/syntax_kind.rs @@ -20,7 +20,10 @@ pub enum SyntaxKind { L_CURLY, R_CURLY, L_ANGLE, + LTEQ, + NEQB, R_ANGLE, + GTEQ, AT, POUND, TILDE, @@ -35,8 +38,11 @@ pub enum SyntaxKind { UNDERSCORE, DOT, COLON, + COLON_EQ, EQ, + FAT_ARROW, BANG, + NEQ, MINUS, BACKTICK, ABORT_KW, @@ -722,7 +728,6 @@ pub enum SyntaxKind { COLLATION_REF, COLLATION_RENAME_TO, COLON_COLON, - COLON_EQ, COLUMN, COLUMN_DEF, COLUMN_DEF_LIST, @@ -1015,7 +1020,6 @@ pub enum SyntaxKind { EXTRACT_FIELD_LITERAL, EXTRACT_FIELD_NAME, EXTRACT_FN, - FAT_ARROW, FDW_OPTION_LIST, FETCH, FETCH_CLAUSE, @@ -1088,7 +1092,6 @@ pub enum SyntaxKind { GROUPING_SETS, GROUP_BY_CLAUSE, GROUP_BY_LIST, - GTEQ, HANDLER_CLAUSE, HAVING_CLAUSE, IF_EXISTS, @@ -1231,7 +1234,6 @@ pub enum SyntaxKind { LOCKING_CLAUSE, LOCKING_OF, LOCK_MODE_CLAUSE, - LTEQ, MATCH_FULL, MATCH_PARTIAL, MATCH_SIMPLE, @@ -1251,8 +1253,6 @@ pub enum SyntaxKind { NAMED_LABEL, NAMED_SCHEMA, NAME_REF, - NEQ, - NEQB, NEW_TABLE, NEXT, NON_STANDARD_PARAM, diff --git a/crates/squawk_parser/src/grammar.rs b/crates/squawk_parser/src/grammar.rs index 17c1f8ba..b73fb4b0 100644 --- a/crates/squawk_parser/src/grammar.rs +++ b/crates/squawk_parser/src/grammar.rs @@ -2364,12 +2364,13 @@ fn arg_expr(p: &mut Parser<'_>) -> Option { // https://www.postgresql.org/docs/17/typeconv-func.html let m = p.start(); p.eat(VARIADIC_KW); - let r = Restrictions { - order_by_allowed: true, - ..Restrictions::default() - }; - match expr_bp(p, 1, &r) { - Some(_) => Some(m.complete(p, ARG)), + match expr_bp(p, 1, &Restrictions::default()) { + Some(_) => { + if p.at(ORDER_KW) { + opt_order_by_clause(p); + } + Some(m.complete(p, ARG)) + } None => { m.abandon(p); None @@ -2969,7 +2970,6 @@ const OVERLAPPING_TOKENS: TokenSet = TokenSet::new(&[OR_KW, AND_KW, IS_KW, COLLA #[derive(Default)] struct Restrictions { - order_by_allowed: bool, escape_disabled: bool, in_disabled: bool, is_disabled: bool, @@ -3012,9 +3012,6 @@ fn expr_bp(p: &mut Parser<'_>, bp: u8, r: &Restrictions) -> Option<(CompletedMar m.complete(p, AS_NAME); return Some((lhs, expr_kind)); } - if r.order_by_allowed && p.at(ORDER_KW) { - opt_order_by_clause(p); - } loop { let (op_bp, op, associativity) = current_op(p, r); if op_bp < bp { @@ -3048,9 +3045,6 @@ fn expr_bp(p: &mut Parser<'_>, bp: u8, r: &Restrictions) -> Option<(CompletedMar }; expr_kind = ExprKind::Other; } - if r.order_by_allowed && p.at(ORDER_KW) { - opt_order_by_clause(p); - } Some((lhs, expr_kind)) } diff --git a/crates/squawk_parser/tests/data/ok/select_funcs.sql b/crates/squawk_parser/tests/data/ok/select_funcs.sql index 6b02dc13..e68b8057 100644 --- a/crates/squawk_parser/tests/data/ok/select_funcs.sql +++ b/crates/squawk_parser/tests/data/ok/select_funcs.sql @@ -230,6 +230,9 @@ select pg_sleep_until('tomorrow 03:00'); -- order by one param select array_agg(v order by v desc) from vals; +-- order by named param +select foo(a => x order by y); + -- order by param 2 select jsonb_object_agg(k, v order by v) from vals; diff --git a/crates/squawk_parser/tests/snapshots/tests__select_funcs_ok.snap b/crates/squawk_parser/tests/snapshots/tests__select_funcs_ok.snap index 9c88ec2c..c2b2fceb 100644 --- a/crates/squawk_parser/tests/snapshots/tests__select_funcs_ok.snap +++ b/crates/squawk_parser/tests/snapshots/tests__select_funcs_ok.snap @@ -3954,6 +3954,41 @@ SOURCE_FILE IDENT "vals" SEMICOLON ";" WHITESPACE "\n\n" + COMMENT "-- order by named param" + WHITESPACE "\n" + SELECT + SELECT_CLAUSE + SELECT_KW "select" + WHITESPACE " " + TARGET_LIST + TARGET + CALL_EXPR + NAME_REF + IDENT "foo" + ARG_LIST + L_PAREN "(" + ARG + NAMED_ARG + PARAM_NAME_REF + IDENT "a" + WHITESPACE " " + FAT_ARROW "=>" + WHITESPACE " " + NAME_REF + IDENT "x" + WHITESPACE " " + ORDER_BY_CLAUSE + ORDER_KW "order" + WHITESPACE " " + BY_KW "by" + WHITESPACE " " + SORT_BY_LIST + SORT_BY + NAME_REF + IDENT "y" + R_PAREN ")" + SEMICOLON ";" + WHITESPACE "\n\n" COMMENT "-- order by param 2" WHITESPACE "\n" SELECT diff --git a/crates/squawk_syntax/src/ast/generated/nodes.rs b/crates/squawk_syntax/src/ast/generated/nodes.rs index e1873d6c..8566d749 100644 --- a/crates/squawk_syntax/src/ast/generated/nodes.rs +++ b/crates/squawk_syntax/src/ast/generated/nodes.rs @@ -3769,21 +3769,6 @@ impl ColonColon { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct ColonEq { - pub(crate) syntax: SyntaxNode, -} -impl ColonEq { - #[inline] - pub fn colon_token(&self) -> Option { - support::token(&self.syntax, SyntaxKind::COLON) - } - #[inline] - pub fn eq_token(&self) -> Option { - support::token(&self.syntax, SyntaxKind::EQ) - } -} - #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Column { pub(crate) syntax: SyntaxNode, @@ -11100,21 +11085,6 @@ impl ExtractFn { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct FatArrow { - pub(crate) syntax: SyntaxNode, -} -impl FatArrow { - #[inline] - pub fn eq_token(&self) -> Option { - support::token(&self.syntax, SyntaxKind::EQ) - } - #[inline] - pub fn r_angle_token(&self) -> Option { - support::token(&self.syntax, SyntaxKind::R_ANGLE) - } -} - #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct FdwOptionList { pub(crate) syntax: SyntaxNode, @@ -12443,21 +12413,6 @@ impl GroupingSets { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct Gteq { - pub(crate) syntax: SyntaxNode, -} -impl Gteq { - #[inline] - pub fn eq_token(&self) -> Option { - support::token(&self.syntax, SyntaxKind::EQ) - } - #[inline] - pub fn r_angle_token(&self) -> Option { - support::token(&self.syntax, SyntaxKind::R_ANGLE) - } -} - #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct HandlerClause { pub(crate) syntax: SyntaxNode, @@ -15215,21 +15170,6 @@ impl LockingOf { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct Lteq { - pub(crate) syntax: SyntaxNode, -} -impl Lteq { - #[inline] - pub fn l_angle_token(&self) -> Option { - support::token(&self.syntax, SyntaxKind::L_ANGLE) - } - #[inline] - pub fn eq_token(&self) -> Option { - support::token(&self.syntax, SyntaxKind::EQ) - } -} - #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct MatchFull { pub(crate) syntax: SyntaxNode, @@ -15592,20 +15532,20 @@ pub struct NamedArg { } impl NamedArg { #[inline] - pub fn colon_eq(&self) -> Option { + pub fn expr(&self) -> Option { support::child(&self.syntax) } #[inline] - pub fn expr(&self) -> Option { + pub fn name(&self) -> Option { support::child(&self.syntax) } #[inline] - pub fn fat_arrow(&self) -> Option { - support::child(&self.syntax) + pub fn colon_eq_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::COLON_EQ) } #[inline] - pub fn name(&self) -> Option { - support::child(&self.syntax) + pub fn fat_arrow_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::FAT_ARROW) } } @@ -15643,36 +15583,6 @@ impl NamedSchema { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct Neq { - pub(crate) syntax: SyntaxNode, -} -impl Neq { - #[inline] - pub fn bang_token(&self) -> Option { - support::token(&self.syntax, SyntaxKind::BANG) - } - #[inline] - pub fn eq_token(&self) -> Option { - support::token(&self.syntax, SyntaxKind::EQ) - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct Neqb { - pub(crate) syntax: SyntaxNode, -} -impl Neqb { - #[inline] - pub fn l_angle_token(&self) -> Option { - support::token(&self.syntax, SyntaxKind::L_ANGLE) - } - #[inline] - pub fn r_angle_token(&self) -> Option { - support::token(&self.syntax, SyntaxKind::R_ANGLE) - } -} - #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct NewTable { pub(crate) syntax: SyntaxNode, @@ -17274,22 +17184,10 @@ impl Op { support::child(&self.syntax) } #[inline] - pub fn colon_eq(&self) -> Option { - support::child(&self.syntax) - } - #[inline] pub fn custom_op(&self) -> Option { support::child(&self.syntax) } #[inline] - pub fn fat_arrow(&self) -> Option { - support::child(&self.syntax) - } - #[inline] - pub fn gteq(&self) -> Option { - support::child(&self.syntax) - } - #[inline] pub fn is_distinct_from(&self) -> Option { support::child(&self.syntax) } @@ -17350,18 +17248,6 @@ impl Op { support::child(&self.syntax) } #[inline] - pub fn lteq(&self) -> Option { - support::child(&self.syntax) - } - #[inline] - pub fn neq(&self) -> Option { - support::child(&self.syntax) - } - #[inline] - pub fn neqb(&self) -> Option { - support::child(&self.syntax) - } - #[inline] pub fn not_ilike(&self) -> Option { support::child(&self.syntax) } @@ -17390,6 +17276,10 @@ impl Op { support::child(&self.syntax) } #[inline] + pub fn neq_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::NEQ) + } + #[inline] pub fn percent_token(&self) -> Option { support::token(&self.syntax, SyntaxKind::PERCENT) } @@ -17410,18 +17300,38 @@ impl Op { support::token(&self.syntax, SyntaxKind::COLON) } #[inline] + pub fn colon_eq_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::COLON_EQ) + } + #[inline] pub fn l_angle_token(&self) -> Option { support::token(&self.syntax, SyntaxKind::L_ANGLE) } #[inline] + pub fn lteq_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::LTEQ) + } + #[inline] + pub fn neqb_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::NEQB) + } + #[inline] pub fn eq_token(&self) -> Option { support::token(&self.syntax, SyntaxKind::EQ) } #[inline] + pub fn fat_arrow_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::FAT_ARROW) + } + #[inline] pub fn r_angle_token(&self) -> Option { support::token(&self.syntax, SyntaxKind::R_ANGLE) } #[inline] + pub fn gteq_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::GTEQ) + } + #[inline] pub fn caret_token(&self) -> Option { support::token(&self.syntax, SyntaxKind::CARET) } @@ -32795,24 +32705,6 @@ impl AstNode for ColonColon { &self.syntax } } -impl AstNode for ColonEq { - #[inline] - fn can_cast(kind: SyntaxKind) -> bool { - kind == SyntaxKind::COLON_EQ - } - #[inline] - fn cast(syntax: SyntaxNode) -> Option { - if Self::can_cast(syntax.kind()) { - Some(Self { syntax }) - } else { - None - } - } - #[inline] - fn syntax(&self) -> &SyntaxNode { - &self.syntax - } -} impl AstNode for Column { #[inline] fn can_cast(kind: SyntaxKind) -> bool { @@ -38069,24 +37961,6 @@ impl AstNode for ExtractFn { &self.syntax } } -impl AstNode for FatArrow { - #[inline] - fn can_cast(kind: SyntaxKind) -> bool { - kind == SyntaxKind::FAT_ARROW - } - #[inline] - fn cast(syntax: SyntaxNode) -> Option { - if Self::can_cast(syntax.kind()) { - Some(Self { syntax }) - } else { - None - } - } - #[inline] - fn syntax(&self) -> &SyntaxNode { - &self.syntax - } -} impl AstNode for FdwOptionList { #[inline] fn can_cast(kind: SyntaxKind) -> bool { @@ -39383,24 +39257,6 @@ impl AstNode for GroupingSets { &self.syntax } } -impl AstNode for Gteq { - #[inline] - fn can_cast(kind: SyntaxKind) -> bool { - kind == SyntaxKind::GTEQ - } - #[inline] - fn cast(syntax: SyntaxNode) -> Option { - if Self::can_cast(syntax.kind()) { - Some(Self { syntax }) - } else { - None - } - } - #[inline] - fn syntax(&self) -> &SyntaxNode { - &self.syntax - } -} impl AstNode for HandlerClause { #[inline] fn can_cast(kind: SyntaxKind) -> bool { @@ -41957,24 +41813,6 @@ impl AstNode for LockingOf { &self.syntax } } -impl AstNode for Lteq { - #[inline] - fn can_cast(kind: SyntaxKind) -> bool { - kind == SyntaxKind::LTEQ - } - #[inline] - fn cast(syntax: SyntaxNode) -> Option { - if Self::can_cast(syntax.kind()) { - Some(Self { syntax }) - } else { - None - } - } - #[inline] - fn syntax(&self) -> &SyntaxNode { - &self.syntax - } -} impl AstNode for MatchFull { #[inline] fn can_cast(kind: SyntaxKind) -> bool { @@ -42317,42 +42155,6 @@ impl AstNode for NamedSchema { &self.syntax } } -impl AstNode for Neq { - #[inline] - fn can_cast(kind: SyntaxKind) -> bool { - kind == SyntaxKind::NEQ - } - #[inline] - fn cast(syntax: SyntaxNode) -> Option { - if Self::can_cast(syntax.kind()) { - Some(Self { syntax }) - } else { - None - } - } - #[inline] - fn syntax(&self) -> &SyntaxNode { - &self.syntax - } -} -impl AstNode for Neqb { - #[inline] - fn can_cast(kind: SyntaxKind) -> bool { - kind == SyntaxKind::NEQB - } - #[inline] - fn cast(syntax: SyntaxNode) -> Option { - if Self::can_cast(syntax.kind()) { - Some(Self { syntax }) - } else { - None - } - } - #[inline] - fn syntax(&self) -> &SyntaxNode { - &self.syntax - } -} impl AstNode for NewTable { #[inline] fn can_cast(kind: SyntaxKind) -> bool { diff --git a/crates/squawk_syntax/src/postgresql.ungram b/crates/squawk_syntax/src/postgresql.ungram index 2556a14f..4ae0e790 100644 --- a/crates/squawk_syntax/src/postgresql.ungram +++ b/crates/squawk_syntax/src/postgresql.ungram @@ -561,7 +561,7 @@ Literal = ) NamedArg = - name:ParamNameRef (FatArrow | ColonEq) Expr + name:ParamNameRef ('=>' | ':=') Expr JsonFormatClause = 'format' 'json' JsonEncodingClause? @@ -572,18 +572,6 @@ JsonValueExpr = JsonKeyValue = Expr (':' | 'value') JsonValueExpr -Gteq = - '>' '=' - -FatArrow = - '=' '>' - -Neqb = - '<' '>' - -Lteq = - '<' '=' - NotLike = 'not' 'like' @@ -617,15 +605,9 @@ IsNotNormalized = OperatorCall = 'operator' '(' Op ')' -ColonEq = - ':' '=' - ColonColon = ':' ':' -Neq = - '!' '=' - SimilarTo = 'similar' 'to' @@ -667,10 +649,10 @@ Op = | AtLocal | AtTimeZone | ColonColon - | ColonEq + | ':=' | CustomOp - | FatArrow - | Gteq + | '=>' + | '>=' | IsDistinctFrom | IsJson | IsJsonArray @@ -686,9 +668,9 @@ Op = | IsNotJsonScalar | IsNotJsonValue | IsNotNormalized - | Lteq - | Neq - | Neqb + | '<=' + | '!=' + | '<>' | NotIlike | NotIn | NotLike diff --git a/crates/xtask/src/codegen.rs b/crates/xtask/src/codegen.rs index 8ae06aad..e00cae90 100644 --- a/crates/xtask/src/codegen.rs +++ b/crates/xtask/src/codegen.rs @@ -130,7 +130,10 @@ const PUNCT: &[(&str, &str)] = &[ ("{", "L_CURLY"), ("}", "R_CURLY"), ("<", "L_ANGLE"), + ("<=", "LTEQ"), + ("<>", "NEQB"), (">", "R_ANGLE"), + (">=", "GTEQ"), ("@", "AT"), ("#", "POUND"), ("~", "TILDE"), @@ -145,8 +148,11 @@ const PUNCT: &[(&str, &str)] = &[ ("_", "UNDERSCORE"), (".", "DOT"), (":", "COLON"), + (":=", "COLON_EQ"), ("=", "EQ"), + ("=>", "FAT_ARROW"), ("!", "BANG"), + ("!=", "NEQ"), ("-", "MINUS"), ("`", "BACKTICK"), ]; @@ -495,8 +501,12 @@ fn token_to_name(tk: &str) -> Option<&'static str> { "]" => "r_brack", "<" => "l_angle", ">" => "r_angle", + ">=" => "gteq", + "<=" => "lteq", + "<>" => "neqb", "=" => "eq", "!" => "bang", + "!=" => "neq", "*" => "star", "&" => "amp", "+" => "plus", @@ -507,6 +517,7 @@ fn token_to_name(tk: &str) -> Option<&'static str> { "_" => "underscore", "." => "dot", "=>" => "fat_arrow", + ":=" => "colon_eq", "@" => "at", "%" => "percent", ":" => "colon", From df4989a5e51f49cfd5d3d7359a6efdd9cdc2a2ce Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Sun, 23 Aug 2026 17:53:08 -0400 Subject: [PATCH 02/17] fmt: any/all/some --- crates/squawk_fmt/src/fmt.rs | 62 +++++++++++++++-- .../squawk_fmt/tests/after/select_expr.snap | 4 ++ .../squawk_fmt/tests/before/select_expr.sql | 4 ++ .../squawk_syntax/src/ast/generated/nodes.rs | 67 +++---------------- crates/squawk_syntax/src/postgresql.ungram | 9 +-- 5 files changed, 76 insertions(+), 70 deletions(-) diff --git a/crates/squawk_fmt/src/fmt.rs b/crates/squawk_fmt/src/fmt.rs index f839cf10..c505d6dd 100644 --- a/crates/squawk_fmt/src/fmt.rs +++ b/crates/squawk_fmt/src/fmt.rs @@ -791,10 +791,22 @@ fn build_call_expr<'a>(call_expr: ast::CallExpr) -> Doc<'a> { build_expr(expr) .append(comments_before(arg_list.syntax().clone())) .append(build_call_arg_list(arg_list)) - } else if let Some(_all_fn) = call_expr.all_fn() { - todo!("all function expressions are not supported yet") - } else if let Some(_any_fn) = call_expr.any_fn() { - todo!("any function expressions are not supported yet") + } else if let Some(all_fn) = call_expr.all_fn() { + build_quantified_fn( + "all", + all_fn.l_paren_token(), + all_fn.expr(), + all_fn.select_variant(), + all_fn.r_paren_token(), + ) + } else if let Some(any_fn) = call_expr.any_fn() { + build_quantified_fn( + "any", + any_fn.l_paren_token(), + any_fn.expr(), + any_fn.select_variant(), + any_fn.r_paren_token(), + ) } else if let Some(_collation_for_fn) = call_expr.collation_for_fn() { todo!("collation_for function expressions are not supported yet") } else if let Some(_exists_fn) = call_expr.exists_fn() { @@ -827,8 +839,14 @@ fn build_call_expr<'a>(call_expr: ast::CallExpr) -> Doc<'a> { todo!("overlay function expressions are not supported yet") } else if let Some(_position_fn) = call_expr.position_fn() { todo!("position function expressions are not supported yet") - } else if let Some(_some_fn) = call_expr.some_fn() { - todo!("some function expressions are not supported yet") + } else if let Some(some_fn) = call_expr.some_fn() { + build_quantified_fn( + "some", + some_fn.l_paren_token(), + some_fn.expr(), + some_fn.select_variant(), + some_fn.r_paren_token(), + ) } else if let Some(_substring_fn) = call_expr.substring_fn() { todo!("substring function expressions are not supported yet") } else if let Some(_trim_fn) = call_expr.trim_fn() { @@ -852,6 +870,38 @@ fn build_call_expr<'a>(call_expr: ast::CallExpr) -> Doc<'a> { } } +fn build_quantified_fn<'a>( + keyword: &'static str, + l_paren: Option, + expr: Option, + select: Option, + r_paren: Option, +) -> Doc<'a> { + let mut doc = Doc::text(keyword); + if let Some(l_paren) = l_paren { + doc = doc.append(comments_before(l_paren)); + } + doc = doc.append(Doc::text("(")); + + if let Some(expr) = expr { + doc = doc + .append(leading_comments(expr.syntax())) + .append(build_expr(expr)); + } else if let Some(select) = select { + doc = doc + .append(leading_comments(select.syntax())) + .append(match select { + ast::SelectVariant::Select(select) => build_select_doc(&select), + _ => todo!("this select variant is not supported yet"), + }); + } + + if let Some(r_paren) = r_paren { + doc = doc.append(comments_before(r_paren)); + } + doc.append(Doc::text(")")) +} + fn build_call_arg_list<'a>(arg_list: ast::ArgList) -> Doc<'a> { let mut doc = Doc::nil(); if let Some(l_paren) = arg_list.l_paren_token() { diff --git a/crates/squawk_fmt/tests/after/select_expr.snap b/crates/squawk_fmt/tests/after/select_expr.snap index bf8c6809..def17ca9 100644 --- a/crates/squawk_fmt/tests/after/select_expr.snap +++ b/crates/squawk_fmt/tests/after/select_expr.snap @@ -51,6 +51,10 @@ select 6 / 2, 2 * 3, -- call expr + 1 = any(array[1, 2]), + 2 = all(select x from things), + 3 = some(array[3]), + 4 /* before op */ = /* before any */ any /* before opening paren */(/* before expr */ array[4] /* before closing paren */) /* after any */, date_trunc('month', now()), foo(), foo(*), diff --git a/crates/squawk_fmt/tests/before/select_expr.sql b/crates/squawk_fmt/tests/before/select_expr.sql index eedef5c9..c36b3430 100644 --- a/crates/squawk_fmt/tests/before/select_expr.sql +++ b/crates/squawk_fmt/tests/before/select_expr.sql @@ -47,6 +47,10 @@ select 6 / 2, 2 * 3, -- call expr + 1 = ANY ( ARRAY [ 1 , 2 ] ), + 2 = ALL ( SELECT x FROM things ), + 3 = SOME ( ARRAY [ 3 ] ), + 4 /* before op */ = /* before any */ ANY /* before opening paren */ ( /* before expr */ ARRAY [ 4 ] /* before closing paren */ ) /* after any */, date_trunc('month', now()), foo ( ), foo ( * ), diff --git a/crates/squawk_syntax/src/ast/generated/nodes.rs b/crates/squawk_syntax/src/ast/generated/nodes.rs index 8566d749..dd19d9ae 100644 --- a/crates/squawk_syntax/src/ast/generated/nodes.rs +++ b/crates/squawk_syntax/src/ast/generated/nodes.rs @@ -14519,8 +14519,8 @@ pub struct JsonTablePlanClause { } impl JsonTablePlanClause { #[inline] - pub fn json_table_plan(&self) -> Option { - support::child(&self.syntax) + pub fn json_table_plans(&self) -> AstChildren { + support::children(&self.syntax) } #[inline] pub fn l_paren_token(&self) -> Option { @@ -14531,10 +14531,6 @@ impl JsonTablePlanClause { support::token(&self.syntax, SyntaxKind::R_PAREN) } #[inline] - pub fn comma_token(&self) -> Option { - support::token(&self.syntax, SyntaxKind::COMMA) - } - #[inline] pub fn default_token(&self) -> Option { support::token(&self.syntax, SyntaxKind::DEFAULT_KW) } @@ -14613,15 +14609,19 @@ impl JsonTableValueColumn { support::child(&self.syntax) } #[inline] - pub fn json_path_clause(&self) -> Option { + pub fn json_on_empty_clause(&self) -> Option { support::child(&self.syntax) } #[inline] - pub fn json_quotes_clause(&self) -> Option { + pub fn json_on_error_clause(&self) -> Option { + support::child(&self.syntax) + } + #[inline] + pub fn json_path_clause(&self) -> Option { support::child(&self.syntax) } #[inline] - pub fn json_table_value_behavior(&self) -> Option { + pub fn json_quotes_clause(&self) -> Option { support::child(&self.syntax) } #[inline] @@ -28971,12 +28971,6 @@ pub enum JsonTablePlanOperator { JsonTablePlanUnion(JsonTablePlanUnion), } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum JsonTableValueBehavior { - JsonOnEmptyClause(JsonOnEmptyClause), - JsonOnErrorClause(JsonOnErrorClause), -} - #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum JsonWrapperBehaviorClause { JsonWithConditionalWrapper(JsonWithConditionalWrapper), @@ -63754,49 +63748,6 @@ impl From for JsonTablePlanOperator { JsonTablePlanOperator::JsonTablePlanUnion(node) } } -impl AstNode for JsonTableValueBehavior { - #[inline] - fn can_cast(kind: SyntaxKind) -> bool { - matches!( - kind, - SyntaxKind::JSON_ON_EMPTY_CLAUSE | SyntaxKind::JSON_ON_ERROR_CLAUSE - ) - } - #[inline] - fn cast(syntax: SyntaxNode) -> Option { - let res = match syntax.kind() { - SyntaxKind::JSON_ON_EMPTY_CLAUSE => { - JsonTableValueBehavior::JsonOnEmptyClause(JsonOnEmptyClause { syntax }) - } - SyntaxKind::JSON_ON_ERROR_CLAUSE => { - JsonTableValueBehavior::JsonOnErrorClause(JsonOnErrorClause { syntax }) - } - _ => { - return None; - } - }; - Some(res) - } - #[inline] - fn syntax(&self) -> &SyntaxNode { - match self { - JsonTableValueBehavior::JsonOnEmptyClause(it) => &it.syntax, - JsonTableValueBehavior::JsonOnErrorClause(it) => &it.syntax, - } - } -} -impl From for JsonTableValueBehavior { - #[inline] - fn from(node: JsonOnEmptyClause) -> JsonTableValueBehavior { - JsonTableValueBehavior::JsonOnEmptyClause(node) - } -} -impl From for JsonTableValueBehavior { - #[inline] - fn from(node: JsonOnErrorClause) -> JsonTableValueBehavior { - JsonTableValueBehavior::JsonOnErrorClause(node) - } -} impl AstNode for JsonWrapperBehaviorClause { #[inline] fn can_cast(kind: SyntaxKind) -> bool { diff --git a/crates/squawk_syntax/src/postgresql.ungram b/crates/squawk_syntax/src/postgresql.ungram index 4ae0e790..a45ee6a3 100644 --- a/crates/squawk_syntax/src/postgresql.ungram +++ b/crates/squawk_syntax/src/postgresql.ungram @@ -267,7 +267,7 @@ JsonTable = ')' JsonTablePlanClause = - 'plan' 'default'? '(' JsonTablePlan (',' JsonTablePlan)? ')' + 'plan' 'default'? '(' (JsonTablePlan (',' JsonTablePlan)*) ')' JsonTablePlan = JsonPathNameRef @@ -2562,11 +2562,8 @@ JsonTableValueColumn = JsonPathClause? JsonWrapperBehaviorClause? JsonQuotesClause? - JsonTableValueBehavior? - -JsonTableValueBehavior = - JsonOnErrorClause -| JsonOnEmptyClause + JsonOnEmptyClause? + JsonOnErrorClause? JsonTableExistsColumn = ColumnName Type From 749fca6eb892a08640b2970e49d7a6c4cf490809 Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Sun, 23 Aug 2026 18:49:57 -0400 Subject: [PATCH 03/17] fmt: exists/extract/position/substring/trim --- crates/squawk_fmt/src/fmt.rs | 264 ++++++++++++++++-- .../squawk_fmt/tests/after/select_expr.snap | 24 ++ .../squawk_fmt/tests/before/select_expr.sql | 23 ++ 3 files changed, 280 insertions(+), 31 deletions(-) diff --git a/crates/squawk_fmt/src/fmt.rs b/crates/squawk_fmt/src/fmt.rs index c505d6dd..02476b17 100644 --- a/crates/squawk_fmt/src/fmt.rs +++ b/crates/squawk_fmt/src/fmt.rs @@ -732,23 +732,10 @@ fn build_tuple_expr<'a>(tuple_expr: ast::TupleExpr) -> Doc<'a> { } doc = doc.append(Doc::text("(")); - let exprs: Vec> = tuple_expr - .exprs() - .map(|expr| { - let leading = leading_comments(expr.syntax()); - let trailing = trailing_comments(expr.syntax()); - leading.append(build_expr(expr)).append(trailing) - }) - .collect(); - if exprs.is_empty() { - if let Some(r_paren) = tuple_expr.r_paren_token() { - doc = doc.append(comments_before(r_paren)); - } - } else { - doc = doc.append(Doc::list( - Itertools::intersperse(exprs.into_iter(), Doc::text(",").append(Doc::space())) - .collect(), - )); + if let Some(exprs) = build_comma_separated_exprs(tuple_expr.exprs()) { + doc = doc.append(exprs); + } else if let Some(r_paren) = tuple_expr.r_paren_token() { + doc = doc.append(comments_before(r_paren)); } doc.append(Doc::text(")")) @@ -792,7 +779,7 @@ fn build_call_expr<'a>(call_expr: ast::CallExpr) -> Doc<'a> { .append(comments_before(arg_list.syntax().clone())) .append(build_call_arg_list(arg_list)) } else if let Some(all_fn) = call_expr.all_fn() { - build_quantified_fn( + build_parenthesized_expr_or_select_fn( "all", all_fn.l_paren_token(), all_fn.expr(), @@ -800,7 +787,7 @@ fn build_call_expr<'a>(call_expr: ast::CallExpr) -> Doc<'a> { all_fn.r_paren_token(), ) } else if let Some(any_fn) = call_expr.any_fn() { - build_quantified_fn( + build_parenthesized_expr_or_select_fn( "any", any_fn.l_paren_token(), any_fn.expr(), @@ -809,10 +796,16 @@ fn build_call_expr<'a>(call_expr: ast::CallExpr) -> Doc<'a> { ) } else if let Some(_collation_for_fn) = call_expr.collation_for_fn() { todo!("collation_for function expressions are not supported yet") - } else if let Some(_exists_fn) = call_expr.exists_fn() { - todo!("exists function expressions are not supported yet") - } else if let Some(_extract_fn) = call_expr.extract_fn() { - todo!("extract function expressions are not supported yet") + } else if let Some(exists_fn) = call_expr.exists_fn() { + build_parenthesized_expr_or_select_fn( + "exists", + exists_fn.l_paren_token(), + None, + exists_fn.select_variant(), + exists_fn.r_paren_token(), + ) + } else if let Some(extract_fn) = call_expr.extract_fn() { + build_extract_fn(extract_fn) } else if let Some(_graph_table_fn) = call_expr.graph_table_fn() { todo!("graph_table function expressions are not supported yet") } else if let Some(_json_array_agg_fn) = call_expr.json_array_agg_fn() { @@ -837,20 +830,20 @@ fn build_call_expr<'a>(call_expr: ast::CallExpr) -> Doc<'a> { todo!("json_value function expressions are not supported yet") } else if let Some(_overlay_fn) = call_expr.overlay_fn() { todo!("overlay function expressions are not supported yet") - } else if let Some(_position_fn) = call_expr.position_fn() { - todo!("position function expressions are not supported yet") + } else if let Some(position_fn) = call_expr.position_fn() { + build_position_fn(position_fn) } else if let Some(some_fn) = call_expr.some_fn() { - build_quantified_fn( + build_parenthesized_expr_or_select_fn( "some", some_fn.l_paren_token(), some_fn.expr(), some_fn.select_variant(), some_fn.r_paren_token(), ) - } else if let Some(_substring_fn) = call_expr.substring_fn() { - todo!("substring function expressions are not supported yet") - } else if let Some(_trim_fn) = call_expr.trim_fn() { - todo!("trim function expressions are not supported yet") + } else if let Some(substring_fn) = call_expr.substring_fn() { + build_substring_fn(substring_fn) + } else if let Some(trim_fn) = call_expr.trim_fn() { + build_trim_fn(trim_fn) } else if let Some(_xml_element_fn) = call_expr.xml_element_fn() { todo!("xmlelement function expressions are not supported yet") } else if let Some(_xml_exists_fn) = call_expr.xml_exists_fn() { @@ -870,7 +863,216 @@ fn build_call_expr<'a>(call_expr: ast::CallExpr) -> Doc<'a> { } } -fn build_quantified_fn<'a>( +fn build_substring_fn<'a>(substring_fn: ast::SubstringFn) -> Doc<'a> { + let mut doc = Doc::text("substring"); + if let Some(l_paren) = substring_fn.l_paren_token() { + doc = doc.append(comments_before(l_paren)); + } + doc = doc.append(Doc::text("(")); + + if let Some(args) = substring_fn.substring_args() { + doc = doc + .append(leading_comments(args.syntax())) + .append(match args { + ast::SubstringArgs::SubstringForFrom(args) => { + let mut doc = args.string().map(build_expr).unwrap_or_else(Doc::nil); + doc = append_keyword_expr(doc, args.for_token(), "for", args.count()); + append_keyword_expr(doc, args.from_token(), "from", args.start()) + } + ast::SubstringArgs::SubstringFromFor(args) => { + let mut doc = args.string().map(build_expr).unwrap_or_else(Doc::nil); + doc = append_keyword_expr(doc, args.from_token(), "from", args.start()); + append_keyword_expr(doc, args.for_token(), "for", args.count()) + } + ast::SubstringArgs::SubstringSimilarEscape(args) => { + let mut doc = args.string().map(build_expr).unwrap_or_else(Doc::nil); + doc = append_keyword_expr(doc, args.similar_token(), "similar", args.pattern()); + append_keyword_expr(doc, args.escape_token(), "escape", args.escape()) + } + ast::SubstringArgs::SubstringExprs(args) => { + build_comma_separated_exprs(args.exprs()).unwrap_or_else(Doc::nil) + } + }); + } + + if let Some(r_paren) = substring_fn.r_paren_token() { + doc = doc.append(comments_before(r_paren)); + } + doc.append(Doc::text(")")) +} + +fn append_keyword_expr<'a>( + mut doc: Doc<'a>, + token: Option, + keyword: &'static str, + expr: Option, +) -> Doc<'a> { + if let Some(token) = token { + doc = doc + .append(Doc::space()) + .append(leading_comments_token(&token)) + .append(Doc::text(keyword)); + } + if let Some(expr) = expr { + doc = doc + .append(Doc::space()) + .append(leading_comments(expr.syntax())) + .append(build_expr(expr)); + } + doc +} + +fn build_trim_fn<'a>(trim_fn: ast::TrimFn) -> Doc<'a> { + let mut doc = Doc::text("trim"); + if let Some(l_paren) = trim_fn.l_paren_token() { + doc = doc.append(comments_before(l_paren)); + } + doc = doc.append(Doc::text("(")); + + let has_side = if let Some(side) = trim_fn.trim_side() { + doc = doc + .append(leading_comments(side.syntax())) + .append(match side { + ast::TrimSide::TrimBoth(_) => Doc::text("both"), + ast::TrimSide::TrimLeading(_) => Doc::text("leading"), + ast::TrimSide::TrimTrailing(_) => Doc::text("trailing"), + }); + true + } else { + false + }; + + if let Some(args) = trim_fn.trim_args() { + if has_side { + doc = doc.append(Doc::space()); + } + doc = doc + .append(leading_comments(args.syntax())) + .append(match args { + ast::TrimArgs::TrimFrom(args) => { + let mut doc = Doc::text("from"); + if let Some(exprs) = build_comma_separated_exprs(args.exprs()) { + doc = doc.append(Doc::space()).append(exprs); + } + doc + } + ast::TrimArgs::TrimExprFrom(args) => { + let mut exprs = args.exprs(); + let mut doc = exprs.next().map(build_expr).unwrap_or_else(Doc::nil); + if let Some(from) = args.from_token() { + doc = doc + .append(Doc::space()) + .append(leading_comments_token(&from)) + .append(Doc::text("from")); + } + if let Some(exprs) = build_comma_separated_exprs(exprs) { + doc = doc.append(Doc::space()).append(exprs); + } + doc + } + ast::TrimArgs::TrimExprs(args) => { + build_comma_separated_exprs(args.exprs()).unwrap_or_else(Doc::nil) + } + }); + } + + if let Some(r_paren) = trim_fn.r_paren_token() { + doc = doc.append(comments_before(r_paren)); + } + doc.append(Doc::text(")")) +} + +fn build_comma_separated_exprs<'a>(exprs: impl Iterator) -> Option> { + let exprs: Vec> = exprs + .map(|expr| { + let leading = leading_comments(expr.syntax()); + let trailing = trailing_comments(expr.syntax()); + leading.append(build_expr(expr)).append(trailing) + }) + .collect(); + if exprs.is_empty() { + None + } else { + Some(Doc::list( + Itertools::intersperse(exprs.into_iter(), Doc::text(",").append(Doc::space())) + .collect(), + )) + } +} + +fn build_position_fn<'a>(position_fn: ast::PositionFn) -> Doc<'a> { + let mut doc = Doc::text("position"); + if let Some(l_paren) = position_fn.l_paren_token() { + doc = doc.append(comments_before(l_paren)); + } + doc = doc.append(Doc::text("(")); + + if let Some(pos) = position_fn.pos() { + doc = doc + .append(leading_comments(pos.syntax())) + .append(build_expr(pos)); + } + if let Some(in_token) = position_fn.in_token() { + doc = doc + .append(Doc::space()) + .append(leading_comments_token(&in_token)) + .append(Doc::text("in")); + } + if let Some(string) = position_fn.string() { + doc = doc + .append(Doc::space()) + .append(leading_comments(string.syntax())) + .append(build_expr(string)); + } + if let Some(r_paren) = position_fn.r_paren_token() { + doc = doc.append(comments_before(r_paren)); + } + doc.append(Doc::text(")")) +} + +fn build_extract_fn<'a>(extract_fn: ast::ExtractFn) -> Doc<'a> { + let mut doc = Doc::text("extract"); + if let Some(l_paren) = extract_fn.l_paren_token() { + doc = doc.append(comments_before(l_paren)); + } + doc = doc.append(Doc::text("(")); + + if let Some(field) = extract_fn.extract_field() { + doc = doc + .append(leading_comments(field.syntax())) + .append(match field { + ast::ExtractField::ExtractFieldLiteral(field) => { + field.literal().map(build_literal).unwrap_or_else(Doc::nil) + } + ast::ExtractField::ExtractFieldName(field) => { + if field.ident_token().is_some() { + build_name(field.syntax()) + } else { + build_keyword_node(field.syntax()) + } + } + }); + } + + if let Some(from) = extract_fn.from_token() { + doc = doc + .append(Doc::space()) + .append(leading_comments_token(&from)) + .append(Doc::text("from")); + } + if let Some(expr) = extract_fn.expr() { + doc = doc + .append(Doc::space()) + .append(leading_comments(expr.syntax())) + .append(build_expr(expr)); + } + if let Some(r_paren) = extract_fn.r_paren_token() { + doc = doc.append(comments_before(r_paren)); + } + doc.append(Doc::text(")")) +} + +fn build_parenthesized_expr_or_select_fn<'a>( keyword: &'static str, l_paren: Option, expr: Option, diff --git a/crates/squawk_fmt/tests/after/select_expr.snap b/crates/squawk_fmt/tests/after/select_expr.snap index def17ca9..a10baf8f 100644 --- a/crates/squawk_fmt/tests/after/select_expr.snap +++ b/crates/squawk_fmt/tests/after/select_expr.snap @@ -55,6 +55,30 @@ select 2 = all(select x from things), 3 = some(array[3]), 4 /* before op */ = /* before any */ any /* before opening paren */(/* before expr */ array[4] /* before closing paren */) /* after any */, + exists(select 1 from things), + /* before exists */ exists /* before opening paren */(/* before select */ select + 1 /* before closing paren */) /* after exists */, + extract(year from timestamp '2001-02-16 20:38:40'), + extract('month' from ts), + extract("Field" from ts), + /* before extract */ extract /* before opening paren */(/* before field */ day /* before from */ from /* before expr */ ts /* before closing paren */) /* after extract */, + position('om' in 'Thomas'), + /* before position */ position /* before opening paren */(/* before substring */ 'om' /* before in */ in /* before string */ 'Thomas' /* before closing paren */) /* after position */, + substring('Thomas' from 2 for 3), + substring('Thomas' for 3 from 2), + substring('Thomas' from 2), + substring('Thomas' for 3), + substring('Thomas' similar '%#"o_a#"_' escape '#'), + substring('Thomas', 2, 3), + substring('Thomas' /* before comma */, /* before start */ 2, /* before count */ 3), + /* before substring */ substring /* before opening paren */(/* before string */ 'Thomas' /* before from */ from /* before start */ 2 /* before for */ for /* before count */ 3 /* before closing paren */) /* after substring */, + trim(' foo '), + trim(both 'x' from 'xfoox'), + trim(leading from ' foo '), + trim(trailing 'x' from 'foox'), + trim('x' from 'xfoox'), + trim(foo, bar), + /* before trim */ trim /* before opening paren */(/* before side */ both /* before trim char */ 'x' /* before from */ from /* before string */ 'xfoox' /* before closing paren */) /* after trim */, date_trunc('month', now()), foo(), foo(*), diff --git a/crates/squawk_fmt/tests/before/select_expr.sql b/crates/squawk_fmt/tests/before/select_expr.sql index c36b3430..066dc3c9 100644 --- a/crates/squawk_fmt/tests/before/select_expr.sql +++ b/crates/squawk_fmt/tests/before/select_expr.sql @@ -51,6 +51,29 @@ select 2 = ALL ( SELECT x FROM things ), 3 = SOME ( ARRAY [ 3 ] ), 4 /* before op */ = /* before any */ ANY /* before opening paren */ ( /* before expr */ ARRAY [ 4 ] /* before closing paren */ ) /* after any */, + EXISTS ( SELECT 1 FROM things ), + /* before exists */ EXISTS /* before opening paren */ ( /* before select */ SELECT 1 /* before closing paren */ ) /* after exists */, + EXTRACT ( YEAR FROM TIMESTAMP '2001-02-16 20:38:40' ), + EXTRACT ( 'month' FROM ts ), + EXTRACT ( "Field" FROM ts ), + /* before extract */ EXTRACT /* before opening paren */ ( /* before field */ DAY /* before from */ FROM /* before expr */ ts /* before closing paren */ ) /* after extract */, + POSITION ( 'om' IN 'Thomas' ), + /* before position */ POSITION /* before opening paren */ ( /* before substring */ 'om' /* before in */ IN /* before string */ 'Thomas' /* before closing paren */ ) /* after position */, + SUBSTRING ( 'Thomas' FROM 2 FOR 3 ), + SUBSTRING ( 'Thomas' FOR 3 FROM 2 ), + SUBSTRING ( 'Thomas' FROM 2 ), + SUBSTRING ( 'Thomas' FOR 3 ), + SUBSTRING ( 'Thomas' SIMILAR '%#"o_a#"_' ESCAPE '#' ), + SUBSTRING ( 'Thomas', 2, 3 ), + SUBSTRING ( 'Thomas' /* before comma */, /* before start */ 2, /* before count */ 3 ), + /* before substring */ SUBSTRING /* before opening paren */ ( /* before string */ 'Thomas' /* before from */ FROM /* before start */ 2 /* before for */ FOR /* before count */ 3 /* before closing paren */ ) /* after substring */, + TRIM ( ' foo ' ), + TRIM ( BOTH 'x' FROM 'xfoox' ), + TRIM ( LEADING FROM ' foo ' ), + TRIM ( TRAILING 'x' FROM 'foox' ), + TRIM ( 'x' FROM 'xfoox' ), + TRIM ( foo, bar ), + /* before trim */ TRIM /* before opening paren */ ( /* before side */ BOTH /* before trim char */ 'x' /* before from */ FROM /* before string */ 'xfoox' /* before closing paren */ ) /* after trim */, date_trunc('month', now()), foo ( ), foo ( * ), From 07f6c211cf1f2ba41ccd8fe3ad0b04355db9bfbd Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Sun, 23 Aug 2026 19:05:55 -0400 Subject: [PATCH 04/17] fmt: within/filter/over clauses, null treatment --- crates/squawk_fmt/src/fmt.rs | 296 +++++++++++++++++- .../squawk_fmt/tests/after/select_expr.snap | 15 + .../squawk_fmt/tests/before/select_expr.sql | 15 + 3 files changed, 315 insertions(+), 11 deletions(-) diff --git a/crates/squawk_fmt/src/fmt.rs b/crates/squawk_fmt/src/fmt.rs index 02476b17..6ec69bbf 100644 --- a/crates/squawk_fmt/src/fmt.rs +++ b/crates/squawk_fmt/src/fmt.rs @@ -766,18 +766,34 @@ fn build_between_expr<'a>(between_expr: ast::BetweenExpr) -> Doc<'a> { fn build_call_expr<'a>(call_expr: ast::CallExpr) -> Doc<'a> { if let (Some(expr), Some(arg_list)) = (call_expr.expr(), call_expr.arg_list()) { - if call_expr.within_clause().is_some() { - todo!("within clauses on call expressions are not supported yet") - } else if call_expr.filter_clause().is_some() { - todo!("filter clauses on call expressions are not supported yet") - } else if call_expr.null_treatment().is_some() { - todo!("null treatment on call expressions is not supported yet") - } else if call_expr.over_clause().is_some() { - todo!("over clauses on call expressions are not supported yet") - } - build_expr(expr) + let mut doc = build_expr(expr) .append(comments_before(arg_list.syntax().clone())) - .append(build_call_arg_list(arg_list)) + .append(build_call_arg_list(arg_list)); + if let Some(within_clause) = call_expr.within_clause() { + doc = doc + .append(Doc::space()) + .append(leading_comments(within_clause.syntax())) + .append(build_within_clause(within_clause)); + } + if let Some(filter_clause) = call_expr.filter_clause() { + doc = doc + .append(Doc::space()) + .append(leading_comments(filter_clause.syntax())) + .append(build_filter_clause(filter_clause)); + } + if let Some(null_treatment) = call_expr.null_treatment() { + doc = doc + .append(Doc::space()) + .append(leading_comments(null_treatment.syntax())) + .append(build_null_treatment(null_treatment)); + } + if let Some(over_clause) = call_expr.over_clause() { + doc = doc + .append(Doc::space()) + .append(leading_comments(over_clause.syntax())) + .append(build_over_clause(over_clause)); + } + doc } else if let Some(all_fn) = call_expr.all_fn() { build_parenthesized_expr_or_select_fn( "all", @@ -1561,6 +1577,264 @@ fn build_bin_expr<'a>(bin_expr: ast::BinExpr) -> Doc<'a> { .append(build_expr(rhs)) } +fn build_within_clause<'a>(within_clause: ast::WithinClause) -> Doc<'a> { + let mut doc = Doc::text("within").append(Doc::space()); + if let Some(group_token) = within_clause.group_token() { + doc = doc.append(leading_comments_token(&group_token)); + } + doc = doc.append(Doc::text("group")); + if let Some(l_paren) = within_clause.l_paren_token() { + doc = doc + .append(Doc::space()) + .append(leading_comments_token(&l_paren)) + .append(Doc::text("(")); + } + if let Some(order_by) = within_clause.order_by_clause() { + doc = doc + .append(leading_comments(order_by.syntax())) + .append(build_order_by_clause(order_by)); + } + if let Some(r_paren) = within_clause.r_paren_token() { + doc = doc.append(comments_before(r_paren)); + } + doc.append(Doc::text(")")) +} + +fn build_over_clause<'a>(over_clause: ast::OverClause) -> Doc<'a> { + let mut doc = Doc::text("over"); + if let Some(target) = over_clause.over_target() { + doc = doc + .append(Doc::space()) + .append(leading_comments(target.syntax())) + .append(match target { + ast::OverTarget::WindowRef(window_ref) => build_name(window_ref.syntax()), + ast::OverTarget::OverWindowSpec(window_spec) => build_over_window_spec(window_spec), + }); + } + doc +} + +fn build_over_window_spec<'a>(over_window_spec: ast::OverWindowSpec) -> Doc<'a> { + let mut doc = Doc::text("("); + if let Some(window_spec) = over_window_spec.window_spec() { + doc = doc + .append(leading_comments(window_spec.syntax())) + .append(build_window_spec(window_spec)); + } + if let Some(r_paren) = over_window_spec.r_paren_token() { + doc = doc.append(comments_before(r_paren)); + } + doc.append(Doc::text(")")) +} + +fn build_window_spec<'a>(window_spec: ast::WindowSpec) -> Doc<'a> { + let mut parts = Vec::new(); + if let Some(window_ref) = window_spec.window_ref() { + parts.push(leading_comments(window_ref.syntax()).append(build_name(window_ref.syntax()))); + } + if let Some(partition_by) = window_spec.partition_by_clause() { + parts.push( + leading_comments(partition_by.syntax()).append(build_partition_by_clause(partition_by)), + ); + } + if let Some(order_by) = window_spec.order_by_clause() { + parts.push(leading_comments(order_by.syntax()).append(build_order_by_clause(order_by))); + } + if let Some(frame) = window_spec.frame_clause() { + parts.push(leading_comments(frame.syntax()).append(build_frame_clause(frame))); + } + + Doc::list(Itertools::intersperse(parts.into_iter(), Doc::space()).collect()) +} + +fn build_partition_by_clause<'a>(partition_by: ast::PartitionByClause) -> Doc<'a> { + let mut doc = Doc::text("partition").append(Doc::space()); + if let Some(by_token) = partition_by.by_token() { + doc = doc.append(leading_comments_token(&by_token)); + } + doc = doc.append(Doc::text("by")); + if let Some(exprs) = build_comma_separated_exprs(partition_by.exprs()) { + doc = doc.append(Doc::space()).append(exprs); + } + doc +} + +fn build_frame_clause<'a>(frame: ast::FrameClause) -> Doc<'a> { + let mut doc = match frame.frame_units() { + Some(ast::FrameUnits::FrameGroups(_)) => Doc::text("groups"), + Some(ast::FrameUnits::FrameRange(_)) => Doc::text("range"), + Some(ast::FrameUnits::FrameRows(_)) => Doc::text("rows"), + None => Doc::nil(), + }; + if let Some(extent) = frame.frame_extent() { + doc = doc + .append(Doc::space()) + .append(leading_comments(extent.syntax())) + .append(build_frame_extent(extent)); + } + if let Some(exclude) = frame.frame_exclude() { + doc = doc + .append(Doc::space()) + .append(leading_comments(exclude.syntax())) + .append(build_frame_exclude(exclude)); + } + doc +} + +fn build_frame_extent<'a>(extent: ast::FrameExtent) -> Doc<'a> { + match extent { + ast::FrameExtent::FrameBetween(between) => { + let mut doc = Doc::text("between"); + if let Some(start) = between.start() { + doc = doc + .append(Doc::space()) + .append(leading_comments(start.syntax())) + .append(build_frame_bound(start)); + } + if let Some(and_token) = between.and_token() { + doc = doc + .append(Doc::space()) + .append(leading_comments_token(&and_token)) + .append(Doc::text("and")); + } + if let Some(end) = between.end() { + doc = doc + .append(Doc::space()) + .append(leading_comments(end.syntax())) + .append(build_frame_bound(end)); + } + doc + } + ast::FrameExtent::FrameBound(bound) => build_frame_bound(bound), + } +} + +fn build_frame_bound<'a>(bound: ast::FrameBound) -> Doc<'a> { + match bound { + ast::FrameBound::CurrentRow(current_row) => Doc::text("current") + .append(Doc::space()) + .append( + current_row + .row_token() + .map(|token| leading_comments_token(&token)) + .unwrap_or_else(Doc::nil), + ) + .append(Doc::text("row")), + ast::FrameBound::ExprFollowing(following) => { + build_expr_frame_bound(following.expr(), following.following_token(), "following") + } + ast::FrameBound::ExprPreceding(preceding) => { + build_expr_frame_bound(preceding.expr(), preceding.preceding_token(), "preceding") + } + ast::FrameBound::UnboundedFollowing(following) => Doc::text("unbounded") + .append(Doc::space()) + .append( + following + .following_token() + .map(|token| leading_comments_token(&token)) + .unwrap_or_else(Doc::nil), + ) + .append(Doc::text("following")), + ast::FrameBound::UnboundedPreceding(preceding) => Doc::text("unbounded") + .append(Doc::space()) + .append( + preceding + .preceding_token() + .map(|token| leading_comments_token(&token)) + .unwrap_or_else(Doc::nil), + ) + .append(Doc::text("preceding")), + } +} + +fn build_expr_frame_bound<'a>( + expr: Option, + suffix_token: Option, + suffix: &'static str, +) -> Doc<'a> { + let mut doc = expr.map(build_expr).unwrap_or_else(Doc::nil); + if let Some(suffix_token) = suffix_token { + doc = doc + .append(Doc::space()) + .append(leading_comments_token(&suffix_token)) + .append(Doc::text(suffix)); + } + doc +} + +fn build_frame_exclude<'a>(exclude: ast::FrameExclude) -> Doc<'a> { + let mut doc = Doc::text("exclude"); + if let Some(target) = exclude.frame_exclude_target() { + doc = doc + .append(Doc::space()) + .append(leading_comments(target.syntax())) + .append(match target { + ast::FrameExcludeTarget::CurrentRow(current_row) => Doc::text("current") + .append(Doc::space()) + .append( + current_row + .row_token() + .map(|token| leading_comments_token(&token)) + .unwrap_or_else(Doc::nil), + ) + .append(Doc::text("row")), + ast::FrameExcludeTarget::Group(_) => Doc::text("group"), + ast::FrameExcludeTarget::NoOthers(no_others) => Doc::text("no") + .append(Doc::space()) + .append( + no_others + .others_token() + .map(|token| leading_comments_token(&token)) + .unwrap_or_else(Doc::nil), + ) + .append(Doc::text("others")), + ast::FrameExcludeTarget::Ties(_) => Doc::text("ties"), + }); + } + doc +} + +fn build_filter_clause<'a>(filter_clause: ast::FilterClause) -> Doc<'a> { + let mut doc = Doc::text("filter"); + if let Some(l_paren) = filter_clause.l_paren_token() { + doc = doc + .append(Doc::space()) + .append(leading_comments_token(&l_paren)) + .append(Doc::text("(")); + } + if let Some(where_token) = filter_clause.where_token() { + doc = doc + .append(leading_comments_token(&where_token)) + .append(Doc::text("where")); + } + if let Some(expr) = filter_clause.expr() { + doc = doc + .append(Doc::space()) + .append(leading_comments(expr.syntax())) + .append(build_expr(expr)); + } + if let Some(r_paren) = filter_clause.r_paren_token() { + doc = doc.append(comments_before(r_paren)); + } + doc.append(Doc::text(")")) +} + +fn build_null_treatment<'a>(null_treatment: ast::NullTreatment) -> Doc<'a> { + let (keyword, nulls_token) = match null_treatment { + ast::NullTreatment::IgnoreNulls(ignore_nulls) => ("ignore", ignore_nulls.nulls_token()), + ast::NullTreatment::RespectNulls(respect_nulls) => ("respect", respect_nulls.nulls_token()), + }; + + let mut doc = Doc::text(keyword); + if let Some(nulls_token) = nulls_token { + doc = doc + .append(Doc::space()) + .append(leading_comments_token(&nulls_token)) + .append(Doc::text("nulls")); + } + doc +} + fn build_json_keys_unique_clause<'a>(clause: ast::JsonKeysUniqueClause) -> Doc<'a> { let prefix = match clause { ast::JsonKeysUniqueClause::JsonWithoutUniqueKeys(_) => "without", diff --git a/crates/squawk_fmt/tests/after/select_expr.snap b/crates/squawk_fmt/tests/after/select_expr.snap index a10baf8f..b94cc41d 100644 --- a/crates/squawk_fmt/tests/after/select_expr.snap +++ b/crates/squawk_fmt/tests/after/select_expr.snap @@ -96,6 +96,21 @@ select array_agg(x /* before order */ order /* before by */ by /* before first */ y /* before desc */ desc /* before nulls */ nulls /* before last */ last /* before comma */, /* before second */ z /* before asc */ asc), public.foo(1), foo /* before opening paren */(/* before first arg */ 1 /* before comma */, /* before second arg */ 2 /* before closing paren */), + percentile_cont(0.5) within group (order by x), + mode() within group (order by y desc), + percentile_disc(0.5) /* before within */ within /* before group */ group /* before opening paren */ (/* before order */ order /* before by */ by /* before sort */ x /* before closing paren */) /* after within */, + count(*) filter (where x > 1), + sum(x) /* before filter */ filter /* before opening paren */ (/* before where */ where /* before expr */ x > 0 /* before closing paren */) /* after filter */, + first_value(x) ignore nulls, + last_value(y) respect nulls, + first_value(x) /* before treatment */ ignore /* before nulls */ nulls /* after nulls */, + sum(x) over window_name, + count(*) over (), + avg(x) over (w partition by a, b order by c desc rows between unbounded preceding and current row exclude ties), + sum(x) over (range 1 preceding), + sum(x) over (groups between current row and unbounded following exclude group), + sum(x) over (rows between 1 preceding and 2 following exclude no others), + sum(x) /* before over */ over /* before target */ (/* before partition */ partition /* before by */ by /* before expr */ a /* before comma */, /* before second */ b /* before order */ order /* before order by */ by /* before sort */ c /* before rows */ rows /* before between */ between /* before start */ 1 /* before preceding */ preceding /* before and */ and /* before end */ current /* before row */ row /* before exclude */ exclude /* before ties */ ties /* before close */) /* after over */, -- case expr case when x > 1 then 1 else 0 end, case x when 1 then 'one' when 2 then 'two' else 'other' end, diff --git a/crates/squawk_fmt/tests/before/select_expr.sql b/crates/squawk_fmt/tests/before/select_expr.sql index 066dc3c9..be6d00e8 100644 --- a/crates/squawk_fmt/tests/before/select_expr.sql +++ b/crates/squawk_fmt/tests/before/select_expr.sql @@ -91,6 +91,21 @@ select array_agg(x /* before order */ order /* before by */ by /* before first */ y /* before desc */ desc /* before nulls */ nulls /* before last */ last /* before comma */, /* before second */ z /* before asc */ asc), public.foo(1), foo /* before opening paren */ ( /* before first arg */ 1 /* before comma */, /* before second arg */ 2 /* before closing paren */ ), + percentile_cont(0.5) WITHIN GROUP (ORDER BY x), + mode() WITHIN GROUP (ORDER BY y DESC), + percentile_disc(0.5) /* before within */ WITHIN /* before group */ GROUP /* before opening paren */ ( /* before order */ ORDER /* before by */ BY /* before sort */ x /* before closing paren */ ) /* after within */, + count(*) FILTER (WHERE x > 1), + sum(x) /* before filter */ FILTER /* before opening paren */ ( /* before where */ WHERE /* before expr */ x > 0 /* before closing paren */ ) /* after filter */, + first_value(x) IGNORE NULLS, + last_value(y) RESPECT NULLS, + first_value(x) /* before treatment */ IGNORE /* before nulls */ NULLS /* after nulls */, + sum(x) OVER window_name, + count(*) OVER (), + avg(x) OVER (w PARTITION BY a, b ORDER BY c DESC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW EXCLUDE TIES), + sum(x) OVER (RANGE 1 PRECEDING), + sum(x) OVER (GROUPS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING EXCLUDE GROUP), + sum(x) OVER (ROWS BETWEEN 1 PRECEDING AND 2 FOLLOWING EXCLUDE NO OTHERS), + sum(x) /* before over */ OVER /* before target */ ( /* before partition */ PARTITION /* before by */ BY /* before expr */ a /* before comma */, /* before second */ b /* before order */ ORDER /* before order by */ BY /* before sort */ c /* before rows */ ROWS /* before between */ BETWEEN /* before start */ 1 /* before preceding */ PRECEDING /* before and */ AND /* before end */ CURRENT /* before row */ ROW /* before exclude */ EXCLUDE /* before ties */ TIES /* before close */ ) /* after over */, -- case expr case when x > 1 then 1 else 0 end, case x when 1 then 'one' when 2 then 'two' else 'other' end, From ddad9fe46c27a2606bfd396c289f95f9c58888ac Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Sun, 23 Aug 2026 19:23:05 -0400 Subject: [PATCH 05/17] fmt: json_array, json_array_agg --- crates/squawk_fmt/src/fmt.rs | 206 +++++++++++++++++- .../squawk_fmt/tests/after/select_expr.snap | 8 + .../squawk_fmt/tests/before/select_expr.sql | 7 + 3 files changed, 217 insertions(+), 4 deletions(-) diff --git a/crates/squawk_fmt/src/fmt.rs b/crates/squawk_fmt/src/fmt.rs index 6ec69bbf..2ec3f7d8 100644 --- a/crates/squawk_fmt/src/fmt.rs +++ b/crates/squawk_fmt/src/fmt.rs @@ -824,10 +824,10 @@ fn build_call_expr<'a>(call_expr: ast::CallExpr) -> Doc<'a> { build_extract_fn(extract_fn) } else if let Some(_graph_table_fn) = call_expr.graph_table_fn() { todo!("graph_table function expressions are not supported yet") - } else if let Some(_json_array_agg_fn) = call_expr.json_array_agg_fn() { - todo!("json_arrayagg function expressions are not supported yet") - } else if let Some(_json_array_fn) = call_expr.json_array_fn() { - todo!("json_array function expressions are not supported yet") + } else if let Some(json_array_agg_fn) = call_expr.json_array_agg_fn() { + build_json_array_agg_fn(json_array_agg_fn) + } else if let Some(json_array_fn) = call_expr.json_array_fn() { + build_json_array_fn(json_array_fn) } else if let Some(_json_exists_fn) = call_expr.json_exists_fn() { todo!("json_exists function expressions are not supported yet") } else if let Some(_json_fn) = call_expr.json_fn() { @@ -879,6 +879,204 @@ fn build_call_expr<'a>(call_expr: ast::CallExpr) -> Doc<'a> { } } +fn build_json_array_fn<'a>(json_array_fn: ast::JsonArrayFn) -> Doc<'a> { + let mut doc = Doc::text("json_array"); + if let Some(l_paren) = json_array_fn.l_paren_token() { + doc = doc.append(comments_before(l_paren)); + } + doc = doc.append(Doc::text("(")); + + let exprs = json_array_fn.json_expr_formats().map(|value| { + ( + leading_comments(value.syntax()).append(build_json_expr_format(value.clone())), + value.syntax().clone(), + ) + }); + let selects = json_array_fn.json_select_formats().map(|select| { + ( + leading_comments(select.syntax()).append(build_json_select_format(select.clone())), + select.syntax().clone(), + ) + }); + let mut items = exprs.chain(selects); + if let Some((first, mut previous_syntax)) = items.next() { + let mut item_docs = vec![first]; + for (item, syntax) in items { + item_docs.push( + trailing_comments(&previous_syntax) + .append(Doc::text(",")) + .append(Doc::space()) + .append(item), + ); + previous_syntax = syntax; + } + doc = doc.append(Doc::list(item_docs)); + } + + if let Some(null_clause) = json_array_fn.json_null_clause() { + doc = doc + .append(Doc::space()) + .append(leading_comments(null_clause.syntax())) + .append(build_json_null_clause(null_clause)); + } + if let Some(returning) = json_array_fn.json_returning_clause() { + doc = doc + .append(Doc::space()) + .append(leading_comments(returning.syntax())) + .append(build_json_returning_clause(returning)); + } + if let Some(r_paren) = json_array_fn.r_paren_token() { + doc = doc.append(comments_before(r_paren)); + } + doc.append(Doc::text(")")) +} + +fn build_json_expr_format<'a>(value: ast::JsonExprFormat) -> Doc<'a> { + let mut doc = value.expr().map(build_expr).unwrap_or_else(Doc::nil); + if let Some(format) = value.json_format_clause() { + doc = doc + .append(Doc::space()) + .append(leading_comments(format.syntax())) + .append(build_json_format_clause(format)); + } + doc +} + +fn build_json_select_format<'a>(select: ast::JsonSelectFormat) -> Doc<'a> { + let mut doc = select + .select_variant() + .map(|select| match select { + ast::SelectVariant::Select(select) => build_select_doc(&select), + _ => todo!("this select variant is not supported yet"), + }) + .unwrap_or_else(Doc::nil); + if let Some(format) = select.json_format_clause() { + doc = doc + .append(Doc::space()) + .append(leading_comments(format.syntax())) + .append(build_json_format_clause(format)); + } + doc +} + +fn build_json_array_agg_fn<'a>(json_array_agg_fn: ast::JsonArrayAggFn) -> Doc<'a> { + let mut doc = Doc::text("json_arrayagg"); + if let Some(l_paren) = json_array_agg_fn.l_paren_token() { + doc = doc.append(comments_before(l_paren)); + } + doc = doc.append(Doc::text("(")); + + if let Some(value) = json_array_agg_fn.json_value_expr() { + doc = doc + .append(leading_comments(value.syntax())) + .append(build_json_value_expr(value)); + } + if let Some(order_by) = json_array_agg_fn.order_by_clause() { + doc = doc + .append(Doc::space()) + .append(leading_comments(order_by.syntax())) + .append(build_order_by_clause(order_by)); + } + if let Some(null_clause) = json_array_agg_fn.json_null_clause() { + doc = doc + .append(Doc::space()) + .append(leading_comments(null_clause.syntax())) + .append(build_json_null_clause(null_clause)); + } + if let Some(returning) = json_array_agg_fn.json_returning_clause() { + doc = doc + .append(Doc::space()) + .append(leading_comments(returning.syntax())) + .append(build_json_returning_clause(returning)); + } + if let Some(r_paren) = json_array_agg_fn.r_paren_token() { + doc = doc.append(comments_before(r_paren)); + } + doc.append(Doc::text(")")) +} + +fn build_json_value_expr<'a>(value: ast::JsonValueExpr) -> Doc<'a> { + let mut doc = value.expr().map(build_expr).unwrap_or_else(Doc::nil); + if let Some(format) = value.json_format_clause() { + doc = doc + .append(Doc::space()) + .append(leading_comments(format.syntax())) + .append(build_json_format_clause(format)); + } + doc +} + +fn build_json_format_clause<'a>(format: ast::JsonFormatClause) -> Doc<'a> { + let mut doc = Doc::text("format"); + if let Some(json_token) = format.json_token() { + doc = doc + .append(Doc::space()) + .append(leading_comments_token(&json_token)) + .append(Doc::text("json")); + } + if let Some(encoding) = format.json_encoding_clause() { + doc = doc + .append(Doc::space()) + .append(leading_comments(encoding.syntax())) + .append(build_json_encoding_clause(encoding)); + } + doc +} + +fn build_json_encoding_clause<'a>(clause: ast::JsonEncodingClause) -> Doc<'a> { + let mut doc = Doc::text("encoding"); + if let Some(encoding) = clause.json_encoding() { + doc = doc + .append(Doc::space()) + .append(leading_comments(encoding.syntax())) + .append(build_name(encoding.syntax())); + } + doc +} + +fn build_json_null_clause<'a>(clause: ast::JsonNullClause) -> Doc<'a> { + let (prefix, on_token, null_token) = match clause { + ast::JsonNullClause::JsonAbsentOnNull(clause) => { + ("absent", clause.on_token(), clause.null_token()) + } + ast::JsonNullClause::JsonNullOnNull(clause) => { + ("null", clause.on_token(), clause.null_token()) + } + }; + + let mut doc = Doc::text(prefix); + if let Some(on_token) = on_token { + doc = doc + .append(Doc::space()) + .append(leading_comments_token(&on_token)) + .append(Doc::text("on")); + } + if let Some(null_token) = null_token { + doc = doc + .append(Doc::space()) + .append(leading_comments_token(&null_token)) + .append(Doc::text("null")); + } + doc +} + +fn build_json_returning_clause<'a>(returning: ast::JsonReturningClause) -> Doc<'a> { + let mut doc = Doc::text("returning"); + if let Some(ty) = returning.ty() { + doc = doc + .append(Doc::space()) + .append(leading_comments(ty.syntax())) + .append(build_type(ty)); + } + if let Some(format) = returning.json_format_clause() { + doc = doc + .append(Doc::space()) + .append(leading_comments(format.syntax())) + .append(build_json_format_clause(format)); + } + doc +} + fn build_substring_fn<'a>(substring_fn: ast::SubstringFn) -> Doc<'a> { let mut doc = Doc::text("substring"); if let Some(l_paren) = substring_fn.l_paren_token() { diff --git a/crates/squawk_fmt/tests/after/select_expr.snap b/crates/squawk_fmt/tests/after/select_expr.snap index b94cc41d..5c76f8e5 100644 --- a/crates/squawk_fmt/tests/after/select_expr.snap +++ b/crates/squawk_fmt/tests/after/select_expr.snap @@ -94,6 +94,14 @@ select foo(a => x order by y), array_agg(x order by y using > nulls first), array_agg(x /* before order */ order /* before by */ by /* before first */ y /* before desc */ desc /* before nulls */ nulls /* before last */ last /* before comma */, /* before second */ z /* before asc */ asc), + json_arrayagg(v), + json_arrayagg(v format json order by sort_col desc null on null returning jsonb format json), + json_arrayagg /* before opening paren */(/* before value */ v /* before value format */ format /* before value json */ json /* before encoding */ encoding /* before encoding name */ utf8 /* before order */ order /* before by */ by /* before sort */ x /* before absent */ absent /* before on */ on /* before null */ null /* before returning */ returning /* before type */ jsonb /* before returning format */ format /* before returning json */ json /* before closing paren */), + json_array(), + json_array(1, 2 format json returning jsonb), + json_array /* before opening paren */(/* before first */ 1 /* before comma */, /* before second */ 2 /* before format */ format /* before json */ json /* before encoding */ encoding /* before encoding name */ utf8 /* before returning */ returning /* before type */ jsonb /* before returning format */ format /* before returning json */ json /* before closing paren */), + json_array(/* before select */ select + /* before target */ x /* before format */ format /* before json */ json /* before absent */ absent /* before on */ on /* before null */ null /* before returning */ returning /* before type */ jsonb /* before closing paren */), public.foo(1), foo /* before opening paren */(/* before first arg */ 1 /* before comma */, /* before second arg */ 2 /* before closing paren */), percentile_cont(0.5) within group (order by x), diff --git a/crates/squawk_fmt/tests/before/select_expr.sql b/crates/squawk_fmt/tests/before/select_expr.sql index be6d00e8..f1003589 100644 --- a/crates/squawk_fmt/tests/before/select_expr.sql +++ b/crates/squawk_fmt/tests/before/select_expr.sql @@ -89,6 +89,13 @@ select foo(a => x order by y), array_agg(x order by y using > nulls first), array_agg(x /* before order */ order /* before by */ by /* before first */ y /* before desc */ desc /* before nulls */ nulls /* before last */ last /* before comma */, /* before second */ z /* before asc */ asc), + JSON_ARRAYAGG(v), + JSON_ARRAYAGG(v FORMAT JSON ORDER BY sort_col DESC NULL ON NULL RETURNING jsonb FORMAT JSON), + JSON_ARRAYAGG /* before opening paren */ (/* before value */ v /* before value format */ FORMAT /* before value json */ JSON /* before encoding */ ENCODING /* before encoding name */ UTF8 /* before order */ ORDER /* before by */ BY /* before sort */ x /* before absent */ ABSENT /* before on */ ON /* before null */ NULL /* before returning */ RETURNING /* before type */ jsonb /* before returning format */ FORMAT /* before returning json */ JSON /* before closing paren */), + JSON_ARRAY(), + JSON_ARRAY(1, 2 FORMAT JSON RETURNING jsonb), + JSON_ARRAY /* before opening paren */ (/* before first */ 1 /* before comma */, /* before second */ 2 /* before format */ FORMAT /* before json */ JSON /* before encoding */ ENCODING /* before encoding name */ UTF8 /* before returning */ RETURNING /* before type */ jsonb /* before returning format */ FORMAT /* before returning json */ JSON /* before closing paren */), + JSON_ARRAY(/* before select */ SELECT /* before target */ x /* before format */ FORMAT /* before json */ JSON /* before absent */ ABSENT /* before on */ ON /* before null */ NULL /* before returning */ RETURNING /* before type */ jsonb /* before closing paren */), public.foo(1), foo /* before opening paren */ ( /* before first arg */ 1 /* before comma */, /* before second arg */ 2 /* before closing paren */ ), percentile_cont(0.5) WITHIN GROUP (ORDER BY x), From 03fc8e688c47d9ed66504dc66a303c2163e9507b Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Sun, 23 Aug 2026 20:17:17 -0400 Subject: [PATCH 06/17] fmt: json_exists/json/json_object_agg/json_object --- crates/squawk_fmt/src/fmt.rs | 368 ++++++++++++++++-- .../squawk_fmt/tests/after/select_expr.snap | 21 + .../squawk_fmt/tests/before/select_expr.sql | 21 + crates/squawk_syntax/src/ast/node_ext.rs | 23 ++ 4 files changed, 403 insertions(+), 30 deletions(-) diff --git a/crates/squawk_fmt/src/fmt.rs b/crates/squawk_fmt/src/fmt.rs index 2ec3f7d8..19c8f633 100644 --- a/crates/squawk_fmt/src/fmt.rs +++ b/crates/squawk_fmt/src/fmt.rs @@ -828,14 +828,14 @@ fn build_call_expr<'a>(call_expr: ast::CallExpr) -> Doc<'a> { build_json_array_agg_fn(json_array_agg_fn) } else if let Some(json_array_fn) = call_expr.json_array_fn() { build_json_array_fn(json_array_fn) - } else if let Some(_json_exists_fn) = call_expr.json_exists_fn() { - todo!("json_exists function expressions are not supported yet") - } else if let Some(_json_fn) = call_expr.json_fn() { - todo!("json function expressions are not supported yet") - } else if let Some(_json_object_agg_fn) = call_expr.json_object_agg_fn() { - todo!("json_objectagg function expressions are not supported yet") - } else if let Some(_json_object_fn) = call_expr.json_object_fn() { - todo!("json_object function expressions are not supported yet") + } else if let Some(json_exists_fn) = call_expr.json_exists_fn() { + build_json_exists_fn(json_exists_fn) + } else if let Some(json_fn) = call_expr.json_fn() { + build_json_fn(json_fn) + } else if let Some(json_object_agg_fn) = call_expr.json_object_agg_fn() { + build_json_object_agg_fn(json_object_agg_fn) + } else if let Some(json_object_fn) = call_expr.json_object_fn() { + build_json_object_fn(json_object_fn) } else if let Some(_json_query_fn) = call_expr.json_query_fn() { todo!("json_query function expressions are not supported yet") } else if let Some(_json_scalar_fn) = call_expr.json_scalar_fn() { @@ -879,6 +879,294 @@ fn build_call_expr<'a>(call_expr: ast::CallExpr) -> Doc<'a> { } } +fn build_json_object_fn<'a>(json_object_fn: ast::JsonObjectFn) -> Doc<'a> { + let mut doc = Doc::text("json_object"); + if let Some(l_paren) = json_object_fn.l_paren_token() { + doc = doc.append(comments_before(l_paren)); + } + doc = doc.append(Doc::text("(")); + + let exprs = json_object_fn.exprs().map(|expr| { + ( + leading_comments(expr.syntax()).append(build_expr(expr.clone())), + expr.syntax().clone(), + ) + }); + let key_values = json_object_fn.json_key_values().map(|key_value| { + ( + leading_comments(key_value.syntax()).append(build_json_key_value(key_value.clone())), + key_value.syntax().clone(), + ) + }); + let items = build_comma_separated_docs(exprs.chain(key_values)); + let mut has_content = items.is_some(); + if let Some(items) = items { + doc = doc.append(items); + } + + if let Some(null_clause) = json_object_fn.json_null_clause() { + if has_content { + doc = doc.append(Doc::space()); + } + doc = doc + .append(leading_comments(null_clause.syntax())) + .append(build_json_null_clause(null_clause)); + has_content = true; + } + if let Some(unique) = json_object_fn.json_keys_unique_clause() { + if has_content { + doc = doc.append(Doc::space()); + } + doc = doc + .append(leading_comments(unique.syntax())) + .append(build_json_keys_unique_clause(unique)); + has_content = true; + } + if let Some(returning) = json_object_fn.json_returning_clause() { + if has_content { + doc = doc.append(Doc::space()); + } + doc = doc + .append(leading_comments(returning.syntax())) + .append(build_json_returning_clause(returning)); + } + if let Some(r_paren) = json_object_fn.r_paren_token() { + doc = doc.append(comments_before(r_paren)); + } + doc.append(Doc::text(")")) +} + +fn build_json_object_agg_fn<'a>(json_object_agg_fn: ast::JsonObjectAggFn) -> Doc<'a> { + let mut doc = Doc::text("json_objectagg"); + if let Some(l_paren) = json_object_agg_fn.l_paren_token() { + doc = doc.append(comments_before(l_paren)); + } + doc = doc.append(Doc::text("(")); + + if let Some(key_value) = json_object_agg_fn.json_key_value() { + doc = doc + .append(leading_comments(key_value.syntax())) + .append(build_json_key_value(key_value)); + } + if let Some(null_clause) = json_object_agg_fn.json_null_clause() { + doc = doc + .append(Doc::space()) + .append(leading_comments(null_clause.syntax())) + .append(build_json_null_clause(null_clause)); + } + if let Some(unique) = json_object_agg_fn.json_keys_unique_clause() { + doc = doc + .append(Doc::space()) + .append(leading_comments(unique.syntax())) + .append(build_json_keys_unique_clause(unique)); + } + if let Some(returning) = json_object_agg_fn.json_returning_clause() { + doc = doc + .append(Doc::space()) + .append(leading_comments(returning.syntax())) + .append(build_json_returning_clause(returning)); + } + if let Some(r_paren) = json_object_agg_fn.r_paren_token() { + doc = doc.append(comments_before(r_paren)); + } + doc.append(Doc::text(")")) +} + +fn build_json_key_value<'a>(key_value: ast::JsonKeyValue) -> Doc<'a> { + let mut doc = key_value.expr().map(build_expr).unwrap_or_else(Doc::nil); + if let Some(colon) = key_value.colon_token() { + doc = doc.append(comments_before(colon)).append(Doc::text(":")); + } else if let Some(value_token) = key_value.value_token() { + doc = doc + .append(Doc::space()) + .append(leading_comments_token(&value_token)) + .append(Doc::text("value")); + } + if let Some(value) = key_value.json_value_expr() { + doc = doc + .append(Doc::space()) + .append(leading_comments(value.syntax())) + .append(build_json_value_expr(value)); + } + doc +} + +fn build_json_fn<'a>(json_fn: ast::JsonFn) -> Doc<'a> { + let mut doc = Doc::text("json"); + if let Some(l_paren) = json_fn.l_paren_token() { + doc = doc.append(comments_before(l_paren)); + } + doc = doc.append(Doc::text("(")); + + if let Some(expr) = json_fn.expr() { + doc = doc + .append(leading_comments(expr.syntax())) + .append(build_expr(expr)); + } + if let Some(format) = json_fn.json_format_clause() { + doc = doc + .append(Doc::space()) + .append(leading_comments(format.syntax())) + .append(build_json_format_clause(format)); + } + if let Some(unique) = json_fn.json_keys_unique_clause() { + doc = doc + .append(Doc::space()) + .append(leading_comments(unique.syntax())) + .append(build_json_keys_unique_clause(unique)); + } + if let Some(r_paren) = json_fn.r_paren_token() { + doc = doc.append(comments_before(r_paren)); + } + doc.append(Doc::text(")")) +} + +fn build_json_exists_fn<'a>(json_exists_fn: ast::JsonExistsFn) -> Doc<'a> { + let mut doc = Doc::text("json_exists"); + if let Some(l_paren) = json_exists_fn.l_paren_token() { + doc = doc.append(comments_before(l_paren)); + } + doc = doc.append(Doc::text("(")); + + if let Some(document) = json_exists_fn.document() { + doc = doc + .append(leading_comments(document.syntax())) + .append(build_expr(document)); + } + if let Some(format) = json_exists_fn.json_format_clause() { + doc = doc + .append(Doc::space()) + .append(leading_comments(format.syntax())) + .append(build_json_format_clause(format)); + } + if let Some(comma) = json_exists_fn.comma_token() { + doc = doc + .append(comments_before(comma)) + .append(Doc::text(",")) + .append(Doc::space()); + } + if let Some(path) = json_exists_fn.path() { + doc = doc + .append(leading_comments(path.syntax())) + .append(build_expr(path)); + } + if let Some(passing) = json_exists_fn.json_passing_clause() { + doc = doc + .append(Doc::space()) + .append(leading_comments(passing.syntax())) + .append(build_json_passing_clause(passing)); + } + if let Some(on_error) = json_exists_fn.json_on_error_clause() { + doc = doc + .append(Doc::space()) + .append(leading_comments(on_error.syntax())) + .append(build_json_on_error_clause(on_error)); + } + if let Some(r_paren) = json_exists_fn.r_paren_token() { + doc = doc.append(comments_before(r_paren)); + } + doc.append(Doc::text(")")) +} + +fn build_json_passing_clause<'a>(passing: ast::JsonPassingClause) -> Doc<'a> { + let mut doc = Doc::text("passing"); + let mut args = passing.json_passing_args(); + if let Some(first) = args.next() { + let mut previous_syntax = first.syntax().clone(); + doc = doc + .append(Doc::space()) + .append(leading_comments(first.syntax())) + .append(build_json_passing_arg(first)); + for arg in args { + doc = doc + .append(trailing_comments(&previous_syntax)) + .append(Doc::text(",")) + .append(Doc::space()) + .append(leading_comments(arg.syntax())) + .append(build_json_passing_arg(arg.clone())); + previous_syntax = arg.syntax().clone(); + } + } + doc +} + +fn build_json_passing_arg<'a>(arg: ast::JsonPassingArg) -> Doc<'a> { + let mut doc = arg.expr().map(build_expr).unwrap_or_else(Doc::nil); + if let Some(as_token) = arg.as_token() { + doc = doc + .append(Doc::space()) + .append(leading_comments_token(&as_token)) + .append(Doc::text("as")); + } + if let Some(name) = arg.name() { + doc = doc + .append(Doc::space()) + .append(leading_comments(name.syntax())) + .append(build_name(name.syntax())); + } + doc +} + +fn build_json_on_error_clause<'a>(clause: ast::JsonOnErrorClause) -> Doc<'a> { + let mut doc = clause + .json_behavior() + .map(build_json_behavior) + .unwrap_or_else(Doc::nil); + if let Some(on_token) = clause.on_token() { + doc = doc + .append(Doc::space()) + .append(leading_comments_token(&on_token)) + .append(Doc::text("on")); + } + if let Some(error_token) = clause.error_token() { + doc = doc + .append(Doc::space()) + .append(leading_comments_token(&error_token)) + .append(Doc::text("error")); + } + doc +} + +fn build_json_behavior<'a>(behavior: ast::JsonBehavior) -> Doc<'a> { + match behavior { + ast::JsonBehavior::JsonBehaviorDefault(behavior) => { + let mut doc = Doc::text("default"); + if let Some(expr) = behavior.expr() { + doc = doc + .append(Doc::space()) + .append(leading_comments(expr.syntax())) + .append(build_expr(expr)); + } + doc + } + ast::JsonBehavior::JsonBehaviorEmptyArray(behavior) => { + let mut doc = Doc::text("empty"); + if let Some(array_token) = behavior.array_token() { + doc = doc + .append(Doc::space()) + .append(leading_comments_token(&array_token)) + .append(Doc::text("array")); + } + doc + } + ast::JsonBehavior::JsonBehaviorEmptyObject(behavior) => { + let mut doc = Doc::text("empty"); + if let Some(object_token) = behavior.object_token() { + doc = doc + .append(Doc::space()) + .append(leading_comments_token(&object_token)) + .append(Doc::text("object")); + } + doc + } + ast::JsonBehavior::JsonBehaviorError(_) => Doc::text("error"), + ast::JsonBehavior::JsonBehaviorFalse(_) => Doc::text("false"), + ast::JsonBehavior::JsonBehaviorNull(_) => Doc::text("null"), + ast::JsonBehavior::JsonBehaviorTrue(_) => Doc::text("true"), + ast::JsonBehavior::JsonBehaviorUnknown(_) => Doc::text("unknown"), + } +} + fn build_json_array_fn<'a>(json_array_fn: ast::JsonArrayFn) -> Doc<'a> { let mut doc = Doc::text("json_array"); if let Some(l_paren) = json_array_fn.l_paren_token() { @@ -898,19 +1186,8 @@ fn build_json_array_fn<'a>(json_array_fn: ast::JsonArrayFn) -> Doc<'a> { select.syntax().clone(), ) }); - let mut items = exprs.chain(selects); - if let Some((first, mut previous_syntax)) = items.next() { - let mut item_docs = vec![first]; - for (item, syntax) in items { - item_docs.push( - trailing_comments(&previous_syntax) - .append(Doc::text(",")) - .append(Doc::space()) - .append(item), - ); - previous_syntax = syntax; - } - doc = doc.append(Doc::list(item_docs)); + if let Some(items) = build_comma_separated_docs(exprs.chain(selects)) { + doc = doc.append(items); } if let Some(null_clause) = json_array_fn.json_null_clause() { @@ -931,6 +1208,23 @@ fn build_json_array_fn<'a>(json_array_fn: ast::JsonArrayFn) -> Doc<'a> { doc.append(Doc::text(")")) } +fn build_comma_separated_docs<'a>( + mut items: impl Iterator, SyntaxNode)>, +) -> Option> { + let (first, mut previous_syntax) = items.next()?; + let mut docs = vec![first]; + for (item, syntax) in items { + docs.push( + trailing_comments(&previous_syntax) + .append(Doc::text(",")) + .append(Doc::space()) + .append(item), + ); + previous_syntax = syntax; + } + Some(Doc::list(docs)) +} + fn build_json_expr_format<'a>(value: ast::JsonExprFormat) -> Doc<'a> { let mut doc = value.expr().map(build_expr).unwrap_or_else(Doc::nil); if let Some(format) = value.json_format_clause() { @@ -1040,7 +1334,7 @@ fn build_json_null_clause<'a>(clause: ast::JsonNullClause) -> Doc<'a> { ("absent", clause.on_token(), clause.null_token()) } ast::JsonNullClause::JsonNullOnNull(clause) => { - ("null", clause.on_token(), clause.null_token()) + ("null", clause.on_token(), clause.on_null_token()) } }; @@ -2034,15 +2328,29 @@ fn build_null_treatment<'a>(null_treatment: ast::NullTreatment) -> Doc<'a> { } fn build_json_keys_unique_clause<'a>(clause: ast::JsonKeysUniqueClause) -> Doc<'a> { - let prefix = match clause { - ast::JsonKeysUniqueClause::JsonWithoutUniqueKeys(_) => "without", - ast::JsonKeysUniqueClause::JsonWithUniqueKeys(_) => "with", + let (prefix, unique_token, keys_token) = match clause { + ast::JsonKeysUniqueClause::JsonWithoutUniqueKeys(clause) => { + ("without", clause.unique_token(), clause.keys_token()) + } + ast::JsonKeysUniqueClause::JsonWithUniqueKeys(clause) => { + ("with", clause.unique_token(), clause.keys_token()) + } }; - Doc::text(prefix) - .append(Doc::space()) - .append(Doc::text("unique")) - .append(Doc::space()) - .append(Doc::text("keys")) + + let mut doc = Doc::text(prefix); + if let Some(unique_token) = unique_token { + doc = doc + .append(Doc::space()) + .append(leading_comments_token(&unique_token)) + .append(Doc::text("unique")); + } + if let Some(keys_token) = keys_token { + doc = doc + .append(Doc::space()) + .append(leading_comments_token(&keys_token)) + .append(Doc::text("keys")); + } + doc } fn build_unicode_normal_form<'a>(form: ast::UnicodeNormalForm) -> Doc<'a> { diff --git a/crates/squawk_fmt/tests/after/select_expr.snap b/crates/squawk_fmt/tests/after/select_expr.snap index 5c76f8e5..55dca1ec 100644 --- a/crates/squawk_fmt/tests/after/select_expr.snap +++ b/crates/squawk_fmt/tests/after/select_expr.snap @@ -102,6 +102,27 @@ select json_array /* before opening paren */(/* before first */ 1 /* before comma */, /* before second */ 2 /* before format */ format /* before json */ json /* before encoding */ encoding /* before encoding name */ utf8 /* before returning */ returning /* before type */ jsonb /* before returning format */ format /* before returning json */ json /* before closing paren */), json_array(/* before select */ select /* before target */ x /* before format */ format /* before json */ json /* before absent */ absent /* before on */ on /* before null */ null /* before returning */ returning /* before type */ jsonb /* before closing paren */), + json_exists(doc, '$.a'), + json_exists(doc, '$.a' passing x as foo, y as bar true on error), + json_exists /* before opening paren */(/* before document */ doc /* before format */ format /* before json */ json /* before encoding */ encoding /* before encoding name */ utf8 /* before comma */, /* before path */ '$.a' /* before passing */ passing /* before arg */ x /* before as */ as /* before name */ foo /* before arg comma */, /* before second arg */ y /* before second as */ as /* before second name */ bar /* before behavior */ default /* before default */ false /* before on */ on /* before error */ error /* before closing paren */), + json_exists(doc, '$' error on error), + json_exists(doc, '$' null on error), + json_exists(doc, '$' false on error), + json_exists(doc, '$' unknown on error), + json_exists(doc, '$' empty array on error), + json_exists(doc, '$' empty object on error), + json(doc), + json(doc format json encoding utf8 with unique keys), + json(doc without unique keys), + json /* before opening paren */(/* before expression */ doc /* before format */ format /* before json */ json /* before encoding */ encoding /* before encoding name */ utf8 /* before without */ without /* before unique */ unique /* before keys */ keys /* before closing paren */), + json_objectagg(k: v), + json_objectagg(k value v format json absent on null with unique keys returning jsonb format json), + json_objectagg /* before opening paren */(/* before key */ k /* before value keyword */ value /* before value */ v /* before value format */ format /* before value json */ json /* before null */ null /* before on */ on /* before second null */ null /* before without */ without /* before unique */ unique /* before keys */ keys /* before returning */ returning /* before type */ jsonb /* before returning format */ format /* before returning json */ json /* before closing paren */), + json_object(), + json_object('a', 1, 'b', 2), + json_object('a': 1, 'b' value 2 format json null on null with unique keys returning jsonb format json), + json_object(returning jsonb), + json_object /* before opening paren */(/* before key */ 'a' /* before colon */: /* before value */ 1 /* before comma */, /* before second key */ 'b' /* before value keyword */ value /* before second value */ 2 /* before format */ format /* before json */ json /* before absent */ absent /* before on */ on /* before null */ null /* before with */ with /* before unique */ unique /* before keys */ keys /* before returning */ returning /* before type */ jsonb /* before returning format */ format /* before returning json */ json /* before closing paren */), public.foo(1), foo /* before opening paren */(/* before first arg */ 1 /* before comma */, /* before second arg */ 2 /* before closing paren */), percentile_cont(0.5) within group (order by x), diff --git a/crates/squawk_fmt/tests/before/select_expr.sql b/crates/squawk_fmt/tests/before/select_expr.sql index f1003589..b26c090b 100644 --- a/crates/squawk_fmt/tests/before/select_expr.sql +++ b/crates/squawk_fmt/tests/before/select_expr.sql @@ -96,6 +96,27 @@ select JSON_ARRAY(1, 2 FORMAT JSON RETURNING jsonb), JSON_ARRAY /* before opening paren */ (/* before first */ 1 /* before comma */, /* before second */ 2 /* before format */ FORMAT /* before json */ JSON /* before encoding */ ENCODING /* before encoding name */ UTF8 /* before returning */ RETURNING /* before type */ jsonb /* before returning format */ FORMAT /* before returning json */ JSON /* before closing paren */), JSON_ARRAY(/* before select */ SELECT /* before target */ x /* before format */ FORMAT /* before json */ JSON /* before absent */ ABSENT /* before on */ ON /* before null */ NULL /* before returning */ RETURNING /* before type */ jsonb /* before closing paren */), + JSON_EXISTS(doc, '$.a'), + JSON_EXISTS(doc, '$.a' PASSING x AS foo, y AS bar TRUE ON ERROR), + JSON_EXISTS /* before opening paren */ (/* before document */ doc /* before format */ FORMAT /* before json */ JSON /* before encoding */ ENCODING /* before encoding name */ UTF8 /* before comma */, /* before path */ '$.a' /* before passing */ PASSING /* before arg */ x /* before as */ AS /* before name */ foo /* before arg comma */, /* before second arg */ y /* before second as */ AS /* before second name */ bar /* before behavior */ DEFAULT /* before default */ false /* before on */ ON /* before error */ ERROR /* before closing paren */), + JSON_EXISTS(doc, '$' ERROR ON ERROR), + JSON_EXISTS(doc, '$' NULL ON ERROR), + JSON_EXISTS(doc, '$' FALSE ON ERROR), + JSON_EXISTS(doc, '$' UNKNOWN ON ERROR), + JSON_EXISTS(doc, '$' EMPTY ARRAY ON ERROR), + JSON_EXISTS(doc, '$' EMPTY OBJECT ON ERROR), + JSON(doc), + JSON(doc FORMAT JSON ENCODING UTF8 WITH UNIQUE KEYS), + JSON(doc WITHOUT UNIQUE KEYS), + JSON /* before opening paren */ (/* before expression */ doc /* before format */ FORMAT /* before json */ JSON /* before encoding */ ENCODING /* before encoding name */ UTF8 /* before without */ WITHOUT /* before unique */ UNIQUE /* before keys */ KEYS /* before closing paren */), + JSON_OBJECTAGG(k: v), + JSON_OBJECTAGG(k VALUE v FORMAT JSON ABSENT ON NULL WITH UNIQUE KEYS RETURNING jsonb FORMAT JSON), + JSON_OBJECTAGG /* before opening paren */ (/* before key */ k /* before value keyword */ VALUE /* before value */ v /* before value format */ FORMAT /* before value json */ JSON /* before null */ NULL /* before on */ ON /* before second null */ NULL /* before without */ WITHOUT /* before unique */ UNIQUE /* before keys */ KEYS /* before returning */ RETURNING /* before type */ jsonb /* before returning format */ FORMAT /* before returning json */ JSON /* before closing paren */), + JSON_OBJECT(), + JSON_OBJECT('a', 1, 'b', 2), + JSON_OBJECT('a': 1, 'b' VALUE 2 FORMAT JSON NULL ON NULL WITH UNIQUE KEYS RETURNING jsonb FORMAT JSON), + JSON_OBJECT(RETURNING jsonb), + JSON_OBJECT /* before opening paren */ (/* before key */ 'a' /* before colon */ : /* before value */ 1 /* before comma */, /* before second key */ 'b' /* before value keyword */ VALUE /* before second value */ 2 /* before format */ FORMAT /* before json */ JSON /* before absent */ ABSENT /* before on */ ON /* before null */ NULL /* before with */ WITH /* before unique */ UNIQUE /* before keys */ KEYS /* before returning */ RETURNING /* before type */ jsonb /* before returning format */ FORMAT /* before returning json */ JSON /* before closing paren */), public.foo(1), foo /* before opening paren */ ( /* before first arg */ 1 /* before comma */, /* before second arg */ 2 /* before closing paren */ ), percentile_cont(0.5) WITHIN GROUP (ORDER BY x), diff --git a/crates/squawk_syntax/src/ast/node_ext.rs b/crates/squawk_syntax/src/ast/node_ext.rs index 2b60e5d6..e24fc575 100644 --- a/crates/squawk_syntax/src/ast/node_ext.rs +++ b/crates/squawk_syntax/src/ast/node_ext.rs @@ -846,6 +846,29 @@ impl ast::ExtractFieldName { } } +impl ast::JsonNullOnNull { + #[inline] + pub fn on_null_token(&self) -> Option { + self.syntax() + .children_with_tokens() + .filter_map(|element| element.into_token()) + .filter(|token| token.kind() == SyntaxKind::NULL_KW) + .nth(1) + } +} + +impl ast::JsonExistsFn { + #[inline] + pub fn document(&self) -> Option { + support::children(self.syntax()).next() + } + + #[inline] + pub fn path(&self) -> Option { + support::children(self.syntax()).nth(1) + } +} + impl ast::PositionFn { #[inline] pub fn pos(&self) -> Option { From 31534fb4698d04ca4907bcf1b7c737712fa8f4c5 Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Sun, 23 Aug 2026 20:29:02 -0400 Subject: [PATCH 07/17] fmt: collate for/overlay --- crates/squawk_fmt/src/fmt.rs | 78 ++++++++++++++++++- .../squawk_fmt/tests/after/select_expr.snap | 8 ++ .../squawk_fmt/tests/before/select_expr.sql | 8 ++ 3 files changed, 90 insertions(+), 4 deletions(-) diff --git a/crates/squawk_fmt/src/fmt.rs b/crates/squawk_fmt/src/fmt.rs index 19c8f633..daebb942 100644 --- a/crates/squawk_fmt/src/fmt.rs +++ b/crates/squawk_fmt/src/fmt.rs @@ -810,8 +810,8 @@ fn build_call_expr<'a>(call_expr: ast::CallExpr) -> Doc<'a> { any_fn.select_variant(), any_fn.r_paren_token(), ) - } else if let Some(_collation_for_fn) = call_expr.collation_for_fn() { - todo!("collation_for function expressions are not supported yet") + } else if let Some(collation_for_fn) = call_expr.collation_for_fn() { + build_collation_for_fn(collation_for_fn) } else if let Some(exists_fn) = call_expr.exists_fn() { build_parenthesized_expr_or_select_fn( "exists", @@ -844,8 +844,8 @@ fn build_call_expr<'a>(call_expr: ast::CallExpr) -> Doc<'a> { todo!("json_serialize function expressions are not supported yet") } else if let Some(_json_value_fn) = call_expr.json_value_fn() { todo!("json_value function expressions are not supported yet") - } else if let Some(_overlay_fn) = call_expr.overlay_fn() { - todo!("overlay function expressions are not supported yet") + } else if let Some(overlay_fn) = call_expr.overlay_fn() { + build_overlay_fn(overlay_fn) } else if let Some(position_fn) = call_expr.position_fn() { build_position_fn(position_fn) } else if let Some(some_fn) = call_expr.some_fn() { @@ -1371,6 +1371,46 @@ fn build_json_returning_clause<'a>(returning: ast::JsonReturningClause) -> Doc<' doc } +fn build_overlay_fn<'a>(overlay_fn: ast::OverlayFn) -> Doc<'a> { + let mut doc = Doc::text("overlay"); + if let Some(l_paren) = overlay_fn.l_paren_token() { + doc = doc.append(comments_before(l_paren)); + } + doc = doc.append(Doc::text("(")); + + if let Some(args) = overlay_fn.overlay_args() { + doc = doc + .append(leading_comments(args.syntax())) + .append(match args { + ast::OverlayArgs::OverlayPlacing(args) => { + let mut doc = args + .string() + .map(|expr| leading_comments(expr.syntax()).append(build_expr(expr))) + .unwrap_or_else(Doc::nil); + doc = append_keyword_expr(doc, args.placing_token(), "placing", args.placing()); + doc = append_keyword_expr(doc, args.from_token(), "from", args.from()); + append_keyword_expr(doc, args.for_token(), "for", args.for_()) + } + ast::OverlayArgs::OverlayExprs(args) => { + let items = args.overlay_exprs().map(|arg| { + let syntax = arg.syntax().clone(); + let doc = leading_comments(arg.syntax()).append(match arg { + ast::OverlayExpr::Expr(expr) => build_expr(expr), + ast::OverlayExpr::NamedArg(arg) => build_named_call_arg(arg), + }); + (doc, syntax) + }); + build_comma_separated_docs(items).unwrap_or_else(Doc::nil) + } + }); + } + + if let Some(r_paren) = overlay_fn.r_paren_token() { + doc = doc.append(comments_before(r_paren)); + } + doc.append(Doc::text(")")) +} + fn build_substring_fn<'a>(substring_fn: ast::SubstringFn) -> Doc<'a> { let mut doc = Doc::text("substring"); if let Some(l_paren) = substring_fn.l_paren_token() { @@ -1538,6 +1578,36 @@ fn build_position_fn<'a>(position_fn: ast::PositionFn) -> Doc<'a> { doc.append(Doc::text(")")) } +fn build_collation_for_fn<'a>(collation_for_fn: ast::CollationForFn) -> Doc<'a> { + let mut doc = Doc::text("collation"); + if let Some(for_token) = collation_for_fn.for_token() { + doc = doc + .append(Doc::space()) + .append(leading_comments_token(&for_token)) + .append(Doc::text("for")); + } + if let Some(l_paren) = collation_for_fn.l_paren_token() { + if comment_tokens_before(l_paren.clone()).is_empty() { + doc = doc.append(Doc::space()); + } else { + doc = doc.append(comments_before(l_paren)); + } + } else { + doc = doc.append(Doc::space()); + } + doc = doc.append(Doc::text("(")); + + if let Some(expr) = collation_for_fn.expr() { + doc = doc + .append(leading_comments(expr.syntax())) + .append(build_expr(expr)); + } + if let Some(r_paren) = collation_for_fn.r_paren_token() { + doc = doc.append(comments_before(r_paren)); + } + doc.append(Doc::text(")")) +} + fn build_extract_fn<'a>(extract_fn: ast::ExtractFn) -> Doc<'a> { let mut doc = Doc::text("extract"); if let Some(l_paren) = extract_fn.l_paren_token() { diff --git a/crates/squawk_fmt/tests/after/select_expr.snap b/crates/squawk_fmt/tests/after/select_expr.snap index 55dca1ec..0410ceaf 100644 --- a/crates/squawk_fmt/tests/after/select_expr.snap +++ b/crates/squawk_fmt/tests/after/select_expr.snap @@ -58,12 +58,20 @@ select exists(select 1 from things), /* before exists */ exists /* before opening paren */(/* before select */ select 1 /* before closing paren */) /* after exists */, + collation for (b + c), + /* before collation */ collation /* before for */ for /* before opening paren */(/* before expr */ foo /* before closing paren */) /* after collation */, extract(year from timestamp '2001-02-16 20:38:40'), extract('month' from ts), extract("Field" from ts), /* before extract */ extract /* before opening paren */(/* before field */ day /* before from */ from /* before expr */ ts /* before closing paren */) /* after extract */, position('om' in 'Thomas'), /* before position */ position /* before opening paren */(/* before substring */ 'om' /* before in */ in /* before string */ 'Thomas' /* before closing paren */) /* after position */, + overlay('Txxxxas' placing 'hom' from 2 for 4), + overlay('Txxxxas' placing 'hom' from 2), + overlay(), + overlay('Txxxxas', 'hom', 2, count => 4), + /* before overlay */ overlay /* before opening paren */(/* before string */ 'Txxxxas' /* before placing */ placing /* before replacement */ 'hom' /* before from */ from /* before start */ 2 /* before for */ for /* before count */ 4 /* before closing paren */) /* after overlay */, + overlay /* before opening paren */(/* before string */ 'Txxxxas' /* before comma */, /* before replacement */ 'hom' /* before comma */, /* before start */ 2 /* before comma */, /* before name */ count /* before arrow */ => /* before count */ 4 /* before closing paren */), substring('Thomas' from 2 for 3), substring('Thomas' for 3 from 2), substring('Thomas' from 2), diff --git a/crates/squawk_fmt/tests/before/select_expr.sql b/crates/squawk_fmt/tests/before/select_expr.sql index b26c090b..94759d99 100644 --- a/crates/squawk_fmt/tests/before/select_expr.sql +++ b/crates/squawk_fmt/tests/before/select_expr.sql @@ -53,12 +53,20 @@ select 4 /* before op */ = /* before any */ ANY /* before opening paren */ ( /* before expr */ ARRAY [ 4 ] /* before closing paren */ ) /* after any */, EXISTS ( SELECT 1 FROM things ), /* before exists */ EXISTS /* before opening paren */ ( /* before select */ SELECT 1 /* before closing paren */ ) /* after exists */, + COLLATION FOR ( b + c ), + /* before collation */ COLLATION /* before for */ FOR /* before opening paren */ ( /* before expr */ foo /* before closing paren */ ) /* after collation */, EXTRACT ( YEAR FROM TIMESTAMP '2001-02-16 20:38:40' ), EXTRACT ( 'month' FROM ts ), EXTRACT ( "Field" FROM ts ), /* before extract */ EXTRACT /* before opening paren */ ( /* before field */ DAY /* before from */ FROM /* before expr */ ts /* before closing paren */ ) /* after extract */, POSITION ( 'om' IN 'Thomas' ), /* before position */ POSITION /* before opening paren */ ( /* before substring */ 'om' /* before in */ IN /* before string */ 'Thomas' /* before closing paren */ ) /* after position */, + OVERLAY ( 'Txxxxas' PLACING 'hom' FROM 2 FOR 4 ), + OVERLAY ( 'Txxxxas' PLACING 'hom' FROM 2 ), + OVERLAY ( ), + OVERLAY ( 'Txxxxas', 'hom', 2, count => 4 ), + /* before overlay */ OVERLAY /* before opening paren */ ( /* before string */ 'Txxxxas' /* before placing */ PLACING /* before replacement */ 'hom' /* before from */ FROM /* before start */ 2 /* before for */ FOR /* before count */ 4 /* before closing paren */ ) /* after overlay */, + OVERLAY /* before opening paren */ ( /* before string */ 'Txxxxas' /* before comma */, /* before replacement */ 'hom' /* before comma */, /* before start */ 2 /* before comma */, /* before name */ count /* before arrow */ => /* before count */ 4 /* before closing paren */ ), SUBSTRING ( 'Thomas' FROM 2 FOR 3 ), SUBSTRING ( 'Thomas' FOR 3 FROM 2 ), SUBSTRING ( 'Thomas' FROM 2 ), From 24b12ba77ca5daca4c173862fe9d1bbcf5d61c6d Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Sun, 23 Aug 2026 20:39:28 -0400 Subject: [PATCH 08/17] fmt: json_query/json_scalar/json_serialize/json_value --- crates/squawk_fmt/src/fmt.rs | 252 +++++++++++++++++- .../squawk_fmt/tests/after/select_expr.snap | 13 + .../squawk_fmt/tests/before/select_expr.sql | 13 + crates/squawk_syntax/src/ast/node_ext.rs | 24 ++ 4 files changed, 294 insertions(+), 8 deletions(-) diff --git a/crates/squawk_fmt/src/fmt.rs b/crates/squawk_fmt/src/fmt.rs index daebb942..fd0366b7 100644 --- a/crates/squawk_fmt/src/fmt.rs +++ b/crates/squawk_fmt/src/fmt.rs @@ -836,14 +836,14 @@ fn build_call_expr<'a>(call_expr: ast::CallExpr) -> Doc<'a> { build_json_object_agg_fn(json_object_agg_fn) } else if let Some(json_object_fn) = call_expr.json_object_fn() { build_json_object_fn(json_object_fn) - } else if let Some(_json_query_fn) = call_expr.json_query_fn() { - todo!("json_query function expressions are not supported yet") - } else if let Some(_json_scalar_fn) = call_expr.json_scalar_fn() { - todo!("json_scalar function expressions are not supported yet") - } else if let Some(_json_serialize_fn) = call_expr.json_serialize_fn() { - todo!("json_serialize function expressions are not supported yet") - } else if let Some(_json_value_fn) = call_expr.json_value_fn() { - todo!("json_value function expressions are not supported yet") + } else if let Some(json_query_fn) = call_expr.json_query_fn() { + build_json_query_fn(json_query_fn) + } else if let Some(json_scalar_fn) = call_expr.json_scalar_fn() { + build_json_scalar_fn(json_scalar_fn) + } else if let Some(json_serialize_fn) = call_expr.json_serialize_fn() { + build_json_serialize_fn(json_serialize_fn) + } else if let Some(json_value_fn) = call_expr.json_value_fn() { + build_json_value_fn(json_value_fn) } else if let Some(overlay_fn) = call_expr.overlay_fn() { build_overlay_fn(overlay_fn) } else if let Some(position_fn) = call_expr.position_fn() { @@ -1021,6 +1021,174 @@ fn build_json_fn<'a>(json_fn: ast::JsonFn) -> Doc<'a> { doc.append(Doc::text(")")) } +fn build_json_scalar_fn<'a>(json_scalar_fn: ast::JsonScalarFn) -> Doc<'a> { + build_parenthesized_expr_or_select_fn( + "json_scalar", + json_scalar_fn.l_paren_token(), + json_scalar_fn.expr(), + None, + json_scalar_fn.r_paren_token(), + ) +} + +fn build_json_serialize_fn<'a>(json_serialize_fn: ast::JsonSerializeFn) -> Doc<'a> { + let mut doc = Doc::text("json_serialize"); + if let Some(l_paren) = json_serialize_fn.l_paren_token() { + doc = doc.append(comments_before(l_paren)); + } + doc = doc.append(Doc::text("(")); + + if let Some(expr) = json_serialize_fn.expr() { + doc = doc + .append(leading_comments(expr.syntax())) + .append(build_expr(expr)); + } + if let Some(format) = json_serialize_fn.json_format_clause() { + doc = doc + .append(Doc::space()) + .append(leading_comments(format.syntax())) + .append(build_json_format_clause(format)); + } + if let Some(returning) = json_serialize_fn.json_returning_clause() { + doc = doc + .append(Doc::space()) + .append(leading_comments(returning.syntax())) + .append(build_json_returning_clause(returning)); + } + if let Some(r_paren) = json_serialize_fn.r_paren_token() { + doc = doc.append(comments_before(r_paren)); + } + doc.append(Doc::text(")")) +} + +fn build_json_query_fn<'a>(json_query_fn: ast::JsonQueryFn) -> Doc<'a> { + let mut doc = build_json_document_path_fn( + "json_query", + json_query_fn.l_paren_token(), + json_query_fn.document(), + json_query_fn.json_format_clause(), + json_query_fn.comma_token(), + json_query_fn.path(), + ); + if let Some(passing) = json_query_fn.json_passing_clause() { + doc = doc + .append(Doc::space()) + .append(leading_comments(passing.syntax())) + .append(build_json_passing_clause(passing)); + } + if let Some(returning) = json_query_fn.json_returning_clause() { + doc = doc + .append(Doc::space()) + .append(leading_comments(returning.syntax())) + .append(build_json_returning_clause(returning)); + } + if let Some(wrapper) = json_query_fn.json_wrapper_behavior_clause() { + doc = doc + .append(Doc::space()) + .append(leading_comments(wrapper.syntax())) + .append(build_json_wrapper_behavior_clause(wrapper)); + } + if let Some(quotes) = json_query_fn.json_quotes_clause() { + doc = doc + .append(Doc::space()) + .append(leading_comments(quotes.syntax())) + .append(build_json_quotes_clause(quotes)); + } + if let Some(on_empty) = json_query_fn.json_on_empty_clause() { + doc = doc + .append(Doc::space()) + .append(leading_comments(on_empty.syntax())) + .append(build_json_on_empty_clause(on_empty)); + } + if let Some(on_error) = json_query_fn.json_on_error_clause() { + doc = doc + .append(Doc::space()) + .append(leading_comments(on_error.syntax())) + .append(build_json_on_error_clause(on_error)); + } + if let Some(r_paren) = json_query_fn.r_paren_token() { + doc = doc.append(comments_before(r_paren)); + } + doc.append(Doc::text(")")) +} + +fn build_json_value_fn<'a>(json_value_fn: ast::JsonValueFn) -> Doc<'a> { + let mut doc = build_json_document_path_fn( + "json_value", + json_value_fn.l_paren_token(), + json_value_fn.document(), + json_value_fn.json_format_clause(), + json_value_fn.comma_token(), + json_value_fn.path(), + ); + if let Some(passing) = json_value_fn.json_passing_clause() { + doc = doc + .append(Doc::space()) + .append(leading_comments(passing.syntax())) + .append(build_json_passing_clause(passing)); + } + if let Some(returning) = json_value_fn.json_returning_clause() { + doc = doc + .append(Doc::space()) + .append(leading_comments(returning.syntax())) + .append(build_json_returning_clause(returning)); + } + if let Some(on_empty) = json_value_fn.json_on_empty_clause() { + doc = doc + .append(Doc::space()) + .append(leading_comments(on_empty.syntax())) + .append(build_json_on_empty_clause(on_empty)); + } + if let Some(on_error) = json_value_fn.json_on_error_clause() { + doc = doc + .append(Doc::space()) + .append(leading_comments(on_error.syntax())) + .append(build_json_on_error_clause(on_error)); + } + if let Some(r_paren) = json_value_fn.r_paren_token() { + doc = doc.append(comments_before(r_paren)); + } + doc.append(Doc::text(")")) +} + +fn build_json_document_path_fn<'a>( + keyword: &'static str, + l_paren: Option, + document: Option, + format: Option, + comma: Option, + path: Option, +) -> Doc<'a> { + let mut doc = Doc::text(keyword); + if let Some(l_paren) = l_paren { + doc = doc.append(comments_before(l_paren)); + } + doc = doc.append(Doc::text("(")); + if let Some(document) = document { + doc = doc + .append(leading_comments(document.syntax())) + .append(build_expr(document)); + } + if let Some(format) = format { + doc = doc + .append(Doc::space()) + .append(leading_comments(format.syntax())) + .append(build_json_format_clause(format)); + } + if let Some(comma) = comma { + doc = doc + .append(comments_before(comma)) + .append(Doc::text(",")) + .append(Doc::space()); + } + if let Some(path) = path { + doc = doc + .append(leading_comments(path.syntax())) + .append(build_expr(path)); + } + doc +} + fn build_json_exists_fn<'a>(json_exists_fn: ast::JsonExistsFn) -> Doc<'a> { let mut doc = Doc::text("json_exists"); if let Some(l_paren) = json_exists_fn.l_paren_token() { @@ -1107,6 +1275,60 @@ fn build_json_passing_arg<'a>(arg: ast::JsonPassingArg) -> Doc<'a> { doc } +fn build_json_wrapper_behavior_clause<'a>(clause: ast::JsonWrapperBehaviorClause) -> Doc<'a> { + match clause { + ast::JsonWrapperBehaviorClause::JsonWithConditionalWrapper(clause) => { + let mut doc = Doc::text("with"); + doc = append_keyword_token(doc, clause.conditional_token(), "conditional"); + doc = append_keyword_token(doc, clause.array_token(), "array"); + append_keyword_token(doc, clause.wrapper_token(), "wrapper") + } + ast::JsonWrapperBehaviorClause::JsonWithUnconditionalWrapper(clause) => { + let mut doc = Doc::text("with"); + doc = append_keyword_token(doc, clause.unconditional_token(), "unconditional"); + doc = append_keyword_token(doc, clause.array_token(), "array"); + append_keyword_token(doc, clause.wrapper_token(), "wrapper") + } + ast::JsonWrapperBehaviorClause::JsonWithoutWrapper(clause) => { + let mut doc = Doc::text("without"); + doc = append_keyword_token(doc, clause.array_token(), "array"); + append_keyword_token(doc, clause.wrapper_token(), "wrapper") + } + } +} + +fn build_json_quotes_clause<'a>(clause: ast::JsonQuotesClause) -> Doc<'a> { + let mut doc = clause + .quotes_behavior() + .map(|behavior| match behavior { + ast::QuotesBehavior::KeepQuotes(behavior) => { + append_keyword_token(Doc::text("keep"), behavior.quotes_token(), "quotes") + } + ast::QuotesBehavior::OmitQuotes(behavior) => { + append_keyword_token(Doc::text("omit"), behavior.quotes_token(), "quotes") + } + }) + .unwrap_or_else(Doc::nil); + if let Some(on_scalar) = clause.on_scalar_string() { + doc = doc + .append(Doc::space()) + .append(leading_comments(on_scalar.syntax())) + .append(Doc::text("on")); + doc = append_keyword_token(doc, on_scalar.scalar_token(), "scalar"); + doc = append_keyword_token(doc, on_scalar.string_token(), "string"); + } + doc +} + +fn build_json_on_empty_clause<'a>(clause: ast::JsonOnEmptyClause) -> Doc<'a> { + let mut doc = clause + .json_behavior() + .map(build_json_behavior) + .unwrap_or_else(Doc::nil); + doc = append_keyword_token(doc, clause.on_token(), "on"); + append_keyword_token(doc, clause.empty_token(), "empty") +} + fn build_json_on_error_clause<'a>(clause: ast::JsonOnErrorClause) -> Doc<'a> { let mut doc = clause .json_behavior() @@ -1449,6 +1671,20 @@ fn build_substring_fn<'a>(substring_fn: ast::SubstringFn) -> Doc<'a> { doc.append(Doc::text(")")) } +fn append_keyword_token<'a>( + mut doc: Doc<'a>, + token: Option, + keyword: &'static str, +) -> Doc<'a> { + if let Some(token) = token { + doc = doc + .append(Doc::space()) + .append(leading_comments_token(&token)) + .append(Doc::text(keyword)); + } + doc +} + fn append_keyword_expr<'a>( mut doc: Doc<'a>, token: Option, diff --git a/crates/squawk_fmt/tests/after/select_expr.snap b/crates/squawk_fmt/tests/after/select_expr.snap index 0410ceaf..2a2c5b93 100644 --- a/crates/squawk_fmt/tests/after/select_expr.snap +++ b/crates/squawk_fmt/tests/after/select_expr.snap @@ -119,6 +119,19 @@ select json_exists(doc, '$' unknown on error), json_exists(doc, '$' empty array on error), json_exists(doc, '$' empty object on error), + json_scalar(1), + json_scalar(), + json_scalar /* before opening paren */(/* before expression */ 1 /* before closing paren */), + json_serialize(doc), + json_serialize(doc format json returning text format json), + json_serialize /* before opening paren */(/* before document */ doc /* before format */ format /* before json */ json /* before returning */ returning /* before type */ text /* before returning format */ format /* before returning json */ json /* before closing paren */), + json_query(doc, '$.a'), + json_query(doc, '$.a' with wrapper), + json_query(doc, '$.a' with unconditional array wrapper omit quotes), + json_query(doc, '$.a' without array wrapper), + json_query /* before opening paren */(/* before document */ doc /* before format */ format /* before json */ json /* before encoding */ encoding /* before encoding name */ utf8 /* before comma */, /* before path */ '$.a' /* before passing */ passing /* before arg */ x /* before as */ as /* before name */ foo /* before returning */ returning /* before type */ text /* before returning format */ format /* before returning json */ json /* before wrapper */ with /* before conditional */ conditional /* before array */ array /* before wrapper keyword */ wrapper /* before keep */ keep /* before quotes */ quotes /* before on scalar */ on /* before scalar */ scalar /* before string */ string /* before empty behavior */ default /* before default */ 'empty' /* before empty on */ on /* before empty */ empty /* before error behavior */ empty /* before object */ object /* before error on */ on /* before error */ error /* before closing paren */), + json_value(doc, '$.a'), + json_value /* before opening paren */(/* before document */ doc /* before format */ format /* before json */ json /* before comma */, /* before path */ '$.a' /* before passing */ passing /* before arg */ x /* before as */ as /* before name */ foo /* before returning */ returning /* before type */ text /* before empty behavior */ null /* before empty on */ on /* before empty */ empty /* before error behavior */ error /* before error on */ on /* before error */ error /* before closing paren */), json(doc), json(doc format json encoding utf8 with unique keys), json(doc without unique keys), diff --git a/crates/squawk_fmt/tests/before/select_expr.sql b/crates/squawk_fmt/tests/before/select_expr.sql index 94759d99..0df58b83 100644 --- a/crates/squawk_fmt/tests/before/select_expr.sql +++ b/crates/squawk_fmt/tests/before/select_expr.sql @@ -113,6 +113,19 @@ select JSON_EXISTS(doc, '$' UNKNOWN ON ERROR), JSON_EXISTS(doc, '$' EMPTY ARRAY ON ERROR), JSON_EXISTS(doc, '$' EMPTY OBJECT ON ERROR), + JSON_SCALAR(1), + JSON_SCALAR(), + JSON_SCALAR /* before opening paren */ (/* before expression */ 1 /* before closing paren */), + JSON_SERIALIZE(doc), + JSON_SERIALIZE(doc FORMAT JSON RETURNING text FORMAT JSON), + JSON_SERIALIZE /* before opening paren */ (/* before document */ doc /* before format */ FORMAT /* before json */ JSON /* before returning */ RETURNING /* before type */ text /* before returning format */ FORMAT /* before returning json */ JSON /* before closing paren */), + JSON_QUERY(doc, '$.a'), + JSON_QUERY(doc, '$.a' WITH WRAPPER), + JSON_QUERY(doc, '$.a' WITH UNCONDITIONAL ARRAY WRAPPER OMIT QUOTES), + JSON_QUERY(doc, '$.a' WITHOUT ARRAY WRAPPER), + JSON_QUERY /* before opening paren */ (/* before document */ doc /* before format */ FORMAT /* before json */ JSON /* before encoding */ ENCODING /* before encoding name */ UTF8 /* before comma */, /* before path */ '$.a' /* before passing */ PASSING /* before arg */ x /* before as */ AS /* before name */ foo /* before returning */ RETURNING /* before type */ text /* before returning format */ FORMAT /* before returning json */ JSON /* before wrapper */ WITH /* before conditional */ CONDITIONAL /* before array */ ARRAY /* before wrapper keyword */ WRAPPER /* before keep */ KEEP /* before quotes */ QUOTES /* before on scalar */ ON /* before scalar */ SCALAR /* before string */ STRING /* before empty behavior */ DEFAULT /* before default */ 'empty' /* before empty on */ ON /* before empty */ EMPTY /* before error behavior */ EMPTY /* before object */ OBJECT /* before error on */ ON /* before error */ ERROR /* before closing paren */), + JSON_VALUE(doc, '$.a'), + JSON_VALUE /* before opening paren */ (/* before document */ doc /* before format */ FORMAT /* before json */ JSON /* before comma */, /* before path */ '$.a' /* before passing */ PASSING /* before arg */ x /* before as */ AS /* before name */ foo /* before returning */ RETURNING /* before type */ text /* before empty behavior */ NULL /* before empty on */ ON /* before empty */ EMPTY /* before error behavior */ ERROR /* before error on */ ON /* before error */ ERROR /* before closing paren */), JSON(doc), JSON(doc FORMAT JSON ENCODING UTF8 WITH UNIQUE KEYS), JSON(doc WITHOUT UNIQUE KEYS), diff --git a/crates/squawk_syntax/src/ast/node_ext.rs b/crates/squawk_syntax/src/ast/node_ext.rs index e24fc575..6e2558d0 100644 --- a/crates/squawk_syntax/src/ast/node_ext.rs +++ b/crates/squawk_syntax/src/ast/node_ext.rs @@ -869,6 +869,30 @@ impl ast::JsonExistsFn { } } +impl ast::JsonQueryFn { + #[inline] + pub fn document(&self) -> Option { + support::children(self.syntax()).next() + } + + #[inline] + pub fn path(&self) -> Option { + support::children(self.syntax()).nth(1) + } +} + +impl ast::JsonValueFn { + #[inline] + pub fn document(&self) -> Option { + support::children(self.syntax()).next() + } + + #[inline] + pub fn path(&self) -> Option { + support::children(self.syntax()).nth(1) + } +} + impl ast::PositionFn { #[inline] pub fn pos(&self) -> Option { From 9ff182d817a998f9a4aa37165c24909df882b0ac Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Sun, 23 Aug 2026 21:10:49 -0400 Subject: [PATCH 09/17] fmt: xml functions --- crates/squawk_fmt/src/fmt.rs | 508 +++++++++++++++++- .../squawk_fmt/tests/after/xml_functions.snap | 31 ++ .../squawk_fmt/tests/before/xml_functions.sql | 27 + 3 files changed, 552 insertions(+), 14 deletions(-) create mode 100644 crates/squawk_fmt/tests/after/xml_functions.snap create mode 100644 crates/squawk_fmt/tests/before/xml_functions.sql diff --git a/crates/squawk_fmt/src/fmt.rs b/crates/squawk_fmt/src/fmt.rs index fd0366b7..bcc4b2de 100644 --- a/crates/squawk_fmt/src/fmt.rs +++ b/crates/squawk_fmt/src/fmt.rs @@ -860,25 +860,505 @@ fn build_call_expr<'a>(call_expr: ast::CallExpr) -> Doc<'a> { build_substring_fn(substring_fn) } else if let Some(trim_fn) = call_expr.trim_fn() { build_trim_fn(trim_fn) - } else if let Some(_xml_element_fn) = call_expr.xml_element_fn() { - todo!("xmlelement function expressions are not supported yet") - } else if let Some(_xml_exists_fn) = call_expr.xml_exists_fn() { - todo!("xmlexists function expressions are not supported yet") - } else if let Some(_xml_forest_fn) = call_expr.xml_forest_fn() { - todo!("xmlforest function expressions are not supported yet") - } else if let Some(_xml_parse_fn) = call_expr.xml_parse_fn() { - todo!("xmlparse function expressions are not supported yet") - } else if let Some(_xml_pi_fn) = call_expr.xml_pi_fn() { - todo!("xmlpi function expressions are not supported yet") - } else if let Some(_xml_root_fn) = call_expr.xml_root_fn() { - todo!("xmlroot function expressions are not supported yet") - } else if let Some(_xml_serialize_fn) = call_expr.xml_serialize_fn() { - todo!("xmlserialize function expressions are not supported yet") + } else if let Some(xml_element_fn) = call_expr.xml_element_fn() { + build_xml_element_fn(xml_element_fn) + } else if let Some(xml_exists_fn) = call_expr.xml_exists_fn() { + build_xml_exists_fn(xml_exists_fn) + } else if let Some(xml_forest_fn) = call_expr.xml_forest_fn() { + build_xml_forest_fn(xml_forest_fn) + } else if let Some(xml_parse_fn) = call_expr.xml_parse_fn() { + build_xml_parse_fn(xml_parse_fn) + } else if let Some(xml_pi_fn) = call_expr.xml_pi_fn() { + build_xml_pi_fn(xml_pi_fn) + } else if let Some(xml_root_fn) = call_expr.xml_root_fn() { + build_xml_root_fn(xml_root_fn) + } else if let Some(xml_serialize_fn) = call_expr.xml_serialize_fn() { + build_xml_serialize_fn(xml_serialize_fn) } else { unreachable!("a call expression should contain a supported function node") } } +fn build_xml_element_fn<'a>(xml_element_fn: ast::XmlElementFn) -> Doc<'a> { + let mut doc = Doc::text("xmlelement"); + if let Some(l_paren) = xml_element_fn.l_paren_token() { + doc = doc.append(comments_before(l_paren)); + } + doc = doc.append(Doc::text("(")); + + if let Some(name) = xml_element_fn.name_token() { + doc = doc + .append(leading_comments_token(&name)) + .append(Doc::text("name")); + } + + let Some(tag) = xml_element_fn.tag() else { + return doc.append(Doc::text(")")); + }; + doc = doc + .append(Doc::space()) + .append(leading_comments(tag.syntax())) + .append(build_name(tag.syntax())); + + let mut items = Vec::new(); + if let Some(attrs) = xml_element_fn.expr_as_xml_attr_list() { + let attrs_doc = xml_element_fn + .xmlattributes_token() + .map(|token| { + leading_comments_token(&token) + .append(Doc::text("xmlattributes")) + .append(comments_before(attrs.syntax().clone())) + }) + .unwrap_or_else(Doc::nil) + .append(build_expr_as_xml_attr_list(attrs.clone())); + items.push((attrs_doc, attrs.syntax().clone())); + } + items.extend(xml_element_fn.exprs().map(|expr| { + ( + leading_comments(expr.syntax()).append(build_expr(expr.clone())), + expr.syntax().clone(), + ) + })); + + let mut previous = tag.syntax().clone(); + for (item, syntax) in items { + doc = doc + .append(trailing_comments(&previous)) + .append(Doc::text(",")) + .append(Doc::space()) + .append(item); + previous = syntax; + } + + if let Some(r_paren) = xml_element_fn.r_paren_token() { + doc = doc.append(comments_before(r_paren)); + } + doc.append(Doc::text(")")) +} + +fn build_expr_as_xml_attr_list<'a>(attrs: ast::ExprAsXmlAttrList) -> Doc<'a> { + let mut doc = Doc::nil(); + if let Some(l_paren) = attrs.l_paren_token() { + doc = doc.append(comments_before(l_paren)); + } + doc = doc.append(Doc::text("(")); + + let items = attrs.expr_as_xml_attrs().map(|attr| { + let mut item = attr.expr().map(build_expr).unwrap_or_else(Doc::nil); + if let Some(as_token) = attr.as_token() { + item = item + .append(Doc::space()) + .append(leading_comments_token(&as_token)) + .append(Doc::text("as")); + } + if let Some(name) = attr.attr() { + item = item + .append(Doc::space()) + .append(leading_comments(name.syntax())) + .append(build_name(name.syntax())); + } + ( + leading_comments(attr.syntax()).append(item), + attr.syntax().clone(), + ) + }); + if let Some(items) = build_comma_separated_docs(items) { + doc = doc.append(items); + } + + if let Some(r_paren) = attrs.r_paren_token() { + doc = doc.append(comments_before(r_paren)); + } + doc.append(Doc::text(")")) +} + +fn build_xml_exists_fn<'a>(xml_exists_fn: ast::XmlExistsFn) -> Doc<'a> { + let mut doc = Doc::text("xmlexists"); + if let Some(l_paren) = xml_exists_fn.l_paren_token() { + doc = doc.append(comments_before(l_paren)); + } + doc = doc.append(Doc::text("(")); + + if let Some(passing) = xml_exists_fn.xml_row_passing_clause() { + if let Some(row) = passing.row() { + doc = doc + .append(leading_comments(passing.syntax())) + .append(build_expr(row)); + } + if let Some(passing_token) = passing.passing_token() { + doc = doc + .append(Doc::space()) + .append(leading_comments_token(&passing_token)) + .append(Doc::text("passing")); + } + if let Some(mech) = passing.xml_passing_mech() { + doc = doc + .append(Doc::space()) + .append(leading_comments(mech.syntax())) + .append(build_xml_passing_mech(mech)); + } + if let Some(passing_doc) = passing.xml_passing_doc() { + if let Some(expr) = passing_doc.expr() { + doc = doc + .append(Doc::space()) + .append(leading_comments(passing_doc.syntax())) + .append(build_expr(expr)); + } + if let Some(mech) = passing_doc.xml_passing_mech() { + doc = doc + .append(Doc::space()) + .append(leading_comments(mech.syntax())) + .append(build_xml_passing_mech(mech)); + } + } + } + + if let Some(r_paren) = xml_exists_fn.r_paren_token() { + doc = doc.append(comments_before(r_paren)); + } + doc.append(Doc::text(")")) +} + +fn build_xml_forest_fn<'a>(xml_forest_fn: ast::XmlForestFn) -> Doc<'a> { + Doc::text("xmlforest").append( + xml_forest_fn + .expr_as_element_tag_list() + .map(|list| { + comments_before(list.syntax().clone()).append(build_expr_as_element_tag_list(list)) + }) + .unwrap_or_else(Doc::nil), + ) +} + +fn build_expr_as_element_tag_list<'a>(list: ast::ExprAsElementTagList) -> Doc<'a> { + let mut doc = Doc::nil(); + if let Some(l_paren) = list.l_paren_token() { + doc = doc.append(comments_before(l_paren)); + } + doc = doc.append(Doc::text("(")); + + let items = list.expr_as_element_tags().map(|item| { + let mut item_doc = item.expr().map(build_expr).unwrap_or_else(Doc::nil); + if let Some(as_token) = item.as_token() { + item_doc = item_doc + .append(Doc::space()) + .append(leading_comments_token(&as_token)) + .append(Doc::text("as")); + } + if let Some(tag) = item.tag() { + item_doc = item_doc + .append(Doc::space()) + .append(leading_comments(tag.syntax())) + .append(build_name(tag.syntax())); + } + ( + leading_comments(item.syntax()).append(item_doc), + item.syntax().clone(), + ) + }); + if let Some(items) = build_comma_separated_docs(items) { + doc = doc.append(items); + } + + if let Some(r_paren) = list.r_paren_token() { + doc = doc.append(comments_before(r_paren)); + } + doc.append(Doc::text(")")) +} + +fn build_xml_parse_fn<'a>(xml_parse_fn: ast::XmlParseFn) -> Doc<'a> { + let mut doc = Doc::text("xmlparse"); + if let Some(l_paren) = xml_parse_fn.l_paren_token() { + doc = doc.append(comments_before(l_paren)); + } + doc = doc.append(Doc::text("(")); + + if let Some(kind) = xml_parse_fn.xml_document_or_content() { + doc = doc + .append(leading_comments(kind.syntax())) + .append(build_xml_document_or_content(kind)); + } + if let Some(expr) = xml_parse_fn.expr() { + doc = doc + .append(Doc::space()) + .append(leading_comments(expr.syntax())) + .append(build_expr(expr)); + } + if let Some(whitespace) = xml_parse_fn.xml_whitespace() { + doc = doc + .append(Doc::space()) + .append(leading_comments(whitespace.syntax())) + .append(build_xml_whitespace(whitespace)); + } + + if let Some(r_paren) = xml_parse_fn.r_paren_token() { + doc = doc.append(comments_before(r_paren)); + } + doc.append(Doc::text(")")) +} + +fn build_xml_pi_fn<'a>(xml_pi_fn: ast::XmlPiFn) -> Doc<'a> { + let mut doc = Doc::text("xmlpi"); + if let Some(l_paren) = xml_pi_fn.l_paren_token() { + doc = doc.append(comments_before(l_paren)); + } + doc = doc.append(Doc::text("(")); + + if let Some(name) = xml_pi_fn.name_token() { + doc = doc + .append(leading_comments_token(&name)) + .append(Doc::text("name")); + } + if let Some(target) = xml_pi_fn.target() { + doc = doc + .append(Doc::space()) + .append(leading_comments(target.syntax())) + .append(build_name(target.syntax())); + } + if let Some(expr) = xml_pi_fn.expr() { + if let Some(comma) = xml_pi_fn.comma_token() { + doc = doc.append(comments_before(comma)); + } + doc = doc + .append(Doc::text(",")) + .append(Doc::space()) + .append(leading_comments(expr.syntax())) + .append(build_expr(expr)); + } + + if let Some(r_paren) = xml_pi_fn.r_paren_token() { + doc = doc.append(comments_before(r_paren)); + } + doc.append(Doc::text(")")) +} + +fn build_xml_root_fn<'a>(xml_root_fn: ast::XmlRootFn) -> Doc<'a> { + let mut doc = Doc::text("xmlroot"); + if let Some(l_paren) = xml_root_fn.l_paren_token() { + doc = doc.append(comments_before(l_paren)); + } + doc = doc.append(Doc::text("(")); + + if let Some(expr) = xml_root_fn.expr() { + doc = doc + .append(leading_comments(expr.syntax())) + .append(build_expr(expr)); + } + if let Some(comma) = xml_root_fn.comma_token() { + doc = doc.append(comments_before(comma)); + } + doc = doc.append(Doc::text(",")).append(Doc::space()); + if let Some(version) = xml_root_fn.xml_root_version() { + doc = doc + .append(leading_comments(version.syntax())) + .append(build_xml_root_version(version)); + } + if let Some(standalone) = xml_root_fn.xml_standalone() { + doc = doc + .append(leading_comments(standalone.syntax())) + .append(build_xml_standalone(standalone)); + } + + if let Some(r_paren) = xml_root_fn.r_paren_token() { + doc = doc.append(comments_before(r_paren)); + } + doc.append(Doc::text(")")) +} + +fn build_xml_root_version<'a>(version: ast::XmlRootVersion) -> Doc<'a> { + match version { + ast::XmlRootVersion::XmlVersionExpr(version) => { + let mut doc = version + .version_token() + .map(|token| leading_comments_token(&token).append(Doc::text("version"))) + .unwrap_or_else(Doc::nil); + if let Some(expr) = version.expr() { + doc = doc + .append(Doc::space()) + .append(leading_comments(expr.syntax())) + .append(build_expr(expr)); + } + doc + } + ast::XmlRootVersion::XmlVersionNoValue(version) => { + let mut doc = version + .version_token() + .map(|token| leading_comments_token(&token).append(Doc::text("version"))) + .unwrap_or_else(Doc::nil); + if let Some(no) = version.no_token() { + doc = doc + .append(Doc::space()) + .append(leading_comments_token(&no)) + .append(Doc::text("no")); + } + if let Some(value) = version.value_token() { + doc = doc + .append(Doc::space()) + .append(leading_comments_token(&value)) + .append(Doc::text("value")); + } + doc + } + } +} + +fn build_xml_standalone<'a>(standalone: ast::XmlStandalone) -> Doc<'a> { + let (comma, standalone_token, no_or_yes, value, text) = match standalone { + ast::XmlStandalone::StandaloneYes(node) => ( + node.comma_token(), + node.standalone_token(), + node.yes_token(), + None, + "yes", + ), + ast::XmlStandalone::StandaloneNo(node) => ( + node.comma_token(), + node.standalone_token(), + node.no_token(), + None, + "no", + ), + ast::XmlStandalone::StandaloneNoValue(node) => ( + node.comma_token(), + node.standalone_token(), + node.no_token(), + node.value_token(), + "no", + ), + }; + + let mut doc = comma.map(comments_before).unwrap_or_else(Doc::nil); + doc = doc.append(Doc::text(",")).append(Doc::space()); + if let Some(token) = standalone_token { + doc = doc + .append(leading_comments_token(&token)) + .append(Doc::text("standalone")); + } + if let Some(token) = no_or_yes { + doc = doc + .append(Doc::space()) + .append(leading_comments_token(&token)) + .append(Doc::text(text)); + } + if let Some(token) = value { + doc = doc + .append(Doc::space()) + .append(leading_comments_token(&token)) + .append(Doc::text("value")); + } + doc +} + +fn build_xml_serialize_fn<'a>(xml_serialize_fn: ast::XmlSerializeFn) -> Doc<'a> { + let mut doc = Doc::text("xmlserialize"); + if let Some(l_paren) = xml_serialize_fn.l_paren_token() { + doc = doc.append(comments_before(l_paren)); + } + doc = doc.append(Doc::text("(")); + + if let Some(kind) = xml_serialize_fn.xml_document_or_content() { + doc = doc + .append(leading_comments(kind.syntax())) + .append(build_xml_document_or_content(kind)); + } + if let Some(expr) = xml_serialize_fn.expr() { + doc = doc + .append(Doc::space()) + .append(leading_comments(expr.syntax())) + .append(build_expr(expr)); + } + if let Some(as_token) = xml_serialize_fn.as_token() { + doc = doc + .append(Doc::space()) + .append(leading_comments_token(&as_token)) + .append(Doc::text("as")); + } + if let Some(ty) = xml_serialize_fn.ty() { + doc = doc + .append(Doc::space()) + .append(leading_comments(ty.syntax())) + .append(build_type(ty)); + } + if let Some(indent) = xml_serialize_fn.xml_indent() { + doc = doc + .append(Doc::space()) + .append(leading_comments(indent.syntax())) + .append(build_xml_indent(indent)); + } + + if let Some(r_paren) = xml_serialize_fn.r_paren_token() { + doc = doc.append(comments_before(r_paren)); + } + doc.append(Doc::text(")")) +} + +fn build_xml_document_or_content<'a>(kind: ast::XmlDocumentOrContent) -> Doc<'a> { + match kind { + ast::XmlDocumentOrContent::XmlDocument(_) => Doc::text("document"), + ast::XmlDocumentOrContent::XmlContent(_) => Doc::text("content"), + } +} + +fn build_xml_whitespace<'a>(whitespace: ast::XmlWhitespace) -> Doc<'a> { + let (first, second, text) = match whitespace { + ast::XmlWhitespace::PreserveWhitespace(node) => { + (node.preserve_token(), node.whitespace_token(), "preserve") + } + ast::XmlWhitespace::StripWhitespace(node) => { + (node.strip_token(), node.whitespace_token(), "strip") + } + }; + build_two_keywords(first, text, second, "whitespace") +} + +fn build_xml_indent<'a>(indent: ast::XmlIndent) -> Doc<'a> { + match indent { + ast::XmlIndent::Indent(_) => Doc::text("indent"), + ast::XmlIndent::NoIndent(node) => { + build_two_keywords(node.no_token(), "no", node.indent_token(), "indent") + } + } +} + +fn build_two_keywords<'a>( + first: Option, + first_text: &'static str, + second: Option, + second_text: &'static str, +) -> Doc<'a> { + let mut doc = first + .map(|token| leading_comments_token(&token).append(Doc::text(first_text))) + .unwrap_or_else(Doc::nil); + if let Some(token) = second { + doc = doc + .append(Doc::space()) + .append(leading_comments_token(&token)) + .append(Doc::text(second_text)); + } + doc +} + +fn build_xml_passing_mech<'a>(mech: ast::XmlPassingMech) -> Doc<'a> { + let (by, end, text) = match mech { + ast::XmlPassingMech::XmlPassingMechByRef(mech) => { + (mech.by_token(), mech.ref_token(), "ref") + } + ast::XmlPassingMech::XmlPassingMechByValue(mech) => { + (mech.by_token(), mech.value_token(), "value") + } + }; + let mut doc = by + .map(|token| leading_comments_token(&token).append(Doc::text("by"))) + .unwrap_or_else(Doc::nil); + if let Some(end) = end { + doc = doc + .append(Doc::space()) + .append(leading_comments_token(&end)) + .append(Doc::text(text)); + } + doc +} + fn build_json_object_fn<'a>(json_object_fn: ast::JsonObjectFn) -> Doc<'a> { let mut doc = Doc::text("json_object"); if let Some(l_paren) = json_object_fn.l_paren_token() { diff --git a/crates/squawk_fmt/tests/after/xml_functions.snap b/crates/squawk_fmt/tests/after/xml_functions.snap new file mode 100644 index 00000000..5908b42a --- /dev/null +++ b/crates/squawk_fmt/tests/after/xml_functions.snap @@ -0,0 +1,31 @@ +--- +source: crates/squawk_fmt/tests/tests.rs +input_file: crates/squawk_fmt/tests/before/xml_functions.sql +--- +select + xmlelement(name foo), + xmlelement(name foo, 1, 2), + xmlelement(name foo, xmlattributes(a, b as c)), + xmlelement(name foo, xmlattributes(a as attr), x, y), + /* before element */ xmlelement /* before outer opening paren */(/* before name */ name /* before tag */ tag /* before attributes comma */, /* before xmlattributes */ xmlattributes /* before attributes opening paren */(/* before first attribute */ a /* before as */ as /* before attribute name */ attr /* before attribute comma */, /* before second attribute */ b /* before attributes closing paren */) /* before content comma */, /* before content */ x /* before outer closing paren */) /* after element */, + xmlexists('/foo' passing doc), + xmlexists('/foo' passing by ref doc), + xmlexists('/foo' passing doc by value), + /* before exists */ xmlexists /* before opening paren */(/* before row */ '/foo' /* before passing */ passing /* before first by */ by /* before ref */ ref /* before document */ doc /* before second by */ by /* before value */ value /* before closing paren */) /* after exists */, + xmlforest(a, b as foo), + /* before forest */ xmlforest /* before opening paren */(/* before first expression */ a /* before as */ as /* before tag */ first /* before comma */, /* before second expression */ b /* before closing paren */) /* after forest */, + xmlparse(document '' preserve whitespace), + xmlparse(content value strip whitespace), + /* before parse */ xmlparse /* before opening paren */(/* before kind */ document /* before expression */ value /* before preserve */ preserve /* before whitespace */ whitespace /* before closing paren */) /* after parse */, + xmlpi(name php), + xmlpi(name php, 'echo'), + /* before pi */ xmlpi /* before opening paren */(/* before name */ name /* before target */ php /* before comma */, /* before expression */ 'echo' /* before closing paren */) /* after pi */, + xmlroot(doc, version '1.0'), + xmlroot(doc, version no value, standalone yes), + xmlroot(doc, version '1.0', standalone no), + xmlroot(doc, version '1.0', standalone no value), + /* before root */ xmlroot /* before opening paren */(/* before expression */ doc /* before version comma */, /* before version */ version /* before no */ no /* before version value */ value/* before standalone comma */ , /* before standalone */ standalone /* before standalone no */ no /* before standalone value */ value /* before closing paren */) /* after root */, + xmlserialize(document doc as text), + xmlserialize(content doc as varchar(20) indent), + xmlserialize(content doc as text no indent), + /* before serialize */ xmlserialize /* before opening paren */(/* before kind */ content /* before expression */ doc /* before as */ as /* before type */ text /* before no */ no /* before indent */ indent /* before closing paren */)/* after serialize */; diff --git a/crates/squawk_fmt/tests/before/xml_functions.sql b/crates/squawk_fmt/tests/before/xml_functions.sql new file mode 100644 index 00000000..e2ae99ba --- /dev/null +++ b/crates/squawk_fmt/tests/before/xml_functions.sql @@ -0,0 +1,27 @@ +select + XMLELEMENT(NAME foo), + XMLELEMENT(NAME foo, 1, 2), + XMLELEMENT(NAME foo, XMLATTRIBUTES(a, b AS c)), + XMLELEMENT(NAME foo, XMLATTRIBUTES(a AS attr), x, y), + /* before element */ XMLELEMENT /* before outer opening paren */ ( /* before name */ NAME /* before tag */ "tag" /* before attributes comma */, /* before xmlattributes */ XMLATTRIBUTES /* before attributes opening paren */ ( /* before first attribute */ a /* before as */ AS /* before attribute name */ "attr" /* before attribute comma */, /* before second attribute */ b /* before attributes closing paren */ ) /* before content comma */, /* before content */ x /* before outer closing paren */ ) /* after element */, + XMLEXISTS('/foo' PASSING doc), + XMLEXISTS('/foo' PASSING BY REF doc), + XMLEXISTS('/foo' PASSING doc BY VALUE), + /* before exists */ XMLEXISTS /* before opening paren */ ( /* before row */ '/foo' /* before passing */ PASSING /* before first by */ BY /* before ref */ REF /* before document */ doc /* before second by */ BY /* before value */ VALUE /* before closing paren */ ) /* after exists */, + XMLFOREST(a, b AS foo), + /* before forest */ XMLFOREST /* before opening paren */ ( /* before first expression */ a /* before as */ AS /* before tag */ "first" /* before comma */, /* before second expression */ b /* before closing paren */ ) /* after forest */, + XMLPARSE(DOCUMENT '' PRESERVE WHITESPACE), + XMLPARSE(CONTENT value STRIP WHITESPACE), + /* before parse */ XMLPARSE /* before opening paren */ ( /* before kind */ DOCUMENT /* before expression */ value /* before preserve */ PRESERVE /* before whitespace */ WHITESPACE /* before closing paren */ ) /* after parse */, + XMLPI(NAME php), + XMLPI(NAME php, 'echo'), + /* before pi */ XMLPI /* before opening paren */ ( /* before name */ NAME /* before target */ php /* before comma */, /* before expression */ 'echo' /* before closing paren */ ) /* after pi */, + XMLROOT(doc, VERSION '1.0'), + XMLROOT(doc, VERSION NO VALUE, STANDALONE YES), + XMLROOT(doc, VERSION '1.0', STANDALONE NO), + XMLROOT(doc, VERSION '1.0', STANDALONE NO VALUE), + /* before root */ XMLROOT /* before opening paren */ ( /* before expression */ doc /* before version comma */, /* before version */ VERSION /* before no */ NO /* before version value */ VALUE /* before standalone comma */, /* before standalone */ STANDALONE /* before standalone no */ NO /* before standalone value */ VALUE /* before closing paren */ ) /* after root */, + XMLSERIALIZE(DOCUMENT doc AS text), + XMLSERIALIZE(CONTENT doc AS varchar(20) INDENT), + XMLSERIALIZE(CONTENT doc AS text NO INDENT), + /* before serialize */ XMLSERIALIZE /* before opening paren */ ( /* before kind */ CONTENT /* before expression */ doc /* before as */ AS /* before type */ text /* before no */ NO /* before indent */ INDENT /* before closing paren */ ) /* after serialize */; From 4c48e20839ebc6859e7a987fca8db02f238aea3e Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Sun, 23 Aug 2026 21:52:32 -0400 Subject: [PATCH 10/17] fmt: graph_table --- crates/squawk_fmt/src/fmt.rs | 355 +++++++++++++++++- .../squawk_fmt/tests/after/graph_table.snap | 21 ++ .../squawk_fmt/tests/before/graph_table.sql | 27 ++ crates/squawk_syntax/src/ast/node_ext.rs | 25 ++ 4 files changed, 423 insertions(+), 5 deletions(-) create mode 100644 crates/squawk_fmt/tests/after/graph_table.snap create mode 100644 crates/squawk_fmt/tests/before/graph_table.sql diff --git a/crates/squawk_fmt/src/fmt.rs b/crates/squawk_fmt/src/fmt.rs index bcc4b2de..670f100e 100644 --- a/crates/squawk_fmt/src/fmt.rs +++ b/crates/squawk_fmt/src/fmt.rs @@ -305,9 +305,7 @@ fn build_from_item<'a>(item: ast::FromItem) -> Doc<'a> { todo!("parenthesized from items are not supported yet") } ast::FromItem::RowsFromItem(_) => todo!("rows from items are not supported yet"), - ast::FromItem::GraphTableFromItem(_) => { - todo!("graph_table from items are not supported yet") - } + ast::FromItem::GraphTableFromItem(graph_table) => build_graph_table_from_item(graph_table), ast::FromItem::JsonTableFromItem(_) => { todo!("json_table from items are not supported yet") } @@ -317,6 +315,22 @@ fn build_from_item<'a>(item: ast::FromItem) -> Doc<'a> { } } +fn build_graph_table_from_item<'a>(item: ast::GraphTableFromItem) -> Doc<'a> { + let mut doc = Doc::nil(); + if let Some(lateral) = item.lateral_token() { + doc = doc + .append(leading_comments_token(&lateral)) + .append(Doc::text("lateral")) + .append(Doc::space()); + } + if let Some(graph_table) = item.graph_table_fn() { + doc = doc + .append(leading_comments(graph_table.syntax())) + .append(build_graph_table_fn(graph_table)); + } + doc.append(build_from_alias(item.alias())) +} + fn build_relation_from_item<'a>(relation: ast::RelationFromItem) -> Doc<'a> { let mut doc = if relation.only_token().is_some() { Doc::text("only").append(Doc::space()) @@ -822,8 +836,8 @@ fn build_call_expr<'a>(call_expr: ast::CallExpr) -> Doc<'a> { ) } else if let Some(extract_fn) = call_expr.extract_fn() { build_extract_fn(extract_fn) - } else if let Some(_graph_table_fn) = call_expr.graph_table_fn() { - todo!("graph_table function expressions are not supported yet") + } else if let Some(graph_table_fn) = call_expr.graph_table_fn() { + build_graph_table_fn(graph_table_fn) } else if let Some(json_array_agg_fn) = call_expr.json_array_agg_fn() { build_json_array_agg_fn(json_array_agg_fn) } else if let Some(json_array_fn) = call_expr.json_array_fn() { @@ -879,6 +893,337 @@ fn build_call_expr<'a>(call_expr: ast::CallExpr) -> Doc<'a> { } } +fn build_graph_table_fn<'a>(graph_table_fn: ast::GraphTableFn) -> Doc<'a> { + let mut doc = Doc::text("graph_table"); + if let Some(l_paren) = graph_table_fn.l_paren_token() { + doc = doc.append(comments_before(l_paren)); + } + doc = doc.append(Doc::text("(")); + + if let Some(graph) = graph_table_fn.property_graph_ref() { + if let Some(path) = graph.path_ref() { + doc = doc + .append(leading_comments(graph.syntax())) + .append(build_path_ref(&path)); + } + } + if let Some(match_token) = graph_table_fn.match_token() { + doc = doc + .append(Doc::space()) + .append(leading_comments_token(&match_token)) + .append(Doc::text("match")); + } + if let Some(patterns) = graph_table_fn.path_pattern_list() { + doc = doc + .append(Doc::space()) + .append(leading_comments(patterns.syntax())) + .append(build_path_pattern_list(patterns)); + } + if let Some(where_clause) = graph_table_fn.where_clause() { + doc = doc + .append(Doc::space()) + .append(leading_comments(where_clause.syntax())) + .append(build_where_clause(where_clause)); + } + if let Some(columns) = graph_table_fn.columns_token() { + doc = doc + .append(Doc::space()) + .append(leading_comments_token(&columns)) + .append(Doc::text("columns")); + } + if let Some(columns) = graph_table_fn.expr_as_column_name_list() { + if comment_tokens_before(columns.syntax().clone()).is_empty() { + doc = doc.append(Doc::space()); + } else { + doc = doc.append(comments_before(columns.syntax().clone())); + } + doc = doc.append(build_expr_as_column_name_list(columns)); + } + + if let Some(r_paren) = graph_table_fn.r_paren_token() { + doc = doc.append(comments_before(r_paren)); + } + doc.append(Doc::text(")")) +} + +fn build_path_pattern_list<'a>(patterns: ast::PathPatternList) -> Doc<'a> { + let items = patterns.path_patterns().map(|pattern| { + ( + leading_comments(pattern.syntax()).append(build_path_pattern(pattern.clone())), + pattern.syntax().clone(), + ) + }); + build_comma_separated_docs(items).unwrap_or_else(Doc::nil) +} + +fn build_path_pattern<'a>(pattern: ast::PathPattern) -> Doc<'a> { + Doc::list( + pattern + .path_factors() + .map(|factor| leading_comments(factor.syntax()).append(build_path_factor(factor))) + .collect(), + ) +} + +fn build_path_factor<'a>(factor: ast::PathFactor) -> Doc<'a> { + let mut doc = factor + .path_primary() + .map(build_path_primary) + .unwrap_or_else(Doc::nil); + if let Some(qualifier) = factor.graph_pattern_qualifier() { + doc = doc + .append(leading_comments(qualifier.syntax())) + .append(build_graph_pattern_qualifier(qualifier)); + } + doc +} + +fn build_path_primary<'a>(primary: ast::PathPrimary) -> Doc<'a> { + match primary { + ast::PathPrimary::VertexPattern(pattern) => build_vertex_pattern(pattern), + ast::PathPrimary::EdgeLeft(edge) => build_edge_left(edge), + ast::PathPrimary::EdgeRight(edge) => build_edge_right(edge), + ast::PathPrimary::EdgeAny(edge) => build_edge_any(edge), + ast::PathPrimary::ParenGraphPattern(pattern) => build_paren_graph_pattern(pattern), + } +} + +fn build_vertex_pattern<'a>(pattern: ast::VertexPattern) -> Doc<'a> { + let mut doc = pattern + .l_paren_token() + .map(comments_before) + .unwrap_or_else(Doc::nil) + .append(Doc::text("(")) + .append(build_graph_pattern_inner( + pattern.element_variable(), + pattern.is_label(), + pattern.where_clause(), + )); + if let Some(r_paren) = pattern.r_paren_token() { + doc = doc.append(comments_before(r_paren)); + } + doc.append(Doc::text(")")) +} + +fn build_edge_left<'a>(edge: ast::EdgeLeft) -> Doc<'a> { + let mut doc = Doc::text("<"); + if let Some(minus) = edge.minus_token() { + doc = doc.append(comments_before(minus)); + } + doc = doc.append(Doc::text("-")); + if let Some(l_brack) = edge.l_brack_token() { + doc = doc + .append(comments_before(l_brack)) + .append(Doc::text("[")) + .append(build_graph_pattern_inner( + edge.element_variable(), + edge.is_label(), + edge.where_clause(), + )); + if let Some(r_brack) = edge.r_brack_token() { + doc = doc.append(comments_before(r_brack)); + } + doc = doc.append(Doc::text("]")); + if let Some(minus) = edge.end_minus_token() { + doc = doc.append(comments_before(minus)); + } + doc = doc.append(Doc::text("-")); + } + doc +} + +fn build_edge_right<'a>(edge: ast::EdgeRight) -> Doc<'a> { + let mut doc = Doc::text("-"); + if let Some(l_brack) = edge.l_brack_token() { + doc = doc + .append(comments_before(l_brack)) + .append(Doc::text("[")) + .append(build_graph_pattern_inner( + edge.element_variable(), + edge.is_label(), + edge.where_clause(), + )); + if let Some(r_brack) = edge.r_brack_token() { + doc = doc.append(comments_before(r_brack)); + } + doc = doc.append(Doc::text("]")); + if let Some(minus) = edge.end_minus_token() { + doc = doc.append(comments_before(minus)); + } + doc = doc.append(Doc::text("-")); + } + if let Some(r_angle) = edge.r_angle_token() { + doc = doc.append(comments_before(r_angle)); + } + doc.append(Doc::text(">")) +} + +fn build_edge_any<'a>(edge: ast::EdgeAny) -> Doc<'a> { + let mut doc = Doc::text("-"); + if let Some(l_brack) = edge.l_brack_token() { + doc = doc + .append(comments_before(l_brack)) + .append(Doc::text("[")) + .append(build_graph_pattern_inner( + edge.element_variable(), + edge.is_label(), + edge.where_clause(), + )); + if let Some(r_brack) = edge.r_brack_token() { + doc = doc.append(comments_before(r_brack)); + } + doc = doc.append(Doc::text("]")); + if let Some(minus) = edge.end_minus_token() { + doc = doc.append(comments_before(minus)); + } + doc = doc.append(Doc::text("-")); + } + doc +} + +fn build_graph_pattern_inner<'a>( + variable: Option, + label: Option, + where_clause: Option, +) -> Doc<'a> { + let mut doc = Doc::nil(); + if let Some(variable) = variable { + doc = doc + .append(leading_comments(variable.syntax())) + .append(build_name(variable.syntax())); + } + if let Some(label) = label { + doc = doc + .append(Doc::space()) + .append(leading_comments(label.syntax())) + .append(build_is_label(label)); + } + if let Some(where_clause) = where_clause { + doc = doc + .append(Doc::space()) + .append(leading_comments(where_clause.syntax())) + .append(build_where_clause(where_clause)); + } + doc +} + +fn build_is_label<'a>(label: ast::IsLabel) -> Doc<'a> { + let mut doc = label + .is_token() + .map(|token| leading_comments_token(&token).append(Doc::text("is"))) + .unwrap_or_else(Doc::nil); + if let Some(expr) = label.expr() { + doc = doc + .append(Doc::space()) + .append(leading_comments(expr.syntax())) + .append(build_expr(expr)); + } + doc +} + +fn build_where_clause<'a>(where_clause: ast::WhereClause) -> Doc<'a> { + let mut doc = where_clause + .where_token() + .map(|token| leading_comments_token(&token).append(Doc::text("where"))) + .unwrap_or_else(Doc::nil); + if let Some(expr) = where_clause.expr() { + doc = doc + .append(Doc::space()) + .append(leading_comments(expr.syntax())) + .append(build_expr(expr)); + } + doc +} + +fn build_paren_graph_pattern<'a>(pattern: ast::ParenGraphPattern) -> Doc<'a> { + let mut doc = pattern + .l_paren_token() + .map(comments_before) + .unwrap_or_else(Doc::nil) + .append(Doc::text("(")); + if let Some(inner) = pattern.path_pattern() { + doc = doc + .append(leading_comments(inner.syntax())) + .append(build_path_pattern(inner)); + } + if let Some(where_clause) = pattern.where_clause() { + doc = doc + .append(Doc::space()) + .append(leading_comments(where_clause.syntax())) + .append(build_where_clause(where_clause)); + } + if let Some(r_paren) = pattern.r_paren_token() { + doc = doc.append(comments_before(r_paren)); + } + doc.append(Doc::text(")")) +} + +fn build_graph_pattern_qualifier<'a>(qualifier: ast::GraphPatternQualifier) -> Doc<'a> { + let mut doc = qualifier + .l_curly_token() + .map(comments_before) + .unwrap_or_else(Doc::nil) + .append(Doc::text("{")); + if let Some(min) = qualifier.min() { + if let Some(literal) = min.literal() { + doc = doc + .append(leading_comments(min.syntax())) + .append(build_literal(literal)); + } + } + if let Some(comma) = qualifier.comma_token() { + doc = doc.append(comments_before(comma)).append(Doc::text(",")); + } + if let Some(max) = qualifier.max() { + if qualifier.comma_token().is_some() { + doc = doc.append(Doc::space()); + } + if let Some(literal) = max.literal() { + doc = doc + .append(leading_comments(max.syntax())) + .append(build_literal(literal)); + } + } + if let Some(r_curly) = qualifier.r_curly_token() { + doc = doc.append(comments_before(r_curly)); + } + doc.append(Doc::text("}")) +} + +fn build_expr_as_column_name_list<'a>(list: ast::ExprAsColumnNameList) -> Doc<'a> { + let mut doc = list + .l_paren_token() + .map(comments_before) + .unwrap_or_else(Doc::nil) + .append(Doc::text("(")); + let items = list.expr_as_column_names().map(|item| { + let mut item_doc = item.expr().map(build_expr).unwrap_or_else(Doc::nil); + if let Some(as_token) = item.as_token() { + item_doc = item_doc + .append(Doc::space()) + .append(leading_comments_token(&as_token)) + .append(Doc::text("as")); + } + if let Some(name) = item.column_name() { + item_doc = item_doc + .append(Doc::space()) + .append(leading_comments(name.syntax())) + .append(build_name(name.syntax())); + } + ( + leading_comments(item.syntax()).append(item_doc), + item.syntax().clone(), + ) + }); + if let Some(items) = build_comma_separated_docs(items) { + doc = doc.append(items); + } + if let Some(r_paren) = list.r_paren_token() { + doc = doc.append(comments_before(r_paren)); + } + doc.append(Doc::text(")")) +} + fn build_xml_element_fn<'a>(xml_element_fn: ast::XmlElementFn) -> Doc<'a> { let mut doc = Doc::text("xmlelement"); if let Some(l_paren) = xml_element_fn.l_paren_token() { diff --git a/crates/squawk_fmt/tests/after/graph_table.snap b/crates/squawk_fmt/tests/after/graph_table.snap new file mode 100644 index 00000000..ad5e5f00 --- /dev/null +++ b/crates/squawk_fmt/tests/after/graph_table.snap @@ -0,0 +1,21 @@ +--- +source: crates/squawk_fmt/tests/tests.rs +input_file: crates/squawk_fmt/tests/before/graph_table.sql +--- +select * from graph_table(g match (a) columns (a)); + +select + * +from lateral graph_table(public.g match (a is person)-[e is knows where e.weight > 1]->(b) where a.active columns (a.name as person_name, b.name)) as matches; + +select + * +from graph_table(g match (a)<-[left_edge]-(b), (c)-[any_edge]-(d), (e)<-(f), (h)->(i), (j)-(k), ((x)->(y) where x.active) columns (a)); + +select + * +from graph_table(g match (a)->{1}(b), (c)-{, 3}(d), (e)-{2, 4}(f) columns (a)); + +select + * +from /* before graph table */ graph_table /* before outer opening paren */(/* before graph */ public /* before graph dot */./* before graph name */ g /* before match */ match /* before first vertex opening paren */ (/* before first variable */ a /* before is */ is /* before first label */ person /* before vertex where */ where /* before vertex expression */ a.active /* before first vertex closing paren */)/* before first edge minus */ - /* before edge opening bracket */[/* before edge variable */ e /* before edge is */ is /* before edge label */ knows /* before edge where */ where /* before edge expression */ e.active /* before edge closing bracket */] /* before edge ending minus */- /* before right angle */>/* before second vertex */ (b)/* before qualifier opening curly */ {/* before qualifier min */ 1 /* before qualifier comma */, /* before qualifier max */ 3 /* before qualifier closing curly */} /* before pattern comma */, /* before second pattern */ /* before left angle */ < /* before left minus */- /* before left opening bracket */[left_edge /* before left closing bracket */] /* before left ending minus */-(c), (d)/* before any edge */ - /* before any opening bracket */[any_edge /* before any closing bracket */] /* before any ending minus */-(e), /* before nested opening paren */ (/* before nested pattern */ (x)/* before simple edge */ - /* before simple right angle */>(y) /* before nested where */ where /* before nested expression */ x.active /* before nested closing paren */) /* before graph where */ where /* before graph expression */ b.active /* before columns */ columns /* before columns opening paren */(/* before first column */ a.name /* before column as */ as /* before column name */ source /* before column comma */, /* before second column */ b.name /* before columns closing paren */) /* before outer closing paren */) /* after graph table */ as /* before alias */ result; diff --git a/crates/squawk_fmt/tests/before/graph_table.sql b/crates/squawk_fmt/tests/before/graph_table.sql new file mode 100644 index 00000000..2b84b2a4 --- /dev/null +++ b/crates/squawk_fmt/tests/before/graph_table.sql @@ -0,0 +1,27 @@ +select * from GRAPH_TABLE ( g MATCH (a) COLUMNS (a) ); + +select * from lateral GRAPH_TABLE(public.g MATCH (a IS person)-[e IS knows WHERE e.weight>1]->(b) WHERE a.active COLUMNS(a.name AS person_name,b.name)) AS matches; + +select * from GRAPH_TABLE(g MATCH (a)<-[left_edge]-(b), (c)-[any_edge]-(d), (e)<-(f), (h)->(i), (j)-(k), ((x)->(y) WHERE x.active) COLUMNS(a)); + +select * from GRAPH_TABLE(g MATCH (a)->{1}(b), (c)-{,3}(d), (e)-{2,4}(f) COLUMNS(a)); + +select * from + /* before graph table */ GRAPH_TABLE /* before outer opening paren */ ( + /* before graph */ public /* before graph dot */ . /* before graph name */ g + /* before match */ MATCH + /* before first vertex opening paren */ ( /* before first variable */ a /* before is */ IS /* before first label */ person /* before vertex where */ WHERE /* before vertex expression */ a.active /* before first vertex closing paren */ ) + /* before first edge minus */ - /* before edge opening bracket */ [ /* before edge variable */ e /* before edge is */ IS /* before edge label */ knows /* before edge where */ WHERE /* before edge expression */ e.active /* before edge closing bracket */ ] /* before edge ending minus */ - /* before right angle */ > + /* before second vertex */ (b) + /* before qualifier opening curly */ { /* before qualifier min */ 1 /* before qualifier comma */, /* before qualifier max */ 3 /* before qualifier closing curly */ } + /* before pattern comma */, /* before second pattern */ + /* before left angle */ < /* before left minus */ - /* before left opening bracket */ [left_edge /* before left closing bracket */ ] /* before left ending minus */ - (c), + (d) /* before any edge */ - /* before any opening bracket */ [any_edge /* before any closing bracket */ ] /* before any ending minus */ - (e), + /* before nested opening paren */ ( /* before nested pattern */ (x) /* before simple edge */ - /* before simple right angle */ > (y) /* before nested where */ WHERE /* before nested expression */ x.active /* before nested closing paren */ ) + /* before graph where */ WHERE /* before graph expression */ b.active + /* before columns */ COLUMNS /* before columns opening paren */ ( + /* before first column */ a.name /* before column as */ AS /* before column name */ source /* before column comma */, + /* before second column */ b.name + /* before columns closing paren */ ) + /* before outer closing paren */ + ) /* after graph table */ AS /* before alias */ result; diff --git a/crates/squawk_syntax/src/ast/node_ext.rs b/crates/squawk_syntax/src/ast/node_ext.rs index 6e2558d0..1ec02abb 100644 --- a/crates/squawk_syntax/src/ast/node_ext.rs +++ b/crates/squawk_syntax/src/ast/node_ext.rs @@ -495,6 +495,31 @@ impl ast::ForeignKeyConstraint { } } +fn second_minus_token(node: &SyntaxNode) -> Option { + node.children_with_tokens() + .filter_map(|element| element.into_token()) + .filter(|token| token.kind() == SyntaxKind::MINUS) + .nth(1) +} + +impl ast::EdgeAny { + pub fn end_minus_token(&self) -> Option { + second_minus_token(self.syntax()) + } +} + +impl ast::EdgeLeft { + pub fn end_minus_token(&self) -> Option { + second_minus_token(self.syntax()) + } +} + +impl ast::EdgeRight { + pub fn end_minus_token(&self) -> Option { + second_minus_token(self.syntax()) + } +} + impl ast::XmlPiFn { #[inline] pub fn target(&self) -> Option { From de4e64da470566ee187e17d858622b0fba87e1e9 Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Mon, 24 Aug 2026 00:01:11 -0400 Subject: [PATCH 11/17] fmt: table constraints --- crates/squawk_fmt/src/fmt.rs | 611 +++++++++++++++++- .../tests/after/table_constraints.snap | 62 ++ .../tests/before/table_constraints.sql | 35 + 3 files changed, 707 insertions(+), 1 deletion(-) create mode 100644 crates/squawk_fmt/tests/after/table_constraints.snap create mode 100644 crates/squawk_fmt/tests/before/table_constraints.sql diff --git a/crates/squawk_fmt/src/fmt.rs b/crates/squawk_fmt/src/fmt.rs index 670f100e..56c44b8b 100644 --- a/crates/squawk_fmt/src/fmt.rs +++ b/crates/squawk_fmt/src/fmt.rs @@ -156,11 +156,620 @@ fn build_table_arg<'a>(arg: ast::TableArg) -> Doc<'a> { doc } ast::TableArg::LikeClause(like_clause) => build_like_clause(like_clause), - ast::TableArg::TableConstraint(_table_constraint) => todo!(), + ast::TableArg::TableConstraint(table_constraint) => { + build_table_constraint(table_constraint.clone()) + } }); doc.append(trailing_comments(arg.syntax())) } +fn build_table_constraint<'a>(constraint: ast::TableConstraint) -> Doc<'a> { + match constraint { + ast::TableConstraint::CheckConstraint(constraint) => build_check_constraint(constraint), + ast::TableConstraint::ExcludeConstraint(constraint) => build_exclude_constraint(constraint), + ast::TableConstraint::ForeignKeyConstraint(constraint) => { + build_foreign_key_constraint(constraint) + } + ast::TableConstraint::PrimaryKeyConstraint(constraint) => { + build_primary_key_constraint(constraint) + } + ast::TableConstraint::UniqueConstraint(constraint) => build_unique_constraint(constraint), + } +} + +fn build_constraint_name_clause<'a>(clause: Option) -> Doc<'a> { + let Some(clause) = clause else { + return Doc::nil(); + }; + let mut doc = Doc::text("constraint"); + if let Some(name) = clause.constraint_name() { + doc = doc + .append(Doc::space()) + .append(leading_comments(name.syntax())) + .append(build_name(name.syntax())); + } + doc.append(Doc::space()) +} + +fn build_check_constraint<'a>(constraint: ast::CheckConstraint) -> Doc<'a> { + let mut doc = build_constraint_name_clause(constraint.constraint_name_clause()); + if let Some(check) = constraint.check_token() { + doc = doc + .append(leading_comments_token(&check)) + .append(Doc::text("check")); + } + if let Some(l_paren) = constraint.l_paren_token() { + doc = doc + .append(Doc::space()) + .append(leading_comments_token(&l_paren)); + } + doc = doc.append(Doc::text("(")); + if let Some(expr) = constraint.expr() { + doc = doc + .append(leading_comments(expr.syntax())) + .append(build_expr(expr)); + } + if let Some(r_paren) = constraint.r_paren_token() { + doc = doc.append(comments_before(r_paren)); + } + append_constraint_options(doc.append(Doc::text(")")), constraint.constraint_options()) + .nest(2) + .group() +} + +fn build_primary_key_constraint<'a>(constraint: ast::PrimaryKeyConstraint) -> Doc<'a> { + let mut doc = build_constraint_name_clause(constraint.constraint_name_clause()); + if let Some(primary) = constraint.primary_token() { + doc = doc + .append(leading_comments_token(&primary)) + .append(Doc::text("primary")); + } + if let Some(key) = constraint.key_token() { + doc = doc + .append(Doc::space()) + .append(leading_comments_token(&key)) + .append(Doc::text("key")); + } + if let Some(using_index) = constraint.using_index() { + doc = doc + .append(Doc::space()) + .append(leading_comments(using_index.syntax())) + .append(build_using_index_name(using_index)); + } else if let Some(parameters) = constraint.index_parameters() { + doc = doc + .append(leading_comments(parameters.syntax())) + .append(build_index_parameters(parameters)); + } + append_constraint_options(doc, constraint.constraint_options()) + .nest(2) + .group() +} + +fn build_unique_constraint<'a>(constraint: ast::UniqueConstraint) -> Doc<'a> { + let mut doc = build_constraint_name_clause(constraint.constraint_name_clause()); + if let Some(unique) = constraint.unique_token() { + doc = doc + .append(leading_comments_token(&unique)) + .append(Doc::text("unique")); + } + if let Some(using_index) = constraint.using_index() { + doc = doc + .append(Doc::space()) + .append(leading_comments(using_index.syntax())) + .append(build_using_index_name(using_index)); + } else if let Some(parameters) = constraint.index_parameters() { + doc = doc + .append(leading_comments(parameters.syntax())) + .append(build_index_parameters(parameters)); + } + append_constraint_options(doc, constraint.constraint_options()) + .nest(2) + .group() +} + +fn build_using_index_name<'a>(using_index: ast::UsingIndexName) -> Doc<'a> { + let mut doc = Doc::text("using"); + if let Some(index) = using_index.index_token() { + doc = doc + .append(Doc::space()) + .append(leading_comments_token(&index)) + .append(Doc::text("index")); + } + if let Some(index) = using_index.index_ref() { + if let Some(path) = index.path_ref() { + doc = doc + .append(Doc::space()) + .append(leading_comments(index.syntax())) + .append(build_path_ref(&path)); + } + } + doc +} + +fn build_index_parameters<'a>(parameters: ast::IndexParameters) -> Doc<'a> { + let mut doc = Doc::nil(); + if let Some(nulls) = parameters.nulls_distinct_option() { + doc = doc + .append(Doc::space()) + .append(leading_comments(nulls.syntax())) + .append(build_keyword_node(nulls.syntax())); + } + if let Some(columns) = parameters.column_list() { + doc = doc + .append(Doc::space()) + .append(leading_comments(columns.syntax())) + .append(build_constraint_column_ref_list(columns)); + } + if let Some(include) = parameters.constraint_include_clause() { + doc = doc + .append(Doc::line_or_space()) + .append(leading_comments(include.syntax())) + .append(build_constraint_include_clause(include)); + } + if let Some(with_params) = parameters.with_params() { + doc = doc + .append(Doc::line_or_space()) + .append(leading_comments(with_params.syntax())) + .append(build_with_params(with_params)); + } + if let Some(tablespace) = parameters.constraint_index_tablespace() { + doc = doc + .append(Doc::line_or_space()) + .append(leading_comments(tablespace.syntax())) + .append(build_constraint_index_tablespace(tablespace)); + } + doc +} + +fn build_constraint_column_ref_list<'a>(list: ast::ConstraintColumnRefList) -> Doc<'a> { + let suffix = list.without_overlaps().map(|overlaps| { + Doc::space() + .append(leading_comments(overlaps.syntax())) + .append(build_keyword_node(overlaps.syntax())) + }); + build_column_names( + list.l_paren_token(), + list.column_name_refs(), + suffix, + list.r_paren_token(), + ) +} + +fn build_column_ref_list<'a>(list: ast::ColumnRefList) -> Doc<'a> { + build_column_names( + list.l_paren_token(), + list.column_name_refs(), + None, + list.r_paren_token(), + ) +} + +fn build_column_names<'a>( + l_paren: Option, + names: impl Iterator, + suffix: Option>, + r_paren: Option, +) -> Doc<'a> { + let mut doc = l_paren + .map(comments_before) + .unwrap_or_else(Doc::nil) + .append(Doc::text("(")); + let items = names.map(|name| { + ( + leading_comments(name.syntax()).append(build_name(name.syntax())), + name.syntax().clone(), + ) + }); + if let Some(items) = build_comma_separated_docs(items) { + doc = doc.append(items); + } + if let Some(suffix) = suffix { + doc = doc.append(suffix); + } + if let Some(r_paren) = r_paren { + doc = doc.append(comments_before(r_paren)); + } + doc.append(Doc::text(")")) +} + +fn build_constraint_include_clause<'a>(include: ast::ConstraintIncludeClause) -> Doc<'a> { + let mut doc = Doc::text("include"); + if let Some(columns) = include.column_ref_list() { + doc = doc + .append(Doc::space()) + .append(leading_comments(columns.syntax())) + .append(build_column_ref_list(columns)); + } + doc +} + +fn build_with_params<'a>(with_params: ast::WithParams) -> Doc<'a> { + let mut doc = Doc::text("with"); + if let Some(attributes) = with_params.attribute_list() { + doc = doc + .append(Doc::space()) + .append(leading_comments(attributes.syntax())) + .append(build_attribute_list(attributes)); + } + doc +} + +fn build_attribute_list<'a>(list: ast::AttributeList) -> Doc<'a> { + let mut doc = list + .l_paren_token() + .map(comments_before) + .unwrap_or_else(Doc::nil) + .append(Doc::text("(")); + let items = list.attribute_options().map(|option| { + let mut item = option + .namespace() + .map(|namespace| build_name(namespace.syntax())) + .unwrap_or_else(Doc::nil); + if let Some(dot) = option.dot_token() { + item = item.append(comments_before(dot)).append(Doc::text(".")); + } + if let Some(name) = option.name() { + item = item + .append(leading_comments(name.syntax())) + .append(build_name(name.syntax())); + } + if let Some(eq) = option.eq_token() { + item = item + .append(Doc::space()) + .append(leading_comments_token(&eq)) + .append(Doc::text("=")); + } + if let Some(value) = option.attribute_value() { + item = item + .append(Doc::space()) + .append(leading_comments(value.syntax())) + .append(build_attribute_value(value)); + } + ( + leading_comments(option.syntax()).append(item), + option.syntax().clone(), + ) + }); + if let Some(items) = build_comma_separated_docs(items) { + doc = doc.append(items); + } + if let Some(r_paren) = list.r_paren_token() { + doc = doc.append(comments_before(r_paren)); + } + doc.append(Doc::text(")")) +} + +fn build_attribute_value<'a>(value: ast::AttributeValue) -> Doc<'a> { + if let Some(literal) = value.literal() { + build_literal(literal) + } else if let Some(ty) = value.ty() { + build_type(ty) + } else if value.none_token().is_some() { + Doc::text("none") + } else if let Some(op) = value.op() { + if value.operator_token().is_some() { + let mut doc = Doc::text("operator"); + if let Some(l_paren) = value.l_paren_token() { + doc = doc.append(comments_before(l_paren)); + } + doc = doc.append(Doc::text("(")).append(build_operator(&op)); + if let Some(r_paren) = value.r_paren_token() { + doc = doc.append(comments_before(r_paren)); + } + doc.append(Doc::text(")")) + } else { + build_operator(&op) + } + } else { + Doc::nil() + } +} + +fn build_constraint_index_tablespace<'a>(tablespace: ast::ConstraintIndexTablespace) -> Doc<'a> { + let mut doc = Doc::text("using"); + if let Some(index) = tablespace.index_token() { + doc = doc + .append(Doc::space()) + .append(leading_comments_token(&index)) + .append(Doc::text("index")); + } + if let Some(token) = tablespace.tablespace_token() { + doc = doc + .append(Doc::space()) + .append(leading_comments_token(&token)) + .append(Doc::text("tablespace")); + } + if let Some(name) = tablespace.tablespace_ref() { + doc = doc + .append(Doc::space()) + .append(leading_comments(name.syntax())) + .append(build_name(name.syntax())); + } + doc +} + +fn append_constraint_options<'a>( + mut doc: Doc<'a>, + options: impl Iterator, +) -> Doc<'a> { + for option in options { + doc = doc + .append(Doc::line_or_space()) + .append(leading_comments(option.syntax())) + .append(build_keyword_node(option.syntax())); + } + doc +} + +fn build_foreign_key_constraint<'a>(constraint: ast::ForeignKeyConstraint) -> Doc<'a> { + let mut doc = build_constraint_name_clause(constraint.constraint_name_clause()); + if let Some(foreign) = constraint.foreign_token() { + doc = doc + .append(leading_comments_token(&foreign)) + .append(Doc::text("foreign")); + } + if let Some(key) = constraint.key_token() { + doc = doc + .append(Doc::space()) + .append(leading_comments_token(&key)) + .append(Doc::text("key")); + } + if let Some(columns) = constraint.from_columns() { + doc = doc + .append(Doc::space()) + .append(leading_comments(columns.syntax())) + .append(build_foreign_key_column_list(columns)); + } + if let Some(references) = constraint.references_token() { + doc = doc + .append(Doc::space()) + .append(leading_comments_token(&references)) + .append(Doc::text("references")); + } + if let Some(table) = constraint.table_name_ref() { + if let Some(path) = table.path_ref() { + doc = doc + .append(Doc::space()) + .append(leading_comments(table.syntax())) + .append(build_path_ref(&path)); + } + } + if let Some(columns) = constraint.to_columns() { + doc = doc + .append(Doc::space()) + .append(leading_comments(columns.syntax())) + .append(build_foreign_key_column_list(columns)); + } + if let Some(match_type) = constraint.match_type() { + doc = doc + .append(Doc::line_or_space()) + .append(leading_comments(match_type.syntax())) + .append(build_keyword_node(match_type.syntax())); + } + if let Some(action) = constraint.on_delete_action() { + doc = doc + .append(Doc::line_or_space()) + .append(leading_comments(action.syntax())) + .append(build_reference_action( + action.on_token(), + action.delete_token(), + "delete", + action.ref_action(), + )); + } + if let Some(action) = constraint.on_update_action() { + doc = doc + .append(Doc::line_or_space()) + .append(leading_comments(action.syntax())) + .append(build_reference_action( + action.on_token(), + action.update_token(), + "update", + action.ref_action(), + )); + } + append_constraint_options(doc, constraint.constraint_options()) + .nest(2) + .group() +} + +fn build_foreign_key_column_list<'a>(list: ast::ForeignKeyColumnList) -> Doc<'a> { + let suffix = list.period_column().map(|period| { + let mut doc = Doc::space() + .append(leading_comments(period.syntax())) + .append(Doc::text("period")); + if let Some(name) = period.name() { + doc = doc + .append(Doc::space()) + .append(leading_comments(name.syntax())) + .append(build_name(name.syntax())); + } + doc + }); + build_column_names( + list.l_paren_token(), + list.column_name_refs(), + suffix, + list.r_paren_token(), + ) +} + +fn build_reference_action<'a>( + on: Option, + kind_token: Option, + kind: &'static str, + action: Option, +) -> Doc<'a> { + let mut doc = on + .map(|token| leading_comments_token(&token).append(Doc::text("on"))) + .unwrap_or_else(Doc::nil); + if let Some(token) = kind_token { + doc = doc + .append(Doc::space()) + .append(leading_comments_token(&token)) + .append(Doc::text(kind)); + } + let Some(action) = action else { + return doc; + }; + doc = doc + .append(Doc::space()) + .append(leading_comments(action.syntax())); + match action { + ast::RefAction::SetNullColumns(action) => { + if let Some(set) = action.set_token() { + doc = doc + .append(leading_comments_token(&set)) + .append(Doc::text("set")); + } + if let Some(null) = action.null_token() { + doc = doc + .append(Doc::space()) + .append(leading_comments_token(&null)) + .append(Doc::text("null")); + } + if let Some(columns) = action.column_ref_list() { + doc = doc + .append(Doc::space()) + .append(leading_comments(columns.syntax())) + .append(build_column_ref_list(columns)); + } + doc + } + ast::RefAction::SetDefaultColumns(action) => { + if let Some(set) = action.set_token() { + doc = doc + .append(leading_comments_token(&set)) + .append(Doc::text("set")); + } + if let Some(default) = action.default_token() { + doc = doc + .append(Doc::space()) + .append(leading_comments_token(&default)) + .append(Doc::text("default")); + } + if let Some(columns) = action.column_ref_list() { + doc = doc + .append(Doc::space()) + .append(leading_comments(columns.syntax())) + .append(build_column_ref_list(columns)); + } + doc + } + action => doc.append(build_keyword_node(action.syntax())), + } +} + +fn build_exclude_constraint<'a>(constraint: ast::ExcludeConstraint) -> Doc<'a> { + let mut doc = build_constraint_name_clause(constraint.constraint_name_clause()); + if let Some(exclude) = constraint.exclude_token() { + doc = doc + .append(leading_comments_token(&exclude)) + .append(Doc::text("exclude")); + } + if let Some(method) = constraint.constraint_index_method() { + doc = doc + .append(Doc::space()) + .append(leading_comments(method.syntax())) + .append(Doc::text("using")); + if let Some(name) = method.access_method_ref() { + doc = doc + .append(Doc::space()) + .append(leading_comments(name.syntax())) + .append(build_name(name.syntax())); + } + } + if let Some(list) = constraint.constraint_exclusion_list() { + doc = doc + .append(Doc::space()) + .append(leading_comments(list.syntax())) + .append(build_constraint_exclusion_list(list)); + } + if let Some(include) = constraint.constraint_include_clause() { + doc = doc + .append(Doc::line_or_space()) + .append(leading_comments(include.syntax())) + .append(build_constraint_include_clause(include)); + } + if let Some(with_params) = constraint.with_params() { + doc = doc + .append(Doc::line_or_space()) + .append(leading_comments(with_params.syntax())) + .append(build_with_params(with_params)); + } + if let Some(tablespace) = constraint.constraint_index_tablespace() { + doc = doc + .append(Doc::line_or_space()) + .append(leading_comments(tablespace.syntax())) + .append(build_constraint_index_tablespace(tablespace)); + } + if let Some(where_clause) = constraint.where_condition_clause() { + doc = doc + .append(Doc::line_or_space()) + .append(leading_comments(where_clause.syntax())) + .append(build_where_condition_clause(where_clause)); + } + append_constraint_options(doc, constraint.constraint_options()) + .nest(2) + .group() +} + +fn build_constraint_exclusion_list<'a>(list: ast::ConstraintExclusionList) -> Doc<'a> { + let mut doc = list + .l_paren_token() + .map(comments_before) + .unwrap_or_else(Doc::nil) + .append(Doc::text("(")); + let items = list.constraint_exclusions().map(|exclusion| { + let mut item = exclusion.expr().map(build_expr).unwrap_or_else(Doc::nil); + if let Some(with) = exclusion.with_token() { + item = item + .append(Doc::space()) + .append(leading_comments_token(&with)) + .append(Doc::text("with")); + } + if let Some(op) = exclusion.op() { + item = item + .append(Doc::space()) + .append(leading_comments(op.syntax())) + .append(build_operator(&op)); + } else if let Some(op) = exclusion.operator_call() { + item = item + .append(Doc::space()) + .append(leading_comments(op.syntax())) + .append(build_operator_call(&op)); + } + ( + leading_comments(exclusion.syntax()).append(item), + exclusion.syntax().clone(), + ) + }); + if let Some(items) = build_comma_separated_docs(items) { + doc = doc.append(items); + } + if let Some(r_paren) = list.r_paren_token() { + doc = doc.append(comments_before(r_paren)); + } + doc.append(Doc::text(")")) +} + +fn build_where_condition_clause<'a>(where_clause: ast::WhereConditionClause) -> Doc<'a> { + let mut doc = Doc::text("where"); + if let Some(l_paren) = where_clause.l_paren_token() { + doc = doc.append(Doc::space()).append(comments_before(l_paren)); + } + doc = doc.append(Doc::text("(")); + if let Some(expr) = where_clause.expr() { + doc = doc + .append(leading_comments(expr.syntax())) + .append(build_expr(expr)); + } + if let Some(r_paren) = where_clause.r_paren_token() { + doc = doc.append(comments_before(r_paren)); + } + doc.append(Doc::text(")")) +} + fn build_like_clause<'a>(like_clause: &ast::LikeClause) -> Doc<'a> { let mut doc = Doc::text("like"); diff --git a/crates/squawk_fmt/tests/after/table_constraints.snap b/crates/squawk_fmt/tests/after/table_constraints.snap new file mode 100644 index 00000000..3095104b --- /dev/null +++ b/crates/squawk_fmt/tests/after/table_constraints.snap @@ -0,0 +1,62 @@ +--- +source: crates/squawk_fmt/tests/tests.rs +input_file: crates/squawk_fmt/tests/before/table_constraints.sql +--- +create table simple_constraints( + id bigint, + parent_id bigint, + name text, + primary key (id), + unique nulls not distinct (name), + check (id > 0), + foreign key (parent_id) references parents (id) +); + +create table named_constraints( + id bigint, + valid_at tstzrange, + constraint pk primary key (id) deferrable initially deferred, + constraint name_unique unique (id) + include (valid_at) + with (fillfactor = 70) + using index tablespace fast, + constraint id_check check (id > 0) not valid no inherit, + constraint parent_fk foreign key (id) references public.parents (id) + match full + on delete set null (id) + on update no action + not deferrable, + constraint no_overlap exclude using gist (id with =, valid_at with &&) + include (id) + with (fillfactor = 80) + using index tablespace fast + where (id > 0) + deferrable +); + +create table using_indexes( + id bigint, + unique using index existing_unique, + primary key using index existing_primary +); + +create table commented_constraints( + id bigint, + parent_id bigint, + valid_at tstzrange, + /* before constraint */ constraint /* before constraint name */ named_pk /* before primary */ primary /* before key */ key/* before column opening paren */ (/* before column */ id /* before column closing paren */) + /* before deferrable */ deferrable, + constraint named_check /* before check */ check /* before check opening paren */ (/* before check expression */ id > 0 /* before check closing paren */) + /* before not */ not /* before valid */ valid, + constraint named_fk /* before foreign */ foreign /* before key */ key /* before from opening paren */ (/* before from column */ parent_id /* before from closing paren */) /* before references */ references /* before table */ public /* before dot */./* before table name */ parents /* before to opening paren */ (/* before to column */ id /* before to closing paren */) + /* before match */ match /* before simple */ simple + /* before on delete */ on /* before delete */ delete /* before set */ set /* before null */ null /* before set columns */ (parent_id) + /* before on update */ on /* before update */ update /* before cascade */ cascade + /* before enforced */ enforced, + constraint named_exclude /* before exclude */ exclude /* before using */ using /* before method */ gist /* before exclusion opening paren */ (/* before exclusion expression */ id /* before exclusion with */ with /* before exclusion op */ = /* before exclusion comma */, /* before second exclusion */ valid_at with /* before operator */ operator /* before operator opening paren */(/* before operator name */ public /* before operator dot */./* before operator op */ && /* before operator closing paren */) /* before exclusion closing paren */) + /* before include */ include /* before include opening paren */ (id /* before include closing paren */) + /* before with params */ with /* before params opening paren */ (/* before param */ fillfactor /* before equals */ = /* before value */ 80 /* before params closing paren */) + /* before tablespace using */ using /* before index */ index /* before tablespace */ tablespace /* before tablespace name */ fast + /* before where */ where /* before where opening paren */(/* before where expression */ id > 0 /* before where closing paren */) + /* before initially */ initially /* before immediate */ immediate +); diff --git a/crates/squawk_fmt/tests/before/table_constraints.sql b/crates/squawk_fmt/tests/before/table_constraints.sql new file mode 100644 index 00000000..43ed7b96 --- /dev/null +++ b/crates/squawk_fmt/tests/before/table_constraints.sql @@ -0,0 +1,35 @@ +create table simple_constraints ( + id bigint, + parent_id bigint, + name text, + PRIMARY KEY (id), + UNIQUE NULLS NOT DISTINCT (name), + CHECK (id > 0), + FOREIGN KEY (parent_id) REFERENCES parents(id) +); + +create table named_constraints ( + id bigint, + valid_at tstzrange, + CONSTRAINT pk PRIMARY KEY (id) DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT name_unique UNIQUE (id) INCLUDE (valid_at) WITH (fillfactor = 70) USING INDEX TABLESPACE fast, + CONSTRAINT id_check CHECK (id > 0) NOT VALID NO INHERIT, + CONSTRAINT parent_fk FOREIGN KEY (id) REFERENCES public.parents(id) MATCH FULL ON DELETE SET NULL (id) ON UPDATE NO ACTION NOT DEFERRABLE, + CONSTRAINT no_overlap EXCLUDE USING gist (id WITH =, valid_at WITH &&) INCLUDE (id) WITH (fillfactor = 80) USING INDEX TABLESPACE fast WHERE (id > 0) DEFERRABLE +); + +create table using_indexes ( + id bigint, + UNIQUE USING INDEX existing_unique, + PRIMARY KEY USING INDEX existing_primary +); + +create table commented_constraints ( + id bigint, + parent_id bigint, + valid_at tstzrange, + /* before constraint */ CONSTRAINT /* before constraint name */ "named_pk" /* before primary */ PRIMARY /* before key */ KEY /* before column opening paren */ ( /* before column */ id /* before column closing paren */ ) /* before deferrable */ DEFERRABLE, + CONSTRAINT named_check /* before check */ CHECK /* before check opening paren */ ( /* before check expression */ id > 0 /* before check closing paren */ ) /* before not */ NOT /* before valid */ VALID, + CONSTRAINT named_fk /* before foreign */ FOREIGN /* before key */ KEY /* before from opening paren */ ( /* before from column */ parent_id /* before from closing paren */ ) /* before references */ REFERENCES /* before table */ public /* before dot */ . /* before table name */ parents /* before to opening paren */ ( /* before to column */ id /* before to closing paren */ ) /* before match */ MATCH /* before simple */ SIMPLE /* before on delete */ ON /* before delete */ DELETE /* before set */ SET /* before null */ NULL /* before set columns */ (parent_id) /* before on update */ ON /* before update */ UPDATE /* before cascade */ CASCADE /* before enforced */ ENFORCED, + CONSTRAINT named_exclude /* before exclude */ EXCLUDE /* before using */ USING /* before method */ gist /* before exclusion opening paren */ ( /* before exclusion expression */ id /* before exclusion with */ WITH /* before exclusion op */ = /* before exclusion comma */, /* before second exclusion */ valid_at WITH /* before operator */ OPERATOR /* before operator opening paren */ ( /* before operator name */ public /* before operator dot */ . /* before operator op */ && /* before operator closing paren */ ) /* before exclusion closing paren */ ) /* before include */ INCLUDE /* before include opening paren */ (id /* before include closing paren */ ) /* before with params */ WITH /* before params opening paren */ ( /* before param */ fillfactor /* before equals */ = /* before value */ 80 /* before params closing paren */ ) /* before tablespace using */ USING /* before index */ INDEX /* before tablespace */ TABLESPACE /* before tablespace name */ fast /* before where */ WHERE /* before where opening paren */ ( /* before where expression */ id > 0 /* before where closing paren */ ) /* before initially */ INITIALLY /* before immediate */ IMMEDIATE +); From 21fedeaa58c73dd049e7858430d5b8ef752d78a0 Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Mon, 24 Aug 2026 00:25:50 -0400 Subject: [PATCH 12/17] fmt: wrap stuff --- crates/squawk_fmt/src/fmt.rs | 302 ++++++++++-------- crates/squawk_fmt/tests/after/from.snap | 6 +- .../squawk_fmt/tests/after/graph_table.snap | 74 ++++- crates/squawk_fmt/tests/after/group_by.snap | 32 +- .../squawk_fmt/tests/after/select_expr.snap | 189 +++++++++-- .../tests/after/table_constraints.snap | 39 ++- .../squawk_fmt/tests/after/xml_functions.snap | 53 ++- .../squawk_fmt/tests/before/graph_table.sql | 2 + crates/squawk_fmt/tests/before/group_by.sql | 1 + .../squawk_fmt/tests/before/select_expr.sql | 13 + .../tests/before/table_constraints.sql | 8 +- .../squawk_fmt/tests/before/xml_functions.sql | 7 + 12 files changed, 530 insertions(+), 196 deletions(-) diff --git a/crates/squawk_fmt/src/fmt.rs b/crates/squawk_fmt/src/fmt.rs index 56c44b8b..54996e31 100644 --- a/crates/squawk_fmt/src/fmt.rs +++ b/crates/squawk_fmt/src/fmt.rs @@ -522,7 +522,7 @@ fn build_foreign_key_constraint<'a>(constraint: ast::ForeignKeyConstraint) -> Do } if let Some(references) = constraint.references_token() { doc = doc - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments_token(&references)) .append(Doc::text("references")); } @@ -724,7 +724,7 @@ fn build_constraint_exclusion_list<'a>(list: ast::ConstraintExclusionList) -> Do let mut item = exclusion.expr().map(build_expr).unwrap_or_else(Doc::nil); if let Some(with) = exclusion.with_token() { item = item - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments_token(&with)) .append(Doc::text("with")); } @@ -740,7 +740,7 @@ fn build_constraint_exclusion_list<'a>(list: ast::ConstraintExclusionList) -> Do .append(build_operator_call(&op)); } ( - leading_comments(exclusion.syntax()).append(item), + leading_comments(exclusion.syntax()).append(item.nest(2).group()), exclusion.syntax().clone(), ) }); @@ -841,6 +841,9 @@ fn build_select_doc<'a>(select: &ast::Select) -> Doc<'a> { .nest(2); } } + if select.from_clause().is_some() { + doc = doc.group(); + } if let Some(from) = select.from_clause() { doc = doc.append( @@ -1191,14 +1194,16 @@ fn build_grouping_list<'a>( } } else { doc = doc.append( - Doc::list( - Itertools::intersperse( - items.into_iter(), - Doc::text(",").append(Doc::line_or_space()), - ) - .collect(), - ) - .nest(2), + Doc::line_or_nil() + .append(Doc::list( + Itertools::intersperse( + items.into_iter(), + Doc::text(",").append(Doc::line_or_space()), + ) + .collect(), + )) + .nest(2) + .append(Doc::line_or_nil()), ); } @@ -1509,50 +1514,60 @@ fn build_graph_table_fn<'a>(graph_table_fn: ast::GraphTableFn) -> Doc<'a> { } doc = doc.append(Doc::text("(")); + let mut body = Doc::nil(); if let Some(graph) = graph_table_fn.property_graph_ref() { if let Some(path) = graph.path_ref() { - doc = doc + body = body .append(leading_comments(graph.syntax())) .append(build_path_ref(&path)); } } if let Some(match_token) = graph_table_fn.match_token() { - doc = doc - .append(Doc::space()) + body = body + .append(Doc::line_or_space()) .append(leading_comments_token(&match_token)) .append(Doc::text("match")); } if let Some(patterns) = graph_table_fn.path_pattern_list() { - doc = doc - .append(Doc::space()) - .append(leading_comments(patterns.syntax())) - .append(build_path_pattern_list(patterns)); + body = body.append( + Doc::line_or_space() + .append(leading_comments(patterns.syntax())) + .append(build_path_pattern_list(patterns)) + .nest(2), + ); } if let Some(where_clause) = graph_table_fn.where_clause() { - doc = doc - .append(Doc::space()) + body = body + .append(Doc::line_or_space()) .append(leading_comments(where_clause.syntax())) .append(build_where_clause(where_clause)); } if let Some(columns) = graph_table_fn.columns_token() { - doc = doc - .append(Doc::space()) + body = body + .append(Doc::line_or_space()) .append(leading_comments_token(&columns)) .append(Doc::text("columns")); } if let Some(columns) = graph_table_fn.expr_as_column_name_list() { if comment_tokens_before(columns.syntax().clone()).is_empty() { - doc = doc.append(Doc::space()); + body = body.append(Doc::space()); } else { - doc = doc.append(comments_before(columns.syntax().clone())); + body = body.append(comments_before(columns.syntax().clone())); } - doc = doc.append(build_expr_as_column_name_list(columns)); + body = body.append(build_expr_as_column_name_list(columns)); } if let Some(r_paren) = graph_table_fn.r_paren_token() { - doc = doc.append(comments_before(r_paren)); - } - doc.append(Doc::text(")")) + body = body.append(comments_before(r_paren)); + } + doc.append( + Doc::line_or_nil() + .append(body) + .nest(2) + .append(Doc::line_or_nil()) + .group(), + ) + .append(Doc::text(")")) } fn build_path_pattern_list<'a>(patterns: ast::PathPatternList) -> Doc<'a> { @@ -1567,11 +1582,16 @@ fn build_path_pattern_list<'a>(patterns: ast::PathPatternList) -> Doc<'a> { fn build_path_pattern<'a>(pattern: ast::PathPattern) -> Doc<'a> { Doc::list( - pattern - .path_factors() - .map(|factor| leading_comments(factor.syntax()).append(build_path_factor(factor))) - .collect(), + Itertools::intersperse( + pattern + .path_factors() + .map(|factor| leading_comments(factor.syntax()).append(build_path_factor(factor))), + Doc::line_or_nil(), + ) + .collect(), ) + .nest(2) + .group() } fn build_path_factor<'a>(factor: ast::PathFactor) -> Doc<'a> { @@ -1703,17 +1723,17 @@ fn build_graph_pattern_inner<'a>( } if let Some(label) = label { doc = doc - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments(label.syntax())) .append(build_is_label(label)); } if let Some(where_clause) = where_clause { doc = doc - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments(where_clause.syntax())) .append(build_where_clause(where_clause)); } - doc + doc.nest(2).group() } fn build_is_label<'a>(label: ast::IsLabel) -> Doc<'a> { @@ -1830,7 +1850,7 @@ fn build_expr_as_column_name_list<'a>(list: ast::ExprAsColumnNameList) -> Doc<'a if let Some(r_paren) = list.r_paren_token() { doc = doc.append(comments_before(r_paren)); } - doc.append(Doc::text(")")) + doc.append(Doc::text(")")).nest(2).group() } fn build_xml_element_fn<'a>(xml_element_fn: ast::XmlElementFn) -> Doc<'a> { @@ -1879,7 +1899,7 @@ fn build_xml_element_fn<'a>(xml_element_fn: ast::XmlElementFn) -> Doc<'a> { doc = doc .append(trailing_comments(&previous)) .append(Doc::text(",")) - .append(Doc::space()) + .append(Doc::line_or_space()) .append(item); previous = syntax; } @@ -1887,7 +1907,7 @@ fn build_xml_element_fn<'a>(xml_element_fn: ast::XmlElementFn) -> Doc<'a> { if let Some(r_paren) = xml_element_fn.r_paren_token() { doc = doc.append(comments_before(r_paren)); } - doc.append(Doc::text(")")) + doc.append(Doc::text(")")).nest(2).group() } fn build_expr_as_xml_attr_list<'a>(attrs: ast::ExprAsXmlAttrList) -> Doc<'a> { @@ -1923,7 +1943,7 @@ fn build_expr_as_xml_attr_list<'a>(attrs: ast::ExprAsXmlAttrList) -> Doc<'a> { if let Some(r_paren) = attrs.r_paren_token() { doc = doc.append(comments_before(r_paren)); } - doc.append(Doc::text(")")) + doc.append(Doc::text(")")).nest(2).group() } fn build_xml_exists_fn<'a>(xml_exists_fn: ast::XmlExistsFn) -> Doc<'a> { @@ -1941,26 +1961,26 @@ fn build_xml_exists_fn<'a>(xml_exists_fn: ast::XmlExistsFn) -> Doc<'a> { } if let Some(passing_token) = passing.passing_token() { doc = doc - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments_token(&passing_token)) .append(Doc::text("passing")); } if let Some(mech) = passing.xml_passing_mech() { doc = doc - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments(mech.syntax())) .append(build_xml_passing_mech(mech)); } if let Some(passing_doc) = passing.xml_passing_doc() { if let Some(expr) = passing_doc.expr() { doc = doc - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments(passing_doc.syntax())) .append(build_expr(expr)); } if let Some(mech) = passing_doc.xml_passing_mech() { doc = doc - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments(mech.syntax())) .append(build_xml_passing_mech(mech)); } @@ -1970,18 +1990,22 @@ fn build_xml_exists_fn<'a>(xml_exists_fn: ast::XmlExistsFn) -> Doc<'a> { if let Some(r_paren) = xml_exists_fn.r_paren_token() { doc = doc.append(comments_before(r_paren)); } - doc.append(Doc::text(")")) + doc.append(Doc::text(")")).nest(2).group() } fn build_xml_forest_fn<'a>(xml_forest_fn: ast::XmlForestFn) -> Doc<'a> { - Doc::text("xmlforest").append( - xml_forest_fn - .expr_as_element_tag_list() - .map(|list| { - comments_before(list.syntax().clone()).append(build_expr_as_element_tag_list(list)) - }) - .unwrap_or_else(Doc::nil), - ) + Doc::text("xmlforest") + .append( + xml_forest_fn + .expr_as_element_tag_list() + .map(|list| { + comments_before(list.syntax().clone()) + .append(build_expr_as_element_tag_list(list)) + }) + .unwrap_or_else(Doc::nil), + ) + .nest(2) + .group() } fn build_expr_as_element_tag_list<'a>(list: ast::ExprAsElementTagList) -> Doc<'a> { @@ -2017,7 +2041,7 @@ fn build_expr_as_element_tag_list<'a>(list: ast::ExprAsElementTagList) -> Doc<'a if let Some(r_paren) = list.r_paren_token() { doc = doc.append(comments_before(r_paren)); } - doc.append(Doc::text(")")) + doc.append(Doc::text(")")).group() } fn build_xml_parse_fn<'a>(xml_parse_fn: ast::XmlParseFn) -> Doc<'a> { @@ -2034,13 +2058,13 @@ fn build_xml_parse_fn<'a>(xml_parse_fn: ast::XmlParseFn) -> Doc<'a> { } if let Some(expr) = xml_parse_fn.expr() { doc = doc - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments(expr.syntax())) .append(build_expr(expr)); } if let Some(whitespace) = xml_parse_fn.xml_whitespace() { doc = doc - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments(whitespace.syntax())) .append(build_xml_whitespace(whitespace)); } @@ -2048,7 +2072,7 @@ fn build_xml_parse_fn<'a>(xml_parse_fn: ast::XmlParseFn) -> Doc<'a> { if let Some(r_paren) = xml_parse_fn.r_paren_token() { doc = doc.append(comments_before(r_paren)); } - doc.append(Doc::text(")")) + doc.append(Doc::text(")")).nest(2).group() } fn build_xml_pi_fn<'a>(xml_pi_fn: ast::XmlPiFn) -> Doc<'a> { @@ -2075,7 +2099,7 @@ fn build_xml_pi_fn<'a>(xml_pi_fn: ast::XmlPiFn) -> Doc<'a> { } doc = doc .append(Doc::text(",")) - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments(expr.syntax())) .append(build_expr(expr)); } @@ -2083,7 +2107,7 @@ fn build_xml_pi_fn<'a>(xml_pi_fn: ast::XmlPiFn) -> Doc<'a> { if let Some(r_paren) = xml_pi_fn.r_paren_token() { doc = doc.append(comments_before(r_paren)); } - doc.append(Doc::text(")")) + doc.append(Doc::text(")")).nest(2).group() } fn build_xml_root_fn<'a>(xml_root_fn: ast::XmlRootFn) -> Doc<'a> { @@ -2101,7 +2125,7 @@ fn build_xml_root_fn<'a>(xml_root_fn: ast::XmlRootFn) -> Doc<'a> { if let Some(comma) = xml_root_fn.comma_token() { doc = doc.append(comments_before(comma)); } - doc = doc.append(Doc::text(",")).append(Doc::space()); + doc = doc.append(Doc::text(",")).append(Doc::line_or_space()); if let Some(version) = xml_root_fn.xml_root_version() { doc = doc .append(leading_comments(version.syntax())) @@ -2116,7 +2140,7 @@ fn build_xml_root_fn<'a>(xml_root_fn: ast::XmlRootFn) -> Doc<'a> { if let Some(r_paren) = xml_root_fn.r_paren_token() { doc = doc.append(comments_before(r_paren)); } - doc.append(Doc::text(")")) + doc.append(Doc::text(")")).nest(2).group() } fn build_xml_root_version<'a>(version: ast::XmlRootVersion) -> Doc<'a> { @@ -2182,7 +2206,7 @@ fn build_xml_standalone<'a>(standalone: ast::XmlStandalone) -> Doc<'a> { }; let mut doc = comma.map(comments_before).unwrap_or_else(Doc::nil); - doc = doc.append(Doc::text(",")).append(Doc::space()); + doc = doc.append(Doc::text(",")).append(Doc::line_or_space()); if let Some(token) = standalone_token { doc = doc .append(leading_comments_token(&token)) @@ -2223,7 +2247,7 @@ fn build_xml_serialize_fn<'a>(xml_serialize_fn: ast::XmlSerializeFn) -> Doc<'a> } if let Some(as_token) = xml_serialize_fn.as_token() { doc = doc - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments_token(&as_token)) .append(Doc::text("as")); } @@ -2235,7 +2259,7 @@ fn build_xml_serialize_fn<'a>(xml_serialize_fn: ast::XmlSerializeFn) -> Doc<'a> } if let Some(indent) = xml_serialize_fn.xml_indent() { doc = doc - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments(indent.syntax())) .append(build_xml_indent(indent)); } @@ -2243,7 +2267,7 @@ fn build_xml_serialize_fn<'a>(xml_serialize_fn: ast::XmlSerializeFn) -> Doc<'a> if let Some(r_paren) = xml_serialize_fn.r_paren_token() { doc = doc.append(comments_before(r_paren)); } - doc.append(Doc::text(")")) + doc.append(Doc::text(")")).nest(2).group() } fn build_xml_document_or_content<'a>(kind: ast::XmlDocumentOrContent) -> Doc<'a> { @@ -2340,7 +2364,7 @@ fn build_json_object_fn<'a>(json_object_fn: ast::JsonObjectFn) -> Doc<'a> { if let Some(null_clause) = json_object_fn.json_null_clause() { if has_content { - doc = doc.append(Doc::space()); + doc = doc.append(Doc::line_or_space()); } doc = doc .append(leading_comments(null_clause.syntax())) @@ -2349,7 +2373,7 @@ fn build_json_object_fn<'a>(json_object_fn: ast::JsonObjectFn) -> Doc<'a> { } if let Some(unique) = json_object_fn.json_keys_unique_clause() { if has_content { - doc = doc.append(Doc::space()); + doc = doc.append(Doc::line_or_space()); } doc = doc .append(leading_comments(unique.syntax())) @@ -2358,7 +2382,7 @@ fn build_json_object_fn<'a>(json_object_fn: ast::JsonObjectFn) -> Doc<'a> { } if let Some(returning) = json_object_fn.json_returning_clause() { if has_content { - doc = doc.append(Doc::space()); + doc = doc.append(Doc::line_or_space()); } doc = doc .append(leading_comments(returning.syntax())) @@ -2367,7 +2391,7 @@ fn build_json_object_fn<'a>(json_object_fn: ast::JsonObjectFn) -> Doc<'a> { if let Some(r_paren) = json_object_fn.r_paren_token() { doc = doc.append(comments_before(r_paren)); } - doc.append(Doc::text(")")) + doc.append(Doc::text(")")).nest(2).group() } fn build_json_object_agg_fn<'a>(json_object_agg_fn: ast::JsonObjectAggFn) -> Doc<'a> { @@ -2384,26 +2408,26 @@ fn build_json_object_agg_fn<'a>(json_object_agg_fn: ast::JsonObjectAggFn) -> Doc } if let Some(null_clause) = json_object_agg_fn.json_null_clause() { doc = doc - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments(null_clause.syntax())) .append(build_json_null_clause(null_clause)); } if let Some(unique) = json_object_agg_fn.json_keys_unique_clause() { doc = doc - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments(unique.syntax())) .append(build_json_keys_unique_clause(unique)); } if let Some(returning) = json_object_agg_fn.json_returning_clause() { doc = doc - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments(returning.syntax())) .append(build_json_returning_clause(returning)); } if let Some(r_paren) = json_object_agg_fn.r_paren_token() { doc = doc.append(comments_before(r_paren)); } - doc.append(Doc::text(")")) + doc.append(Doc::text(")")).nest(2).group() } fn build_json_key_value<'a>(key_value: ast::JsonKeyValue) -> Doc<'a> { @@ -2439,20 +2463,20 @@ fn build_json_fn<'a>(json_fn: ast::JsonFn) -> Doc<'a> { } if let Some(format) = json_fn.json_format_clause() { doc = doc - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments(format.syntax())) .append(build_json_format_clause(format)); } if let Some(unique) = json_fn.json_keys_unique_clause() { doc = doc - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments(unique.syntax())) .append(build_json_keys_unique_clause(unique)); } if let Some(r_paren) = json_fn.r_paren_token() { doc = doc.append(comments_before(r_paren)); } - doc.append(Doc::text(")")) + doc.append(Doc::text(")")).nest(2).group() } fn build_json_scalar_fn<'a>(json_scalar_fn: ast::JsonScalarFn) -> Doc<'a> { @@ -2479,20 +2503,20 @@ fn build_json_serialize_fn<'a>(json_serialize_fn: ast::JsonSerializeFn) -> Doc<' } if let Some(format) = json_serialize_fn.json_format_clause() { doc = doc - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments(format.syntax())) .append(build_json_format_clause(format)); } if let Some(returning) = json_serialize_fn.json_returning_clause() { doc = doc - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments(returning.syntax())) .append(build_json_returning_clause(returning)); } if let Some(r_paren) = json_serialize_fn.r_paren_token() { doc = doc.append(comments_before(r_paren)); } - doc.append(Doc::text(")")) + doc.append(Doc::text(")")).nest(2).group() } fn build_json_query_fn<'a>(json_query_fn: ast::JsonQueryFn) -> Doc<'a> { @@ -2506,44 +2530,44 @@ fn build_json_query_fn<'a>(json_query_fn: ast::JsonQueryFn) -> Doc<'a> { ); if let Some(passing) = json_query_fn.json_passing_clause() { doc = doc - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments(passing.syntax())) .append(build_json_passing_clause(passing)); } if let Some(returning) = json_query_fn.json_returning_clause() { doc = doc - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments(returning.syntax())) .append(build_json_returning_clause(returning)); } if let Some(wrapper) = json_query_fn.json_wrapper_behavior_clause() { doc = doc - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments(wrapper.syntax())) .append(build_json_wrapper_behavior_clause(wrapper)); } if let Some(quotes) = json_query_fn.json_quotes_clause() { doc = doc - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments(quotes.syntax())) .append(build_json_quotes_clause(quotes)); } if let Some(on_empty) = json_query_fn.json_on_empty_clause() { doc = doc - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments(on_empty.syntax())) .append(build_json_on_empty_clause(on_empty)); } if let Some(on_error) = json_query_fn.json_on_error_clause() { doc = doc - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments(on_error.syntax())) .append(build_json_on_error_clause(on_error)); } if let Some(r_paren) = json_query_fn.r_paren_token() { doc = doc.append(comments_before(r_paren)); } - doc.append(Doc::text(")")) + doc.append(Doc::text(")")).nest(2).group() } fn build_json_value_fn<'a>(json_value_fn: ast::JsonValueFn) -> Doc<'a> { @@ -2557,32 +2581,32 @@ fn build_json_value_fn<'a>(json_value_fn: ast::JsonValueFn) -> Doc<'a> { ); if let Some(passing) = json_value_fn.json_passing_clause() { doc = doc - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments(passing.syntax())) .append(build_json_passing_clause(passing)); } if let Some(returning) = json_value_fn.json_returning_clause() { doc = doc - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments(returning.syntax())) .append(build_json_returning_clause(returning)); } if let Some(on_empty) = json_value_fn.json_on_empty_clause() { doc = doc - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments(on_empty.syntax())) .append(build_json_on_empty_clause(on_empty)); } if let Some(on_error) = json_value_fn.json_on_error_clause() { doc = doc - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments(on_error.syntax())) .append(build_json_on_error_clause(on_error)); } if let Some(r_paren) = json_value_fn.r_paren_token() { doc = doc.append(comments_before(r_paren)); } - doc.append(Doc::text(")")) + doc.append(Doc::text(")")).nest(2).group() } fn build_json_document_path_fn<'a>( @@ -2605,7 +2629,7 @@ fn build_json_document_path_fn<'a>( } if let Some(format) = format { doc = doc - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments(format.syntax())) .append(build_json_format_clause(format)); } @@ -2613,7 +2637,7 @@ fn build_json_document_path_fn<'a>( doc = doc .append(comments_before(comma)) .append(Doc::text(",")) - .append(Doc::space()); + .append(Doc::line_or_space()); } if let Some(path) = path { doc = doc @@ -2637,7 +2661,7 @@ fn build_json_exists_fn<'a>(json_exists_fn: ast::JsonExistsFn) -> Doc<'a> { } if let Some(format) = json_exists_fn.json_format_clause() { doc = doc - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments(format.syntax())) .append(build_json_format_clause(format)); } @@ -2645,7 +2669,7 @@ fn build_json_exists_fn<'a>(json_exists_fn: ast::JsonExistsFn) -> Doc<'a> { doc = doc .append(comments_before(comma)) .append(Doc::text(",")) - .append(Doc::space()); + .append(Doc::line_or_space()); } if let Some(path) = json_exists_fn.path() { doc = doc @@ -2654,20 +2678,20 @@ fn build_json_exists_fn<'a>(json_exists_fn: ast::JsonExistsFn) -> Doc<'a> { } if let Some(passing) = json_exists_fn.json_passing_clause() { doc = doc - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments(passing.syntax())) .append(build_json_passing_clause(passing)); } if let Some(on_error) = json_exists_fn.json_on_error_clause() { doc = doc - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments(on_error.syntax())) .append(build_json_on_error_clause(on_error)); } if let Some(r_paren) = json_exists_fn.r_paren_token() { doc = doc.append(comments_before(r_paren)); } - doc.append(Doc::text(")")) + doc.append(Doc::text(")")).nest(2).group() } fn build_json_passing_clause<'a>(passing: ast::JsonPassingClause) -> Doc<'a> { @@ -2683,13 +2707,13 @@ fn build_json_passing_clause<'a>(passing: ast::JsonPassingClause) -> Doc<'a> { doc = doc .append(trailing_comments(&previous_syntax)) .append(Doc::text(",")) - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments(arg.syntax())) .append(build_json_passing_arg(arg.clone())); previous_syntax = arg.syntax().clone(); } } - doc + doc.nest(2).group() } fn build_json_passing_arg<'a>(arg: ast::JsonPassingArg) -> Doc<'a> { @@ -2848,20 +2872,20 @@ fn build_json_array_fn<'a>(json_array_fn: ast::JsonArrayFn) -> Doc<'a> { if let Some(null_clause) = json_array_fn.json_null_clause() { doc = doc - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments(null_clause.syntax())) .append(build_json_null_clause(null_clause)); } if let Some(returning) = json_array_fn.json_returning_clause() { doc = doc - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments(returning.syntax())) .append(build_json_returning_clause(returning)); } if let Some(r_paren) = json_array_fn.r_paren_token() { doc = doc.append(comments_before(r_paren)); } - doc.append(Doc::text(")")) + doc.append(Doc::text(")")).nest(2).group() } fn build_comma_separated_docs<'a>( @@ -2873,23 +2897,23 @@ fn build_comma_separated_docs<'a>( docs.push( trailing_comments(&previous_syntax) .append(Doc::text(",")) - .append(Doc::space()) + .append(Doc::line_or_space()) .append(item), ); previous_syntax = syntax; } - Some(Doc::list(docs)) + Some(Doc::list(docs).group()) } fn build_json_expr_format<'a>(value: ast::JsonExprFormat) -> Doc<'a> { let mut doc = value.expr().map(build_expr).unwrap_or_else(Doc::nil); if let Some(format) = value.json_format_clause() { doc = doc - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments(format.syntax())) .append(build_json_format_clause(format)); } - doc + doc.group() } fn build_json_select_format<'a>(select: ast::JsonSelectFormat) -> Doc<'a> { @@ -2902,11 +2926,11 @@ fn build_json_select_format<'a>(select: ast::JsonSelectFormat) -> Doc<'a> { .unwrap_or_else(Doc::nil); if let Some(format) = select.json_format_clause() { doc = doc - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments(format.syntax())) .append(build_json_format_clause(format)); } - doc + doc.group() } fn build_json_array_agg_fn<'a>(json_array_agg_fn: ast::JsonArrayAggFn) -> Doc<'a> { @@ -2923,37 +2947,37 @@ fn build_json_array_agg_fn<'a>(json_array_agg_fn: ast::JsonArrayAggFn) -> Doc<'a } if let Some(order_by) = json_array_agg_fn.order_by_clause() { doc = doc - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments(order_by.syntax())) .append(build_order_by_clause(order_by)); } if let Some(null_clause) = json_array_agg_fn.json_null_clause() { doc = doc - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments(null_clause.syntax())) .append(build_json_null_clause(null_clause)); } if let Some(returning) = json_array_agg_fn.json_returning_clause() { doc = doc - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments(returning.syntax())) .append(build_json_returning_clause(returning)); } if let Some(r_paren) = json_array_agg_fn.r_paren_token() { doc = doc.append(comments_before(r_paren)); } - doc.append(Doc::text(")")) + doc.append(Doc::text(")")).nest(2).group() } fn build_json_value_expr<'a>(value: ast::JsonValueExpr) -> Doc<'a> { let mut doc = value.expr().map(build_expr).unwrap_or_else(Doc::nil); if let Some(format) = value.json_format_clause() { doc = doc - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments(format.syntax())) .append(build_json_format_clause(format)); } - doc + doc.group() } fn build_json_format_clause<'a>(format: ast::JsonFormatClause) -> Doc<'a> { @@ -2966,11 +2990,11 @@ fn build_json_format_clause<'a>(format: ast::JsonFormatClause) -> Doc<'a> { } if let Some(encoding) = format.json_encoding_clause() { doc = doc - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments(encoding.syntax())) .append(build_json_encoding_clause(encoding)); } - doc + doc.group() } fn build_json_encoding_clause<'a>(clause: ast::JsonEncodingClause) -> Doc<'a> { @@ -3020,11 +3044,11 @@ fn build_json_returning_clause<'a>(returning: ast::JsonReturningClause) -> Doc<' } if let Some(format) = returning.json_format_clause() { doc = doc - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments(format.syntax())) .append(build_json_format_clause(format)); } - doc + doc.nest(2).group() } fn build_overlay_fn<'a>(overlay_fn: ast::OverlayFn) -> Doc<'a> { @@ -3211,10 +3235,17 @@ fn build_comma_separated_exprs<'a>(exprs: impl Iterator) -> Op if exprs.is_empty() { None } else { - Some(Doc::list( - Itertools::intersperse(exprs.into_iter(), Doc::text(",").append(Doc::space())) + Some( + Doc::list( + Itertools::intersperse( + exprs.into_iter(), + Doc::text(",").append(Doc::line_or_space()), + ) .collect(), - )) + ) + .nest(2) + .group(), + ) } } @@ -3396,12 +3427,19 @@ fn build_call_arg_list<'a>(arg_list: ast::ArgList) -> Doc<'a> { if has_quantifier { doc = doc.append(Doc::space()); } - doc = doc.append(Doc::list( - Itertools::intersperse(args.into_iter(), Doc::text(",").append(Doc::space())).collect(), - )); + doc = doc.append( + Doc::list( + Itertools::intersperse( + args.into_iter(), + Doc::text(",").append(Doc::line_or_space()), + ) + .collect(), + ) + .nest(2), + ); } - doc.append(Doc::text(")")) + doc.append(Doc::text(")")).group() } fn build_call_arg<'a>(arg: ast::Arg) -> Doc<'a> { @@ -3856,7 +3894,7 @@ fn build_over_window_spec<'a>(over_window_spec: ast::OverWindowSpec) -> Doc<'a> if let Some(r_paren) = over_window_spec.r_paren_token() { doc = doc.append(comments_before(r_paren)); } - doc.append(Doc::text(")")) + doc.append(Doc::text(")")).group() } fn build_window_spec<'a>(window_spec: ast::WindowSpec) -> Doc<'a> { @@ -3876,7 +3914,9 @@ fn build_window_spec<'a>(window_spec: ast::WindowSpec) -> Doc<'a> { parts.push(leading_comments(frame.syntax()).append(build_frame_clause(frame))); } - Doc::list(Itertools::intersperse(parts.into_iter(), Doc::space()).collect()) + Doc::list(Itertools::intersperse(parts.into_iter(), Doc::line_or_space()).collect()) + .nest(2) + .group() } fn build_partition_by_clause<'a>(partition_by: ast::PartitionByClause) -> Doc<'a> { @@ -3906,11 +3946,11 @@ fn build_frame_clause<'a>(frame: ast::FrameClause) -> Doc<'a> { } if let Some(exclude) = frame.frame_exclude() { doc = doc - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments(exclude.syntax())) .append(build_frame_exclude(exclude)); } - doc + doc.nest(2).group() } fn build_frame_extent<'a>(extent: ast::FrameExtent) -> Doc<'a> { @@ -3925,7 +3965,7 @@ fn build_frame_extent<'a>(extent: ast::FrameExtent) -> Doc<'a> { } if let Some(and_token) = between.and_token() { doc = doc - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments_token(&and_token)) .append(Doc::text("and")); } @@ -3935,7 +3975,7 @@ fn build_frame_extent<'a>(extent: ast::FrameExtent) -> Doc<'a> { .append(leading_comments(end.syntax())) .append(build_frame_bound(end)); } - doc + doc.nest(2).group() } ast::FrameExtent::FrameBound(bound) => build_frame_bound(bound), } diff --git a/crates/squawk_fmt/tests/after/from.snap b/crates/squawk_fmt/tests/after/from.snap index b6c255c4..49f91538 100644 --- a/crates/squawk_fmt/tests/after/from.snap +++ b/crates/squawk_fmt/tests/after/from.snap @@ -6,12 +6,10 @@ select * from foo; select * from public.foo as f, bar b; select * from foo as f(id, display_name); select * from foo f(id int, display_name text collate "C"); -select - * +select * from foo /* before alias */ as /* before alias name */ f /* before open paren */(/* after open paren */ id /* before comma */, /* after comma */ display_name /* before close paren */); select * from users tablesample bernoulli(10) repeatable(42); -select - * +select * /* before from */ from /* before item */ only /* before relation */ public /* before dot */./* before table */ foo /* before star */ * /* before alias */ as /* before alias name */ f /* before item comma */, /* before second item */ other /* before second alias */ o; diff --git a/crates/squawk_fmt/tests/after/graph_table.snap b/crates/squawk_fmt/tests/after/graph_table.snap index ad5e5f00..b2c00402 100644 --- a/crates/squawk_fmt/tests/after/graph_table.snap +++ b/crates/squawk_fmt/tests/after/graph_table.snap @@ -4,18 +4,70 @@ input_file: crates/squawk_fmt/tests/before/graph_table.sql --- select * from graph_table(g match (a) columns (a)); -select - * -from lateral graph_table(public.g match (a is person)-[e is knows where e.weight > 1]->(b) where a.active columns (a.name as person_name, b.name)) as matches; +select * +from lateral graph_table( + public.g + match + (a is person)-[e is knows where e.weight > 1]->(b) + where a.active + columns (a.name as person_name, b.name) + ) as matches; -select - * -from graph_table(g match (a)<-[left_edge]-(b), (c)-[any_edge]-(d), (e)<-(f), (h)->(i), (j)-(k), ((x)->(y) where x.active) columns (a)); +select * +from graph_table( + g + match + (a)<-[left_edge]-(b), + (c)-[any_edge]-(d), + (e)<-(f), + (h)->(i), + (j)-(k), + ((x)->(y) where x.active) + columns (a) + ); -select - * +select * from graph_table(g match (a)->{1}(b), (c)-{, 3}(d), (e)-{2, 4}(f) columns (a)); -select - * -from /* before graph table */ graph_table /* before outer opening paren */(/* before graph */ public /* before graph dot */./* before graph name */ g /* before match */ match /* before first vertex opening paren */ (/* before first variable */ a /* before is */ is /* before first label */ person /* before vertex where */ where /* before vertex expression */ a.active /* before first vertex closing paren */)/* before first edge minus */ - /* before edge opening bracket */[/* before edge variable */ e /* before edge is */ is /* before edge label */ knows /* before edge where */ where /* before edge expression */ e.active /* before edge closing bracket */] /* before edge ending minus */- /* before right angle */>/* before second vertex */ (b)/* before qualifier opening curly */ {/* before qualifier min */ 1 /* before qualifier comma */, /* before qualifier max */ 3 /* before qualifier closing curly */} /* before pattern comma */, /* before second pattern */ /* before left angle */ < /* before left minus */- /* before left opening bracket */[left_edge /* before left closing bracket */] /* before left ending minus */-(c), (d)/* before any edge */ - /* before any opening bracket */[any_edge /* before any closing bracket */] /* before any ending minus */-(e), /* before nested opening paren */ (/* before nested pattern */ (x)/* before simple edge */ - /* before simple right angle */>(y) /* before nested where */ where /* before nested expression */ x.active /* before nested closing paren */) /* before graph where */ where /* before graph expression */ b.active /* before columns */ columns /* before columns opening paren */(/* before first column */ a.name /* before column as */ as /* before column name */ source /* before column comma */, /* before second column */ b.name /* before columns closing paren */) /* before outer closing paren */) /* after graph table */ as /* before alias */ result; +select * +from graph_table( + a_very_long_property_graph_name + match + (a_very_long_vertex_variable + is a_very_long_vertex_label + where a_very_long_vertex_filter_expression) + -[a_very_long_edge_variable + is a_very_long_edge_label + where a_very_long_edge_filter_expression]-> + (a_second_very_long_vertex_variable is a_second_very_long_vertex_label), + (a_third_very_long_vertex_variable) + -[a_second_very_long_edge_variable]- + (a_fourth_very_long_vertex_variable) + where a_very_long_graph_filter_expression + columns (a_very_long_vertex_variable.long_property_name as first_long_output_column_name, + a_second_very_long_vertex_variable.other_long_property_name as second_output_column_name) + ); + +select * +from /* before graph table */ graph_table /* before outer opening paren */( + /* before graph */ public /* before graph dot */./* before graph name */ g + /* before match */ match + /* before first vertex opening paren */ (/* before first variable */ a + /* before is */ is /* before first label */ person + /* before vertex where */ where /* before vertex expression */ a.active /* before first vertex closing paren */) + /* before first edge minus */ - /* before edge opening bracket */[/* before edge variable */ e + /* before edge is */ is /* before edge label */ knows + /* before edge where */ where /* before edge expression */ e.active /* before edge closing bracket */] /* before edge ending minus */- /* before right angle */> + /* before second vertex */ (b)/* before qualifier opening curly */ {/* before qualifier min */ 1 /* before qualifier comma */, /* before qualifier max */ 3 /* before qualifier closing curly */} /* before pattern comma */, + /* before second pattern */ /* before left angle */ < /* before left minus */- /* before left opening bracket */[left_edge /* before left closing bracket */] /* before left ending minus */- + (c), + (d) + /* before any edge */ - /* before any opening bracket */[any_edge /* before any closing bracket */] /* before any ending minus */- + (e), + /* before nested opening paren */ (/* before nested pattern */ (x) + /* before simple edge */ - /* before simple right angle */> + (y) /* before nested where */ where /* before nested expression */ x.active /* before nested closing paren */) + /* before graph where */ where /* before graph expression */ b.active + /* before columns */ columns /* before columns opening paren */(/* before first column */ a.name /* before column as */ as /* before column name */ source /* before column comma */, + /* before second column */ b.name /* before columns closing paren */) /* before outer closing paren */ + ) /* after graph table */ as /* before alias */ result; diff --git a/crates/squawk_fmt/tests/after/group_by.snap b/crates/squawk_fmt/tests/after/group_by.snap index ec389997..7ae7a8da 100644 --- a/crates/squawk_fmt/tests/after/group_by.snap +++ b/crates/squawk_fmt/tests/after/group_by.snap @@ -5,11 +5,33 @@ input_file: crates/squawk_fmt/tests/before/group_by.sql select 1 group by 1, foo + 2; select 1 group by all rollup(1, 2), cube(3, 4); select 1 group by distinct grouping sets((), (1, 2), rollup(3), cube(4)); +select + 1 +group by grouping sets( + (first_very_long_grouping_expression, second_very_long_grouping_expression), + rollup( + third_very_long_grouping_expression, + fourth_very_long_grouping_expression + ), + cube( + fifth_very_long_grouping_expression, + sixth_very_long_grouping_expression + ) + ); select 1 -group /* before by */ by /* before distinct */ distinct /* before grouping */ grouping /* before sets */ sets /* before outer paren */(/* before rollup */ rollup /* before rollup paren */(/* before first expression */ 1 /* before expression comma */, - /* before second expression */ 2 /* before rollup close */) /* before group-by comma */, - /* before cube */ cube /* before cube paren */(/* before cube expression */ 3 /* before cube close */) /* before second group-by comma */, - /* before nested grouping */ grouping /* before nested sets */ sets /* before nested paren */(/* before empty tuple */ () /* before nested comma */, - /* before grouping expression */ (4 /* before tuple comma */, /* before tuple expression */ 5 /* before tuple close */) /* before nested close */) /* before outer close */)/* before semicolon */; +group /* before by */ by /* before distinct */ distinct /* before grouping */ grouping /* before sets */ sets /* before outer paren */( + /* before rollup */ rollup /* before rollup paren */( + /* before first expression */ 1 /* before expression comma */, + /* before second expression */ 2 /* before rollup close */ + ) /* before group-by comma */, + /* before cube */ cube /* before cube paren */( + /* before cube expression */ 3 /* before cube close */ + ) /* before second group-by comma */, + /* before nested grouping */ grouping /* before nested sets */ sets /* before nested paren */( + /* before empty tuple */ () /* before nested comma */, + /* before grouping expression */ (4 /* before tuple comma */, + /* before tuple expression */ 5 /* before tuple close */) /* before nested close */ + ) /* before outer close */ + )/* before semicolon */; diff --git a/crates/squawk_fmt/tests/after/select_expr.snap b/crates/squawk_fmt/tests/after/select_expr.snap index 2a2c5b93..4f44f620 100644 --- a/crates/squawk_fmt/tests/after/select_expr.snap +++ b/crates/squawk_fmt/tests/after/select_expr.snap @@ -71,14 +71,19 @@ select overlay(), overlay('Txxxxas', 'hom', 2, count => 4), /* before overlay */ overlay /* before opening paren */(/* before string */ 'Txxxxas' /* before placing */ placing /* before replacement */ 'hom' /* before from */ from /* before start */ 2 /* before for */ for /* before count */ 4 /* before closing paren */) /* after overlay */, - overlay /* before opening paren */(/* before string */ 'Txxxxas' /* before comma */, /* before replacement */ 'hom' /* before comma */, /* before start */ 2 /* before comma */, /* before name */ count /* before arrow */ => /* before count */ 4 /* before closing paren */), + overlay /* before opening paren */(/* before string */ 'Txxxxas' /* before comma */, + /* before replacement */ 'hom' /* before comma */, + /* before start */ 2 /* before comma */, + /* before name */ count /* before arrow */ => /* before count */ 4 /* before closing paren */), substring('Thomas' from 2 for 3), substring('Thomas' for 3 from 2), substring('Thomas' from 2), substring('Thomas' for 3), substring('Thomas' similar '%#"o_a#"_' escape '#'), substring('Thomas', 2, 3), - substring('Thomas' /* before comma */, /* before start */ 2, /* before count */ 3), + substring('Thomas' /* before comma */, + /* before start */ 2, + /* before count */ 3), /* before substring */ substring /* before opening paren */(/* before string */ 'Thomas' /* before from */ from /* before start */ 2 /* before for */ for /* before count */ 3 /* before closing paren */) /* after substring */, trim(' foo '), trim(both 'x' from 'xfoox'), @@ -88,6 +93,9 @@ select trim(foo, bar), /* before trim */ trim /* before opening paren */(/* before side */ both /* before trim char */ 'x' /* before from */ from /* before string */ 'xfoox' /* before closing paren */) /* after trim */, date_trunc('month', now()), + a_very_long_function_name(first_very_long_argument_name, + second_very_long_argument_name, + third_very_long_argument_name), foo(), foo(*), foo(all 1, 2), @@ -97,22 +105,63 @@ select foo(variadic xs), foo(/* before variadic */ variadic /* after variadic */ xs), foo(a => 1, b := 2), - foo(/* before name */ a /* before arrow */ => /* before value */ 1 /* before comma */, /* before arg */ b /* before assign */ := /* before value 2 */ 2), + foo(/* before name */ a /* before arrow */ => /* before value */ 1 /* before comma */, + /* before arg */ b /* before assign */ := /* before value 2 */ 2), array_agg(x order by y desc nulls last, z asc), foo(a => x order by y), array_agg(x order by y using > nulls first), array_agg(x /* before order */ order /* before by */ by /* before first */ y /* before desc */ desc /* before nulls */ nulls /* before last */ last /* before comma */, /* before second */ z /* before asc */ asc), json_arrayagg(v), - json_arrayagg(v format json order by sort_col desc null on null returning jsonb format json), - json_arrayagg /* before opening paren */(/* before value */ v /* before value format */ format /* before value json */ json /* before encoding */ encoding /* before encoding name */ utf8 /* before order */ order /* before by */ by /* before sort */ x /* before absent */ absent /* before on */ on /* before null */ null /* before returning */ returning /* before type */ jsonb /* before returning format */ format /* before returning json */ json /* before closing paren */), + json_arrayagg(v format json + order by sort_col desc + null on null + returning jsonb format json), + json_arrayagg(a_very_long_json_arrayagg_value format json encoding utf8 + order by a_very_long_json_arrayagg_sort_expression desc + absent on null + returning a_very_long_json_arrayagg_return_type format json), + json_arrayagg /* before opening paren */(/* before value */ v + /* before value format */ format /* before value json */ json + /* before encoding */ encoding /* before encoding name */ utf8 + /* before order */ order /* before by */ by /* before sort */ x + /* before absent */ absent /* before on */ on /* before null */ null + /* before returning */ returning /* before type */ jsonb + /* before returning format */ format /* before returning json */ json /* before closing paren */), json_array(), json_array(1, 2 format json returning jsonb), - json_array /* before opening paren */(/* before first */ 1 /* before comma */, /* before second */ 2 /* before format */ format /* before json */ json /* before encoding */ encoding /* before encoding name */ utf8 /* before returning */ returning /* before type */ jsonb /* before returning format */ format /* before returning json */ json /* before closing paren */), - json_array(/* before select */ select - /* before target */ x /* before format */ format /* before json */ json /* before absent */ absent /* before on */ on /* before null */ null /* before returning */ returning /* before type */ jsonb /* before closing paren */), + json_array(first_very_long_json_array_expression, + second_very_long_json_array_expression, + third_very_long_json_array_expression + returning a_very_long_json_array_return_type), + json_array(select + a_very_long_json_select_expression_that_forces_the_json_select_format_node_to_wrap + format json + absent on null + returning a_very_long_json_select_return_type), + json_array /* before opening paren */(/* before first */ 1 /* before comma */, + /* before second */ 2 + /* before format */ format /* before json */ json + /* before encoding */ encoding /* before encoding name */ utf8 + /* before returning */ returning /* before type */ jsonb + /* before returning format */ format /* before returning json */ json /* before closing paren */), + json_array(/* before select */ select /* before target */ x + /* before format */ format /* before json */ json + /* before absent */ absent /* before on */ on /* before null */ null + /* before returning */ returning /* before type */ jsonb /* before closing paren */), json_exists(doc, '$.a'), json_exists(doc, '$.a' passing x as foo, y as bar true on error), - json_exists /* before opening paren */(/* before document */ doc /* before format */ format /* before json */ json /* before encoding */ encoding /* before encoding name */ utf8 /* before comma */, /* before path */ '$.a' /* before passing */ passing /* before arg */ x /* before as */ as /* before name */ foo /* before arg comma */, /* before second arg */ y /* before second as */ as /* before second name */ bar /* before behavior */ default /* before default */ false /* before on */ on /* before error */ error /* before closing paren */), + json_exists(a_very_long_json_exists_document + format json encoding utf8, + a_very_long_json_exists_path + passing a_very_long_json_exists_argument as a_very_long_json_exists_name + false on error), + json_exists /* before opening paren */(/* before document */ doc + /* before format */ format /* before json */ json + /* before encoding */ encoding /* before encoding name */ utf8 /* before comma */, + /* before path */ '$.a' + /* before passing */ passing /* before arg */ x /* before as */ as /* before name */ foo /* before arg comma */, + /* before second arg */ y /* before second as */ as /* before second name */ bar + /* before behavior */ default /* before default */ false /* before on */ on /* before error */ error /* before closing paren */), json_exists(doc, '$' error on error), json_exists(doc, '$' null on error), json_exists(doc, '$' false on error), @@ -124,28 +173,104 @@ select json_scalar /* before opening paren */(/* before expression */ 1 /* before closing paren */), json_serialize(doc), json_serialize(doc format json returning text format json), - json_serialize /* before opening paren */(/* before document */ doc /* before format */ format /* before json */ json /* before returning */ returning /* before type */ text /* before returning format */ format /* before returning json */ json /* before closing paren */), + json_serialize(a_very_long_json_serialize_document + format json + encoding a_very_long_json_encoding_name_that_forces_the_json_format_clause_to_wrap + returning a_very_long_json_serialize_return_type_name_that_forces_returning_to_wrap + format json), + json_serialize /* before opening paren */(/* before document */ doc + /* before format */ format /* before json */ json + /* before returning */ returning /* before type */ text + /* before returning format */ format /* before returning json */ json /* before closing paren */), json_query(doc, '$.a'), json_query(doc, '$.a' with wrapper), json_query(doc, '$.a' with unconditional array wrapper omit quotes), json_query(doc, '$.a' without array wrapper), - json_query /* before opening paren */(/* before document */ doc /* before format */ format /* before json */ json /* before encoding */ encoding /* before encoding name */ utf8 /* before comma */, /* before path */ '$.a' /* before passing */ passing /* before arg */ x /* before as */ as /* before name */ foo /* before returning */ returning /* before type */ text /* before returning format */ format /* before returning json */ json /* before wrapper */ with /* before conditional */ conditional /* before array */ array /* before wrapper keyword */ wrapper /* before keep */ keep /* before quotes */ quotes /* before on scalar */ on /* before scalar */ scalar /* before string */ string /* before empty behavior */ default /* before default */ 'empty' /* before empty on */ on /* before empty */ empty /* before error behavior */ empty /* before object */ object /* before error on */ on /* before error */ error /* before closing paren */), + json_query(a_very_long_json_query_document + format json encoding utf8, + a_very_long_json_query_path + passing a_very_long_json_query_argument as a_very_long_json_query_name + returning a_very_long_json_query_return_type format json + with unconditional array wrapper + keep quotes on scalar string + default a_very_long_json_query_empty_value on empty + error on error), + json_query /* before opening paren */(/* before document */ doc + /* before format */ format /* before json */ json + /* before encoding */ encoding /* before encoding name */ utf8 /* before comma */, + /* before path */ '$.a' + /* before passing */ passing /* before arg */ x /* before as */ as /* before name */ foo + /* before returning */ returning /* before type */ text + /* before returning format */ format /* before returning json */ json + /* before wrapper */ with /* before conditional */ conditional /* before array */ array /* before wrapper keyword */ wrapper + /* before keep */ keep /* before quotes */ quotes /* before on scalar */ on /* before scalar */ scalar /* before string */ string + /* before empty behavior */ default /* before default */ 'empty' /* before empty on */ on /* before empty */ empty + /* before error behavior */ empty /* before object */ object /* before error on */ on /* before error */ error /* before closing paren */), json_value(doc, '$.a'), - json_value /* before opening paren */(/* before document */ doc /* before format */ format /* before json */ json /* before comma */, /* before path */ '$.a' /* before passing */ passing /* before arg */ x /* before as */ as /* before name */ foo /* before returning */ returning /* before type */ text /* before empty behavior */ null /* before empty on */ on /* before empty */ empty /* before error behavior */ error /* before error on */ on /* before error */ error /* before closing paren */), + json_value(a_very_long_json_value_document + format json encoding utf8, + a_very_long_json_value_path + passing a_very_long_json_value_argument as a_very_long_json_value_name + returning a_very_long_json_value_return_type + default a_very_long_json_value_empty_value on empty + error on error), + json_value /* before opening paren */(/* before document */ doc + /* before format */ format /* before json */ json /* before comma */, + /* before path */ '$.a' + /* before passing */ passing /* before arg */ x /* before as */ as /* before name */ foo + /* before returning */ returning /* before type */ text + /* before empty behavior */ null /* before empty on */ on /* before empty */ empty + /* before error behavior */ error /* before error on */ on /* before error */ error /* before closing paren */), json(doc), json(doc format json encoding utf8 with unique keys), json(doc without unique keys), - json /* before opening paren */(/* before expression */ doc /* before format */ format /* before json */ json /* before encoding */ encoding /* before encoding name */ utf8 /* before without */ without /* before unique */ unique /* before keys */ keys /* before closing paren */), + json(a_very_long_json_document_expression + format json encoding utf8 + with unique keys), + json /* before opening paren */(/* before expression */ doc + /* before format */ format /* before json */ json + /* before encoding */ encoding /* before encoding name */ utf8 + /* before without */ without /* before unique */ unique /* before keys */ keys /* before closing paren */), json_objectagg(k: v), - json_objectagg(k value v format json absent on null with unique keys returning jsonb format json), - json_objectagg /* before opening paren */(/* before key */ k /* before value keyword */ value /* before value */ v /* before value format */ format /* before value json */ json /* before null */ null /* before on */ on /* before second null */ null /* before without */ without /* before unique */ unique /* before keys */ keys /* before returning */ returning /* before type */ jsonb /* before returning format */ format /* before returning json */ json /* before closing paren */), + json_objectagg(k value v format json + absent on null + with unique keys + returning jsonb format json), + json_objectagg(a_very_long_json_objectagg_key value a_very_long_json_objectagg_value + format json + absent on null + with unique keys + returning a_very_long_json_objectagg_return_type format json), + json_objectagg /* before opening paren */(/* before key */ k /* before value keyword */ value /* before value */ v + /* before value format */ format /* before value json */ json + /* before null */ null /* before on */ on /* before second null */ null + /* before without */ without /* before unique */ unique /* before keys */ keys + /* before returning */ returning /* before type */ jsonb + /* before returning format */ format /* before returning json */ json /* before closing paren */), json_object(), json_object('a', 1, 'b', 2), - json_object('a': 1, 'b' value 2 format json null on null with unique keys returning jsonb format json), + json_object('a': 1, 'b' value 2 format json + null on null + with unique keys + returning jsonb format json), + json_object(a_very_long_json_object_key value a_very_long_json_object_value + format json, + a_second_very_long_json_object_key value a_second_very_long_json_object_value + format json + absent on null + with unique keys + returning a_very_long_json_object_return_type format json), json_object(returning jsonb), - json_object /* before opening paren */(/* before key */ 'a' /* before colon */: /* before value */ 1 /* before comma */, /* before second key */ 'b' /* before value keyword */ value /* before second value */ 2 /* before format */ format /* before json */ json /* before absent */ absent /* before on */ on /* before null */ null /* before with */ with /* before unique */ unique /* before keys */ keys /* before returning */ returning /* before type */ jsonb /* before returning format */ format /* before returning json */ json /* before closing paren */), + json_object /* before opening paren */(/* before key */ 'a' /* before colon */: /* before value */ 1 /* before comma */, + /* before second key */ 'b' /* before value keyword */ value /* before second value */ 2 + /* before format */ format /* before json */ json + /* before absent */ absent /* before on */ on /* before null */ null + /* before with */ with /* before unique */ unique /* before keys */ keys + /* before returning */ returning /* before type */ jsonb + /* before returning format */ format /* before returning json */ json /* before closing paren */), public.foo(1), - foo /* before opening paren */(/* before first arg */ 1 /* before comma */, /* before second arg */ 2 /* before closing paren */), + foo /* before opening paren */(/* before first arg */ 1 /* before comma */, + /* before second arg */ 2 /* before closing paren */), percentile_cont(0.5) within group (order by x), mode() within group (order by y desc), percentile_disc(0.5) /* before within */ within /* before group */ group /* before opening paren */ (/* before order */ order /* before by */ by /* before sort */ x /* before closing paren */) /* after within */, @@ -156,11 +281,27 @@ select first_value(x) /* before treatment */ ignore /* before nulls */ nulls /* after nulls */, sum(x) over window_name, count(*) over (), - avg(x) over (w partition by a, b order by c desc rows between unbounded preceding and current row exclude ties), + avg(x) over (w + partition by a, b + order by c desc + rows between unbounded preceding and current row exclude ties), sum(x) over (range 1 preceding), - sum(x) over (groups between current row and unbounded following exclude group), + sum(x) over (groups between current row and unbounded following + exclude group), sum(x) over (rows between 1 preceding and 2 following exclude no others), - sum(x) /* before over */ over /* before target */ (/* before partition */ partition /* before by */ by /* before expr */ a /* before comma */, /* before second */ b /* before order */ order /* before order by */ by /* before sort */ c /* before rows */ rows /* before between */ between /* before start */ 1 /* before preceding */ preceding /* before and */ and /* before end */ current /* before row */ row /* before exclude */ exclude /* before ties */ ties /* before close */) /* after over */, + sum(a_very_long_window_value_expression) over (a_very_long_window_reference + partition by a_very_long_partition_expression, + a_second_very_long_partition_expression + order by a_very_long_order_expression desc + rows between a_very_long_frame_start_expression preceding + and a_very_long_frame_end_expression following + exclude ties), + sum(x) /* before over */ over /* before target */ (/* before partition */ partition /* before by */ by /* before expr */ a /* before comma */, + /* before second */ b + /* before order */ order /* before order by */ by /* before sort */ c + /* before rows */ rows /* before between */ between /* before start */ 1 /* before preceding */ preceding + /* before and */ and /* before end */ current /* before row */ row + /* before exclude */ exclude /* before ties */ ties /* before close */) /* after over */, -- case expr case when x > 1 then 1 else 0 end, case x when 1 then 'one' when 2 then 'two' else 'other' end, @@ -243,8 +384,12 @@ select c /* before bracket */[/* before start */ 1 /* before colon */:/* before end */ 2 /* before closing bracket */], -- tuple expr (1, 2, 3), + (first_very_long_tuple_expression, + second_very_long_tuple_expression, + third_very_long_tuple_expression), row(), row(1), row /* before opening paren */(1), row(1, 2), - (/* before first */ 1 /* before comma */, /* before second */ 2 /* before closing paren */); + (/* before first */ 1 /* before comma */, + /* before second */ 2 /* before closing paren */); diff --git a/crates/squawk_fmt/tests/after/table_constraints.snap b/crates/squawk_fmt/tests/after/table_constraints.snap index 3095104b..da6c550e 100644 --- a/crates/squawk_fmt/tests/after/table_constraints.snap +++ b/crates/squawk_fmt/tests/after/table_constraints.snap @@ -16,21 +16,32 @@ create table named_constraints( id bigint, valid_at tstzrange, constraint pk primary key (id) deferrable initially deferred, - constraint name_unique unique (id) - include (valid_at) - with (fillfactor = 70) + constraint name_unique unique (first_very_long_unique_column_name, + second_very_long_unique_column_name) + include (first_very_long_included_column_name, + second_very_long_included_column_name) + with (fillfactor = 70, a_very_long_storage_parameter_name = false) using index tablespace fast, - constraint id_check check (id > 0) not valid no inherit, - constraint parent_fk foreign key (id) references public.parents (id) + constraint id_check check (a_long_check_expression_name > another_long_check_expression_name) + not valid + no inherit, + constraint parent_fk foreign key (first_very_long_foreign_key_column_name, + second_very_long_foreign_key_column_name) + references public.parents (first_very_long_referenced_column_name, + second_very_long_referenced_column_name) match full - on delete set null (id) + on delete set null (first_very_long_foreign_key_column_name, + second_very_long_foreign_key_column_name) on update no action not deferrable, - constraint no_overlap exclude using gist (id with =, valid_at with &&) - include (id) - with (fillfactor = 80) + constraint no_overlap exclude using gist (first_very_long_exclusion_expression + with =, + second_very_long_exclusion_expression with &&) + include (first_very_long_excluded_column_name, + second_very_long_excluded_column_name) + with (fillfactor = 80, a_very_long_exclusion_storage_parameter = false) using index tablespace fast - where (id > 0) + where (a_very_long_exclusion_predicate_expression > 0) deferrable ); @@ -48,12 +59,16 @@ create table commented_constraints( /* before deferrable */ deferrable, constraint named_check /* before check */ check /* before check opening paren */ (/* before check expression */ id > 0 /* before check closing paren */) /* before not */ not /* before valid */ valid, - constraint named_fk /* before foreign */ foreign /* before key */ key /* before from opening paren */ (/* before from column */ parent_id /* before from closing paren */) /* before references */ references /* before table */ public /* before dot */./* before table name */ parents /* before to opening paren */ (/* before to column */ id /* before to closing paren */) + constraint named_fk /* before foreign */ foreign /* before key */ key /* before from opening paren */ (/* before from column */ parent_id /* before from closing paren */) + /* before references */ references /* before table */ public /* before dot */./* before table name */ parents /* before to opening paren */ (/* before to column */ id /* before to closing paren */) /* before match */ match /* before simple */ simple /* before on delete */ on /* before delete */ delete /* before set */ set /* before null */ null /* before set columns */ (parent_id) /* before on update */ on /* before update */ update /* before cascade */ cascade /* before enforced */ enforced, - constraint named_exclude /* before exclude */ exclude /* before using */ using /* before method */ gist /* before exclusion opening paren */ (/* before exclusion expression */ id /* before exclusion with */ with /* before exclusion op */ = /* before exclusion comma */, /* before second exclusion */ valid_at with /* before operator */ operator /* before operator opening paren */(/* before operator name */ public /* before operator dot */./* before operator op */ && /* before operator closing paren */) /* before exclusion closing paren */) + constraint named_exclude /* before exclude */ exclude /* before using */ using /* before method */ gist /* before exclusion opening paren */ (/* before exclusion expression */ id + /* before exclusion with */ with /* before exclusion op */ = /* before exclusion comma */, + /* before second exclusion */ valid_at + with /* before operator */ operator /* before operator opening paren */(/* before operator name */ public /* before operator dot */./* before operator op */ && /* before operator closing paren */) /* before exclusion closing paren */) /* before include */ include /* before include opening paren */ (id /* before include closing paren */) /* before with params */ with /* before params opening paren */ (/* before param */ fillfactor /* before equals */ = /* before value */ 80 /* before params closing paren */) /* before tablespace using */ using /* before index */ index /* before tablespace */ tablespace /* before tablespace name */ fast diff --git a/crates/squawk_fmt/tests/after/xml_functions.snap b/crates/squawk_fmt/tests/after/xml_functions.snap index 5908b42a..59249ab5 100644 --- a/crates/squawk_fmt/tests/after/xml_functions.snap +++ b/crates/squawk_fmt/tests/after/xml_functions.snap @@ -7,25 +7,64 @@ select xmlelement(name foo, 1, 2), xmlelement(name foo, xmlattributes(a, b as c)), xmlelement(name foo, xmlattributes(a as attr), x, y), - /* before element */ xmlelement /* before outer opening paren */(/* before name */ name /* before tag */ tag /* before attributes comma */, /* before xmlattributes */ xmlattributes /* before attributes opening paren */(/* before first attribute */ a /* before as */ as /* before attribute name */ attr /* before attribute comma */, /* before second attribute */ b /* before attributes closing paren */) /* before content comma */, /* before content */ x /* before outer closing paren */) /* after element */, + xmlelement(name a_very_long_element_name, + xmlattributes(a_very_long_xml_attribute_expression as a_very_long_xml_attribute_name, + a_second_very_long_xml_attribute_expression as a_second_very_long_xml_attribute_name), + first_very_long_content_expression, + second_very_long_content_expression, + third_very_long_content_expression), + /* before element */ xmlelement /* before outer opening paren */(/* before name */ name /* before tag */ tag /* before attributes comma */, + /* before xmlattributes */ xmlattributes /* before attributes opening paren */(/* before first attribute */ a /* before as */ as /* before attribute name */ attr /* before attribute comma */, + /* before second attribute */ b /* before attributes closing paren */) /* before content comma */, + /* before content */ x /* before outer closing paren */) /* after element */, xmlexists('/foo' passing doc), xmlexists('/foo' passing by ref doc), xmlexists('/foo' passing doc by value), - /* before exists */ xmlexists /* before opening paren */(/* before row */ '/foo' /* before passing */ passing /* before first by */ by /* before ref */ ref /* before document */ doc /* before second by */ by /* before value */ value /* before closing paren */) /* after exists */, + xmlexists(a_very_long_xml_exists_path_expression + passing + by ref + a_very_long_xml_exists_document_expression + by value), + /* before exists */ xmlexists /* before opening paren */(/* before row */ '/foo' + /* before passing */ passing + /* before first by */ by /* before ref */ ref + /* before document */ doc + /* before second by */ by /* before value */ value /* before closing paren */) /* after exists */, xmlforest(a, b as foo), - /* before forest */ xmlforest /* before opening paren */(/* before first expression */ a /* before as */ as /* before tag */ first /* before comma */, /* before second expression */ b /* before closing paren */) /* after forest */, + xmlforest(first_very_long_expression as first_very_long_element_name, + second_very_long_expression as second_very_long_element_name), + /* before forest */ xmlforest /* before opening paren */(/* before first expression */ a /* before as */ as /* before tag */ first /* before comma */, + /* before second expression */ b /* before closing paren */) /* after forest */, xmlparse(document '' preserve whitespace), xmlparse(content value strip whitespace), - /* before parse */ xmlparse /* before opening paren */(/* before kind */ document /* before expression */ value /* before preserve */ preserve /* before whitespace */ whitespace /* before closing paren */) /* after parse */, + xmlparse(document + a_very_long_xml_parse_document_expression_that_forces_the_xml_parse_node_to_wrap + preserve whitespace), + /* before parse */ xmlparse /* before opening paren */(/* before kind */ document + /* before expression */ value + /* before preserve */ preserve /* before whitespace */ whitespace /* before closing paren */) /* after parse */, xmlpi(name php), xmlpi(name php, 'echo'), - /* before pi */ xmlpi /* before opening paren */(/* before name */ name /* before target */ php /* before comma */, /* before expression */ 'echo' /* before closing paren */) /* after pi */, + xmlpi(name a_very_long_xml_processing_instruction_target, + a_very_long_xml_processing_instruction_expression_that_forces_the_xml_pi_node_to_wrap), + /* before pi */ xmlpi /* before opening paren */(/* before name */ name /* before target */ php /* before comma */, + /* before expression */ 'echo' /* before closing paren */) /* after pi */, xmlroot(doc, version '1.0'), xmlroot(doc, version no value, standalone yes), xmlroot(doc, version '1.0', standalone no), xmlroot(doc, version '1.0', standalone no value), - /* before root */ xmlroot /* before opening paren */(/* before expression */ doc /* before version comma */, /* before version */ version /* before no */ no /* before version value */ value/* before standalone comma */ , /* before standalone */ standalone /* before standalone no */ no /* before standalone value */ value /* before closing paren */) /* after root */, + xmlroot(a_very_long_xml_root_document_expression_that_forces_the_xml_root_node_to_wrap, + version a_very_long_xml_root_version_expression, + standalone no value), + /* before root */ xmlroot /* before opening paren */(/* before expression */ doc /* before version comma */, + /* before version */ version /* before no */ no /* before version value */ value/* before standalone comma */ , + /* before standalone */ standalone /* before standalone no */ no /* before standalone value */ value /* before closing paren */) /* after root */, xmlserialize(document doc as text), xmlserialize(content doc as varchar(20) indent), xmlserialize(content doc as text no indent), - /* before serialize */ xmlserialize /* before opening paren */(/* before kind */ content /* before expression */ doc /* before as */ as /* before type */ text /* before no */ no /* before indent */ indent /* before closing paren */)/* after serialize */; + xmlserialize(content a_long_xml_serialize_content_expression_that_forces_wrapping + as a_very_long_xml_serialize_return_type + no indent), + /* before serialize */ xmlserialize /* before opening paren */(/* before kind */ content /* before expression */ doc + /* before as */ as /* before type */ text + /* before no */ no /* before indent */ indent /* before closing paren */)/* after serialize */; diff --git a/crates/squawk_fmt/tests/before/graph_table.sql b/crates/squawk_fmt/tests/before/graph_table.sql index 2b84b2a4..63e52c53 100644 --- a/crates/squawk_fmt/tests/before/graph_table.sql +++ b/crates/squawk_fmt/tests/before/graph_table.sql @@ -6,6 +6,8 @@ select * from GRAPH_TABLE(g MATCH (a)<-[left_edge]-(b), (c)-[any_edge]-(d), (e)< select * from GRAPH_TABLE(g MATCH (a)->{1}(b), (c)-{,3}(d), (e)-{2,4}(f) COLUMNS(a)); +select * from GRAPH_TABLE(a_very_long_property_graph_name MATCH (a_very_long_vertex_variable IS a_very_long_vertex_label WHERE a_very_long_vertex_filter_expression)-[a_very_long_edge_variable IS a_very_long_edge_label WHERE a_very_long_edge_filter_expression]->(a_second_very_long_vertex_variable IS a_second_very_long_vertex_label), (a_third_very_long_vertex_variable)-[a_second_very_long_edge_variable]-(a_fourth_very_long_vertex_variable) WHERE a_very_long_graph_filter_expression COLUMNS (a_very_long_vertex_variable.long_property_name AS first_long_output_column_name, a_second_very_long_vertex_variable.other_long_property_name AS second_output_column_name)); + select * from /* before graph table */ GRAPH_TABLE /* before outer opening paren */ ( /* before graph */ public /* before graph dot */ . /* before graph name */ g diff --git a/crates/squawk_fmt/tests/before/group_by.sql b/crates/squawk_fmt/tests/before/group_by.sql index bcadeb04..5e070fec 100644 --- a/crates/squawk_fmt/tests/before/group_by.sql +++ b/crates/squawk_fmt/tests/before/group_by.sql @@ -1,6 +1,7 @@ select 1 group by 1, foo + 2; select 1 group by all rollup (1, 2), cube (3, 4); select 1 group by distinct grouping sets ((), (1, 2), rollup (3), cube (4)); +select 1 group by grouping sets ((first_very_long_grouping_expression, second_very_long_grouping_expression), rollup (third_very_long_grouping_expression, fourth_very_long_grouping_expression), cube (fifth_very_long_grouping_expression, sixth_very_long_grouping_expression)); select 1 group /* before by */ by /* before distinct */ distinct /* before grouping */ grouping /* before sets */ sets /* before outer paren */ ( diff --git a/crates/squawk_fmt/tests/before/select_expr.sql b/crates/squawk_fmt/tests/before/select_expr.sql index 0df58b83..eb0a6dd5 100644 --- a/crates/squawk_fmt/tests/before/select_expr.sql +++ b/crates/squawk_fmt/tests/before/select_expr.sql @@ -83,6 +83,7 @@ select TRIM ( foo, bar ), /* before trim */ TRIM /* before opening paren */ ( /* before side */ BOTH /* before trim char */ 'x' /* before from */ FROM /* before string */ 'xfoox' /* before closing paren */ ) /* after trim */, date_trunc('month', now()), + a_very_long_function_name(first_very_long_argument_name, second_very_long_argument_name, third_very_long_argument_name), foo ( ), foo ( * ), foo ( ALL 1, 2 ), @@ -99,13 +100,17 @@ select array_agg(x /* before order */ order /* before by */ by /* before first */ y /* before desc */ desc /* before nulls */ nulls /* before last */ last /* before comma */, /* before second */ z /* before asc */ asc), JSON_ARRAYAGG(v), JSON_ARRAYAGG(v FORMAT JSON ORDER BY sort_col DESC NULL ON NULL RETURNING jsonb FORMAT JSON), + JSON_ARRAYAGG(a_very_long_json_arrayagg_value FORMAT JSON ENCODING UTF8 ORDER BY a_very_long_json_arrayagg_sort_expression DESC ABSENT ON NULL RETURNING a_very_long_json_arrayagg_return_type FORMAT JSON), JSON_ARRAYAGG /* before opening paren */ (/* before value */ v /* before value format */ FORMAT /* before value json */ JSON /* before encoding */ ENCODING /* before encoding name */ UTF8 /* before order */ ORDER /* before by */ BY /* before sort */ x /* before absent */ ABSENT /* before on */ ON /* before null */ NULL /* before returning */ RETURNING /* before type */ jsonb /* before returning format */ FORMAT /* before returning json */ JSON /* before closing paren */), JSON_ARRAY(), JSON_ARRAY(1, 2 FORMAT JSON RETURNING jsonb), + JSON_ARRAY(first_very_long_json_array_expression, second_very_long_json_array_expression, third_very_long_json_array_expression RETURNING a_very_long_json_array_return_type), + JSON_ARRAY(SELECT a_very_long_json_select_expression_that_forces_the_json_select_format_node_to_wrap FORMAT JSON ABSENT ON NULL RETURNING a_very_long_json_select_return_type), JSON_ARRAY /* before opening paren */ (/* before first */ 1 /* before comma */, /* before second */ 2 /* before format */ FORMAT /* before json */ JSON /* before encoding */ ENCODING /* before encoding name */ UTF8 /* before returning */ RETURNING /* before type */ jsonb /* before returning format */ FORMAT /* before returning json */ JSON /* before closing paren */), JSON_ARRAY(/* before select */ SELECT /* before target */ x /* before format */ FORMAT /* before json */ JSON /* before absent */ ABSENT /* before on */ ON /* before null */ NULL /* before returning */ RETURNING /* before type */ jsonb /* before closing paren */), JSON_EXISTS(doc, '$.a'), JSON_EXISTS(doc, '$.a' PASSING x AS foo, y AS bar TRUE ON ERROR), + JSON_EXISTS(a_very_long_json_exists_document FORMAT JSON ENCODING UTF8, a_very_long_json_exists_path PASSING a_very_long_json_exists_argument AS a_very_long_json_exists_name FALSE ON ERROR), JSON_EXISTS /* before opening paren */ (/* before document */ doc /* before format */ FORMAT /* before json */ JSON /* before encoding */ ENCODING /* before encoding name */ UTF8 /* before comma */, /* before path */ '$.a' /* before passing */ PASSING /* before arg */ x /* before as */ AS /* before name */ foo /* before arg comma */, /* before second arg */ y /* before second as */ AS /* before second name */ bar /* before behavior */ DEFAULT /* before default */ false /* before on */ ON /* before error */ ERROR /* before closing paren */), JSON_EXISTS(doc, '$' ERROR ON ERROR), JSON_EXISTS(doc, '$' NULL ON ERROR), @@ -118,24 +123,30 @@ select JSON_SCALAR /* before opening paren */ (/* before expression */ 1 /* before closing paren */), JSON_SERIALIZE(doc), JSON_SERIALIZE(doc FORMAT JSON RETURNING text FORMAT JSON), + JSON_SERIALIZE(a_very_long_json_serialize_document FORMAT JSON ENCODING a_very_long_json_encoding_name_that_forces_the_json_format_clause_to_wrap RETURNING a_very_long_json_serialize_return_type_name_that_forces_returning_to_wrap FORMAT JSON), JSON_SERIALIZE /* before opening paren */ (/* before document */ doc /* before format */ FORMAT /* before json */ JSON /* before returning */ RETURNING /* before type */ text /* before returning format */ FORMAT /* before returning json */ JSON /* before closing paren */), JSON_QUERY(doc, '$.a'), JSON_QUERY(doc, '$.a' WITH WRAPPER), JSON_QUERY(doc, '$.a' WITH UNCONDITIONAL ARRAY WRAPPER OMIT QUOTES), JSON_QUERY(doc, '$.a' WITHOUT ARRAY WRAPPER), + JSON_QUERY(a_very_long_json_query_document FORMAT JSON ENCODING UTF8, a_very_long_json_query_path PASSING a_very_long_json_query_argument AS a_very_long_json_query_name RETURNING a_very_long_json_query_return_type FORMAT JSON WITH UNCONDITIONAL ARRAY WRAPPER KEEP QUOTES ON SCALAR STRING DEFAULT a_very_long_json_query_empty_value ON EMPTY ERROR ON ERROR), JSON_QUERY /* before opening paren */ (/* before document */ doc /* before format */ FORMAT /* before json */ JSON /* before encoding */ ENCODING /* before encoding name */ UTF8 /* before comma */, /* before path */ '$.a' /* before passing */ PASSING /* before arg */ x /* before as */ AS /* before name */ foo /* before returning */ RETURNING /* before type */ text /* before returning format */ FORMAT /* before returning json */ JSON /* before wrapper */ WITH /* before conditional */ CONDITIONAL /* before array */ ARRAY /* before wrapper keyword */ WRAPPER /* before keep */ KEEP /* before quotes */ QUOTES /* before on scalar */ ON /* before scalar */ SCALAR /* before string */ STRING /* before empty behavior */ DEFAULT /* before default */ 'empty' /* before empty on */ ON /* before empty */ EMPTY /* before error behavior */ EMPTY /* before object */ OBJECT /* before error on */ ON /* before error */ ERROR /* before closing paren */), JSON_VALUE(doc, '$.a'), + JSON_VALUE(a_very_long_json_value_document FORMAT JSON ENCODING UTF8, a_very_long_json_value_path PASSING a_very_long_json_value_argument AS a_very_long_json_value_name RETURNING a_very_long_json_value_return_type DEFAULT a_very_long_json_value_empty_value ON EMPTY ERROR ON ERROR), JSON_VALUE /* before opening paren */ (/* before document */ doc /* before format */ FORMAT /* before json */ JSON /* before comma */, /* before path */ '$.a' /* before passing */ PASSING /* before arg */ x /* before as */ AS /* before name */ foo /* before returning */ RETURNING /* before type */ text /* before empty behavior */ NULL /* before empty on */ ON /* before empty */ EMPTY /* before error behavior */ ERROR /* before error on */ ON /* before error */ ERROR /* before closing paren */), JSON(doc), JSON(doc FORMAT JSON ENCODING UTF8 WITH UNIQUE KEYS), JSON(doc WITHOUT UNIQUE KEYS), + JSON(a_very_long_json_document_expression FORMAT JSON ENCODING UTF8 WITH UNIQUE KEYS), JSON /* before opening paren */ (/* before expression */ doc /* before format */ FORMAT /* before json */ JSON /* before encoding */ ENCODING /* before encoding name */ UTF8 /* before without */ WITHOUT /* before unique */ UNIQUE /* before keys */ KEYS /* before closing paren */), JSON_OBJECTAGG(k: v), JSON_OBJECTAGG(k VALUE v FORMAT JSON ABSENT ON NULL WITH UNIQUE KEYS RETURNING jsonb FORMAT JSON), + JSON_OBJECTAGG(a_very_long_json_objectagg_key VALUE a_very_long_json_objectagg_value FORMAT JSON ABSENT ON NULL WITH UNIQUE KEYS RETURNING a_very_long_json_objectagg_return_type FORMAT JSON), JSON_OBJECTAGG /* before opening paren */ (/* before key */ k /* before value keyword */ VALUE /* before value */ v /* before value format */ FORMAT /* before value json */ JSON /* before null */ NULL /* before on */ ON /* before second null */ NULL /* before without */ WITHOUT /* before unique */ UNIQUE /* before keys */ KEYS /* before returning */ RETURNING /* before type */ jsonb /* before returning format */ FORMAT /* before returning json */ JSON /* before closing paren */), JSON_OBJECT(), JSON_OBJECT('a', 1, 'b', 2), JSON_OBJECT('a': 1, 'b' VALUE 2 FORMAT JSON NULL ON NULL WITH UNIQUE KEYS RETURNING jsonb FORMAT JSON), + JSON_OBJECT(a_very_long_json_object_key VALUE a_very_long_json_object_value FORMAT JSON, a_second_very_long_json_object_key VALUE a_second_very_long_json_object_value FORMAT JSON ABSENT ON NULL WITH UNIQUE KEYS RETURNING a_very_long_json_object_return_type FORMAT JSON), JSON_OBJECT(RETURNING jsonb), JSON_OBJECT /* before opening paren */ (/* before key */ 'a' /* before colon */ : /* before value */ 1 /* before comma */, /* before second key */ 'b' /* before value keyword */ VALUE /* before second value */ 2 /* before format */ FORMAT /* before json */ JSON /* before absent */ ABSENT /* before on */ ON /* before null */ NULL /* before with */ WITH /* before unique */ UNIQUE /* before keys */ KEYS /* before returning */ RETURNING /* before type */ jsonb /* before returning format */ FORMAT /* before returning json */ JSON /* before closing paren */), public.foo(1), @@ -154,6 +165,7 @@ select sum(x) OVER (RANGE 1 PRECEDING), sum(x) OVER (GROUPS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING EXCLUDE GROUP), sum(x) OVER (ROWS BETWEEN 1 PRECEDING AND 2 FOLLOWING EXCLUDE NO OTHERS), + sum(a_very_long_window_value_expression) OVER (a_very_long_window_reference PARTITION BY a_very_long_partition_expression, a_second_very_long_partition_expression ORDER BY a_very_long_order_expression DESC ROWS BETWEEN a_very_long_frame_start_expression PRECEDING AND a_very_long_frame_end_expression FOLLOWING EXCLUDE TIES), sum(x) /* before over */ OVER /* before target */ ( /* before partition */ PARTITION /* before by */ BY /* before expr */ a /* before comma */, /* before second */ b /* before order */ ORDER /* before order by */ BY /* before sort */ c /* before rows */ ROWS /* before between */ BETWEEN /* before start */ 1 /* before preceding */ PRECEDING /* before and */ AND /* before end */ CURRENT /* before row */ ROW /* before exclude */ EXCLUDE /* before ties */ TIES /* before close */ ) /* after over */, -- case expr case when x > 1 then 1 else 0 end, @@ -225,6 +237,7 @@ select c /* before bracket */ [ /* before start */ 1 /* before colon */ : /* before end */ 2 /* before closing bracket */ ], -- tuple expr ( 1 , 2 , 3 ), + (first_very_long_tuple_expression, second_very_long_tuple_expression, third_very_long_tuple_expression), row ( ), row ( 1 ), row /* before opening paren */ ( 1 ), diff --git a/crates/squawk_fmt/tests/before/table_constraints.sql b/crates/squawk_fmt/tests/before/table_constraints.sql index 43ed7b96..d837c44f 100644 --- a/crates/squawk_fmt/tests/before/table_constraints.sql +++ b/crates/squawk_fmt/tests/before/table_constraints.sql @@ -12,10 +12,10 @@ create table named_constraints ( id bigint, valid_at tstzrange, CONSTRAINT pk PRIMARY KEY (id) DEFERRABLE INITIALLY DEFERRED, - CONSTRAINT name_unique UNIQUE (id) INCLUDE (valid_at) WITH (fillfactor = 70) USING INDEX TABLESPACE fast, - CONSTRAINT id_check CHECK (id > 0) NOT VALID NO INHERIT, - CONSTRAINT parent_fk FOREIGN KEY (id) REFERENCES public.parents(id) MATCH FULL ON DELETE SET NULL (id) ON UPDATE NO ACTION NOT DEFERRABLE, - CONSTRAINT no_overlap EXCLUDE USING gist (id WITH =, valid_at WITH &&) INCLUDE (id) WITH (fillfactor = 80) USING INDEX TABLESPACE fast WHERE (id > 0) DEFERRABLE + CONSTRAINT name_unique UNIQUE (first_very_long_unique_column_name, second_very_long_unique_column_name) INCLUDE (first_very_long_included_column_name, second_very_long_included_column_name) WITH (fillfactor = 70, a_very_long_storage_parameter_name = false) USING INDEX TABLESPACE fast, + CONSTRAINT id_check CHECK (a_long_check_expression_name > another_long_check_expression_name) NOT VALID NO INHERIT, + CONSTRAINT parent_fk FOREIGN KEY (first_very_long_foreign_key_column_name, second_very_long_foreign_key_column_name) REFERENCES public.parents (first_very_long_referenced_column_name, second_very_long_referenced_column_name) MATCH FULL ON DELETE SET NULL (first_very_long_foreign_key_column_name, second_very_long_foreign_key_column_name) ON UPDATE NO ACTION NOT DEFERRABLE, + CONSTRAINT no_overlap EXCLUDE USING gist (first_very_long_exclusion_expression WITH =, second_very_long_exclusion_expression WITH &&) INCLUDE (first_very_long_excluded_column_name, second_very_long_excluded_column_name) WITH (fillfactor = 80, a_very_long_exclusion_storage_parameter = false) USING INDEX TABLESPACE fast WHERE (a_very_long_exclusion_predicate_expression > 0) DEFERRABLE ); create table using_indexes ( diff --git a/crates/squawk_fmt/tests/before/xml_functions.sql b/crates/squawk_fmt/tests/before/xml_functions.sql index e2ae99ba..68cca217 100644 --- a/crates/squawk_fmt/tests/before/xml_functions.sql +++ b/crates/squawk_fmt/tests/before/xml_functions.sql @@ -3,25 +3,32 @@ select XMLELEMENT(NAME foo, 1, 2), XMLELEMENT(NAME foo, XMLATTRIBUTES(a, b AS c)), XMLELEMENT(NAME foo, XMLATTRIBUTES(a AS attr), x, y), + XMLELEMENT(NAME a_very_long_element_name, XMLATTRIBUTES(a_very_long_xml_attribute_expression AS a_very_long_xml_attribute_name, a_second_very_long_xml_attribute_expression AS a_second_very_long_xml_attribute_name), first_very_long_content_expression, second_very_long_content_expression, third_very_long_content_expression), /* before element */ XMLELEMENT /* before outer opening paren */ ( /* before name */ NAME /* before tag */ "tag" /* before attributes comma */, /* before xmlattributes */ XMLATTRIBUTES /* before attributes opening paren */ ( /* before first attribute */ a /* before as */ AS /* before attribute name */ "attr" /* before attribute comma */, /* before second attribute */ b /* before attributes closing paren */ ) /* before content comma */, /* before content */ x /* before outer closing paren */ ) /* after element */, XMLEXISTS('/foo' PASSING doc), XMLEXISTS('/foo' PASSING BY REF doc), XMLEXISTS('/foo' PASSING doc BY VALUE), + XMLEXISTS(a_very_long_xml_exists_path_expression PASSING BY REF a_very_long_xml_exists_document_expression BY VALUE), /* before exists */ XMLEXISTS /* before opening paren */ ( /* before row */ '/foo' /* before passing */ PASSING /* before first by */ BY /* before ref */ REF /* before document */ doc /* before second by */ BY /* before value */ VALUE /* before closing paren */ ) /* after exists */, XMLFOREST(a, b AS foo), + XMLFOREST(first_very_long_expression AS first_very_long_element_name, second_very_long_expression AS second_very_long_element_name), /* before forest */ XMLFOREST /* before opening paren */ ( /* before first expression */ a /* before as */ AS /* before tag */ "first" /* before comma */, /* before second expression */ b /* before closing paren */ ) /* after forest */, XMLPARSE(DOCUMENT '' PRESERVE WHITESPACE), XMLPARSE(CONTENT value STRIP WHITESPACE), + XMLPARSE(DOCUMENT a_very_long_xml_parse_document_expression_that_forces_the_xml_parse_node_to_wrap PRESERVE WHITESPACE), /* before parse */ XMLPARSE /* before opening paren */ ( /* before kind */ DOCUMENT /* before expression */ value /* before preserve */ PRESERVE /* before whitespace */ WHITESPACE /* before closing paren */ ) /* after parse */, XMLPI(NAME php), XMLPI(NAME php, 'echo'), + XMLPI(NAME a_very_long_xml_processing_instruction_target, a_very_long_xml_processing_instruction_expression_that_forces_the_xml_pi_node_to_wrap), /* before pi */ XMLPI /* before opening paren */ ( /* before name */ NAME /* before target */ php /* before comma */, /* before expression */ 'echo' /* before closing paren */ ) /* after pi */, XMLROOT(doc, VERSION '1.0'), XMLROOT(doc, VERSION NO VALUE, STANDALONE YES), XMLROOT(doc, VERSION '1.0', STANDALONE NO), XMLROOT(doc, VERSION '1.0', STANDALONE NO VALUE), + XMLROOT(a_very_long_xml_root_document_expression_that_forces_the_xml_root_node_to_wrap, VERSION a_very_long_xml_root_version_expression, STANDALONE NO VALUE), /* before root */ XMLROOT /* before opening paren */ ( /* before expression */ doc /* before version comma */, /* before version */ VERSION /* before no */ NO /* before version value */ VALUE /* before standalone comma */, /* before standalone */ STANDALONE /* before standalone no */ NO /* before standalone value */ VALUE /* before closing paren */ ) /* after root */, XMLSERIALIZE(DOCUMENT doc AS text), XMLSERIALIZE(CONTENT doc AS varchar(20) INDENT), XMLSERIALIZE(CONTENT doc AS text NO INDENT), + XMLSERIALIZE(CONTENT a_long_xml_serialize_content_expression_that_forces_wrapping AS a_very_long_xml_serialize_return_type NO INDENT), /* before serialize */ XMLSERIALIZE /* before opening paren */ ( /* before kind */ CONTENT /* before expression */ doc /* before as */ AS /* before type */ text /* before no */ NO /* before indent */ INDENT /* before closing paren */ ) /* after serialize */; From fef1ff64d864466ed0bf5c3831094e4aa8aa4862 Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Mon, 24 Aug 2026 18:27:51 -0400 Subject: [PATCH 13/17] add long tests cases --- .../squawk_fmt/tests/after/create_table.snap | 7 ++ .../tests/after/create_table_like.snap | 13 ++++ .../tests/after/custom_operator.snap | 3 + crates/squawk_fmt/tests/after/from.snap | 13 ++++ crates/squawk_fmt/tests/after/select.snap | 9 +++ .../squawk_fmt/tests/after/select_expr.snap | 71 +++++++++++++++++++ .../tests/after/select_literals.snap | 27 +++++++ .../tests/after/table_constraints.snap | 7 ++ crates/squawk_fmt/tests/after/types.snap | 20 ++++++ .../squawk_fmt/tests/before/create_table.sql | 2 + .../tests/before/create_table_like.sql | 2 + .../tests/before/custom_operator.sql | 1 + crates/squawk_fmt/tests/before/from.sql | 5 ++ crates/squawk_fmt/tests/before/select.sql | 2 + .../squawk_fmt/tests/before/select_expr.sql | 50 +++++++++++++ .../tests/before/select_literals.sql | 9 +++ .../tests/before/table_constraints.sql | 2 + crates/squawk_fmt/tests/before/types.sql | 3 + 18 files changed, 246 insertions(+) diff --git a/crates/squawk_fmt/tests/after/create_table.snap b/crates/squawk_fmt/tests/after/create_table.snap index 69f9e429..c8937172 100644 --- a/crates/squawk_fmt/tests/after/create_table.snap +++ b/crates/squawk_fmt/tests/after/create_table.snap @@ -68,3 +68,10 @@ create table t( -- line comment before the semicolon create table t(a int)-- one ; + +create table a_very_long_schema_name.a_very_long_table_name( + a_very_long_integer_column_name int, + a_very_long_text_column_name text, + a_very_long_qualified_type_column_name a_very_long_type_schema_name.a_very_long_type_name, + U&"c!006fl" uescape '!' a_very_long_type_schema_name.a_very_long_type_name +); diff --git a/crates/squawk_fmt/tests/after/create_table_like.snap b/crates/squawk_fmt/tests/after/create_table_like.snap index 21170705..4584be1b 100644 --- a/crates/squawk_fmt/tests/after/create_table_like.snap +++ b/crates/squawk_fmt/tests/after/create_table_like.snap @@ -42,3 +42,16 @@ create table t( -- a line comment including all ); + +create table a_very_long_destination_table_name( + like a_very_long_source_schema_name.a_very_long_source_table_name + including comments + including compression + including constraints + including defaults + including generated + including identity + including indexes + including statistics + including storage +); diff --git a/crates/squawk_fmt/tests/after/custom_operator.snap b/crates/squawk_fmt/tests/after/custom_operator.snap index 41c9fc2b..300b25b7 100644 --- a/crates/squawk_fmt/tests/after/custom_operator.snap +++ b/crates/squawk_fmt/tests/after/custom_operator.snap @@ -4,3 +4,6 @@ input_file: crates/squawk_fmt/tests/before/custom_operator.sql --- select 1 /*before*/ <<<< /*after*/ 1; select ##### /*after*/ 1; +select + a_very_long_left_operand_name <<<< a_very_long_right_operand_name, + a_second_very_long_left_operand_name ##### a_second_very_long_right_operand_name; diff --git a/crates/squawk_fmt/tests/after/from.snap b/crates/squawk_fmt/tests/after/from.snap index 49f91538..16290010 100644 --- a/crates/squawk_fmt/tests/after/from.snap +++ b/crates/squawk_fmt/tests/after/from.snap @@ -13,3 +13,16 @@ select * from users tablesample bernoulli(10) repeatable(42); select * /* before from */ from /* before item */ only /* before relation */ public /* before dot */./* before table */ foo /* before star */ * /* before alias */ as /* before alias name */ f /* before item comma */, /* before second item */ other /* before second alias */ o; + +select * +from a_very_long_schema_name.a_very_long_relation_name as a_very_long_relation_alias(a_very_long_first_column_alias, + a_very_long_second_column_alias, + a_very_long_third_column_alias); +select * +from a_very_long_relation_name a_very_long_relation_alias(a_very_long_first_column_name a_very_long_type_schema.a_very_long_first_type_name, + a_very_long_second_column_name a_very_long_type_schema.a_very_long_second_type_name collate a_very_long_collation_name); +select * +from a_very_long_relation_name tablesample bernoulli(a_very_long_sampling_percentage_expression) repeatable(a_very_long_repeatable_seed_expression); +select * +from only a_very_long_schema_name.a_very_long_first_relation_name * as a_very_long_first_alias, + a_very_long_schema_name.a_very_long_second_relation_name as a_very_long_second_alias; diff --git a/crates/squawk_fmt/tests/after/select.snap b/crates/squawk_fmt/tests/after/select.snap index ad014cdf..10ff9ae7 100644 --- a/crates/squawk_fmt/tests/after/select.snap +++ b/crates/squawk_fmt/tests/after/select.snap @@ -20,3 +20,12 @@ select 1 foo, 2 "filter", 3 "day", 4 "array", 5 "Mixed"; select 1 as foo, 2 as filter, 3 as day, 4 as array; select 1 /*a*/ group /* b */ by /*c */ 1; + +select + a_very_long_first_target_expression as a_very_long_first_column_alias, + a_very_long_second_target_expression a_very_long_second_column_alias, + a_very_long_third_target_expression as "A Very Long Quoted Third Column Alias" +from a_very_long_schema_name.a_very_long_table_name as a_very_long_table_alias +group by a_very_long_first_target_expression, + a_very_long_second_target_expression, + a_very_long_third_target_expression; diff --git a/crates/squawk_fmt/tests/after/select_expr.snap b/crates/squawk_fmt/tests/after/select_expr.snap index 4f44f620..2b92bfd6 100644 --- a/crates/squawk_fmt/tests/after/select_expr.snap +++ b/crates/squawk_fmt/tests/after/select_expr.snap @@ -7,11 +7,16 @@ select array[1, 2], array(select 1), array[[1, 2], [3, 4]], + array[a_very_long_first_array_expression, a_very_long_second_array_expression, a_very_long_third_array_expression], + array(select a_very_long_array_select_expression + from a_very_long_array_select_relation_name), + array[[a_very_long_first_nested_array_expression, a_very_long_second_nested_array_expression], [a_very_long_third_nested_array_expression, a_very_long_fourth_nested_array_expression]], -- between expr 2 between 1 and 3, 2 not between 1 and 3, 2 between asymmetric 1 and 3, 2 between symmetric 1 and 3, + a_very_long_between_target_expression between symmetric a_very_long_between_start_expression and a_very_long_between_end_expression, -- bin expr 1 + 1, 1 /* before op */ + /* after op */ 1, @@ -50,26 +55,42 @@ select 'foo' similar to 'f%', 6 / 2, 2 * 3, + a_very_long_binary_left_expression + a_very_long_binary_middle_expression * a_very_long_binary_right_expression, -- call expr 1 = any(array[1, 2]), 2 = all(select x from things), 3 = some(array[3]), 4 /* before op */ = /* before any */ any /* before opening paren */(/* before expr */ array[4] /* before closing paren */) /* after any */, exists(select 1 from things), + a_very_long_any_comparison_expression = any(a_very_long_any_input_expression_that_forces_wrapping), + a_very_long_all_comparison_expression = all(select + a_very_long_all_select_expression + from a_very_long_all_relation_name), + a_very_long_some_comparison_expression = some(a_very_long_some_input_expression), + exists(select a_very_long_exists_select_expression + from a_very_long_exists_relation_name), /* before exists */ exists /* before opening paren */(/* before select */ select 1 /* before closing paren */) /* after exists */, collation for (b + c), + collation for (a_very_long_collation_input_expression_that_forces_the_builtin_to_wrap), /* before collation */ collation /* before for */ for /* before opening paren */(/* before expr */ foo /* before closing paren */) /* after collation */, extract(year from timestamp '2001-02-16 20:38:40'), extract('month' from ts), extract("Field" from ts), + extract(a_very_long_field_name from a_very_long_timestamp_expression_that_forces_the_builtin_to_wrap), /* before extract */ extract /* before opening paren */(/* before field */ day /* before from */ from /* before expr */ ts /* before closing paren */) /* after extract */, position('om' in 'Thomas'), + position(a_very_long_substring_expression_that_forces_wrapping in a_very_long_string_expression_that_forces_wrapping), /* before position */ position /* before opening paren */(/* before substring */ 'om' /* before in */ in /* before string */ 'Thomas' /* before closing paren */) /* after position */, overlay('Txxxxas' placing 'hom' from 2 for 4), overlay('Txxxxas' placing 'hom' from 2), overlay(), overlay('Txxxxas', 'hom', 2, count => 4), + overlay(a_very_long_string_expression placing a_very_long_replacement_expression from a_very_long_start_expression for a_very_long_count_expression), + overlay(a_very_long_string_expression, + a_very_long_replacement_expression, + a_very_long_start_expression, + a_very_long_count_name => a_very_long_count_expression), /* before overlay */ overlay /* before opening paren */(/* before string */ 'Txxxxas' /* before placing */ placing /* before replacement */ 'hom' /* before from */ from /* before start */ 2 /* before for */ for /* before count */ 4 /* before closing paren */) /* after overlay */, overlay /* before opening paren */(/* before string */ 'Txxxxas' /* before comma */, /* before replacement */ 'hom' /* before comma */, @@ -81,6 +102,12 @@ select substring('Thomas' for 3), substring('Thomas' similar '%#"o_a#"_' escape '#'), substring('Thomas', 2, 3), + substring(a_very_long_string_expression from a_very_long_start_expression for a_very_long_count_expression), + substring(a_very_long_string_expression for a_very_long_count_expression from a_very_long_start_expression), + substring(a_very_long_string_expression similar a_very_long_pattern_expression escape a_very_long_escape_expression), + substring(a_very_long_string_expression, + a_very_long_start_expression, + a_very_long_count_expression), substring('Thomas' /* before comma */, /* before start */ 2, /* before count */ 3), @@ -91,6 +118,14 @@ select trim(trailing 'x' from 'foox'), trim('x' from 'xfoox'), trim(foo, bar), + trim(both a_very_long_trim_character_expression from a_very_long_trim_input_expression), + trim(leading from a_very_long_first_trim_input_expression, + a_very_long_second_trim_input_expression), + trim(a_very_long_trim_character_expression from a_very_long_first_trim_input_expression, + a_very_long_second_trim_input_expression), + trim(a_very_long_trim_expression, + a_second_very_long_trim_expression, + a_third_very_long_trim_expression), /* before trim */ trim /* before opening paren */(/* before side */ both /* before trim char */ 'x' /* before from */ from /* before string */ 'xfoox' /* before closing paren */) /* after trim */, date_trunc('month', now()), a_very_long_function_name(first_very_long_argument_name, @@ -110,6 +145,14 @@ select array_agg(x order by y desc nulls last, z asc), foo(a => x order by y), array_agg(x order by y using > nulls first), + a_very_long_function_name(all a_very_long_first_quantified_argument_name, + a_very_long_second_quantified_argument_name), + a_very_long_function_name(distinct a_very_long_first_distinct_argument_name, + a_very_long_second_distinct_argument_name), + a_very_long_function_name(variadic a_very_long_variadic_argument_expression_that_forces_wrapping), + a_very_long_function_name(a_very_long_first_argument_name => a_very_long_first_argument_expression, + a_very_long_second_argument_name := a_very_long_second_argument_expression), + a_very_long_aggregate_name(a_very_long_aggregate_argument_expression order by a_very_long_first_order_expression desc nulls last, a_very_long_second_order_expression using > nulls first), array_agg(x /* before order */ order /* before by */ by /* before first */ y /* before desc */ desc /* before nulls */ nulls /* before last */ last /* before comma */, /* before second */ z /* before asc */ asc), json_arrayagg(v), json_arrayagg(v format json @@ -169,6 +212,7 @@ select json_exists(doc, '$' empty array on error), json_exists(doc, '$' empty object on error), json_scalar(1), + json_scalar(a_very_long_json_scalar_input_expression_that_forces_the_builtin_to_wrap), json_scalar(), json_scalar /* before opening paren */(/* before expression */ 1 /* before closing paren */), json_serialize(doc), @@ -279,6 +323,9 @@ select first_value(x) ignore nulls, last_value(y) respect nulls, first_value(x) /* before treatment */ ignore /* before nulls */ nulls /* after nulls */, + a_very_long_ordered_set_aggregate_name(a_very_long_direct_argument_expression) within group (order by a_very_long_ordered_set_expression desc nulls last), + a_very_long_filtered_aggregate_name(a_very_long_filter_argument_expression) filter (where a_very_long_filter_condition_expression > a_very_long_filter_threshold_expression), + a_very_long_window_function_name(a_very_long_null_treatment_argument_expression) ignore nulls, sum(x) over window_name, count(*) over (), avg(x) over (w @@ -325,6 +372,10 @@ select treat(2 as bigint), 1::int8, int8 '1', + cast(a_very_long_cast_input_expression as a_very_long_type_schema_name.a_very_long_cast_type_name), + treat(a_very_long_treat_input_expression as a_very_long_type_schema_name.a_very_long_treat_type_name), + a_very_long_postgres_cast_input_expression::a_very_long_type_schema_name.a_very_long_postgres_cast_type_name, + a_very_long_type_schema_name.a_very_long_typed_literal_type_name 'a very long typed literal input value', -- field expr foo.bar, foo.bar.baz, @@ -332,23 +383,30 @@ select (foo).bar, (/* after opening paren */ foo /* before closing paren */).bar, foo /* before dot */./* before field */ bar, + a_very_long_field_base_expression.a_very_long_first_field_name.a_very_long_second_field_name, + (a_very_long_parenthesized_field_base_expression_that_forces_wrapping).a_very_long_field_name_that_forces_wrapping, -- index expr a[1], a[1][2], a[1 + 2], a /* before bracket */[/* before index */ 1 /* before closing bracket */], + a_very_long_indexed_expression[a_very_long_first_index_expression][a_very_long_second_index_expression + a_very_long_index_offset_expression], -- literal 42, + 'a very long literal expression value that forces the literal expression target to wrap across the configured line width', -- name ref foo, foo, "Mixed", U&"@0066@006f@006f" uescape '@', + a_very_long_unquoted_name_reference_that_forces_the_name_reference_target_to_wrap_past_eighty_characters, + U&"a@005fvery@005flong@005funicode@005fname@005freference" uescape '@', -- paren expr (1), (1 + 2), ((1)), (/* before expr */ 1 /* before closing paren */), + (a_very_long_parenthesized_expression + a_second_very_long_parenthesized_expression), -- postfix expr 1 isnull, 2 notnull, @@ -369,12 +427,16 @@ select x is not json value, x is not normalized, x is not nfkd normalized, + a_very_long_json_postfix_input_expression_that_forces_wrapping is json array with unique keys, + a_very_long_normalized_postfix_input_expression_that_forces_wrapping is not nfkd normalized, -- prefix expr @-@ 10, operator(public.+) /* after op */ 1, +1, -1, not true, + not -+a_very_long_prefix_input_expression_that_is_long_enough_to_force_wrapping_past_eighty_characters, + operator(a_very_long_operator_schema_name.###) a_very_long_custom_prefix_input_expression, -- slice expr c[:2][2:], c[1:2], @@ -382,6 +444,7 @@ select c[:3], c[:], c /* before bracket */[/* before start */ 1 /* before colon */:/* before end */ 2 /* before closing bracket */], + a_very_long_sliced_expression[a_very_long_slice_start_expression:a_very_long_slice_end_expression][a_second_very_long_slice_start_expression:a_second_very_long_slice_end_expression], -- tuple expr (1, 2, 3), (first_very_long_tuple_expression, @@ -391,5 +454,13 @@ select row(1), row /* before opening paren */(1), row(1, 2), + row(a_very_long_first_row_expression, + a_very_long_second_row_expression, + a_very_long_third_row_expression), (/* before first */ 1 /* before comma */, /* before second */ 2 /* before closing paren */); + +select + a_very_long_function_name(first_very_long_argument_name, + second_very_long_argument_name, + third_very_long_argument_name); diff --git a/crates/squawk_fmt/tests/after/select_literals.snap b/crates/squawk_fmt/tests/after/select_literals.snap index 20fe325b..92aa1f34 100644 --- a/crates/squawk_fmt/tests/after/select_literals.snap +++ b/crates/squawk_fmt/tests/after/select_literals.snap @@ -53,3 +53,30 @@ select select x'AF' 'BE'; + +select + null as a_very_long_null_literal_column_alias, + true as a_very_long_true_literal_column_alias, + false as a_very_long_false_literal_column_alias, + $1234567890 as a_very_long_positional_parameter_column_alias; +select + 1234567890123456789012345678901234567890, + 12345678901234567890.12345678901234567890, + 'a very long ordinary string literal value that forces the literal target list to wrap', + e'a very long escaped string literal value that forces the literal target list to wrap\n', + u&'a very long unicode string literal value that forces the literal target list to wrap', + $$a very long dollar quoted string literal value that forces the literal target list to wrap$$, + $a_very_long_dollar_quote_tag$a very long tagged dollar quoted string literal value$a_very_long_dollar_quote_tag$, + b'10101010101010101010101010101010101010101010101010101010101010101010101010101010', + x'ABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEF'; +select + 'a very long continued ordinary string literal value' + 'with a very long continuation segment', + e'a very long continued escaped string literal value\n' + 'with a very long continuation segment', + u&'a very long continued unicode string literal value' + 'with a very long continuation segment', + b'1010101010101010101010101010101010101010' + '0101010101010101010101010101010101010101', + x'ABCDEFABCDEFABCDEFABCDEFABCDEFABCDEF' + '123456123456123456123456123456123456'; diff --git a/crates/squawk_fmt/tests/after/table_constraints.snap b/crates/squawk_fmt/tests/after/table_constraints.snap index da6c550e..f38277c7 100644 --- a/crates/squawk_fmt/tests/after/table_constraints.snap +++ b/crates/squawk_fmt/tests/after/table_constraints.snap @@ -51,6 +51,13 @@ create table using_indexes( primary key using index existing_primary ); +create table a_very_long_table_name_using_existing_indexes( + a_very_long_first_identifier_column_name bigint, + a_very_long_second_identifier_column_name bigint, + unique using index a_very_long_existing_unique_index_name, + primary key using index a_very_long_existing_primary_index_name +); + create table commented_constraints( id bigint, parent_id bigint, diff --git a/crates/squawk_fmt/tests/after/types.snap b/crates/squawk_fmt/tests/after/types.snap index bb7da10c..48a65f60 100644 --- a/crates/squawk_fmt/tests/after/types.snap +++ b/crates/squawk_fmt/tests/after/types.snap @@ -135,3 +135,23 @@ create table t( select 1::double -- eight precision; + +create table a_very_long_table_name_for_type_wrapping( + a_very_long_numeric_column_name numeric(12345, 12345), + a_very_long_varchar_column_name varchar(12345), + a_very_long_character_varying_column_name character varying(12345), + a_very_long_national_character_varying_column_name national character varying(12345), + a_very_long_nchar_column_name nchar(12345), + a_very_long_bit_varying_column_name bit varying(12345), + a_very_long_double_precision_column_name double precision, + a_very_long_timestamp_column_name timestamp(12345) without time zone, + a_very_long_time_column_name time(12345) with time zone, + a_very_long_interval_column_name interval day to second(12345), + a_very_long_array_column_name text[12345][12345] +); +select + a_very_long_expression_name::a_very_long_type_schema_name.a_very_long_type_name, + cast(a_very_long_expression_name as character varying(12345)), + treat(a_very_long_expression_name as a_very_long_type_schema_name.a_very_long_type_name), + a_very_long_type_schema_name.a_very_long_type_name(12345) 'a very long typed string literal value', + interval 'a very long interval literal value' day to second(12345); diff --git a/crates/squawk_fmt/tests/before/create_table.sql b/crates/squawk_fmt/tests/before/create_table.sql index 52bcaff2..d7e1dd74 100644 --- a/crates/squawk_fmt/tests/before/create_table.sql +++ b/crates/squawk_fmt/tests/before/create_table.sql @@ -39,3 +39,5 @@ create table t (a int, b int -- two -- line comment before the semicolon create table t (a int) -- one ; + +create table a_very_long_schema_name.a_very_long_table_name (a_very_long_integer_column_name int, a_very_long_text_column_name text, a_very_long_qualified_type_column_name a_very_long_type_schema_name.a_very_long_type_name, U&"c!006fl" uescape '!' a_very_long_type_schema_name.a_very_long_type_name); diff --git a/crates/squawk_fmt/tests/before/create_table_like.sql b/crates/squawk_fmt/tests/before/create_table_like.sql index ddcb1db4..8f698423 100644 --- a/crates/squawk_fmt/tests/before/create_table_like.sql +++ b/crates/squawk_fmt/tests/before/create_table_like.sql @@ -19,3 +19,5 @@ create table t (like U&"d!0061tum" uescape '!' including all); create table t (like /* a */ foo /* b */ including /* c */ all); create table t (like foo -- a line comment including all); + +create table a_very_long_destination_table_name (like a_very_long_source_schema_name.a_very_long_source_table_name including comments including compression including constraints including defaults including generated including identity including indexes including statistics including storage); diff --git a/crates/squawk_fmt/tests/before/custom_operator.sql b/crates/squawk_fmt/tests/before/custom_operator.sql index 949bd8ba..33b823fa 100644 --- a/crates/squawk_fmt/tests/before/custom_operator.sql +++ b/crates/squawk_fmt/tests/before/custom_operator.sql @@ -1,2 +1,3 @@ select 1 /*before*/ <<<< /*after*/ 1; select ##### /*after*/ 1; +select a_very_long_left_operand_name <<<< a_very_long_right_operand_name, a_second_very_long_left_operand_name ##### a_second_very_long_right_operand_name; diff --git a/crates/squawk_fmt/tests/before/from.sql b/crates/squawk_fmt/tests/before/from.sql index 338400d8..42856827 100644 --- a/crates/squawk_fmt/tests/before/from.sql +++ b/crates/squawk_fmt/tests/before/from.sql @@ -14,3 +14,8 @@ select * /* before star */ * /* before alias */ as /* before alias name */ f /* before item comma */, /* before second item */ other /* before second alias */ o; + +select * from a_very_long_schema_name.a_very_long_relation_name as a_very_long_relation_alias (a_very_long_first_column_alias, a_very_long_second_column_alias, a_very_long_third_column_alias); +select * from a_very_long_relation_name a_very_long_relation_alias (a_very_long_first_column_name a_very_long_type_schema.a_very_long_first_type_name, a_very_long_second_column_name a_very_long_type_schema.a_very_long_second_type_name collate a_very_long_collation_name); +select * from a_very_long_relation_name tablesample bernoulli(a_very_long_sampling_percentage_expression) repeatable (a_very_long_repeatable_seed_expression); +select * from only a_very_long_schema_name.a_very_long_first_relation_name * as a_very_long_first_alias, a_very_long_schema_name.a_very_long_second_relation_name as a_very_long_second_alias; diff --git a/crates/squawk_fmt/tests/before/select.sql b/crates/squawk_fmt/tests/before/select.sql index 04c31722..9a8a7959 100644 --- a/crates/squawk_fmt/tests/before/select.sql +++ b/crates/squawk_fmt/tests/before/select.sql @@ -14,3 +14,5 @@ select 1 "foo", 2 "filter", 3 "day", 4 "array", 5 "Mixed"; select 1 as "foo", 2 as "filter", 3 as "day", 4 as "array"; select 1 /*a*/group /* b */by/*c */ 1; + +select a_very_long_first_target_expression as a_very_long_first_column_alias, a_very_long_second_target_expression a_very_long_second_column_alias, a_very_long_third_target_expression as "A Very Long Quoted Third Column Alias" from a_very_long_schema_name.a_very_long_table_name as a_very_long_table_alias group by a_very_long_first_target_expression, a_very_long_second_target_expression, a_very_long_third_target_expression; diff --git a/crates/squawk_fmt/tests/before/select_expr.sql b/crates/squawk_fmt/tests/before/select_expr.sql index eb0a6dd5..6371c1e2 100644 --- a/crates/squawk_fmt/tests/before/select_expr.sql +++ b/crates/squawk_fmt/tests/before/select_expr.sql @@ -3,11 +3,15 @@ select array[1,2], array(select 1), array[[1,2],[3,4]], + array[a_very_long_first_array_expression, a_very_long_second_array_expression, a_very_long_third_array_expression], + array(select a_very_long_array_select_expression from a_very_long_array_select_relation_name), + array[[a_very_long_first_nested_array_expression, a_very_long_second_nested_array_expression], [a_very_long_third_nested_array_expression, a_very_long_fourth_nested_array_expression]], -- between expr 2 between 1 and 3, 2 not between 1 and 3, 2 between asymmetric 1 and 3, 2 between symmetric 1 and 3, + a_very_long_between_target_expression between symmetric a_very_long_between_start_expression and a_very_long_between_end_expression, -- bin expr 1 + 1, 1 /* before op */ + /* after op */ 1, @@ -46,25 +50,35 @@ select 'foo' similar to 'f%', 6 / 2, 2 * 3, + a_very_long_binary_left_expression + a_very_long_binary_middle_expression * a_very_long_binary_right_expression, -- call expr 1 = ANY ( ARRAY [ 1 , 2 ] ), 2 = ALL ( SELECT x FROM things ), 3 = SOME ( ARRAY [ 3 ] ), 4 /* before op */ = /* before any */ ANY /* before opening paren */ ( /* before expr */ ARRAY [ 4 ] /* before closing paren */ ) /* after any */, EXISTS ( SELECT 1 FROM things ), + a_very_long_any_comparison_expression = ANY(a_very_long_any_input_expression_that_forces_wrapping), + a_very_long_all_comparison_expression = ALL(SELECT a_very_long_all_select_expression FROM a_very_long_all_relation_name), + a_very_long_some_comparison_expression = SOME(a_very_long_some_input_expression), + EXISTS(SELECT a_very_long_exists_select_expression FROM a_very_long_exists_relation_name), /* before exists */ EXISTS /* before opening paren */ ( /* before select */ SELECT 1 /* before closing paren */ ) /* after exists */, COLLATION FOR ( b + c ), + COLLATION FOR ( a_very_long_collation_input_expression_that_forces_the_builtin_to_wrap ), /* before collation */ COLLATION /* before for */ FOR /* before opening paren */ ( /* before expr */ foo /* before closing paren */ ) /* after collation */, EXTRACT ( YEAR FROM TIMESTAMP '2001-02-16 20:38:40' ), EXTRACT ( 'month' FROM ts ), EXTRACT ( "Field" FROM ts ), + EXTRACT(a_very_long_field_name FROM a_very_long_timestamp_expression_that_forces_the_builtin_to_wrap), /* before extract */ EXTRACT /* before opening paren */ ( /* before field */ DAY /* before from */ FROM /* before expr */ ts /* before closing paren */ ) /* after extract */, POSITION ( 'om' IN 'Thomas' ), + POSITION(a_very_long_substring_expression_that_forces_wrapping IN a_very_long_string_expression_that_forces_wrapping), /* before position */ POSITION /* before opening paren */ ( /* before substring */ 'om' /* before in */ IN /* before string */ 'Thomas' /* before closing paren */ ) /* after position */, OVERLAY ( 'Txxxxas' PLACING 'hom' FROM 2 FOR 4 ), OVERLAY ( 'Txxxxas' PLACING 'hom' FROM 2 ), OVERLAY ( ), OVERLAY ( 'Txxxxas', 'hom', 2, count => 4 ), + OVERLAY(a_very_long_string_expression PLACING a_very_long_replacement_expression FROM a_very_long_start_expression FOR a_very_long_count_expression), + OVERLAY(a_very_long_string_expression, a_very_long_replacement_expression, a_very_long_start_expression, a_very_long_count_name => a_very_long_count_expression), /* before overlay */ OVERLAY /* before opening paren */ ( /* before string */ 'Txxxxas' /* before placing */ PLACING /* before replacement */ 'hom' /* before from */ FROM /* before start */ 2 /* before for */ FOR /* before count */ 4 /* before closing paren */ ) /* after overlay */, OVERLAY /* before opening paren */ ( /* before string */ 'Txxxxas' /* before comma */, /* before replacement */ 'hom' /* before comma */, /* before start */ 2 /* before comma */, /* before name */ count /* before arrow */ => /* before count */ 4 /* before closing paren */ ), SUBSTRING ( 'Thomas' FROM 2 FOR 3 ), @@ -73,6 +87,10 @@ select SUBSTRING ( 'Thomas' FOR 3 ), SUBSTRING ( 'Thomas' SIMILAR '%#"o_a#"_' ESCAPE '#' ), SUBSTRING ( 'Thomas', 2, 3 ), + SUBSTRING(a_very_long_string_expression FROM a_very_long_start_expression FOR a_very_long_count_expression), + SUBSTRING(a_very_long_string_expression FOR a_very_long_count_expression FROM a_very_long_start_expression), + SUBSTRING(a_very_long_string_expression SIMILAR a_very_long_pattern_expression ESCAPE a_very_long_escape_expression), + SUBSTRING(a_very_long_string_expression, a_very_long_start_expression, a_very_long_count_expression), SUBSTRING ( 'Thomas' /* before comma */, /* before start */ 2, /* before count */ 3 ), /* before substring */ SUBSTRING /* before opening paren */ ( /* before string */ 'Thomas' /* before from */ FROM /* before start */ 2 /* before for */ FOR /* before count */ 3 /* before closing paren */ ) /* after substring */, TRIM ( ' foo ' ), @@ -81,6 +99,10 @@ select TRIM ( TRAILING 'x' FROM 'foox' ), TRIM ( 'x' FROM 'xfoox' ), TRIM ( foo, bar ), + TRIM(BOTH a_very_long_trim_character_expression FROM a_very_long_trim_input_expression), + TRIM(LEADING FROM a_very_long_first_trim_input_expression, a_very_long_second_trim_input_expression), + TRIM(a_very_long_trim_character_expression FROM a_very_long_first_trim_input_expression, a_very_long_second_trim_input_expression), + TRIM(a_very_long_trim_expression, a_second_very_long_trim_expression, a_third_very_long_trim_expression), /* before trim */ TRIM /* before opening paren */ ( /* before side */ BOTH /* before trim char */ 'x' /* before from */ FROM /* before string */ 'xfoox' /* before closing paren */ ) /* after trim */, date_trunc('month', now()), a_very_long_function_name(first_very_long_argument_name, second_very_long_argument_name, third_very_long_argument_name), @@ -97,6 +119,11 @@ select array_agg(x order by y desc nulls last, z asc), foo(a => x order by y), array_agg(x order by y using > nulls first), + a_very_long_function_name(ALL a_very_long_first_quantified_argument_name, a_very_long_second_quantified_argument_name), + a_very_long_function_name(DISTINCT a_very_long_first_distinct_argument_name, a_very_long_second_distinct_argument_name), + a_very_long_function_name(VARIADIC a_very_long_variadic_argument_expression_that_forces_wrapping), + a_very_long_function_name(a_very_long_first_argument_name => a_very_long_first_argument_expression, a_very_long_second_argument_name := a_very_long_second_argument_expression), + a_very_long_aggregate_name(a_very_long_aggregate_argument_expression ORDER BY a_very_long_first_order_expression DESC NULLS LAST, a_very_long_second_order_expression USING > NULLS FIRST), array_agg(x /* before order */ order /* before by */ by /* before first */ y /* before desc */ desc /* before nulls */ nulls /* before last */ last /* before comma */, /* before second */ z /* before asc */ asc), JSON_ARRAYAGG(v), JSON_ARRAYAGG(v FORMAT JSON ORDER BY sort_col DESC NULL ON NULL RETURNING jsonb FORMAT JSON), @@ -119,6 +146,7 @@ select JSON_EXISTS(doc, '$' EMPTY ARRAY ON ERROR), JSON_EXISTS(doc, '$' EMPTY OBJECT ON ERROR), JSON_SCALAR(1), + JSON_SCALAR(a_very_long_json_scalar_input_expression_that_forces_the_builtin_to_wrap), JSON_SCALAR(), JSON_SCALAR /* before opening paren */ (/* before expression */ 1 /* before closing paren */), JSON_SERIALIZE(doc), @@ -159,6 +187,9 @@ select first_value(x) IGNORE NULLS, last_value(y) RESPECT NULLS, first_value(x) /* before treatment */ IGNORE /* before nulls */ NULLS /* after nulls */, + a_very_long_ordered_set_aggregate_name(a_very_long_direct_argument_expression) WITHIN GROUP (ORDER BY a_very_long_ordered_set_expression DESC NULLS LAST), + a_very_long_filtered_aggregate_name(a_very_long_filter_argument_expression) FILTER (WHERE a_very_long_filter_condition_expression > a_very_long_filter_threshold_expression), + a_very_long_window_function_name(a_very_long_null_treatment_argument_expression) IGNORE NULLS, sum(x) OVER window_name, count(*) OVER (), avg(x) OVER (w PARTITION BY a, b ORDER BY c DESC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW EXCLUDE TIES), @@ -178,6 +209,10 @@ select treat(2 as bigint), 1::int8, int8 '1', + cast(a_very_long_cast_input_expression as a_very_long_type_schema_name.a_very_long_cast_type_name), + treat(a_very_long_treat_input_expression as a_very_long_type_schema_name.a_very_long_treat_type_name), + a_very_long_postgres_cast_input_expression::a_very_long_type_schema_name.a_very_long_postgres_cast_type_name, + a_very_long_type_schema_name.a_very_long_typed_literal_type_name 'a very long typed literal input value', -- field expr foo . bar, foo.bar.baz, @@ -185,23 +220,30 @@ select (foo) . bar, ( /* after opening paren */ foo /* before closing paren */ ) . bar, foo /* before dot */ . /* before field */ "bar", + a_very_long_field_base_expression.a_very_long_first_field_name.a_very_long_second_field_name, + (a_very_long_parenthesized_field_base_expression_that_forces_wrapping).a_very_long_field_name_that_forces_wrapping, -- index expr a [ 1 ], a[1][2], a[1 + 2], a /* before bracket */ [ /* before index */ 1 /* before closing bracket */ ], + a_very_long_indexed_expression[a_very_long_first_index_expression][a_very_long_second_index_expression + a_very_long_index_offset_expression], -- literal 42, + 'a very long literal expression value that forces the literal expression target to wrap across the configured line width', -- name ref FOO, "foo", "Mixed", U&"@0066@006f@006f" UESCAPE '@', + a_very_long_unquoted_name_reference_that_forces_the_name_reference_target_to_wrap_past_eighty_characters, + U&"a@005fvery@005flong@005funicode@005fname@005freference" UESCAPE '@', -- paren expr ( 1 ), ( 1 + 2 ), ( ( 1 ) ), ( /* before expr */ 1 /* before closing paren */ ), + (a_very_long_parenthesized_expression + a_second_very_long_parenthesized_expression), -- postfix expr 1 isnull, 2 notnull, @@ -222,12 +264,16 @@ select x is not json value, x is not normalized, x is not nfkd normalized, + a_very_long_json_postfix_input_expression_that_forces_wrapping is json array with unique keys, + a_very_long_normalized_postfix_input_expression_that_forces_wrapping is not nfkd normalized, -- prefix expr @-@ 10, OPERATOR ( PUBLIC . + ) /* after op */ 1, + 1, - 1, not true, + not - + a_very_long_prefix_input_expression_that_is_long_enough_to_force_wrapping_past_eighty_characters, + OPERATOR(a_very_long_operator_schema_name.###) a_very_long_custom_prefix_input_expression, -- slice expr c[:2][2:], c[1:2], @@ -235,6 +281,7 @@ select c[:3], c[:], c /* before bracket */ [ /* before start */ 1 /* before colon */ : /* before end */ 2 /* before closing bracket */ ], + a_very_long_sliced_expression[a_very_long_slice_start_expression:a_very_long_slice_end_expression][a_second_very_long_slice_start_expression:a_second_very_long_slice_end_expression], -- tuple expr ( 1 , 2 , 3 ), (first_very_long_tuple_expression, second_very_long_tuple_expression, third_very_long_tuple_expression), @@ -242,4 +289,7 @@ select row ( 1 ), row /* before opening paren */ ( 1 ), row ( 1 , 2 ), + row(a_very_long_first_row_expression, a_very_long_second_row_expression, a_very_long_third_row_expression), ( /* before first */ 1 /* before comma */ , /* before second */ 2 /* before closing paren */ ); + +select a_very_long_function_name(first_very_long_argument_name, second_very_long_argument_name, third_very_long_argument_name); diff --git a/crates/squawk_fmt/tests/before/select_literals.sql b/crates/squawk_fmt/tests/before/select_literals.sql index 74077f09..a2d95475 100644 --- a/crates/squawk_fmt/tests/before/select_literals.sql +++ b/crates/squawk_fmt/tests/before/select_literals.sql @@ -43,3 +43,12 @@ select B'1010' -- byte string continuation select X'AF' 'BE'; + +select null as a_very_long_null_literal_column_alias, true as a_very_long_true_literal_column_alias, false as a_very_long_false_literal_column_alias, $1234567890 as a_very_long_positional_parameter_column_alias; +select 1234567890123456789012345678901234567890, 12345678901234567890.12345678901234567890, 'a very long ordinary string literal value that forces the literal target list to wrap', E'a very long escaped string literal value that forces the literal target list to wrap\n', U&'a very long unicode string literal value that forces the literal target list to wrap', $$a very long dollar quoted string literal value that forces the literal target list to wrap$$, $a_very_long_dollar_quote_tag$a very long tagged dollar quoted string literal value$a_very_long_dollar_quote_tag$, B'10101010101010101010101010101010101010101010101010101010101010101010101010101010', X'ABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEF'; +select 'a very long continued ordinary string literal value' + 'with a very long continuation segment', E'a very long continued escaped string literal value\n' + 'with a very long continuation segment', U&'a very long continued unicode string literal value' + 'with a very long continuation segment', B'1010101010101010101010101010101010101010' + '0101010101010101010101010101010101010101', X'ABCDEFABCDEFABCDEFABCDEFABCDEFABCDEF' + '123456123456123456123456123456123456'; diff --git a/crates/squawk_fmt/tests/before/table_constraints.sql b/crates/squawk_fmt/tests/before/table_constraints.sql index d837c44f..d1357f05 100644 --- a/crates/squawk_fmt/tests/before/table_constraints.sql +++ b/crates/squawk_fmt/tests/before/table_constraints.sql @@ -24,6 +24,8 @@ create table using_indexes ( PRIMARY KEY USING INDEX existing_primary ); +create table a_very_long_table_name_using_existing_indexes (a_very_long_first_identifier_column_name bigint, a_very_long_second_identifier_column_name bigint, unique using index a_very_long_existing_unique_index_name, primary key using index a_very_long_existing_primary_index_name); + create table commented_constraints ( id bigint, parent_id bigint, diff --git a/crates/squawk_fmt/tests/before/types.sql b/crates/squawk_fmt/tests/before/types.sql index 2abe5ecb..88d38c85 100644 --- a/crates/squawk_fmt/tests/before/types.sql +++ b/crates/squawk_fmt/tests/before/types.sql @@ -97,3 +97,6 @@ create table t ( ); select 1::DOUBLE -- eight PRECISION; + +create table a_very_long_table_name_for_type_wrapping (a_very_long_numeric_column_name numeric(12345, 12345), a_very_long_varchar_column_name varchar(12345), a_very_long_character_varying_column_name character varying(12345), a_very_long_national_character_varying_column_name national character varying(12345), a_very_long_nchar_column_name nchar(12345), a_very_long_bit_varying_column_name bit varying(12345), a_very_long_double_precision_column_name double precision, a_very_long_timestamp_column_name timestamp(12345) without time zone, a_very_long_time_column_name time(12345) with time zone, a_very_long_interval_column_name interval day to second(12345), a_very_long_array_column_name text[12345][12345]); +select a_very_long_expression_name::a_very_long_type_schema_name.a_very_long_type_name, cast(a_very_long_expression_name as character varying(12345)), treat(a_very_long_expression_name as a_very_long_type_schema_name.a_very_long_type_name), a_very_long_type_schema_name.a_very_long_type_name(12345) 'a very long typed string literal value', interval 'a very long interval literal value' day to second(12345); From f130aec3c098cf00ab37cea9cd7f691422323072 Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Mon, 24 Aug 2026 18:41:07 -0400 Subject: [PATCH 14/17] fmt: wrap func calls --- crates/squawk_fmt/src/fmt.rs | 110 +++++++++++------- crates/squawk_fmt/tests/after/from.snap | 4 +- .../squawk_fmt/tests/after/select_expr.snap | 102 +++++++++++----- crates/squawk_fmt/tests/after/types.snap | 32 ++++- crates/squawk_fmt/tests/before/types.sql | 3 +- 5 files changed, 172 insertions(+), 79 deletions(-) diff --git a/crates/squawk_fmt/src/fmt.rs b/crates/squawk_fmt/src/fmt.rs index 54996e31..09476bbd 100644 --- a/crates/squawk_fmt/src/fmt.rs +++ b/crates/squawk_fmt/src/fmt.rs @@ -3390,53 +3390,68 @@ fn build_call_arg_list<'a>(arg_list: ast::ArgList) -> Doc<'a> { } doc = doc.append(Doc::text("(")); + let mut body = Doc::nil(); + let mut has_body = false; + let mut has_args = false; if let Some(star) = arg_list.star_token() { - doc = doc + has_body = true; + body = body .append(leading_comments_token(&star)) .append(Doc::text("*")); - if let Some(r_paren) = arg_list.r_paren_token() { - doc = doc.append(comments_before(r_paren)); + } else { + let mut has_quantifier = false; + if let Some(quantifier) = arg_list.all_or_distinct() { + has_body = true; + has_quantifier = true; + body = body + .append(leading_comments(quantifier.syntax())) + .append(match quantifier { + ast::AllOrDistinct::All(_) => Doc::text("all"), + ast::AllOrDistinct::Distinct(_) => Doc::text("distinct"), + }); } - return doc.append(Doc::text(")")); - } - let mut has_quantifier = false; - if let Some(quantifier) = arg_list.all_or_distinct() { - has_quantifier = true; - doc = doc - .append(leading_comments(quantifier.syntax())) - .append(match quantifier { - ast::AllOrDistinct::All(_) => Doc::text("all"), - ast::AllOrDistinct::Distinct(_) => Doc::text("distinct"), - }); + let args: Vec> = arg_list + .args() + .map(|arg| { + let leading = leading_comments(arg.syntax()); + let trailing = trailing_comments(arg.syntax()); + leading.append(build_call_arg(arg)).append(trailing) + }) + .collect(); + if !args.is_empty() { + has_body = true; + has_args = true; + if has_quantifier { + body = body.append(Doc::space()); + } + body = body.append( + Doc::list( + Itertools::intersperse( + args.into_iter(), + Doc::text(",").append(Doc::line_or_space()), + ) + .collect(), + ) + .group(), + ); + } } - let args: Vec> = arg_list - .args() - .map(|arg| { - let leading = leading_comments(arg.syntax()); - let trailing = trailing_comments(arg.syntax()); - leading.append(build_call_arg(arg)).append(trailing) - }) - .collect(); - if args.is_empty() { + if !has_args { if let Some(r_paren) = arg_list.r_paren_token() { - doc = doc.append(comments_before(r_paren)); - } - } else { - if has_quantifier { - doc = doc.append(Doc::space()); + body = body.append(comments_before(r_paren)); } + } + if has_body { doc = doc.append( - Doc::list( - Itertools::intersperse( - args.into_iter(), - Doc::text(",").append(Doc::line_or_space()), - ) - .collect(), - ) - .nest(2), + Doc::line_or_nil() + .append(body) + .nest(2) + .append(Doc::line_or_nil()), ); + } else { + doc = doc.append(body); } doc.append(Doc::text(")")).group() @@ -3676,20 +3691,27 @@ fn build_cast_expr<'a>(cast_expr: ast::CastExpr) -> Doc<'a> { if let Some(l_paren) = cast_expr.l_paren_token() { doc = doc.append(comments_before(l_paren)); } - doc = doc - .append(Doc::text("(")) - .append(leading_comments(expr.syntax())) + let mut body = leading_comments(expr.syntax()) .append(build_expr(expr)) - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments_token(&as_token)) .append(Doc::text("as")) - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments(ty.syntax())) - .append(build_type(ty)); + .append(build_type(ty)) + .group(); if let Some(r_paren) = cast_expr.r_paren_token() { - doc = doc.append(comments_before(r_paren)); + body = body.append(comments_before(r_paren)); } - doc = doc.append(Doc::text(")")) + doc = doc + .append(Doc::text("(")) + .append( + Doc::line_or_nil() + .append(body) + .nest(2) + .append(Doc::line_or_nil()), + ) + .append(Doc::text(")")) } else { let literal = cast_expr.literal().unwrap(); doc = doc diff --git a/crates/squawk_fmt/tests/after/from.snap b/crates/squawk_fmt/tests/after/from.snap index 16290010..4b1cb616 100644 --- a/crates/squawk_fmt/tests/after/from.snap +++ b/crates/squawk_fmt/tests/after/from.snap @@ -22,7 +22,9 @@ select * from a_very_long_relation_name a_very_long_relation_alias(a_very_long_first_column_name a_very_long_type_schema.a_very_long_first_type_name, a_very_long_second_column_name a_very_long_type_schema.a_very_long_second_type_name collate a_very_long_collation_name); select * -from a_very_long_relation_name tablesample bernoulli(a_very_long_sampling_percentage_expression) repeatable(a_very_long_repeatable_seed_expression); +from a_very_long_relation_name tablesample bernoulli( + a_very_long_sampling_percentage_expression + ) repeatable(a_very_long_repeatable_seed_expression); select * from only a_very_long_schema_name.a_very_long_first_relation_name * as a_very_long_first_alias, a_very_long_schema_name.a_very_long_second_relation_name as a_very_long_second_alias; diff --git a/crates/squawk_fmt/tests/after/select_expr.snap b/crates/squawk_fmt/tests/after/select_expr.snap index 2b92bfd6..4c591ccc 100644 --- a/crates/squawk_fmt/tests/after/select_expr.snap +++ b/crates/squawk_fmt/tests/after/select_expr.snap @@ -128,9 +128,11 @@ select a_third_very_long_trim_expression), /* before trim */ trim /* before opening paren */(/* before side */ both /* before trim char */ 'x' /* before from */ from /* before string */ 'xfoox' /* before closing paren */) /* after trim */, date_trunc('month', now()), - a_very_long_function_name(first_very_long_argument_name, + a_very_long_function_name( + first_very_long_argument_name, second_very_long_argument_name, - third_very_long_argument_name), + third_very_long_argument_name + ), foo(), foo(*), foo(all 1, 2), @@ -140,20 +142,34 @@ select foo(variadic xs), foo(/* before variadic */ variadic /* after variadic */ xs), foo(a => 1, b := 2), - foo(/* before name */ a /* before arrow */ => /* before value */ 1 /* before comma */, - /* before arg */ b /* before assign */ := /* before value 2 */ 2), + foo( + /* before name */ a /* before arrow */ => /* before value */ 1 /* before comma */, + /* before arg */ b /* before assign */ := /* before value 2 */ 2 + ), array_agg(x order by y desc nulls last, z asc), foo(a => x order by y), array_agg(x order by y using > nulls first), - a_very_long_function_name(all a_very_long_first_quantified_argument_name, - a_very_long_second_quantified_argument_name), - a_very_long_function_name(distinct a_very_long_first_distinct_argument_name, - a_very_long_second_distinct_argument_name), - a_very_long_function_name(variadic a_very_long_variadic_argument_expression_that_forces_wrapping), - a_very_long_function_name(a_very_long_first_argument_name => a_very_long_first_argument_expression, - a_very_long_second_argument_name := a_very_long_second_argument_expression), - a_very_long_aggregate_name(a_very_long_aggregate_argument_expression order by a_very_long_first_order_expression desc nulls last, a_very_long_second_order_expression using > nulls first), - array_agg(x /* before order */ order /* before by */ by /* before first */ y /* before desc */ desc /* before nulls */ nulls /* before last */ last /* before comma */, /* before second */ z /* before asc */ asc), + a_very_long_function_name( + all a_very_long_first_quantified_argument_name, + a_very_long_second_quantified_argument_name + ), + a_very_long_function_name( + distinct a_very_long_first_distinct_argument_name, + a_very_long_second_distinct_argument_name + ), + a_very_long_function_name( + variadic a_very_long_variadic_argument_expression_that_forces_wrapping + ), + a_very_long_function_name( + a_very_long_first_argument_name => a_very_long_first_argument_expression, + a_very_long_second_argument_name := a_very_long_second_argument_expression + ), + a_very_long_aggregate_name( + a_very_long_aggregate_argument_expression order by a_very_long_first_order_expression desc nulls last, a_very_long_second_order_expression using > nulls first + ), + array_agg( + x /* before order */ order /* before by */ by /* before first */ y /* before desc */ desc /* before nulls */ nulls /* before last */ last /* before comma */, /* before second */ z /* before asc */ asc + ), json_arrayagg(v), json_arrayagg(v format json order by sort_col desc @@ -313,19 +329,33 @@ select /* before returning */ returning /* before type */ jsonb /* before returning format */ format /* before returning json */ json /* before closing paren */), public.foo(1), - foo /* before opening paren */(/* before first arg */ 1 /* before comma */, - /* before second arg */ 2 /* before closing paren */), + foo /* before opening paren */( + /* before first arg */ 1 /* before comma */, + /* before second arg */ 2 /* before closing paren */ + ), percentile_cont(0.5) within group (order by x), mode() within group (order by y desc), - percentile_disc(0.5) /* before within */ within /* before group */ group /* before opening paren */ (/* before order */ order /* before by */ by /* before sort */ x /* before closing paren */) /* after within */, + percentile_disc( + 0.5 + ) /* before within */ within /* before group */ group /* before opening paren */ (/* before order */ order /* before by */ by /* before sort */ x /* before closing paren */) /* after within */, count(*) filter (where x > 1), - sum(x) /* before filter */ filter /* before opening paren */ (/* before where */ where /* before expr */ x > 0 /* before closing paren */) /* after filter */, + sum( + x + ) /* before filter */ filter /* before opening paren */ (/* before where */ where /* before expr */ x > 0 /* before closing paren */) /* after filter */, first_value(x) ignore nulls, last_value(y) respect nulls, - first_value(x) /* before treatment */ ignore /* before nulls */ nulls /* after nulls */, - a_very_long_ordered_set_aggregate_name(a_very_long_direct_argument_expression) within group (order by a_very_long_ordered_set_expression desc nulls last), - a_very_long_filtered_aggregate_name(a_very_long_filter_argument_expression) filter (where a_very_long_filter_condition_expression > a_very_long_filter_threshold_expression), - a_very_long_window_function_name(a_very_long_null_treatment_argument_expression) ignore nulls, + first_value( + x + ) /* before treatment */ ignore /* before nulls */ nulls /* after nulls */, + a_very_long_ordered_set_aggregate_name( + a_very_long_direct_argument_expression + ) within group (order by a_very_long_ordered_set_expression desc nulls last), + a_very_long_filtered_aggregate_name( + a_very_long_filter_argument_expression + ) filter (where a_very_long_filter_condition_expression > a_very_long_filter_threshold_expression), + a_very_long_window_function_name( + a_very_long_null_treatment_argument_expression + ) ignore nulls, sum(x) over window_name, count(*) over (), avg(x) over (w @@ -343,7 +373,9 @@ select rows between a_very_long_frame_start_expression preceding and a_very_long_frame_end_expression following exclude ties), - sum(x) /* before over */ over /* before target */ (/* before partition */ partition /* before by */ by /* before expr */ a /* before comma */, + sum( + x + ) /* before over */ over /* before target */ (/* before partition */ partition /* before by */ by /* before expr */ a /* before comma */, /* before second */ b /* before order */ order /* before order by */ by /* before sort */ c /* before rows */ rows /* before between */ between /* before start */ 1 /* before preceding */ preceding @@ -368,12 +400,24 @@ select /* after else */ 3 /* before end */ end, -- cast expr - cast(1 as int8), - treat(2 as bigint), + cast( + 1 as int8 + ), + treat( + 2 as bigint + ), 1::int8, int8 '1', - cast(a_very_long_cast_input_expression as a_very_long_type_schema_name.a_very_long_cast_type_name), - treat(a_very_long_treat_input_expression as a_very_long_type_schema_name.a_very_long_treat_type_name), + cast( + a_very_long_cast_input_expression + as + a_very_long_type_schema_name.a_very_long_cast_type_name + ), + treat( + a_very_long_treat_input_expression + as + a_very_long_type_schema_name.a_very_long_treat_type_name + ), a_very_long_postgres_cast_input_expression::a_very_long_type_schema_name.a_very_long_postgres_cast_type_name, a_very_long_type_schema_name.a_very_long_typed_literal_type_name 'a very long typed literal input value', -- field expr @@ -461,6 +505,8 @@ select /* before second */ 2 /* before closing paren */); select - a_very_long_function_name(first_very_long_argument_name, + a_very_long_function_name( + first_very_long_argument_name, second_very_long_argument_name, - third_very_long_argument_name); + third_very_long_argument_name + ); diff --git a/crates/squawk_fmt/tests/after/types.snap b/crates/squawk_fmt/tests/after/types.snap index 48a65f60..99d61225 100644 --- a/crates/squawk_fmt/tests/after/types.snap +++ b/crates/squawk_fmt/tests/after/types.snap @@ -61,8 +61,12 @@ create table t( -- types in casts select 1::int8, - cast(1 as int8), - treat(2 as bigint), + cast( + 1 as int8 + ), + treat( + 2 as bigint + ), pg_catalog.varchar(10) 'foo'; select '1'::interval day to second(3), @@ -111,6 +115,11 @@ select -- comments around casts select 1 /*a*/::/*b*/ int8; select cast /*c*/(/*d*/ 1 /*e*/ as /*f*/ int8 /*g*/); +select + a_very_long_expression_name_that_forces_the_select_to_wrap, + cast /*c*/( + /*d*/ 1 /*e*/ as /*f*/ int8 /*g*/ + ); select treat /*h*/(2 as /*i*/ bigint); select pg_catalog.varchar(10) /*j*/ 'foo'; select interval '4' /*k*/ year to month; @@ -151,7 +160,20 @@ create table a_very_long_table_name_for_type_wrapping( ); select a_very_long_expression_name::a_very_long_type_schema_name.a_very_long_type_name, - cast(a_very_long_expression_name as character varying(12345)), - treat(a_very_long_expression_name as a_very_long_type_schema_name.a_very_long_type_name), - a_very_long_type_schema_name.a_very_long_type_name(12345) 'a very long typed string literal value', + cast( + a_very_long_expression_name as character varying(12345) + ), + cast( + a_very_long_expression_name_long_long_long_long_long_long + as + character varying(12345) + ), + treat( + a_very_long_expression_name + as + a_very_long_type_schema_name.a_very_long_type_name + ), + a_very_long_type_schema_name.a_very_long_type_name( + 12345 + ) 'a very long typed string literal value', interval 'a very long interval literal value' day to second(12345); diff --git a/crates/squawk_fmt/tests/before/types.sql b/crates/squawk_fmt/tests/before/types.sql index 88d38c85..875f7b43 100644 --- a/crates/squawk_fmt/tests/before/types.sql +++ b/crates/squawk_fmt/tests/before/types.sql @@ -74,6 +74,7 @@ select interval '1' day to second(3), interval '2' year to month, interval(4) '3 -- comments around casts select 1 /*a*/ :: /*b*/ INT8; select cast /*c*/ ( /*d*/ 1 /*e*/ as /*f*/ INT8 /*g*/ ); +select a_very_long_expression_name_that_forces_the_select_to_wrap, cast /*c*/ ( /*d*/ 1 /*e*/ as /*f*/ INT8 /*g*/ ); select treat /*h*/ ( 2 as /*i*/ BIGINT ); select pg_catalog.varchar(10) /*j*/ 'foo'; select interval '4' /*k*/ year to month; @@ -99,4 +100,4 @@ select 1::DOUBLE -- eight PRECISION; create table a_very_long_table_name_for_type_wrapping (a_very_long_numeric_column_name numeric(12345, 12345), a_very_long_varchar_column_name varchar(12345), a_very_long_character_varying_column_name character varying(12345), a_very_long_national_character_varying_column_name national character varying(12345), a_very_long_nchar_column_name nchar(12345), a_very_long_bit_varying_column_name bit varying(12345), a_very_long_double_precision_column_name double precision, a_very_long_timestamp_column_name timestamp(12345) without time zone, a_very_long_time_column_name time(12345) with time zone, a_very_long_interval_column_name interval day to second(12345), a_very_long_array_column_name text[12345][12345]); -select a_very_long_expression_name::a_very_long_type_schema_name.a_very_long_type_name, cast(a_very_long_expression_name as character varying(12345)), treat(a_very_long_expression_name as a_very_long_type_schema_name.a_very_long_type_name), a_very_long_type_schema_name.a_very_long_type_name(12345) 'a very long typed string literal value', interval 'a very long interval literal value' day to second(12345); +select a_very_long_expression_name::a_very_long_type_schema_name.a_very_long_type_name, cast(a_very_long_expression_name as character varying(12345)), cast(a_very_long_expression_name_long_long_long_long_long_long as character varying(12345)), treat(a_very_long_expression_name as a_very_long_type_schema_name.a_very_long_type_name), a_very_long_type_schema_name.a_very_long_type_name(12345) 'a very long typed string literal value', interval 'a very long interval literal value' day to second(12345); From 2d6083da7946fc09e8b0a566bc99e13052093645 Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Mon, 24 Aug 2026 19:00:39 -0400 Subject: [PATCH 15/17] simplify --- crates/squawk_fmt/src/fmt.rs | 210 +++++++++--------- .../squawk_fmt/tests/after/select_expr.snap | 110 ++++++--- .../squawk_fmt/tests/before/select_expr.sql | 2 + 3 files changed, 191 insertions(+), 131 deletions(-) diff --git a/crates/squawk_fmt/src/fmt.rs b/crates/squawk_fmt/src/fmt.rs index 09476bbd..e0bc301b 100644 --- a/crates/squawk_fmt/src/fmt.rs +++ b/crates/squawk_fmt/src/fmt.rs @@ -56,21 +56,18 @@ fn build_create_table<'a>(create_table: &ast::CreateTable) -> Doc<'a> { .append(build_path(&table_name.path().unwrap())) .append(Doc::text("(")) .append( - Doc::line_or_nil() - .append(Doc::list( - Itertools::intersperse( - create_table - .table_arg_list() - .unwrap() - .args() - .map(build_table_arg), - Doc::text(",").append(Doc::hard_line()), - ) - .collect(), - )) - .nest(2) - .append(Doc::line_or_nil()) - .group(), + wrap_body(Doc::list( + Itertools::intersperse( + create_table + .table_arg_list() + .unwrap() + .args() + .map(build_table_arg), + Doc::text(",").append(Doc::hard_line()), + ) + .collect(), + )) + .group(), ) .append(Doc::text(")")); @@ -1193,23 +1190,25 @@ fn build_grouping_list<'a>( doc = doc.append(comments_before(r_paren)); } } else { - doc = doc.append( - Doc::line_or_nil() - .append(Doc::list( - Itertools::intersperse( - items.into_iter(), - Doc::text(",").append(Doc::line_or_space()), - ) - .collect(), - )) - .nest(2) - .append(Doc::line_or_nil()), - ); + doc = doc.append(wrap_body(Doc::list( + Itertools::intersperse( + items.into_iter(), + Doc::text(",").append(Doc::line_or_space()), + ) + .collect(), + ))); } doc.append(Doc::text(")")).group() } +fn wrap_body<'a>(body: Doc<'a>) -> Doc<'a> { + Doc::line_or_nil() + .append(body) + .nest(2) + .append(Doc::line_or_nil()) +} + fn build_semicolon<'a>(semi: Option) -> Doc<'a> { let Some(semi) = semi else { return Doc::nil(); @@ -1254,19 +1253,33 @@ fn build_array_expr<'a>(array_expr: ast::ArrayExpr) -> Doc<'a> { }; if let Some(select) = array_expr.select() { + if let Some(l_paren) = array_expr.l_paren_token() { + doc = doc.append(comments_before(l_paren)); + } + let mut body = leading_comments(select.syntax()).append(build_select_doc(&select)); + if let Some(r_paren) = array_expr.r_paren_token() { + body = body.append(comments_before(r_paren)); + } doc.append(Doc::text("(")) - .append(build_select_doc(&select)) + .append(wrap_body(body)) .append(Doc::text(")")) + .group() } else { - doc.append(Doc::text("[")) - .append(Doc::list( - Itertools::intersperse( - array_expr.exprs().map(build_expr), - Doc::text(",").append(Doc::space()), - ) - .collect(), - )) - .append(Doc::text("]")) + if let Some(l_brack) = array_expr.l_brack_token() { + doc = doc.append(comments_before(l_brack)); + } + doc = doc.append(Doc::text("[")); + + let exprs = array_expr.exprs().map(|expr| { + let syntax = expr.syntax().clone(); + let doc = leading_comments(expr.syntax()).append(build_expr(expr)); + (doc, syntax) + }); + let mut body = build_comma_separated_docs(exprs).unwrap_or_else(Doc::nil); + if let Some(r_brack) = array_expr.r_brack_token() { + body = body.append(comments_before(r_brack)); + } + doc.append(wrap_body(body)).append(Doc::text("]")).group() } } @@ -1560,14 +1573,7 @@ fn build_graph_table_fn<'a>(graph_table_fn: ast::GraphTableFn) -> Doc<'a> { if let Some(r_paren) = graph_table_fn.r_paren_token() { body = body.append(comments_before(r_paren)); } - doc.append( - Doc::line_or_nil() - .append(body) - .nest(2) - .append(Doc::line_or_nil()) - .group(), - ) - .append(Doc::text(")")) + doc.append(wrap_body(body).group()).append(Doc::text(")")) } fn build_path_pattern_list<'a>(patterns: ast::PathPatternList) -> Doc<'a> { @@ -3058,8 +3064,9 @@ fn build_overlay_fn<'a>(overlay_fn: ast::OverlayFn) -> Doc<'a> { } doc = doc.append(Doc::text("(")); + let mut body = Doc::nil(); if let Some(args) = overlay_fn.overlay_args() { - doc = doc + body = body .append(leading_comments(args.syntax())) .append(match args { ast::OverlayArgs::OverlayPlacing(args) => { @@ -3067,9 +3074,14 @@ fn build_overlay_fn<'a>(overlay_fn: ast::OverlayFn) -> Doc<'a> { .string() .map(|expr| leading_comments(expr.syntax()).append(build_expr(expr))) .unwrap_or_else(Doc::nil); - doc = append_keyword_expr(doc, args.placing_token(), "placing", args.placing()); - doc = append_keyword_expr(doc, args.from_token(), "from", args.from()); - append_keyword_expr(doc, args.for_token(), "for", args.for_()) + doc = append_line_keyword_expr( + doc, + args.placing_token(), + "placing", + args.placing(), + ); + doc = append_line_keyword_expr(doc, args.from_token(), "from", args.from()); + append_line_keyword_expr(doc, args.for_token(), "for", args.for_()).group() } ast::OverlayArgs::OverlayExprs(args) => { let items = args.overlay_exprs().map(|arg| { @@ -3086,9 +3098,10 @@ fn build_overlay_fn<'a>(overlay_fn: ast::OverlayFn) -> Doc<'a> { } if let Some(r_paren) = overlay_fn.r_paren_token() { - doc = doc.append(comments_before(r_paren)); + body = body.append(comments_before(r_paren)); } - doc.append(Doc::text(")")) + doc = doc.append(wrap_body(body)); + doc.append(Doc::text(")")).group() } fn build_substring_fn<'a>(substring_fn: ast::SubstringFn) -> Doc<'a> { @@ -3164,6 +3177,27 @@ fn append_keyword_expr<'a>( doc } +fn append_line_keyword_expr<'a>( + mut doc: Doc<'a>, + token: Option, + keyword: &'static str, + expr: Option, +) -> Doc<'a> { + if let Some(token) = token { + doc = doc + .append(Doc::line_or_space()) + .append(leading_comments_token(&token)) + .append(Doc::text(keyword)); + } + if let Some(expr) = expr { + doc = doc + .append(Doc::space()) + .append(leading_comments(expr.syntax())) + .append(build_expr(expr)); + } + doc +} + fn build_trim_fn<'a>(trim_fn: ast::TrimFn) -> Doc<'a> { let mut doc = Doc::text("trim"); if let Some(l_paren) = trim_fn.l_paren_token() { @@ -3298,15 +3332,17 @@ fn build_collation_for_fn<'a>(collation_for_fn: ast::CollationForFn) -> Doc<'a> } doc = doc.append(Doc::text("(")); + let mut body = Doc::nil(); if let Some(expr) = collation_for_fn.expr() { - doc = doc + body = body .append(leading_comments(expr.syntax())) .append(build_expr(expr)); } if let Some(r_paren) = collation_for_fn.r_paren_token() { - doc = doc.append(comments_before(r_paren)); + body = body.append(comments_before(r_paren)); } - doc.append(Doc::text(")")) + doc = doc.append(wrap_body(body)); + doc.append(Doc::text(")")).group() } fn build_extract_fn<'a>(extract_fn: ast::ExtractFn) -> Doc<'a> { @@ -3364,12 +3400,13 @@ fn build_parenthesized_expr_or_select_fn<'a>( } doc = doc.append(Doc::text("(")); + let mut body = Doc::nil(); if let Some(expr) = expr { - doc = doc + body = body .append(leading_comments(expr.syntax())) .append(build_expr(expr)); } else if let Some(select) = select { - doc = doc + body = body .append(leading_comments(select.syntax())) .append(match select { ast::SelectVariant::Select(select) => build_select_doc(&select), @@ -3378,9 +3415,10 @@ fn build_parenthesized_expr_or_select_fn<'a>( } if let Some(r_paren) = r_paren { - doc = doc.append(comments_before(r_paren)); + body = body.append(comments_before(r_paren)); } - doc.append(Doc::text(")")) + doc = doc.append(wrap_body(body)); + doc.append(Doc::text(")")).group() } fn build_call_arg_list<'a>(arg_list: ast::ArgList) -> Doc<'a> { @@ -3391,17 +3429,13 @@ fn build_call_arg_list<'a>(arg_list: ast::ArgList) -> Doc<'a> { doc = doc.append(Doc::text("(")); let mut body = Doc::nil(); - let mut has_body = false; - let mut has_args = false; if let Some(star) = arg_list.star_token() { - has_body = true; body = body .append(leading_comments_token(&star)) .append(Doc::text("*")); } else { let mut has_quantifier = false; if let Some(quantifier) = arg_list.all_or_distinct() { - has_body = true; has_quantifier = true; body = body .append(leading_comments(quantifier.syntax())) @@ -3411,48 +3445,23 @@ fn build_call_arg_list<'a>(arg_list: ast::ArgList) -> Doc<'a> { }); } - let args: Vec> = arg_list - .args() - .map(|arg| { - let leading = leading_comments(arg.syntax()); - let trailing = trailing_comments(arg.syntax()); - leading.append(build_call_arg(arg)).append(trailing) - }) - .collect(); - if !args.is_empty() { - has_body = true; - has_args = true; + let args = arg_list.args().map(|arg| { + let syntax = arg.syntax().clone(); + let doc = leading_comments(arg.syntax()).append(build_call_arg(arg)); + (doc, syntax) + }); + if let Some(args) = build_comma_separated_docs(args) { if has_quantifier { body = body.append(Doc::space()); } - body = body.append( - Doc::list( - Itertools::intersperse( - args.into_iter(), - Doc::text(",").append(Doc::line_or_space()), - ) - .collect(), - ) - .group(), - ); + body = body.append(args); } } - if !has_args { - if let Some(r_paren) = arg_list.r_paren_token() { - body = body.append(comments_before(r_paren)); - } - } - if has_body { - doc = doc.append( - Doc::line_or_nil() - .append(body) - .nest(2) - .append(Doc::line_or_nil()), - ); - } else { - doc = doc.append(body); + if let Some(r_paren) = arg_list.r_paren_token() { + body = body.append(comments_before(r_paren)); } + doc = doc.append(wrap_body(body)); doc.append(Doc::text(")")).group() } @@ -3705,12 +3714,7 @@ fn build_cast_expr<'a>(cast_expr: ast::CastExpr) -> Doc<'a> { } doc = doc .append(Doc::text("(")) - .append( - Doc::line_or_nil() - .append(body) - .nest(2) - .append(Doc::line_or_nil()), - ) + .append(wrap_body(body)) .append(Doc::text(")")) } else { let literal = cast_expr.literal().unwrap(); diff --git a/crates/squawk_fmt/tests/after/select_expr.snap b/crates/squawk_fmt/tests/after/select_expr.snap index 4c591ccc..e0e9880b 100644 --- a/crates/squawk_fmt/tests/after/select_expr.snap +++ b/crates/squawk_fmt/tests/after/select_expr.snap @@ -7,10 +7,33 @@ select array[1, 2], array(select 1), array[[1, 2], [3, 4]], - array[a_very_long_first_array_expression, a_very_long_second_array_expression, a_very_long_third_array_expression], - array(select a_very_long_array_select_expression - from a_very_long_array_select_relation_name), - array[[a_very_long_first_nested_array_expression, a_very_long_second_nested_array_expression], [a_very_long_third_nested_array_expression, a_very_long_fourth_nested_array_expression]], + /* before array */ array /* before opening bracket */[ + /* before first */ 1 /* before comma */, + /* before second */ 2 /* before closing bracket */ + ] /* after array */, + array[ + a_very_long_first_array_expression, + a_very_long_second_array_expression, + a_very_long_third_array_expression + ], + array( + select a_very_long_array_select_expression + from a_very_long_array_select_relation_name + ), + array[ + [ + a_very_long_first_nested_array_expression, + a_very_long_second_nested_array_expression + ], + [ + a_very_long_third_nested_array_expression, + a_very_long_fourth_nested_array_expression + ] + ], + array[ + [a_very_long_first, a_very_long_second], + [a_very_long_third, a_very_long_fourth] + ], -- between expr 2 between 1 and 3, 2 not between 1 and 3, @@ -60,20 +83,33 @@ select 1 = any(array[1, 2]), 2 = all(select x from things), 3 = some(array[3]), - 4 /* before op */ = /* before any */ any /* before opening paren */(/* before expr */ array[4] /* before closing paren */) /* after any */, + 4 /* before op */ = /* before any */ any /* before opening paren */( + /* before expr */ array[4] /* before closing paren */ + ) /* after any */, exists(select 1 from things), - a_very_long_any_comparison_expression = any(a_very_long_any_input_expression_that_forces_wrapping), - a_very_long_all_comparison_expression = all(select - a_very_long_all_select_expression - from a_very_long_all_relation_name), - a_very_long_some_comparison_expression = some(a_very_long_some_input_expression), - exists(select a_very_long_exists_select_expression - from a_very_long_exists_relation_name), - /* before exists */ exists /* before opening paren */(/* before select */ select - 1 /* before closing paren */) /* after exists */, + a_very_long_any_comparison_expression = any( + a_very_long_any_input_expression_that_forces_wrapping + ), + a_very_long_all_comparison_expression = all( + select a_very_long_all_select_expression from a_very_long_all_relation_name + ), + a_very_long_some_comparison_expression = some( + a_very_long_some_input_expression + ), + exists( + select a_very_long_exists_select_expression + from a_very_long_exists_relation_name + ), + /* before exists */ exists /* before opening paren */( + /* before select */ select 1 /* before closing paren */ + ) /* after exists */, collation for (b + c), - collation for (a_very_long_collation_input_expression_that_forces_the_builtin_to_wrap), - /* before collation */ collation /* before for */ for /* before opening paren */(/* before expr */ foo /* before closing paren */) /* after collation */, + collation for ( + a_very_long_collation_input_expression_that_forces_the_builtin_to_wrap + ), + /* before collation */ collation /* before for */ for /* before opening paren */( + /* before expr */ foo /* before closing paren */ + ) /* after collation */, extract(year from timestamp '2001-02-16 20:38:40'), extract('month' from ts), extract("Field" from ts), @@ -86,16 +122,30 @@ select overlay('Txxxxas' placing 'hom' from 2), overlay(), overlay('Txxxxas', 'hom', 2, count => 4), - overlay(a_very_long_string_expression placing a_very_long_replacement_expression from a_very_long_start_expression for a_very_long_count_expression), - overlay(a_very_long_string_expression, - a_very_long_replacement_expression, - a_very_long_start_expression, - a_very_long_count_name => a_very_long_count_expression), - /* before overlay */ overlay /* before opening paren */(/* before string */ 'Txxxxas' /* before placing */ placing /* before replacement */ 'hom' /* before from */ from /* before start */ 2 /* before for */ for /* before count */ 4 /* before closing paren */) /* after overlay */, - overlay /* before opening paren */(/* before string */ 'Txxxxas' /* before comma */, - /* before replacement */ 'hom' /* before comma */, - /* before start */ 2 /* before comma */, - /* before name */ count /* before arrow */ => /* before count */ 4 /* before closing paren */), + overlay( + a_very_long_string_expression + placing a_very_long_replacement_expression + from a_very_long_start_expression + for a_very_long_count_expression + ), + overlay( + a_very_long_string_expression, + a_very_long_replacement_expression, + a_very_long_start_expression, + a_very_long_count_name => a_very_long_count_expression + ), + /* before overlay */ overlay /* before opening paren */( + /* before string */ 'Txxxxas' + /* before placing */ placing /* before replacement */ 'hom' + /* before from */ from /* before start */ 2 + /* before for */ for /* before count */ 4 /* before closing paren */ + ) /* after overlay */, + overlay /* before opening paren */( + /* before string */ 'Txxxxas' /* before comma */, + /* before replacement */ 'hom' /* before comma */, + /* before start */ 2 /* before comma */, + /* before name */ count /* before arrow */ => /* before count */ 4 /* before closing paren */ + ), substring('Thomas' from 2 for 3), substring('Thomas' for 3 from 2), substring('Thomas' from 2), @@ -228,9 +278,13 @@ select json_exists(doc, '$' empty array on error), json_exists(doc, '$' empty object on error), json_scalar(1), - json_scalar(a_very_long_json_scalar_input_expression_that_forces_the_builtin_to_wrap), + json_scalar( + a_very_long_json_scalar_input_expression_that_forces_the_builtin_to_wrap + ), json_scalar(), - json_scalar /* before opening paren */(/* before expression */ 1 /* before closing paren */), + json_scalar /* before opening paren */( + /* before expression */ 1 /* before closing paren */ + ), json_serialize(doc), json_serialize(doc format json returning text format json), json_serialize(a_very_long_json_serialize_document diff --git a/crates/squawk_fmt/tests/before/select_expr.sql b/crates/squawk_fmt/tests/before/select_expr.sql index 6371c1e2..f9a6ead8 100644 --- a/crates/squawk_fmt/tests/before/select_expr.sql +++ b/crates/squawk_fmt/tests/before/select_expr.sql @@ -3,9 +3,11 @@ select array[1,2], array(select 1), array[[1,2],[3,4]], + /* before array */ ARRAY /* before opening bracket */ [ /* before first */ 1 /* before comma */, /* before second */ 2 /* before closing bracket */ ] /* after array */, array[a_very_long_first_array_expression, a_very_long_second_array_expression, a_very_long_third_array_expression], array(select a_very_long_array_select_expression from a_very_long_array_select_relation_name), array[[a_very_long_first_nested_array_expression, a_very_long_second_nested_array_expression], [a_very_long_third_nested_array_expression, a_very_long_fourth_nested_array_expression]], + array[[a_very_long_first, a_very_long_second], [a_very_long_third, a_very_long_fourth]], -- between expr 2 between 1 and 3, 2 not between 1 and 3, From b3318d534a1b9e40e351aa6cc169a777c32ba98e Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Mon, 24 Aug 2026 19:25:03 -0400 Subject: [PATCH 16/17] more --- crates/squawk_fmt/src/fmt.rs | 512 ++++++++++-------- .../squawk_fmt/tests/after/create_table.snap | 46 +- .../tests/after/create_table_like.snap | 30 +- crates/squawk_fmt/tests/after/from.snap | 28 +- .../squawk_fmt/tests/after/graph_table.snap | 12 +- crates/squawk_fmt/tests/after/group_by.snap | 16 +- .../squawk_fmt/tests/after/select_expr.snap | 284 ++++++---- .../tests/after/table_constraints.snap | 70 ++- crates/squawk_fmt/tests/after/types.snap | 20 +- .../squawk_fmt/tests/after/xml_functions.snap | 96 ++-- 10 files changed, 662 insertions(+), 452 deletions(-) diff --git a/crates/squawk_fmt/src/fmt.rs b/crates/squawk_fmt/src/fmt.rs index e0bc301b..2f2b369a 100644 --- a/crates/squawk_fmt/src/fmt.rs +++ b/crates/squawk_fmt/src/fmt.rs @@ -48,21 +48,26 @@ fn build_source_file(source_file: &ast::SourceFile) -> Doc<'_> { fn build_create_table<'a>(create_table: &ast::CreateTable) -> Doc<'a> { let table_name = create_table.table_name().unwrap(); + let arg_list = create_table.table_arg_list().unwrap(); let mut doc = Doc::text("create") .append(Doc::space()) .append(Doc::text("table")) .append(Doc::space()) .append(leading_comments(table_name.syntax())) - .append(build_path(&table_name.path().unwrap())) + .append(build_path(&table_name.path().unwrap())); + if let Some(l_paren) = arg_list.l_paren_token() { + if comment_tokens_before(l_paren.clone()).is_empty() { + doc = doc.append(Doc::space()); + } else { + doc = doc.append(comments_before(l_paren)); + } + } + doc = doc .append(Doc::text("(")) .append( wrap_body(Doc::list( Itertools::intersperse( - create_table - .table_arg_list() - .unwrap() - .args() - .map(build_table_arg), + arg_list.args().map(build_table_arg), Doc::text(",").append(Doc::hard_line()), ) .collect(), @@ -347,7 +352,7 @@ fn build_column_names<'a>( suffix: Option>, r_paren: Option, ) -> Doc<'a> { - let mut doc = l_paren + let doc = l_paren .map(comments_before) .unwrap_or_else(Doc::nil) .append(Doc::text("(")); @@ -357,16 +362,14 @@ fn build_column_names<'a>( name.syntax().clone(), ) }); - if let Some(items) = build_comma_separated_docs(items) { - doc = doc.append(items); - } + let mut body = build_comma_separated_docs(items).unwrap_or_else(Doc::nil); if let Some(suffix) = suffix { - doc = doc.append(suffix); + body = body.append(suffix); } if let Some(r_paren) = r_paren { - doc = doc.append(comments_before(r_paren)); + body = body.append(comments_before(r_paren)); } - doc.append(Doc::text(")")) + doc.append(wrap_body(body)).append(Doc::text(")")).group() } fn build_constraint_include_clause<'a>(include: ast::ConstraintIncludeClause) -> Doc<'a> { @@ -392,7 +395,7 @@ fn build_with_params<'a>(with_params: ast::WithParams) -> Doc<'a> { } fn build_attribute_list<'a>(list: ast::AttributeList) -> Doc<'a> { - let mut doc = list + let doc = list .l_paren_token() .map(comments_before) .unwrap_or_else(Doc::nil) @@ -427,13 +430,11 @@ fn build_attribute_list<'a>(list: ast::AttributeList) -> Doc<'a> { option.syntax().clone(), ) }); - if let Some(items) = build_comma_separated_docs(items) { - doc = doc.append(items); - } + let mut body = build_comma_separated_docs(items).unwrap_or_else(Doc::nil); if let Some(r_paren) = list.r_paren_token() { - doc = doc.append(comments_before(r_paren)); + body = body.append(comments_before(r_paren)); } - doc.append(Doc::text(")")) + doc.append(wrap_body(body)).append(Doc::text(")")).group() } fn build_attribute_value<'a>(value: ast::AttributeValue) -> Doc<'a> { @@ -973,7 +974,7 @@ fn build_tablesample_clause<'a>(tablesample: ast::TablesampleClause) -> Doc<'a> if let Some(call) = tablesample.call_expr() { doc = doc .append(leading_comments(call.syntax())) - .append(build_call_expr(call)); + .append(build_call_expr_with_spacing(call, true)); } if let Some(repeatable) = tablesample.repeatable_clause() { doc = doc @@ -981,7 +982,11 @@ fn build_tablesample_clause<'a>(tablesample: ast::TablesampleClause) -> Doc<'a> .append(leading_comments(repeatable.syntax())) .append(Doc::text("repeatable")); if let Some(l_paren) = repeatable.l_paren_token() { - doc = doc.append(comments_before(l_paren)); + if comment_tokens_before(l_paren.clone()).is_empty() { + doc = doc.append(Doc::space()); + } else { + doc = doc.append(comments_before(l_paren)); + } } doc = doc.append(Doc::text("(")); if let Some(expr) = repeatable.expr() { @@ -1072,26 +1077,32 @@ fn build_from_alias_column_list<'a>( ) -> Doc<'a> { let mut doc = Doc::nil(); if let Some(l_paren) = l_paren { - doc = doc.append(comments_before(l_paren)); + if comment_tokens_before(l_paren.clone()).is_empty() { + doc = doc.append(Doc::space()); + } else { + doc = doc.append(comments_before(l_paren)); + } } doc = doc.append(Doc::text("(")); - if items.is_empty() { + + let has_items = !items.is_empty(); + let mut body = if has_items { + Doc::list( + Itertools::intersperse( + items.into_iter(), + Doc::text(",").append(Doc::line_or_space()), + ) + .collect(), + ) + } else { + Doc::nil() + }; + if !has_items { if let Some(r_paren) = r_paren { - doc = doc.append(comments_before(r_paren)); + body = body.append(comments_before(r_paren)); } - } else { - doc = doc.append( - Doc::list( - Itertools::intersperse( - items.into_iter(), - Doc::text(",").append(Doc::line_or_space()), - ) - .collect(), - ) - .nest(2), - ); } - doc.append(Doc::text(")")).group() + doc.append(wrap_body(body)).append(Doc::text(")")).group() } fn build_group_by_list<'a>(list: ast::GroupByList) -> Doc<'a> { @@ -1181,7 +1192,11 @@ fn build_grouping_list<'a>( ) -> Doc<'a> { let mut doc = Doc::nil(); if let Some(l_paren) = l_paren { - doc = doc.append(comments_before(l_paren)); + if comment_tokens_before(l_paren.clone()).is_empty() { + doc = doc.append(Doc::space()); + } else { + doc = doc.append(comments_before(l_paren)); + } } doc = doc.append(Doc::text("(")); @@ -1373,13 +1388,16 @@ fn build_tuple_expr<'a>(tuple_expr: ast::TupleExpr) -> Doc<'a> { } doc = doc.append(Doc::text("(")); - if let Some(exprs) = build_comma_separated_exprs(tuple_expr.exprs()) { - doc = doc.append(exprs); - } else if let Some(r_paren) = tuple_expr.r_paren_token() { - doc = doc.append(comments_before(r_paren)); + let exprs = build_comma_separated_exprs(tuple_expr.exprs()); + let has_exprs = exprs.is_some(); + let mut body = exprs.unwrap_or_else(Doc::nil); + if !has_exprs { + if let Some(r_paren) = tuple_expr.r_paren_token() { + body = body.append(comments_before(r_paren)); + } } - doc.append(Doc::text(")")) + doc.append(wrap_body(body)).append(Doc::text(")")).group() } fn build_between_expr<'a>(between_expr: ast::BetweenExpr) -> Doc<'a> { @@ -1406,8 +1424,16 @@ fn build_between_expr<'a>(between_expr: ast::BetweenExpr) -> Doc<'a> { } fn build_call_expr<'a>(call_expr: ast::CallExpr) -> Doc<'a> { + build_call_expr_with_spacing(call_expr, false) +} + +fn build_call_expr_with_spacing<'a>(call_expr: ast::CallExpr, space_before_paren: bool) -> Doc<'a> { if let (Some(expr), Some(arg_list)) = (call_expr.expr(), call_expr.arg_list()) { - let mut doc = build_expr(expr) + let mut doc = build_expr(expr); + if space_before_paren && comment_tokens_before(arg_list.syntax().clone()).is_empty() { + doc = doc.append(Doc::space()); + } + doc = doc .append(comments_before(arg_list.syntax().clone())) .append(build_call_arg_list(arg_list)); if let Some(within_clause) = call_expr.within_clause() { @@ -1826,7 +1852,7 @@ fn build_graph_pattern_qualifier<'a>(qualifier: ast::GraphPatternQualifier) -> D } fn build_expr_as_column_name_list<'a>(list: ast::ExprAsColumnNameList) -> Doc<'a> { - let mut doc = list + let doc = list .l_paren_token() .map(comments_before) .unwrap_or_else(Doc::nil) @@ -1850,13 +1876,11 @@ fn build_expr_as_column_name_list<'a>(list: ast::ExprAsColumnNameList) -> Doc<'a item.syntax().clone(), ) }); - if let Some(items) = build_comma_separated_docs(items) { - doc = doc.append(items); - } + let mut body = build_comma_separated_docs(items).unwrap_or_else(Doc::nil); if let Some(r_paren) = list.r_paren_token() { - doc = doc.append(comments_before(r_paren)); + body = body.append(comments_before(r_paren)); } - doc.append(Doc::text(")")).nest(2).group() + doc.append(wrap_body(body)).append(Doc::text(")")).group() } fn build_xml_element_fn<'a>(xml_element_fn: ast::XmlElementFn) -> Doc<'a> { @@ -1866,8 +1890,9 @@ fn build_xml_element_fn<'a>(xml_element_fn: ast::XmlElementFn) -> Doc<'a> { } doc = doc.append(Doc::text("(")); + let mut body = Doc::nil(); if let Some(name) = xml_element_fn.name_token() { - doc = doc + body = body .append(leading_comments_token(&name)) .append(Doc::text("name")); } @@ -1875,7 +1900,7 @@ fn build_xml_element_fn<'a>(xml_element_fn: ast::XmlElementFn) -> Doc<'a> { let Some(tag) = xml_element_fn.tag() else { return doc.append(Doc::text(")")); }; - doc = doc + body = body .append(Doc::space()) .append(leading_comments(tag.syntax())) .append(build_name(tag.syntax())); @@ -1902,7 +1927,7 @@ fn build_xml_element_fn<'a>(xml_element_fn: ast::XmlElementFn) -> Doc<'a> { let mut previous = tag.syntax().clone(); for (item, syntax) in items { - doc = doc + body = body .append(trailing_comments(&previous)) .append(Doc::text(",")) .append(Doc::line_or_space()) @@ -1911,9 +1936,9 @@ fn build_xml_element_fn<'a>(xml_element_fn: ast::XmlElementFn) -> Doc<'a> { } if let Some(r_paren) = xml_element_fn.r_paren_token() { - doc = doc.append(comments_before(r_paren)); + body = body.append(comments_before(r_paren)); } - doc.append(Doc::text(")")).nest(2).group() + doc.append(wrap_body(body)).append(Doc::text(")")).group() } fn build_expr_as_xml_attr_list<'a>(attrs: ast::ExprAsXmlAttrList) -> Doc<'a> { @@ -1942,14 +1967,12 @@ fn build_expr_as_xml_attr_list<'a>(attrs: ast::ExprAsXmlAttrList) -> Doc<'a> { attr.syntax().clone(), ) }); - if let Some(items) = build_comma_separated_docs(items) { - doc = doc.append(items); - } + let mut body = build_comma_separated_docs(items).unwrap_or_else(Doc::nil); if let Some(r_paren) = attrs.r_paren_token() { - doc = doc.append(comments_before(r_paren)); + body = body.append(comments_before(r_paren)); } - doc.append(Doc::text(")")).nest(2).group() + doc.append(wrap_body(body)).append(Doc::text(")")).group() } fn build_xml_exists_fn<'a>(xml_exists_fn: ast::XmlExistsFn) -> Doc<'a> { @@ -1959,33 +1982,35 @@ fn build_xml_exists_fn<'a>(xml_exists_fn: ast::XmlExistsFn) -> Doc<'a> { } doc = doc.append(Doc::text("(")); + let mut body = Doc::nil(); + if let Some(passing) = xml_exists_fn.xml_row_passing_clause() { if let Some(row) = passing.row() { - doc = doc + body = body .append(leading_comments(passing.syntax())) .append(build_expr(row)); } if let Some(passing_token) = passing.passing_token() { - doc = doc + body = body .append(Doc::line_or_space()) .append(leading_comments_token(&passing_token)) .append(Doc::text("passing")); } if let Some(mech) = passing.xml_passing_mech() { - doc = doc + body = body .append(Doc::line_or_space()) .append(leading_comments(mech.syntax())) .append(build_xml_passing_mech(mech)); } if let Some(passing_doc) = passing.xml_passing_doc() { if let Some(expr) = passing_doc.expr() { - doc = doc + body = body .append(Doc::line_or_space()) .append(leading_comments(passing_doc.syntax())) .append(build_expr(expr)); } if let Some(mech) = passing_doc.xml_passing_mech() { - doc = doc + body = body .append(Doc::line_or_space()) .append(leading_comments(mech.syntax())) .append(build_xml_passing_mech(mech)); @@ -1994,9 +2019,9 @@ fn build_xml_exists_fn<'a>(xml_exists_fn: ast::XmlExistsFn) -> Doc<'a> { } if let Some(r_paren) = xml_exists_fn.r_paren_token() { - doc = doc.append(comments_before(r_paren)); + body = body.append(comments_before(r_paren)); } - doc.append(Doc::text(")")).nest(2).group() + doc.append(wrap_body(body)).append(Doc::text(")")).group() } fn build_xml_forest_fn<'a>(xml_forest_fn: ast::XmlForestFn) -> Doc<'a> { @@ -2010,7 +2035,6 @@ fn build_xml_forest_fn<'a>(xml_forest_fn: ast::XmlForestFn) -> Doc<'a> { }) .unwrap_or_else(Doc::nil), ) - .nest(2) .group() } @@ -2040,14 +2064,12 @@ fn build_expr_as_element_tag_list<'a>(list: ast::ExprAsElementTagList) -> Doc<'a item.syntax().clone(), ) }); - if let Some(items) = build_comma_separated_docs(items) { - doc = doc.append(items); - } + let mut body = build_comma_separated_docs(items).unwrap_or_else(Doc::nil); if let Some(r_paren) = list.r_paren_token() { - doc = doc.append(comments_before(r_paren)); + body = body.append(comments_before(r_paren)); } - doc.append(Doc::text(")")).group() + doc.append(wrap_body(body)).append(Doc::text(")")).group() } fn build_xml_parse_fn<'a>(xml_parse_fn: ast::XmlParseFn) -> Doc<'a> { @@ -2057,28 +2079,30 @@ fn build_xml_parse_fn<'a>(xml_parse_fn: ast::XmlParseFn) -> Doc<'a> { } doc = doc.append(Doc::text("(")); + let mut body = Doc::nil(); + if let Some(kind) = xml_parse_fn.xml_document_or_content() { - doc = doc + body = body .append(leading_comments(kind.syntax())) .append(build_xml_document_or_content(kind)); } if let Some(expr) = xml_parse_fn.expr() { - doc = doc + body = body .append(Doc::line_or_space()) .append(leading_comments(expr.syntax())) .append(build_expr(expr)); } if let Some(whitespace) = xml_parse_fn.xml_whitespace() { - doc = doc + body = body .append(Doc::line_or_space()) .append(leading_comments(whitespace.syntax())) .append(build_xml_whitespace(whitespace)); } if let Some(r_paren) = xml_parse_fn.r_paren_token() { - doc = doc.append(comments_before(r_paren)); + body = body.append(comments_before(r_paren)); } - doc.append(Doc::text(")")).nest(2).group() + doc.append(wrap_body(body)).append(Doc::text(")")).group() } fn build_xml_pi_fn<'a>(xml_pi_fn: ast::XmlPiFn) -> Doc<'a> { @@ -2088,22 +2112,24 @@ fn build_xml_pi_fn<'a>(xml_pi_fn: ast::XmlPiFn) -> Doc<'a> { } doc = doc.append(Doc::text("(")); + let mut body = Doc::nil(); + if let Some(name) = xml_pi_fn.name_token() { - doc = doc + body = body .append(leading_comments_token(&name)) .append(Doc::text("name")); } if let Some(target) = xml_pi_fn.target() { - doc = doc + body = body .append(Doc::space()) .append(leading_comments(target.syntax())) .append(build_name(target.syntax())); } if let Some(expr) = xml_pi_fn.expr() { if let Some(comma) = xml_pi_fn.comma_token() { - doc = doc.append(comments_before(comma)); + body = body.append(comments_before(comma)); } - doc = doc + body = body .append(Doc::text(",")) .append(Doc::line_or_space()) .append(leading_comments(expr.syntax())) @@ -2111,9 +2137,9 @@ fn build_xml_pi_fn<'a>(xml_pi_fn: ast::XmlPiFn) -> Doc<'a> { } if let Some(r_paren) = xml_pi_fn.r_paren_token() { - doc = doc.append(comments_before(r_paren)); + body = body.append(comments_before(r_paren)); } - doc.append(Doc::text(")")).nest(2).group() + doc.append(wrap_body(body)).append(Doc::text(")")).group() } fn build_xml_root_fn<'a>(xml_root_fn: ast::XmlRootFn) -> Doc<'a> { @@ -2123,30 +2149,32 @@ fn build_xml_root_fn<'a>(xml_root_fn: ast::XmlRootFn) -> Doc<'a> { } doc = doc.append(Doc::text("(")); + let mut body = Doc::nil(); + if let Some(expr) = xml_root_fn.expr() { - doc = doc + body = body .append(leading_comments(expr.syntax())) .append(build_expr(expr)); } if let Some(comma) = xml_root_fn.comma_token() { - doc = doc.append(comments_before(comma)); + body = body.append(comments_before(comma)); } - doc = doc.append(Doc::text(",")).append(Doc::line_or_space()); + body = body.append(Doc::text(",")).append(Doc::line_or_space()); if let Some(version) = xml_root_fn.xml_root_version() { - doc = doc + body = body .append(leading_comments(version.syntax())) .append(build_xml_root_version(version)); } if let Some(standalone) = xml_root_fn.xml_standalone() { - doc = doc + body = body .append(leading_comments(standalone.syntax())) .append(build_xml_standalone(standalone)); } if let Some(r_paren) = xml_root_fn.r_paren_token() { - doc = doc.append(comments_before(r_paren)); + body = body.append(comments_before(r_paren)); } - doc.append(Doc::text(")")).nest(2).group() + doc.append(wrap_body(body)).append(Doc::text(")")).group() } fn build_xml_root_version<'a>(version: ast::XmlRootVersion) -> Doc<'a> { @@ -2240,40 +2268,42 @@ fn build_xml_serialize_fn<'a>(xml_serialize_fn: ast::XmlSerializeFn) -> Doc<'a> } doc = doc.append(Doc::text("(")); + let mut body = Doc::nil(); + if let Some(kind) = xml_serialize_fn.xml_document_or_content() { - doc = doc + body = body .append(leading_comments(kind.syntax())) .append(build_xml_document_or_content(kind)); } if let Some(expr) = xml_serialize_fn.expr() { - doc = doc + body = body .append(Doc::space()) .append(leading_comments(expr.syntax())) .append(build_expr(expr)); } if let Some(as_token) = xml_serialize_fn.as_token() { - doc = doc + body = body .append(Doc::line_or_space()) .append(leading_comments_token(&as_token)) .append(Doc::text("as")); } if let Some(ty) = xml_serialize_fn.ty() { - doc = doc + body = body .append(Doc::space()) .append(leading_comments(ty.syntax())) .append(build_type(ty)); } if let Some(indent) = xml_serialize_fn.xml_indent() { - doc = doc + body = body .append(Doc::line_or_space()) .append(leading_comments(indent.syntax())) .append(build_xml_indent(indent)); } if let Some(r_paren) = xml_serialize_fn.r_paren_token() { - doc = doc.append(comments_before(r_paren)); + body = body.append(comments_before(r_paren)); } - doc.append(Doc::text(")")).nest(2).group() + doc.append(wrap_body(body)).append(Doc::text(")")).group() } fn build_xml_document_or_content<'a>(kind: ast::XmlDocumentOrContent) -> Doc<'a> { @@ -2350,6 +2380,8 @@ fn build_json_object_fn<'a>(json_object_fn: ast::JsonObjectFn) -> Doc<'a> { } doc = doc.append(Doc::text("(")); + let mut body = Doc::nil(); + let exprs = json_object_fn.exprs().map(|expr| { ( leading_comments(expr.syntax()).append(build_expr(expr.clone())), @@ -2365,39 +2397,39 @@ fn build_json_object_fn<'a>(json_object_fn: ast::JsonObjectFn) -> Doc<'a> { let items = build_comma_separated_docs(exprs.chain(key_values)); let mut has_content = items.is_some(); if let Some(items) = items { - doc = doc.append(items); + body = body.append(items); } if let Some(null_clause) = json_object_fn.json_null_clause() { if has_content { - doc = doc.append(Doc::line_or_space()); + body = body.append(Doc::line_or_space()); } - doc = doc + body = body .append(leading_comments(null_clause.syntax())) .append(build_json_null_clause(null_clause)); has_content = true; } if let Some(unique) = json_object_fn.json_keys_unique_clause() { if has_content { - doc = doc.append(Doc::line_or_space()); + body = body.append(Doc::line_or_space()); } - doc = doc + body = body .append(leading_comments(unique.syntax())) .append(build_json_keys_unique_clause(unique)); has_content = true; } if let Some(returning) = json_object_fn.json_returning_clause() { if has_content { - doc = doc.append(Doc::line_or_space()); + body = body.append(Doc::line_or_space()); } - doc = doc + body = body .append(leading_comments(returning.syntax())) .append(build_json_returning_clause(returning)); } if let Some(r_paren) = json_object_fn.r_paren_token() { - doc = doc.append(comments_before(r_paren)); + body = body.append(comments_before(r_paren)); } - doc.append(Doc::text(")")).nest(2).group() + doc.append(wrap_body(body)).append(Doc::text(")")).group() } fn build_json_object_agg_fn<'a>(json_object_agg_fn: ast::JsonObjectAggFn) -> Doc<'a> { @@ -2407,33 +2439,35 @@ fn build_json_object_agg_fn<'a>(json_object_agg_fn: ast::JsonObjectAggFn) -> Doc } doc = doc.append(Doc::text("(")); + let mut body = Doc::nil(); + if let Some(key_value) = json_object_agg_fn.json_key_value() { - doc = doc + body = body .append(leading_comments(key_value.syntax())) .append(build_json_key_value(key_value)); } if let Some(null_clause) = json_object_agg_fn.json_null_clause() { - doc = doc + body = body .append(Doc::line_or_space()) .append(leading_comments(null_clause.syntax())) .append(build_json_null_clause(null_clause)); } if let Some(unique) = json_object_agg_fn.json_keys_unique_clause() { - doc = doc + body = body .append(Doc::line_or_space()) .append(leading_comments(unique.syntax())) .append(build_json_keys_unique_clause(unique)); } if let Some(returning) = json_object_agg_fn.json_returning_clause() { - doc = doc + body = body .append(Doc::line_or_space()) .append(leading_comments(returning.syntax())) .append(build_json_returning_clause(returning)); } if let Some(r_paren) = json_object_agg_fn.r_paren_token() { - doc = doc.append(comments_before(r_paren)); + body = body.append(comments_before(r_paren)); } - doc.append(Doc::text(")")).nest(2).group() + doc.append(wrap_body(body)).append(Doc::text(")")).group() } fn build_json_key_value<'a>(key_value: ast::JsonKeyValue) -> Doc<'a> { @@ -2462,27 +2496,29 @@ fn build_json_fn<'a>(json_fn: ast::JsonFn) -> Doc<'a> { } doc = doc.append(Doc::text("(")); + let mut body = Doc::nil(); + if let Some(expr) = json_fn.expr() { - doc = doc + body = body .append(leading_comments(expr.syntax())) .append(build_expr(expr)); } if let Some(format) = json_fn.json_format_clause() { - doc = doc + body = body .append(Doc::line_or_space()) .append(leading_comments(format.syntax())) .append(build_json_format_clause(format)); } if let Some(unique) = json_fn.json_keys_unique_clause() { - doc = doc + body = body .append(Doc::line_or_space()) .append(leading_comments(unique.syntax())) .append(build_json_keys_unique_clause(unique)); } if let Some(r_paren) = json_fn.r_paren_token() { - doc = doc.append(comments_before(r_paren)); + body = body.append(comments_before(r_paren)); } - doc.append(Doc::text(")")).nest(2).group() + doc.append(wrap_body(body)).append(Doc::text(")")).group() } fn build_json_scalar_fn<'a>(json_scalar_fn: ast::JsonScalarFn) -> Doc<'a> { @@ -2502,31 +2538,33 @@ fn build_json_serialize_fn<'a>(json_serialize_fn: ast::JsonSerializeFn) -> Doc<' } doc = doc.append(Doc::text("(")); + let mut body = Doc::nil(); + if let Some(expr) = json_serialize_fn.expr() { - doc = doc + body = body .append(leading_comments(expr.syntax())) .append(build_expr(expr)); } if let Some(format) = json_serialize_fn.json_format_clause() { - doc = doc + body = body .append(Doc::line_or_space()) .append(leading_comments(format.syntax())) .append(build_json_format_clause(format)); } if let Some(returning) = json_serialize_fn.json_returning_clause() { - doc = doc + body = body .append(Doc::line_or_space()) .append(leading_comments(returning.syntax())) .append(build_json_returning_clause(returning)); } if let Some(r_paren) = json_serialize_fn.r_paren_token() { - doc = doc.append(comments_before(r_paren)); + body = body.append(comments_before(r_paren)); } - doc.append(Doc::text(")")).nest(2).group() + doc.append(wrap_body(body)).append(Doc::text(")")).group() } fn build_json_query_fn<'a>(json_query_fn: ast::JsonQueryFn) -> Doc<'a> { - let mut doc = build_json_document_path_fn( + let (doc, mut body) = build_json_document_path_fn( "json_query", json_query_fn.l_paren_token(), json_query_fn.document(), @@ -2535,49 +2573,49 @@ fn build_json_query_fn<'a>(json_query_fn: ast::JsonQueryFn) -> Doc<'a> { json_query_fn.path(), ); if let Some(passing) = json_query_fn.json_passing_clause() { - doc = doc + body = body .append(Doc::line_or_space()) .append(leading_comments(passing.syntax())) .append(build_json_passing_clause(passing)); } if let Some(returning) = json_query_fn.json_returning_clause() { - doc = doc + body = body .append(Doc::line_or_space()) .append(leading_comments(returning.syntax())) .append(build_json_returning_clause(returning)); } if let Some(wrapper) = json_query_fn.json_wrapper_behavior_clause() { - doc = doc + body = body .append(Doc::line_or_space()) .append(leading_comments(wrapper.syntax())) .append(build_json_wrapper_behavior_clause(wrapper)); } if let Some(quotes) = json_query_fn.json_quotes_clause() { - doc = doc + body = body .append(Doc::line_or_space()) .append(leading_comments(quotes.syntax())) .append(build_json_quotes_clause(quotes)); } if let Some(on_empty) = json_query_fn.json_on_empty_clause() { - doc = doc + body = body .append(Doc::line_or_space()) .append(leading_comments(on_empty.syntax())) .append(build_json_on_empty_clause(on_empty)); } if let Some(on_error) = json_query_fn.json_on_error_clause() { - doc = doc + body = body .append(Doc::line_or_space()) .append(leading_comments(on_error.syntax())) .append(build_json_on_error_clause(on_error)); } if let Some(r_paren) = json_query_fn.r_paren_token() { - doc = doc.append(comments_before(r_paren)); + body = body.append(comments_before(r_paren)); } - doc.append(Doc::text(")")).nest(2).group() + doc.append(wrap_body(body)).append(Doc::text(")")).group() } fn build_json_value_fn<'a>(json_value_fn: ast::JsonValueFn) -> Doc<'a> { - let mut doc = build_json_document_path_fn( + let (doc, mut body) = build_json_document_path_fn( "json_value", json_value_fn.l_paren_token(), json_value_fn.document(), @@ -2586,33 +2624,33 @@ fn build_json_value_fn<'a>(json_value_fn: ast::JsonValueFn) -> Doc<'a> { json_value_fn.path(), ); if let Some(passing) = json_value_fn.json_passing_clause() { - doc = doc + body = body .append(Doc::line_or_space()) .append(leading_comments(passing.syntax())) .append(build_json_passing_clause(passing)); } if let Some(returning) = json_value_fn.json_returning_clause() { - doc = doc + body = body .append(Doc::line_or_space()) .append(leading_comments(returning.syntax())) .append(build_json_returning_clause(returning)); } if let Some(on_empty) = json_value_fn.json_on_empty_clause() { - doc = doc + body = body .append(Doc::line_or_space()) .append(leading_comments(on_empty.syntax())) .append(build_json_on_empty_clause(on_empty)); } if let Some(on_error) = json_value_fn.json_on_error_clause() { - doc = doc + body = body .append(Doc::line_or_space()) .append(leading_comments(on_error.syntax())) .append(build_json_on_error_clause(on_error)); } if let Some(r_paren) = json_value_fn.r_paren_token() { - doc = doc.append(comments_before(r_paren)); + body = body.append(comments_before(r_paren)); } - doc.append(Doc::text(")")).nest(2).group() + doc.append(wrap_body(body)).append(Doc::text(")")).group() } fn build_json_document_path_fn<'a>( @@ -2622,35 +2660,37 @@ fn build_json_document_path_fn<'a>( format: Option, comma: Option, path: Option, -) -> Doc<'a> { +) -> (Doc<'a>, Doc<'a>) { let mut doc = Doc::text(keyword); if let Some(l_paren) = l_paren { doc = doc.append(comments_before(l_paren)); } doc = doc.append(Doc::text("(")); + + let mut body = Doc::nil(); if let Some(document) = document { - doc = doc + body = body .append(leading_comments(document.syntax())) .append(build_expr(document)); } if let Some(format) = format { - doc = doc + body = body .append(Doc::line_or_space()) .append(leading_comments(format.syntax())) .append(build_json_format_clause(format)); } if let Some(comma) = comma { - doc = doc + body = body .append(comments_before(comma)) .append(Doc::text(",")) .append(Doc::line_or_space()); } if let Some(path) = path { - doc = doc + body = body .append(leading_comments(path.syntax())) .append(build_expr(path)); } - doc + (doc, body) } fn build_json_exists_fn<'a>(json_exists_fn: ast::JsonExistsFn) -> Doc<'a> { @@ -2660,44 +2700,46 @@ fn build_json_exists_fn<'a>(json_exists_fn: ast::JsonExistsFn) -> Doc<'a> { } doc = doc.append(Doc::text("(")); + let mut body = Doc::nil(); + if let Some(document) = json_exists_fn.document() { - doc = doc + body = body .append(leading_comments(document.syntax())) .append(build_expr(document)); } if let Some(format) = json_exists_fn.json_format_clause() { - doc = doc + body = body .append(Doc::line_or_space()) .append(leading_comments(format.syntax())) .append(build_json_format_clause(format)); } if let Some(comma) = json_exists_fn.comma_token() { - doc = doc + body = body .append(comments_before(comma)) .append(Doc::text(",")) .append(Doc::line_or_space()); } if let Some(path) = json_exists_fn.path() { - doc = doc + body = body .append(leading_comments(path.syntax())) .append(build_expr(path)); } if let Some(passing) = json_exists_fn.json_passing_clause() { - doc = doc + body = body .append(Doc::line_or_space()) .append(leading_comments(passing.syntax())) .append(build_json_passing_clause(passing)); } if let Some(on_error) = json_exists_fn.json_on_error_clause() { - doc = doc + body = body .append(Doc::line_or_space()) .append(leading_comments(on_error.syntax())) .append(build_json_on_error_clause(on_error)); } if let Some(r_paren) = json_exists_fn.r_paren_token() { - doc = doc.append(comments_before(r_paren)); + body = body.append(comments_before(r_paren)); } - doc.append(Doc::text(")")).nest(2).group() + doc.append(wrap_body(body)).append(Doc::text(")")).group() } fn build_json_passing_clause<'a>(passing: ast::JsonPassingClause) -> Doc<'a> { @@ -2860,6 +2902,8 @@ fn build_json_array_fn<'a>(json_array_fn: ast::JsonArrayFn) -> Doc<'a> { } doc = doc.append(Doc::text("(")); + let mut body = Doc::nil(); + let exprs = json_array_fn.json_expr_formats().map(|value| { ( leading_comments(value.syntax()).append(build_json_expr_format(value.clone())), @@ -2873,25 +2917,25 @@ fn build_json_array_fn<'a>(json_array_fn: ast::JsonArrayFn) -> Doc<'a> { ) }); if let Some(items) = build_comma_separated_docs(exprs.chain(selects)) { - doc = doc.append(items); + body = body.append(items); } if let Some(null_clause) = json_array_fn.json_null_clause() { - doc = doc + body = body .append(Doc::line_or_space()) .append(leading_comments(null_clause.syntax())) .append(build_json_null_clause(null_clause)); } if let Some(returning) = json_array_fn.json_returning_clause() { - doc = doc + body = body .append(Doc::line_or_space()) .append(leading_comments(returning.syntax())) .append(build_json_returning_clause(returning)); } if let Some(r_paren) = json_array_fn.r_paren_token() { - doc = doc.append(comments_before(r_paren)); + body = body.append(comments_before(r_paren)); } - doc.append(Doc::text(")")).nest(2).group() + doc.append(wrap_body(body)).append(Doc::text(")")).group() } fn build_comma_separated_docs<'a>( @@ -2908,7 +2952,7 @@ fn build_comma_separated_docs<'a>( ); previous_syntax = syntax; } - Some(Doc::list(docs).group()) + Some(Doc::list(docs)) } fn build_json_expr_format<'a>(value: ast::JsonExprFormat) -> Doc<'a> { @@ -2946,33 +2990,35 @@ fn build_json_array_agg_fn<'a>(json_array_agg_fn: ast::JsonArrayAggFn) -> Doc<'a } doc = doc.append(Doc::text("(")); + let mut body = Doc::nil(); + if let Some(value) = json_array_agg_fn.json_value_expr() { - doc = doc + body = body .append(leading_comments(value.syntax())) .append(build_json_value_expr(value)); } if let Some(order_by) = json_array_agg_fn.order_by_clause() { - doc = doc + body = body .append(Doc::line_or_space()) .append(leading_comments(order_by.syntax())) .append(build_order_by_clause(order_by)); } if let Some(null_clause) = json_array_agg_fn.json_null_clause() { - doc = doc + body = body .append(Doc::line_or_space()) .append(leading_comments(null_clause.syntax())) .append(build_json_null_clause(null_clause)); } if let Some(returning) = json_array_agg_fn.json_returning_clause() { - doc = doc + body = body .append(Doc::line_or_space()) .append(leading_comments(returning.syntax())) .append(build_json_returning_clause(returning)); } if let Some(r_paren) = json_array_agg_fn.r_paren_token() { - doc = doc.append(comments_before(r_paren)); + body = body.append(comments_before(r_paren)); } - doc.append(Doc::text(")")).nest(2).group() + doc.append(wrap_body(body)).append(Doc::text(")")).group() } fn build_json_value_expr<'a>(value: ast::JsonValueExpr) -> Doc<'a> { @@ -3111,24 +3157,31 @@ fn build_substring_fn<'a>(substring_fn: ast::SubstringFn) -> Doc<'a> { } doc = doc.append(Doc::text("(")); + let mut body = Doc::nil(); + if let Some(args) = substring_fn.substring_args() { - doc = doc + body = body .append(leading_comments(args.syntax())) .append(match args { ast::SubstringArgs::SubstringForFrom(args) => { - let mut doc = args.string().map(build_expr).unwrap_or_else(Doc::nil); - doc = append_keyword_expr(doc, args.for_token(), "for", args.count()); - append_keyword_expr(doc, args.from_token(), "from", args.start()) + let mut body = args.string().map(build_expr).unwrap_or_else(Doc::nil); + body = append_line_keyword_expr(body, args.for_token(), "for", args.count()); + append_line_keyword_expr(body, args.from_token(), "from", args.start()) } ast::SubstringArgs::SubstringFromFor(args) => { - let mut doc = args.string().map(build_expr).unwrap_or_else(Doc::nil); - doc = append_keyword_expr(doc, args.from_token(), "from", args.start()); - append_keyword_expr(doc, args.for_token(), "for", args.count()) + let mut body = args.string().map(build_expr).unwrap_or_else(Doc::nil); + body = append_line_keyword_expr(body, args.from_token(), "from", args.start()); + append_line_keyword_expr(body, args.for_token(), "for", args.count()) } ast::SubstringArgs::SubstringSimilarEscape(args) => { - let mut doc = args.string().map(build_expr).unwrap_or_else(Doc::nil); - doc = append_keyword_expr(doc, args.similar_token(), "similar", args.pattern()); - append_keyword_expr(doc, args.escape_token(), "escape", args.escape()) + let mut body = args.string().map(build_expr).unwrap_or_else(Doc::nil); + body = append_line_keyword_expr( + body, + args.similar_token(), + "similar", + args.pattern(), + ); + append_line_keyword_expr(body, args.escape_token(), "escape", args.escape()) } ast::SubstringArgs::SubstringExprs(args) => { build_comma_separated_exprs(args.exprs()).unwrap_or_else(Doc::nil) @@ -3137,9 +3190,9 @@ fn build_substring_fn<'a>(substring_fn: ast::SubstringFn) -> Doc<'a> { } if let Some(r_paren) = substring_fn.r_paren_token() { - doc = doc.append(comments_before(r_paren)); + body = body.append(comments_before(r_paren)); } - doc.append(Doc::text(")")) + doc.append(wrap_body(body)).append(Doc::text(")")).group() } fn append_keyword_token<'a>( @@ -3156,27 +3209,6 @@ fn append_keyword_token<'a>( doc } -fn append_keyword_expr<'a>( - mut doc: Doc<'a>, - token: Option, - keyword: &'static str, - expr: Option, -) -> Doc<'a> { - if let Some(token) = token { - doc = doc - .append(Doc::space()) - .append(leading_comments_token(&token)) - .append(Doc::text(keyword)); - } - if let Some(expr) = expr { - doc = doc - .append(Doc::space()) - .append(leading_comments(expr.syntax())) - .append(build_expr(expr)); - } - doc -} - fn append_line_keyword_expr<'a>( mut doc: Doc<'a>, token: Option, @@ -3205,8 +3237,10 @@ fn build_trim_fn<'a>(trim_fn: ast::TrimFn) -> Doc<'a> { } doc = doc.append(Doc::text("(")); + let mut body = Doc::nil(); + let has_side = if let Some(side) = trim_fn.trim_side() { - doc = doc + body = body .append(leading_comments(side.syntax())) .append(match side { ast::TrimSide::TrimBoth(_) => Doc::text("both"), @@ -3220,31 +3254,31 @@ fn build_trim_fn<'a>(trim_fn: ast::TrimFn) -> Doc<'a> { if let Some(args) = trim_fn.trim_args() { if has_side { - doc = doc.append(Doc::space()); + body = body.append(Doc::space()); } - doc = doc + body = body .append(leading_comments(args.syntax())) .append(match args { ast::TrimArgs::TrimFrom(args) => { - let mut doc = Doc::text("from"); + let mut body = Doc::text("from"); if let Some(exprs) = build_comma_separated_exprs(args.exprs()) { - doc = doc.append(Doc::space()).append(exprs); + body = body.append(Doc::space()).append(exprs); } - doc + body } ast::TrimArgs::TrimExprFrom(args) => { let mut exprs = args.exprs(); - let mut doc = exprs.next().map(build_expr).unwrap_or_else(Doc::nil); + let mut body = exprs.next().map(build_expr).unwrap_or_else(Doc::nil); if let Some(from) = args.from_token() { - doc = doc - .append(Doc::space()) + body = body + .append(Doc::line_or_space()) .append(leading_comments_token(&from)) .append(Doc::text("from")); } if let Some(exprs) = build_comma_separated_exprs(exprs) { - doc = doc.append(Doc::space()).append(exprs); + body = body.append(Doc::space()).append(exprs); } - doc + body } ast::TrimArgs::TrimExprs(args) => { build_comma_separated_exprs(args.exprs()).unwrap_or_else(Doc::nil) @@ -3253,9 +3287,9 @@ fn build_trim_fn<'a>(trim_fn: ast::TrimFn) -> Doc<'a> { } if let Some(r_paren) = trim_fn.r_paren_token() { - doc = doc.append(comments_before(r_paren)); + body = body.append(comments_before(r_paren)); } - doc.append(Doc::text(")")) + doc.append(wrap_body(body)).append(Doc::text(")")).group() } fn build_comma_separated_exprs<'a>(exprs: impl Iterator) -> Option> { @@ -3277,7 +3311,6 @@ fn build_comma_separated_exprs<'a>(exprs: impl Iterator) -> Op ) .collect(), ) - .nest(2) .group(), ) } @@ -3290,27 +3323,29 @@ fn build_position_fn<'a>(position_fn: ast::PositionFn) -> Doc<'a> { } doc = doc.append(Doc::text("(")); + let mut body = Doc::nil(); + if let Some(pos) = position_fn.pos() { - doc = doc + body = body .append(leading_comments(pos.syntax())) .append(build_expr(pos)); } if let Some(in_token) = position_fn.in_token() { - doc = doc + body = body .append(Doc::space()) .append(leading_comments_token(&in_token)) .append(Doc::text("in")); } if let Some(string) = position_fn.string() { - doc = doc + body = body .append(Doc::space()) .append(leading_comments(string.syntax())) .append(build_expr(string)); } if let Some(r_paren) = position_fn.r_paren_token() { - doc = doc.append(comments_before(r_paren)); + body = body.append(comments_before(r_paren)); } - doc.append(Doc::text(")")) + doc.append(wrap_body(body)).append(Doc::text(")")).group() } fn build_collation_for_fn<'a>(collation_for_fn: ast::CollationForFn) -> Doc<'a> { @@ -3352,8 +3387,10 @@ fn build_extract_fn<'a>(extract_fn: ast::ExtractFn) -> Doc<'a> { } doc = doc.append(Doc::text("(")); + let mut body = Doc::nil(); + if let Some(field) = extract_fn.extract_field() { - doc = doc + body = body .append(leading_comments(field.syntax())) .append(match field { ast::ExtractField::ExtractFieldLiteral(field) => { @@ -3370,21 +3407,21 @@ fn build_extract_fn<'a>(extract_fn: ast::ExtractFn) -> Doc<'a> { } if let Some(from) = extract_fn.from_token() { - doc = doc - .append(Doc::space()) + body = body + .append(Doc::line_or_space()) .append(leading_comments_token(&from)) .append(Doc::text("from")); } if let Some(expr) = extract_fn.expr() { - doc = doc + body = body .append(Doc::space()) .append(leading_comments(expr.syntax())) .append(build_expr(expr)); } if let Some(r_paren) = extract_fn.r_paren_token() { - doc = doc.append(comments_before(r_paren)); + body = body.append(comments_before(r_paren)); } - doc.append(Doc::text(")")) + doc.append(wrap_body(body)).append(Doc::text(")")).group() } fn build_parenthesized_expr_or_select_fn<'a>( @@ -3885,15 +3922,17 @@ fn build_within_clause<'a>(within_clause: ast::WithinClause) -> Doc<'a> { .append(leading_comments_token(&l_paren)) .append(Doc::text("(")); } + + let mut body = Doc::nil(); if let Some(order_by) = within_clause.order_by_clause() { - doc = doc + body = body .append(leading_comments(order_by.syntax())) .append(build_order_by_clause(order_by)); } if let Some(r_paren) = within_clause.r_paren_token() { - doc = doc.append(comments_before(r_paren)); + body = body.append(comments_before(r_paren)); } - doc.append(Doc::text(")")) + doc.append(wrap_body(body)).append(Doc::text(")")).group() } fn build_over_clause<'a>(over_clause: ast::OverClause) -> Doc<'a> { @@ -3911,16 +3950,17 @@ fn build_over_clause<'a>(over_clause: ast::OverClause) -> Doc<'a> { } fn build_over_window_spec<'a>(over_window_spec: ast::OverWindowSpec) -> Doc<'a> { - let mut doc = Doc::text("("); + let doc = Doc::text("("); + let mut body = Doc::nil(); if let Some(window_spec) = over_window_spec.window_spec() { - doc = doc + body = body .append(leading_comments(window_spec.syntax())) .append(build_window_spec(window_spec)); } if let Some(r_paren) = over_window_spec.r_paren_token() { - doc = doc.append(comments_before(r_paren)); + body = body.append(comments_before(r_paren)); } - doc.append(Doc::text(")")).group() + doc.append(wrap_body(body)).append(Doc::text(")")).group() } fn build_window_spec<'a>(window_spec: ast::WindowSpec) -> Doc<'a> { @@ -3940,9 +3980,7 @@ fn build_window_spec<'a>(window_spec: ast::WindowSpec) -> Doc<'a> { parts.push(leading_comments(frame.syntax()).append(build_frame_clause(frame))); } - Doc::list(Itertools::intersperse(parts.into_iter(), Doc::line_or_space()).collect()) - .nest(2) - .group() + Doc::list(Itertools::intersperse(parts.into_iter(), Doc::line_or_space()).collect()).group() } fn build_partition_by_clause<'a>(partition_by: ast::PartitionByClause) -> Doc<'a> { @@ -4100,21 +4138,23 @@ fn build_filter_clause<'a>(filter_clause: ast::FilterClause) -> Doc<'a> { .append(leading_comments_token(&l_paren)) .append(Doc::text("(")); } + + let mut body = Doc::nil(); if let Some(where_token) = filter_clause.where_token() { - doc = doc + body = body .append(leading_comments_token(&where_token)) .append(Doc::text("where")); } if let Some(expr) = filter_clause.expr() { - doc = doc + body = body .append(Doc::space()) .append(leading_comments(expr.syntax())) .append(build_expr(expr)); } if let Some(r_paren) = filter_clause.r_paren_token() { - doc = doc.append(comments_before(r_paren)); + body = body.append(comments_before(r_paren)); } - doc.append(Doc::text(")")) + doc.append(wrap_body(body)).append(Doc::text(")")).group() } fn build_null_treatment<'a>(null_treatment: ast::NullTreatment) -> Doc<'a> { diff --git a/crates/squawk_fmt/tests/after/create_table.snap b/crates/squawk_fmt/tests/after/create_table.snap index c8937172..6414cfd6 100644 --- a/crates/squawk_fmt/tests/after/create_table.snap +++ b/crates/squawk_fmt/tests/after/create_table.snap @@ -2,18 +2,18 @@ source: crates/squawk_fmt/tests/tests.rs input_file: crates/squawk_fmt/tests/before/create_table.sql --- -create table u(); +create table u (); -create table t( +create table t ( a int, b text ); -- users table -create table users(id int); +create table users (id int); -- columns that have various quoting requirements -create table cols( +create table cols ( "left" int, "select" text, data int, @@ -23,53 +23,53 @@ create table cols( ); -- table names -create table public.accounts(id int); -create table "Public"."Users"(id int); -create table foo.quoted_names( +create table public.accounts (id int); +create table "Public"."Users" (id int); +create table foo.quoted_names ( data int, value text ); -create table "left"(id int); -create table "table"(id int); -create table U&"d\0061t\+000061"(id int); -create table U&"d!0061tum" uescape '!'(id int); -create table /* foo */ foo /* bar */./* buzz */ bar(id int); +create table "left" (id int); +create table "table" (id int); +create table U&"d\0061t\+000061" (id int); +create table U&"d!0061tum" uescape '!' (id int); +create table /* foo */ foo /* bar */./* buzz */ bar (id int); -- comments inside a name node -create table U&"d!0061tum" /* mid */ uescape '!'(id int); -create table t(U&"c!006fl" /* c */ uescape '!' int); -create table t(U&"c!006fl" uescape /* c */ '!' int); +create table U&"d!0061tum" /* mid */ uescape '!' (id int); +create table t (U&"c!006fl" /* c */ uescape '!' int); +create table t (U&"c!006fl" uescape /* c */ '!' int); -- comments inside a path -create table foo /*a*/ /*b*/.bar(id int); +create table foo /*a*/ /*b*/.bar (id int); create table foo -- a line comment -.bar(id int); +.bar (id int); -- comments between table args -create table t( +create table t ( a int /*x*/, b int ); -create table t( +create table t ( /*a*/ a int, /*b*/ b int /*c*/ ); -create table t( +create table t ( a int -- one , b int ); -create table t( +create table t ( a int, b int -- two ); -- line comment before the semicolon -create table t(a int)-- one +create table t (a int)-- one ; -create table a_very_long_schema_name.a_very_long_table_name( +create table a_very_long_schema_name.a_very_long_table_name ( a_very_long_integer_column_name int, a_very_long_text_column_name text, a_very_long_qualified_type_column_name a_very_long_type_schema_name.a_very_long_type_name, diff --git a/crates/squawk_fmt/tests/after/create_table_like.snap b/crates/squawk_fmt/tests/after/create_table_like.snap index 4584be1b..9c1d3c3a 100644 --- a/crates/squawk_fmt/tests/after/create_table_like.snap +++ b/crates/squawk_fmt/tests/after/create_table_like.snap @@ -2,24 +2,24 @@ source: crates/squawk_fmt/tests/tests.rs input_file: crates/squawk_fmt/tests/before/create_table_like.sql --- -create table t(like foo); -create table t( +create table t (like foo); +create table t ( like foo, id int ); -create table t( +create table t ( id int, like foo, like bar ); -- like options -create table t(like foo including all); -create table t(like foo excluding indexes); -create table t( +create table t (like foo including all); +create table t (like foo excluding indexes); +create table t ( like foo including defaults excluding constraints including identity ); -create table t( +create table t ( like foo including comments including compression @@ -29,21 +29,21 @@ create table t( ); -- table names -create table t(like public.accounts); -create table t(like "Public"."Users" including all); -create table t(like foo.bar); -create table t(like "select"); -create table t(like U&"d!0061tum" uescape '!' including all); +create table t (like public.accounts); +create table t (like "Public"."Users" including all); +create table t (like foo.bar); +create table t (like "select"); +create table t (like U&"d!0061tum" uescape '!' including all); -- comments -create table t(like /* a */ foo /* b */ including /* c */ all); -create table t( +create table t (like /* a */ foo /* b */ including /* c */ all); +create table t ( like foo -- a line comment including all ); -create table a_very_long_destination_table_name( +create table a_very_long_destination_table_name ( like a_very_long_source_schema_name.a_very_long_source_table_name including comments including compression diff --git a/crates/squawk_fmt/tests/after/from.snap b/crates/squawk_fmt/tests/after/from.snap index 4b1cb616..7f6cf144 100644 --- a/crates/squawk_fmt/tests/after/from.snap +++ b/crates/squawk_fmt/tests/after/from.snap @@ -4,27 +4,33 @@ input_file: crates/squawk_fmt/tests/before/from.sql --- select * from foo; select * from public.foo as f, bar b; -select * from foo as f(id, display_name); -select * from foo f(id int, display_name text collate "C"); +select * from foo as f (id, display_name); +select * from foo f (id int, display_name text collate "C"); select * -from foo /* before alias */ as /* before alias name */ f /* before open paren */(/* after open paren */ id /* before comma */, - /* after comma */ display_name /* before close paren */); -select * from users tablesample bernoulli(10) repeatable(42); +from foo /* before alias */ as /* before alias name */ f /* before open paren */ ( + /* after open paren */ id /* before comma */, + /* after comma */ display_name /* before close paren */ + ); +select * from users tablesample bernoulli (10) repeatable (42); select * /* before from */ from /* before item */ only /* before relation */ public /* before dot */./* before table */ foo /* before star */ * /* before alias */ as /* before alias name */ f /* before item comma */, /* before second item */ other /* before second alias */ o; select * -from a_very_long_schema_name.a_very_long_relation_name as a_very_long_relation_alias(a_very_long_first_column_alias, +from a_very_long_schema_name.a_very_long_relation_name as a_very_long_relation_alias ( + a_very_long_first_column_alias, a_very_long_second_column_alias, - a_very_long_third_column_alias); + a_very_long_third_column_alias + ); select * -from a_very_long_relation_name a_very_long_relation_alias(a_very_long_first_column_name a_very_long_type_schema.a_very_long_first_type_name, - a_very_long_second_column_name a_very_long_type_schema.a_very_long_second_type_name collate a_very_long_collation_name); +from a_very_long_relation_name a_very_long_relation_alias ( + a_very_long_first_column_name a_very_long_type_schema.a_very_long_first_type_name, + a_very_long_second_column_name a_very_long_type_schema.a_very_long_second_type_name collate a_very_long_collation_name + ); select * -from a_very_long_relation_name tablesample bernoulli( +from a_very_long_relation_name tablesample bernoulli ( a_very_long_sampling_percentage_expression - ) repeatable(a_very_long_repeatable_seed_expression); + ) repeatable (a_very_long_repeatable_seed_expression); select * from only a_very_long_schema_name.a_very_long_first_relation_name * as a_very_long_first_alias, a_very_long_schema_name.a_very_long_second_relation_name as a_very_long_second_alias; diff --git a/crates/squawk_fmt/tests/after/graph_table.snap b/crates/squawk_fmt/tests/after/graph_table.snap index b2c00402..01c1852c 100644 --- a/crates/squawk_fmt/tests/after/graph_table.snap +++ b/crates/squawk_fmt/tests/after/graph_table.snap @@ -44,8 +44,10 @@ from graph_table( -[a_second_very_long_edge_variable]- (a_fourth_very_long_vertex_variable) where a_very_long_graph_filter_expression - columns (a_very_long_vertex_variable.long_property_name as first_long_output_column_name, - a_second_very_long_vertex_variable.other_long_property_name as second_output_column_name) + columns ( + a_very_long_vertex_variable.long_property_name as first_long_output_column_name, + a_second_very_long_vertex_variable.other_long_property_name as second_output_column_name + ) ); select * @@ -68,6 +70,8 @@ from /* before graph table */ graph_table /* before outer opening paren */( /* before simple edge */ - /* before simple right angle */> (y) /* before nested where */ where /* before nested expression */ x.active /* before nested closing paren */) /* before graph where */ where /* before graph expression */ b.active - /* before columns */ columns /* before columns opening paren */(/* before first column */ a.name /* before column as */ as /* before column name */ source /* before column comma */, - /* before second column */ b.name /* before columns closing paren */) /* before outer closing paren */ + /* before columns */ columns /* before columns opening paren */( + /* before first column */ a.name /* before column as */ as /* before column name */ source /* before column comma */, + /* before second column */ b.name /* before columns closing paren */ + ) /* before outer closing paren */ ) /* after graph table */ as /* before alias */ result; diff --git a/crates/squawk_fmt/tests/after/group_by.snap b/crates/squawk_fmt/tests/after/group_by.snap index 7ae7a8da..67282137 100644 --- a/crates/squawk_fmt/tests/after/group_by.snap +++ b/crates/squawk_fmt/tests/after/group_by.snap @@ -3,17 +3,17 @@ source: crates/squawk_fmt/tests/tests.rs input_file: crates/squawk_fmt/tests/before/group_by.sql --- select 1 group by 1, foo + 2; -select 1 group by all rollup(1, 2), cube(3, 4); -select 1 group by distinct grouping sets((), (1, 2), rollup(3), cube(4)); +select 1 group by all rollup (1, 2), cube (3, 4); +select 1 group by distinct grouping sets ((), (1, 2), rollup (3), cube (4)); select 1 -group by grouping sets( +group by grouping sets ( (first_very_long_grouping_expression, second_very_long_grouping_expression), - rollup( + rollup ( third_very_long_grouping_expression, fourth_very_long_grouping_expression ), - cube( + cube ( fifth_very_long_grouping_expression, sixth_very_long_grouping_expression ) @@ -31,7 +31,9 @@ group /* before by */ by /* before distinct */ distinct /* before grouping */ gr ) /* before second group-by comma */, /* before nested grouping */ grouping /* before nested sets */ sets /* before nested paren */( /* before empty tuple */ () /* before nested comma */, - /* before grouping expression */ (4 /* before tuple comma */, - /* before tuple expression */ 5 /* before tuple close */) /* before nested close */ + /* before grouping expression */ ( + 4 /* before tuple comma */, + /* before tuple expression */ 5 /* before tuple close */ + ) /* before nested close */ ) /* before outer close */ )/* before semicolon */; diff --git a/crates/squawk_fmt/tests/after/select_expr.snap b/crates/squawk_fmt/tests/after/select_expr.snap index e0e9880b..5365a668 100644 --- a/crates/squawk_fmt/tests/after/select_expr.snap +++ b/crates/squawk_fmt/tests/after/select_expr.snap @@ -113,11 +113,21 @@ select extract(year from timestamp '2001-02-16 20:38:40'), extract('month' from ts), extract("Field" from ts), - extract(a_very_long_field_name from a_very_long_timestamp_expression_that_forces_the_builtin_to_wrap), - /* before extract */ extract /* before opening paren */(/* before field */ day /* before from */ from /* before expr */ ts /* before closing paren */) /* after extract */, + extract( + a_very_long_field_name + from a_very_long_timestamp_expression_that_forces_the_builtin_to_wrap + ), + /* before extract */ extract /* before opening paren */( + /* before field */ day + /* before from */ from /* before expr */ ts /* before closing paren */ + ) /* after extract */, position('om' in 'Thomas'), - position(a_very_long_substring_expression_that_forces_wrapping in a_very_long_string_expression_that_forces_wrapping), - /* before position */ position /* before opening paren */(/* before substring */ 'om' /* before in */ in /* before string */ 'Thomas' /* before closing paren */) /* after position */, + position( + a_very_long_substring_expression_that_forces_wrapping in a_very_long_string_expression_that_forces_wrapping + ), + /* before position */ position /* before opening paren */( + /* before substring */ 'om' /* before in */ in /* before string */ 'Thomas' /* before closing paren */ + ) /* after position */, overlay('Txxxxas' placing 'hom' from 2 for 4), overlay('Txxxxas' placing 'hom' from 2), overlay(), @@ -152,31 +162,62 @@ select substring('Thomas' for 3), substring('Thomas' similar '%#"o_a#"_' escape '#'), substring('Thomas', 2, 3), - substring(a_very_long_string_expression from a_very_long_start_expression for a_very_long_count_expression), - substring(a_very_long_string_expression for a_very_long_count_expression from a_very_long_start_expression), - substring(a_very_long_string_expression similar a_very_long_pattern_expression escape a_very_long_escape_expression), - substring(a_very_long_string_expression, + substring( + a_very_long_string_expression + from a_very_long_start_expression + for a_very_long_count_expression + ), + substring( + a_very_long_string_expression + for a_very_long_count_expression + from a_very_long_start_expression + ), + substring( + a_very_long_string_expression + similar a_very_long_pattern_expression + escape a_very_long_escape_expression + ), + substring( + a_very_long_string_expression, a_very_long_start_expression, - a_very_long_count_expression), - substring('Thomas' /* before comma */, - /* before start */ 2, - /* before count */ 3), - /* before substring */ substring /* before opening paren */(/* before string */ 'Thomas' /* before from */ from /* before start */ 2 /* before for */ for /* before count */ 3 /* before closing paren */) /* after substring */, + a_very_long_count_expression + ), + substring( + 'Thomas' /* before comma */, /* before start */ 2, /* before count */ 3 + ), + /* before substring */ substring /* before opening paren */( + /* before string */ 'Thomas' + /* before from */ from /* before start */ 2 + /* before for */ for /* before count */ 3 /* before closing paren */ + ) /* after substring */, trim(' foo '), trim(both 'x' from 'xfoox'), trim(leading from ' foo '), trim(trailing 'x' from 'foox'), trim('x' from 'xfoox'), trim(foo, bar), - trim(both a_very_long_trim_character_expression from a_very_long_trim_input_expression), - trim(leading from a_very_long_first_trim_input_expression, - a_very_long_second_trim_input_expression), - trim(a_very_long_trim_character_expression from a_very_long_first_trim_input_expression, - a_very_long_second_trim_input_expression), - trim(a_very_long_trim_expression, + trim( + both a_very_long_trim_character_expression + from a_very_long_trim_input_expression + ), + trim( + leading from a_very_long_first_trim_input_expression, + a_very_long_second_trim_input_expression + ), + trim( + a_very_long_trim_character_expression + from a_very_long_first_trim_input_expression, + a_very_long_second_trim_input_expression + ), + trim( + a_very_long_trim_expression, a_second_very_long_trim_expression, - a_third_very_long_trim_expression), - /* before trim */ trim /* before opening paren */(/* before side */ both /* before trim char */ 'x' /* before from */ from /* before string */ 'xfoox' /* before closing paren */) /* after trim */, + a_third_very_long_trim_expression + ), + /* before trim */ trim /* before opening paren */( + /* before side */ both /* before trim char */ 'x' + /* before from */ from /* before string */ 'xfoox' /* before closing paren */ + ) /* after trim */, date_trunc('month', now()), a_very_long_function_name( first_very_long_argument_name, @@ -221,56 +262,74 @@ select x /* before order */ order /* before by */ by /* before first */ y /* before desc */ desc /* before nulls */ nulls /* before last */ last /* before comma */, /* before second */ z /* before asc */ asc ), json_arrayagg(v), - json_arrayagg(v format json + json_arrayagg( + v format json order by sort_col desc null on null - returning jsonb format json), - json_arrayagg(a_very_long_json_arrayagg_value format json encoding utf8 + returning jsonb format json + ), + json_arrayagg( + a_very_long_json_arrayagg_value format json encoding utf8 order by a_very_long_json_arrayagg_sort_expression desc absent on null - returning a_very_long_json_arrayagg_return_type format json), - json_arrayagg /* before opening paren */(/* before value */ v + returning a_very_long_json_arrayagg_return_type format json + ), + json_arrayagg /* before opening paren */( + /* before value */ v /* before value format */ format /* before value json */ json /* before encoding */ encoding /* before encoding name */ utf8 /* before order */ order /* before by */ by /* before sort */ x /* before absent */ absent /* before on */ on /* before null */ null /* before returning */ returning /* before type */ jsonb - /* before returning format */ format /* before returning json */ json /* before closing paren */), + /* before returning format */ format /* before returning json */ json /* before closing paren */ + ), json_array(), json_array(1, 2 format json returning jsonb), - json_array(first_very_long_json_array_expression, + json_array( + first_very_long_json_array_expression, second_very_long_json_array_expression, third_very_long_json_array_expression - returning a_very_long_json_array_return_type), - json_array(select + returning a_very_long_json_array_return_type + ), + json_array( + select a_very_long_json_select_expression_that_forces_the_json_select_format_node_to_wrap format json absent on null - returning a_very_long_json_select_return_type), - json_array /* before opening paren */(/* before first */ 1 /* before comma */, + returning a_very_long_json_select_return_type + ), + json_array /* before opening paren */( + /* before first */ 1 /* before comma */, /* before second */ 2 /* before format */ format /* before json */ json /* before encoding */ encoding /* before encoding name */ utf8 /* before returning */ returning /* before type */ jsonb - /* before returning format */ format /* before returning json */ json /* before closing paren */), - json_array(/* before select */ select /* before target */ x + /* before returning format */ format /* before returning json */ json /* before closing paren */ + ), + json_array( + /* before select */ select /* before target */ x /* before format */ format /* before json */ json /* before absent */ absent /* before on */ on /* before null */ null - /* before returning */ returning /* before type */ jsonb /* before closing paren */), + /* before returning */ returning /* before type */ jsonb /* before closing paren */ + ), json_exists(doc, '$.a'), json_exists(doc, '$.a' passing x as foo, y as bar true on error), - json_exists(a_very_long_json_exists_document + json_exists( + a_very_long_json_exists_document format json encoding utf8, a_very_long_json_exists_path passing a_very_long_json_exists_argument as a_very_long_json_exists_name - false on error), - json_exists /* before opening paren */(/* before document */ doc + false on error + ), + json_exists /* before opening paren */( + /* before document */ doc /* before format */ format /* before json */ json /* before encoding */ encoding /* before encoding name */ utf8 /* before comma */, /* before path */ '$.a' /* before passing */ passing /* before arg */ x /* before as */ as /* before name */ foo /* before arg comma */, /* before second arg */ y /* before second as */ as /* before second name */ bar - /* before behavior */ default /* before default */ false /* before on */ on /* before error */ error /* before closing paren */), + /* before behavior */ default /* before default */ false /* before on */ on /* before error */ error /* before closing paren */ + ), json_exists(doc, '$' error on error), json_exists(doc, '$' null on error), json_exists(doc, '$' false on error), @@ -287,20 +346,25 @@ select ), json_serialize(doc), json_serialize(doc format json returning text format json), - json_serialize(a_very_long_json_serialize_document + json_serialize( + a_very_long_json_serialize_document format json encoding a_very_long_json_encoding_name_that_forces_the_json_format_clause_to_wrap returning a_very_long_json_serialize_return_type_name_that_forces_returning_to_wrap - format json), - json_serialize /* before opening paren */(/* before document */ doc + format json + ), + json_serialize /* before opening paren */( + /* before document */ doc /* before format */ format /* before json */ json /* before returning */ returning /* before type */ text - /* before returning format */ format /* before returning json */ json /* before closing paren */), + /* before returning format */ format /* before returning json */ json /* before closing paren */ + ), json_query(doc, '$.a'), json_query(doc, '$.a' with wrapper), json_query(doc, '$.a' with unconditional array wrapper omit quotes), json_query(doc, '$.a' without array wrapper), - json_query(a_very_long_json_query_document + json_query( + a_very_long_json_query_document format json encoding utf8, a_very_long_json_query_path passing a_very_long_json_query_argument as a_very_long_json_query_name @@ -308,8 +372,10 @@ select with unconditional array wrapper keep quotes on scalar string default a_very_long_json_query_empty_value on empty - error on error), - json_query /* before opening paren */(/* before document */ doc + error on error + ), + json_query /* before opening paren */( + /* before document */ doc /* before format */ format /* before json */ json /* before encoding */ encoding /* before encoding name */ utf8 /* before comma */, /* before path */ '$.a' @@ -319,69 +385,90 @@ select /* before wrapper */ with /* before conditional */ conditional /* before array */ array /* before wrapper keyword */ wrapper /* before keep */ keep /* before quotes */ quotes /* before on scalar */ on /* before scalar */ scalar /* before string */ string /* before empty behavior */ default /* before default */ 'empty' /* before empty on */ on /* before empty */ empty - /* before error behavior */ empty /* before object */ object /* before error on */ on /* before error */ error /* before closing paren */), + /* before error behavior */ empty /* before object */ object /* before error on */ on /* before error */ error /* before closing paren */ + ), json_value(doc, '$.a'), - json_value(a_very_long_json_value_document + json_value( + a_very_long_json_value_document format json encoding utf8, a_very_long_json_value_path passing a_very_long_json_value_argument as a_very_long_json_value_name returning a_very_long_json_value_return_type default a_very_long_json_value_empty_value on empty - error on error), - json_value /* before opening paren */(/* before document */ doc + error on error + ), + json_value /* before opening paren */( + /* before document */ doc /* before format */ format /* before json */ json /* before comma */, /* before path */ '$.a' /* before passing */ passing /* before arg */ x /* before as */ as /* before name */ foo /* before returning */ returning /* before type */ text /* before empty behavior */ null /* before empty on */ on /* before empty */ empty - /* before error behavior */ error /* before error on */ on /* before error */ error /* before closing paren */), + /* before error behavior */ error /* before error on */ on /* before error */ error /* before closing paren */ + ), json(doc), json(doc format json encoding utf8 with unique keys), json(doc without unique keys), - json(a_very_long_json_document_expression + json( + a_very_long_json_document_expression format json encoding utf8 - with unique keys), - json /* before opening paren */(/* before expression */ doc + with unique keys + ), + json /* before opening paren */( + /* before expression */ doc /* before format */ format /* before json */ json /* before encoding */ encoding /* before encoding name */ utf8 - /* before without */ without /* before unique */ unique /* before keys */ keys /* before closing paren */), + /* before without */ without /* before unique */ unique /* before keys */ keys /* before closing paren */ + ), json_objectagg(k: v), - json_objectagg(k value v format json + json_objectagg( + k value v format json absent on null with unique keys - returning jsonb format json), - json_objectagg(a_very_long_json_objectagg_key value a_very_long_json_objectagg_value + returning jsonb format json + ), + json_objectagg( + a_very_long_json_objectagg_key value a_very_long_json_objectagg_value format json absent on null with unique keys - returning a_very_long_json_objectagg_return_type format json), - json_objectagg /* before opening paren */(/* before key */ k /* before value keyword */ value /* before value */ v + returning a_very_long_json_objectagg_return_type format json + ), + json_objectagg /* before opening paren */( + /* before key */ k /* before value keyword */ value /* before value */ v /* before value format */ format /* before value json */ json /* before null */ null /* before on */ on /* before second null */ null /* before without */ without /* before unique */ unique /* before keys */ keys /* before returning */ returning /* before type */ jsonb - /* before returning format */ format /* before returning json */ json /* before closing paren */), + /* before returning format */ format /* before returning json */ json /* before closing paren */ + ), json_object(), json_object('a', 1, 'b', 2), - json_object('a': 1, 'b' value 2 format json + json_object( + 'a': 1, + 'b' value 2 format json null on null with unique keys - returning jsonb format json), - json_object(a_very_long_json_object_key value a_very_long_json_object_value - format json, + returning jsonb format json + ), + json_object( + a_very_long_json_object_key value a_very_long_json_object_value format json, a_second_very_long_json_object_key value a_second_very_long_json_object_value format json absent on null with unique keys - returning a_very_long_json_object_return_type format json), + returning a_very_long_json_object_return_type format json + ), json_object(returning jsonb), - json_object /* before opening paren */(/* before key */ 'a' /* before colon */: /* before value */ 1 /* before comma */, + json_object /* before opening paren */( + /* before key */ 'a' /* before colon */: /* before value */ 1 /* before comma */, /* before second key */ 'b' /* before value keyword */ value /* before second value */ 2 /* before format */ format /* before json */ json /* before absent */ absent /* before on */ on /* before null */ null /* before with */ with /* before unique */ unique /* before keys */ keys /* before returning */ returning /* before type */ jsonb - /* before returning format */ format /* before returning json */ json /* before closing paren */), + /* before returning format */ format /* before returning json */ json /* before closing paren */ + ), public.foo(1), foo /* before opening paren */( /* before first arg */ 1 /* before comma */, @@ -391,11 +478,13 @@ select mode() within group (order by y desc), percentile_disc( 0.5 - ) /* before within */ within /* before group */ group /* before opening paren */ (/* before order */ order /* before by */ by /* before sort */ x /* before closing paren */) /* after within */, + ) /* before within */ within /* before group */ group /* before opening paren */ ( + /* before order */ order /* before by */ by /* before sort */ x /* before closing paren */ + ) /* after within */, count(*) filter (where x > 1), - sum( - x - ) /* before filter */ filter /* before opening paren */ (/* before where */ where /* before expr */ x > 0 /* before closing paren */) /* after filter */, + sum(x) /* before filter */ filter /* before opening paren */ ( + /* before where */ where /* before expr */ x > 0 /* before closing paren */ + ) /* after filter */, first_value(x) ignore nulls, last_value(y) respect nulls, first_value( @@ -406,35 +495,42 @@ select ) within group (order by a_very_long_ordered_set_expression desc nulls last), a_very_long_filtered_aggregate_name( a_very_long_filter_argument_expression - ) filter (where a_very_long_filter_condition_expression > a_very_long_filter_threshold_expression), + ) filter ( + where a_very_long_filter_condition_expression > a_very_long_filter_threshold_expression + ), a_very_long_window_function_name( a_very_long_null_treatment_argument_expression ) ignore nulls, sum(x) over window_name, count(*) over (), - avg(x) over (w + avg(x) over ( + w partition by a, b order by c desc - rows between unbounded preceding and current row exclude ties), + rows between unbounded preceding and current row exclude ties + ), sum(x) over (range 1 preceding), - sum(x) over (groups between current row and unbounded following - exclude group), + sum(x) over ( + groups between current row and unbounded following exclude group + ), sum(x) over (rows between 1 preceding and 2 following exclude no others), - sum(a_very_long_window_value_expression) over (a_very_long_window_reference + sum(a_very_long_window_value_expression) over ( + a_very_long_window_reference partition by a_very_long_partition_expression, - a_second_very_long_partition_expression + a_second_very_long_partition_expression order by a_very_long_order_expression desc rows between a_very_long_frame_start_expression preceding and a_very_long_frame_end_expression following - exclude ties), - sum( - x - ) /* before over */ over /* before target */ (/* before partition */ partition /* before by */ by /* before expr */ a /* before comma */, - /* before second */ b + exclude ties + ), + sum(x) /* before over */ over /* before target */ ( + /* before partition */ partition /* before by */ by /* before expr */ a /* before comma */, + /* before second */ b /* before order */ order /* before order by */ by /* before sort */ c /* before rows */ rows /* before between */ between /* before start */ 1 /* before preceding */ preceding /* before and */ and /* before end */ current /* before row */ row - /* before exclude */ exclude /* before ties */ ties /* before close */) /* after over */, + /* before exclude */ exclude /* before ties */ ties /* before close */ + ) /* after over */, -- case expr case when x > 1 then 1 else 0 end, case x when 1 then 'one' when 2 then 'two' else 'other' end, @@ -545,18 +641,24 @@ select a_very_long_sliced_expression[a_very_long_slice_start_expression:a_very_long_slice_end_expression][a_second_very_long_slice_start_expression:a_second_very_long_slice_end_expression], -- tuple expr (1, 2, 3), - (first_very_long_tuple_expression, + ( + first_very_long_tuple_expression, second_very_long_tuple_expression, - third_very_long_tuple_expression), + third_very_long_tuple_expression + ), row(), row(1), row /* before opening paren */(1), row(1, 2), - row(a_very_long_first_row_expression, + row( + a_very_long_first_row_expression, a_very_long_second_row_expression, - a_very_long_third_row_expression), - (/* before first */ 1 /* before comma */, - /* before second */ 2 /* before closing paren */); + a_very_long_third_row_expression + ), + ( + /* before first */ 1 /* before comma */, + /* before second */ 2 /* before closing paren */ + ); select a_very_long_function_name( diff --git a/crates/squawk_fmt/tests/after/table_constraints.snap b/crates/squawk_fmt/tests/after/table_constraints.snap index f38277c7..a0b890df 100644 --- a/crates/squawk_fmt/tests/after/table_constraints.snap +++ b/crates/squawk_fmt/tests/after/table_constraints.snap @@ -2,7 +2,7 @@ source: crates/squawk_fmt/tests/tests.rs input_file: crates/squawk_fmt/tests/before/table_constraints.sql --- -create table simple_constraints( +create table simple_constraints ( id bigint, parent_id bigint, name text, @@ -12,72 +12,96 @@ create table simple_constraints( foreign key (parent_id) references parents (id) ); -create table named_constraints( +create table named_constraints ( id bigint, valid_at tstzrange, constraint pk primary key (id) deferrable initially deferred, - constraint name_unique unique (first_very_long_unique_column_name, - second_very_long_unique_column_name) - include (first_very_long_included_column_name, - second_very_long_included_column_name) + constraint name_unique unique ( + first_very_long_unique_column_name, + second_very_long_unique_column_name + ) + include ( + first_very_long_included_column_name, + second_very_long_included_column_name + ) with (fillfactor = 70, a_very_long_storage_parameter_name = false) using index tablespace fast, constraint id_check check (a_long_check_expression_name > another_long_check_expression_name) not valid no inherit, - constraint parent_fk foreign key (first_very_long_foreign_key_column_name, - second_very_long_foreign_key_column_name) - references public.parents (first_very_long_referenced_column_name, - second_very_long_referenced_column_name) + constraint parent_fk foreign key ( + first_very_long_foreign_key_column_name, + second_very_long_foreign_key_column_name + ) + references public.parents ( + first_very_long_referenced_column_name, + second_very_long_referenced_column_name + ) match full - on delete set null (first_very_long_foreign_key_column_name, - second_very_long_foreign_key_column_name) + on delete set null ( + first_very_long_foreign_key_column_name, + second_very_long_foreign_key_column_name + ) on update no action not deferrable, constraint no_overlap exclude using gist (first_very_long_exclusion_expression with =, second_very_long_exclusion_expression with &&) - include (first_very_long_excluded_column_name, - second_very_long_excluded_column_name) + include ( + first_very_long_excluded_column_name, + second_very_long_excluded_column_name + ) with (fillfactor = 80, a_very_long_exclusion_storage_parameter = false) using index tablespace fast where (a_very_long_exclusion_predicate_expression > 0) deferrable ); -create table using_indexes( +create table using_indexes ( id bigint, unique using index existing_unique, primary key using index existing_primary ); -create table a_very_long_table_name_using_existing_indexes( +create table a_very_long_table_name_using_existing_indexes ( a_very_long_first_identifier_column_name bigint, a_very_long_second_identifier_column_name bigint, unique using index a_very_long_existing_unique_index_name, primary key using index a_very_long_existing_primary_index_name ); -create table commented_constraints( +create table commented_constraints ( id bigint, parent_id bigint, valid_at tstzrange, - /* before constraint */ constraint /* before constraint name */ named_pk /* before primary */ primary /* before key */ key/* before column opening paren */ (/* before column */ id /* before column closing paren */) + /* before constraint */ constraint /* before constraint name */ named_pk /* before primary */ primary /* before key */ key/* before column opening paren */ ( + /* before column */ id /* before column closing paren */ + ) /* before deferrable */ deferrable, constraint named_check /* before check */ check /* before check opening paren */ (/* before check expression */ id > 0 /* before check closing paren */) /* before not */ not /* before valid */ valid, - constraint named_fk /* before foreign */ foreign /* before key */ key /* before from opening paren */ (/* before from column */ parent_id /* before from closing paren */) - /* before references */ references /* before table */ public /* before dot */./* before table name */ parents /* before to opening paren */ (/* before to column */ id /* before to closing paren */) + constraint named_fk /* before foreign */ foreign /* before key */ key /* before from opening paren */ ( + /* before from column */ parent_id /* before from closing paren */ + ) + /* before references */ references /* before table */ public /* before dot */./* before table name */ parents /* before to opening paren */ ( + /* before to column */ id /* before to closing paren */ + ) /* before match */ match /* before simple */ simple - /* before on delete */ on /* before delete */ delete /* before set */ set /* before null */ null /* before set columns */ (parent_id) + /* before on delete */ on /* before delete */ delete /* before set */ set /* before null */ null /* before set columns */ ( + parent_id + ) /* before on update */ on /* before update */ update /* before cascade */ cascade /* before enforced */ enforced, constraint named_exclude /* before exclude */ exclude /* before using */ using /* before method */ gist /* before exclusion opening paren */ (/* before exclusion expression */ id /* before exclusion with */ with /* before exclusion op */ = /* before exclusion comma */, /* before second exclusion */ valid_at with /* before operator */ operator /* before operator opening paren */(/* before operator name */ public /* before operator dot */./* before operator op */ && /* before operator closing paren */) /* before exclusion closing paren */) - /* before include */ include /* before include opening paren */ (id /* before include closing paren */) - /* before with params */ with /* before params opening paren */ (/* before param */ fillfactor /* before equals */ = /* before value */ 80 /* before params closing paren */) + /* before include */ include /* before include opening paren */ ( + id /* before include closing paren */ + ) + /* before with params */ with /* before params opening paren */ ( + /* before param */ fillfactor /* before equals */ = /* before value */ 80 /* before params closing paren */ + ) /* before tablespace using */ using /* before index */ index /* before tablespace */ tablespace /* before tablespace name */ fast /* before where */ where /* before where opening paren */(/* before where expression */ id > 0 /* before where closing paren */) /* before initially */ initially /* before immediate */ immediate diff --git a/crates/squawk_fmt/tests/after/types.snap b/crates/squawk_fmt/tests/after/types.snap index 99d61225..95e5c425 100644 --- a/crates/squawk_fmt/tests/after/types.snap +++ b/crates/squawk_fmt/tests/after/types.snap @@ -3,7 +3,7 @@ source: crates/squawk_fmt/tests/tests.rs input_file: crates/squawk_fmt/tests/before/types.sql --- -- keywords in types are lowercased -create table t( +create table t ( a int, b numeric(10, 2), c pg_catalog.varchar(10), @@ -11,7 +11,7 @@ create table t( ); -- character types -create table t( +create table t ( a varchar(10), b character varying, c national char varying(2), @@ -21,7 +21,7 @@ create table t( ); -- bit & double types -create table t( +create table t ( a bit, b bit(4), c bit varying, @@ -30,7 +30,7 @@ create table t( ); -- date & time types -create table t( +create table t ( a time, b time(3) with time zone, c timestamp without time zone, @@ -38,7 +38,7 @@ create table t( ); -- interval types -create table t( +create table t ( a interval, b interval(6), c interval year, @@ -50,7 +50,7 @@ create table t( ); -- array types -create table t( +create table t ( a int[], b text array, c text array[4], @@ -75,7 +75,7 @@ select select 1::setof int; -- comments inside types -create table t( +create table t ( a national /*a*/ char /*b*/ varying /*c*/(2), b int /*d*/[], c interval /*e*/ day to /*f*/ second /*g*/(3), @@ -93,7 +93,7 @@ create table t( select 1::setof /*a*/ int, 2::pg_catalog /*b*/./*c*/ int4; -- line comments inside types -create table t( +create table t ( a int -- one [], b numeric -- two @@ -125,7 +125,7 @@ select pg_catalog.varchar(10) /*j*/ 'foo'; select interval '4' /*k*/ year to month; -- line comments before a type's trailing keywords -create table t( +create table t ( a double -- one precision, b bit -- two @@ -145,7 +145,7 @@ select 1::double -- eight precision; -create table a_very_long_table_name_for_type_wrapping( +create table a_very_long_table_name_for_type_wrapping ( a_very_long_numeric_column_name numeric(12345, 12345), a_very_long_varchar_column_name varchar(12345), a_very_long_character_varying_column_name character varying(12345), diff --git a/crates/squawk_fmt/tests/after/xml_functions.snap b/crates/squawk_fmt/tests/after/xml_functions.snap index 59249ab5..991d2745 100644 --- a/crates/squawk_fmt/tests/after/xml_functions.snap +++ b/crates/squawk_fmt/tests/after/xml_functions.snap @@ -7,64 +7,96 @@ select xmlelement(name foo, 1, 2), xmlelement(name foo, xmlattributes(a, b as c)), xmlelement(name foo, xmlattributes(a as attr), x, y), - xmlelement(name a_very_long_element_name, - xmlattributes(a_very_long_xml_attribute_expression as a_very_long_xml_attribute_name, - a_second_very_long_xml_attribute_expression as a_second_very_long_xml_attribute_name), + xmlelement( + name a_very_long_element_name, + xmlattributes( + a_very_long_xml_attribute_expression as a_very_long_xml_attribute_name, + a_second_very_long_xml_attribute_expression as a_second_very_long_xml_attribute_name + ), first_very_long_content_expression, second_very_long_content_expression, - third_very_long_content_expression), - /* before element */ xmlelement /* before outer opening paren */(/* before name */ name /* before tag */ tag /* before attributes comma */, - /* before xmlattributes */ xmlattributes /* before attributes opening paren */(/* before first attribute */ a /* before as */ as /* before attribute name */ attr /* before attribute comma */, - /* before second attribute */ b /* before attributes closing paren */) /* before content comma */, - /* before content */ x /* before outer closing paren */) /* after element */, + third_very_long_content_expression + ), + /* before element */ xmlelement /* before outer opening paren */( + /* before name */ name /* before tag */ tag /* before attributes comma */, + /* before xmlattributes */ xmlattributes /* before attributes opening paren */( + /* before first attribute */ a /* before as */ as /* before attribute name */ attr /* before attribute comma */, + /* before second attribute */ b /* before attributes closing paren */ + ) /* before content comma */, + /* before content */ x /* before outer closing paren */ + ) /* after element */, xmlexists('/foo' passing doc), xmlexists('/foo' passing by ref doc), xmlexists('/foo' passing doc by value), - xmlexists(a_very_long_xml_exists_path_expression + xmlexists( + a_very_long_xml_exists_path_expression passing by ref a_very_long_xml_exists_document_expression - by value), - /* before exists */ xmlexists /* before opening paren */(/* before row */ '/foo' + by value + ), + /* before exists */ xmlexists /* before opening paren */( + /* before row */ '/foo' /* before passing */ passing /* before first by */ by /* before ref */ ref /* before document */ doc - /* before second by */ by /* before value */ value /* before closing paren */) /* after exists */, + /* before second by */ by /* before value */ value /* before closing paren */ + ) /* after exists */, xmlforest(a, b as foo), - xmlforest(first_very_long_expression as first_very_long_element_name, - second_very_long_expression as second_very_long_element_name), - /* before forest */ xmlforest /* before opening paren */(/* before first expression */ a /* before as */ as /* before tag */ first /* before comma */, - /* before second expression */ b /* before closing paren */) /* after forest */, + xmlforest( + first_very_long_expression as first_very_long_element_name, + second_very_long_expression as second_very_long_element_name + ), + /* before forest */ xmlforest /* before opening paren */( + /* before first expression */ a /* before as */ as /* before tag */ first /* before comma */, + /* before second expression */ b /* before closing paren */ + ) /* after forest */, xmlparse(document '' preserve whitespace), xmlparse(content value strip whitespace), - xmlparse(document + xmlparse( + document a_very_long_xml_parse_document_expression_that_forces_the_xml_parse_node_to_wrap - preserve whitespace), - /* before parse */ xmlparse /* before opening paren */(/* before kind */ document + preserve whitespace + ), + /* before parse */ xmlparse /* before opening paren */( + /* before kind */ document /* before expression */ value - /* before preserve */ preserve /* before whitespace */ whitespace /* before closing paren */) /* after parse */, + /* before preserve */ preserve /* before whitespace */ whitespace /* before closing paren */ + ) /* after parse */, xmlpi(name php), xmlpi(name php, 'echo'), - xmlpi(name a_very_long_xml_processing_instruction_target, - a_very_long_xml_processing_instruction_expression_that_forces_the_xml_pi_node_to_wrap), - /* before pi */ xmlpi /* before opening paren */(/* before name */ name /* before target */ php /* before comma */, - /* before expression */ 'echo' /* before closing paren */) /* after pi */, + xmlpi( + name a_very_long_xml_processing_instruction_target, + a_very_long_xml_processing_instruction_expression_that_forces_the_xml_pi_node_to_wrap + ), + /* before pi */ xmlpi /* before opening paren */( + /* before name */ name /* before target */ php /* before comma */, + /* before expression */ 'echo' /* before closing paren */ + ) /* after pi */, xmlroot(doc, version '1.0'), xmlroot(doc, version no value, standalone yes), xmlroot(doc, version '1.0', standalone no), xmlroot(doc, version '1.0', standalone no value), - xmlroot(a_very_long_xml_root_document_expression_that_forces_the_xml_root_node_to_wrap, + xmlroot( + a_very_long_xml_root_document_expression_that_forces_the_xml_root_node_to_wrap, version a_very_long_xml_root_version_expression, - standalone no value), - /* before root */ xmlroot /* before opening paren */(/* before expression */ doc /* before version comma */, + standalone no value + ), + /* before root */ xmlroot /* before opening paren */( + /* before expression */ doc /* before version comma */, /* before version */ version /* before no */ no /* before version value */ value/* before standalone comma */ , - /* before standalone */ standalone /* before standalone no */ no /* before standalone value */ value /* before closing paren */) /* after root */, + /* before standalone */ standalone /* before standalone no */ no /* before standalone value */ value /* before closing paren */ + ) /* after root */, xmlserialize(document doc as text), xmlserialize(content doc as varchar(20) indent), xmlserialize(content doc as text no indent), - xmlserialize(content a_long_xml_serialize_content_expression_that_forces_wrapping + xmlserialize( + content a_long_xml_serialize_content_expression_that_forces_wrapping as a_very_long_xml_serialize_return_type - no indent), - /* before serialize */ xmlserialize /* before opening paren */(/* before kind */ content /* before expression */ doc + no indent + ), + /* before serialize */ xmlserialize /* before opening paren */( + /* before kind */ content /* before expression */ doc /* before as */ as /* before type */ text - /* before no */ no /* before indent */ indent /* before closing paren */)/* after serialize */; + /* before no */ no /* before indent */ indent /* before closing paren */ + )/* after serialize */; From 80ea6bc9b0170e4b64b50f4f4d6505064340d844 Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Mon, 24 Aug 2026 19:50:19 -0400 Subject: [PATCH 17/17] wip --- crates/squawk_fmt/src/fmt.rs | 115 ++++++++++++------ .../tests/after/custom_operator.snap | 3 +- .../squawk_fmt/tests/after/select_expr.snap | 52 +++++--- .../tests/after/table_constraints.snap | 43 +++++-- .../squawk_fmt/tests/before/select_expr.sql | 1 + .../tests/before/table_constraints.sql | 3 +- 6 files changed, 155 insertions(+), 62 deletions(-) diff --git a/crates/squawk_fmt/src/fmt.rs b/crates/squawk_fmt/src/fmt.rs index 2f2b369a..85304496 100644 --- a/crates/squawk_fmt/src/fmt.rs +++ b/crates/squawk_fmt/src/fmt.rs @@ -206,17 +206,26 @@ fn build_check_constraint<'a>(constraint: ast::CheckConstraint) -> Doc<'a> { .append(leading_comments_token(&l_paren)); } doc = doc.append(Doc::text("(")); + + let mut body = Doc::nil(); if let Some(expr) = constraint.expr() { - doc = doc + body = body .append(leading_comments(expr.syntax())) .append(build_expr(expr)); } if let Some(r_paren) = constraint.r_paren_token() { - doc = doc.append(comments_before(r_paren)); + body = body.append(comments_before(r_paren)); } - append_constraint_options(doc.append(Doc::text(")")), constraint.constraint_options()) - .nest(2) - .group() + doc = doc.append(wrap_body(body)).append(Doc::text(")")).group(); + + let mut options = Doc::nil(); + for option in constraint.constraint_options() { + options = options + .append(Doc::line_or_space()) + .append(leading_comments(option.syntax())) + .append(build_keyword_node(option.syntax())); + } + doc.append(options.nest(2)).group() } fn build_primary_key_constraint<'a>(constraint: ast::PrimaryKeyConstraint) -> Doc<'a> { @@ -713,7 +722,7 @@ fn build_exclude_constraint<'a>(constraint: ast::ExcludeConstraint) -> Doc<'a> { } fn build_constraint_exclusion_list<'a>(list: ast::ConstraintExclusionList) -> Doc<'a> { - let mut doc = list + let doc = list .l_paren_token() .map(comments_before) .unwrap_or_else(Doc::nil) @@ -742,30 +751,34 @@ fn build_constraint_exclusion_list<'a>(list: ast::ConstraintExclusionList) -> Do exclusion.syntax().clone(), ) }); - if let Some(items) = build_comma_separated_docs(items) { - doc = doc.append(items); - } + let mut body = build_comma_separated_docs(items).unwrap_or_else(Doc::nil); if let Some(r_paren) = list.r_paren_token() { - doc = doc.append(comments_before(r_paren)); + body = body.append(comments_before(r_paren)); } - doc.append(Doc::text(")")) + doc.append(wrap_body(body)).append(Doc::text(")")).group() } fn build_where_condition_clause<'a>(where_clause: ast::WhereConditionClause) -> Doc<'a> { let mut doc = Doc::text("where"); if let Some(l_paren) = where_clause.l_paren_token() { - doc = doc.append(Doc::space()).append(comments_before(l_paren)); + if comment_tokens_before(l_paren.clone()).is_empty() { + doc = doc.append(Doc::space()); + } else { + doc = doc.append(comments_before(l_paren)); + } } doc = doc.append(Doc::text("(")); + + let mut body = Doc::nil(); if let Some(expr) = where_clause.expr() { - doc = doc + body = body .append(leading_comments(expr.syntax())) .append(build_expr(expr)); } if let Some(r_paren) = where_clause.r_paren_token() { - doc = doc.append(comments_before(r_paren)); + body = body.append(comments_before(r_paren)); } - doc.append(Doc::text(")")) + doc.append(wrap_body(body)).append(Doc::text(")")).group() } fn build_like_clause<'a>(like_clause: &ast::LikeClause) -> Doc<'a> { @@ -812,6 +825,10 @@ fn build_like_option<'a>(option: &ast::LikeOption) -> Doc<'a> { } fn build_select_doc<'a>(select: &ast::Select) -> Doc<'a> { + build_select_doc_ungrouped(select).group() +} + +fn build_select_doc_ungrouped<'a>(select: &ast::Select) -> Doc<'a> { let mut doc = Doc::text("select").append(Doc::line_or_space()); if let Some(select_clause) = select.select_clause() { @@ -875,7 +892,7 @@ fn build_select_doc<'a>(select: &ast::Select) -> Doc<'a> { doc = doc.append(build_semicolon(select.semicolon_token())); - doc.group() + doc } fn build_from_clause<'a>(from: ast::FromClause) -> Doc<'a> { @@ -1333,15 +1350,19 @@ fn build_index_expr<'a>(index_expr: ast::IndexExpr) -> Doc<'a> { } doc = doc.append(Doc::text("[")); + let mut body = Doc::nil(); if let Some(index) = index_expr.index() { - doc = doc + body = body .append(leading_comments(index.syntax())) - .append(build_expr(index)); + .append(match index { + ast::Expr::BinExpr(binary) => build_bin_expr_doc(binary, false), + expression => build_expr(expression), + }); } if let Some(r_brack) = index_expr.r_brack_token() { - doc = doc.append(comments_before(r_brack)); + body = body.append(comments_before(r_brack)); } - doc.append(Doc::text("]")) + doc.append(wrap_body(body)).append(Doc::text("]")).group() } fn build_slice_expr<'a>(slice_expr: ast::SliceExpr) -> Doc<'a> { @@ -1401,11 +1422,11 @@ fn build_tuple_expr<'a>(tuple_expr: ast::TupleExpr) -> Doc<'a> { } fn build_between_expr<'a>(between_expr: ast::BetweenExpr) -> Doc<'a> { - let mut doc = build_expr(between_expr.target().unwrap()); + let mut doc = build_expr(between_expr.target().unwrap()).append(Doc::line_or_space()); if between_expr.not_token().is_some() { - doc = doc.append(Doc::space()).append(Doc::text("not")); + doc = doc.append(Doc::text("not")).append(Doc::space()); } - doc = doc.append(Doc::space()).append(Doc::text("between")); + doc = doc.append(Doc::text("between")); match between_expr.between_symmetry() { Some(ast::BetweenSymmetry::Asymmetric(_)) => { doc = doc.append(Doc::space()).append(Doc::text("asymmetric")); @@ -1417,10 +1438,12 @@ fn build_between_expr<'a>(between_expr: ast::BetweenExpr) -> Doc<'a> { } doc.append(Doc::space()) .append(build_expr(between_expr.start().unwrap())) - .append(Doc::space()) + .append(Doc::line_or_space()) .append(Doc::text("and")) .append(Doc::space()) .append(build_expr(between_expr.end().unwrap())) + .nest(2) + .group() } fn build_call_expr<'a>(call_expr: ast::CallExpr) -> Doc<'a> { @@ -3332,13 +3355,13 @@ fn build_position_fn<'a>(position_fn: ast::PositionFn) -> Doc<'a> { } if let Some(in_token) = position_fn.in_token() { body = body - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments_token(&in_token)) .append(Doc::text("in")); } if let Some(string) = position_fn.string() { body = body - .append(Doc::space()) + .append(Doc::line_or_space()) .append(leading_comments(string.syntax())) .append(build_expr(string)); } @@ -3446,7 +3469,7 @@ fn build_parenthesized_expr_or_select_fn<'a>( body = body .append(leading_comments(select.syntax())) .append(match select { - ast::SelectVariant::Select(select) => build_select_doc(&select), + ast::SelectVariant::Select(select) => build_select_doc_ungrouped(&select), _ => todo!("this select variant is not supported yet"), }); } @@ -3801,10 +3824,14 @@ fn build_paren_expr<'a>(paren_expr: ast::ParenExpr) -> Doc<'a> { } doc = doc.append(Doc::text("(")); + let mut body = Doc::nil(); if let Some(expr) = paren_expr.expr() { - doc = doc + body = body .append(leading_comments(expr.syntax())) - .append(build_expr(expr)); + .append(match expr { + ast::Expr::BinExpr(binary) => build_bin_expr_doc(binary, false), + expression => build_expr(expression), + }); } else if let Some(_compound_select) = paren_expr.compound_select() { todo!("parenthesized compound select nodes are not supported yet") } else if let Some(_from_item) = paren_expr.from_item() { @@ -3822,9 +3849,9 @@ fn build_paren_expr<'a>(paren_expr: ast::ParenExpr) -> Doc<'a> { } if let Some(r_paren) = paren_expr.r_paren_token() { - doc = doc.append(comments_before(r_paren)); + body = body.append(comments_before(r_paren)); } - doc.append(Doc::text(")")) + doc.append(wrap_body(body)).append(Doc::text(")")).group() } fn build_postfix_expr<'a>(postfix_expr: ast::PostfixExpr) -> Doc<'a> { @@ -3896,18 +3923,38 @@ fn build_normalized_postfix<'a>( } fn build_bin_expr<'a>(bin_expr: ast::BinExpr) -> Doc<'a> { + build_bin_expr_doc(bin_expr, true) +} + +fn build_bin_expr_doc<'a>(bin_expr: ast::BinExpr, wrap: bool) -> Doc<'a> { let lhs = bin_expr.lhs().unwrap(); let rhs = bin_expr.rhs().unwrap(); let before_op = trailing_comments(lhs.syntax()); let after_op = leading_comments(rhs.syntax()); + let rhs_is_uncommented_quantifier = comment_tokens_before(rhs.syntax().clone()).is_empty() + && match &rhs { + ast::Expr::CallExpr(call) => { + call.all_fn().is_some() || call.any_fn().is_some() || call.some_fn().is_some() + } + _ => false, + }; - build_expr(lhs) + let doc = build_expr(lhs) .append(before_op) - .append(Doc::space()) + .append(if rhs_is_uncommented_quantifier { + Doc::space() + } else { + Doc::line_or_space() + }) .append(build_op(bin_expr.op().unwrap())) .append(Doc::space()) .append(after_op) - .append(build_expr(rhs)) + .append(build_expr(rhs)); + if rhs_is_uncommented_quantifier || !wrap { + doc + } else { + doc.nest(2).group() + } } fn build_within_clause<'a>(within_clause: ast::WithinClause) -> Doc<'a> { diff --git a/crates/squawk_fmt/tests/after/custom_operator.snap b/crates/squawk_fmt/tests/after/custom_operator.snap index 300b25b7..6fc3bb35 100644 --- a/crates/squawk_fmt/tests/after/custom_operator.snap +++ b/crates/squawk_fmt/tests/after/custom_operator.snap @@ -6,4 +6,5 @@ select 1 /*before*/ <<<< /*after*/ 1; select ##### /*after*/ 1; select a_very_long_left_operand_name <<<< a_very_long_right_operand_name, - a_second_very_long_left_operand_name ##### a_second_very_long_right_operand_name; + a_second_very_long_left_operand_name + ##### a_second_very_long_right_operand_name; diff --git a/crates/squawk_fmt/tests/after/select_expr.snap b/crates/squawk_fmt/tests/after/select_expr.snap index 5365a668..4fbaa901 100644 --- a/crates/squawk_fmt/tests/after/select_expr.snap +++ b/crates/squawk_fmt/tests/after/select_expr.snap @@ -39,10 +39,15 @@ select 2 not between 1 and 3, 2 between asymmetric 1 and 3, 2 between symmetric 1 and 3, - a_very_long_between_target_expression between symmetric a_very_long_between_start_expression and a_very_long_between_end_expression, + a_very_long_between_target_expression + between symmetric a_very_long_between_start_expression + and a_very_long_between_end_expression, -- bin expr 1 + 1, 1 /* before op */ + /* after op */ 1, + a_very_long_binary_left_expression + + a_very_long_binary_middle_expression + * a_very_long_binary_right_expression, 2 @@@ 2, true and false, ts at time zone 'UTC', @@ -70,7 +75,8 @@ select 'foo' not similar to 'f%', 1 operator(+) 1, 1 operator(public.+) 1, - 1 operator /* before paren */(/* before path */ public /* before dot */./* before op */ + /* after op */) /* after paren */ 1, + 1 + operator /* before paren */(/* before path */ public /* before dot */./* before op */ + /* after op */) /* after paren */ 1, true or false, (1, 2) overlaps (3, 4), 10 % 3, @@ -78,20 +84,24 @@ select 'foo' similar to 'f%', 6 / 2, 2 * 3, - a_very_long_binary_left_expression + a_very_long_binary_middle_expression * a_very_long_binary_right_expression, + a_very_long_binary_left_expression + + a_very_long_binary_middle_expression + * a_very_long_binary_right_expression, -- call expr 1 = any(array[1, 2]), 2 = all(select x from things), 3 = some(array[3]), - 4 /* before op */ = /* before any */ any /* before opening paren */( - /* before expr */ array[4] /* before closing paren */ - ) /* after any */, + 4 /* before op */ + = /* before any */ any /* before opening paren */( + /* before expr */ array[4] /* before closing paren */ + ) /* after any */, exists(select 1 from things), a_very_long_any_comparison_expression = any( a_very_long_any_input_expression_that_forces_wrapping ), a_very_long_all_comparison_expression = all( - select a_very_long_all_select_expression from a_very_long_all_relation_name + select a_very_long_all_select_expression + from a_very_long_all_relation_name ), a_very_long_some_comparison_expression = some( a_very_long_some_input_expression @@ -101,7 +111,8 @@ select from a_very_long_exists_relation_name ), /* before exists */ exists /* before opening paren */( - /* before select */ select 1 /* before closing paren */ + /* before select */ select + 1 /* before closing paren */ ) /* after exists */, collation for (b + c), collation for ( @@ -123,10 +134,14 @@ select ) /* after extract */, position('om' in 'Thomas'), position( - a_very_long_substring_expression_that_forces_wrapping in a_very_long_string_expression_that_forces_wrapping + a_very_long_substring_expression_that_forces_wrapping + in + a_very_long_string_expression_that_forces_wrapping ), /* before position */ position /* before opening paren */( - /* before substring */ 'om' /* before in */ in /* before string */ 'Thomas' /* before closing paren */ + /* before substring */ 'om' + /* before in */ in + /* before string */ 'Thomas' /* before closing paren */ ) /* after position */, overlay('Txxxxas' placing 'hom' from 2 for 4), overlay('Txxxxas' placing 'hom' from 2), @@ -496,7 +511,8 @@ select a_very_long_filtered_aggregate_name( a_very_long_filter_argument_expression ) filter ( - where a_very_long_filter_condition_expression > a_very_long_filter_threshold_expression + where a_very_long_filter_condition_expression + > a_very_long_filter_threshold_expression ), a_very_long_window_function_name( a_very_long_null_treatment_argument_expression @@ -578,13 +594,18 @@ select (/* after opening paren */ foo /* before closing paren */).bar, foo /* before dot */./* before field */ bar, a_very_long_field_base_expression.a_very_long_first_field_name.a_very_long_second_field_name, - (a_very_long_parenthesized_field_base_expression_that_forces_wrapping).a_very_long_field_name_that_forces_wrapping, + ( + a_very_long_parenthesized_field_base_expression_that_forces_wrapping + ).a_very_long_field_name_that_forces_wrapping, -- index expr a[1], a[1][2], a[1 + 2], a /* before bracket */[/* before index */ 1 /* before closing bracket */], - a_very_long_indexed_expression[a_very_long_first_index_expression][a_very_long_second_index_expression + a_very_long_index_offset_expression], + a_very_long_indexed_expression[a_very_long_first_index_expression][ + a_very_long_second_index_expression + + a_very_long_index_offset_expression + ], -- literal 42, 'a very long literal expression value that forces the literal expression target to wrap across the configured line width', @@ -600,7 +621,10 @@ select (1 + 2), ((1)), (/* before expr */ 1 /* before closing paren */), - (a_very_long_parenthesized_expression + a_second_very_long_parenthesized_expression), + ( + a_very_long_parenthesized_expression + + a_second_very_long_parenthesized_expression + ), -- postfix expr 1 isnull, 2 notnull, diff --git a/crates/squawk_fmt/tests/after/table_constraints.snap b/crates/squawk_fmt/tests/after/table_constraints.snap index a0b890df..830f4c87 100644 --- a/crates/squawk_fmt/tests/after/table_constraints.snap +++ b/crates/squawk_fmt/tests/after/table_constraints.snap @@ -26,9 +26,15 @@ create table named_constraints ( ) with (fillfactor = 70, a_very_long_storage_parameter_name = false) using index tablespace fast, - constraint id_check check (a_long_check_expression_name > another_long_check_expression_name) + constraint id_check check ( + a_long_check_expression_name > another_long_check_expression_name + ) not valid no inherit, + constraint long_check check ( + a_very_long_check_left_hand_expression_name + > a_very_long_check_right_hand_expression_name + ), constraint parent_fk foreign key ( first_very_long_foreign_key_column_name, second_very_long_foreign_key_column_name @@ -44,16 +50,23 @@ create table named_constraints ( ) on update no action not deferrable, - constraint no_overlap exclude using gist (first_very_long_exclusion_expression - with =, - second_very_long_exclusion_expression with &&) + constraint no_overlap exclude using gist ( + first_very_long_exclusion_expression with =, + second_very_long_exclusion_expression with && + ) include ( first_very_long_excluded_column_name, second_very_long_excluded_column_name ) - with (fillfactor = 80, a_very_long_exclusion_storage_parameter = false) + with ( + fillfactor = 80, + an_extremely_long_exclusion_storage_parameter_name_that_forces_wrapping = false + ) using index tablespace fast - where (a_very_long_exclusion_predicate_expression > 0) + where ( + a_very_long_exclusion_predicate_expression + > an_extremely_long_exclusion_predicate_value_that_forces_wrapping + ) deferrable ); @@ -78,7 +91,9 @@ create table commented_constraints ( /* before column */ id /* before column closing paren */ ) /* before deferrable */ deferrable, - constraint named_check /* before check */ check /* before check opening paren */ (/* before check expression */ id > 0 /* before check closing paren */) + constraint named_check /* before check */ check /* before check opening paren */ ( + /* before check expression */ id > 0 /* before check closing paren */ + ) /* before not */ not /* before valid */ valid, constraint named_fk /* before foreign */ foreign /* before key */ key /* before from opening paren */ ( /* before from column */ parent_id /* before from closing paren */ @@ -92,10 +107,12 @@ create table commented_constraints ( ) /* before on update */ on /* before update */ update /* before cascade */ cascade /* before enforced */ enforced, - constraint named_exclude /* before exclude */ exclude /* before using */ using /* before method */ gist /* before exclusion opening paren */ (/* before exclusion expression */ id - /* before exclusion with */ with /* before exclusion op */ = /* before exclusion comma */, - /* before second exclusion */ valid_at - with /* before operator */ operator /* before operator opening paren */(/* before operator name */ public /* before operator dot */./* before operator op */ && /* before operator closing paren */) /* before exclusion closing paren */) + constraint named_exclude /* before exclude */ exclude /* before using */ using /* before method */ gist /* before exclusion opening paren */ ( + /* before exclusion expression */ id + /* before exclusion with */ with /* before exclusion op */ = /* before exclusion comma */, + /* before second exclusion */ valid_at + with /* before operator */ operator /* before operator opening paren */(/* before operator name */ public /* before operator dot */./* before operator op */ && /* before operator closing paren */) /* before exclusion closing paren */ + ) /* before include */ include /* before include opening paren */ ( id /* before include closing paren */ ) @@ -103,6 +120,8 @@ create table commented_constraints ( /* before param */ fillfactor /* before equals */ = /* before value */ 80 /* before params closing paren */ ) /* before tablespace using */ using /* before index */ index /* before tablespace */ tablespace /* before tablespace name */ fast - /* before where */ where /* before where opening paren */(/* before where expression */ id > 0 /* before where closing paren */) + /* before where */ where /* before where opening paren */( + /* before where expression */ id > 0 /* before where closing paren */ + ) /* before initially */ initially /* before immediate */ immediate ); diff --git a/crates/squawk_fmt/tests/before/select_expr.sql b/crates/squawk_fmt/tests/before/select_expr.sql index f9a6ead8..6f2c5340 100644 --- a/crates/squawk_fmt/tests/before/select_expr.sql +++ b/crates/squawk_fmt/tests/before/select_expr.sql @@ -17,6 +17,7 @@ select -- bin expr 1 + 1, 1 /* before op */ + /* after op */ 1, + a_very_long_binary_left_expression + a_very_long_binary_middle_expression * a_very_long_binary_right_expression, 2@@@2, true and false, ts at time zone 'UTC', diff --git a/crates/squawk_fmt/tests/before/table_constraints.sql b/crates/squawk_fmt/tests/before/table_constraints.sql index d1357f05..560dfe16 100644 --- a/crates/squawk_fmt/tests/before/table_constraints.sql +++ b/crates/squawk_fmt/tests/before/table_constraints.sql @@ -14,8 +14,9 @@ create table named_constraints ( CONSTRAINT pk PRIMARY KEY (id) DEFERRABLE INITIALLY DEFERRED, CONSTRAINT name_unique UNIQUE (first_very_long_unique_column_name, second_very_long_unique_column_name) INCLUDE (first_very_long_included_column_name, second_very_long_included_column_name) WITH (fillfactor = 70, a_very_long_storage_parameter_name = false) USING INDEX TABLESPACE fast, CONSTRAINT id_check CHECK (a_long_check_expression_name > another_long_check_expression_name) NOT VALID NO INHERIT, + CONSTRAINT long_check CHECK (a_very_long_check_left_hand_expression_name > a_very_long_check_right_hand_expression_name), CONSTRAINT parent_fk FOREIGN KEY (first_very_long_foreign_key_column_name, second_very_long_foreign_key_column_name) REFERENCES public.parents (first_very_long_referenced_column_name, second_very_long_referenced_column_name) MATCH FULL ON DELETE SET NULL (first_very_long_foreign_key_column_name, second_very_long_foreign_key_column_name) ON UPDATE NO ACTION NOT DEFERRABLE, - CONSTRAINT no_overlap EXCLUDE USING gist (first_very_long_exclusion_expression WITH =, second_very_long_exclusion_expression WITH &&) INCLUDE (first_very_long_excluded_column_name, second_very_long_excluded_column_name) WITH (fillfactor = 80, a_very_long_exclusion_storage_parameter = false) USING INDEX TABLESPACE fast WHERE (a_very_long_exclusion_predicate_expression > 0) DEFERRABLE + CONSTRAINT no_overlap EXCLUDE USING gist (first_very_long_exclusion_expression WITH =, second_very_long_exclusion_expression WITH &&) INCLUDE (first_very_long_excluded_column_name, second_very_long_excluded_column_name) WITH (fillfactor = 80, an_extremely_long_exclusion_storage_parameter_name_that_forces_wrapping = false) USING INDEX TABLESPACE fast WHERE (a_very_long_exclusion_predicate_expression > an_extremely_long_exclusion_predicate_value_that_forces_wrapping) DEFERRABLE ); create table using_indexes (