Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41fdaf415c |
+42
-8
@@ -209,15 +209,31 @@ to route ident tokens through `AliasCase` (after AS) or `BuiltinCase` (before `(
|
|||||||
- `space_after_comma_in_calls` applied in `dmlInline`.
|
- `space_after_comma_in_calls` applied in `dmlInline`.
|
||||||
- `binary_op_align` registered in config (enforcement in WHERE/expression context deferred).
|
- `binary_op_align` registered in config (enforcement in WHERE/expression context deferred).
|
||||||
|
|
||||||
### ⬜ Formatter — subquery formatting
|
### ✅ Formatter — subquery formatting
|
||||||
|
|
||||||
`subquery_opening/content/closing/space_before_paren` fields are wired in config.
|
`pkg/format/dml.go`: `dmlIsSubqueryOpen` detects a `(` immediately followed by `SELECT`/
|
||||||
Enforcement in `dml.go` is not yet implemented — subqueries use current CTE formatting
|
`WITH` (derived tables, scalar subqueries, `IN`/`EXISTS`/`ARRAY(...)` subqueries — a plain
|
||||||
as a proxy (new_line for content, inline for single-arg subexpressions).
|
value tuple like `IN (1, 2, 3)` is left alone). `dmlInline` splices these in via
|
||||||
|
`dmlWrapSubquery`, which recursively formats the inner tokens with `formatDML` and wraps
|
||||||
|
them per `subquery_content`/`subquery_closing`; `dmlWriteSubquerySep` handles
|
||||||
|
`subquery_opening` (same_line/new_line) and additively applies `subquery_space_before_paren`
|
||||||
|
(only adds a space where one wouldn't already be there — never removes the space `IN`/
|
||||||
|
`EXISTS`/`AS` already get). `formatCTEDef` now calls the same `dmlWrapSubquery` helper
|
||||||
|
instead of a hardcoded new_line-only layout, so CTE bodies honor the config too (the
|
||||||
|
`AS (` space itself stays unconditional — that's fixed CTE syntax, not the subquery-space
|
||||||
|
setting). Nested subqueries-in-CASE and CASE-in-subqueries recurse correctly. Not
|
||||||
|
column/Doc-IR-aligned (documented "fixed-layout, not Wadler" limitation) — wrapped content
|
||||||
|
is indented one level relative to its own local frame, which composes correctly under
|
||||||
|
JOIN/WHERE/SELECT-list embedding but isn't perfectly column-aligned for deeply nested cases.
|
||||||
|
Tests: `TestDMLSubquery*`, `TestDMLCTEUsesSubqueryConfig` in `pkg/format/dml_test.go`.
|
||||||
|
|
||||||
### ⬜ Formatter — INSERT VALUES collapse
|
### ✅ Formatter — INSERT VALUES collapse
|
||||||
|
|
||||||
`insert_collapse_values` field is wired in config. Enforcement in `dml.go` not yet implemented.
|
`dmlValuesClause` (`pkg/format/dml.go`): when `insert_collapse_values` is `true` (default)
|
||||||
|
multi-row `VALUES` stays packed on one line (matches prior behavior); when `false`, each row
|
||||||
|
gets its own line via the shared `dmlCommaList` helper (same leading/trailing-comma layout
|
||||||
|
as SELECT/SET lists). Single-row VALUES is unaffected either way.
|
||||||
|
Tests: `TestDMLInsertValues*`.
|
||||||
|
|
||||||
### ✅ Formatter — routine param alignment (`pkg/format/format.go`)
|
### ✅ Formatter — routine param alignment (`pkg/format/format.go`)
|
||||||
|
|
||||||
@@ -238,9 +254,27 @@ as a proxy (new_line for content, inline for single-arg subexpressions).
|
|||||||
collapses them to one line.
|
collapses them to one line.
|
||||||
- CRLF normalization in trivia emission (comment text, body trivia before DECLARE).
|
- CRLF normalization in trivia emission (comment text, body trivia before DECLARE).
|
||||||
|
|
||||||
### ⬜ Formatter — expression settings (case_when_wrap, case_end, case_collapse, record_space_before_paren)
|
### ✅ Formatter — expression settings (case_when_wrap, case_end, case_collapse, record_space_before_paren)
|
||||||
|
|
||||||
Config fields wired. Expression-level CASE/ROW formatting not yet implemented.
|
`pkg/format/dml.go`: `dmlInline` detects `CASE` tokens (`dmlIsCaseStart`/`dmlMatchCaseEnd`,
|
||||||
|
tracking nested-CASE depth so an inner `CASE…END`'s own `WHEN`/`THEN`/`ELSE` don't get
|
||||||
|
mistaken for the outer one's boundaries) and splices in `dmlFormatCase`. `dmlSplitCase`
|
||||||
|
breaks the body into operand/WHEN/THEN/ELSE segments (each rendered via a recursive
|
||||||
|
`dmlInline` call, so subqueries and nested CASEs inside a branch format correctly too).
|
||||||
|
Both the simple (`CASE x WHEN ...`) and searched (`CASE WHEN ...`) forms are supported.
|
||||||
|
- `case_when_wrap` (default `false`): `false` keeps everything on one line (unchanged
|
||||||
|
default behavior); `true` puts each `WHEN … THEN …` and `ELSE` on its own line, indented
|
||||||
|
one level.
|
||||||
|
- `case_end` (default `new_line`): placement of the closing `END` when wrapped —
|
||||||
|
`new_line` on its own line, `same_line` glued to the last WHEN/ELSE line.
|
||||||
|
- `case_collapse` (default `false`): when `true` *and* the fully-inlined rendering is
|
||||||
|
≤ `caseCollapseWidth` (60 chars), keeps the wrapped CASE on one line anyway, overriding
|
||||||
|
`case_when_wrap`; longer CASEs still wrap. (No Doc-IR/line-width awareness exists yet, so
|
||||||
|
this is a fixed length threshold rather than a true "does it fit the line" check.)
|
||||||
|
- `record_space_before_paren` (default `false`): scoped to the `ROW` keyword specifically
|
||||||
|
(`ROW(1, 2)` vs `ROW (1, 2)`) — bare `(a, b)` record literals are indistinguishable from
|
||||||
|
grouping parens at the token level, so this setting only fires on an explicit `ROW(`.
|
||||||
|
Tests: `TestDMLCase*`, `TestDMLRecordSpaceBeforeParen`.
|
||||||
|
|
||||||
### ⬜ DataGrip XML import/export (optional, V4+)
|
### ⬜ DataGrip XML import/export (optional, V4+)
|
||||||
|
|
||||||
|
|||||||
+433
-84
@@ -182,6 +182,8 @@ func dmlSegText(seg dmlSeg, st config.Style) string {
|
|||||||
case "set":
|
case "set":
|
||||||
items := dmlSplitCommas(seg.body)
|
items := dmlSplitCommas(seg.body)
|
||||||
return dmlColListSet(kwText, items, st)
|
return dmlColListSet(kwText, items, st)
|
||||||
|
case "values":
|
||||||
|
return dmlValuesClause(kwText, seg.body, st)
|
||||||
case "where":
|
case "where":
|
||||||
return dmlWhereClause(kwText, seg.body, st)
|
return dmlWhereClause(kwText, seg.body, st)
|
||||||
case "join", "left", "right", "inner", "full", "cross", "natural":
|
case "join", "left", "right", "inner", "full", "cross", "natural":
|
||||||
@@ -247,16 +249,14 @@ func dmlWhereClause(kwText string, body []cst.Tok, st config.Style) string {
|
|||||||
for i, cond := range conditions {
|
for i, cond := range conditions {
|
||||||
b.WriteString(nl)
|
b.WriteString(nl)
|
||||||
text := dmlInline(cond, st)
|
text := dmlInline(cond, st)
|
||||||
|
prefix := ""
|
||||||
if st.WhereAndOrIndent {
|
if st.WhereAndOrIndent {
|
||||||
b.WriteString(st.Indent)
|
prefix = st.Indent
|
||||||
}
|
}
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
// First condition: no leading AND/OR
|
prefix += " " // align with AND/OR token width
|
||||||
b.WriteString(" ") // align with AND/OR token width
|
|
||||||
b.WriteString(text)
|
|
||||||
} else {
|
|
||||||
b.WriteString(text)
|
|
||||||
}
|
}
|
||||||
|
writeListItem(&b, prefix, prefix, text, false, nl)
|
||||||
}
|
}
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
@@ -384,28 +384,16 @@ func formatCTEDef(toks []cst.Tok, st config.Style) string {
|
|||||||
// Format the header (name, optional column list, AS, optional MATERIALIZED).
|
// Format the header (name, optional column list, AS, optional MATERIALIZED).
|
||||||
header := dmlInline(toks[:parenOpen], st)
|
header := dmlInline(toks[:parenOpen], st)
|
||||||
|
|
||||||
// Format the subquery as DML.
|
// Format and wrap the subquery per subquery_content/subquery_closing.
|
||||||
|
// The "AS (" space is standard CTE syntax and independent of
|
||||||
|
// subquery_space_before_paren; only subquery_opening's newline choice
|
||||||
|
// applies here.
|
||||||
subToks := toks[parenOpen+1 : parenClose]
|
subToks := toks[parenOpen+1 : parenClose]
|
||||||
subFormatted := strings.TrimRight(formatDML(subToks, st), nl)
|
sep := " "
|
||||||
|
if st.SubqueryOpening == config.PlacementNewLine {
|
||||||
if subFormatted == "" {
|
sep = nl
|
||||||
return header + " ()"
|
|
||||||
}
|
}
|
||||||
|
return header + sep + dmlWrapSubquery(subToks, st)
|
||||||
// Indent every non-empty line of the subquery by st.Indent.
|
|
||||||
indent := st.Indent
|
|
||||||
var indented strings.Builder
|
|
||||||
for i, line := range strings.Split(subFormatted, nl) {
|
|
||||||
if i > 0 {
|
|
||||||
indented.WriteString(nl)
|
|
||||||
}
|
|
||||||
if line != "" {
|
|
||||||
indented.WriteString(indent)
|
|
||||||
}
|
|
||||||
indented.WriteString(line)
|
|
||||||
}
|
|
||||||
|
|
||||||
return header + " (" + nl + indented.String() + nl + ")"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// dmlKeywordIdx returns the index of the first token equal to kw at paren depth 0,
|
// dmlKeywordIdx returns the index of the first token equal to kw at paren depth 0,
|
||||||
@@ -450,9 +438,62 @@ func dmlMatchParen(toks []cst.Tok, open int) int {
|
|||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// dmlIsSubqueryOpen reports whether toks[i] is a '(' immediately followed by
|
||||||
|
// SELECT or WITH — i.e. it opens a subquery (derived table, scalar subquery,
|
||||||
|
// or an IN/EXISTS/ANY/ALL/ARRAY(...) subquery), as opposed to a function-call
|
||||||
|
// argument list, a value tuple, or a grouping paren.
|
||||||
|
func dmlIsSubqueryOpen(toks []cst.Tok, i int) bool {
|
||||||
|
if toks[i].Tok.Kind != lexer.LParen {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
j := i + 1
|
||||||
|
if j >= len(toks) || toks[j].Tok.Kind != lexer.Ident {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch lowerASCII(toks[j].Tok.Text) {
|
||||||
|
case "select", "with":
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// dmlWrapSubquery formats a subquery's inner tokens (excluding the enclosing
|
||||||
|
// parens) as DML and wraps them in "(" … ")" per the subquery_content and
|
||||||
|
// subquery_closing settings. The result is rendered relative to column 0;
|
||||||
|
// callers that splice it mid-line are responsible for re-indenting any
|
||||||
|
// continuation lines to the surrounding context.
|
||||||
|
func dmlWrapSubquery(inner []cst.Tok, st config.Style) string {
|
||||||
|
nl := st.Newline
|
||||||
|
sub := strings.TrimRight(formatDML(inner, st), nl)
|
||||||
|
if sub == "" {
|
||||||
|
return "()"
|
||||||
|
}
|
||||||
|
lines := strings.Split(sub, nl)
|
||||||
|
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString("(")
|
||||||
|
for i, line := range lines {
|
||||||
|
if i == 0 && st.SubqueryContent != config.PlacementNewLine {
|
||||||
|
b.WriteString(line)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
b.WriteString(nl)
|
||||||
|
if line != "" {
|
||||||
|
b.WriteString(st.Indent)
|
||||||
|
}
|
||||||
|
b.WriteString(line)
|
||||||
|
}
|
||||||
|
if st.SubqueryClosing == config.PlacementNewLine {
|
||||||
|
b.WriteString(nl)
|
||||||
|
}
|
||||||
|
b.WriteString(")")
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
// dmlInline renders toks on one line with keyword casing and proper spacing.
|
// dmlInline renders toks on one line with keyword casing and proper spacing.
|
||||||
// If toks[1:] contains comment trivia the function falls back to verbatimSpan
|
// If toks[1:] contains comment trivia the function falls back to verbatimSpan
|
||||||
// so no comment is lost.
|
// so no comment is lost. Subquery parens and CASE…END expressions embedded
|
||||||
|
// anywhere in toks are recursively formatted and spliced in.
|
||||||
func dmlInline(toks []cst.Tok, st config.Style) string {
|
func dmlInline(toks []cst.Tok, st config.Style) string {
|
||||||
if len(toks) == 0 {
|
if len(toks) == 0 {
|
||||||
return ""
|
return ""
|
||||||
@@ -460,10 +501,41 @@ func dmlInline(toks []cst.Tok, st config.Style) string {
|
|||||||
if anyComment(toks[1:]) {
|
if anyComment(toks[1:]) {
|
||||||
return verbatimSpan(toks)
|
return verbatimSpan(toks)
|
||||||
}
|
}
|
||||||
|
nl := st.Newline
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
for i, t := range toks {
|
i := 0
|
||||||
if i > 0 && needSpace(toks[i-1].Tok, t.Tok) && !isPctTypeBoundary(toks, i) {
|
for i < len(toks) {
|
||||||
b.WriteByte(' ')
|
t := toks[i]
|
||||||
|
|
||||||
|
if dmlIsSubqueryOpen(toks, i) {
|
||||||
|
if closeIdx := dmlMatchParen(toks, i); closeIdx > i {
|
||||||
|
dmlWriteSubquerySep(&b, toks, i, st, nl)
|
||||||
|
b.WriteString(dmlWrapSubquery(toks[i+1:closeIdx], st))
|
||||||
|
i = closeIdx + 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if dmlIsCaseStart(t) {
|
||||||
|
if endIdx := dmlMatchCaseEnd(toks, i); endIdx > i {
|
||||||
|
if i > 0 && needSpace(toks[i-1].Tok, t.Tok) {
|
||||||
|
b.WriteByte(' ')
|
||||||
|
}
|
||||||
|
b.WriteString(dmlFormatCase(toks[i:endIdx+1], st))
|
||||||
|
i = endIdx + 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if i > 0 {
|
||||||
|
space := needSpace(toks[i-1].Tok, t.Tok) && !isPctTypeBoundary(toks, i)
|
||||||
|
if !space && st.RecordSpaceBeforeParen && t.Tok.Kind == lexer.LParen &&
|
||||||
|
toks[i-1].Tok.Kind == lexer.Ident && lowerASCII(toks[i-1].Tok.Text) == "row" {
|
||||||
|
space = true
|
||||||
|
}
|
||||||
|
if space {
|
||||||
|
b.WriteByte(' ')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Space after comma in calls: func(a, b) vs func(a,b).
|
// Space after comma in calls: func(a, b) vs func(a,b).
|
||||||
if st.SpaceAfterCommaInCalls && i > 0 && toks[i-1].Tok.Kind == lexer.Comma {
|
if st.SpaceAfterCommaInCalls && i > 0 && toks[i-1].Tok.Kind == lexer.Comma {
|
||||||
@@ -477,10 +549,31 @@ func dmlInline(toks []cst.Tok, st config.Style) string {
|
|||||||
}
|
}
|
||||||
nextIsLParen := i+1 < len(toks) && toks[i+1].Tok.Kind == lexer.LParen
|
nextIsLParen := i+1 < len(toks) && toks[i+1].Tok.Kind == lexer.LParen
|
||||||
b.WriteString(caseTextCtx(t.Tok, prev, nextIsLParen, st))
|
b.WriteString(caseTextCtx(t.Tok, prev, nextIsLParen, st))
|
||||||
|
i++
|
||||||
}
|
}
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// dmlWriteSubquerySep writes the separator between the token preceding a
|
||||||
|
// subquery-opening '(' at toks[i] and the '(' itself, honoring
|
||||||
|
// subquery_opening (same_line|new_line) and subquery_space_before_paren.
|
||||||
|
func dmlWriteSubquerySep(b *strings.Builder, toks []cst.Tok, i int, st config.Style, nl string) {
|
||||||
|
if i == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if st.SubqueryOpening == config.PlacementNewLine {
|
||||||
|
b.WriteString(nl)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
space := needSpace(toks[i-1].Tok, toks[i].Tok) && !isPctTypeBoundary(toks, i)
|
||||||
|
if !space && st.SubquerySpaceBeforeParen {
|
||||||
|
space = true
|
||||||
|
}
|
||||||
|
if space {
|
||||||
|
b.WriteByte(' ')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// dmlSplitCommas splits toks at depth-0 commas and returns the items between
|
// dmlSplitCommas splits toks at depth-0 commas and returns the items between
|
||||||
// them (the comma tokens themselves are discarded).
|
// them (the comma tokens themselves are discarded).
|
||||||
func dmlSplitCommas(toks []cst.Tok) [][]cst.Tok {
|
func dmlSplitCommas(toks []cst.Tok) [][]cst.Tok {
|
||||||
@@ -507,22 +600,76 @@ func dmlSplitCommas(toks []cst.Tok) [][]cst.Tok {
|
|||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
// dmlColListSelect formats a SELECT / RETURNING column list with optional
|
// filterEmpty drops empty token slices (spurious items from a trailing
|
||||||
// align_columns and select_align_as settings.
|
// comma or similar).
|
||||||
func dmlColListSelect(kwText string, items [][]cst.Tok, st config.Style) string {
|
func filterEmpty(items [][]cst.Tok) [][]cst.Tok {
|
||||||
var kept [][]cst.Tok
|
var kept [][]cst.Tok
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
if len(item) > 0 {
|
if len(item) > 0 {
|
||||||
kept = append(kept, item)
|
kept = append(kept, item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
items = kept
|
return kept
|
||||||
|
}
|
||||||
|
|
||||||
nl := st.Newline
|
// dmlCommaList renders texts as a one-item-per-line list under kwText, using
|
||||||
switch len(items) {
|
// leading or trailing commas per st.Commas. Items whose rendered text spans
|
||||||
|
// multiple lines (e.g. an embedded subquery or wrapped CASE) have their
|
||||||
|
// continuation lines re-indented to align under the item's first line.
|
||||||
|
func dmlCommaList(kwText string, texts []string, st config.Style) string {
|
||||||
|
switch len(texts) {
|
||||||
case 0:
|
case 0:
|
||||||
return kwText
|
return kwText
|
||||||
case 1:
|
case 1:
|
||||||
|
if texts[0] == "" {
|
||||||
|
return kwText
|
||||||
|
}
|
||||||
|
return kwText + " " + texts[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
nl := st.Newline
|
||||||
|
first := st.Indent + " "
|
||||||
|
cont := st.Indent + ","
|
||||||
|
contPad := strings.Repeat(" ", len(cont))
|
||||||
|
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString(kwText)
|
||||||
|
for i, text := range texts {
|
||||||
|
b.WriteString(nl)
|
||||||
|
trailingComma := st.Commas == config.CommaTrailing && i < len(texts)-1
|
||||||
|
if i == 0 || st.Commas != config.CommaLeading {
|
||||||
|
writeListItem(&b, first, first, text, trailingComma, nl)
|
||||||
|
} else {
|
||||||
|
writeListItem(&b, cont, contPad, text, false, nl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeListItem writes text prefixed with headPfx (its first line) and
|
||||||
|
// tailPfx (any continuation lines), optionally followed by a trailing comma.
|
||||||
|
func writeListItem(b *strings.Builder, headPfx, tailPfx, text string, trailingComma bool, nl string) {
|
||||||
|
for j, line := range strings.Split(text, nl) {
|
||||||
|
if j > 0 {
|
||||||
|
b.WriteString(nl)
|
||||||
|
if line != "" {
|
||||||
|
b.WriteString(tailPfx)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
b.WriteString(headPfx)
|
||||||
|
}
|
||||||
|
b.WriteString(line)
|
||||||
|
}
|
||||||
|
if trailingComma {
|
||||||
|
b.WriteString(",")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// dmlColListSelect formats a SELECT / RETURNING column list with optional
|
||||||
|
// align_columns and select_align_as settings.
|
||||||
|
func dmlColListSelect(kwText string, items [][]cst.Tok, st config.Style) string {
|
||||||
|
items = filterEmpty(items)
|
||||||
|
if len(items) == 1 {
|
||||||
body := dmlInline(items[0], st)
|
body := dmlInline(items[0], st)
|
||||||
if body == "" {
|
if body == "" {
|
||||||
return kwText
|
return kwText
|
||||||
@@ -530,7 +677,6 @@ func dmlColListSelect(kwText string, items [][]cst.Tok, st config.Style) string
|
|||||||
return kwText + " " + body
|
return kwText + " " + body
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render each item text.
|
|
||||||
texts := make([]string, len(items))
|
texts := make([]string, len(items))
|
||||||
for i, item := range items {
|
for i, item := range items {
|
||||||
texts[i] = dmlInline(item, st)
|
texts[i] = dmlInline(item, st)
|
||||||
@@ -541,41 +687,13 @@ func dmlColListSelect(kwText string, items [][]cst.Tok, st config.Style) string
|
|||||||
texts = alignSelectItems(texts, st)
|
texts = alignSelectItems(texts, st)
|
||||||
}
|
}
|
||||||
|
|
||||||
first := st.Indent + " "
|
return dmlCommaList(kwText, texts, st)
|
||||||
cont := st.Indent + ","
|
|
||||||
var b strings.Builder
|
|
||||||
b.WriteString(kwText)
|
|
||||||
for i, text := range texts {
|
|
||||||
b.WriteString(nl)
|
|
||||||
if i == 0 || st.Commas != config.CommaLeading {
|
|
||||||
b.WriteString(first)
|
|
||||||
b.WriteString(text)
|
|
||||||
if st.Commas == config.CommaTrailing && i < len(items)-1 {
|
|
||||||
b.WriteString(",")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
b.WriteString(cont)
|
|
||||||
b.WriteString(text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return b.String()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// dmlColListSet formats an UPDATE SET column list with optional set_align_equal.
|
// dmlColListSet formats an UPDATE SET column list with optional set_align_equal.
|
||||||
func dmlColListSet(kwText string, items [][]cst.Tok, st config.Style) string {
|
func dmlColListSet(kwText string, items [][]cst.Tok, st config.Style) string {
|
||||||
var kept [][]cst.Tok
|
items = filterEmpty(items)
|
||||||
for _, item := range items {
|
if len(items) == 1 {
|
||||||
if len(item) > 0 {
|
|
||||||
kept = append(kept, item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
items = kept
|
|
||||||
|
|
||||||
nl := st.Newline
|
|
||||||
switch len(items) {
|
|
||||||
case 0:
|
|
||||||
return kwText
|
|
||||||
case 1:
|
|
||||||
body := dmlInline(items[0], st)
|
body := dmlInline(items[0], st)
|
||||||
if body == "" {
|
if body == "" {
|
||||||
return kwText
|
return kwText
|
||||||
@@ -593,24 +711,28 @@ func dmlColListSet(kwText string, items [][]cst.Tok, st config.Style) string {
|
|||||||
texts = alignSetItems(texts)
|
texts = alignSetItems(texts)
|
||||||
}
|
}
|
||||||
|
|
||||||
first := st.Indent + " "
|
return dmlCommaList(kwText, texts, st)
|
||||||
cont := st.Indent + ","
|
}
|
||||||
var b strings.Builder
|
|
||||||
b.WriteString(kwText)
|
// dmlValuesClause formats a VALUES clause. When insert_collapse_values is
|
||||||
for i, text := range texts {
|
// true (the default), multiple rows stay packed onto one line, matching the
|
||||||
b.WriteString(nl)
|
// pre-existing flat rendering. When false, each row gets its own line.
|
||||||
if i == 0 || st.Commas != config.CommaLeading {
|
func dmlValuesClause(kwText string, body []cst.Tok, st config.Style) string {
|
||||||
b.WriteString(first)
|
rows := filterEmpty(dmlSplitCommas(body))
|
||||||
b.WriteString(text)
|
|
||||||
if st.Commas == config.CommaTrailing && i < len(items)-1 {
|
if len(rows) <= 1 || st.InsertCollapseValues {
|
||||||
b.WriteString(",")
|
text := dmlInline(body, st)
|
||||||
}
|
if text == "" {
|
||||||
} else {
|
return kwText
|
||||||
b.WriteString(cont)
|
|
||||||
b.WriteString(text)
|
|
||||||
}
|
}
|
||||||
|
return kwText + " " + text
|
||||||
}
|
}
|
||||||
return b.String()
|
|
||||||
|
texts := make([]string, len(rows))
|
||||||
|
for i, row := range rows {
|
||||||
|
texts[i] = dmlInline(row, st)
|
||||||
|
}
|
||||||
|
return dmlCommaList(kwText, texts, st)
|
||||||
}
|
}
|
||||||
|
|
||||||
// alignSelectItems pads SELECT list item expressions so that AS keywords and
|
// alignSelectItems pads SELECT list item expressions so that AS keywords and
|
||||||
@@ -696,3 +818,230 @@ func alignSetItems(texts []string) []string {
|
|||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// caseCollapseWidth is the inline-length threshold under which case_collapse
|
||||||
|
// keeps a CASE expression on one line even when case_when_wrap is set.
|
||||||
|
const caseCollapseWidth = 60
|
||||||
|
|
||||||
|
// dmlIsCaseStart reports whether t is a CASE keyword token.
|
||||||
|
func dmlIsCaseStart(t cst.Tok) bool {
|
||||||
|
return t.Tok.Kind == lexer.Ident && lowerASCII(t.Tok.Text) == "case"
|
||||||
|
}
|
||||||
|
|
||||||
|
// dmlMatchCaseEnd returns the index of the END token that closes the CASE
|
||||||
|
// token at toks[start], accounting for nested CASE…END and paren depth.
|
||||||
|
// Returns -1 if no matching END is found.
|
||||||
|
func dmlMatchCaseEnd(toks []cst.Tok, start int) int {
|
||||||
|
depth := 0
|
||||||
|
caseDepth := 1
|
||||||
|
for i := start + 1; i < len(toks); i++ {
|
||||||
|
switch toks[i].Tok.Kind {
|
||||||
|
case lexer.LParen, lexer.LBracket:
|
||||||
|
depth++
|
||||||
|
continue
|
||||||
|
case lexer.RParen, lexer.RBracket:
|
||||||
|
if depth > 0 {
|
||||||
|
depth--
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if depth != 0 || toks[i].Tok.Kind != lexer.Ident {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch lowerASCII(toks[i].Tok.Text) {
|
||||||
|
case "case":
|
||||||
|
caseDepth++
|
||||||
|
case "end":
|
||||||
|
caseDepth--
|
||||||
|
if caseDepth == 0 {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// caseSeg is one part of a CASE expression's body: the optional leading
|
||||||
|
// operand (kw == nil), or a WHEN/THEN/ELSE-led span.
|
||||||
|
type caseSeg struct {
|
||||||
|
kw *cst.Tok
|
||||||
|
toks []cst.Tok
|
||||||
|
}
|
||||||
|
|
||||||
|
// dmlSplitCase splits a CASE expression's body (the tokens strictly between
|
||||||
|
// CASE and its matching END) into operand/when/then/else segments at
|
||||||
|
// depth-0 boundaries, skipping over any nested CASE…END.
|
||||||
|
func dmlSplitCase(body []cst.Tok) []caseSeg {
|
||||||
|
var segs []caseSeg
|
||||||
|
depth := 0
|
||||||
|
caseDepth := 0
|
||||||
|
start := 0
|
||||||
|
var curKw *cst.Tok
|
||||||
|
flush := func(end int) {
|
||||||
|
if end > start {
|
||||||
|
segs = append(segs, caseSeg{kw: curKw, toks: body[start:end]})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := range body {
|
||||||
|
t := body[i]
|
||||||
|
switch t.Tok.Kind {
|
||||||
|
case lexer.LParen, lexer.LBracket:
|
||||||
|
depth++
|
||||||
|
continue
|
||||||
|
case lexer.RParen, lexer.RBracket:
|
||||||
|
if depth > 0 {
|
||||||
|
depth--
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if depth != 0 || t.Tok.Kind != lexer.Ident {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch lowerASCII(t.Tok.Text) {
|
||||||
|
case "case":
|
||||||
|
caseDepth++
|
||||||
|
case "end":
|
||||||
|
if caseDepth > 0 {
|
||||||
|
caseDepth--
|
||||||
|
}
|
||||||
|
case "when", "then", "else":
|
||||||
|
if caseDepth == 0 {
|
||||||
|
flush(i)
|
||||||
|
start = i + 1
|
||||||
|
kw := body[i]
|
||||||
|
curKw = &kw
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flush(len(body))
|
||||||
|
return segs
|
||||||
|
}
|
||||||
|
|
||||||
|
// whenThen is one rendered WHEN … THEN … branch of a CASE expression.
|
||||||
|
type whenThen struct {
|
||||||
|
whenKw, cond, thenKw, then string
|
||||||
|
}
|
||||||
|
|
||||||
|
// dmlFormatCase renders a CASE…END expression honoring case_when_wrap,
|
||||||
|
// case_end, and case_collapse. toks[0] must be CASE and toks[len(toks)-1]
|
||||||
|
// its matching END.
|
||||||
|
func dmlFormatCase(toks []cst.Tok, st config.Style) string {
|
||||||
|
body := toks[1 : len(toks)-1]
|
||||||
|
segs := dmlSplitCase(body)
|
||||||
|
|
||||||
|
operand := ""
|
||||||
|
var whens []whenThen
|
||||||
|
elseKw, elseText := "", ""
|
||||||
|
haveElse := false
|
||||||
|
pendingWhenKw, pendingCond := "", ""
|
||||||
|
|
||||||
|
for _, s := range segs {
|
||||||
|
text := dmlInline(s.toks, st)
|
||||||
|
if s.kw == nil {
|
||||||
|
operand = text
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
kwText := caseText(s.kw.Tok, st)
|
||||||
|
switch lowerASCII(s.kw.Tok.Text) {
|
||||||
|
case "when":
|
||||||
|
pendingWhenKw, pendingCond = kwText, text
|
||||||
|
case "then":
|
||||||
|
whens = append(whens, whenThen{whenKw: pendingWhenKw, cond: pendingCond, thenKw: kwText, then: text})
|
||||||
|
case "else":
|
||||||
|
elseKw, elseText, haveElse = kwText, text, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
caseKw := caseText(toks[0].Tok, st)
|
||||||
|
endKw := caseText(toks[len(toks)-1].Tok, st)
|
||||||
|
|
||||||
|
inline := dmlCaseInline(caseKw, operand, whens, elseKw, elseText, haveElse, endKw)
|
||||||
|
if !st.CaseWhenWrap {
|
||||||
|
return inline
|
||||||
|
}
|
||||||
|
if st.CaseCollapse && len(inline) <= caseCollapseWidth {
|
||||||
|
return inline
|
||||||
|
}
|
||||||
|
return dmlCaseWrapped(caseKw, operand, whens, elseKw, elseText, haveElse, endKw, st)
|
||||||
|
}
|
||||||
|
|
||||||
|
// dmlCaseInline renders a CASE expression on a single line.
|
||||||
|
func dmlCaseInline(caseKw, operand string, whens []whenThen, elseKw, elseText string, haveElse bool, endKw string) string {
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString(caseKw)
|
||||||
|
if operand != "" {
|
||||||
|
b.WriteByte(' ')
|
||||||
|
b.WriteString(operand)
|
||||||
|
}
|
||||||
|
for _, w := range whens {
|
||||||
|
b.WriteByte(' ')
|
||||||
|
b.WriteString(w.whenKw)
|
||||||
|
if w.cond != "" {
|
||||||
|
b.WriteByte(' ')
|
||||||
|
b.WriteString(w.cond)
|
||||||
|
}
|
||||||
|
b.WriteByte(' ')
|
||||||
|
b.WriteString(w.thenKw)
|
||||||
|
if w.then != "" {
|
||||||
|
b.WriteByte(' ')
|
||||||
|
b.WriteString(w.then)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if haveElse {
|
||||||
|
b.WriteByte(' ')
|
||||||
|
b.WriteString(elseKw)
|
||||||
|
if elseText != "" {
|
||||||
|
b.WriteByte(' ')
|
||||||
|
b.WriteString(elseText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.WriteByte(' ')
|
||||||
|
b.WriteString(endKw)
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// dmlCaseWrapped renders a CASE expression with each WHEN … THEN branch (and
|
||||||
|
// ELSE) on its own line, per case_end for the closing END's placement.
|
||||||
|
func dmlCaseWrapped(caseKw, operand string, whens []whenThen, elseKw, elseText string, haveElse bool, endKw string, st config.Style) string {
|
||||||
|
nl := st.Newline
|
||||||
|
indent := st.Indent
|
||||||
|
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString(caseKw)
|
||||||
|
if operand != "" {
|
||||||
|
b.WriteByte(' ')
|
||||||
|
b.WriteString(operand)
|
||||||
|
}
|
||||||
|
for _, w := range whens {
|
||||||
|
b.WriteString(nl)
|
||||||
|
b.WriteString(indent)
|
||||||
|
b.WriteString(w.whenKw)
|
||||||
|
if w.cond != "" {
|
||||||
|
b.WriteByte(' ')
|
||||||
|
b.WriteString(w.cond)
|
||||||
|
}
|
||||||
|
b.WriteByte(' ')
|
||||||
|
b.WriteString(w.thenKw)
|
||||||
|
if w.then != "" {
|
||||||
|
b.WriteByte(' ')
|
||||||
|
b.WriteString(w.then)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if haveElse {
|
||||||
|
b.WriteString(nl)
|
||||||
|
b.WriteString(indent)
|
||||||
|
b.WriteString(elseKw)
|
||||||
|
if elseText != "" {
|
||||||
|
b.WriteByte(' ')
|
||||||
|
b.WriteString(elseText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if st.CaseEnd == config.PlacementNewLine {
|
||||||
|
b.WriteString(nl)
|
||||||
|
b.WriteString(endKw)
|
||||||
|
} else {
|
||||||
|
b.WriteByte(' ')
|
||||||
|
b.WriteString(endKw)
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package format
|
package format
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.warky.dev/wdevs/pgtidy/pkg/config"
|
"git.warky.dev/wdevs/pgtidy/pkg/config"
|
||||||
@@ -295,3 +296,318 @@ func TestCorpusUnaffectedByDML(t *testing.T) {
|
|||||||
t.Errorf("create function: DML formatter changed semantics")
|
t.Errorf("create function: DML formatter changed semantics")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Subquery formatting ---
|
||||||
|
|
||||||
|
func TestDMLSubqueryDerivedTable(t *testing.T) {
|
||||||
|
src := "select a from (select x, y from t) s where s.x = 1;"
|
||||||
|
want := "SELECT a\n" +
|
||||||
|
"FROM (\n" +
|
||||||
|
" SELECT\n" +
|
||||||
|
" x\n" +
|
||||||
|
" ,y\n" +
|
||||||
|
" FROM t\n" +
|
||||||
|
") s\n" +
|
||||||
|
"WHERE s.x = 1;\n"
|
||||||
|
got := format(src)
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("derived table\n--- got ---\n%s\n--- want ---\n%s", got, want)
|
||||||
|
}
|
||||||
|
checkDML(t, "derived table", got)
|
||||||
|
if !semanticallyEqual(src, got) {
|
||||||
|
t.Errorf("derived table: formatting changed semantics")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDMLSubqueryScalarInSelect(t *testing.T) {
|
||||||
|
src := "select a, (select max(x) from t2) as m from t1;"
|
||||||
|
got := format(src)
|
||||||
|
if got == "" {
|
||||||
|
t.Error("empty output")
|
||||||
|
}
|
||||||
|
checkDML(t, "scalar subquery", got)
|
||||||
|
if !semanticallyEqual(src, got) {
|
||||||
|
t.Errorf("scalar subquery: formatting changed semantics")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDMLSubqueryIn(t *testing.T) {
|
||||||
|
src := "select a from t where a in (select b from t2);"
|
||||||
|
want := "SELECT a\n" +
|
||||||
|
"FROM t\n" +
|
||||||
|
"WHERE a IN (\n" +
|
||||||
|
" SELECT b\n" +
|
||||||
|
" FROM t2\n" +
|
||||||
|
");\n"
|
||||||
|
got := format(src)
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("in subquery\n--- got ---\n%s\n--- want ---\n%s", got, want)
|
||||||
|
}
|
||||||
|
checkDML(t, "in subquery", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDMLSubqueryExists(t *testing.T) {
|
||||||
|
src := "select a from t where exists (select 1 from t2 where t2.a = t.a);"
|
||||||
|
got := format(src)
|
||||||
|
if got == "" {
|
||||||
|
t.Error("empty output")
|
||||||
|
}
|
||||||
|
checkDML(t, "exists subquery", got)
|
||||||
|
if !semanticallyEqual(src, got) {
|
||||||
|
t.Errorf("exists subquery: formatting changed semantics")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDMLSubqueryInValueList(t *testing.T) {
|
||||||
|
// A plain value list must not be mistaken for a subquery.
|
||||||
|
src := "select a from t where a in (1, 2, 3);"
|
||||||
|
want := "SELECT a\nFROM t\nWHERE a IN (1, 2, 3);\n"
|
||||||
|
got := format(src)
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("value list in()\n--- got ---\n%s\n--- want ---\n%s", got, want)
|
||||||
|
}
|
||||||
|
checkDML(t, "value list in()", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDMLSubqueryPlacementConfig(t *testing.T) {
|
||||||
|
st := config.Default()
|
||||||
|
st.SubqueryContent = config.PlacementSameLine
|
||||||
|
st.SubqueryClosing = config.PlacementSameLine
|
||||||
|
src := "select a from t where a in (select b from t2);"
|
||||||
|
got := File(parser.Parse(src), st)
|
||||||
|
if got == "" {
|
||||||
|
t.Error("empty output")
|
||||||
|
}
|
||||||
|
twice := File(parser.Parse(got), st)
|
||||||
|
if twice != got {
|
||||||
|
t.Errorf("subquery placement config not idempotent:\n--- once ---\n%s\n--- twice ---\n%s", got, twice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDMLSubquerySpaceBeforeParen(t *testing.T) {
|
||||||
|
st := config.Default()
|
||||||
|
st.SubquerySpaceBeforeParen = true
|
||||||
|
src := "select array(select x from t) from t2;"
|
||||||
|
got := File(parser.Parse(src), st)
|
||||||
|
want := "SELECT ARRAY (\n SELECT x\n FROM t\n)\nFROM t2;\n"
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("subquery_space_before_paren\n--- got ---\n%s\n--- want ---\n%s", got, want)
|
||||||
|
}
|
||||||
|
twice := File(parser.Parse(got), st)
|
||||||
|
if twice != got {
|
||||||
|
t.Errorf("subquery_space_before_paren not idempotent:\n--- once ---\n%s\n--- twice ---\n%s", got, twice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDMLCTEUsesSubqueryConfig(t *testing.T) {
|
||||||
|
// CTE bodies should honor the same subquery_* settings, not a hardcoded layout.
|
||||||
|
st := config.Default()
|
||||||
|
st.SubqueryOpening = config.PlacementNewLine
|
||||||
|
src := "with cte as (select x from y) select x from cte;"
|
||||||
|
got := File(parser.Parse(src), st)
|
||||||
|
want := "WITH cte AS\n(\n SELECT x\n FROM y\n)\nSELECT x\nFROM cte;\n"
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("cte subquery_opening=new_line\n--- got ---\n%s\n--- want ---\n%s", got, want)
|
||||||
|
}
|
||||||
|
twice := File(parser.Parse(got), st)
|
||||||
|
if twice != got {
|
||||||
|
t.Errorf("cte subquery_opening not idempotent:\n--- once ---\n%s\n--- twice ---\n%s", got, twice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- INSERT VALUES collapse ---
|
||||||
|
|
||||||
|
func TestDMLInsertValuesCollapseDefault(t *testing.T) {
|
||||||
|
// insert_collapse_values defaults to true: multiple rows stay on one line.
|
||||||
|
src := "insert into t (a, b) values (1, 2), (3, 4), (5, 6);"
|
||||||
|
want := "INSERT INTO t(a, b)\nVALUES (1, 2), (3, 4), (5, 6);\n"
|
||||||
|
got := format(src)
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("values collapse default\n--- got ---\n%s\n--- want ---\n%s", got, want)
|
||||||
|
}
|
||||||
|
checkDML(t, "values collapse default", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDMLInsertValuesNoCollapse(t *testing.T) {
|
||||||
|
st := config.Default()
|
||||||
|
st.InsertCollapseValues = false
|
||||||
|
src := "insert into t (a, b) values (1, 2), (3, 4), (5, 6);"
|
||||||
|
want := "INSERT INTO t(a, b)\n" +
|
||||||
|
"VALUES\n" +
|
||||||
|
" (1, 2)\n" +
|
||||||
|
" ,(3, 4)\n" +
|
||||||
|
" ,(5, 6);\n"
|
||||||
|
got := File(parser.Parse(src), st)
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("values no collapse\n--- got ---\n%s\n--- want ---\n%s", got, want)
|
||||||
|
}
|
||||||
|
twice := File(parser.Parse(got), st)
|
||||||
|
if twice != got {
|
||||||
|
t.Errorf("values no collapse not idempotent:\n--- once ---\n%s\n--- twice ---\n%s", got, twice)
|
||||||
|
}
|
||||||
|
if !semanticallyEqual(src, got) {
|
||||||
|
t.Errorf("values no collapse: formatting changed semantics")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDMLInsertValuesSingleRowUnaffected(t *testing.T) {
|
||||||
|
// A single-row VALUES is unaffected by insert_collapse_values either way.
|
||||||
|
st := config.Default()
|
||||||
|
st.InsertCollapseValues = false
|
||||||
|
src := "insert into t (a, b) values (1, 2);"
|
||||||
|
want := "INSERT INTO t(a, b)\nVALUES (1, 2);\n"
|
||||||
|
got := File(parser.Parse(src), st)
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("single row values\n--- got ---\n%s\n--- want ---\n%s", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- CASE expression formatting ---
|
||||||
|
|
||||||
|
func TestDMLCaseInlineDefault(t *testing.T) {
|
||||||
|
src := "select case when a = 1 then 'one' when a = 2 then 'two' else 'other' end as label from t;"
|
||||||
|
want := "SELECT CASE WHEN a = 1 THEN 'one' WHEN a = 2 THEN 'two' ELSE 'other' END AS label\nFROM t;\n"
|
||||||
|
got := format(src)
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("case inline default\n--- got ---\n%s\n--- want ---\n%s", got, want)
|
||||||
|
}
|
||||||
|
checkDML(t, "case inline default", got)
|
||||||
|
if !semanticallyEqual(src, got) {
|
||||||
|
t.Errorf("case inline default: formatting changed semantics")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDMLCaseWhenWrap(t *testing.T) {
|
||||||
|
st := config.Default()
|
||||||
|
st.CaseWhenWrap = true
|
||||||
|
src := "select case when a = 1 then 'one' when a = 2 then 'two' else 'other' end as label from t;"
|
||||||
|
want := "SELECT CASE\n" +
|
||||||
|
" WHEN a = 1 THEN 'one'\n" +
|
||||||
|
" WHEN a = 2 THEN 'two'\n" +
|
||||||
|
" ELSE 'other'\n" +
|
||||||
|
"END AS label\n" +
|
||||||
|
"FROM t;\n"
|
||||||
|
got := File(parser.Parse(src), st)
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("case when_wrap\n--- got ---\n%s\n--- want ---\n%s", got, want)
|
||||||
|
}
|
||||||
|
twice := File(parser.Parse(got), st)
|
||||||
|
if twice != got {
|
||||||
|
t.Errorf("case when_wrap not idempotent:\n--- once ---\n%s\n--- twice ---\n%s", got, twice)
|
||||||
|
}
|
||||||
|
if !semanticallyEqual(src, got) {
|
||||||
|
t.Errorf("case when_wrap: formatting changed semantics")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDMLCaseEndSameLine(t *testing.T) {
|
||||||
|
st := config.Default()
|
||||||
|
st.CaseWhenWrap = true
|
||||||
|
st.CaseEnd = config.PlacementSameLine
|
||||||
|
src := "select case when a = 1 then 'one' else 'other' end as label from t;"
|
||||||
|
want := "SELECT CASE\n" +
|
||||||
|
" WHEN a = 1 THEN 'one'\n" +
|
||||||
|
" ELSE 'other' END AS label\n" +
|
||||||
|
"FROM t;\n"
|
||||||
|
got := File(parser.Parse(src), st)
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("case_end same_line\n--- got ---\n%s\n--- want ---\n%s", got, want)
|
||||||
|
}
|
||||||
|
twice := File(parser.Parse(got), st)
|
||||||
|
if twice != got {
|
||||||
|
t.Errorf("case_end same_line not idempotent:\n--- once ---\n%s\n--- twice ---\n%s", got, twice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDMLCaseCollapseShort(t *testing.T) {
|
||||||
|
// case_collapse keeps a short CASE on one line even with case_when_wrap set.
|
||||||
|
st := config.Default()
|
||||||
|
st.CaseWhenWrap = true
|
||||||
|
st.CaseCollapse = true
|
||||||
|
src := "select case when a = 1 then 'x' else 'y' end from t;"
|
||||||
|
want := "SELECT CASE WHEN a = 1 THEN 'x' ELSE 'y' END\nFROM t;\n"
|
||||||
|
got := File(parser.Parse(src), st)
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("case_collapse short\n--- got ---\n%s\n--- want ---\n%s", got, want)
|
||||||
|
}
|
||||||
|
twice := File(parser.Parse(got), st)
|
||||||
|
if twice != got {
|
||||||
|
t.Errorf("case_collapse short not idempotent:\n--- once ---\n%s\n--- twice ---\n%s", got, twice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDMLCaseCollapseLongStillWraps(t *testing.T) {
|
||||||
|
// case_collapse only keeps CASE inline when it is short; a long CASE still wraps.
|
||||||
|
st := config.Default()
|
||||||
|
st.CaseWhenWrap = true
|
||||||
|
st.CaseCollapse = true
|
||||||
|
src := "select case when a = 1 then 'a fairly long result value one' " +
|
||||||
|
"when a = 2 then 'a fairly long result value two' else 'a fairly long default value' end from t;"
|
||||||
|
got := File(parser.Parse(src), st)
|
||||||
|
if !strings.Contains(got, "\n WHEN a = 1") {
|
||||||
|
t.Errorf("case_collapse long: expected wrapped WHEN branches, got:\n%s", got)
|
||||||
|
}
|
||||||
|
twice := File(parser.Parse(got), st)
|
||||||
|
if twice != got {
|
||||||
|
t.Errorf("case_collapse long not idempotent:\n--- once ---\n%s\n--- twice ---\n%s", got, twice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDMLCaseNestedInSubquery(t *testing.T) {
|
||||||
|
src := "select a from (select case when x = 1 then 'y' else 'n' end as c from t) s;"
|
||||||
|
got := format(src)
|
||||||
|
if got == "" {
|
||||||
|
t.Error("empty output")
|
||||||
|
}
|
||||||
|
checkDML(t, "case nested in subquery", got)
|
||||||
|
if !semanticallyEqual(src, got) {
|
||||||
|
t.Errorf("case nested in subquery: formatting changed semantics")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDMLSubqueryNestedInCase(t *testing.T) {
|
||||||
|
src := "select case when exists (select 1 from t2 where t2.a = t1.a) then 'y' else 'n' end from t1;"
|
||||||
|
got := format(src)
|
||||||
|
if got == "" {
|
||||||
|
t.Error("empty output")
|
||||||
|
}
|
||||||
|
checkDML(t, "subquery nested in case", got)
|
||||||
|
if !semanticallyEqual(src, got) {
|
||||||
|
t.Errorf("subquery nested in case: formatting changed semantics")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDMLCaseSimpleForm(t *testing.T) {
|
||||||
|
// Simple CASE (with an operand) must round-trip too.
|
||||||
|
src := "select case a when 1 then 'one' when 2 then 'two' else 'other' end from t;"
|
||||||
|
want := "SELECT CASE a WHEN 1 THEN 'one' WHEN 2 THEN 'two' ELSE 'other' END\nFROM t;\n"
|
||||||
|
got := format(src)
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("simple case\n--- got ---\n%s\n--- want ---\n%s", got, want)
|
||||||
|
}
|
||||||
|
checkDML(t, "simple case", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- record_space_before_paren ---
|
||||||
|
|
||||||
|
func TestDMLRecordSpaceBeforeParen(t *testing.T) {
|
||||||
|
src := "select row(1, 2) from t;"
|
||||||
|
|
||||||
|
got := format(src)
|
||||||
|
want := "SELECT ROW(1, 2)\nFROM t;\n"
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("row() default\n--- got ---\n%s\n--- want ---\n%s", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
st := config.Default()
|
||||||
|
st.RecordSpaceBeforeParen = true
|
||||||
|
gotSpaced := File(parser.Parse(src), st)
|
||||||
|
wantSpaced := "SELECT ROW (1, 2)\nFROM t;\n"
|
||||||
|
if gotSpaced != wantSpaced {
|
||||||
|
t.Errorf("row() space_before_paren\n--- got ---\n%s\n--- want ---\n%s", gotSpaced, wantSpaced)
|
||||||
|
}
|
||||||
|
twice := File(parser.Parse(gotSpaced), st)
|
||||||
|
if twice != gotSpaced {
|
||||||
|
t.Errorf("row() space_before_paren not idempotent:\n--- once ---\n%s\n--- twice ---\n%s", gotSpaced, twice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user