Files
upstream-sqlc/internal/endtoend/fmt_test.go
upstream-mirror 0f4df23290
Some checks failed
kotlin / test (push) Has been skipped
python / test (push) Has been skipped
typescript / test (push) Has been skipped
go / build darwin/amd64 (push) Has been cancelled
go / build linux/amd64 (push) Has been cancelled
go / build windows/amd64 (push) Has been cancelled
go / build darwin/arm64 (push) Has been cancelled
go / build linux/arm64 (push) Has been cancelled
go / build windows/arm64 (push) Has been cancelled
go / test (push) Has been cancelled
go / vuln_check (push) Has been cancelled
snapshot of https://github.com/sqlc-dev/sqlc.git (main, 2026-09-16; blobs > 131072B omitted)
2026-09-16 02:17:34 +05:00

195 lines
5.2 KiB
Go

package main
import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"testing"
"github.com/sqlc-dev/sqlc/internal/config"
"github.com/sqlc-dev/sqlc/internal/debug"
"github.com/sqlc-dev/sqlc/internal/engine/dolphin"
"github.com/sqlc-dev/sqlc/internal/engine/postgresql"
"github.com/sqlc-dev/sqlc/internal/engine/sqlite"
"github.com/sqlc-dev/sqlc/internal/sql/ast"
"github.com/sqlc-dev/sqlc/internal/sql/format"
)
// sqlParser is an interface for SQL parsers
type sqlParser interface {
Parse(r io.Reader) ([]ast.Statement, error)
}
// sqlFormatter is an interface for formatters
type sqlFormatter interface {
format.Dialect
}
func TestFormat(t *testing.T) {
t.Parallel()
for _, tc := range FindTests(t, "testdata", "base") {
// Config-less command tests (parse, analyze) have no config to format.
if tc.ConfigName == "" {
continue
}
t.Run(tc.Name, func(t *testing.T) {
// Parse the config file to determine the engine
configPath := filepath.Join(tc.Path, tc.ConfigName)
configFile, err := os.Open(configPath)
if err != nil {
t.Fatal(err)
}
conf, err := config.ParseConfig(configFile)
configFile.Close()
if err != nil {
t.Fatal(err)
}
// Skip if there are no SQL packages configured
if len(conf.SQL) == 0 {
return
}
engine := conf.SQL[0].Engine
// Select the appropriate parser and fingerprint function based on engine
var parse sqlParser
var formatter sqlFormatter
var fingerprint func(string) (string, error)
switch engine {
case config.EnginePostgreSQL:
pgParser := postgresql.NewParser()
parse = pgParser
formatter = pgParser
fingerprint = postgresql.Fingerprint
case config.EngineMySQL:
mysqlParser := dolphin.NewParser()
parse = mysqlParser
formatter = mysqlParser
// For MySQL, we use a "round-trip" fingerprint: parse the SQL, format it,
// and return the formatted string. This tests that our formatting produces
// valid SQL that parses to the same AST structure.
fingerprint = func(sql string) (string, error) {
stmts, err := mysqlParser.Parse(strings.NewReader(sql))
if err != nil {
return "", err
}
if len(stmts) == 0 {
return "", nil
}
return ast.Format(stmts[0].Raw, mysqlParser), nil
}
case config.EngineSQLite:
sqliteParser := sqlite.NewParser()
parse = sqliteParser
formatter = sqliteParser
// For SQLite, we use the same "round-trip" fingerprint strategy as MySQL:
// parse the SQL, format it, and return the formatted string.
fingerprint = func(sql string) (string, error) {
stmts, err := sqliteParser.Parse(strings.NewReader(sql))
if err != nil {
return "", err
}
if len(stmts) == 0 {
return "", nil
}
return strings.ToLower(ast.Format(stmts[0].Raw, sqliteParser)), nil
}
default:
// Skip unsupported engines
return
}
// Find query files from config
var queryFiles []string
for _, sql := range conf.SQL {
for _, q := range sql.Queries {
queryPath := filepath.Join(tc.Path, q)
info, err := os.Stat(queryPath)
if err != nil {
continue
}
if info.IsDir() {
// If it's a directory, glob for .sql files
matches, err := filepath.Glob(filepath.Join(queryPath, "*.sql"))
if err != nil {
continue
}
queryFiles = append(queryFiles, matches...)
} else {
queryFiles = append(queryFiles, queryPath)
}
}
}
if len(queryFiles) == 0 {
return
}
for _, queryFile := range queryFiles {
if _, err := os.Stat(queryFile); os.IsNotExist(err) {
continue
}
contents, err := os.ReadFile(queryFile)
if err != nil {
t.Fatal(err)
}
// Parse the entire file to get proper statement boundaries
stmts, err := parse.Parse(bytes.NewReader(contents))
if err != nil {
// Skip files with parse errors (e.g., syntax_errors test cases)
return
}
for i, stmt := range stmts {
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
// Extract the original query text using statement location and length
start := stmt.Raw.StmtLocation
length := stmt.Raw.StmtLen
if length == 0 {
// If StmtLen is 0, it means the statement goes to the end of the input
length = len(contents) - start
}
query := strings.TrimSpace(string(contents[start : start+length]))
expected, err := fingerprint(query)
if err != nil {
t.Fatal(err)
}
if false {
r, err := postgresql.Parse(query)
debug.Dump(r, err)
}
out := ast.Format(stmt.Raw, formatter)
// An empty rendering is the formatter's signal that
// the statement carries syntax the AST does not
// model; sqlc fmt keeps such statements as written.
if strings.TrimSpace(strings.TrimSuffix(strings.TrimSpace(out), ";")) == "" {
t.Skip("statement has no faithful rendering")
}
actual, err := fingerprint(out)
if err != nil {
t.Error(err)
}
if expected != actual {
debug.Dump(stmt.Raw)
t.Errorf("- %s", expected)
t.Errorf("- %s", query)
t.Errorf("+ %s", actual)
t.Errorf("+ %s", out)
}
})
}
}
})
}
}