refactor: move formatting logic from ast to pkg/formatter#342
refactor: move formatting logic from ast to pkg/formatter#342ajitpratap0 merged 4 commits intomainfrom
Conversation
|
Claude finished @ajitpratap0's task —— View job Comprehensive Code ReviewThis PR successfully refactors the formatting logic from AST nodes to a dedicated visitor-based formatter package. Here's my detailed review: ✅ Architectural Quality - ExcellentClean Separation of Concerns: The refactoring properly separates data structures from rendering logic, following the Go standard library pattern ( Visitor Pattern Implementation: The new
Dependency Architecture: Clean module hierarchy maintained:
✅ Code Quality & Go Best Practices - Very GoodMemory Management: Proper object pool usage maintained: defer tokenizer.PutTokenizer(tkz) // ✓
defer ast.ReleaseAST(parsedAST) // ✓Error Handling: Robust error propagation with proper wrapping: return "", fmt.Errorf("tokenization failed: %w", err) // ✓Null Safety: Consistent nil checks throughout render functions: if s == nil { return "" } // ✓ Pattern used everywhereFallback Mechanism: Smart fallback to original func exprSQL(e ast.Expression) string {
if s, ok := e.(interface{ SQL() string }); ok {
return s.SQL() // Maintains compatibility
}
return e.TokenLiteral()
}✅ Potential Issues - MinorPerformance Considerations:
Code Duplication: Some helper functions like ✅ Test Coverage & Migration - ExcellentComplete Test Migration: All 1,116 LOC successfully ported from Test Structure:
Backward Compatibility: Old test files properly updated to test only configuration types, maintaining API compatibility. ✅ Security & Performance - GoodNo Security Concerns:
Performance Analysis:
Race Safety: Object pooling patterns preserved, maintaining thread-safety claims (20,000+ concurrent ops tested).
|
ajitpratap0
left a comment
There was a problem hiding this comment.
Architect Review — PR #342: refactor: move formatting logic from ast to pkg/formatter
Summary
This PR implements the go/ast + go/printer separation pattern — AST nodes become pure data structures and all rendering logic moves to pkg/formatter. This is the architecturally correct direction and aligns with Go stdlib conventions. The 1,379-line render.go is well-organized.
✅ Strong Points
1. Correct pattern, right rationale
The AST package having dual responsibility (data representation + SQL generation) is a legitimate code smell. Separating these concerns enables independent evolution — you can add new rendering styles without touching AST data types.
2. FormatStatement fallback is pragmatic
default:
// Fallback to SQL() for unrecognized statement types
return stmtSQL(s)This is a safe catch-all — unrecognized types gracefully degrade to their SQL() method rather than panicking or returning empty string. Good defensive design.
3. FormatExpression same pattern
Handles the complex expression types (CaseExpression, BetweenExpression, InExpression, ExistsExpression, SubqueryExpression) with explicit renderers, falls back to exprSQL() for simple types. Correct.
4. transform.FormatSQL simplified
The old implementation used a double type assertion dance. The new one is a single formatter.FormatStatement(stmt, ast.CompactStyle()) call. Much cleaner.
🔴 Breaking Change: AST Format() Methods Removed
The PR removes Format(opts FormatOptions) string methods from all AST node types. This is a public API breaking change if any external code calls these methods directly.
Specifically removed:
(AST).Format()(*SelectStatement).Format()(*InsertStatement).Format()(*UpdateStatement).Format()(*DeleteStatement).Format()(*CreateTableStatement).Format()(*SetOperation).Format()- (and others)
Impact assessment:
- Any downstream code calling
stmt.Format(ast.ReadableStyle())will fail to compile - If GoSQLX is a library (it appears to be), this is a semver-major change
- Mitigation: Keep the methods as thin wrappers delegating to
formatter.FormatStatement()for one release cycle, then deprecate
Recommendation: Either keep deprecated wrapper methods OR ensure this is shipped as a major version bump.
Design Concerns
1. render.go is 1,379 lines — consider splitting
While moving rendering to pkg/formatter is correct, a 1,379-line file may grow unwieldy. Consider:
render_select.go— SELECT renderingrender_ddl.go— DDL statement renderingrender_dml.go— DML statement renderingrender_expressions.go— expression rendering
This is a follow-up suggestion, not blocking.
2. Comment rendering in FormatAST is incomplete
The comment handling distinguishes Inline comments (trailing) vs non-inline (leading). But inline comments are only appended after ALL statements, not after their respective statement. This means:
SELECT 1; -- comment1
SELECT 2; -- comment2Would render as:
SELECT 1
SELECT 2 -- comment1 -- comment2
This should be noted as a known limitation or fixed.
3. sep logic in FormatAST has a bug
sep := ";\n"
if opts.AddSemicolon {
sep = "\n"
}
result := strings.Join(parts, sep)When AddSemicolon is true, individual statements are expected to end with ;, so the separator becomes \n. But FormatStatement doesn't add semicolons — the caller is responsible. If AddSemicolon = false and sep = ";\n", the last statement won't have a trailing semicolon. This is slightly inconsistent behavior worth documenting.
4. nodeFormatter.result() method is never called externally
The result() method on nodeFormatter is only accessible within render.go internals. The public API goes through the top-level FormatAST/FormatStatement/FormatExpression functions. This is fine, but nodeFormatter could be unexported to prevent misuse.
5. Conflict with #345 (token unification)
The formatter PR's render.go calls into AST node SQL() methods as fallbacks (exprSQL(), stmtSQL()). After #345 merges, the SQL() methods themselves may need to be updated if they relied on the old token literal fields. Verify no transitive breakage.
Test Coverage
PR body says "All existing format tests ported and passing" and "Output is byte-identical to previous implementation." This is the right claim to make. Ensure the test suite includes:
- Round-trip tests (format → parse → format, check idempotency)
- All statement types covered in render tests
- Compact vs Readable style outputs verified
Verdict
Approve with conditions:
- Either retain deprecated
Format()methods as wrappers OR explicitly document this as a breaking API change - Fix or document the inline comment rendering issue
- Resolve conflict with #345 before merge
Restores public Format(FormatOptions) string methods on all AST statement and expression types that were removed without a deprecation shim in refactor/move-formatting (PR #342). This is a breaking change for downstream consumers who call stmt.Format(opts) directly. ## Approach A circular import cycle prevents ast from directly importing pkg/formatter (formatter already imports ast). We use a function-hook bridge: - pkg/sql/ast/format.go — adds FormatStatementFunc, FormatExpressionFunc, FormatASTFunc package-level variables - pkg/formatter/compat.go — init() registers the real renderers into those variables (runs automatically when formatter is imported) - pkg/sql/ast/format_compat.go — deprecated Format() shims on every statement and expression type that had a Format() method before the refactor If pkg/formatter is not imported the shims fall back to SQL() output, so existing code always compiles. When formatter IS imported (the common path) the shims produce fully formatted output, identical to the old behaviour. ## Types restored Statement: SelectStatement, InsertStatement, UpdateStatement, DeleteStatement, CreateTableStatement, AlterTableStatement, CreateIndexStatement, MergeStatement, CreateViewStatement, CreateMaterializedViewStatement, RefreshMaterializedViewStatement, DropStatement, TruncateStatement, SetOperation, AST Expression: CaseExpression, BetweenExpression, InExpression, ExistsExpression, SubqueryExpression All methods carry a // Deprecated: use pkg/formatter.FormatStatement() instead. doc comment.
ajitpratap0
left a comment
There was a problem hiding this comment.
Architect Review — PR #342: Move Formatting Logic from AST to pkg/formatter
Summary
This PR correctly moves formatting logic from the pkg/sql/ast package into a dedicated pkg/formatter package, breaking the circular concern of AST nodes knowing how to render themselves. The visitor-based renderer approach is architecturally clean. The PR also removes the old Format() methods from AST types.
⛔ Blocking Issue: Breaking API Change — Backward Compat Wrappers Missing From This PR Branch
This PR removes Format(FormatOptions) methods from AST types without providing backward-compatible shims. The branch fix/deprecated-format-wrappers contains the critical fix:
fc5cde5 fix: add deprecated Format() wrappers for backward compatibility
This commit adds:
pkg/sql/ast/format_compat.go— Re-adds Format() methods on SelectStatement, InsertStatement, UpdateStatement, DeleteStatement, CreateTableStatement, AlterTableStatement, CreateIndexStatement, and AST that delegate to pkg/formatter via registered hookspkg/sql/ast/format.go— Adds FormatStatementFunc, FormatExpressionFunc, FormatASTFunc hook variablespkg/formatter/compat.go— init() registers the formatter functions into the ast hook variables
Without this commit, any downstream code that calls stmt.Format(opts) will fail to compile since the methods have been removed. This is a hard breaking change on a public API.
Action required: Merge fix/deprecated-format-wrappers into fix-formatter before this PR can be approved.
Design Review of the Backward Compat Approach (Fix Branch)
The chosen implementation pattern is clever and correct:
Import cycle avoidance:
The pkg/sql/ast → pkg/formatter → pkg/sql/ast import cycle is broken by:
pkg/sql/astdefines function variable hooks (FormatStatementFunc, etc.) initialized to nilpkg/formattersets these hooks viainit()- Deprecated methods in
pkg/sql/astcall through the hooks, falling back toSQL()if hooks are nil
This is a valid pattern (similar to net/http plugin registration), but has one significant risk:
If user code calls stmt.Format(opts) in its own init() before pkg/formatter's init() has run, the hook will be nil and the fallback SQL() output (unformatted) will be returned silently. This is subtle because the caller gets output that looks plausible but is wrong. The fallback should at minimum log a warning or be documented clearly. Consider adding a runtime check or making the nil case more visible in the docs.
Type coverage gap:
format_compat.go adds Format() for SelectStatement, InsertStatement, UpdateStatement, DeleteStatement, CreateTableStatement, AlterTableStatement, CreateIndexStatement, and AST. However, the original AST package also had Format() on:
- CreateViewStatement
- CreateMaterializedViewStatement
- RefreshMaterializedViewStatement
- DropStatement
- AlterStatement (as opposed to AlterTableStatement)
- BetweenExpression, InExpression, ExistsExpression, SubqueryExpression, CaseExpression
If these were previously public methods, omitting them from the compat file is still a breaking change. The PR removes tests for these types (TestBetweenFormat_Not, TestInFormat_Values, TestExistsFormat, etc.) — confirm these tests were removed because the types no longer have Format() or because the tests moved to pkg/formatter.
The fuzz test and transform package changes are correct:
fuzz_test.go: Updatingparsed.Format(style)→formatter.FormatAST(parsed, style)is correct for tests inside the ast package that can import formatter without cycle issuestransform.go:FormatSQL()now callsformatter.FormatStatement()directly — correct and cleaner
What Is Good
- The architectural separation is correct: AST types should not contain rendering logic
- The visitor-based formatter in pkg/formatter is the right design
- The test removals from pkg/sql/ast are appropriate — formatting tests belong in pkg/formatter
- The new format_test.go in pkg/sql/ast correctly tests only style constants, not rendering behavior
- The PR correctly updates the fuzz test and transform package
Decision
Status: CHANGES REQUIRED
Required before merge:
- Merge
fix/deprecated-format-wrappersintofix-formatter(backward compat is mandatory for a public API change) - Audit the compat file for completeness — ensure all previously public Format() methods are covered or explicitly documented as removed
- Document the init() ordering caveat in the public API comments on the hook variables
|
Claude finished @ajitpratap0's task —— View job Comprehensive Code Review: PR #342 - Move Formatting Logic to pkg/formatter✅ Architectural Quality - ExcellentPerfect Separation of Concerns: This refactoring successfully implements the go/ast + go/printer pattern used by the Go standard library. AST nodes are now pure data structures (1,167 LOC of formatting logic removed), while all rendering lives in the dedicated Clean Module Dependencies:
✅ Backward Compatibility - Sophisticated ImplementationThe compatibility mechanism is exceptionally well-designed: Hook System: Uses function variables in AST package: // pkg/sql/ast/format.go:26
var FormatStatementFunc func(Statement, FormatOptions) string
var FormatExpressionFunc func(Expression, FormatOptions) string
var FormatASTFunc func(*AST, FormatOptions) stringAuto-Registration: func init() {
ast.FormatStatementFunc = FormatStatement
ast.FormatExpressionFunc = FormatExpression
ast.FormatASTFunc = FormatAST
}Graceful Fallback: If formatter isn't imported, methods fall back to Comprehensive Coverage: All statement types (SelectStatement, InsertStatement, UpdateStatement, DeleteStatement, CreateTableStatement, etc.) and expression types (CaseExpression, BetweenExpression, InExpression, etc.) have deprecated ✅ Code Quality & Best Practices - Very GoodMemory Efficiency: Proper use of
Error Handling: Consistent nil checks and fallback patterns: func FormatStatement(s ast.Statement, opts ast.FormatOptions) string {
if s == nil { return "" } // ✓ Defensive programming
// Type switch with fallback to stmtSQL(s)
}Smart Fallback Logic: func exprSQL(e ast.Expression) string {
if s, ok := e.(interface{ SQL() string }); ok {
return s.SQL() // Maintains compatibility
}
return e.TokenLiteral()
}✅ Test Coverage - Complete MigrationThorough Port: All 1,116 LOC successfully migrated from Test Structure:
AST Package Cleanup: Original
|
|
Claude finished @ajitpratap0's task —— View job Pull Request Review: Move Formatting Logic to pkg/formatterComprehensive review of PR #342 - refactor to move formatting logic from AST to dedicated formatter package. Todo List
✅ Architectural Quality - ExcellentPerfect Separation of Concerns: This refactoring successfully implements the go/ast + go/printer pattern from the Go standard library. The 1,379-line Visitor Pattern Implementation: Clean type-switch based rendering in
Module Dependencies: Clean hierarchy maintained - ✅ Backward Compatibility - Sophisticated ImplementationThe compatibility mechanism is exceptionally well-designed: Hook System:
Comprehensive Coverage: 202 lines of backward compatibility wrappers covering:
Import Cycle Avoidance: Clever use of function variables avoids the forbidden ✅ Code Quality & Best Practices - Very GoodMemory Efficiency: Proper use of f := newNodeFormatter(opts)
sb := f.sb // Reused strings.BuilderPre-allocated Slices: Smart capacity hints in many places: parts := make([]string, 0, len(a.Statements)) // ✓Defensive Programming: Consistent nil checks: func FormatStatement(s ast.Statement, opts ast.FormatOptions) string {
if s == nil { return "" } // ✓
// ...
}Helper Function Organization: Clean separation of concerns with focused helper functions:
✅ Test Coverage - Complete MigrationThorough Port: All 1,116 lines successfully migrated from Test Structure:
AST Package Cleanup: Original format tests now only cover style constants (
|
Restores public Format(FormatOptions) string methods on all AST statement and expression types that were removed without a deprecation shim in refactor/move-formatting (PR #342). This is a breaking change for downstream consumers who call stmt.Format(opts) directly. ## Approach A circular import cycle prevents ast from directly importing pkg/formatter (formatter already imports ast). We use a function-hook bridge: - pkg/sql/ast/format.go — adds FormatStatementFunc, FormatExpressionFunc, FormatASTFunc package-level variables - pkg/formatter/compat.go — init() registers the real renderers into those variables (runs automatically when formatter is imported) - pkg/sql/ast/format_compat.go — deprecated Format() shims on every statement and expression type that had a Format() method before the refactor If pkg/formatter is not imported the shims fall back to SQL() output, so existing code always compiles. When formatter IS imported (the common path) the shims produce fully formatted output, identical to the old behaviour. ## Types restored Statement: SelectStatement, InsertStatement, UpdateStatement, DeleteStatement, CreateTableStatement, AlterTableStatement, CreateIndexStatement, MergeStatement, CreateViewStatement, CreateMaterializedViewStatement, RefreshMaterializedViewStatement, DropStatement, TruncateStatement, SetOperation, AST Expression: CaseExpression, BetweenExpression, InExpression, ExistsExpression, SubqueryExpression All methods carry a // Deprecated: use pkg/formatter.FormatStatement() instead. doc comment.
8f734a6 to
a8b002f
Compare
|
Claude finished @ajitpratap0's task —— View job Pull Request Review: Move Formatting Logic to pkg/formatterComprehensive review of PR #342 - refactor to move formatting logic from AST to dedicated formatter package. ✅ Architectural Quality - ExcellentPerfect Separation of Concerns: This PR successfully implements the go/ast + go/printer pattern from the Go standard library. The refactoring moves 1,379 lines of formatting logic from Clean Visitor Pattern: The new
Proper Module Dependencies: Clean hierarchy maintained - ✅ Backward Compatibility - Sophisticated ImplementationThe compatibility mechanism is exceptionally well-designed: Hook System: Uses function variables in AST package: // pkg/sql/ast/format.go:96
var FormatStatementFunc func(Statement, FormatOptions) string
var FormatExpressionFunc func(Expression, FormatOptions) string
var FormatASTFunc func(*AST, FormatOptions) stringAuto-Registration: func init() {
ast.FormatStatementFunc = FormatStatement
ast.FormatExpressionFunc = FormatExpression
ast.FormatASTFunc = FormatAST
}Graceful Fallback: If formatter isn't imported, methods fall back to Comprehensive Coverage: All statement types (SelectStatement, InsertStatement, UpdateStatement, DeleteStatement, CreateTableStatement, etc.) and expression types (CaseExpression, BetweenExpression, InExpression, etc.) have deprecated ✅ Code Quality & Best Practices - Very GoodMemory Efficiency: Proper use of
Error Handling: Consistent nil checks and fallback patterns: func FormatStatement(s ast.Statement, opts ast.FormatOptions) string {
if s == nil { return "" } // ✓ Defensive programming
// Type switch with fallback to stmtSQL(s)
}Smart Fallback Logic: func exprSQL(e ast.Expression) string {
if s, ok := e.(interface{ SQL() string }); ok {
return s.SQL() // Maintains compatibility
}
return e.TokenLiteral()
}
|
…sion The standalone staticcheck action (dominikh/staticcheck-action) requires //lint:ignore SA1019 directives (on the line before the issue), while golangci-lint requires //nolint:staticcheck inline. This commit uses both forms to satisfy both linters without errors. Previous commit used //nolint:staticcheck only, which passed golangci-lint but failed the CI staticcheck action with SA1019 on the three deprecated ast bridge variables.
|
Claude finished @ajitpratap0's task —— View job Pull Request Review: Move Formatting Logic to pkg/formatterReviewing PR #342 - refactor to move formatting logic from AST to dedicated formatter package. Todo List
✅ Architectural Quality - ExcellentPerfect Separation of Concerns: This PR successfully implements the go/ast + go/printer pattern from the Go standard library. The refactoring moves 1,379 lines of formatting logic from Clean Visitor Pattern: The new
Proper Module Dependencies: Clean hierarchy maintained - ✅ Backward Compatibility - Sophisticated ImplementationThe compatibility mechanism is exceptionally well-designed: Hook System: Uses function variables in AST package: // pkg/sql/ast/format.go:96
var FormatStatementFunc func(Statement, FormatOptions) string
var FormatExpressionFunc func(Expression, FormatOptions) string
var FormatASTFunc func(*AST, FormatOptions) stringAuto-Registration: func init() {
ast.FormatStatementFunc = FormatStatement
ast.FormatExpressionFunc = FormatExpression
ast.FormatASTFunc = FormatAST
}Graceful Fallback: If formatter isn't imported, methods fall back to Comprehensive Coverage: All statement types (SelectStatement, InsertStatement, UpdateStatement, DeleteStatement, CreateTableStatement, etc.) and expression types (CaseExpression, BetweenExpression, InExpression, etc.) have deprecated ✅ Code Quality & Best Practices - Very GoodMemory Efficiency: Proper use of
Defensive Programming: Consistent nil checks and fallback patterns: func FormatStatement(s ast.Statement, opts ast.FormatOptions) string {
if s == nil { return "" } // ✓ Defensive programming
// Type switch with fallback to stmtSQL(s)
}Smart Fallback Logic: func exprSQL(e ast.Expression) string {
if s, ok := e.(interface{ SQL() string }); ok {
return s.SQL() // Maintains compatibility
}
return e.TokenLiteral()
}
|
Problem
Formatting logic (1,167 LOC) lives on AST nodes in pkg/sql/ast/format.go. AST package has dual responsibility: data representation AND SQL generation. pkg/formatter is just a thin wrapper.
Fix