diff --git a/pkg/sql/catalog/descs/collection.go b/pkg/sql/catalog/descs/collection.go index 66963635af2c..898db4063400 100644 --- a/pkg/sql/catalog/descs/collection.go +++ b/pkg/sql/catalog/descs/collection.go @@ -743,6 +743,13 @@ func (tc *Collection) GetAll(ctx context.Context, txn *kv.Txn) (nstree.Catalog, return ret.Catalog, nil } +// GetDescriptorsInSpans returns all descriptors within a given span. +func (tc *Collection) GetDescriptorsInSpans( + ctx context.Context, txn *kv.Txn, spans []roachpb.Span, +) (nstree.Catalog, error) { + return tc.cr.ScanDescriptorsInSpans(ctx, txn, spans) +} + // GetAllComments gets all comments for all descriptors in the given database. // This method never returns the underlying catalog, since it will be incomplete and only // contain comments. diff --git a/pkg/sql/catalog/internal/catkv/BUILD.bazel b/pkg/sql/catalog/internal/catkv/BUILD.bazel index c1c1ec3f74ab..f730b814c152 100644 --- a/pkg/sql/catalog/internal/catkv/BUILD.bazel +++ b/pkg/sql/catalog/internal/catkv/BUILD.bazel @@ -55,6 +55,7 @@ go_test( ":catkv", "//pkg/base", "//pkg/kv", + "//pkg/roachpb", "//pkg/security/securityassets", "//pkg/security/securitytest", "//pkg/server", @@ -67,6 +68,7 @@ go_test( "//pkg/testutils/datapathutils", "//pkg/testutils/serverutils", "//pkg/testutils/sqlutils", + "//pkg/util/encoding", "//pkg/util/leaktest", "//pkg/util/log", "//pkg/util/randutil", diff --git a/pkg/sql/catalog/internal/catkv/catalog_reader.go b/pkg/sql/catalog/internal/catkv/catalog_reader.go index 8699d8b96295..e2d24876c894 100644 --- a/pkg/sql/catalog/internal/catkv/catalog_reader.go +++ b/pkg/sql/catalog/internal/catkv/catalog_reader.go @@ -49,6 +49,9 @@ type CatalogReader interface { // ScanAll scans the entirety of the descriptor and namespace tables. ScanAll(ctx context.Context, txn *kv.Txn) (nstree.Catalog, error) + // ScanDescriptorsInSpans scans the descriptors specified in a given span. + ScanDescriptorsInSpans(ctx context.Context, txn *kv.Txn, span []roachpb.Span) (nstree.Catalog, error) + // ScanAllComments scans the entirety of the comments table as well as the namespace entries for the given database. // If the dbContext is nil, we scan the database-level namespace entries. ScanAllComments(ctx context.Context, txn *kv.Txn, db catalog.DatabaseDescriptor) (nstree.Catalog, error) @@ -158,6 +161,76 @@ func (cr catalogReader) ScanAll(ctx context.Context, txn *kv.Txn) (nstree.Catalo return mc.Catalog, nil } +// getDescriptorIDFromExclusiveKey translates an exclusive upper bound roach key +// into an upper bound descriptor ID. It does this by turning the key into a +// descriptor ID, and then moving it upwards if and only if it is not the prefix +// of the current index / table span. +func getDescriptorIDFromExclusiveKey(codec keys.SQLCodec, key roachpb.Key) (uint32, error) { + keyWithoutTable, endID, err := codec.DecodeTablePrefix(key) + if err != nil { + return 0, err + } + if len(keyWithoutTable) == 0 { + return endID, nil + } + + keyWithoutIndex, _, indexId, err := codec.DecodeIndexPrefix(key) + if err != nil { + return 0, err + } + // if there's remaining bytes or the index isn't the primary, increment + // the end so that the descriptor under the key is included. + if len(keyWithoutIndex) != 0 || indexId > 1 { + endID++ + } + return endID, nil +} + +// getDescriptorSpanFromSpan returns a start and end descriptor ID from a given span +func getDescriptorSpanFromSpan(codec keys.SQLCodec, span roachpb.Span) (roachpb.Span, error) { + _, startID, err := codec.DecodeTablePrefix(span.Key) + if err != nil { + return roachpb.Span{}, err + } + endID, err := getDescriptorIDFromExclusiveKey(codec, span.EndKey) + if err != nil { + return roachpb.Span{}, err + } + + return roachpb.Span{ + Key: catalogkeys.MakeDescMetadataKey(codec, descpb.ID(startID)), + EndKey: catalogkeys.MakeDescMetadataKey(codec, descpb.ID(endID)), + }, nil +} + +// ScanDescriptorsInSpans is part of the CatalogReader interface. +func (cr catalogReader) ScanDescriptorsInSpans( + ctx context.Context, txn *kv.Txn, spans []roachpb.Span, +) (nstree.Catalog, error) { + var mc nstree.MutableCatalog + + descSpans := make([]roachpb.Span, len(spans)) + for i, span := range spans { + descSpan, err := getDescriptorSpanFromSpan(cr.Codec(), span) + if err != nil { + return mc.Catalog, err + } + descSpans[i] = descSpan + } + + cq := catalogQuery{codec: cr.codec} + err := cq.query(ctx, txn, &mc, func(codec keys.SQLCodec, b *kv.Batch) { + for _, descSpan := range descSpans { + scanRange(ctx, b, descSpan.Key, descSpan.EndKey) + } + }) + if err != nil { + return mc.Catalog, err + } + + return mc.Catalog, nil +} + // ScanAllComments is part of the CatalogReader interface. func (cr catalogReader) ScanAllComments( ctx context.Context, txn *kv.Txn, db catalog.DatabaseDescriptor, diff --git a/pkg/sql/catalog/internal/catkv/catalog_reader_cached.go b/pkg/sql/catalog/internal/catkv/catalog_reader_cached.go index 080d882f0986..3550044b899c 100644 --- a/pkg/sql/catalog/internal/catkv/catalog_reader_cached.go +++ b/pkg/sql/catalog/internal/catkv/catalog_reader_cached.go @@ -11,6 +11,7 @@ import ( "github.com/cockroachdb/cockroach/pkg/clusterversion" "github.com/cockroachdb/cockroach/pkg/keys" "github.com/cockroachdb/cockroach/pkg/kv" + "github.com/cockroachdb/cockroach/pkg/roachpb" "github.com/cockroachdb/cockroach/pkg/sql/catalog" "github.com/cockroachdb/cockroach/pkg/sql/catalog/descpb" "github.com/cockroachdb/cockroach/pkg/sql/catalog/nstree" @@ -220,6 +221,15 @@ func (c *cachedCatalogReader) ScanAll(ctx context.Context, txn *kv.Txn) (nstree. return read, nil } +// ScanDescriptorsInSpans is part of the CatalogReader interface. +func (c *cachedCatalogReader) ScanDescriptorsInSpans( + ctx context.Context, txn *kv.Txn, spans []roachpb.Span, +) (nstree.Catalog, error) { + // TODO (brian.dillmann@): explore caching these calls. + // https://github.com/cockroachdb/cockroach/issues/134666 + return c.cr.ScanDescriptorsInSpans(ctx, txn, spans) +} + // ScanNamespaceForDatabases is part of the CatalogReader interface. func (c *cachedCatalogReader) ScanNamespaceForDatabases( ctx context.Context, txn *kv.Txn, diff --git a/pkg/sql/catalog/internal/catkv/catalog_reader_test.go b/pkg/sql/catalog/internal/catkv/catalog_reader_test.go index cd165239dffe..e3be1a937b83 100644 --- a/pkg/sql/catalog/internal/catkv/catalog_reader_test.go +++ b/pkg/sql/catalog/internal/catkv/catalog_reader_test.go @@ -16,6 +16,7 @@ import ( "github.com/cockroachdb/cockroach/pkg/base" "github.com/cockroachdb/cockroach/pkg/kv" + "github.com/cockroachdb/cockroach/pkg/roachpb" "github.com/cockroachdb/cockroach/pkg/sql" "github.com/cockroachdb/cockroach/pkg/sql/catalog" "github.com/cockroachdb/cockroach/pkg/sql/catalog/catalogkeys" @@ -26,6 +27,7 @@ import ( "github.com/cockroachdb/cockroach/pkg/testutils/datapathutils" "github.com/cockroachdb/cockroach/pkg/testutils/serverutils" "github.com/cockroachdb/cockroach/pkg/testutils/sqlutils" + "github.com/cockroachdb/cockroach/pkg/util/encoding" "github.com/cockroachdb/cockroach/pkg/util/leaktest" "github.com/cockroachdb/cockroach/pkg/util/log" "github.com/cockroachdb/cockroach/pkg/util/tracing" @@ -203,6 +205,24 @@ func TestDataDriven(t *testing.T) { return cr.GetByNames(ctx, txn, nis) } return h.doCatalogQuery(ctx, q) + case "scan_descriptors_in_span": + { + start := h.parseKeyFromArgKey("start") + end := h.parseKeyFromArgKey("end") + q := func(ctx context.Context, txn *kv.Txn, cr catkv.CatalogReader) (nstree.Catalog, error) { + return cr.ScanDescriptorsInSpans(ctx, txn, []roachpb.Span{{Key: start, EndKey: end}}) + } + return h.doCatalogQuery(ctx, q) + } + case "scan_descriptors_in_multiple_spans": + { + first := h.parseSpanFromArgKey("first") + second := h.parseSpanFromArgKey("second") + q := func(ctx context.Context, txn *kv.Txn, cr catkv.CatalogReader) (nstree.Catalog, error) { + return cr.ScanDescriptorsInSpans(ctx, txn, []roachpb.Span{first, second}) + } + return h.doCatalogQuery(ctx, q) + } } return fmt.Sprintf("%s: unknown command: %s", d.Pos, d.Cmd) }) @@ -217,6 +237,58 @@ type testHelper struct { ucr, ccr catkv.CatalogReader } +func (h testHelper) parseSpanFromArgKey(argkey string) roachpb.Span { + arg, exists := h.d.Arg(argkey) + if !exists { + h.t.Fatalf("scan_descriptors_in_span requires '%s' arg", argkey) + } + start, end := arg.TwoVals(h.t) + return roachpb.Span{ + Key: h.parseKeyFromArgStr(start), + EndKey: h.parseKeyFromArgStr(end), + } +} + +func (h testHelper) parseKeyFromArgKey(argkey string) roachpb.Key { + arg, exists := h.d.Arg(argkey) + if !exists { + h.t.Fatalf("scan_descriptors_in_span requires '%s' arg", argkey) + } + return h.parseKeyFromArgStr(arg.SingleVal(h.t)) +} + +func (h testHelper) parseKeyFromArgStr(argstr string) roachpb.Key { + parts := strings.Split(argstr, "/") + if len(parts) == 0 { + h.t.Fatal("cannot parse key without at least one key part") + } else if len(parts) > 4 { + h.t.Fatal("key argument has too many parts") + } + + tableId, err := strconv.Atoi(parts[0]) + require.NoError(h.t, err) + if len(parts) == 1 { + return h.execCfg.Codec.TablePrefix(uint32(tableId)) + } + + indexId, err := strconv.Atoi(parts[1]) + require.NoError(h.t, err) + + key := h.execCfg.Codec.IndexPrefix(uint32(tableId), uint32(indexId)) + if len(parts) == 3 && parts[2] != "" { + // only supports integer and string key values + if encoding.PeekType([]byte(parts[2])) == encoding.Int { + pkey, err := strconv.Atoi(parts[1]) + require.NoError(h.t, err) + return encoding.EncodeVarintAscending(key, int64(pkey)) + } else { + return encoding.EncodeStringAscending(key, parts[2]) + } + } + + return key +} + func (h testHelper) argDesc( ctx context.Context, idArgName string, expectedType catalog.DescriptorType, ) catalog.Descriptor { diff --git a/pkg/sql/catalog/internal/catkv/testdata/testdata_app b/pkg/sql/catalog/internal/catkv/testdata/testdata_app index fa751bade8f8..dd6ac6bbb6b0 100644 --- a/pkg/sql/catalog/internal/catkv/testdata/testdata_app +++ b/pkg/sql/catalog/internal/catkv/testdata/testdata_app @@ -16,6 +16,18 @@ COMMENT ON TABLE kv IS 'this is a table'; COMMENT ON INDEX mv@idx IS 'this is an index'; COMMENT ON CONSTRAINT ck ON kv IS 'this is a check constraint'; COMMENT ON CONSTRAINT kv_pkey ON kv IS 'this is a primary key constraint'; + +-- below queries are for scan_descriptors_in_span tests +CREATE TABLE scan_test_1(main SERIAL PRIMARY KEY, alternate VARCHAR UNIQUE); +CREATE TABLE scan_test_2(main INT PRIMARY KEY, alternate VARCHAR UNIQUE); + +INSERT INTO scan_test_1(alternate) VALUES ('a'); +INSERT INTO scan_test_1(alternate) VALUES ('c'); +INSERT INTO scan_test_1(alternate) VALUES ('f'); + +INSERT INTO scan_test_2(main, alternate) VALUES (1, 'b'); +INSERT INTO scan_test_2(main, alternate) VALUES (4, 'c'); +INSERT INTO scan_test_2(main, alternate) VALUES (9, 'd'); ---- scan_namespace_for_databases @@ -70,6 +82,10 @@ catalog: namespace: (100, 101, "kv") "109": namespace: (100, 101, "mv") + "110": + namespace: (100, 101, "scan_test_1") + "111": + namespace: (100, 101, "scan_test_2") trace: - Scan /NamespaceTable/30/1/100 @@ -421,6 +437,12 @@ catalog: index_2: this is an index descriptor: relation namespace: (100, 101, "mv") + "110": + descriptor: relation + namespace: (100, 101, "scan_test_1") + "111": + descriptor: relation + namespace: (100, 101, "scan_test_2") trace: - Scan /Table/3/1 - Scan /NamespaceTable/30/1 @@ -512,6 +534,10 @@ catalog: comments: index_2: this is an index namespace: (100, 101, "mv") + "110": + namespace: (100, 101, "scan_test_1") + "111": + namespace: (100, 101, "scan_test_2") trace: - Scan /NamespaceTable/30/1/100 - Scan /Table/24/1 @@ -551,3 +577,180 @@ catalog: trace: - Scan /NamespaceTable/30/1/0 - Scan /Table/24/1 + +# The below tests test the many circumstances for scanning a set +# of descriptors within a span. +# +# For this test, there are two relevant tables, 'scan_test_1' and 'scan_test_2' +# with two indexes each on columns main and secondary, denoted +# by the below notation: +# T1I1 (scan_test_1, main), T1I2, T2I1, T2I2 +# +# The boundaries will be marked by keys on either side of it /. +# +# Indexes = └────T1I1────┴────T1I2───┴────T2I1────┴────T2I2───┘ +# Keys = min/1 3/a f/1 9/b d/max +# +# Args 'start' and 'end' take the format "/(/?)" + +# start key after end key should panic +# disabled because the panic seems to appear in a different goroutine +# scan_descriptors_in_span start=111/1 end=110/1 panics=true +# ---- + +# # same start and end key should return first descriptor +# disabled because it panics if the same key is passed +# scan_descriptors_in_span start=110/1 end=110/1 +# ---- + +# test with only table prefix +scan_descriptors_in_span start=110 end=111 +---- +catalog: + "110": + descriptor: relation +trace: +- Scan Range /Table/3/1/110/2/1 /Table/3/1/111/2/1 + +# start and end are one key away from each other +scan_descriptors_in_span start=110/1/1 end=110/1/2 +---- +catalog: + "110": + descriptor: relation +trace: +- Scan Range /Table/3/1/110/2/1 /Table/3/1/111/2/1 + +# scan with the start at the prefix, the end in the table span +scan_descriptors_in_span start=110/1 end=110/1/1 +---- +catalog: + "110": + descriptor: relation +trace: +- Scan Range /Table/3/1/110/2/1 /Table/3/1/111/2/1 + +# end after the first index +scan_descriptors_in_span start=110/1 end=110/2 +---- +catalog: + "110": + descriptor: relation +trace: +- Scan Range /Table/3/1/110/2/1 /Table/3/1/111/2/1 + + +# end in the second index +scan_descriptors_in_span start=110/1 end=110/2/a +---- +catalog: + "110": + descriptor: relation +trace: +- Scan Range /Table/3/1/110/2/1 /Table/3/1/111/2/1 + + +# end on a value which doesn't exist +scan_descriptors_in_span start=110/1 end=110/2/b +---- +catalog: + "110": + descriptor: relation +trace: +- Scan Range /Table/3/1/110/2/1 /Table/3/1/111/2/1 + + +# end on the last value in the second index +scan_descriptors_in_span start=110/1 end=110/2/f +---- +catalog: + "110": + descriptor: relation +trace: +- Scan Range /Table/3/1/110/2/1 /Table/3/1/111/2/1 + + +# start on the last value in the second index +scan_descriptors_in_span start=110/1 end=110/2/f +---- +catalog: + "110": + descriptor: relation +trace: +- Scan Range /Table/3/1/110/2/1 /Table/3/1/111/2/1 + + +# end directly on first index is exclusive +scan_descriptors_in_span start=110/1 end=111/1 +---- +catalog: + "110": + descriptor: relation +trace: +- Scan Range /Table/3/1/110/2/1 /Table/3/1/111/2/1 + + +# end on the first key of the second table is inclusive +scan_descriptors_in_span start=110/1 end=111/1/0 +---- +catalog: + "110": + descriptor: relation + "111": + descriptor: relation +trace: +- Scan Range /Table/3/1/110/2/1 /Table/3/1/112/2/1 + +# end on an absurd key +scan_descriptors_in_span start=110/2/f end=9000/1 +---- +catalog: + "110": + descriptor: relation + "111": + descriptor: relation +trace: +- Scan Range /Table/3/1/110/2/1 /Table/3/1/9000/2/1 + +# start in the middle of the first table +scan_descriptors_in_span start=110/1/1 end=112/1 +---- +catalog: + "110": + descriptor: relation + "111": + descriptor: relation +trace: +- Scan Range /Table/3/1/110/2/1 /Table/3/1/112/2/1 + +# start on the last key of the first table +scan_descriptors_in_span start=110/2/f end=112/1 +---- +catalog: + "110": + descriptor: relation + "111": + descriptor: relation +trace: +- Scan Range /Table/3/1/110/2/1 /Table/3/1/112/2/1 + +# start on the first key of the second table +scan_descriptors_in_span start=111/1 end=112/1 +---- +catalog: + "111": + descriptor: relation +trace: +- Scan Range /Table/3/1/111/2/1 /Table/3/1/112/2/1 + +# verify that multiple span scanning works +scan_descriptors_in_multiple_spans first=(110/1,111/1) second=(111/1,112/1) +---- +catalog: + "110": + descriptor: relation + "111": + descriptor: relation +trace: +- Scan Range /Table/3/1/110/2/1 /Table/3/1/111/2/1 +- Scan Range /Table/3/1/111/2/1 /Table/3/1/112/2/1 diff --git a/pkg/sql/catalog/internal/catkv/testdata/testdata_system b/pkg/sql/catalog/internal/catkv/testdata/testdata_system index 829b2dd83378..58b78e92fdb2 100644 --- a/pkg/sql/catalog/internal/catkv/testdata/testdata_system +++ b/pkg/sql/catalog/internal/catkv/testdata/testdata_system @@ -16,6 +16,18 @@ COMMENT ON TABLE kv IS 'this is a table'; COMMENT ON INDEX mv@idx IS 'this is an index'; COMMENT ON CONSTRAINT ck ON kv IS 'this is a check constraint'; COMMENT ON CONSTRAINT kv_pkey ON kv IS 'this is a primary key constraint'; + +-- below queries are for scan_descriptors_in_span tests +CREATE TABLE scan_test_1(main SERIAL PRIMARY KEY, alternate VARCHAR UNIQUE); +CREATE TABLE scan_test_2(main INT PRIMARY KEY, alternate VARCHAR UNIQUE); + +INSERT INTO scan_test_1(alternate) VALUES ('a'); +INSERT INTO scan_test_1(alternate) VALUES ('c'); +INSERT INTO scan_test_1(alternate) VALUES ('f'); + +INSERT INTO scan_test_2(main, alternate) VALUES (1, 'b'); +INSERT INTO scan_test_2(main, alternate) VALUES (4, 'c'); +INSERT INTO scan_test_2(main, alternate) VALUES (9, 'd'); ---- scan_namespace_for_databases @@ -70,6 +82,10 @@ catalog: namespace: (100, 101, "kv") "109": namespace: (100, 101, "mv") + "110": + namespace: (100, 101, "scan_test_1") + "111": + namespace: (100, 101, "scan_test_2") trace: - Scan /NamespaceTable/30/1/100 @@ -439,6 +455,12 @@ catalog: index_2: this is an index descriptor: relation namespace: (100, 101, "mv") + "110": + descriptor: relation + namespace: (100, 101, "scan_test_1") + "111": + descriptor: relation + namespace: (100, 101, "scan_test_2") trace: - Scan /Table/3/1 - Scan /NamespaceTable/30/1 @@ -530,6 +552,10 @@ catalog: comments: index_2: this is an index namespace: (100, 101, "mv") + "110": + namespace: (100, 101, "scan_test_1") + "111": + namespace: (100, 101, "scan_test_2") trace: - Scan /NamespaceTable/30/1/100 - Scan /Table/24/1 @@ -569,3 +595,180 @@ catalog: trace: - Scan /NamespaceTable/30/1/0 - Scan /Table/24/1 + +# The below tests test the many circumstances for scanning a set +# of descriptors within a span. +# +# For this test, there are two relevant tables, 'scan_test_1' and 'scan_test_2' +# with two indexes each on columns main and secondary, denoted +# by the below notation: +# T1I1 (scan_test_1, main), T1I2, T2I1, T2I2 +# +# The boundaries will be marked by keys on either side of it /. +# +# Indexes = └────T1I1────┴────T1I2───┴────T2I1────┴────T2I2───┘ +# Keys = min/1 3/a f/1 9/b d/max +# +# Args 'start' and 'end' take the format "/(/?)" + +# start key after end key should panic +# disabled because the panic seems to appear in a different goroutine +# scan_descriptors_in_span start=111/1 end=110/1 panics=true +# ---- + +# # same start and end key should return first descriptor +# disabled because it panics if the same key is passed +# scan_descriptors_in_span start=110/1 end=110/1 +# ---- + +# test with only table prefix +scan_descriptors_in_span start=110 end=111 +---- +catalog: + "110": + descriptor: relation +trace: +- Scan Range /Table/3/1/110/2/1 /Table/3/1/111/2/1 + +# start and end are one key away from each other +scan_descriptors_in_span start=110/1/1 end=110/1/2 +---- +catalog: + "110": + descriptor: relation +trace: +- Scan Range /Table/3/1/110/2/1 /Table/3/1/111/2/1 + +# scan with the start at the prefix, the end in the table span +scan_descriptors_in_span start=110/1 end=110/1/1 +---- +catalog: + "110": + descriptor: relation +trace: +- Scan Range /Table/3/1/110/2/1 /Table/3/1/111/2/1 + +# end after the first index +scan_descriptors_in_span start=110/1 end=110/2 +---- +catalog: + "110": + descriptor: relation +trace: +- Scan Range /Table/3/1/110/2/1 /Table/3/1/111/2/1 + + +# end in the second index +scan_descriptors_in_span start=110/1 end=110/2/a +---- +catalog: + "110": + descriptor: relation +trace: +- Scan Range /Table/3/1/110/2/1 /Table/3/1/111/2/1 + + +# end on a value which doesn't exist +scan_descriptors_in_span start=110/1 end=110/2/b +---- +catalog: + "110": + descriptor: relation +trace: +- Scan Range /Table/3/1/110/2/1 /Table/3/1/111/2/1 + + +# end on the last value in the second index +scan_descriptors_in_span start=110/1 end=110/2/f +---- +catalog: + "110": + descriptor: relation +trace: +- Scan Range /Table/3/1/110/2/1 /Table/3/1/111/2/1 + + +# start on the last value in the second index +scan_descriptors_in_span start=110/1 end=110/2/f +---- +catalog: + "110": + descriptor: relation +trace: +- Scan Range /Table/3/1/110/2/1 /Table/3/1/111/2/1 + + +# end directly on first index is exclusive +scan_descriptors_in_span start=110/1 end=111/1 +---- +catalog: + "110": + descriptor: relation +trace: +- Scan Range /Table/3/1/110/2/1 /Table/3/1/111/2/1 + + +# end on the first key of the second table is inclusive +scan_descriptors_in_span start=110/1 end=111/1/0 +---- +catalog: + "110": + descriptor: relation + "111": + descriptor: relation +trace: +- Scan Range /Table/3/1/110/2/1 /Table/3/1/112/2/1 + +# end on an absurd key +scan_descriptors_in_span start=110/2/f end=9000/1 +---- +catalog: + "110": + descriptor: relation + "111": + descriptor: relation +trace: +- Scan Range /Table/3/1/110/2/1 /Table/3/1/9000/2/1 + +# start in the middle of the first table +scan_descriptors_in_span start=110/1/1 end=112/1 +---- +catalog: + "110": + descriptor: relation + "111": + descriptor: relation +trace: +- Scan Range /Table/3/1/110/2/1 /Table/3/1/112/2/1 + +# start on the last key of the first table +scan_descriptors_in_span start=110/2/f end=112/1 +---- +catalog: + "110": + descriptor: relation + "111": + descriptor: relation +trace: +- Scan Range /Table/3/1/110/2/1 /Table/3/1/112/2/1 + +# start on the first key of the second table +scan_descriptors_in_span start=111/1 end=112/1 +---- +catalog: + "111": + descriptor: relation +trace: +- Scan Range /Table/3/1/111/2/1 /Table/3/1/112/2/1 + +# verify that multiple span scanning works +scan_descriptors_in_multiple_spans first=(110/1,111/1) second=(111/1,112/1) +---- +catalog: + "110": + descriptor: relation + "111": + descriptor: relation +trace: +- Scan Range /Table/3/1/110/2/1 /Table/3/1/111/2/1 +- Scan Range /Table/3/1/111/2/1 /Table/3/1/112/2/1