From 091356303b237b2feb8a0759748c32b724aaf86b Mon Sep 17 00:00:00 2001 From: c-r-dev Date: Fri, 6 Dec 2024 14:33:10 -0500 Subject: [PATCH 01/12] added a new between vindex interface Signed-off-by: c-r-dev --- examples/vtexplain/sample_schema.sql | 5 +++ examples/vtexplain/sample_vschema.json | 20 +++++++++ go/vt/key/key.go | 5 +++ go/vt/vtgate/engine/routing.go | 43 +++++++++++++++++++ .../planbuilder/operators/sharded_routing.go | 21 +++++++++ go/vt/vtgate/vindexes/binary.go | 15 +++++++ go/vt/vtgate/vindexes/binary_test.go | 15 +++++++ go/vt/vtgate/vindexes/numeric.go | 15 +++++++ go/vt/vtgate/vindexes/vindex.go | 7 +++ 9 files changed, 146 insertions(+) create mode 100644 examples/vtexplain/sample_schema.sql create mode 100644 examples/vtexplain/sample_vschema.json diff --git a/examples/vtexplain/sample_schema.sql b/examples/vtexplain/sample_schema.sql new file mode 100644 index 00000000000..b57b6beb961 --- /dev/null +++ b/examples/vtexplain/sample_schema.sql @@ -0,0 +1,5 @@ +CREATE TABLE IF NOT EXISTS `t1` ( + `c1` bigint unsigned NOT NULL, + `c2` bigint unsigned NOT NULL, + PRIMARY KEY (`c1`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; \ No newline at end of file diff --git a/examples/vtexplain/sample_vschema.json b/examples/vtexplain/sample_vschema.json new file mode 100644 index 00000000000..14e54eb6682 --- /dev/null +++ b/examples/vtexplain/sample_vschema.json @@ -0,0 +1,20 @@ +{ + "ks1": { + "sharded": true, + "vindexes": { + "binary_vdx": { + "type": "binary" + } + }, + "tables": { + "t1": { + "columnVindexes": [ + { + "column": "c1", + "name": "binary_vdx" + } + ] + } + } + } +} \ No newline at end of file diff --git a/go/vt/key/key.go b/go/vt/key/key.go index dcdcda47f81..89d956bd433 100644 --- a/go/vt/key/key.go +++ b/go/vt/key/key.go @@ -90,6 +90,11 @@ func Empty(id []byte) bool { // KeyRange helper methods // +// Make a Key Range +func NewKeyRange(start []byte, end []byte) *topodatapb.KeyRange { + return &topodatapb.KeyRange{Start: start, End: end} +} + // KeyRangeAdd adds two adjacent KeyRange values (in any order) into a single value. If the values are not adjacent, // it returns false. func KeyRangeAdd(a, b *topodatapb.KeyRange) (*topodatapb.KeyRange, bool) { diff --git a/go/vt/vtgate/engine/routing.go b/go/vt/vtgate/engine/routing.go index e05366c4aeb..3159c83b293 100644 --- a/go/vt/vtgate/engine/routing.go +++ b/go/vt/vtgate/engine/routing.go @@ -51,6 +51,9 @@ const ( // IN is for routing a statement to a multi shard. // Requires: A Vindex, and a multi Values. IN + // Between is for routing a statement to a multi shard + // Requires: A Vindex, and a multi Values. + Between // MultiEqual is used for routing queries with IN with tuple clause // Requires: A Vindex, and a multi Tuple Values. MultiEqual @@ -78,6 +81,7 @@ var opName = map[Opcode]string{ EqualUnique: "EqualUnique", Equal: "Equal", IN: "IN", + Between: "Between", MultiEqual: "MultiEqual", Scatter: "Scatter", DBA: "DBA", @@ -157,6 +161,14 @@ func (rp *RoutingParameters) findRoute(ctx context.Context, vcursor VCursor, bin default: return rp.in(ctx, vcursor, bindVars) } + case Between: + switch rp.Vindex.(type) { + case vindexes.SingleColumn: + return rp.between(ctx, vcursor, bindVars) + default: + // Only SingleColumn vindex supported. + return nil, nil, vterrors.Errorf(vtrpcpb.Code_INTERNAL, "opcode: %v not supported", rp.Opcode) + } case MultiEqual: switch rp.Vindex.(type) { case vindexes.MultiColumn: @@ -396,6 +408,19 @@ func (rp *RoutingParameters) inMultiCol(ctx context.Context, vcursor VCursor, bi return rss, shardVarsMultiCol(bindVars, mapVals, isSingleVal), nil } +func (rp *RoutingParameters) between(ctx context.Context, vcursor VCursor, bindVars map[string]*querypb.BindVariable) ([]*srvtopo.ResolvedShard, []map[string]*querypb.BindVariable, error) { + env := evalengine.NewExpressionEnv(ctx, bindVars, vcursor) + value, err := env.Evaluate(rp.Values[0]) + if err != nil { + return nil, nil, err + } + rss, values, err := resolveShardsBetween(ctx, vcursor, rp.Vindex.(vindexes.Between), rp.Keyspace, value.TupleValues()) + if err != nil { + return nil, nil, err + } + return rss, shardVars(bindVars, values), nil +} + func (rp *RoutingParameters) multiEqual(ctx context.Context, vcursor VCursor, bindVars map[string]*querypb.BindVariable) ([]*srvtopo.ResolvedShard, []map[string]*querypb.BindVariable, error) { env := evalengine.NewExpressionEnv(ctx, bindVars, vcursor) value, err := env.Evaluate(rp.Values[0]) @@ -520,6 +545,24 @@ func buildMultiColumnVindexValues(shardsValues [][][]sqltypes.Value) [][][]*quer return shardsIds } +func resolveShardsBetween(ctx context.Context, vcursor VCursor, vindex vindexes.Between, keyspace *vindexes.Keyspace, vindexKeys []sqltypes.Value) ([]*srvtopo.ResolvedShard, [][]*querypb.Value, error) { + // Convert vindexKeys to []*querypb.Value + ids := make([]*querypb.Value, len(vindexKeys)) + for i, vik := range vindexKeys { + ids[i] = sqltypes.ValueToProto(vik) + } + + // RangeMap using the Vindex + destinations, err := vindex.RangeMap(ctx, vcursor, vindexKeys[0], vindexKeys[1]) + if err != nil { + return nil, nil, err + + } + + // And use the Resolver to map to ResolvedShards. + return vcursor.ResolveDestinations(ctx, keyspace.Name, ids, destinations) +} + func shardVars(bv map[string]*querypb.BindVariable, mapVals [][]*querypb.Value) []map[string]*querypb.BindVariable { shardVars := make([]map[string]*querypb.BindVariable, len(mapVals)) for i, vals := range mapVals { diff --git a/go/vt/vtgate/planbuilder/operators/sharded_routing.go b/go/vt/vtgate/planbuilder/operators/sharded_routing.go index 066cb47d9a9..961e3710e12 100644 --- a/go/vt/vtgate/planbuilder/operators/sharded_routing.go +++ b/go/vt/vtgate/planbuilder/operators/sharded_routing.go @@ -223,6 +223,9 @@ func (tr *ShardedRouting) resetRoutingLogic(ctx *plancontext.PlanningContext) Ro func (tr *ShardedRouting) searchForNewVindexes(ctx *plancontext.PlanningContext, predicate sqlparser.Expr) (Routing, bool) { newVindexFound := false switch node := predicate.(type) { + case *sqlparser.BetweenExpr: + return tr.planBetweenOp(ctx, node) + case *sqlparser.ComparisonExpr: return tr.planComparison(ctx, node) @@ -234,6 +237,24 @@ func (tr *ShardedRouting) searchForNewVindexes(ctx *plancontext.PlanningContext, return nil, newVindexFound } +func (tr *ShardedRouting) planBetweenOp(ctx *plancontext.PlanningContext, node *sqlparser.BetweenExpr) (routing Routing, foundNew bool) { + // x BETWEEN a AND b => x >= a AND x <= b + column, ok := node.Left.(*sqlparser.ColName) + if !ok { + return nil, false + } + vals := []sqlparser.Expr{} + vals = append(vals, node.From, node.To) + var vdValue sqlparser.ValTuple = vals + opcode := func(*vindexes.ColumnVindex) engine.Opcode { return engine.Between } + + val := makeEvalEngineExpr(ctx, vdValue) + if val == nil { + return nil, false + } + return nil, tr.haveMatchingVindex(ctx, node, vdValue, column, val, opcode, justTheVindex) +} + func (tr *ShardedRouting) planComparison(ctx *plancontext.PlanningContext, cmp *sqlparser.ComparisonExpr) (routing Routing, foundNew bool) { switch cmp.Operator { case sqlparser.EqualOp: diff --git a/go/vt/vtgate/vindexes/binary.go b/go/vt/vtgate/vindexes/binary.go index b78451ca1fb..69701e3817f 100644 --- a/go/vt/vtgate/vindexes/binary.go +++ b/go/vt/vtgate/vindexes/binary.go @@ -30,6 +30,7 @@ var ( _ Reversible = (*Binary)(nil) _ Hashing = (*Binary)(nil) _ ParamValidating = (*Binary)(nil) + _ Between = (*Binary)(nil) ) // Binary is a vindex that converts binary bits to a keyspace id. @@ -108,6 +109,20 @@ func (*Binary) ReverseMap(_ VCursor, ksids [][]byte) ([]sqltypes.Value, error) { return reverseIds, nil } +// RangeMap can map ids to key.Destination objects. +func (vind *Binary) RangeMap(ctx context.Context, vcursor VCursor, startId sqltypes.Value, endId sqltypes.Value) ([]key.Destination, error) { + startKsId, err := vind.Hash(startId) + if err != nil { + return nil, err + } + endKsId, err := vind.Hash(endId) + if err != nil { + return nil, err + } + out := []key.Destination{&key.DestinationKeyRange{KeyRange: key.NewKeyRange(startKsId, endKsId)}} + return out, nil +} + // UnknownParams implements the ParamValidating interface. func (vind *Binary) UnknownParams() []string { return vind.unknownParams diff --git a/go/vt/vtgate/vindexes/binary_test.go b/go/vt/vtgate/vindexes/binary_test.go index 27ae6ceca11..b9307b390a0 100644 --- a/go/vt/vtgate/vindexes/binary_test.go +++ b/go/vt/vtgate/vindexes/binary_test.go @@ -145,3 +145,18 @@ func TestBinaryReverseMap(t *testing.T) { t.Errorf("ReverseMap(): %v, want %s", err, wantErr) } } + +func TestBinaryRangeMap(t *testing.T) { + + startInterval := "0x01" + endInterval := "0x10" + + got, err := binOnlyVindex.(Between).RangeMap(context.Background(), nil, sqltypes.NewHexNum([]byte(startInterval)), + sqltypes.NewHexNum([]byte(endInterval))) + require.NoError(t, err) + want := "DestinationKeyRange(01-10)" + if !reflect.DeepEqual(got[0].String(), want) { + t.Errorf("RangeMap(): %+v, want %+v", got, want) + } + +} diff --git a/go/vt/vtgate/vindexes/numeric.go b/go/vt/vtgate/vindexes/numeric.go index 091807ec2cc..3de5cb83dc0 100644 --- a/go/vt/vtgate/vindexes/numeric.go +++ b/go/vt/vtgate/vindexes/numeric.go @@ -31,6 +31,7 @@ var ( _ Reversible = (*Numeric)(nil) _ Hashing = (*Numeric)(nil) _ ParamValidating = (*Numeric)(nil) + _ Between = (*Numeric)(nil) ) // Numeric defines a bit-pattern mapping of a uint64 to the KeyspaceId. @@ -108,6 +109,20 @@ func (*Numeric) ReverseMap(_ VCursor, ksids [][]byte) ([]sqltypes.Value, error) return reverseIds, nil } +// RangeMap implements Between. +func (vind *Numeric) RangeMap(ctx context.Context, vcursor VCursor, startId sqltypes.Value, endId sqltypes.Value) ([]key.Destination, error) { + startKsId, err := vind.Hash(startId) + if err != nil { + return nil, err + } + endKsId, err := vind.Hash(endId) + if err != nil { + return nil, err + } + out := []key.Destination{&key.DestinationKeyRange{KeyRange: key.NewKeyRange(startKsId, endKsId)}} + return out, nil +} + // UnknownParams implements the ParamValidating interface. func (vind *Numeric) UnknownParams() []string { return vind.unknownParams diff --git a/go/vt/vtgate/vindexes/vindex.go b/go/vt/vtgate/vindexes/vindex.go index e3d5a6d7e4d..75bc1d879ed 100644 --- a/go/vt/vtgate/vindexes/vindex.go +++ b/go/vt/vtgate/vindexes/vindex.go @@ -130,6 +130,13 @@ type ( ReverseMap(vcursor VCursor, ks [][]byte) ([]sqltypes.Value, error) } + // A Between vindex is an optional interface one that maps to a keyspace range + // instead of a single keyspace id. It's being used to reduce the fan out for + // 'BETWEEN' expressions. + Between interface { + RangeMap(ctx context.Context, vcursor VCursor, startId sqltypes.Value, endId sqltypes.Value) ([]key.Destination, error) + } + // A Prefixable vindex is one that maps the prefix of a id to a keyspace range // instead of a single keyspace id. It's being used to reduced the fan out for // 'LIKE' expressions. From 40d041f28f5b371854b4538ccc915b0a32f0b79d Mon Sep 17 00:00:00 2001 From: c-r-dev Date: Tue, 10 Dec 2024 16:18:03 -0500 Subject: [PATCH 02/12] renamed between interface to sequential and incorporated other review feedback Signed-off-by: c-r-dev --- go/vt/vtgate/engine/routing.go | 8 ++++---- .../planbuilder/operators/sharded_routing.go | 15 ++++++++++----- go/vt/vtgate/vindexes/binary.go | 2 +- go/vt/vtgate/vindexes/binary_test.go | 7 +++++-- go/vt/vtgate/vindexes/numeric.go | 2 +- go/vt/vtgate/vindexes/vindex.go | 4 ++-- 6 files changed, 23 insertions(+), 15 deletions(-) diff --git a/go/vt/vtgate/engine/routing.go b/go/vt/vtgate/engine/routing.go index 3159c83b293..067278c1a93 100644 --- a/go/vt/vtgate/engine/routing.go +++ b/go/vt/vtgate/engine/routing.go @@ -52,7 +52,7 @@ const ( // Requires: A Vindex, and a multi Values. IN // Between is for routing a statement to a multi shard - // Requires: A Vindex, and a multi Values. + // Requires: A Vindex, and start and end Value. Between // MultiEqual is used for routing queries with IN with tuple clause // Requires: A Vindex, and a multi Tuple Values. @@ -167,7 +167,7 @@ func (rp *RoutingParameters) findRoute(ctx context.Context, vcursor VCursor, bin return rp.between(ctx, vcursor, bindVars) default: // Only SingleColumn vindex supported. - return nil, nil, vterrors.Errorf(vtrpcpb.Code_INTERNAL, "opcode: %v not supported", rp.Opcode) + return nil, nil, vterrors.VT13001("between supported on SingleColumn vindex only") } case MultiEqual: switch rp.Vindex.(type) { @@ -414,7 +414,7 @@ func (rp *RoutingParameters) between(ctx context.Context, vcursor VCursor, bindV if err != nil { return nil, nil, err } - rss, values, err := resolveShardsBetween(ctx, vcursor, rp.Vindex.(vindexes.Between), rp.Keyspace, value.TupleValues()) + rss, values, err := resolveShardsBetween(ctx, vcursor, rp.Vindex.(vindexes.Sequential), rp.Keyspace, value.TupleValues()) if err != nil { return nil, nil, err } @@ -545,7 +545,7 @@ func buildMultiColumnVindexValues(shardsValues [][][]sqltypes.Value) [][][]*quer return shardsIds } -func resolveShardsBetween(ctx context.Context, vcursor VCursor, vindex vindexes.Between, keyspace *vindexes.Keyspace, vindexKeys []sqltypes.Value) ([]*srvtopo.ResolvedShard, [][]*querypb.Value, error) { +func resolveShardsBetween(ctx context.Context, vcursor VCursor, vindex vindexes.Sequential, keyspace *vindexes.Keyspace, vindexKeys []sqltypes.Value) ([]*srvtopo.ResolvedShard, [][]*querypb.Value, error) { // Convert vindexKeys to []*querypb.Value ids := make([]*querypb.Value, len(vindexKeys)) for i, vik := range vindexKeys { diff --git a/go/vt/vtgate/planbuilder/operators/sharded_routing.go b/go/vt/vtgate/planbuilder/operators/sharded_routing.go index 961e3710e12..5432305136a 100644 --- a/go/vt/vtgate/planbuilder/operators/sharded_routing.go +++ b/go/vt/vtgate/planbuilder/operators/sharded_routing.go @@ -238,21 +238,26 @@ func (tr *ShardedRouting) searchForNewVindexes(ctx *plancontext.PlanningContext, } func (tr *ShardedRouting) planBetweenOp(ctx *plancontext.PlanningContext, node *sqlparser.BetweenExpr) (routing Routing, foundNew bool) { - // x BETWEEN a AND b => x >= a AND x <= b column, ok := node.Left.(*sqlparser.ColName) if !ok { return nil, false } - vals := []sqlparser.Expr{} - vals = append(vals, node.From, node.To) - var vdValue sqlparser.ValTuple = vals + var vdValue sqlparser.ValTuple = sqlparser.ValTuple([]sqlparser.Expr{node.From, node.To}) opcode := func(*vindexes.ColumnVindex) engine.Opcode { return engine.Between } + sequentialVdx := func(vindex *vindexes.ColumnVindex) vindexes.Vindex { + if _, ok := vindex.Vindex.(vindexes.Sequential); ok { + return vindex.Vindex + } + // if vindex is not of type Sequential, we can't use this vindex at all + return nil + } + val := makeEvalEngineExpr(ctx, vdValue) if val == nil { return nil, false } - return nil, tr.haveMatchingVindex(ctx, node, vdValue, column, val, opcode, justTheVindex) + return nil, tr.haveMatchingVindex(ctx, node, vdValue, column, val, opcode, sequentialVdx) } func (tr *ShardedRouting) planComparison(ctx *plancontext.PlanningContext, cmp *sqlparser.ComparisonExpr) (routing Routing, foundNew bool) { diff --git a/go/vt/vtgate/vindexes/binary.go b/go/vt/vtgate/vindexes/binary.go index 69701e3817f..96a72b2c3f4 100644 --- a/go/vt/vtgate/vindexes/binary.go +++ b/go/vt/vtgate/vindexes/binary.go @@ -30,7 +30,7 @@ var ( _ Reversible = (*Binary)(nil) _ Hashing = (*Binary)(nil) _ ParamValidating = (*Binary)(nil) - _ Between = (*Binary)(nil) + _ Sequential = (*Binary)(nil) ) // Binary is a vindex that converts binary bits to a keyspace id. diff --git a/go/vt/vtgate/vindexes/binary_test.go b/go/vt/vtgate/vindexes/binary_test.go index b9307b390a0..775dcb3a5ce 100644 --- a/go/vt/vtgate/vindexes/binary_test.go +++ b/go/vt/vtgate/vindexes/binary_test.go @@ -24,6 +24,7 @@ import ( "reflect" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "vitess.io/vitess/go/sqltypes" @@ -146,16 +147,18 @@ func TestBinaryReverseMap(t *testing.T) { } } +// TestBinaryRangeMap takes start and env values, +// and checks against a destination keyrange. func TestBinaryRangeMap(t *testing.T) { startInterval := "0x01" endInterval := "0x10" - got, err := binOnlyVindex.(Between).RangeMap(context.Background(), nil, sqltypes.NewHexNum([]byte(startInterval)), + got, err := binOnlyVindex.(Sequential).RangeMap(context.Background(), nil, sqltypes.NewHexNum([]byte(startInterval)), sqltypes.NewHexNum([]byte(endInterval))) require.NoError(t, err) want := "DestinationKeyRange(01-10)" - if !reflect.DeepEqual(got[0].String(), want) { + if !assert.Equal(t, got[0].String(), want) { t.Errorf("RangeMap(): %+v, want %+v", got, want) } diff --git a/go/vt/vtgate/vindexes/numeric.go b/go/vt/vtgate/vindexes/numeric.go index 3de5cb83dc0..b40df13a997 100644 --- a/go/vt/vtgate/vindexes/numeric.go +++ b/go/vt/vtgate/vindexes/numeric.go @@ -31,7 +31,7 @@ var ( _ Reversible = (*Numeric)(nil) _ Hashing = (*Numeric)(nil) _ ParamValidating = (*Numeric)(nil) - _ Between = (*Numeric)(nil) + _ Sequential = (*Numeric)(nil) ) // Numeric defines a bit-pattern mapping of a uint64 to the KeyspaceId. diff --git a/go/vt/vtgate/vindexes/vindex.go b/go/vt/vtgate/vindexes/vindex.go index 75bc1d879ed..947877108e0 100644 --- a/go/vt/vtgate/vindexes/vindex.go +++ b/go/vt/vtgate/vindexes/vindex.go @@ -130,10 +130,10 @@ type ( ReverseMap(vcursor VCursor, ks [][]byte) ([]sqltypes.Value, error) } - // A Between vindex is an optional interface one that maps to a keyspace range + // A Sequential vindex is an optional interface one that maps to a keyspace range // instead of a single keyspace id. It's being used to reduce the fan out for // 'BETWEEN' expressions. - Between interface { + Sequential interface { RangeMap(ctx context.Context, vcursor VCursor, startId sqltypes.Value, endId sqltypes.Value) ([]key.Destination, error) } From 99ea8b57970d391f5beea49c4abcb583c9b57eb1 Mon Sep 17 00:00:00 2001 From: c-r-dev Date: Tue, 10 Dec 2024 20:32:24 -0500 Subject: [PATCH 03/12] planBetweenOp to check if vindex is of Sequential type Signed-off-by: c-r-dev --- go/vt/vtgate/planbuilder/operators/sharded_routing.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/go/vt/vtgate/planbuilder/operators/sharded_routing.go b/go/vt/vtgate/planbuilder/operators/sharded_routing.go index 5432305136a..0e28e8342c3 100644 --- a/go/vt/vtgate/planbuilder/operators/sharded_routing.go +++ b/go/vt/vtgate/planbuilder/operators/sharded_routing.go @@ -243,7 +243,13 @@ func (tr *ShardedRouting) planBetweenOp(ctx *plancontext.PlanningContext, node * return nil, false } var vdValue sqlparser.ValTuple = sqlparser.ValTuple([]sqlparser.Expr{node.From, node.To}) - opcode := func(*vindexes.ColumnVindex) engine.Opcode { return engine.Between } + + opcode := func(vindex *vindexes.ColumnVindex) engine.Opcode { + if _, ok := vindex.Vindex.(vindexes.Sequential); ok { + return engine.Between + } + return engine.Scatter + } sequentialVdx := func(vindex *vindexes.ColumnVindex) vindexes.Vindex { if _, ok := vindex.Vindex.(vindexes.Sequential); ok { From e8593a8cd99353f06b10a74a89774589d480b10a Mon Sep 17 00:00:00 2001 From: c-r-dev Date: Tue, 10 Dec 2024 20:37:13 -0500 Subject: [PATCH 04/12] added plan_test for between clause (and cleaned up unused example schema files) Signed-off-by: c-r-dev --- examples/vtexplain/sample_schema.sql | 5 - examples/vtexplain/sample_vschema.json | 20 ---- go/vt/vtgate/planbuilder/plan_test.go | 2 + .../planbuilder/testdata/filter_cases.json | 95 +++++++++++++++++++ .../planbuilder/testdata/vschemas/schema.json | 55 +++++++++++ 5 files changed, 152 insertions(+), 25 deletions(-) delete mode 100644 examples/vtexplain/sample_schema.sql delete mode 100644 examples/vtexplain/sample_vschema.json diff --git a/examples/vtexplain/sample_schema.sql b/examples/vtexplain/sample_schema.sql deleted file mode 100644 index b57b6beb961..00000000000 --- a/examples/vtexplain/sample_schema.sql +++ /dev/null @@ -1,5 +0,0 @@ -CREATE TABLE IF NOT EXISTS `t1` ( - `c1` bigint unsigned NOT NULL, - `c2` bigint unsigned NOT NULL, - PRIMARY KEY (`c1`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; \ No newline at end of file diff --git a/examples/vtexplain/sample_vschema.json b/examples/vtexplain/sample_vschema.json deleted file mode 100644 index 14e54eb6682..00000000000 --- a/examples/vtexplain/sample_vschema.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "ks1": { - "sharded": true, - "vindexes": { - "binary_vdx": { - "type": "binary" - } - }, - "tables": { - "t1": { - "columnVindexes": [ - { - "column": "c1", - "name": "binary_vdx" - } - ] - } - } - } -} \ No newline at end of file diff --git a/go/vt/vtgate/planbuilder/plan_test.go b/go/vt/vtgate/planbuilder/plan_test.go index ccbc9821170..da44fb84687 100644 --- a/go/vt/vtgate/planbuilder/plan_test.go +++ b/go/vt/vtgate/planbuilder/plan_test.go @@ -84,6 +84,8 @@ func (s *planTestSuite) TestPlan() { s.addPKsProvided(vschema, "user", []string{"user_extra"}, []string{"id", "user_id"}) s.addPKsProvided(vschema, "ordering", []string{"order"}, []string{"oid", "region_id"}) s.addPKsProvided(vschema, "ordering", []string{"order_event"}, []string{"oid", "ename"}) + s.addPKsProvided(vschema, "sequential", []string{"seq_tbl"}, []string{"id"}) + s.addPKsProvided(vschema, "sequential", []string{"seq_multicol_tbl"}, []string{"cola", "colb"}) // You will notice that some tests expect user.Id instead of user.id. // This is because we now pre-create vindex columns in the symbol diff --git a/go/vt/vtgate/planbuilder/testdata/filter_cases.json b/go/vt/vtgate/planbuilder/testdata/filter_cases.json index 4194a369bd6..3e1e3614080 100644 --- a/go/vt/vtgate/planbuilder/testdata/filter_cases.json +++ b/go/vt/vtgate/planbuilder/testdata/filter_cases.json @@ -4820,5 +4820,100 @@ "user.authoritative" ] } + }, + { + "comment": "Between clause on primary indexed id column (binary vindex on id)", + "query": "select id from seq_tbl where id between 1 and 5", + "plan": { + "QueryType": "SELECT", + "Original": "select id from seq_tbl where id between 1 and 5", + "Instructions": { + "OperatorType": "Route", + "Variant": "Between", + "Keyspace": { + "Name": "sequential", + "Sharded": true + }, + "FieldQuery": "select id from seq_tbl where 1 != 1", + "Query": "select id from seq_tbl where id between 1 and 5", + "Table": "seq_tbl", + "Values": [ + "(1, 5)" + ], + "Vindex": "binary" + }, + "TablesUsed": [ + "sequential.seq_tbl" + ] + } + }, +{ + "comment": "Between clause on group_id column (xxhash vindex on group_id)", + "query": "select id, group_id from seq_tbl where group_id between 1 and 5", + "plan": { + "QueryType": "SELECT", + "Original": "select id, group_id from seq_tbl where group_id between 1 and 5", + "Instructions": { + "OperatorType": "Route", + "Variant": "Scatter", + "Keyspace": { + "Name": "sequential", + "Sharded": true + }, + "FieldQuery": "select id, group_id from seq_tbl where 1 != 1", + "Query": "select id, group_id from seq_tbl where group_id between 1 and 5", + "Table": "seq_tbl" + }, + "TablesUsed": [ + "sequential.seq_tbl" + ] + } + }, +{ + "comment": "Between clause on col2 column (there is no vindex on this column)", + "query": "select id, col2 from seq_tbl where col2 between 10 and 50", + "plan": { + "QueryType": "SELECT", + "Original": "select id, col2 from seq_tbl where col2 between 10 and 50", + "Instructions": { + "OperatorType": "Route", + "Variant": "Scatter", + "Keyspace": { + "Name": "sequential", + "Sharded": true + }, + "FieldQuery": "select id, col2 from seq_tbl where 1 != 1", + "Query": "select id, col2 from seq_tbl where col2 between 10 and 50", + "Table": "seq_tbl" + }, + "TablesUsed": [ + "sequential.seq_tbl" + ] + } +}, +{ + "comment": "Between clause on multicolumn vindex (cola,colb)", + "query": "select cola,colb,col1 from seq_multicol_tbl where cola between 1 and 5", + "plan": { + "QueryType": "SELECT", + "Original": "select cola,colb,col1 from seq_multicol_tbl where cola between 1 and 5", + "Instructions": { + "OperatorType": "Route", + "Variant": "Scatter", + "Keyspace": { + "Name": "sequential", + "Sharded": true + }, + "FieldQuery": "select cola, colb, col1 from seq_multicol_tbl where 1 != 1", + "Query": "select cola, colb, col1 from seq_multicol_tbl where cola between 1 and 5", + "Table": "seq_multicol_tbl", + "Values": [ + "(1, 5)" + ] + }, + "TablesUsed": [ + "sequential.seq_multicol_tbl" + ] } +} ] diff --git a/go/vt/vtgate/planbuilder/testdata/vschemas/schema.json b/go/vt/vtgate/planbuilder/testdata/vschemas/schema.json index 4fe275f2398..d4381d29ca0 100644 --- a/go/vt/vtgate/planbuilder/testdata/vschemas/schema.json +++ b/go/vt/vtgate/planbuilder/testdata/vschemas/schema.json @@ -948,6 +948,61 @@ ] } } + }, + "sequential": { + "sharded": true, + "vindexes": { + "xxhash": { + "type": "xxhash" + }, + "binary": { + "type": "binary" + }, + "multicolIdx": { + "type": "multiCol_test" + } + }, + "tables": { + "seq_tbl": { + "column_vindexes": [ + { + "column": "id", + "name": "binary" + }, + { + "column": "group_id", + "name": "xxhash" + } + ], + "columns": [ + { + "name": "col1", + "type": "VARCHAR" + }, + { + "name": "col2", + "type": "INT16" + } + ] + }, + "seq_multicol_tbl": { + "column_vindexes": [ + { + "columns": [ + "cola", + "colb" + ], + "name": "multicolIdx" + } + ], + "columns" : [ + { + "name": "col1", + "type": "INT16" + } + ] + } + } } } } From 5a7c8f1affcb237372da8fcd1a189778d0effef8 Mon Sep 17 00:00:00 2001 From: c-r-dev Date: Wed, 11 Dec 2024 09:54:44 -0500 Subject: [PATCH 05/12] modified test cases and reused existing test schema user Signed-off-by: c-r-dev --- go/vt/vtgate/planbuilder/plan_test.go | 2 - .../planbuilder/testdata/filter_cases.json | 60 ++++++++-------- .../planbuilder/testdata/vschemas/schema.json | 72 +++++-------------- go/vt/vtgate/vindexes/binary_test.go | 4 +- 4 files changed, 48 insertions(+), 90 deletions(-) diff --git a/go/vt/vtgate/planbuilder/plan_test.go b/go/vt/vtgate/planbuilder/plan_test.go index da44fb84687..ccbc9821170 100644 --- a/go/vt/vtgate/planbuilder/plan_test.go +++ b/go/vt/vtgate/planbuilder/plan_test.go @@ -84,8 +84,6 @@ func (s *planTestSuite) TestPlan() { s.addPKsProvided(vschema, "user", []string{"user_extra"}, []string{"id", "user_id"}) s.addPKsProvided(vschema, "ordering", []string{"order"}, []string{"oid", "region_id"}) s.addPKsProvided(vschema, "ordering", []string{"order_event"}, []string{"oid", "ename"}) - s.addPKsProvided(vschema, "sequential", []string{"seq_tbl"}, []string{"id"}) - s.addPKsProvided(vschema, "sequential", []string{"seq_multicol_tbl"}, []string{"cola", "colb"}) // You will notice that some tests expect user.Id instead of user.id. // This is because we now pre-create vindex columns in the symbol diff --git a/go/vt/vtgate/planbuilder/testdata/filter_cases.json b/go/vt/vtgate/planbuilder/testdata/filter_cases.json index 3e1e3614080..c46bdd4e585 100644 --- a/go/vt/vtgate/planbuilder/testdata/filter_cases.json +++ b/go/vt/vtgate/planbuilder/testdata/filter_cases.json @@ -4823,96 +4823,96 @@ }, { "comment": "Between clause on primary indexed id column (binary vindex on id)", - "query": "select id from seq_tbl where id between 1 and 5", + "query": "select id from unq_binary_idx where id between 1 and 5", "plan": { "QueryType": "SELECT", - "Original": "select id from seq_tbl where id between 1 and 5", + "Original": "select id from unq_binary_idx where id between 1 and 5", "Instructions": { "OperatorType": "Route", "Variant": "Between", "Keyspace": { - "Name": "sequential", + "Name": "user", "Sharded": true }, - "FieldQuery": "select id from seq_tbl where 1 != 1", - "Query": "select id from seq_tbl where id between 1 and 5", - "Table": "seq_tbl", + "FieldQuery": "select id from unq_binary_idx where 1 != 1", + "Query": "select id from unq_binary_idx where id between 1 and 5", + "Table": "unq_binary_idx", "Values": [ "(1, 5)" ], "Vindex": "binary" }, "TablesUsed": [ - "sequential.seq_tbl" + "user.unq_binary_idx" ] } }, { - "comment": "Between clause on group_id column (xxhash vindex on group_id)", - "query": "select id, group_id from seq_tbl where group_id between 1 and 5", + "comment": "Between clause on customer.id column (xxhash vindex on id)", + "query": "select id from customer where id between 1 and 5", "plan": { "QueryType": "SELECT", - "Original": "select id, group_id from seq_tbl where group_id between 1 and 5", + "Original": "select id from customer where id between 1 and 5", "Instructions": { "OperatorType": "Route", "Variant": "Scatter", "Keyspace": { - "Name": "sequential", + "Name": "user", "Sharded": true }, - "FieldQuery": "select id, group_id from seq_tbl where 1 != 1", - "Query": "select id, group_id from seq_tbl where group_id between 1 and 5", - "Table": "seq_tbl" + "FieldQuery": "select id from customer where 1 != 1", + "Query": "select id from customer where id between 1 and 5", + "Table": "customer" }, "TablesUsed": [ - "sequential.seq_tbl" + "user.customer" ] } }, { - "comment": "Between clause on col2 column (there is no vindex on this column)", - "query": "select id, col2 from seq_tbl where col2 between 10 and 50", + "comment": "Between clause on col1 column (there is no vindex on this column)", + "query": "select id, col1 from unq_binary_idx where col1 between 10 and 50", "plan": { "QueryType": "SELECT", - "Original": "select id, col2 from seq_tbl where col2 between 10 and 50", + "Original": "select id, col1 from unq_binary_idx where col1 between 10 and 50", "Instructions": { "OperatorType": "Route", "Variant": "Scatter", "Keyspace": { - "Name": "sequential", + "Name": "user", "Sharded": true }, - "FieldQuery": "select id, col2 from seq_tbl where 1 != 1", - "Query": "select id, col2 from seq_tbl where col2 between 10 and 50", - "Table": "seq_tbl" + "FieldQuery": "select id, col1 from unq_binary_idx where 1 != 1", + "Query": "select id, col1 from unq_binary_idx where col1 between 10 and 50", + "Table": "unq_binary_idx" }, "TablesUsed": [ - "sequential.seq_tbl" + "user.unq_binary_idx" ] } }, { "comment": "Between clause on multicolumn vindex (cola,colb)", - "query": "select cola,colb,col1 from seq_multicol_tbl where cola between 1 and 5", + "query": "select cola,colb,colc from multicol_tbl where cola between 1 and 5", "plan": { "QueryType": "SELECT", - "Original": "select cola,colb,col1 from seq_multicol_tbl where cola between 1 and 5", + "Original": "select cola,colb,colc from multicol_tbl where cola between 1 and 5", "Instructions": { "OperatorType": "Route", "Variant": "Scatter", "Keyspace": { - "Name": "sequential", + "Name": "user", "Sharded": true }, - "FieldQuery": "select cola, colb, col1 from seq_multicol_tbl where 1 != 1", - "Query": "select cola, colb, col1 from seq_multicol_tbl where cola between 1 and 5", - "Table": "seq_multicol_tbl", + "FieldQuery": "select cola, colb, colc from multicol_tbl where 1 != 1", + "Query": "select cola, colb, colc from multicol_tbl where cola between 1 and 5", + "Table": "multicol_tbl", "Values": [ "(1, 5)" ] }, "TablesUsed": [ - "sequential.seq_multicol_tbl" + "user.multicol_tbl" ] } } diff --git a/go/vt/vtgate/planbuilder/testdata/vschemas/schema.json b/go/vt/vtgate/planbuilder/testdata/vschemas/schema.json index d4381d29ca0..0cabc0f0ea9 100644 --- a/go/vt/vtgate/planbuilder/testdata/vschemas/schema.json +++ b/go/vt/vtgate/planbuilder/testdata/vschemas/schema.json @@ -182,6 +182,9 @@ "to": "keyspace_id", "write_only": "true" } + }, + "binary": { + "type": "binary" } }, "tables": { @@ -513,6 +516,20 @@ "name": "shard_index" } ] + }, + "unq_binary_idx": { + "column_vindexes" : [ + { + "column" : "id", + "name": "binary" + } + ], + "columns" :[ + { + "name": "col1", + "type": "INT16" + } + ] } } }, @@ -948,61 +965,6 @@ ] } } - }, - "sequential": { - "sharded": true, - "vindexes": { - "xxhash": { - "type": "xxhash" - }, - "binary": { - "type": "binary" - }, - "multicolIdx": { - "type": "multiCol_test" - } - }, - "tables": { - "seq_tbl": { - "column_vindexes": [ - { - "column": "id", - "name": "binary" - }, - { - "column": "group_id", - "name": "xxhash" - } - ], - "columns": [ - { - "name": "col1", - "type": "VARCHAR" - }, - { - "name": "col2", - "type": "INT16" - } - ] - }, - "seq_multicol_tbl": { - "column_vindexes": [ - { - "columns": [ - "cola", - "colb" - ], - "name": "multicolIdx" - } - ], - "columns" : [ - { - "name": "col1", - "type": "INT16" - } - ] - } - } } } } diff --git a/go/vt/vtgate/vindexes/binary_test.go b/go/vt/vtgate/vindexes/binary_test.go index 775dcb3a5ce..a6556ec958a 100644 --- a/go/vt/vtgate/vindexes/binary_test.go +++ b/go/vt/vtgate/vindexes/binary_test.go @@ -158,8 +158,6 @@ func TestBinaryRangeMap(t *testing.T) { sqltypes.NewHexNum([]byte(endInterval))) require.NoError(t, err) want := "DestinationKeyRange(01-10)" - if !assert.Equal(t, got[0].String(), want) { - t.Errorf("RangeMap(): %+v, want %+v", got, want) - } + assert.Equal(t, want, got[0].String()) } From 8a0ded00679a5e0c640dfdba0340c241a7364dc7 Mon Sep 17 00:00:00 2001 From: c-r-dev Date: Wed, 11 Dec 2024 10:52:30 -0500 Subject: [PATCH 06/12] added early checks while evaluating matching multicolumnvindex (similar to processSingleColumnVindex) Signed-off-by: c-r-dev --- go/vt/vtgate/planbuilder/operators/sharded_routing.go | 6 ++++++ go/vt/vtgate/planbuilder/testdata/filter_cases.json | 5 +---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/go/vt/vtgate/planbuilder/operators/sharded_routing.go b/go/vt/vtgate/planbuilder/operators/sharded_routing.go index 0e28e8342c3..ff9d9e9550c 100644 --- a/go/vt/vtgate/planbuilder/operators/sharded_routing.go +++ b/go/vt/vtgate/planbuilder/operators/sharded_routing.go @@ -473,6 +473,12 @@ func (tr *ShardedRouting) processMultiColumnVindex( return newVindexFound } + routeOpcode := opcode(v.ColVindex) + vindex := vfunc(v.ColVindex) + if vindex == nil || routeOpcode == engine.Scatter { + return newVindexFound + } + var newOption []*VindexOption for _, op := range v.Options { if op.Ready { diff --git a/go/vt/vtgate/planbuilder/testdata/filter_cases.json b/go/vt/vtgate/planbuilder/testdata/filter_cases.json index c46bdd4e585..51ecfad8079 100644 --- a/go/vt/vtgate/planbuilder/testdata/filter_cases.json +++ b/go/vt/vtgate/planbuilder/testdata/filter_cases.json @@ -4906,10 +4906,7 @@ }, "FieldQuery": "select cola, colb, colc from multicol_tbl where 1 != 1", "Query": "select cola, colb, colc from multicol_tbl where cola between 1 and 5", - "Table": "multicol_tbl", - "Values": [ - "(1, 5)" - ] + "Table": "multicol_tbl" }, "TablesUsed": [ "user.multicol_tbl" From 0697cb091ae081f7e3a94a02365ba0bfb1868f4b Mon Sep 17 00:00:00 2001 From: c-r-dev Date: Wed, 11 Dec 2024 11:51:24 -0500 Subject: [PATCH 07/12] add missing Cost for RoutOpCode Between and corresponding test case ) Signed-off-by: c-r-dev --- .../planbuilder/operators/sharded_routing.go | 2 + .../planbuilder/testdata/filter_cases.json | 51 +++++++++++++++++++ .../planbuilder/testdata/vschemas/schema.json | 42 +++++++++++++++ 3 files changed, 95 insertions(+) diff --git a/go/vt/vtgate/planbuilder/operators/sharded_routing.go b/go/vt/vtgate/planbuilder/operators/sharded_routing.go index ff9d9e9550c..2c8873dee07 100644 --- a/go/vt/vtgate/planbuilder/operators/sharded_routing.go +++ b/go/vt/vtgate/planbuilder/operators/sharded_routing.go @@ -364,6 +364,8 @@ func (tr *ShardedRouting) Cost() int { return 5 case engine.IN: return 10 + case engine.Between: + return 10 case engine.MultiEqual: return 10 case engine.Scatter: diff --git a/go/vt/vtgate/planbuilder/testdata/filter_cases.json b/go/vt/vtgate/planbuilder/testdata/filter_cases.json index 51ecfad8079..cab7aac8243 100644 --- a/go/vt/vtgate/planbuilder/testdata/filter_cases.json +++ b/go/vt/vtgate/planbuilder/testdata/filter_cases.json @@ -4912,5 +4912,56 @@ "user.multicol_tbl" ] } +}, +{ + "comment": "Between clause on a binary vindex field with values from a different table", + "query": "select s.oid,s.col1, se.colb from sales s join sales_extra se on s.col1 = se.cola where s.oid between se.start and se.end", + "plan": { + "QueryType": "SELECT", + "Original": "select s.oid,s.col1, se.colb from sales s join sales_extra se on s.col1 = se.cola where s.oid between se.start and se.end", + "Instructions": { + "OperatorType": "Join", + "Variant": "Join", + "JoinColumnIndexes": "R:0,R:1,L:0", + "JoinVars": { + "se_cola": 1, + "se_end": 3, + "se_start": 2 + }, + "TableName": "sales_extra_sales", + "Inputs": [ + { + "OperatorType": "Route", + "Variant": "Scatter", + "Keyspace": { + "Name": "user", + "Sharded": true + }, + "FieldQuery": "select se.colb, se.cola, se.`start`, se.`end` from sales_extra as se where 1 != 1", + "Query": "select se.colb, se.cola, se.`start`, se.`end` from sales_extra as se", + "Table": "sales_extra" + }, + { + "OperatorType": "Route", + "Variant": "Between", + "Keyspace": { + "Name": "user", + "Sharded": true + }, + "FieldQuery": "select s.oid, s.col1 from sales as s where 1 != 1", + "Query": "select s.oid, s.col1 from sales as s where s.oid between :se_start /* INT16 */ and :se_end /* INT16 */ and s.col1 = :se_cola /* VARCHAR */", + "Table": "sales", + "Values": [ + "(:se_start, :se_end)" + ], + "Vindex": "binary" + } + ] + }, + "TablesUsed": [ + "user.sales", + "user.sales_extra" + ] + } } ] diff --git a/go/vt/vtgate/planbuilder/testdata/vschemas/schema.json b/go/vt/vtgate/planbuilder/testdata/vschemas/schema.json index 0cabc0f0ea9..e56348ecd16 100644 --- a/go/vt/vtgate/planbuilder/testdata/vschemas/schema.json +++ b/go/vt/vtgate/planbuilder/testdata/vschemas/schema.json @@ -530,6 +530,48 @@ "type": "INT16" } ] + }, + "sales": { + "column_vindexes" : [ + { + "column" : "oid", + "name" : "binary" + } + ], + "columns" : [ + { + "name" : "col1", + "type" : "VARCHAR" + } + ] + }, + "sales_extra" : { + "column_vindexes": [ + { + "columns": [ + "colx" + ], + "name": "shard_index" + } + ], + "columns" : [ + { + "name" : "cola", + "type" : "VARCHAR" + }, + { + "name" : "colb", + "type" : "VARCHAR" + }, + { + "name" : "start", + "type" : "INT16" + }, + { + "name" : "end", + "type" : "INT16" + } + ] } } }, From f4a578cab2fb56c208c9bb7fb0182760b0f4cdc1 Mon Sep 17 00:00:00 2001 From: c-r-dev Date: Fri, 13 Dec 2024 23:08:18 -0500 Subject: [PATCH 08/12] added end-to-end testcase support for filter_cases Signed-off-by: c-r-dev --- .../vtgate/plan_tests/plan_e2e_test.go | 19 +++ .../planbuilder/testdata/filter_cases.json | 154 ++++++++++++------ .../planbuilder/testdata/schemas/main.sql | 24 ++- .../planbuilder/testdata/schemas/user.sql | 108 ++++++++++-- 4 files changed, 235 insertions(+), 70 deletions(-) diff --git a/go/test/endtoend/vtgate/plan_tests/plan_e2e_test.go b/go/test/endtoend/vtgate/plan_tests/plan_e2e_test.go index 1594e9b392c..9c110df020a 100644 --- a/go/test/endtoend/vtgate/plan_tests/plan_e2e_test.go +++ b/go/test/endtoend/vtgate/plan_tests/plan_e2e_test.go @@ -40,3 +40,22 @@ func TestSelectCases(t *testing.T) { }) } } + +func TestFilterCases(t *testing.T) { + mcmp, closer := start(t) + defer closer() + tests := readJSONTests("filter_cases.json") + for _, test := range tests { + mcmp.Run(test.Comment, func(mcmp *utils.MySQLCompare) { + if test.SkipE2E { + mcmp.AsT().Skip(test.Query) + } + mcmp.Exec(test.Query) + pd := utils.ExecTrace(mcmp.AsT(), mcmp.VtConn, test.Query) + verifyTestExpectations(mcmp.AsT(), pd, test) + if mcmp.VtConn.IsClosed() { + mcmp.AsT().Fatal("vtgate connection is closed") + } + }) + } +} diff --git a/go/vt/vtgate/planbuilder/testdata/filter_cases.json b/go/vt/vtgate/planbuilder/testdata/filter_cases.json index 33bb24dedbf..3790fa0beec 100644 --- a/go/vt/vtgate/planbuilder/testdata/filter_cases.json +++ b/go/vt/vtgate/planbuilder/testdata/filter_cases.json @@ -699,7 +699,8 @@ "TablesUsed": [ "user.user" ] - } + }, + "skip_e2e": true }, { "comment": "Composite IN: RHS not tuple", @@ -722,7 +723,8 @@ "user.music", "user.user" ] - } + }, + "skip_e2e":true }, { "comment": "Composite IN: RHS has no simple values", @@ -923,10 +925,10 @@ }, { "comment": "Single table equality route with val arg", - "query": "select id from user where name = :a", + "query": "select id from user where name = 'a'", "plan": { "QueryType": "SELECT", - "Original": "select id from user where name = :a", + "Original": "select id from user where name = 'a'", "Instructions": { "OperatorType": "VindexLookup", "Variant": "Equal", @@ -935,7 +937,7 @@ "Sharded": true }, "Values": [ - ":a" + "'a'" ], "Vindex": "name_user_map", "Inputs": [ @@ -962,7 +964,7 @@ "Sharded": true }, "FieldQuery": "select id from `user` where 1 != 1", - "Query": "select id from `user` where `name` = :a", + "Query": "select id from `user` where `name` = 'a'", "Table": "`user`" } ] @@ -1095,7 +1097,8 @@ "TablesUsed": [ "user.user" ] - } + }, + "skip_e2e": true }, { "comment": "Multi-table unique vindex constraint", @@ -1252,7 +1255,8 @@ "user.user", "user.user_extra" ] - } + }, + "skip_e2e":true }, { "comment": "Multi-route unique vindex route on both routes", @@ -1279,7 +1283,8 @@ "user.user", "user.user_extra" ] - } + }, + "skip_e2e":true }, { "comment": "Multi-route with cross-route constraint", @@ -1328,7 +1333,8 @@ "user.user", "user.user_extra" ] - } + }, + "skip_e2e":true }, { "comment": "Multi-route with non-route constraint, should use first route.", @@ -1373,7 +1379,8 @@ "user.user", "user.user_extra" ] - } + }, + "skip_e2e":true }, { "comment": "Route with multiple route constraints, SelectIN is the best constraint.", @@ -1654,7 +1661,8 @@ "main.unsharded", "user.user" ] - } + }, + "skip_e2e":true }, { "comment": "routing rules: choose the redirected table", @@ -1680,7 +1688,8 @@ "TablesUsed": [ "user.user" ] - } + }, + "skip_e2e":true }, { "comment": "subquery", @@ -1729,7 +1738,8 @@ "user.user", "user.user_extra" ] - } + }, + "skip_e2e":true }, { "comment": "correlated subquery merge-able into a route of a join tree", @@ -1778,7 +1788,8 @@ "user.user", "user.user_extra" ] - } + }, + "skip_e2e":true }, { "comment": "ensure subquery reordering gets us a better plan", @@ -1824,7 +1835,8 @@ "user.user", "user.user_extra" ] - } + }, + "skip_e2e":true }, { "comment": "nested subquery", @@ -1873,7 +1885,8 @@ "user.user", "user.user_extra" ] - } + }, + "skip_e2e":true }, { "comment": "Correlated subquery in where clause", @@ -1896,7 +1909,8 @@ "user.user", "user.user_extra" ] - } + }, + "skip_e2e":true }, { "comment": "outer and inner subquery route by same int val", @@ -1923,7 +1937,8 @@ "user.user", "user.user_extra" ] - } + }, + "skip_e2e":true }, { "comment": "outer and inner subquery route by same str val", @@ -1950,7 +1965,8 @@ "user.user", "user.user_extra" ] - } + }, + "skip_e2e":true }, { "comment": "outer and inner subquery route by same val arg", @@ -1977,12 +1993,14 @@ "user.user", "user.user_extra" ] - } + }, + "skip_e2e":true }, { "comment": "unresolved symbol in inner subquery.", "query": "select id from user where id = :a and user.col in (select user_extra.col from user_extra where user_extra.user_id = :a and foo.id = 1)", - "plan": "column 'foo.id' not found" + "plan": "column 'foo.id' not found", + "skip_e2e":true }, { "comment": "outer and inner subquery route by same outermost column value", @@ -2005,7 +2023,8 @@ "user.user", "user.user_extra" ] - } + }, + "skip_e2e":true }, { "comment": "cross-shard subquery in IN clause.\n# Note the improved Underlying plan as SelectIN.", @@ -2290,7 +2309,8 @@ "TablesUsed": [ "user.user" ] - } + }, + "skip_e2e":true }, { "comment": "routing rules subquery pullout", @@ -2339,7 +2359,8 @@ "main.unsharded", "user.user" ] - } + }, + "skip_e2e":true }, { "comment": "Case preservation test", @@ -2366,7 +2387,8 @@ "user.user", "user.user_extra" ] - } + }, + "skip_e2e":true }, { "comment": "database() call in where clause.", @@ -2649,7 +2671,8 @@ "user.user", "user.user_extra" ] - } + }, + "skip_e2e":true }, { "comment": "solving LIKE query with a CFC prefix vindex", @@ -2679,10 +2702,10 @@ }, { "comment": "select * from samecolvin where col = :col", - "query": "select * from samecolvin where col = :col", + "query": "select * from samecolvin where col = 'val'", "plan": { "QueryType": "SELECT", - "Original": "select * from samecolvin where col = :col", + "Original": "select * from samecolvin where col = 'val'", "Instructions": { "OperatorType": "Route", "Variant": "EqualUnique", @@ -2691,10 +2714,10 @@ "Sharded": true }, "FieldQuery": "select col from samecolvin where 1 != 1", - "Query": "select col from samecolvin where col = :col", + "Query": "select col from samecolvin where col = 'val'", "Table": "samecolvin", "Values": [ - ":col" + "'val'" ], "Vindex": "vindex1" }, @@ -2819,7 +2842,8 @@ "user.user", "user.user_extra" ] - } + }, + "skip_e2e":true }, { "comment": "SelectDBA with uncorrelated subqueries", @@ -2838,7 +2862,8 @@ "Query": "select t.table_schema from information_schema.`tables` as t where t.table_schema in (select c.column_name from information_schema.`columns` as c)", "Table": "information_schema.`tables`" } - } + }, + "skip_e2e": true }, { "comment": "SelectReference with uncorrelated subqueries", @@ -3040,7 +3065,8 @@ "user.user", "user.user_extra" ] - } + }, + "skip_e2e":true }, { "comment": "The outer and second inner are SelectEqualUnique with same Vindex value, the first inner has different Vindex value", @@ -3094,7 +3120,8 @@ "user.user", "user.user_extra" ] - } + }, + "skip_e2e":true }, { "comment": "two correlated subqueries that can be merge in a single route", @@ -3117,7 +3144,8 @@ "user.user", "user.user_extra" ] - } + }, + "skip_e2e":true }, { "comment": "transitive closures for the win", @@ -3166,7 +3194,8 @@ "user.user", "user.user_extra" ] - } + }, + "skip_e2e":true }, { "comment": "not supported transitive closures with equality inside of an OR", @@ -3215,7 +3244,8 @@ "user.user", "user.user_extra" ] - } + }, + "skip_e2e":true }, { "comment": "routing rules subquery merge with alias", @@ -3237,7 +3267,8 @@ "TablesUsed": [ "user.user" ] - } + }, + "skip_e2e":true }, { "comment": "left join where clauses where we can optimize into an inner join", @@ -3282,12 +3313,14 @@ "user.user", "user.user_extra" ] - } + }, + "skip_e2e":true }, { "comment": "this query lead to a nil pointer error", "query": "select user.id from user left join user_extra on user.col = user_extra.col where foo(user_extra.foobar)", - "plan": "expr cannot be translated, not supported: foo(user_extra.foobar)" + "plan": "expr cannot be translated, not supported: foo(user_extra.foobar)", + "skip_e2e" :true }, { "comment": "filter after outer join", @@ -3339,7 +3372,8 @@ "user.user", "user.user_extra" ] - } + }, + "skip_e2e":true }, { "comment": "subquery on other table", @@ -3994,7 +4028,8 @@ "user.user", "user.user_extra" ] - } + }, + "skip_e2e":true }, { "comment": "conditions following a null safe comparison operator can be used for routing", @@ -4444,7 +4479,8 @@ "TablesUsed": [ "user.user" ] - } + }, + "skip_e2e":true }, { "comment": "two predicates that mean the same thing", @@ -4530,7 +4566,8 @@ "main.unsharded", "user.user" ] - } + }, + "skip_e2e": true }, { "comment": "push filter under aggregation", @@ -4598,7 +4635,8 @@ "user.user", "user.user_extra" ] - } + }, + "skip_e2e":true }, { "comment": "query that would time out because planning was too slow", @@ -4628,7 +4666,8 @@ "TablesUsed": [ "user.user" ] - } + }, + "skip_e2e":true }, { "comment": "union inside subquery. all routes can be merged by literal value", @@ -4682,7 +4721,8 @@ "TablesUsed": [ "user.user" ] - } + }, + "skip_e2e": true }, { "comment": "list args: single column vindex on non-zero offset", @@ -4708,7 +4748,8 @@ "TablesUsed": [ "user.user" ] - } + }, + "skip_e2e": true }, { "comment": "list args: multi column vindex", @@ -4735,7 +4776,8 @@ "TablesUsed": [ "user.multicol_tbl" ] - } + }, + "skip_e2e": true }, { "comment": "list args: multi column vindex - subshard", @@ -4761,7 +4803,8 @@ "TablesUsed": [ "user.multicol_tbl" ] - } + }, + "skip_e2e": true }, { "comment": "list args: multi column vindex - more columns", @@ -4788,7 +4831,8 @@ "TablesUsed": [ "user.multicol_tbl" ] - } + }, + "skip_e2e": true }, { "comment": "list args: multi column vindex - columns rearranged", @@ -4815,7 +4859,8 @@ "TablesUsed": [ "user.multicol_tbl" ] - } + }, + "skip_e2e": true }, { "comment": "order by with filter removing the keyspace from order by", @@ -5062,6 +5107,7 @@ "user.sales", "user.sales_extra" ] - } + }, + "skip_e2e":true } ] diff --git a/go/vt/vtgate/planbuilder/testdata/schemas/main.sql b/go/vt/vtgate/planbuilder/testdata/schemas/main.sql index 8c15b99218c..fb03b69419b 100644 --- a/go/vt/vtgate/planbuilder/testdata/schemas/main.sql +++ b/go/vt/vtgate/planbuilder/testdata/schemas/main.sql @@ -1,12 +1,26 @@ CREATE TABLE `unsharded` ( - `id` INT NOT NULL PRIMARY KEY, - `col1` VARCHAR(255) DEFAULT NULL, - `col2` VARCHAR(255) DEFAULT NULL, - `name` VARCHAR(255) DEFAULT NULL + `id` INT NOT NULL PRIMARY KEY, + `col` VARCHAR(255) DEFAULT NULL, + `col1` VARCHAR(255) DEFAULT NULL, + `col2` VARCHAR(255) DEFAULT NULL, + `name` VARCHAR(255) DEFAULT NULL, + `baz` INT ); CREATE TABLE `unsharded_auto` ( - `id` INT NOT NULL PRIMARY KEY, + `id` INT NOT NULL PRIMARY KEY, `col1` VARCHAR(255) DEFAULT NULL, `col2` VARCHAR(255) DEFAULT NULL +); + +CREATE TABLE `unsharded_a` ( + `id` INT NOT NULL PRIMARY KEY, + `col` VARCHAR(255) DEFAULT NULL, + `name` VARCHAR(255) DEFAULT NULL +); + +CREATE TABLE `unsharded_b` ( + `id` INT NOT NULL PRIMARY KEY, + `col` VARCHAR(255) DEFAULT NULL, + `name` VARCHAR(255) DEFAULT NULL ); \ No newline at end of file diff --git a/go/vt/vtgate/planbuilder/testdata/schemas/user.sql b/go/vt/vtgate/planbuilder/testdata/schemas/user.sql index 55f4078557a..818d2508069 100644 --- a/go/vt/vtgate/planbuilder/testdata/schemas/user.sql +++ b/go/vt/vtgate/planbuilder/testdata/schemas/user.sql @@ -1,12 +1,25 @@ CREATE TABLE user ( - id INT PRIMARY KEY, - col BIGINT, - predef1 VARCHAR(255), - predef2 VARCHAR(255), - textcol1 VARCHAR(255), - intcol BIGINT, - textcol2 VARCHAR(255) + id INT PRIMARY KEY, + col BIGINT, + intcol BIGINT, + user_id INT, + id1 INT, + id2 INT, + id3 INT, + m INT, + bar INT, + a INT, + name VARCHAR(255), + col1 VARCHAR(255), + col2 VARCHAR(255), + costly VARCHAR(255), + predef1 VARCHAR(255), + predef2 VARCHAR(255), + textcol1 VARCHAR(255), + textcol2 VARCHAR(255), + someColumn VARCHAR(255), + foo VARCHAR(255) ); CREATE TABLE user_metadata @@ -23,6 +36,10 @@ CREATE TABLE music ( user_id INT, id INT, + col1 VARCHAR(255), + col2 VARCHAR(255), + genre VARCHAR(255), + componist VARCHAR(255), PRIMARY KEY (user_id) ); @@ -35,9 +52,9 @@ CREATE TABLE samecolvin CREATE TABLE multicolvin ( kid INT, - column_a VARCHAR(255), - column_b VARCHAR(255), - column_c VARCHAR(255), + column_a INT, + column_b INT, + column_c INT, PRIMARY KEY (kid) ); @@ -97,4 +114,73 @@ CREATE TABLE authoritative col1 VARCHAR(255), col2 bigint, PRIMARY KEY (user_id) -) ENGINE=InnoDB; \ No newline at end of file +) ENGINE=InnoDB; + +CREATE TABLE colb_colc_map +( + colb INT PRIMARY KEY, + colc INT, + keyspace_id VARCHAR(255) +); + +CREATE TABLE seq +( + id INT, + next_id BIGINT, + cache BIGINT, + PRIMARY KEY (id) +) COMMENT 'vitess_sequence'; + +CREATE TABLE user_extra +( + id INT, + user_id INT, + extra_id INT, + col INT, + m2 INT, + PRIMARY KEY (id, extra_id) +); + +CREATE TABLE name_user_map +( + name VARCHAR(255), + keyspace_id VARCHAR(255) +); + +CREATE TABLE name_user_vdx +( + name VARCHAR(255), + keyspace_id VARCHAR(255) +); + +CREATE TABLE costly_map +( + costly VARCHAR(255), + keyspace_id VARCHAR(255) +); + +CREATE TABLE unq_binary_idx +( + id INT PRIMARY KEY, + col1 INT +); + +CREATE TABLE sales +( + oid INT PRIMARY KEY, + col1 VARCHAR(255) +); + +CREATE TABLE sales_extra +( + colx INT PRIMARY KEY, + cola VARCHAR(255), + colb VARCHAR(255), + start INT, + end INT +); + +CREATE TABLE ref +( + col INT PRIMARY KEY +); \ No newline at end of file From b99ebabe960b49792dd426509d68fbad73a3fda3 Mon Sep 17 00:00:00 2001 From: c-r-dev Date: Mon, 16 Dec 2024 15:07:32 -0500 Subject: [PATCH 09/12] restored existing filter_cases test cases Signed-off-by: c-r-dev --- .../planbuilder/testdata/filter_cases.json | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/go/vt/vtgate/planbuilder/testdata/filter_cases.json b/go/vt/vtgate/planbuilder/testdata/filter_cases.json index 3790fa0beec..4bf7a969a50 100644 --- a/go/vt/vtgate/planbuilder/testdata/filter_cases.json +++ b/go/vt/vtgate/planbuilder/testdata/filter_cases.json @@ -925,10 +925,10 @@ }, { "comment": "Single table equality route with val arg", - "query": "select id from user where name = 'a'", + "query": "select id from user where name = :a", "plan": { "QueryType": "SELECT", - "Original": "select id from user where name = 'a'", + "Original": "select id from user where name = :a", "Instructions": { "OperatorType": "VindexLookup", "Variant": "Equal", @@ -937,7 +937,7 @@ "Sharded": true }, "Values": [ - "'a'" + ":a" ], "Vindex": "name_user_map", "Inputs": [ @@ -964,7 +964,7 @@ "Sharded": true }, "FieldQuery": "select id from `user` where 1 != 1", - "Query": "select id from `user` where `name` = 'a'", + "Query": "select id from `user` where `name` = :a", "Table": "`user`" } ] @@ -972,7 +972,8 @@ "TablesUsed": [ "user.user" ] - } + }, + "skip_e2e":true }, { "comment": "Merging subqueries should remove keyspace from query", @@ -2702,10 +2703,10 @@ }, { "comment": "select * from samecolvin where col = :col", - "query": "select * from samecolvin where col = 'val'", + "query": "select * from samecolvin where col = :col", "plan": { "QueryType": "SELECT", - "Original": "select * from samecolvin where col = 'val'", + "Original": "select * from samecolvin where col = :col", "Instructions": { "OperatorType": "Route", "Variant": "EqualUnique", @@ -2714,17 +2715,18 @@ "Sharded": true }, "FieldQuery": "select col from samecolvin where 1 != 1", - "Query": "select col from samecolvin where col = 'val'", + "Query": "select col from samecolvin where col = :col", "Table": "samecolvin", "Values": [ - "'val'" + ":col" ], "Vindex": "vindex1" }, "TablesUsed": [ "user.samecolvin" ] - } + }, + "skip_e2e":true }, { "comment": "non unique predicate on vindex", From 1400fea144a1542314e7c1dee4ad1d7798f63ee2 Mon Sep 17 00:00:00 2001 From: c-r-dev Date: Tue, 17 Dec 2024 15:57:41 -0500 Subject: [PATCH 10/12] added sample test data for filter_cases e2e testing Signed-off-by: c-r-dev --- .../endtoend/vtgate/plan_tests/main_test.go | 26 +++++++++++++++++++ .../vtgate/plan_tests/plan_e2e_test.go | 1 + .../planbuilder/testdata/filter_cases.json | 3 +-- .../planbuilder/testdata/sampledata/user.sql | 14 ++++++++++ 4 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 go/vt/vtgate/planbuilder/testdata/sampledata/user.sql diff --git a/go/test/endtoend/vtgate/plan_tests/main_test.go b/go/test/endtoend/vtgate/plan_tests/main_test.go index d3915af0c8d..32bacdf3c85 100644 --- a/go/test/endtoend/vtgate/plan_tests/main_test.go +++ b/go/test/endtoend/vtgate/plan_tests/main_test.go @@ -27,6 +27,7 @@ import ( "vitess.io/vitess/go/mysql" "vitess.io/vitess/go/test/endtoend/cluster" "vitess.io/vitess/go/test/endtoend/utils" + "vitess.io/vitess/go/vt/sqlparser" "vitess.io/vitess/go/vt/vtgate/engine" "vitess.io/vitess/go/vt/vtgate/planbuilder" ) @@ -128,6 +129,31 @@ func start(t *testing.T) (utils.MySQLCompare, func()) { } } +// parseSQL statements - querySQL may be a multi-line sql blob +func parseSQL(querySQL ...string) ([]string, error) { + parser := sqlparser.NewTestParser() + var sqls []string + for _, sql := range querySQL { + split, err := parser.SplitStatementToPieces(sql) + if err != nil { + return nil, err + } + sqls = append(sqls, split...) + } + return sqls, nil +} + +func loadSampleData(t *testing.T, mcmp utils.MySQLCompare) { + sampleDataSQL := readFile("sampledata/user.sql") + insertSQL, err := parseSQL(sampleDataSQL) + if err != nil { + require.NoError(t, err) + } + for _, sql := range insertSQL { + mcmp.ExecNoCompare(sql) + } +} + func readJSONTests(filename string) []planbuilder.PlanTest { var output []planbuilder.PlanTest file, err := os.Open(locateFile(filename)) diff --git a/go/test/endtoend/vtgate/plan_tests/plan_e2e_test.go b/go/test/endtoend/vtgate/plan_tests/plan_e2e_test.go index 9c110df020a..4e029582d2f 100644 --- a/go/test/endtoend/vtgate/plan_tests/plan_e2e_test.go +++ b/go/test/endtoend/vtgate/plan_tests/plan_e2e_test.go @@ -44,6 +44,7 @@ func TestSelectCases(t *testing.T) { func TestFilterCases(t *testing.T) { mcmp, closer := start(t) defer closer() + loadSampleData(t, mcmp) tests := readJSONTests("filter_cases.json") for _, test := range tests { mcmp.Run(test.Comment, func(mcmp *utils.MySQLCompare) { diff --git a/go/vt/vtgate/planbuilder/testdata/filter_cases.json b/go/vt/vtgate/planbuilder/testdata/filter_cases.json index 4bf7a969a50..72b6c4ddd46 100644 --- a/go/vt/vtgate/planbuilder/testdata/filter_cases.json +++ b/go/vt/vtgate/planbuilder/testdata/filter_cases.json @@ -5109,7 +5109,6 @@ "user.sales", "user.sales_extra" ] - }, - "skip_e2e":true + } } ] diff --git a/go/vt/vtgate/planbuilder/testdata/sampledata/user.sql b/go/vt/vtgate/planbuilder/testdata/sampledata/user.sql new file mode 100644 index 00000000000..044a1ee140d --- /dev/null +++ b/go/vt/vtgate/planbuilder/testdata/sampledata/user.sql @@ -0,0 +1,14 @@ +INSERT INTO sales (oid, col1) + VALUES (1, 'a_1'); + +INSERT INTO sales_extra(colx, cola, colb, start, end) +VALUES (11, 'a_1', 'b_1',0, 500); + +INSERT INTO sales_extra(colx, cola, colb, start, end) +VALUES (12, 'a_2', 'b_2',500, 1000); + +INSERT INTO sales_extra(colx, cola, colb, start, end) +VALUES (13, 'a_3', 'b_3',1000, 1500); + +INSERT INTO sales_extra(colx, cola, colb, start, end) +VALUES (14, 'a_4', 'b_4',1500, 2000); \ No newline at end of file From 7ac0eb2c002e364991bcf3b9630ad58be407d16e Mon Sep 17 00:00:00 2001 From: c-r-dev Date: Thu, 19 Dec 2024 12:31:04 -0500 Subject: [PATCH 11/12] incorporated feedback on e2e testcases Signed-off-by: c-r-dev --- .../endtoend/vtgate/plan_tests/main_test.go | 6 +-- .../vtgate/plan_tests/plan_e2e_test.go | 50 +++++++------------ 2 files changed, 20 insertions(+), 36 deletions(-) diff --git a/go/test/endtoend/vtgate/plan_tests/main_test.go b/go/test/endtoend/vtgate/plan_tests/main_test.go index 32bacdf3c85..504ec3ffb26 100644 --- a/go/test/endtoend/vtgate/plan_tests/main_test.go +++ b/go/test/endtoend/vtgate/plan_tests/main_test.go @@ -129,8 +129,8 @@ func start(t *testing.T) (utils.MySQLCompare, func()) { } } -// parseSQL statements - querySQL may be a multi-line sql blob -func parseSQL(querySQL ...string) ([]string, error) { +// splitSQL statements - querySQL may be a multi-line sql blob +func splitSQL(querySQL ...string) ([]string, error) { parser := sqlparser.NewTestParser() var sqls []string for _, sql := range querySQL { @@ -145,7 +145,7 @@ func parseSQL(querySQL ...string) ([]string, error) { func loadSampleData(t *testing.T, mcmp utils.MySQLCompare) { sampleDataSQL := readFile("sampledata/user.sql") - insertSQL, err := parseSQL(sampleDataSQL) + insertSQL, err := splitSQL(sampleDataSQL) if err != nil { require.NoError(t, err) } diff --git a/go/test/endtoend/vtgate/plan_tests/plan_e2e_test.go b/go/test/endtoend/vtgate/plan_tests/plan_e2e_test.go index 4e029582d2f..b4d6a2b39f6 100644 --- a/go/test/endtoend/vtgate/plan_tests/plan_e2e_test.go +++ b/go/test/endtoend/vtgate/plan_tests/plan_e2e_test.go @@ -22,41 +22,25 @@ import ( "vitess.io/vitess/go/test/endtoend/utils" ) -func TestSelectCases(t *testing.T) { - mcmp, closer := start(t) - defer closer() - tests := readJSONTests("select_cases.json") - for _, test := range tests { - mcmp.Run(test.Comment, func(mcmp *utils.MySQLCompare) { - if test.SkipE2E { - mcmp.AsT().Skip(test.Query) - } - mcmp.Exec(test.Query) - pd := utils.ExecTrace(mcmp.AsT(), mcmp.VtConn, test.Query) - verifyTestExpectations(mcmp.AsT(), pd, test) - if mcmp.VtConn.IsClosed() { - mcmp.AsT().Fatal("vtgate connection is closed") - } - }) - } -} - -func TestFilterCases(t *testing.T) { +func TestE2ECases(t *testing.T) { + e2eTestCaseFiles := []string{"select_cases.json", "filter_cases.json"} mcmp, closer := start(t) defer closer() loadSampleData(t, mcmp) - tests := readJSONTests("filter_cases.json") - for _, test := range tests { - mcmp.Run(test.Comment, func(mcmp *utils.MySQLCompare) { - if test.SkipE2E { - mcmp.AsT().Skip(test.Query) - } - mcmp.Exec(test.Query) - pd := utils.ExecTrace(mcmp.AsT(), mcmp.VtConn, test.Query) - verifyTestExpectations(mcmp.AsT(), pd, test) - if mcmp.VtConn.IsClosed() { - mcmp.AsT().Fatal("vtgate connection is closed") - } - }) + for _, fileName := range e2eTestCaseFiles { + tests := readJSONTests(fileName) + for _, test := range tests { + mcmp.Run(test.Comment, func(mcmp *utils.MySQLCompare) { + if test.SkipE2E { + mcmp.AsT().Skip(test.Query) + } + mcmp.Exec(test.Query) + pd := utils.ExecTrace(mcmp.AsT(), mcmp.VtConn, test.Query) + verifyTestExpectations(mcmp.AsT(), pd, test) + if mcmp.VtConn.IsClosed() { + mcmp.AsT().Fatal("vtgate connection is closed") + } + }) + } } } From 01007a5167dc9dabb494f5520d5f0d3c80917f52 Mon Sep 17 00:00:00 2001 From: Andres Taylor Date: Fri, 20 Dec 2024 09:18:44 +0100 Subject: [PATCH 12/12] Empty commit to trigger CI Signed-off-by: Andres Taylor