Skip to content

Commit f256286

Browse files
committed
Fixing double-cast panic on window SUM/AVG in subqueries
1 parent e18b158 commit f256286

3 files changed

Lines changed: 98 additions & 37 deletions

File tree

server/analyzer/type_sanitizer.go

Lines changed: 18 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -70,25 +70,12 @@ func TypeSanitizer(ctx *sql.Context, a *analyzer.Analyzer, node sql.Node, scope
7070
}
7171
// After this point expr IS a DoltgresType.
7272
doltType := expr.Type(ctx).(*pgtypes.DoltgresType)
73-
// GMS window SUM propagates the child column's DoltgresType via Sum.Type(),
74-
// but always accumulates float64 at runtime. Match by ColumnId in
75-
// Window.SelectExprs; for integer aggregates apply Float64→Int64.
76-
if winNode := findWindowNode(child); winNode != nil {
77-
for _, selectExpr := range winNode.SelectExprs {
78-
ide, ok := selectExpr.(sql.IdExpression)
79-
if !ok || ide.Id() != expr.Id() {
80-
continue
81-
}
82-
if _, isAgg := selectExpr.(sql.Aggregation); isAgg {
83-
if doltType.Equals(pgtypes.Int32) || doltType.Equals(pgtypes.Int16) || doltType.Equals(pgtypes.Int64) {
84-
return pgexprs.NewAssignmentCast(expr, pgtypes.Float64, pgtypes.Int64), transform.NewTree, nil
85-
}
86-
}
87-
break
88-
}
89-
}
90-
// An outer Project may carry a stale declared type; child.Schema() holds
91-
// the corrected type after bottom-up transform. Re-annotate on mismatch.
73+
// Window SUM/AVG over integer or real columns are fixed at the source (see the
74+
// AggCast wrap for *plan.Window below), so the Window's own schema already
75+
// reports the corrected type. An outer Project (possibly reached through a
76+
// SubqueryAlias) may still carry a stale declared type from before that fix, or
77+
// from GMS's own type propagation; child.Schema() holds the corrected type after
78+
// the bottom-up transform. Re-annotate on mismatch.
9279
if innerType := windowSchemaTypeByName(child, ctx, expr.Name()); innerType != nil {
9380
if !doltType.Equals(innerType) {
9481
return pgexprs.NewAssignmentCast(expr, innerType, innerType), transform.NewTree, nil
@@ -148,8 +135,15 @@ func TypeSanitizer(ctx *sql.Context, a *analyzer.Analyzer, node sql.Node, scope
148135
// SUM(real) -> real (needs narrowing back from the float64 accumulator)
149136
// AVG(smallint/integer/bigint) -> numeric
150137
// AVG(real) -> double precision
151-
// In GroupBy context, wrap with AggCast to convert the float64 result to the
152-
// correct target type while preserving the sql.Aggregation interface.
138+
// In GroupBy or Window context, wrap with AggCast to convert the float64 result
139+
// to the correct target type while preserving the sql.Aggregation and
140+
// sql.WindowAdaptableExpression interfaces. Fixing this at the source (the
141+
// aggregate itself) rather than at each outer reference to it is what makes this
142+
// idempotent: AggCast doesn't implement sql.FunctionExpression, so once applied it
143+
// no longer matches this case on a later revisit (e.g. via the opaque traversal
144+
// that re-walks an already-analyzed subquery), unlike wrapping an outer GetField
145+
// reference, whose declared type never changes and so looks like it "still needs
146+
// fixing" on every subsequent visit.
153147
var aggTargetType *pgtypes.DoltgresType
154148
if doltType, ok := expr.Type(ctx).(*pgtypes.DoltgresType); ok {
155149
switch expr.FunctionName() {
@@ -172,7 +166,8 @@ func TypeSanitizer(ctx *sql.Context, a *analyzer.Analyzer, node sql.Node, scope
172166
}
173167
}
174168
if aggTargetType != nil {
175-
if _, isGroupBy := n.(*plan.GroupBy); isGroupBy {
169+
switch n.(type) {
170+
case *plan.GroupBy, *plan.Window:
176171
return pgexprs.NewAggCast(expr.(sql.Aggregation), aggTargetType), transform.NewTree, nil
177172
}
178173
}
@@ -361,7 +356,7 @@ func findWindowNode(n sql.Node) *plan.Window {
361356
return w
362357
}
363358
switch n.(type) {
364-
case *plan.Sort, *plan.Limit, *plan.Offset, *plan.Distinct, *plan.Filter, *plan.Project:
359+
case *plan.Sort, *plan.Limit, *plan.Offset, *plan.Distinct, *plan.Filter, *plan.Project, *plan.SubqueryAlias:
365360
children := n.Children()
366361
if len(children) == 1 {
367362
return findWindowNode(children[0])

server/expression/agg_cast.go

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,21 +25,23 @@ import (
2525
pgtypes "github.com/dolthub/doltgresql/server/types"
2626
)
2727

28-
// AggCast wraps a sql.Aggregation to override its declared return type and
29-
// post-convert the buffer's Eval result. It preserves the sql.Aggregation
30-
// interface so GroupBy aggregation machinery works correctly.
28+
// AggCast wraps a sql.Aggregation to override its declared return type and post-convert
29+
// its result, whether reached through the GroupBy buffer path (NewBuffer/Eval) or the
30+
// window function path (NewWindowFunction/Compute). It preserves both the sql.Aggregation
31+
// and sql.WindowAdaptableExpression interfaces so GroupBy and window execution both work.
3132
//
3233
// GMS SUM and AVG over integer columns always accumulate as float64 internally, but
3334
// Postgres specifies SUM(int2/int4/int8) → bigint and AVG(int2/int4/int8) → numeric.
34-
// AggCast intercepts the float64 buffer result and converts it to the target type so
35-
// the correct wire type is used.
35+
// AggCast intercepts the float64 result and converts it to the target type so the
36+
// correct wire type is used.
3637
type AggCast struct {
3738
inner sql.Aggregation
3839
targetType *pgtypes.DoltgresType
3940
}
4041

4142
var _ sql.Expression = (*AggCast)(nil)
4243
var _ sql.Aggregation = (*AggCast)(nil)
44+
var _ sql.WindowFunction = (*aggCastWindowFunction)(nil)
4345

4446
// NewAggCast wraps inner so that its declared type is targetType and its buffer
4547
// Eval result is converted from float64 to int64.
@@ -59,9 +61,14 @@ func (a *AggCast) NewBuffer(ctx *sql.Context) (sql.AggregationBuffer, error) {
5961
return &aggCastBuffer{inner: buf, targetType: a.targetType}, nil
6062
}
6163

62-
// NewWindowFunction delegates to inner.
64+
// NewWindowFunction delegates to inner but wraps the result so Compute's float64 output is
65+
// converted to match targetType, the same way NewBuffer wraps the AggregationBuffer path.
6366
func (a *AggCast) NewWindowFunction(ctx *sql.Context) (sql.WindowFunction, error) {
64-
return a.inner.NewWindowFunction(ctx)
67+
fn, err := a.inner.NewWindowFunction(ctx)
68+
if err != nil {
69+
return nil, err
70+
}
71+
return &aggCastWindowFunction{inner: fn, targetType: a.targetType}, nil
6572
}
6673

6774
// WithWindow delegates to inner.
@@ -126,26 +133,60 @@ func (b *aggCastBuffer) Eval(ctx *sql.Context) (any, error) {
126133
if err != nil || v == nil {
127134
return v, err
128135
}
136+
return convertAggResult(v, b.targetType)
137+
}
138+
139+
func (b *aggCastBuffer) Dispose(ctx *sql.Context) {
140+
b.inner.Dispose(ctx)
141+
}
142+
143+
// aggCastWindowFunction wraps a sql.WindowFunction and post-converts its float64 Compute
144+
// result the same way aggCastBuffer does for the sql.AggregationBuffer (GroupBy) path.
145+
type aggCastWindowFunction struct {
146+
inner sql.WindowFunction
147+
targetType *pgtypes.DoltgresType
148+
}
149+
150+
func (w *aggCastWindowFunction) StartPartition(ctx *sql.Context, interval sql.WindowInterval, buffer sql.WindowBuffer) error {
151+
return w.inner.StartPartition(ctx, interval, buffer)
152+
}
153+
154+
func (w *aggCastWindowFunction) DefaultFramer() sql.WindowFramer {
155+
return w.inner.DefaultFramer()
156+
}
157+
158+
func (w *aggCastWindowFunction) Compute(ctx *sql.Context, interval sql.WindowInterval, buffer sql.WindowBuffer) (any, error) {
159+
v, err := w.inner.Compute(ctx, interval, buffer)
160+
if err != nil || v == nil {
161+
return v, err
162+
}
163+
return convertAggResult(v, w.targetType)
164+
}
165+
166+
func (w *aggCastWindowFunction) Dispose(ctx *sql.Context) {
167+
w.inner.Dispose(ctx)
168+
}
169+
170+
// convertAggResult converts a raw aggregation result (always float64 from GMS's SUM/AVG
171+
// implementations, regardless of the input column's width) to match targetType: numeric,
172+
// float32, float64 (a no-op), or int64 for everything else (bigint).
173+
func convertAggResult(v any, targetType *pgtypes.DoltgresType) (any, error) {
129174
f, ok := v.(float64)
130175
if !ok {
131176
return v, nil
132177
}
133178
switch {
134-
case b.targetType.Equals(pgtypes.Numeric):
179+
case targetType.Equals(pgtypes.Numeric):
135180
d, _, err := apd.NewFromString(strconv.FormatFloat(f, 'f', -1, 64))
136181
if err != nil {
137182
return nil, err
138183
}
139184
return d, nil
140-
case b.targetType.Equals(pgtypes.Float32):
185+
case targetType.Equals(pgtypes.Float32):
141186
return float32(f), nil
142-
case b.targetType.Equals(pgtypes.Float64):
187+
case targetType.Equals(pgtypes.Float64):
143188
return f, nil
144189
default:
145190
return int64(math.RoundToEven(f)), nil
146191
}
147192
}
148-
149-
func (b *aggCastBuffer) Dispose(ctx *sql.Context) {
150-
b.inner.Dispose(ctx)
151-
}

testing/go/window_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,5 +139,30 @@ func TestWindowFunctions(t *testing.T) {
139139
},
140140
},
141141
},
142+
{
143+
Name: "window SUM/AVG wrapped in a subquery projection",
144+
SetUpScript: []string{
145+
"CREATE TABLE wrapper_probe (grp INT, val INT);",
146+
"INSERT INTO wrapper_probe VALUES (1, 10), (1, 20), (2, 5);",
147+
},
148+
Assertions: []ScriptTestAssertion{
149+
{
150+
Query: "SELECT grp, val, grp_total FROM (SELECT grp, val, SUM(val) OVER (PARTITION BY grp) AS grp_total FROM wrapper_probe) sub ORDER BY grp, val;",
151+
Expected: []sql.Row{
152+
{1, 10, int64(30)},
153+
{1, 20, int64(30)},
154+
{2, 5, int64(5)},
155+
},
156+
},
157+
{
158+
Query: "SELECT grp, val, grp_avg FROM (SELECT grp, val, AVG(val) OVER (PARTITION BY grp) AS grp_avg FROM wrapper_probe) sub ORDER BY grp, val;",
159+
Expected: []sql.Row{
160+
{1, 10, Numeric("15")},
161+
{1, 20, Numeric("15")},
162+
{2, 5, Numeric("5")},
163+
},
164+
},
165+
},
166+
},
142167
})
143168
}

0 commit comments

Comments
 (0)