snapshot of https://github.com/sqlc-dev/sqlc.git (main, 2026-09-16; blobs > 131072B omitted)
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
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
This commit is contained in:
507
internal/x/expander/expander.go
Normal file
507
internal/x/expander/expander.go
Normal file
@@ -0,0 +1,507 @@
|
||||
package expander
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/ast"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/astutils"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/format"
|
||||
)
|
||||
|
||||
// Parser is an interface for SQL parsers that can parse SQL into AST statements.
|
||||
type Parser interface {
|
||||
Parse(r io.Reader) ([]ast.Statement, error)
|
||||
}
|
||||
|
||||
// ColumnGetter retrieves column names for a query by preparing it against a database.
|
||||
type ColumnGetter interface {
|
||||
GetColumnNames(ctx context.Context, query string) ([]string, error)
|
||||
}
|
||||
|
||||
// Expander expands SELECT * and RETURNING * queries by replacing * with explicit column names
|
||||
// obtained from preparing the query against a database.
|
||||
type Expander struct {
|
||||
colGetter ColumnGetter
|
||||
parser Parser
|
||||
dialect format.Dialect
|
||||
}
|
||||
|
||||
// New creates a new Expander with the given column getter, parser, and dialect.
|
||||
func New(colGetter ColumnGetter, parser Parser, dialect format.Dialect) *Expander {
|
||||
return &Expander{
|
||||
colGetter: colGetter,
|
||||
parser: parser,
|
||||
dialect: dialect,
|
||||
}
|
||||
}
|
||||
|
||||
// Expand takes a SQL query, and if it contains * in SELECT or RETURNING clause,
|
||||
// expands it to use explicit column names. Returns the expanded query string.
|
||||
func (e *Expander) Expand(ctx context.Context, query string) (string, error) {
|
||||
// Parse the query
|
||||
stmts, err := e.parser.Parse(strings.NewReader(query))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse query: %w", err)
|
||||
}
|
||||
|
||||
if len(stmts) == 0 {
|
||||
return query, nil
|
||||
}
|
||||
|
||||
stmt := stmts[0].Raw.Stmt
|
||||
|
||||
// Check if there's any star in the statement (including CTEs, subqueries, etc.)
|
||||
if !hasStarAnywhere(stmt) {
|
||||
return query, nil
|
||||
}
|
||||
|
||||
// Expand all stars in the statement recursively
|
||||
if err := e.expandNode(ctx, stmt); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Format the modified AST back to SQL
|
||||
expanded := ast.Format(stmts[0].Raw, e.dialect)
|
||||
|
||||
return expanded, nil
|
||||
}
|
||||
|
||||
// expandNode recursively expands * in all parts of the statement
|
||||
func (e *Expander) expandNode(ctx context.Context, node ast.Node) error {
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch n := node.(type) {
|
||||
case *ast.SelectStmt:
|
||||
return e.expandSelectStmt(ctx, n)
|
||||
case *ast.InsertStmt:
|
||||
return e.expandInsertStmt(ctx, n)
|
||||
case *ast.UpdateStmt:
|
||||
return e.expandUpdateStmt(ctx, n)
|
||||
case *ast.DeleteStmt:
|
||||
return e.expandDeleteStmt(ctx, n)
|
||||
case *ast.CommonTableExpr:
|
||||
return e.expandNode(ctx, n.Ctequery)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// expandSelectStmt expands * in a SELECT statement including CTEs and subqueries
|
||||
func (e *Expander) expandSelectStmt(ctx context.Context, stmt *ast.SelectStmt) error {
|
||||
// First expand any CTEs - must be done in order since later CTEs may depend on earlier ones
|
||||
if stmt.WithClause != nil && stmt.WithClause.Ctes != nil {
|
||||
for _, cteNode := range stmt.WithClause.Ctes.Items {
|
||||
cte, ok := cteNode.(*ast.CommonTableExpr)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
cteSelect, ok := cte.Ctequery.(*ast.SelectStmt)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if hasStarInList(cteSelect.TargetList) {
|
||||
// Get column names for this CTE
|
||||
columns, err := e.getCTEColumnNames(ctx, stmt, cte)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cteSelect.TargetList = rewriteTargetList(cteSelect.TargetList, columns)
|
||||
}
|
||||
// Recursively handle nested CTEs/subqueries in this CTE
|
||||
if err := e.expandSelectStmtInner(ctx, cteSelect); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Expand subqueries in FROM clause
|
||||
if stmt.FromClause != nil {
|
||||
for _, fromItem := range stmt.FromClause.Items {
|
||||
if err := e.expandFromClause(ctx, fromItem); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Expand the target list if it has stars
|
||||
if hasStarInList(stmt.TargetList) {
|
||||
// Format the current state to get columns
|
||||
tempRaw := &ast.RawStmt{Stmt: stmt}
|
||||
tempQuery := ast.Format(tempRaw, e.dialect)
|
||||
columns, err := e.getColumnNames(ctx, tempQuery)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get column names: %w", err)
|
||||
}
|
||||
stmt.TargetList = rewriteTargetList(stmt.TargetList, columns)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// expandSelectStmtInner expands nested structures without re-processing the target list
|
||||
func (e *Expander) expandSelectStmtInner(ctx context.Context, stmt *ast.SelectStmt) error {
|
||||
// Expand subqueries in FROM clause
|
||||
if stmt.FromClause != nil {
|
||||
for _, fromItem := range stmt.FromClause.Items {
|
||||
if err := e.expandFromClause(ctx, fromItem); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getCTEColumnNames gets the column names for a CTE by constructing a query with proper context
|
||||
func (e *Expander) getCTEColumnNames(ctx context.Context, stmt *ast.SelectStmt, targetCTE *ast.CommonTableExpr) ([]string, error) {
|
||||
// Build a temporary query: WITH <all CTEs up to and including target> SELECT * FROM <targetCTE>
|
||||
var ctesToInclude []ast.Node
|
||||
for _, cteNode := range stmt.WithClause.Ctes.Items {
|
||||
ctesToInclude = append(ctesToInclude, cteNode)
|
||||
cte, ok := cteNode.(*ast.CommonTableExpr)
|
||||
if ok && cte.Ctename != nil && targetCTE.Ctename != nil && *cte.Ctename == *targetCTE.Ctename {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Create a SELECT * FROM <ctename> with the relevant CTEs
|
||||
cteName := ""
|
||||
if targetCTE.Ctename != nil {
|
||||
cteName = *targetCTE.Ctename
|
||||
}
|
||||
|
||||
tempStmt := &ast.SelectStmt{
|
||||
WithClause: &ast.WithClause{
|
||||
Ctes: &ast.List{Items: ctesToInclude},
|
||||
Recursive: stmt.WithClause.Recursive,
|
||||
},
|
||||
TargetList: &ast.List{
|
||||
Items: []ast.Node{
|
||||
&ast.ResTarget{
|
||||
Val: &ast.ColumnRef{
|
||||
Fields: &ast.List{
|
||||
Items: []ast.Node{&ast.A_Star{}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
FromClause: &ast.List{
|
||||
Items: []ast.Node{
|
||||
&ast.RangeVar{
|
||||
Relname: &cteName,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tempRaw := &ast.RawStmt{Stmt: tempStmt}
|
||||
tempQuery := ast.Format(tempRaw, e.dialect)
|
||||
|
||||
return e.getColumnNames(ctx, tempQuery)
|
||||
}
|
||||
|
||||
// expandInsertStmt expands * in an INSERT statement's RETURNING clause
|
||||
func (e *Expander) expandInsertStmt(ctx context.Context, stmt *ast.InsertStmt) error {
|
||||
// Expand CTEs first
|
||||
if stmt.WithClause != nil && stmt.WithClause.Ctes != nil {
|
||||
for _, cte := range stmt.WithClause.Ctes.Items {
|
||||
if err := e.expandNode(ctx, cte); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Expand the SELECT part if present
|
||||
if stmt.SelectStmt != nil {
|
||||
if err := e.expandNode(ctx, stmt.SelectStmt); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Expand RETURNING clause
|
||||
if hasStarInList(stmt.ReturningList) {
|
||||
tempRaw := &ast.RawStmt{Stmt: stmt}
|
||||
tempQuery := ast.Format(tempRaw, e.dialect)
|
||||
columns, err := e.getColumnNames(ctx, tempQuery)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get column names: %w", err)
|
||||
}
|
||||
stmt.ReturningList = rewriteTargetList(stmt.ReturningList, columns)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// expandUpdateStmt expands * in an UPDATE statement's RETURNING clause
|
||||
func (e *Expander) expandUpdateStmt(ctx context.Context, stmt *ast.UpdateStmt) error {
|
||||
// Expand CTEs first
|
||||
if stmt.WithClause != nil && stmt.WithClause.Ctes != nil {
|
||||
for _, cte := range stmt.WithClause.Ctes.Items {
|
||||
if err := e.expandNode(ctx, cte); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Expand RETURNING clause
|
||||
if hasStarInList(stmt.ReturningList) {
|
||||
tempRaw := &ast.RawStmt{Stmt: stmt}
|
||||
tempQuery := ast.Format(tempRaw, e.dialect)
|
||||
columns, err := e.getColumnNames(ctx, tempQuery)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get column names: %w", err)
|
||||
}
|
||||
stmt.ReturningList = rewriteTargetList(stmt.ReturningList, columns)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// expandDeleteStmt expands * in a DELETE statement's RETURNING clause
|
||||
func (e *Expander) expandDeleteStmt(ctx context.Context, stmt *ast.DeleteStmt) error {
|
||||
// Expand CTEs first
|
||||
if stmt.WithClause != nil && stmt.WithClause.Ctes != nil {
|
||||
for _, cte := range stmt.WithClause.Ctes.Items {
|
||||
if err := e.expandNode(ctx, cte); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Expand RETURNING clause
|
||||
if hasStarInList(stmt.ReturningList) {
|
||||
tempRaw := &ast.RawStmt{Stmt: stmt}
|
||||
tempQuery := ast.Format(tempRaw, e.dialect)
|
||||
columns, err := e.getColumnNames(ctx, tempQuery)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get column names: %w", err)
|
||||
}
|
||||
stmt.ReturningList = rewriteTargetList(stmt.ReturningList, columns)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// expandFromClause expands * in subqueries within FROM clause
|
||||
func (e *Expander) expandFromClause(ctx context.Context, node ast.Node) error {
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch n := node.(type) {
|
||||
case *ast.RangeSubselect:
|
||||
if n.Subquery != nil {
|
||||
return e.expandNode(ctx, n.Subquery)
|
||||
}
|
||||
case *ast.JoinExpr:
|
||||
if err := e.expandFromClause(ctx, n.Larg); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := e.expandFromClause(ctx, n.Rarg); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// hasStarAnywhere checks if there's a * anywhere in the statement using astutils.Search
|
||||
func hasStarAnywhere(node ast.Node) bool {
|
||||
if node == nil {
|
||||
return false
|
||||
}
|
||||
// Use astutils.Search to find any A_Star node in the AST
|
||||
stars := astutils.Search(node, func(n ast.Node) bool {
|
||||
_, ok := n.(*ast.A_Star)
|
||||
return ok
|
||||
})
|
||||
return len(stars.Items) > 0
|
||||
}
|
||||
|
||||
// hasStarInList checks if a target list contains a * expression using astutils.Search
|
||||
func hasStarInList(targets *ast.List) bool {
|
||||
if targets == nil {
|
||||
return false
|
||||
}
|
||||
// Use astutils.Search to find any A_Star node in the target list
|
||||
stars := astutils.Search(targets, func(n ast.Node) bool {
|
||||
_, ok := n.(*ast.A_Star)
|
||||
return ok
|
||||
})
|
||||
return len(stars.Items) > 0
|
||||
}
|
||||
|
||||
// getColumnNames prepares the query and returns the column names from the result
|
||||
func (e *Expander) getColumnNames(ctx context.Context, query string) ([]string, error) {
|
||||
return e.colGetter.GetColumnNames(ctx, query)
|
||||
}
|
||||
|
||||
// countStarsInList counts the number of * expressions in a target list
|
||||
func countStarsInList(targets *ast.List) int {
|
||||
if targets == nil {
|
||||
return 0
|
||||
}
|
||||
count := 0
|
||||
for _, target := range targets.Items {
|
||||
resTarget, ok := target.(*ast.ResTarget)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if resTarget.Val == nil {
|
||||
continue
|
||||
}
|
||||
colRef, ok := resTarget.Val.(*ast.ColumnRef)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if colRef.Fields == nil {
|
||||
continue
|
||||
}
|
||||
for _, field := range colRef.Fields.Items {
|
||||
if _, ok := field.(*ast.A_Star); ok {
|
||||
count++
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// countNonStarsInList counts the number of non-* expressions in a target list
|
||||
func countNonStarsInList(targets *ast.List) int {
|
||||
if targets == nil {
|
||||
return 0
|
||||
}
|
||||
count := 0
|
||||
for _, target := range targets.Items {
|
||||
resTarget, ok := target.(*ast.ResTarget)
|
||||
if !ok {
|
||||
count++
|
||||
continue
|
||||
}
|
||||
if resTarget.Val == nil {
|
||||
count++
|
||||
continue
|
||||
}
|
||||
colRef, ok := resTarget.Val.(*ast.ColumnRef)
|
||||
if !ok {
|
||||
count++
|
||||
continue
|
||||
}
|
||||
if colRef.Fields == nil {
|
||||
count++
|
||||
continue
|
||||
}
|
||||
isStar := false
|
||||
for _, field := range colRef.Fields.Items {
|
||||
if _, ok := field.(*ast.A_Star); ok {
|
||||
isStar = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isStar {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// rewriteTargetList replaces * in a target list with explicit column references
|
||||
func rewriteTargetList(targets *ast.List, columns []string) *ast.List {
|
||||
if targets == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
starCount := countStarsInList(targets)
|
||||
nonStarCount := countNonStarsInList(targets)
|
||||
|
||||
// Calculate how many columns each * expands to
|
||||
// Total columns = (columns per star * number of stars) + non-star columns
|
||||
// So: columns per star = (total - non-star) / stars
|
||||
columnsPerStar := 0
|
||||
if starCount > 0 {
|
||||
columnsPerStar = (len(columns) - nonStarCount) / starCount
|
||||
}
|
||||
|
||||
newItems := make([]ast.Node, 0, len(columns))
|
||||
colIndex := 0
|
||||
|
||||
for _, target := range targets.Items {
|
||||
resTarget, ok := target.(*ast.ResTarget)
|
||||
if !ok {
|
||||
newItems = append(newItems, target)
|
||||
colIndex++
|
||||
continue
|
||||
}
|
||||
|
||||
if resTarget.Val == nil {
|
||||
newItems = append(newItems, target)
|
||||
colIndex++
|
||||
continue
|
||||
}
|
||||
|
||||
colRef, ok := resTarget.Val.(*ast.ColumnRef)
|
||||
if !ok {
|
||||
newItems = append(newItems, target)
|
||||
colIndex++
|
||||
continue
|
||||
}
|
||||
|
||||
if colRef.Fields == nil {
|
||||
newItems = append(newItems, target)
|
||||
colIndex++
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if this is a * (with or without table qualifier)
|
||||
// and extract any table prefix
|
||||
isStar := false
|
||||
var tablePrefix []string
|
||||
for _, field := range colRef.Fields.Items {
|
||||
if _, ok := field.(*ast.A_Star); ok {
|
||||
isStar = true
|
||||
break
|
||||
}
|
||||
// Collect prefix parts (schema, table name)
|
||||
if str, ok := field.(*ast.String); ok {
|
||||
tablePrefix = append(tablePrefix, str.Str)
|
||||
}
|
||||
}
|
||||
|
||||
if !isStar {
|
||||
newItems = append(newItems, target)
|
||||
colIndex++
|
||||
continue
|
||||
}
|
||||
|
||||
// Replace * with explicit column references
|
||||
for i := 0; i < columnsPerStar && colIndex < len(columns); i++ {
|
||||
newItems = append(newItems, makeColumnTargetWithPrefix(columns[colIndex], tablePrefix))
|
||||
colIndex++
|
||||
}
|
||||
}
|
||||
|
||||
return &ast.List{Items: newItems}
|
||||
}
|
||||
|
||||
// makeColumnTargetWithPrefix creates a ResTarget node for a column reference with optional table prefix
|
||||
func makeColumnTargetWithPrefix(colName string, prefix []string) ast.Node {
|
||||
fields := make([]ast.Node, 0, len(prefix)+1)
|
||||
|
||||
// Add prefix parts (schema, table name)
|
||||
for _, p := range prefix {
|
||||
fields = append(fields, &ast.String{Str: p})
|
||||
}
|
||||
|
||||
// Add column name
|
||||
fields = append(fields, &ast.String{Str: colName})
|
||||
|
||||
return &ast.ResTarget{
|
||||
Val: &ast.ColumnRef{
|
||||
Fields: &ast.List{Items: fields},
|
||||
},
|
||||
}
|
||||
}
|
||||
435
internal/x/expander/expander_test.go
Normal file
435
internal/x/expander/expander_test.go
Normal file
@@ -0,0 +1,435 @@
|
||||
package expander
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
|
||||
"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/sqltest/docker"
|
||||
"github.com/sqlc-dev/sqlc/internal/sqltest/native"
|
||||
)
|
||||
|
||||
// PostgreSQLColumnGetter implements ColumnGetter for PostgreSQL using pgxpool.
|
||||
type PostgreSQLColumnGetter struct {
|
||||
pool *pgxpool.Pool
|
||||
}
|
||||
|
||||
func (g *PostgreSQLColumnGetter) GetColumnNames(ctx context.Context, query string) ([]string, error) {
|
||||
conn, err := g.pool.Acquire(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Release()
|
||||
|
||||
desc, err := conn.Conn().Prepare(ctx, "", query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
columns := make([]string, len(desc.Fields))
|
||||
for i, field := range desc.Fields {
|
||||
columns[i] = field.Name
|
||||
}
|
||||
|
||||
return columns, nil
|
||||
}
|
||||
|
||||
// MySQLColumnGetter implements ColumnGetter for MySQL. Column names are read
|
||||
// from the result set metadata returned by executing the query; the test
|
||||
// tables are empty, so no real rows are transferred.
|
||||
//
|
||||
// An earlier implementation pulled column metadata straight out of a prepared
|
||||
// statement via a forked mysql driver exposing StmtMetadata. That fork
|
||||
// required a `replace` directive in go.mod, which broke `go install
|
||||
// github.com/sqlc-dev/sqlc/cmd/sqlc@latest` (see
|
||||
// https://github.com/sqlc-dev/sqlc/issues/4397). Reading columns from sql.Rows
|
||||
// works with the upstream driver and keeps the test covering the same
|
||||
// behavior.
|
||||
type MySQLColumnGetter struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func (g *MySQLColumnGetter) GetColumnNames(ctx context.Context, query string) ([]string, error) {
|
||||
rows, err := g.db.QueryContext(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
return rows.Columns()
|
||||
}
|
||||
|
||||
// SQLiteColumnGetter implements ColumnGetter for SQLite using the native ncruces/go-sqlite3 API.
|
||||
type SQLiteColumnGetter struct {
|
||||
conn *sqlite3.Conn
|
||||
}
|
||||
|
||||
func (g *SQLiteColumnGetter) GetColumnNames(ctx context.Context, query string) ([]string, error) {
|
||||
// Prepare the statement - this gives us column metadata without executing
|
||||
stmt, _, err := g.conn.Prepare(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
// Get column names from the prepared statement
|
||||
count := stmt.ColumnCount()
|
||||
columns := make([]string, count)
|
||||
for i := range count {
|
||||
columns[i] = stmt.ColumnName(i)
|
||||
}
|
||||
|
||||
return columns, nil
|
||||
}
|
||||
|
||||
func TestExpandPostgreSQL(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
uri := os.Getenv("POSTGRESQL_SERVER_URI")
|
||||
if uri == "" {
|
||||
if err := docker.Installed(); err == nil {
|
||||
u, err := docker.StartPostgreSQLServer(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
uri = u
|
||||
} else if err := native.Supported(); err == nil {
|
||||
u, err := native.StartPostgreSQLServer(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
uri = u
|
||||
} else {
|
||||
t.Skip("POSTGRESQL_SERVER_URI is empty and neither Docker nor native installation is available")
|
||||
}
|
||||
}
|
||||
|
||||
pool, err := pgxpool.New(ctx, uri)
|
||||
if err != nil {
|
||||
t.Skipf("could not connect to database: %v", err)
|
||||
}
|
||||
defer pool.Close()
|
||||
|
||||
// Create a test table
|
||||
_, err = pool.Exec(ctx, `
|
||||
DROP TABLE IF EXISTS authors;
|
||||
CREATE TABLE authors (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
bio TEXT
|
||||
);
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create test table: %v", err)
|
||||
}
|
||||
defer pool.Exec(ctx, "DROP TABLE IF EXISTS authors")
|
||||
|
||||
// Create the parser which also implements format.Dialect
|
||||
parser := postgresql.NewParser()
|
||||
|
||||
// Create the expander
|
||||
colGetter := &PostgreSQLColumnGetter{pool: pool}
|
||||
exp := New(colGetter, parser, parser)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
query string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "simple select star",
|
||||
query: "SELECT * FROM authors",
|
||||
expected: "SELECT id, name, bio FROM authors;",
|
||||
},
|
||||
{
|
||||
name: "select with no star",
|
||||
query: "SELECT id, name FROM authors",
|
||||
expected: "SELECT id, name FROM authors", // No change, returns original
|
||||
},
|
||||
{
|
||||
name: "select star with where clause",
|
||||
query: "SELECT * FROM authors WHERE id = 1",
|
||||
expected: "SELECT id, name, bio FROM authors WHERE id = 1;",
|
||||
},
|
||||
{
|
||||
name: "double star",
|
||||
query: "SELECT *, * FROM authors",
|
||||
expected: "SELECT id, name, bio, id, name, bio FROM authors;",
|
||||
},
|
||||
{
|
||||
name: "table qualified star",
|
||||
query: "SELECT authors.* FROM authors",
|
||||
expected: "SELECT authors.id, authors.name, authors.bio FROM authors;",
|
||||
},
|
||||
{
|
||||
name: "star in middle of columns",
|
||||
query: "SELECT id, *, name FROM authors",
|
||||
expected: "SELECT id, id, name, bio, name FROM authors;",
|
||||
},
|
||||
{
|
||||
name: "insert returning star",
|
||||
query: "INSERT INTO authors (name, bio) VALUES ('John', 'A writer') RETURNING *",
|
||||
expected: "INSERT INTO authors (name, bio) VALUES ('John', 'A writer') RETURNING id, name, bio;",
|
||||
},
|
||||
{
|
||||
name: "insert returning mixed",
|
||||
query: "INSERT INTO authors (name, bio) VALUES ('John', 'A writer') RETURNING id, *",
|
||||
expected: "INSERT INTO authors (name, bio) VALUES ('John', 'A writer') RETURNING id, id, name, bio;",
|
||||
},
|
||||
{
|
||||
name: "update returning star",
|
||||
query: "UPDATE authors SET name = 'Jane' WHERE id = 1 RETURNING *",
|
||||
expected: "UPDATE authors SET name = 'Jane' WHERE id = 1 RETURNING id, name, bio;",
|
||||
},
|
||||
{
|
||||
name: "delete returning star",
|
||||
query: "DELETE FROM authors WHERE id = 1 RETURNING *",
|
||||
expected: "DELETE FROM authors WHERE id = 1 RETURNING id, name, bio;",
|
||||
},
|
||||
{
|
||||
name: "cte with select star",
|
||||
query: "WITH a AS (SELECT * FROM authors) SELECT * FROM a",
|
||||
expected: "WITH a AS (SELECT id, name, bio FROM authors) SELECT id, name, bio FROM a;",
|
||||
},
|
||||
{
|
||||
name: "multiple ctes with dependency",
|
||||
query: "WITH a AS (SELECT * FROM authors), b AS (SELECT * FROM a) SELECT * FROM b",
|
||||
expected: "WITH a AS (SELECT id, name, bio FROM authors), b AS (SELECT id, name, bio FROM a) SELECT id, name, bio FROM b;",
|
||||
},
|
||||
{
|
||||
name: "count star not expanded",
|
||||
query: "SELECT COUNT(*) FROM authors",
|
||||
expected: "SELECT COUNT(*) FROM authors", // No change - COUNT(*) should not be expanded
|
||||
},
|
||||
{
|
||||
name: "count star with other columns",
|
||||
query: "SELECT COUNT(*), name FROM authors GROUP BY name",
|
||||
expected: "SELECT COUNT(*), name FROM authors GROUP BY name", // No change
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result, err := exp.Expand(ctx, tc.query)
|
||||
if err != nil {
|
||||
t.Fatalf("Expand failed: %v", err)
|
||||
}
|
||||
if result != tc.expected {
|
||||
t.Errorf("expected %q, got %q", tc.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpandMySQL(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
source := os.Getenv("MYSQL_SERVER_URI")
|
||||
if source == "" {
|
||||
if err := docker.Installed(); err == nil {
|
||||
u, err := docker.StartMySQLServer(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
source = u
|
||||
} else if err := native.Supported(); err == nil {
|
||||
u, err := native.StartMySQLServer(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
source = u
|
||||
} else {
|
||||
t.Skip("MYSQL_SERVER_URI is empty and neither Docker nor native installation is available")
|
||||
}
|
||||
}
|
||||
|
||||
db, err := sql.Open("mysql", source)
|
||||
if err != nil {
|
||||
t.Skipf("could not connect to MySQL: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Verify connection
|
||||
if err := db.Ping(); err != nil {
|
||||
t.Skipf("could not ping MySQL: %v", err)
|
||||
}
|
||||
|
||||
// Create a test table
|
||||
_, err = db.ExecContext(ctx, `DROP TABLE IF EXISTS authors`)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to drop test table: %v", err)
|
||||
}
|
||||
_, err = db.ExecContext(ctx, `
|
||||
CREATE TABLE authors (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
bio TEXT
|
||||
)
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create test table: %v", err)
|
||||
}
|
||||
defer db.ExecContext(ctx, "DROP TABLE IF EXISTS authors")
|
||||
|
||||
// Create the parser which also implements format.Dialect
|
||||
parser := dolphin.NewParser()
|
||||
|
||||
// Create the expander
|
||||
colGetter := &MySQLColumnGetter{db: db}
|
||||
exp := New(colGetter, parser, parser)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
query string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "simple select star",
|
||||
query: "SELECT * FROM authors",
|
||||
expected: "SELECT id, name, bio FROM authors;",
|
||||
},
|
||||
{
|
||||
name: "select with no star",
|
||||
query: "SELECT id, name FROM authors",
|
||||
expected: "SELECT id, name FROM authors", // No change, returns original
|
||||
},
|
||||
{
|
||||
name: "select star with where clause",
|
||||
query: "SELECT * FROM authors WHERE id = 1",
|
||||
expected: "SELECT id, name, bio FROM authors WHERE id = 1;",
|
||||
},
|
||||
{
|
||||
name: "table qualified star",
|
||||
query: "SELECT authors.* FROM authors",
|
||||
expected: "SELECT authors.id, authors.name, authors.bio FROM authors;",
|
||||
},
|
||||
{
|
||||
name: "double table qualified star",
|
||||
query: "SELECT authors.*, authors.* FROM authors",
|
||||
expected: "SELECT authors.id, authors.name, authors.bio, authors.id, authors.name, authors.bio FROM authors;",
|
||||
},
|
||||
{
|
||||
name: "star in middle of columns table qualified",
|
||||
query: "SELECT id, authors.*, name FROM authors",
|
||||
expected: "SELECT id, authors.id, authors.name, authors.bio, name FROM authors;",
|
||||
},
|
||||
{
|
||||
name: "count star not expanded",
|
||||
query: "SELECT COUNT(*) FROM authors",
|
||||
expected: "SELECT COUNT(*) FROM authors", // No change - COUNT(*) should not be expanded
|
||||
},
|
||||
{
|
||||
name: "count star with other columns",
|
||||
query: "SELECT COUNT(*), name FROM authors GROUP BY name",
|
||||
expected: "SELECT COUNT(*), name FROM authors GROUP BY name", // No change
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result, err := exp.Expand(ctx, tc.query)
|
||||
if err != nil {
|
||||
t.Fatalf("Expand failed: %v", err)
|
||||
}
|
||||
if result != tc.expected {
|
||||
t.Errorf("expected %q, got %q", tc.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpandSQLite(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Create an in-memory SQLite database using native API
|
||||
conn, err := sqlite3.Open(":memory:")
|
||||
if err != nil {
|
||||
t.Fatalf("could not open SQLite: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Create a test table
|
||||
err = conn.Exec(`
|
||||
CREATE TABLE authors (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
bio TEXT
|
||||
)
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create test table: %v", err)
|
||||
}
|
||||
|
||||
// Create the parser which also implements format.Dialect
|
||||
parser := sqlite.NewParser()
|
||||
|
||||
// Create the expander using native SQLite column getter
|
||||
colGetter := &SQLiteColumnGetter{conn: conn}
|
||||
exp := New(colGetter, parser, parser)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
query string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "simple select star",
|
||||
query: "SELECT * FROM authors",
|
||||
expected: "SELECT id, name, bio FROM authors;",
|
||||
},
|
||||
{
|
||||
name: "select with no star",
|
||||
query: "SELECT id, name FROM authors",
|
||||
expected: "SELECT id, name FROM authors", // No change, returns original
|
||||
},
|
||||
{
|
||||
name: "select star with where clause",
|
||||
query: "SELECT * FROM authors WHERE id = 1",
|
||||
expected: "SELECT id, name, bio FROM authors WHERE id = 1;",
|
||||
},
|
||||
{
|
||||
name: "double star",
|
||||
query: "SELECT *, * FROM authors",
|
||||
expected: "SELECT id, name, bio, id, name, bio FROM authors;",
|
||||
},
|
||||
{
|
||||
name: "table qualified star",
|
||||
query: "SELECT authors.* FROM authors",
|
||||
expected: "SELECT authors.id, authors.name, authors.bio FROM authors;",
|
||||
},
|
||||
{
|
||||
name: "star in middle of columns",
|
||||
query: "SELECT id, *, name FROM authors",
|
||||
expected: "SELECT id, id, name, bio, name FROM authors;",
|
||||
},
|
||||
{
|
||||
name: "count star not expanded",
|
||||
query: "SELECT COUNT(*) FROM authors",
|
||||
expected: "SELECT COUNT(*) FROM authors", // No change - COUNT(*) should not be expanded
|
||||
},
|
||||
{
|
||||
name: "count star with other columns",
|
||||
query: "SELECT COUNT(*), name FROM authors GROUP BY name",
|
||||
expected: "SELECT COUNT(*), name FROM authors GROUP BY name", // No change
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result, err := exp.Expand(ctx, tc.query)
|
||||
if err != nil {
|
||||
t.Fatalf("Expand failed: %v", err)
|
||||
}
|
||||
if result != tc.expected {
|
||||
t.Errorf("expected %q, got %q", tc.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user