diff --git a/go/cmd/dolt/commands/sql.go b/go/cmd/dolt/commands/sql.go index 4a1101d562e..a09e986053c 100644 --- a/go/cmd/dolt/commands/sql.go +++ b/go/cmd/dolt/commands/sql.go @@ -1138,10 +1138,67 @@ func processQuery(ctx *sql.Context, query string, qryist cli.Queryist) (sql.Sche return processParsedQuery(ctx, query, qryist, sqlStatement) } +func cleanCommentedSelectExpressionNames(sqlStatement sqlparser.Statement) { + if sqlStatement == nil { + return + } + _ = sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + aliasedExpr, ok := node.(*sqlparser.AliasedExpr) + if ok && aliasedExpr.As.IsEmpty() && containsSQLComment(aliasedExpr.InputExpression) { + aliasedExpr.InputExpression = "" + } + return true, nil + }, sqlStatement) +} + +func containsSQLComment(s string) bool { + var quote byte + for i := 0; i < len(s); i++ { + c := s[i] + if quote != 0 { + if c == '\\' && quote != '`' { + i++ + continue + } + if c == quote { + quote = 0 + } + continue + } + + switch c { + case '\'', '"', '`': + quote = c + case '#': + return true + case '-': + if i+1 < len(s) && s[i+1] == '-' && (i+2 == len(s) || isSQLWhitespace(s[i+2])) { + return true + } + case '/': + if i+1 < len(s) && s[i+1] == '*' { + return true + } + } + } + return false +} + +func isSQLWhitespace(c byte) bool { + switch c { + case ' ', '\t', '\n', '\r', '\f', '\v': + return true + default: + return false + } +} + // processParsedQuery processes a single query with the parsed statement provided. The Root of the sqlEngine // will be updated if necessary. Returns the schema and the row iterator for the results, which may be nil, // and an error if one occurs. func processParsedQuery(ctx *sql.Context, query string, qryist cli.Queryist, sqlStatement sqlparser.Statement) (sql.Schema, sql.RowIter, *sql.QueryFlags, error) { + cleanCommentedSelectExpressionNames(sqlStatement) + switch s := sqlStatement.(type) { case *sqlparser.Use, *sqlparser.Commit: _, ri, _, err := qryist.Query(ctx, query) diff --git a/go/cmd/dolt/commands/sql_test.go b/go/cmd/dolt/commands/sql_test.go index f4a7aba6dce..6a5d928ecfa 100644 --- a/go/cmd/dolt/commands/sql_test.go +++ b/go/cmd/dolt/commands/sql_test.go @@ -19,6 +19,7 @@ import ( "fmt" "testing" + "github.com/dolthub/go-mysql-server/sql" _ "github.com/dolthub/go-mysql-server/sql/variables" "github.com/google/uuid" "github.com/stretchr/testify/assert" @@ -93,6 +94,54 @@ func TestSqlBatchMode(t *testing.T) { } } +func TestSqlLiteralColumnNamesIgnoreTrailingLineComments(t *testing.T) { + tests := []struct { + query string + expectedNames []string + expectedRows []sql.Row + }{ + { + query: "select null -- comment", + expectedNames: []string{"NULL"}, + expectedRows: []sql.Row{{nil}}, + }, + { + query: "select 1, -- one\n2 -- two", + expectedNames: []string{"1", "2"}, + expectedRows: []sql.Row{{int8(1), int8(2)}}, + }, + { + query: "select \"string\", -- string\n\"another string\" -- another string", + expectedNames: []string{"string", "another string"}, + expectedRows: []sql.Row{{"string", "another string"}}, + }, + } + + for _, test := range tests { + t.Run(test.query, func(t *testing.T) { + ctx := t.Context() + dEnv := DEnvWithSeedDataForTest(ctx, t) + cliCtx := NewArgFreeCliContextForTest(ctx, t, dEnv) + + queryEngine, err := cliCtx.QueryEngine(ctx) + require.NoError(t, err) + + sqlSch, rowIter, _, err := processQuery(queryEngine.Context, test.query, queryEngine.Queryist) + require.NoError(t, err) + require.NotNil(t, rowIter) + + rows, err := sql.RowIterToRows(queryEngine.Context, rowIter) + require.NoError(t, err) + require.Equal(t, test.expectedRows, rows) + + require.Len(t, sqlSch, len(test.expectedNames)) + for i, expectedName := range test.expectedNames { + require.Equal(t, expectedName, sqlSch[i].Name) + } + }) + } +} + // Smoke tests, values are printed to console func TestSqlSelect(t *testing.T) { tests := []struct {