chore: more work done and planning with AI.
CI / Test (push) Failing after 25s
CI / Build (push) Has been skipped

This commit is contained in:
2026-06-30 22:43:25 +02:00
parent a58b081cae
commit c8030247f2
15 changed files with 4948 additions and 157 deletions
+264 -8
View File
@@ -179,9 +179,16 @@ func dmlSegText(seg dmlSeg, st config.Style) string {
}
switch kw {
case "select", "set", "returning":
case "select", "returning":
items := dmlSplitCommas(seg.body)
return dmlColList(kwText, items, st)
return dmlColListSelect(kwText, items, st)
case "set":
items := dmlSplitCommas(seg.body)
return dmlColListSet(kwText, items, st)
case "where":
return dmlWhereClause(kwText, seg.body, st)
case "join", "left", "right", "inner", "full", "cross", "natural":
return dmlJoinClause(kwText, seg.body, st)
case "with":
return formatWithBody(kwText, seg.body, st)
default:
@@ -193,6 +200,97 @@ func dmlSegText(seg dmlSeg, st config.Style) string {
}
}
// dmlJoinClause formats a JOIN clause, applying indent_join when configured.
func dmlJoinClause(kwText string, body []cst.Tok, st config.Style) string {
text := dmlInline(body, st)
line := kwText
if text != "" {
line += " " + text
}
if !st.IndentJoin {
return line
}
indent := strings.Repeat(st.Indent, st.JoinIndentSize)
nl := st.Newline
var b strings.Builder
for i, part := range strings.Split(line, nl) {
if i > 0 {
b.WriteString(nl)
}
b.WriteString(indent)
b.WriteString(part)
}
return b.String()
}
// dmlWhereClause formats a WHERE clause, splitting AND/OR conditions per
// the where_wrap and where_and_or_indent settings.
func dmlWhereClause(kwText string, body []cst.Tok, st config.Style) string {
if st.WhereWrap == config.WrapNever {
text := dmlInline(body, st)
if text == "" {
return kwText
}
return kwText + " " + text
}
// Split at depth-0 AND/OR.
conditions := dmlSplitAndOr(body)
if len(conditions) <= 1 {
text := dmlInline(body, st)
if text == "" {
return kwText
}
return kwText + " " + text
}
nl := st.Newline
var b strings.Builder
b.WriteString(kwText)
for i, cond := range conditions {
b.WriteString(nl)
text := dmlInline(cond, st)
if st.WhereAndOrIndent {
b.WriteString(st.Indent)
}
if i == 0 {
// First condition: no leading AND/OR
b.WriteString(" ") // align with AND/OR token width
b.WriteString(text)
} else {
b.WriteString(text)
}
}
return b.String()
}
// dmlSplitAndOr splits toks at depth-0 AND/OR tokens, keeping the AND/OR with
// the following condition.
func dmlSplitAndOr(toks []cst.Tok) [][]cst.Tok {
var result [][]cst.Tok
depth := 0
start := 0
for i, t := range toks {
switch t.Tok.Kind {
case lexer.LParen, lexer.LBracket:
depth++
case lexer.RParen, lexer.RBracket:
if depth > 0 {
depth--
}
}
if depth == 0 && t.Tok.Kind == lexer.Ident {
low := lowerASCII(t.Tok.Text)
if (low == "and" || low == "or") && i > start {
result = append(result, toks[start:i])
start = i
}
}
}
result = append(result, toks[start:])
return result
}
// formatWithBody formats the body of a WITH clause by splitting CTE definitions
// at depth-0 commas and formatting the subquery inside each AS (...) block.
func formatWithBody(kwText string, body []cst.Tok, st config.Style) string {
@@ -370,7 +468,18 @@ func dmlInline(toks []cst.Tok, st config.Style) string {
if i > 0 && needSpace(toks[i-1].Tok, t.Tok) {
b.WriteByte(' ')
}
b.WriteString(caseText(t.Tok, st))
// Space after comma in calls: func(a, b) vs func(a,b).
if st.SpaceAfterCommaInCalls && i > 0 && toks[i-1].Tok.Kind == lexer.Comma {
// Only inside parens (caller manages this at depth > 0, but we add space
// when the comma is not a clause-level comma — heuristic: always add).
b.WriteByte(' ')
}
var prev lexer.Token
if i > 0 {
prev = toks[i-1].Tok
}
nextIsLParen := i+1 < len(toks) && toks[i+1].Tok.Kind == lexer.LParen
b.WriteString(caseTextCtx(t.Tok, prev, nextIsLParen, st))
}
return b.String()
}
@@ -405,7 +514,12 @@ func dmlSplitCommas(toks []cst.Tok) [][]cst.Tok {
// One item: kept on the same line as the keyword.
// Multiple items: each on its own line with the configured comma style.
func dmlColList(kwText string, items [][]cst.Tok, st config.Style) string {
// Filter out spurious empty items (e.g. trailing comma in source).
return dmlColListSelect(kwText, items, st)
}
// 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 {
var kept [][]cst.Tok
for _, item := range items {
if len(item) > 0 {
@@ -426,14 +540,23 @@ func dmlColList(kwText string, items [][]cst.Tok, st config.Style) string {
return kwText + " " + body
}
// Multiple items: one per line.
first := st.Indent + " " // aligns item text one column past the comma
// Render each item text.
texts := make([]string, len(items))
for i, item := range items {
texts[i] = dmlInline(item, st)
}
// align_columns / select_align_as: pad expressions so AS and aliases align.
if (st.AlignColumns || st.SelectAlignAs) && len(texts) > 1 {
texts = alignSelectItems(texts, st)
}
first := st.Indent + " "
cont := st.Indent + ","
var b strings.Builder
b.WriteString(kwText)
for i, item := range items {
for i, text := range texts {
b.WriteString(nl)
text := dmlInline(item, st)
if i == 0 || st.Commas != config.CommaLeading {
b.WriteString(first)
b.WriteString(text)
@@ -447,3 +570,136 @@ func dmlColList(kwText string, items [][]cst.Tok, st config.Style) string {
}
return b.String()
}
// dmlColListSet formats an UPDATE SET column list with optional set_align_equal.
func dmlColListSet(kwText string, items [][]cst.Tok, st config.Style) string {
var kept [][]cst.Tok
for _, item := range items {
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)
if body == "" {
return kwText
}
return kwText + " " + body
}
texts := make([]string, len(items))
for i, item := range items {
texts[i] = dmlInline(item, st)
}
// set_align_equal: pad lhs so = signs align.
if st.SetAlignEqual && len(texts) > 1 {
texts = alignSetItems(texts)
}
first := st.Indent + " "
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()
}
// alignSelectItems pads SELECT list item expressions so that AS keywords and
// alias names align vertically.
func alignSelectItems(texts []string, st config.Style) []string {
// Split each text into (expr, " AS ", alias) or keep as-is.
type part struct{ expr, alias string; hasAs bool }
parts := make([]part, len(texts))
maxExpr := 0
for i, t := range texts {
// Find " AS " or " as " (case-insensitive).
if idx := findAsIndex(t); idx >= 0 {
parts[i] = part{expr: t[:idx], alias: t[idx:], hasAs: true}
if l := len(t[:idx]); l > maxExpr {
maxExpr = l
}
} else {
parts[i] = part{expr: t}
if st.AlignColumns {
if l := len(t); l > maxExpr {
maxExpr = l
}
}
}
}
out := make([]string, len(texts))
for i, p := range parts {
if !p.hasAs || maxExpr == 0 {
out[i] = texts[i]
continue
}
pad := strings.Repeat(" ", maxExpr-len(p.expr))
out[i] = p.expr + pad + p.alias
}
return out
}
// findAsIndex returns the byte index of " AS " (case-insensitive) in s,
// or -1 if not present at depth 0.
func findAsIndex(s string) int {
low := lowerASCII(s)
// Look for " as " boundary.
for i := 0; i < len(low)-3; i++ {
if low[i] == ' ' && low[i+1] == 'a' && low[i+2] == 's' && low[i+3] == ' ' {
return i + 1 // index of 'a'
}
}
return -1
}
// alignSetItems pads SET assignment lhs values so that = signs align.
func alignSetItems(texts []string) []string {
maxLhs := 0
lhsWidths := make([]int, len(texts))
for i, t := range texts {
idx := strings.Index(t, " = ")
if idx < 0 {
idx = strings.Index(t, "=")
}
if idx >= 0 {
lhsWidths[i] = idx
if idx > maxLhs {
maxLhs = idx
}
}
}
if maxLhs == 0 {
return texts
}
out := make([]string, len(texts))
for i, t := range texts {
if lhsWidths[i] == 0 || lhsWidths[i] == maxLhs {
out[i] = t
continue
}
idx := lhsWidths[i]
pad := strings.Repeat(" ", maxLhs-idx)
out[i] = t[:idx] + pad + t[idx:]
}
return out
}