chore: more work done and planning with AI.
This commit is contained in:
+264
-8
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user