Skip to content

Commit cfde3c5

Browse files
committed
Implement values.Wrapper and values.AnyWrapper
1 parent f61a772 commit cfde3c5

File tree

13 files changed

+692
-59
lines changed

13 files changed

+692
-59
lines changed

enginetest/enginetests.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -810,7 +810,9 @@ func TestOrderByGroupBy(t *testing.T, harness Harness) {
810810
panic(fmt.Sprintf("unexpected type %T", v))
811811
}
812812

813-
team := row[1].(string)
813+
team, ok, err := sql.Unwrap[string](ctx, row[1])
814+
require.NoError(t, err)
815+
require.True(t, ok)
814816
switch team {
815817
case "red":
816818
require.True(t, val == 3 || val == 4)

enginetest/evaluation.go

Lines changed: 46 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package enginetest
1616

1717
import (
18+
"context"
1819
"fmt"
1920
"strconv"
2021
"strings"
@@ -679,8 +680,8 @@ func checkResults(
679680
q string,
680681
e QueryEngine,
681682
) {
682-
widenedRows := WidenRows(sch, rows)
683-
widenedExpected := WidenRows(sch, expected)
683+
widenedRows := WidenRows(t, sch, rows)
684+
widenedExpected := WidenRows(t, sch, expected)
684685

685686
upperQuery := strings.ToUpper(q)
686687
orderBy := strings.Contains(upperQuery, "ORDER BY ")
@@ -795,62 +796,70 @@ func simplifyResultSchema(s sql.Schema) []resultSchemaCol {
795796
return fields
796797
}
797798

798-
// WidenRows returns a slice of rows with all values widened to their widest type.
799+
// WidenRows returns a slice of rows with all values widened to their widest type, and wrapper types unwrapped.
799800
// For a variety of reasons, the widths of various primitive types can vary when passed through different SQL queries
800801
// (and different database implementations). We may eventually decide that this undefined behavior is a problem, but
801802
// for now it's mostly just an issue when comparing results in tests. To get around this, we widen every type to its
802803
// widest value in actual and expected results.
803-
func WidenRows(sch sql.Schema, rows []sql.Row) []sql.Row {
804+
func WidenRows(t *testing.T, sch sql.Schema, rows []sql.Row) []sql.Row {
804805
widened := make([]sql.Row, len(rows))
805806
for i, row := range rows {
806-
widened[i] = WidenRow(sch, row)
807+
widened[i] = WidenRow(t, sch, row)
807808
}
808809
return widened
809810
}
810811

811-
// WidenRow returns a row with all values widened to their widest type
812-
func WidenRow(sch sql.Schema, row sql.Row) sql.Row {
812+
// WidenRow returns a row with all values widened to their widest type, and wrapper types unwrapped.
813+
func WidenRow(t *testing.T, sch sql.Schema, row sql.Row) sql.Row {
813814
widened := make(sql.Row, len(row))
814815
for i, v := range row {
815-
816-
var vw interface{}
817816
if i < len(sch) && types.IsJSON(sch[i].Type) {
818817
widened[i] = widenJSONValues(v)
819818
continue
820819
}
821820

822-
switch x := v.(type) {
823-
case int:
824-
vw = int64(x)
825-
case int8:
826-
vw = int64(x)
827-
case int16:
828-
vw = int64(x)
829-
case int32:
830-
vw = int64(x)
831-
case uint:
832-
vw = uint64(x)
833-
case uint8:
834-
vw = uint64(x)
835-
case uint16:
836-
vw = uint64(x)
837-
case uint32:
838-
vw = uint64(x)
839-
case float32:
840-
// casting it to float64 causes approximation, which doesn't work for server engine results.
841-
vw, _ = strconv.ParseFloat(fmt.Sprintf("%v", v), 64)
842-
case decimal.Decimal:
843-
// The exact expected decimal type value cannot be defined in enginetests,
844-
// so convert the result to string format, which is the value we get on sql shell.
845-
vw = x.StringFixed(x.Exponent() * -1)
846-
default:
847-
vw = v
848-
}
849-
widened[i] = vw
821+
widened[i] = widenValue(t, v)
850822
}
851823
return widened
852824
}
853825

826+
// widenValue normalizes the input by widening it to its widest type and unwrapping any wrappers.
827+
func widenValue(t *testing.T, v interface{}) (vw interface{}) {
828+
var err error
829+
switch x := v.(type) {
830+
case int:
831+
vw = int64(x)
832+
case int8:
833+
vw = int64(x)
834+
case int16:
835+
vw = int64(x)
836+
case int32:
837+
vw = int64(x)
838+
case uint:
839+
vw = uint64(x)
840+
case uint8:
841+
vw = uint64(x)
842+
case uint16:
843+
vw = uint64(x)
844+
case uint32:
845+
vw = uint64(x)
846+
case float32:
847+
// casting it to float64 causes approximation, which doesn't work for server engine results.
848+
vw, _ = strconv.ParseFloat(fmt.Sprintf("%v", v), 64)
849+
case decimal.Decimal:
850+
// The exact expected decimal type value cannot be defined in enginetests,
851+
// so convert the result to string format, which is the value we get on sql shell.
852+
vw = x.StringFixed(x.Exponent() * -1)
853+
case sql.AnyWrapper:
854+
vw, err = x.UnwrapAny(context.Background())
855+
vw = widenValue(t, vw)
856+
require.NoError(t, err)
857+
default:
858+
vw = v
859+
}
860+
return vw
861+
}
862+
854863
func widenJSONValues(val interface{}) sql.JSONWrapper {
855864
if val == nil {
856865
return nil

enginetest/wrapper_test.go

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
// Copyright 2025 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 enginetest
16+
17+
import (
18+
"context"
19+
"fmt"
20+
sqle "github.com/dolthub/go-mysql-server"
21+
"github.com/dolthub/go-mysql-server/memory"
22+
"github.com/dolthub/go-mysql-server/sql"
23+
"github.com/dolthub/go-mysql-server/sql/planbuilder"
24+
"github.com/dolthub/go-mysql-server/sql/types"
25+
"github.com/stretchr/testify/require"
26+
"testing"
27+
)
28+
29+
// ErrorWrapper is a wrapped type that errors when unwrapped. This can be used to test that certain operations
30+
// won't trigger an unwrap.
31+
type ErrorWrapper[T any] struct {
32+
maxByteLength int64
33+
isExactLength bool
34+
}
35+
36+
func (w ErrorWrapper[T]) Compare(ctx context.Context, other interface{}) (cmp int, comparable bool, err error) {
37+
return 0, false, nil
38+
}
39+
40+
var textErrorWrapper = ErrorWrapper[string]{maxByteLength: types.Text.MaxByteLength(), isExactLength: false}
41+
var longTextErrorWrapper = ErrorWrapper[string]{maxByteLength: types.LongText.MaxByteLength(), isExactLength: false}
42+
43+
func exactLengthErrorWrapper(maxByteLength int64) ErrorWrapper[string] {
44+
return ErrorWrapper[string]{maxByteLength: maxByteLength, isExactLength: true}
45+
}
46+
47+
func (w ErrorWrapper[T]) assertInterfaces() {
48+
var _ sql.Wrapper[T] = w
49+
}
50+
51+
func (w ErrorWrapper[T]) Unwrap(ctx context.Context) (result T, err error) {
52+
return result, fmt.Errorf("unwrap failed")
53+
}
54+
55+
func (w ErrorWrapper[T]) UnwrapAny(ctx context.Context) (result interface{}, err error) {
56+
return result, fmt.Errorf("unwrap failed")
57+
}
58+
59+
func (w ErrorWrapper[T]) MaxByteLength() int64 {
60+
return w.maxByteLength
61+
}
62+
63+
func (w ErrorWrapper[T]) IsExactLength() bool {
64+
return w.isExactLength
65+
}
66+
67+
func setupWrapperTests(t *testing.T) (*sql.Context, *memory.Database, *MemoryHarness, *sqle.Engine) {
68+
db := memory.NewDatabase("mydb")
69+
pro := memory.NewDBProvider(db)
70+
harness := NewDefaultMemoryHarness().WithProvider(pro)
71+
ctx := NewContext(harness)
72+
e := NewEngineWithProvider(t, harness, pro)
73+
return ctx, db, harness, e
74+
}
75+
76+
// TestWrapperCopyInKey tests that copying a wrapped value in the primary key doesn't require the value to be unwrapped.
77+
// This is skipped because inserting into tables requires comparisons between primary keys, which currently requires
78+
// unwrapping. But in the future, we may be able to skip fully unwrapping values for specific operations.
79+
func TestWrapperCopyInKey(t *testing.T) {
80+
t.Skip()
81+
ctx, db, harness, e := setupWrapperTests(t)
82+
83+
schema := sql.NewPrimaryKeySchema(sql.Schema{
84+
&sql.Column{Name: "col1", Source: "test", Type: types.LongText, Nullable: false, Default: planbuilder.MustStringToColumnDefaultValue(sql.NewEmptyContext(), `""`, types.Text, false)},
85+
})
86+
table := memory.NewTable(db.BaseDatabase, "test", schema, nil)
87+
88+
require.NoError(t, table.Insert(ctx, sql.Row{"brave"}))
89+
require.NoError(t, table.Insert(ctx, sql.Row{longTextErrorWrapper}))
90+
require.NoError(t, table.Insert(ctx, sql.Row{"!"}))
91+
92+
TestQueryWithContext(t, ctx, e, harness, "CREATE TABLE t2 AS SELECT 1, col1, 2 FROM test;", nil, nil, nil, nil)
93+
}
94+
95+
// TestWrapperCopyInKey tests that copying a wrapped value not in the primary key doesn't require the value to be unwrapped.
96+
func TestWrapperCopyNotInKey(t *testing.T) {
97+
ctx, db, harness, e := setupWrapperTests(t)
98+
99+
schema := sql.NewPrimaryKeySchema(sql.Schema{
100+
&sql.Column{Name: "pk", Source: "test", Type: types.Int64, Nullable: false, Default: planbuilder.MustStringToColumnDefaultValue(sql.NewEmptyContext(), `1`, types.Int64, false), PrimaryKey: true},
101+
&sql.Column{Name: "col1", Source: "test", Type: types.LongText, Nullable: false, Default: planbuilder.MustStringToColumnDefaultValue(sql.NewEmptyContext(), `""`, types.Text, false), PrimaryKey: false},
102+
})
103+
104+
testTable := memory.NewTable(db.BaseDatabase, "test", schema, nil)
105+
db.AddTable("test", testTable)
106+
107+
require.NoError(t, testTable.Insert(ctx, sql.Row{int64(1), "brave"}))
108+
require.NoError(t, testTable.Insert(ctx, sql.Row{int64(2), longTextErrorWrapper}))
109+
require.NoError(t, testTable.Insert(ctx, sql.Row{int64(3), "!"}))
110+
111+
copySchema := sql.NewPrimaryKeySchema(sql.Schema{
112+
&sql.Column{Name: "one", Source: "t2", Type: types.Int64, Nullable: false, Default: planbuilder.MustStringToColumnDefaultValue(sql.NewEmptyContext(), `1`, types.Int64, false), PrimaryKey: true},
113+
&sql.Column{Name: "pk", Source: "t2", Type: types.Int64, Nullable: false, Default: planbuilder.MustStringToColumnDefaultValue(sql.NewEmptyContext(), `1`, types.Int64, false), PrimaryKey: true},
114+
&sql.Column{Name: "col1", Source: "t2", Type: types.LongText, Nullable: false, Default: planbuilder.MustStringToColumnDefaultValue(sql.NewEmptyContext(), `""`, types.Text, false), PrimaryKey: false},
115+
&sql.Column{Name: "two", Source: "t2", Type: types.Int64, Nullable: false, Default: planbuilder.MustStringToColumnDefaultValue(sql.NewEmptyContext(), `1`, types.Int64, false), PrimaryKey: false},
116+
})
117+
118+
testTable2 := memory.NewTable(db.BaseDatabase, "t2", copySchema, nil)
119+
db.AddTable("t2", testTable2)
120+
121+
TestQueryWithContext(t, ctx, e, harness, "INSERT INTO t2 SELECT 1, pk, col1, 2 FROM test;", nil, nil, nil, nil)
122+
}
123+
124+
// TestWrapperCopyWhenWideningColumn tests that widening a column doesn't cause values to be unwrapped.
125+
func TestWrapperCopyWhenWideningColumn(t *testing.T) {
126+
ctx, db, harness, e := setupWrapperTests(t)
127+
128+
schema := sql.NewPrimaryKeySchema(sql.Schema{
129+
&sql.Column{Name: "pk", Source: "test", Type: types.Int64, Nullable: false, Default: planbuilder.MustStringToColumnDefaultValue(sql.NewEmptyContext(), `1`, types.Int64, false), PrimaryKey: true},
130+
&sql.Column{Name: "col1", Source: "test", Type: types.Text, Nullable: false, Default: planbuilder.MustStringToColumnDefaultValue(sql.NewEmptyContext(), `""`, types.Text, false), PrimaryKey: false},
131+
})
132+
133+
testTable := memory.NewTable(db.BaseDatabase, "test", schema, nil)
134+
db.AddTable("test", testTable)
135+
136+
require.NoError(t, testTable.Insert(ctx, sql.Row{int64(1), "brave"}))
137+
require.NoError(t, testTable.Insert(ctx, sql.Row{int64(2), textErrorWrapper}))
138+
require.NoError(t, testTable.Insert(ctx, sql.Row{int64(3), "!"}))
139+
140+
TestQueryWithContext(t, ctx, e, harness, "ALTER TABLE test MODIFY COLUMN col1 LONGTEXT;", nil, nil, nil, nil)
141+
}
142+
143+
// TestWrapperCopyWhenWideningColumn tests that widening a column doesn't cause values to be unwrapped.
144+
func TestWrapperCopyWhenNarrowingColumn(t *testing.T) {
145+
ctx, db, harness, e := setupWrapperTests(t)
146+
147+
schema := sql.NewPrimaryKeySchema(sql.Schema{
148+
&sql.Column{Name: "pk", Source: "test", Type: types.Int64, Nullable: false, Default: planbuilder.MustStringToColumnDefaultValue(sql.NewEmptyContext(), `1`, types.Int64, false), PrimaryKey: true},
149+
&sql.Column{Name: "col1", Source: "test", Type: types.LongText, Nullable: false, Default: planbuilder.MustStringToColumnDefaultValue(sql.NewEmptyContext(), `""`, types.Text, false), PrimaryKey: false},
150+
})
151+
152+
testTable := memory.NewTable(db.BaseDatabase, "test", schema, nil)
153+
db.AddTable("test", testTable)
154+
155+
require.NoError(t, testTable.Insert(ctx, sql.Row{int64(1), "brave"}))
156+
require.NoError(t, testTable.Insert(ctx, sql.Row{int64(2), longTextErrorWrapper}))
157+
require.NoError(t, testTable.Insert(ctx, sql.Row{int64(3), "!"}))
158+
159+
AssertErrWithCtx(t, e, harness, ctx, "ALTER TABLE test MODIFY COLUMN col1 TEXT;", nil, nil, "unwrap failed")
160+
}
161+
162+
func TestWrapperCopyWithExactLengthWhenNarrowingColumn(t *testing.T) {
163+
ctx, db, harness, e := setupWrapperTests(t)
164+
165+
schema := sql.NewPrimaryKeySchema(sql.Schema{
166+
&sql.Column{Name: "pk", Source: "test", Type: types.Int64, Nullable: false, Default: planbuilder.MustStringToColumnDefaultValue(sql.NewEmptyContext(), `1`, types.Int64, false), PrimaryKey: true},
167+
&sql.Column{Name: "col1", Source: "test", Type: types.LongText, Nullable: false, Default: planbuilder.MustStringToColumnDefaultValue(sql.NewEmptyContext(), `""`, types.Text, false), PrimaryKey: false},
168+
})
169+
170+
testTable := memory.NewTable(db.BaseDatabase, "test", schema, nil)
171+
db.AddTable("test", testTable)
172+
173+
require.NoError(t, testTable.Insert(ctx, sql.Row{int64(1), "brave"}))
174+
require.NoError(t, testTable.Insert(ctx, sql.Row{int64(2), exactLengthErrorWrapper(64)}))
175+
require.NoError(t, testTable.Insert(ctx, sql.Row{int64(3), "!"}))
176+
177+
TestQueryWithContext(t, ctx, e, harness, "ALTER TABLE test MODIFY COLUMN col1 TEXT;", nil, nil, nil, nil)
178+
}

memory/table_data.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import (
2525
"github.com/dolthub/go-mysql-server/sql"
2626
"github.com/dolthub/go-mysql-server/sql/expression"
2727
"github.com/dolthub/go-mysql-server/sql/transform"
28-
"github.com/dolthub/go-mysql-server/sql/types"
2928
)
3029

3130
// TableData encapsulates all schema and data for a table's schema and rows. Other aspects of a table can change
@@ -139,7 +138,7 @@ func (td TableData) partition(row sql.Row) (int, error) {
139138

140139
t, isStringType := td.schema.Schema[keyColumns[i]].Type.(sql.StringType)
141140
if isStringType && v != nil {
142-
v, err = types.ConvertToString(v, t, nil)
141+
v, _, err = t.Convert(v)
143142
if err == nil {
144143
err = t.Collation().WriteWeightString(hash, v.(string))
145144
}

memory/table_editor.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -862,7 +862,12 @@ func verifyRowTypes(row sql.Row, schema sql.Schema) error {
862862
for i := range schema {
863863
col := schema[i]
864864
rowVal := row[i]
865-
valType := reflect.TypeOf(rowVal)
865+
var valType reflect.Type
866+
if wrapper, isWrapper := rowVal.(sql.AnyWrapper); isWrapper {
867+
method, _ := reflect.TypeOf(wrapper).MethodByName("Unwrap")
868+
valType = method.Type.Out(0)
869+
}
870+
valType = reflect.TypeOf(rowVal)
866871
expectedType := col.Type.ValueType()
867872
if valType != expectedType && rowVal != nil && !valType.AssignableTo(expectedType) {
868873
return fmt.Errorf("Actual Value Type: %s, Expected Value Type: %s", valType.String(), expectedType.String())

0 commit comments

Comments
 (0)