Skip to content

Commit 6eea6c6

Browse files
committed
Fixed multi-array unnest
1 parent b8122e9 commit 6eea6c6

13 files changed

Lines changed: 353 additions & 39 deletions

File tree

‎go.mod‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ require (
66
github.com/PuerkitoBio/goquery v1.8.1
77
github.com/cockroachdb/apd/v3 v3.2.3
88
github.com/cockroachdb/errors v1.7.5
9-
github.com/dolthub/dolt/go v0.40.5-0.20260917172656-9212dcefafe7
9+
github.com/dolthub/dolt/go v0.40.5-0.20260921094718-4236b610a39e
1010
github.com/dolthub/eventsapi_schema v0.0.0-20260715220557-d9b4a1c6b4d4
1111
github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2
12-
github.com/dolthub/go-mysql-server v0.20.1-0.20260917160545-8ae5232535f2
12+
github.com/dolthub/go-mysql-server v0.20.1-0.20260921092145-13a83f1e6133
1313
github.com/dolthub/pg_query_go/v6 v6.0.0-20251215122834-fb20be4254d1
1414
github.com/dolthub/sqllogictest/go v0.0.0-20260624223518-788480b24166
1515
github.com/dolthub/vitess v0.0.0-20260916192104-15c5c4158b37
@@ -214,7 +214,7 @@ require (
214214
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
215215
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
216216
google.golang.org/grpc v1.83.1 // indirect
217-
google.golang.org/protobuf v1.36.11 // indirect
217+
google.golang.org/protobuf v1.36.12 // indirect
218218
gopkg.in/go-jose/go-jose.v2 v2.6.3 // indirect
219219
gopkg.in/yaml.v3 v3.0.1 // indirect
220220
)

‎go.sum‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -246,8 +246,8 @@ github.com/dolthub/aws-sdk-go-ini-parser v0.0.0-20250305001723-2821c37f6c12 h1:I
246246
github.com/dolthub/aws-sdk-go-ini-parser v0.0.0-20250305001723-2821c37f6c12/go.mod h1:rN7X8BHwkjPcfMQQ2QTAq/xM3leUSGLfb+1Js7Y6TVo=
247247
github.com/dolthub/dolt-mcp v0.3.4 h1:AyG5cw+fNWXDHXujtQnqUPZrpWtPg6FN6yYtjv1pP44=
248248
github.com/dolthub/dolt-mcp v0.3.4/go.mod h1:bCZ7KHvDYs+M0e+ySgmGiNvLhcwsN7bbf5YCyillLrk=
249-
github.com/dolthub/dolt/go v0.40.5-0.20260917172656-9212dcefafe7 h1:7/5tP17CPxBGJNmT7vbH51LpT2N5MACj/ik3+BeycmQ=
250-
github.com/dolthub/dolt/go v0.40.5-0.20260917172656-9212dcefafe7/go.mod h1:hvfdKQVmUz0aulQqTwNNlib5I6BWa4cjkO6NLyhYQSg=
249+
github.com/dolthub/dolt/go v0.40.5-0.20260921094718-4236b610a39e h1:p+WuCPyOB4p1mLkVpJXTV5+neNFAORpkIUkhU8OAWLE=
250+
github.com/dolthub/dolt/go v0.40.5-0.20260921094718-4236b610a39e/go.mod h1:EtuL3GgkVaQVjQd2EK0hSBKLZSm8VsFWC/5RxuSZf7w=
251251
github.com/dolthub/eventsapi_schema v0.0.0-20260715220557-d9b4a1c6b4d4 h1:0mg9QEFdkkBwJMxvz1tCjHYmfG2iIC6aShj1InDq9/M=
252252
github.com/dolthub/eventsapi_schema v0.0.0-20260715220557-d9b4a1c6b4d4/go.mod h1:SSLraQS/jGLYFgff3vuZ+JbVUct6vyEeMzjLBqWqoyM=
253253
github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2 h1:u3PMzfF8RkKd3lB9pZ2bfn0qEG+1Gms9599cr0REMww=
@@ -256,8 +256,8 @@ github.com/dolthub/fslock v0.0.5 h1:QoXhBhgY1oumHE26qyE7tgmXUT8qjJwxsIzo54O/B/k=
256256
github.com/dolthub/fslock v0.0.5/go.mod h1:sdofYYqE0D79zNZyB4/kmlnsQOVap1C2yByjGKSirEM=
257257
github.com/dolthub/go-icu-regex v0.0.0-20260610153742-72563bc7ca83 h1:FEMjCGEroDnY/BXyAffVZxUpXhP2GpoUJyyq5KaLn8c=
258258
github.com/dolthub/go-icu-regex v0.0.0-20260610153742-72563bc7ca83/go.mod h1:F3cnm+vMRK1HaU6+rNqQrOCyR03HHhR1GWG2gnPOqaE=
259-
github.com/dolthub/go-mysql-server v0.20.1-0.20260917160545-8ae5232535f2 h1:2zlM4Bl0At3R6uyaJHOsbKAQ8Hj/sOrCPw8rV2bMD3Q=
260-
github.com/dolthub/go-mysql-server v0.20.1-0.20260917160545-8ae5232535f2/go.mod h1:Qnp0PJNtR8JPjkmRDMShYh7Fy9jiSYLFgfQCe6Gmnxg=
259+
github.com/dolthub/go-mysql-server v0.20.1-0.20260921092145-13a83f1e6133 h1:xeGXQevXNBRMXdp8u7GfGbFZ5n6+WaaNMlkFj6zjsEI=
260+
github.com/dolthub/go-mysql-server v0.20.1-0.20260921092145-13a83f1e6133/go.mod h1:Qnp0PJNtR8JPjkmRDMShYh7Fy9jiSYLFgfQCe6Gmnxg=
261261
github.com/dolthub/gozstd v0.0.0-20240423170813-23a2903bca63 h1:OAsXLAPL4du6tfbBgK0xXHZkOlos63RdKYS3Sgw/dfI=
262262
github.com/dolthub/gozstd v0.0.0-20240423170813-23a2903bca63/go.mod h1:lV7lUeuDhH5thVGDCKXbatwKy2KW80L4rMT46n+Y2/Q=
263263
github.com/dolthub/ishell v0.0.0-20260414231531-5f031e3e9037 h1:oIW9HwuWrhxv+4HZxA+QQSKHLqWFyXZ2FmNjUYwkdiM=
@@ -1188,8 +1188,8 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi
11881188
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
11891189
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
11901190
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
1191-
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
1192-
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
1191+
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
1192+
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
11931193
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
11941194
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
11951195
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

‎server/analyzer/assign_insert_casts.go‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,10 @@ func AssignInsertCasts(ctx *sql.Context, a *analyzer.Analyzer, node sql.Node, sc
9292
insertInto = insertInto.WithSource(plan.NewValues(newValues))
9393
} else {
9494
sourceSchema := insertInto.Source.Schema(ctx)
95+
var sourceExprs []sql.Expression
96+
if projector, ok := insertInto.Source.(sql.Projector); ok {
97+
sourceExprs = projector.ProjectedExprs()
98+
}
9599
projections := make([]sql.Expression, len(sourceSchema))
96100
for i, col := range sourceSchema {
97101
colType := col.Type
@@ -103,7 +107,12 @@ func AssignInsertCasts(ctx *sql.Context, a *analyzer.Analyzer, node sql.Node, sc
103107
return nil, transform.NewTree, errors.Errorf("INSERT: non-Doltgres type found in source: %s", colType.String())
104108
}
105109
toColType := destinationTypes[i]
106-
getField := expression.NewGetField(i, fromColType, col.Name, true)
110+
var getField sql.Expression = expression.NewGetField(i, fromColType, col.Name, true)
111+
if i < len(sourceExprs) {
112+
if idExpr, ok := sourceExprs[i].(sql.IdExpression); ok {
113+
getField = getField.(sql.IdExpression).WithId(idExpr.Id())
114+
}
115+
}
107116
// We only assign the GetField if the types perfectly match (same parameters), otherwise we'll cast
108117
if fromColType.Equals(toColType) {
109118
projections[i] = getField

‎server/ast/aliased_table_expr.go‎

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -104,20 +104,20 @@ func nodeAliasedTableExpr(ctx *Context, node *tree.AliasedTableExpr) (*vitess.Al
104104
}
105105
aliasExpr = subquery
106106
case *tree.RowsFromExpr:
107-
var selectStmt vitess.SelectStatement
107+
tableExpr, err := nodeTableExpr(ctx, expr)
108+
if err != nil {
109+
return nil, err
110+
}
111+
112+
// TODO: this should be represented as a table function more directly
113+
var selectStmt vitess.SelectStatement = &vitess.Select{
114+
From: vitess.TableExprs{tableExpr},
115+
}
108116
if node.Ordinality {
109117
// WITH ORDINALITY appends a bigint column numbering the function's result rows, named
110118
// "ordinality" unless renamed by a column alias list. The numbering projection has to
111-
// live one level above the function's expansion, so we expand the function in the
112-
// select list of a wrapped subquery.
113-
items, err := nodeExprs(ctx, expr.Items)
114-
if err != nil {
115-
return nil, err
116-
}
117-
innerExprs := make(vitess.SelectExprs, len(items))
118-
for i := range items {
119-
innerExprs[i] = &vitess.AliasedExpr{Expr: items[i]}
120-
}
119+
// live one level above the function's expansion, so we keep the function in table
120+
// position within a wrapped subquery.
121121
selectStmt = &vitess.Select{
122122
SelectExprs: vitess.SelectExprs{
123123
&vitess.StarExpr{},
@@ -130,22 +130,9 @@ func nodeAliasedTableExpr(ctx *Context, node *tree.AliasedTableExpr) (*vitess.Al
130130
},
131131
},
132132
From: vitess.TableExprs{
133-
&vitess.AliasedTableExpr{
134-
Expr: &vitess.Subquery{Select: &vitess.Select{SelectExprs: innerExprs}},
135-
As: vitess.NewTableIdent("with_ordinality"),
136-
},
133+
rewriteTableFuncExprs(&vitess.AliasedTableExpr{Expr: &vitess.Subquery{Select: selectStmt}}),
137134
},
138135
}
139-
} else {
140-
tableExpr, err := nodeTableExpr(ctx, expr)
141-
if err != nil {
142-
return nil, err
143-
}
144-
145-
// TODO: this should be represented as a table function more directly
146-
selectStmt = &vitess.Select{
147-
From: vitess.TableExprs{tableExpr},
148-
}
149136
}
150137
subquery := &vitess.Subquery{
151138
Select: selectStmt,

‎server/ast/func_expr.go‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,9 @@ func nodeFuncExpr(ctx *Context, node *tree.FuncExpr) (vitess.Expr, error) {
138138
if len(node.OrderBy) > 0 {
139139
return nil, errors.Errorf("function ORDER BY is not yet supported")
140140
}
141+
if !strings.EqualFold(name.String(), "count") {
142+
exprs = dropStarArgument(exprs)
143+
}
141144

142145
return &vitess.FuncExpr{
143146
Qualifier: qualifier,
@@ -152,3 +155,16 @@ func nodeFuncExpr(ctx *Context, node *tree.FuncExpr) (vitess.Expr, error) {
152155
},
153156
}, nil
154157
}
158+
159+
// dropStarArgument removes a lone unqualified `*` argument, which PostgreSQL parses as a call with no arguments. Only
160+
// count has a form that accepts it, so every other function resolves as though it were given none. A qualified star
161+
// such as `t.*` names the whole row and is left alone.
162+
func dropStarArgument(exprs vitess.SelectExprs) vitess.SelectExprs {
163+
if len(exprs) != 1 {
164+
return exprs
165+
}
166+
if starExpr, ok := exprs[0].(*vitess.StarExpr); !ok || !starExpr.TableName.IsEmpty() {
167+
return exprs
168+
}
169+
return nil
170+
}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
// Copyright 2026 Dolthub, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package functions
16+
17+
import (
18+
"fmt"
19+
"io"
20+
"strings"
21+
22+
"github.com/cockroachdb/errors"
23+
"github.com/dolthub/go-mysql-server/sql"
24+
dtablefunctions "github.com/dolthub/go-mysql-server/sql/expression/tablefunction"
25+
26+
"github.com/dolthub/doltgresql/server/functions/framework"
27+
pgtypes "github.com/dolthub/doltgresql/server/types"
28+
)
29+
30+
// UnnestTableFunction is the FROM-clause form of unnest, returning one column per array and padding shorter arrays
31+
// with NULL.
32+
type UnnestTableFunction struct {
33+
database sql.Database
34+
arrays []sql.Expression
35+
}
36+
37+
var _ sql.TableFunction = (*UnnestTableFunction)(nil)
38+
var _ sql.ExecSourceRel = (*UnnestTableFunction)(nil)
39+
40+
// NewInstance implements the interface sql.TableFunction.
41+
func (u *UnnestTableFunction) NewInstance(ctx *sql.Context, database sql.Database, args []sql.Expression) (sql.Node, error) {
42+
if len(args) == 1 {
43+
unnest := sql.FunctionN{Name: u.Name(), Fn: func(ctx *sql.Context, args ...sql.Expression) (sql.Expression, error) {
44+
compiledFunction, _, err := framework.GetFunction(ctx, u.Name(), args...)
45+
return compiledFunction, err
46+
}}
47+
return dtablefunctions.NewTableFunctionWrapper(unnest).NewInstance(ctx, database, args)
48+
}
49+
return (&UnnestTableFunction{database: database}).WithExpressions(ctx, args...)
50+
}
51+
52+
// Name implements the interface sql.TableFunction.
53+
func (u *UnnestTableFunction) Name() string {
54+
return "unnest"
55+
}
56+
57+
// Database implements the interface sql.Databaser.
58+
func (u *UnnestTableFunction) Database() sql.Database {
59+
return u.database
60+
}
61+
62+
// WithDatabase implements the interface sql.Databaser.
63+
func (u *UnnestTableFunction) WithDatabase(database sql.Database) (sql.Node, error) {
64+
nu := *u
65+
nu.database = database
66+
return &nu, nil
67+
}
68+
69+
// Expressions implements the interface sql.Expressioner.
70+
func (u *UnnestTableFunction) Expressions() []sql.Expression {
71+
return u.arrays
72+
}
73+
74+
// WithExpressions implements the interface sql.Expressioner.
75+
func (u *UnnestTableFunction) WithExpressions(ctx *sql.Context, exprs ...sql.Expression) (sql.Node, error) {
76+
for _, expr := range exprs {
77+
typ, ok := expr.Type(ctx).(*pgtypes.DoltgresType)
78+
if !ok {
79+
typ = pgtypes.FromGmsType(expr.Type(ctx))
80+
}
81+
if !typ.IsArrayType() {
82+
return nil, framework.ErrFunctionDoesNotExist.New(fmt.Sprintf("pg_catalog.unnest(%s)", typ))
83+
}
84+
}
85+
nu := *u
86+
nu.arrays = exprs
87+
return &nu, nil
88+
}
89+
90+
// Schema implements the interface sql.Node.
91+
func (u *UnnestTableFunction) Schema(ctx *sql.Context) sql.Schema {
92+
schema := make(sql.Schema, len(u.arrays))
93+
for i, array := range u.arrays {
94+
schema[i] = &sql.Column{Name: u.Name(), Type: array.Type(ctx).(*pgtypes.DoltgresType).ArrayBaseType(), Nullable: true}
95+
}
96+
return schema
97+
}
98+
99+
// Children implements the interface sql.Node.
100+
func (u *UnnestTableFunction) Children() []sql.Node {
101+
return nil
102+
}
103+
104+
// WithChildren implements the interface sql.Node.
105+
func (u *UnnestTableFunction) WithChildren(ctx *sql.Context, children ...sql.Node) (sql.Node, error) {
106+
if len(children) != 0 {
107+
return nil, errors.Errorf("unexpected children")
108+
}
109+
return u, nil
110+
}
111+
112+
// Resolved implements the interface sql.Resolvable.
113+
func (u *UnnestTableFunction) Resolved() bool {
114+
for _, array := range u.arrays {
115+
if !array.Resolved() {
116+
return false
117+
}
118+
}
119+
return true
120+
}
121+
122+
// IsReadOnly implements the interface sql.Node.
123+
func (u *UnnestTableFunction) IsReadOnly() bool {
124+
return true
125+
}
126+
127+
// String implements the interface fmt.Stringer.
128+
func (u *UnnestTableFunction) String() string {
129+
arrays := make([]string, len(u.arrays))
130+
for i, array := range u.arrays {
131+
arrays[i] = array.String()
132+
}
133+
return fmt.Sprintf("unnest(%s)", strings.Join(arrays, ", "))
134+
}
135+
136+
// RowIter implements the interface sql.ExecSourceRel.
137+
func (u *UnnestTableFunction) RowIter(ctx *sql.Context, row sql.Row) (sql.RowIter, error) {
138+
arrays := make([][]any, len(u.arrays))
139+
rowCount := 0
140+
for i, array := range u.arrays {
141+
val, err := array.Eval(ctx, row)
142+
if err != nil {
143+
return nil, err
144+
}
145+
arrays[i], _ = val.([]any)
146+
rowCount = max(rowCount, len(arrays[i]))
147+
}
148+
149+
var i = 0
150+
return pgtypes.NewSetReturningFunctionRowIter(func(ctx *sql.Context) (sql.Row, error) {
151+
defer func() { i++ }()
152+
153+
if i >= rowCount {
154+
return nil, io.EOF
155+
}
156+
result := make(sql.Row, len(arrays))
157+
for j, array := range arrays {
158+
if i < len(array) {
159+
result[j] = array[i]
160+
}
161+
}
162+
return result, nil
163+
}), nil
164+
}

‎server/pg_provider.go‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package server
1616

1717
import (
1818
"context"
19+
"strings"
1920

2021
"github.com/dolthub/dolt/go/libraries/doltcore/env"
2122
"github.com/dolthub/dolt/go/libraries/doltcore/sqle"
@@ -24,6 +25,7 @@ import (
2425
"github.com/dolthub/go-mysql-server/sql"
2526

2627
"github.com/dolthub/doltgresql/core/sequences"
28+
"github.com/dolthub/doltgresql/server/functions"
2729
"github.com/dolthub/doltgresql/server/tables"
2830
)
2931

@@ -58,6 +60,14 @@ func (p *DoltgresDatabaseProvider) AllDatabases(ctx *sql.Context) []sql.Database
5860
return all
5961
}
6062

63+
// TableFunction overrides DoltDatabaseProvider.TableFunction to add the table functions defined by Doltgres.
64+
func (p *DoltgresDatabaseProvider) TableFunction(ctx *sql.Context, name string) (sql.TableFunction, bool) {
65+
if strings.EqualFold(name, "unnest") {
66+
return &functions.UnnestTableFunction{}, true
67+
}
68+
return p.DoltDatabaseProvider.TableFunction(ctx, name)
69+
}
70+
6171
// UnderlyingDoltProvider implements sqle.DoltProviderUnwrapper so that NewSqlEngine can
6272
// access the wrapped *DoltDatabaseProvider for Dolt-specific configuration.
6373
func (p *DoltgresDatabaseProvider) UnderlyingDoltProvider() *sqle.DoltDatabaseProvider {

‎servercfg/config.go‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,10 @@ func (cfg *DoltgresConfig) Overrides() sql.EngineOverrides {
4646
return sql.EngineOverrides{
4747
UpdateExpressionApplier: expression.UpdateExpressionApplier{},
4848
Builder: sql.BuilderOverrides{
49-
ParseTableAsColumn: expression.NewTableToComposite,
50-
ScalarFunctionAliasAsColumn: true,
51-
InsertIgnoreMode: sql.InsertIgnoreModeDuplicateKeysOnly,
49+
ParseTableAsColumn: expression.NewTableToComposite,
50+
ScalarFunctionAliasAsColumn: true,
51+
PermitDerivedTableDuplicateColumnNames: true,
52+
InsertIgnoreMode: sql.InsertIgnoreModeDuplicateKeysOnly,
5253
Parser: pgsql.NewPostgresParserWithOptions(pgsql.ParserOptions{
5354
PermitUnsupportedLockingStatements: cfg.PermitUnsupportedLockingStatements(),
5455
}),

‎testing/go/enginetest/doltgres_engine_test.go‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -842,7 +842,10 @@ func TestVersionedViews(t *testing.T) {
842842
}
843843

844844
func TestWindowFunctions(t *testing.T) {
845-
h := newDoltgresServerHarness(t)
845+
h := newDoltgresServerHarness(t).WithSkippedQueries([]string{
846+
"select 1 as a, 'x' as a", // duplicate derived column names are a MySQL-only error
847+
"t(a, a)", // duplicate derived column names are a MySQL-only error
848+
})
846849
defer h.Close()
847850
enginetest.TestWindowFunctions(t, h)
848851
}

0 commit comments

Comments
 (0)