Skip to content

Commit 4eecb70

Browse files
committed
Override coalesce with a Doltgres-specific implementation that works with DoltgresTypes
1 parent 62c8a16 commit 4eecb70

3 files changed

Lines changed: 243 additions & 0 deletions

File tree

server/analyzer/type_sanitizer.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,32 @@ func TypeSanitizer(ctx *sql.Context, a *analyzer.Analyzer, node sql.Node, scope
7878
// Some aggregation functions cannot be wrapped due to expectations in the analyzer, so we exclude them here.
7979
switch expr.FunctionName() {
8080
case "Count", "CountDistinct", "group_concat", "JSONObjectAgg", "Sum":
81+
case "coalesce":
82+
// Replace GMS Coalesce with a Doltgres-native implementation that uses
83+
// Postgres type-resolution rules (FindCommonType) to infer the result type.
84+
// GMS's Coalesce.Type() falls back to LongText when its arguments are
85+
// DoltgresTypes because they don't satisfy GMS's IsNumber/IsText checks.
86+
if _, isPgCoalesce := expr.(*pgexprs.PgCoalesce); !isPgCoalesce {
87+
children := expr.Children()
88+
allDoltgresTypes := true
89+
for _, child := range children {
90+
if _, ok := child.Type(ctx).(*pgtypes.DoltgresType); !ok {
91+
allDoltgresTypes = false
92+
break
93+
}
94+
}
95+
if allDoltgresTypes {
96+
pgCoalesce, err := pgexprs.NewPgCoalesce(ctx, children...)
97+
if err != nil {
98+
return nil, transform.NewTree, err
99+
}
100+
return pgCoalesce, transform.NewTree, nil
101+
}
102+
}
103+
// Fall through to GMSCast if children aren't DoltgresTypes yet.
104+
if _, ok := expr.Type(ctx).(*pgtypes.DoltgresType); !ok {
105+
return pgexprs.NewGMSCast(expr), transform.NewTree, nil
106+
}
81107
default:
82108
// Some GMS functions wrap Doltgres parameters, so we'll only handle those that return GMS types
83109
if _, ok := expr.Type(ctx).(*pgtypes.DoltgresType); !ok {

server/expression/coalesce.go

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
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 expression
16+
17+
import (
18+
"fmt"
19+
"strings"
20+
21+
"github.com/dolthub/go-mysql-server/sql"
22+
23+
"github.com/dolthub/doltgresql/server/functions/framework"
24+
pgtypes "github.com/dolthub/doltgresql/server/types"
25+
)
26+
27+
// PgCoalesce is a Doltgres-native COALESCE implementation. It uses Postgres type-resolution rules
28+
// (FindCommonType) to compute the correct result type.
29+
type PgCoalesce struct {
30+
args []sql.Expression
31+
typ *pgtypes.DoltgresType // cached result of FindCommonType; nil until first Type() call
32+
}
33+
34+
var _ sql.Expression = (*PgCoalesce)(nil)
35+
var _ sql.FunctionExpression = (*PgCoalesce)(nil)
36+
var _ sql.CollationCoercible = (*PgCoalesce)(nil)
37+
38+
// NewPgCoalesce creates a new PgCoalesce expression.
39+
func NewPgCoalesce(ctx *sql.Context, args ...sql.Expression) (*PgCoalesce, error) {
40+
if len(args) == 0 {
41+
return nil, sql.ErrInvalidArgumentNumber.New("COALESCE", "1 or more", 0)
42+
}
43+
return &PgCoalesce{args: args}, nil
44+
}
45+
46+
// FunctionName implements sql.FunctionExpression.
47+
func (c *PgCoalesce) FunctionName() string { return "coalesce" }
48+
49+
// Description implements sql.FunctionExpression.
50+
func (c *PgCoalesce) Description() string { return "returns the first non-null value in a list." }
51+
52+
// Type implements sql.Expression.
53+
func (c *PgCoalesce) Type(ctx *sql.Context) sql.Type {
54+
if c.typ != nil {
55+
return c.typ
56+
}
57+
childTypes := make([]*pgtypes.DoltgresType, 0, len(c.args))
58+
for _, arg := range c.args {
59+
dt, ok := arg.Type(ctx).(*pgtypes.DoltgresType)
60+
if !ok {
61+
return pgtypes.Unknown
62+
}
63+
childTypes = append(childTypes, dt)
64+
}
65+
commonType, _, err := framework.FindCommonType(ctx, childTypes)
66+
if err != nil || commonType == nil {
67+
return pgtypes.Unknown
68+
}
69+
c.typ = commonType
70+
return commonType
71+
}
72+
73+
// CollationCoercibility implements sql.CollationCoercible.
74+
func (c *PgCoalesce) CollationCoercibility(ctx *sql.Context) (collation sql.CollationID, coercibility byte) {
75+
if cc, ok := c.Type(ctx).(sql.CollationCoercible); ok {
76+
return cc.CollationCoercibility(ctx)
77+
}
78+
return sql.Collation_binary, 6
79+
}
80+
81+
// IsNullable implements sql.Expression.
82+
func (c *PgCoalesce) IsNullable(_ *sql.Context) bool {
83+
return true
84+
}
85+
86+
// Resolved implements sql.Expression.
87+
func (c *PgCoalesce) Resolved() bool {
88+
for _, arg := range c.args {
89+
if arg == nil || !arg.Resolved() {
90+
return false
91+
}
92+
}
93+
return true
94+
}
95+
96+
// Children implements sql.Expression.
97+
func (c *PgCoalesce) Children() []sql.Expression { return c.args }
98+
99+
// WithChildren implements sql.Expression.
100+
func (c *PgCoalesce) WithChildren(ctx *sql.Context, children ...sql.Expression) (sql.Expression, error) {
101+
if len(children) == 0 {
102+
return nil, sql.ErrInvalidArgumentNumber.New("COALESCE", "1 or more", 0)
103+
}
104+
return &PgCoalesce{args: children}, nil
105+
}
106+
107+
// Eval implements sql.Expression. Returns the first non-null argument value, cast to the common type.
108+
func (c *PgCoalesce) Eval(ctx *sql.Context, row sql.Row) (any, error) {
109+
commonType, hasCommonType := c.Type(ctx).(*pgtypes.DoltgresType)
110+
for _, arg := range c.args {
111+
if arg == nil {
112+
continue
113+
}
114+
val, err := arg.Eval(ctx, row)
115+
if err != nil {
116+
return nil, err
117+
}
118+
if val == nil {
119+
continue
120+
}
121+
if !hasCommonType {
122+
return val, nil
123+
}
124+
argType, ok := arg.Type(ctx).(*pgtypes.DoltgresType)
125+
if ok && argType.Equals(commonType) {
126+
return val, nil
127+
}
128+
// Cast the value to the common type (handles mixed-type args, e.g. int2 and int4).
129+
converted, _, err := commonType.Convert(ctx, val)
130+
if err != nil {
131+
return nil, err
132+
}
133+
return converted, nil
134+
}
135+
return nil, nil
136+
}
137+
138+
// String implements sql.Expression.
139+
func (c *PgCoalesce) String() string {
140+
args := make([]string, len(c.args))
141+
for i, arg := range c.args {
142+
args[i] = arg.String()
143+
}
144+
return fmt.Sprintf("coalesce(%s)", strings.Join(args, ","))
145+
}
146+
147+
// DebugString implements the sql.Debuggable interface.
148+
func (c *PgCoalesce) DebugString(ctx *sql.Context) string {
149+
args := make([]string, len(c.args))
150+
for i, arg := range c.args {
151+
args[i] = sql.DebugString(ctx, arg)
152+
}
153+
return fmt.Sprintf("coalesce(%s)", strings.Join(args, ","))
154+
}

testing/go/expressions_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,3 +487,66 @@ func TestSubscript(t *testing.T) {
487487
},
488488
})
489489
}
490+
491+
func TestCoalesce(t *testing.T) {
492+
RunScripts(t, []ScriptTest{
493+
{
494+
// https://github.com/dolthub/doltgresql/issues/2332
495+
Name: "COALESCE(NULL, col) in UPDATE",
496+
SetUpScript: []string{
497+
`CREATE TABLE t (id UUID PRIMARY KEY, val INTEGER NOT NULL DEFAULT 0, d DATE)`,
498+
`INSERT INTO t VALUES ('00000000-0000-0000-0000-000000000001', 42, '2026-01-01')`,
499+
},
500+
Assertions: []ScriptTestAssertion{
501+
{
502+
// Should be a no-op; val stays 42.
503+
Query: `UPDATE t SET val = COALESCE(NULL, val) WHERE id = '00000000-0000-0000-0000-000000000001'`,
504+
SkipResultsCheck: true,
505+
},
506+
{
507+
Query: `SELECT val FROM t WHERE id = '00000000-0000-0000-0000-000000000001'`,
508+
Expected: []sql.Row{{int32(42)}},
509+
},
510+
{
511+
// Should be a no-op; d stays '2026-01-01'.
512+
Query: `UPDATE t SET d = COALESCE(NULL, d) WHERE id = '00000000-0000-0000-0000-000000000001'`,
513+
SkipResultsCheck: true,
514+
},
515+
{
516+
Query: `SELECT d FROM t WHERE id = '00000000-0000-0000-0000-000000000001'`,
517+
Expected: []sql.Row{{"2026-01-01"}},
518+
},
519+
},
520+
},
521+
{
522+
Name: "COALESCE type resolution in SELECT",
523+
Assertions: []ScriptTestAssertion{
524+
{
525+
Query: `SELECT COALESCE(NULL, 42)`,
526+
Expected: []sql.Row{{int32(42)}},
527+
},
528+
{
529+
Query: `SELECT COALESCE(NULL, NULL)`,
530+
Expected: []sql.Row{{nil}},
531+
},
532+
{
533+
Query: `SELECT COALESCE(NULL, NULL, 'hello')`,
534+
Expected: []sql.Row{{"hello"}},
535+
},
536+
{
537+
Query: `SELECT COALESCE(1, 2, 3)`,
538+
Expected: []sql.Row{{int32(1)}},
539+
},
540+
{
541+
Query: `SELECT COALESCE(NULL, 2, 3)`,
542+
Expected: []sql.Row{{int32(2)}},
543+
},
544+
{
545+
// Explicit cast workaround still works.
546+
Query: `SELECT COALESCE(NULL::integer, 42)`,
547+
Expected: []sql.Row{{int32(42)}},
548+
},
549+
},
550+
},
551+
})
552+
}

0 commit comments

Comments
 (0)