From 6c08454ff74f3b2baf27ce2e248f0ab7d02c6adc Mon Sep 17 00:00:00 2001 From: Daylon Wilkins Date: Thu, 8 Feb 2024 05:11:42 -0800 Subject: [PATCH] Referencing columns within arrays --- CONTRIBUTING.md | 4 + go.mod | 8 +- go.sum | 16 ++-- main.go | 16 ++-- server/ast/expr.go | 30 +++++-- server/expression/array.go | 163 +++++++++++++++++++++++++++++++++++++ testing/go/smoke_test.go | 23 ++++++ 7 files changed, 232 insertions(+), 28 deletions(-) create mode 100644 server/expression/array.go diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6ecfe9dd79..3a60a36701 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -245,6 +245,10 @@ There are exceptions, as some statements we do not yet support, and cannot suppo In these cases, we must add a `//TODO:` comment stating what is missing and why it isn't an error. This will at least allow us to track all such instances where we deviate from the expected behavior, which we can also document elsewhere for users of DoltgreSQL. +### `server/expression` + +TODO: FILL ME OUT BEFORE COMMITTING + ### `server/functions` The `functions` package contains the functions, along with an implementation to approximate the function overloading structure (and type coercion). diff --git a/go.mod b/go.mod index b46c93b9c4..99e970f9d9 100644 --- a/go.mod +++ b/go.mod @@ -6,11 +6,11 @@ require ( github.com/PuerkitoBio/goquery v1.8.1 github.com/cockroachdb/apd/v2 v2.0.3-0.20200518165714-d020e156310a github.com/cockroachdb/errors v1.7.5 - github.com/dolthub/dolt/go v0.40.5-0.20240207142355-cbe6b0ce7f01 - github.com/dolthub/dolt/go/gen/proto/dolt/services/eventsapi v0.0.0-20240205125942-fc7c3429f29c - github.com/dolthub/go-mysql-server v0.17.1-0.20240207124505-c0f397a6aaca + github.com/dolthub/dolt/go v0.40.5-0.20240209132845-d391bcf946e7 + github.com/dolthub/dolt/go/gen/proto/dolt/services/eventsapi v0.0.0-20240209132845-d391bcf946e7 + github.com/dolthub/go-mysql-server v0.17.1-0.20240209131710-527b791c74c0 github.com/dolthub/sqllogictest/go v0.0.0-20240118211725-a52e3f5697e3 - github.com/dolthub/vitess v0.0.0-20240207121055-c057d2347007 + github.com/dolthub/vitess v0.0.0-20240209125211-6c93b0341608 github.com/fatih/color v1.13.0 github.com/gogo/protobuf v1.3.2 github.com/golang/geo v0.0.0-20200730024412-e86565bf3f35 diff --git a/go.sum b/go.sum index fd90dc921b..ca86ad3b76 100644 --- a/go.sum +++ b/go.sum @@ -214,18 +214,18 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/dolthub/dolt/go v0.40.5-0.20240207142355-cbe6b0ce7f01 h1:r/1CAcbUpAHR0jG15YqMgoribkfOURNqH60M8lITN2M= -github.com/dolthub/dolt/go v0.40.5-0.20240207142355-cbe6b0ce7f01/go.mod h1:F/oS2i85PyQgoKG4ay4joe/iv2HR9njLbyYaqJ2FSyU= -github.com/dolthub/dolt/go/gen/proto/dolt/services/eventsapi v0.0.0-20240205125942-fc7c3429f29c h1:AVAqyKKv6UVOcKr9anIe1VQItJEIcZENMtU0FF8bycM= -github.com/dolthub/dolt/go/gen/proto/dolt/services/eventsapi v0.0.0-20240205125942-fc7c3429f29c/go.mod h1:gHeHIDGU7em40EhFTliq62pExFcc1hxDTIZ9g5UqXYM= +github.com/dolthub/dolt/go v0.40.5-0.20240209132845-d391bcf946e7 h1:5SlrntR/5HMgX6EQntyphERy+CVKdh6vTlS6Kk6f1aI= +github.com/dolthub/dolt/go v0.40.5-0.20240209132845-d391bcf946e7/go.mod h1:kDOROjdsJ4GK+ooNjO0Zk5KrERKGjFfglgy3vUojBGQ= +github.com/dolthub/dolt/go/gen/proto/dolt/services/eventsapi v0.0.0-20240209132845-d391bcf946e7 h1:G+gH9yQq962klV5vVyrqkGGWl+YJYuURn6SazPnq72E= +github.com/dolthub/dolt/go/gen/proto/dolt/services/eventsapi v0.0.0-20240209132845-d391bcf946e7/go.mod h1:gHeHIDGU7em40EhFTliq62pExFcc1hxDTIZ9g5UqXYM= github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2 h1:u3PMzfF8RkKd3lB9pZ2bfn0qEG+1Gms9599cr0REMww= github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2/go.mod h1:mIEZOHnFx4ZMQeawhw9rhsj+0zwQj7adVsnBX7t+eKY= github.com/dolthub/fslock v0.0.3 h1:iLMpUIvJKMKm92+N1fmHVdxJP5NdyDK5bK7z7Ba2s2U= github.com/dolthub/fslock v0.0.3/go.mod h1:QWql+P17oAAMLnL4HGB5tiovtDuAjdDTPbuqx7bYfa0= github.com/dolthub/go-icu-regex v0.0.0-20230524105445-af7e7991c97e h1:kPsT4a47cw1+y/N5SSCkma7FhAPw7KeGmD6c9PBZW9Y= github.com/dolthub/go-icu-regex v0.0.0-20230524105445-af7e7991c97e/go.mod h1:KPUcpx070QOfJK1gNe0zx4pA5sicIK1GMikIGLKC168= -github.com/dolthub/go-mysql-server v0.17.1-0.20240207124505-c0f397a6aaca h1:tI3X4fIUTOT0N8n+GYkPNa384WlJoOBcztK5c5mBzjU= -github.com/dolthub/go-mysql-server v0.17.1-0.20240207124505-c0f397a6aaca/go.mod h1:ANK0a6tyjrZ2cOzDJT3nFsDp80xksI4UfeijFlvnjwE= +github.com/dolthub/go-mysql-server v0.17.1-0.20240209131710-527b791c74c0 h1:0LLij7LnIfIc2NuLmfxBbojo+eXBZI/r1tDgZF38a4Y= +github.com/dolthub/go-mysql-server v0.17.1-0.20240209131710-527b791c74c0/go.mod h1:OV8F/PIK3eVVDO4rRWrlXxie7n+B9vnzssJeyCCeUFg= github.com/dolthub/ishell v0.0.0-20221214210346-d7db0b066488 h1:0HHu0GWJH0N6a6keStrHhUAK5/o9LVfkh44pvsV4514= github.com/dolthub/ishell v0.0.0-20221214210346-d7db0b066488/go.mod h1:ehexgi1mPxRTk0Mok/pADALuHbvATulTh6gzr7NzZto= github.com/dolthub/jsonpath v0.0.2-0.20240201003050-392940944c15 h1:sfTETOpsrNJPDn2KydiCtDgVu6Xopq8k3JP8PjFT22s= @@ -236,8 +236,8 @@ github.com/dolthub/sqllogictest/go v0.0.0-20240118211725-a52e3f5697e3 h1:+eDpuEJ github.com/dolthub/sqllogictest/go v0.0.0-20240118211725-a52e3f5697e3/go.mod h1:e/FIZVvT2IR53HBCAo41NjqgtEnjMJGKca3Y/dAmZaA= github.com/dolthub/swiss v0.1.0 h1:EaGQct3AqeP/MjASHLiH6i4TAmgbG/c4rA6a1bzCOPc= github.com/dolthub/swiss v0.1.0/go.mod h1:BeucyB08Vb1G9tumVN3Vp/pyY4AMUnr9p7Rz7wJ7kAQ= -github.com/dolthub/vitess v0.0.0-20240207121055-c057d2347007 h1:MvFoe0FnHhxQLyp4Ldw0HRj1yu83YErbtbr7XxhaIFk= -github.com/dolthub/vitess v0.0.0-20240207121055-c057d2347007/go.mod h1:IwjNXSQPymrja5pVqmfnYdcy7Uv7eNJNBPK/MEh9OOw= +github.com/dolthub/vitess v0.0.0-20240209125211-6c93b0341608 h1:jnInva1KcJJf/QQsxbN9tTJckOZf73EzUen8rrik0Yw= +github.com/dolthub/vitess v0.0.0-20240209125211-6c93b0341608/go.mod h1:IwjNXSQPymrja5pVqmfnYdcy7Uv7eNJNBPK/MEh9OOw= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= diff --git a/main.go b/main.go index 20c3b6edef..d700661a0e 100644 --- a/main.go +++ b/main.go @@ -302,7 +302,7 @@ func runServer(ctx context.Context, dEnv *env.DoltEnv) error { // // We also emit a heartbeat event every 24 hours the server is running. // All events will be tagged with the doltgresql app id. - go emitUsageEvent(dEnv) + go emitUsageEvent(ctx, dEnv) controller, err := server.RunOnDisk(ctx, os.Args[1:], dEnv) @@ -589,22 +589,22 @@ func redirectStdio(args []string) ([]string, error) { } // emitUsageEvent emits a usage event to the event server -func emitUsageEvent(dEnv *env.DoltEnv) { +func emitUsageEvent(ctx context.Context, dEnv *env.DoltEnv) { metricsDisabled := dEnv.Config.GetStringOrDefault(config.MetricsDisabled, "false") disabled, err := strconv.ParseBool(metricsDisabled) if err != nil || disabled { return } - evt := events.NewEvent(sqlserver.SqlServerCmd{}.EventType()) - evtCollector := events.NewCollector() - evtCollector.CloseEventAndAdd(evt) - clientEvents := evtCollector.Close() - emitter, err := commands.GRPCEmitterForConfig(dEnv) if err != nil { return } - _ = emitter.LogEvents(server.Version, clientEvents) + evt := events.NewEvent(sqlserver.SqlServerCmd{}.EventType()) + evtCollector := events.NewCollector(server.Version, emitter) + evtCollector.CloseEventAndAdd(evt) + clientEvents := evtCollector.Close() + + _ = emitter.LogEvents(ctx, server.Version, clientEvents) } diff --git a/server/ast/expr.go b/server/ast/expr.go index 681a22b2c3..b26c3df307 100644 --- a/server/ast/expr.go +++ b/server/ast/expr.go @@ -25,6 +25,7 @@ import ( "github.com/dolthub/doltgresql/postgres/parser/sem/tree" "github.com/dolthub/doltgresql/postgres/parser/types" + pgexprs "github.com/dolthub/doltgresql/server/expression" pgtypes "github.com/dolthub/doltgresql/server/types" ) @@ -100,25 +101,38 @@ func nodeExpr(node tree.Expr) (vitess.Expr, error) { return nil, fmt.Errorf("ANNOTATE_TYPE is not yet supported") case *tree.Array: //TODO: right now, this only works with boolean array values for the sake of demonstration - var gmsExpr sql.Expression + var sqlChildren []sql.Expression + var unresolvedChildren []vitess.Expr + var unresolvedIndexes []int if len(node.Exprs) == 0 { if node.ResolvedType().Family() == types.ArrayFamily && node.ResolvedType().ArrayContents().Family() == types.BoolFamily { - gmsExpr = expression.NewLiteral([]bool{}, pgtypes.BoolArray) + sqlChildren = []sql.Expression{expression.NewLiteral([]bool{}, pgtypes.BoolArray)} } else { return nil, fmt.Errorf("arrays are generally not yet supported") } } else { - vals := make([]bool, len(node.Exprs)) - for i, arrayExpr := range node.Exprs { + for _, arrayExpr := range node.Exprs { if arrayVal, ok := arrayExpr.(*tree.DBool); ok && arrayVal != nil { - vals[i] = bool(*arrayVal) + sqlChildren = append(sqlChildren, expression.NewLiteral(bool(*arrayVal), pgtypes.Bool)) } else { - return nil, fmt.Errorf("array value is not yet supported") + unresolvedChild, err := nodeExpr(arrayExpr) + if err != nil { + return nil, err + } + unresolvedChildren = append(unresolvedChildren, unresolvedChild) + unresolvedIndexes = append(unresolvedIndexes, len(sqlChildren)) + sqlChildren = append(sqlChildren, nil) } } - gmsExpr = expression.NewLiteral(vals, pgtypes.BoolArray) } - return vitess.InjectedExpr{Expression: gmsExpr}, nil + arrayExpr, err := pgexprs.NewArray(sqlChildren, unresolvedChildren, unresolvedIndexes, nil) + if err != nil { + return nil, err + } + return vitess.InjectedExpr{ + Expression: arrayExpr, + Children: unresolvedChildren, + }, nil case *tree.ArrayFlatten: return nil, fmt.Errorf("flattening arrays is not yet supported") case *tree.BinaryExpr: diff --git a/server/expression/array.go b/server/expression/array.go new file mode 100644 index 0000000000..4e2d3ee4a0 --- /dev/null +++ b/server/expression/array.go @@ -0,0 +1,163 @@ +// Copyright 2024 Dolthub, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package expression + +import ( + "fmt" + "strings" + + "github.com/dolthub/go-mysql-server/sql" + vitess "github.com/dolthub/vitess/go/vt/sqlparser" + + pgtypes "github.com/dolthub/doltgresql/server/types" +) + +// Array represents an ARRAY[...] expression. +type Array struct { + sqlChildren []sql.Expression + vitessChildren vitess.Exprs + vitessIndexes []int + coercedType sql.Type +} + +var _ vitess.InjectableExpression = (*Array)(nil) +var _ sql.Expression = (*Array)(nil) + +// NewArray returns a new *Array. +func NewArray(expressions []sql.Expression, unresolvedChildren vitess.Exprs, unresolvedIndexes []int, coercedType sql.Type) (*Array, error) { + if len(unresolvedChildren) != len(unresolvedIndexes) { + return nil, fmt.Errorf("ARRAY has an invalid number of unresolved children (%d) and indexes (%d)", + len(unresolvedChildren), len(unresolvedIndexes)) + } + for _, index := range unresolvedIndexes { + if index >= len(expressions) || index < 0 { + return nil, fmt.Errorf("ARRAY unresolved index (%d) is out of bounds (expression count: %d)", + index, len(expressions)) + } + if expressions[index] != nil { + return nil, fmt.Errorf("ARRAY unresolved index (%d) points to a resolved expression", index) + } + } + return &Array{ + sqlChildren: expressions, + vitessChildren: unresolvedChildren, + vitessIndexes: unresolvedIndexes, + coercedType: coercedType, + }, nil +} + +// Children implements the sql.Expression interface. +func (array *Array) Children() []sql.Expression { + return array.sqlChildren +} + +// Eval implements the sql.Expression interface. +func (array *Array) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) { + // TODO: make this an actual implementation instead of the mock boolean implementation + values := make([]bool, len(array.sqlChildren)) + for i, expr := range array.sqlChildren { + val, err := expr.Eval(ctx, row) + if err != nil { + return nil, err + } + boolVal, ok := val.(bool) + if !ok { + return nil, fmt.Errorf("only boolean values are supported within an ARRAY for now") + } + values[i] = boolVal + } + return values, nil +} + +// IsNullable implements the sql.Expression interface. +func (array *Array) IsNullable() bool { + // TODO: verify if this is actually nullable + return false +} + +// Resolved implements the sql.Expression interface. +func (array *Array) Resolved() bool { + for _, child := range array.sqlChildren { + if child == nil || !child.Resolved() { + return false + } + } + return true +} + +// String implements the sql.Expression interface. +func (array *Array) String() string { + sb := strings.Builder{} + sb.WriteString("ARRAY[") + for i, child := range array.sqlChildren { + if i > 0 { + sb.WriteString(", ") + } + if child == nil { + sb.WriteString("...") + } else { + sb.WriteString(child.String()) + } + } + sb.WriteRune(']') + return sb.String() +} + +// Type implements the sql.Expression interface. +func (array *Array) Type() sql.Type { + if array.coercedType != nil { + return array.coercedType + } + // TODO: how do we handle multiple children with different types? + for _, child := range array.sqlChildren { + if child != nil { + childType := child.Type() + if _, ok := childType.(pgtypes.BoolType); ok { + return pgtypes.BoolArray + } + } + } + // TODO: remove mock boolean array demonstration + return pgtypes.BoolArray +} + +// WithChildren implements the sql.Expression interface. +func (array *Array) WithChildren(children ...sql.Expression) (sql.Expression, error) { + return &Array{ + sqlChildren: children, + coercedType: array.coercedType, + }, nil +} + +// WithResolvedChildren implements the vitess.InjectableExpression interface. +func (array *Array) WithResolvedChildren(children []any) (any, error) { + if len(children) != len(array.vitessIndexes) { + return nil, fmt.Errorf("invalid vitess child count, expected `%d` but got `%d`", + len(array.vitessIndexes), len(children)) + } + newExpressions := make([]sql.Expression, len(array.sqlChildren)) + copy(newExpressions, array.sqlChildren) + for i, resolvedChild := range children { + resolvedExpression, ok := resolvedChild.(sql.Expression) + if !ok { + return nil, fmt.Errorf("expected vitess child to be an expression but has type `%T`", resolvedChild) + } + newExpressions[array.vitessIndexes[i]] = resolvedExpression + } + return &Array{ + sqlChildren: newExpressions, + coercedType: array.coercedType, + }, nil +} diff --git a/testing/go/smoke_test.go b/testing/go/smoke_test.go index d298d42920..791139a147 100644 --- a/testing/go/smoke_test.go +++ b/testing/go/smoke_test.go @@ -153,6 +153,29 @@ func TestSmokeTests(t *testing.T) { }, }, }, + { + Name: "ARRAY expression", + SetUpScript: []string{ + "CREATE TABLE test (id INTEGER primary key, v1 BOOLEAN);", + "INSERT INTO test VALUES (1, 'true'), (2, 'false');", + }, + Assertions: []ScriptTestAssertion{ + { + Query: "SELECT ARRAY[v1] FROM test ORDER BY id;", + Expected: []sql.Row{ + {"{t}"}, + {"{f}"}, + }, + }, + { + Query: "SELECT ARRAY[v1, true, v1] FROM test ORDER BY id;", + Expected: []sql.Row{ + {"{t,t,t}"}, + {"{f,t,f}"}, + }, + }, + }, + }, { Name: "Empty statement", Assertions: []ScriptTestAssertion{