diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index edeb79ea23b1..88c6356e314c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -93,6 +93,7 @@ /pkg/ccl/backupccl/ @cockroachdb/bulk-prs /pkg/ccl/importccl/ @cockroachdb/bulk-prs +/pkg/ccl/spanconfigccl/ @cockroachdb/kv-prs /pkg/ccl/storageccl/ @cockroachdb/bulk-prs /pkg/cloud/ @cockroachdb/bulk-prs /pkg/sql/distsql_plan_csv.go @cockroachdb/bulk-prs @@ -221,7 +222,7 @@ /pkg/scheduledjobs/ @cockroachdb/bulk-prs /pkg/security/ @cockroachdb/server-prs /pkg/settings/ @cockroachdb/server-prs -/pkg/spanconfig/ @cockroachdb/multiregion +/pkg/spanconfig/ @cockroachdb/kv-prs /pkg/startupmigrations/ @cockroachdb/server-prs @cockroachdb/sql-schema /pkg/streaming/ @cockroachdb/bulk-prs /pkg/testutils/ @cockroachdb/test-eng-noreview diff --git a/pkg/BUILD.bazel b/pkg/BUILD.bazel index 736c629d4a9e..9f933c304109 100644 --- a/pkg/BUILD.bazel +++ b/pkg/BUILD.bazel @@ -32,6 +32,7 @@ ALL_TESTS = [ "//pkg/ccl/partitionccl:partitionccl_test", "//pkg/ccl/serverccl/diagnosticsccl:diagnosticsccl_test", "//pkg/ccl/serverccl:serverccl_test", + "//pkg/ccl/spanconfigccl:spanconfigccl_test", "//pkg/ccl/sqlproxyccl/denylist:denylist_test", "//pkg/ccl/sqlproxyccl/idle:idle_test", "//pkg/ccl/sqlproxyccl/tenant:tenant_test", diff --git a/pkg/ccl/changefeedccl/helpers_tenant_shim_test.go b/pkg/ccl/changefeedccl/helpers_tenant_shim_test.go index c493b13ec8da..29bfc216faa1 100644 --- a/pkg/ccl/changefeedccl/helpers_tenant_shim_test.go +++ b/pkg/ccl/changefeedccl/helpers_tenant_shim_test.go @@ -70,6 +70,7 @@ func (t *testServerShim) Clock() *hlc.Clock { panic(unsuppor func (t *testServerShim) DistSenderI() interface{} { panic(unsupportedShimMethod) } func (t *testServerShim) MigrationServer() interface{} { panic(unsupportedShimMethod) } func (t *testServerShim) SpanConfigAccessor() interface{} { panic(unsupportedShimMethod) } +func (t *testServerShim) SpanConfigSQLTranslator() interface{} { panic(unsupportedShimMethod) } func (t *testServerShim) SQLServer() interface{} { panic(unsupportedShimMethod) } func (t *testServerShim) SQLLivenessProvider() interface{} { panic(unsupportedShimMethod) } func (t *testServerShim) StartupMigrationsManager() interface{} { panic(unsupportedShimMethod) } diff --git a/pkg/ccl/spanconfigccl/BUILD.bazel b/pkg/ccl/spanconfigccl/BUILD.bazel new file mode 100644 index 000000000000..71f6aa384305 --- /dev/null +++ b/pkg/ccl/spanconfigccl/BUILD.bazel @@ -0,0 +1,32 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_test") + +go_test( + name = "spanconfigccl_test", + srcs = [ + "datadriven_test.go", + "main_test.go", + ], + data = glob(["testdata/**"]), + deps = [ + "//pkg/base", + "//pkg/ccl/partitionccl", + "//pkg/ccl/utilccl", + "//pkg/config/zonepb", + "//pkg/roachpb:with-mocks", + "//pkg/security", + "//pkg/security/securitytest", + "//pkg/server", + "//pkg/spanconfig", + "//pkg/sql", + "//pkg/sql/catalog/catalogkv", + "//pkg/sql/catalog/descpb", + "//pkg/testutils/serverutils", + "//pkg/testutils/sqlutils", + "//pkg/testutils/testcluster", + "//pkg/util/leaktest", + "//pkg/util/log", + "//pkg/util/randutil", + "@com_github_cockroachdb_datadriven//:datadriven", + "@com_github_stretchr_testify//require", + ], +) diff --git a/pkg/ccl/spanconfigccl/datadriven_test.go b/pkg/ccl/spanconfigccl/datadriven_test.go new file mode 100644 index 000000000000..bb287293f5ef --- /dev/null +++ b/pkg/ccl/spanconfigccl/datadriven_test.go @@ -0,0 +1,219 @@ +// Copyright 2021 The Cockroach Authors. +// +// Licensed as a CockroachDB Enterprise file under the Cockroach Community +// License (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// https://github.com/cockroachdb/cockroach/blob/master/licenses/CCL.txt + +package spanconfigccl_test + +import ( + "context" + "fmt" + "reflect" + "strings" + "testing" + + "github.com/cockroachdb/cockroach/pkg/base" + _ "github.com/cockroachdb/cockroach/pkg/ccl/partitionccl" + "github.com/cockroachdb/cockroach/pkg/config/zonepb" + "github.com/cockroachdb/cockroach/pkg/roachpb" + "github.com/cockroachdb/cockroach/pkg/spanconfig" + "github.com/cockroachdb/cockroach/pkg/sql" + "github.com/cockroachdb/cockroach/pkg/sql/catalog/catalogkv" + "github.com/cockroachdb/cockroach/pkg/sql/catalog/descpb" + "github.com/cockroachdb/cockroach/pkg/testutils/sqlutils" + "github.com/cockroachdb/cockroach/pkg/testutils/testcluster" + "github.com/cockroachdb/cockroach/pkg/util/leaktest" + "github.com/cockroachdb/cockroach/pkg/util/log" + "github.com/cockroachdb/datadriven" + "github.com/stretchr/testify/require" +) + +// TestSQLTranslatorDataDriven is a data-driven test for the +// spanconfigsqltranslator.SQLTranslator. It allows users to set up zone config +// hierarchies and validate their translation to SpanConfigs is as expected. +// Only fields that are different from the (default) RANGE DEFAULT are printed +// in the test output for readability. +// +// It offers the following commands: +// +// "exec-sql": executes the input SQL query. +// +// "query-sql": executes the input SQL query and prints the results. +// +// "translate [database=] [table=] [named-zone=] +// [id=]: +// translates the SQL zone config state to the span config state starting from +// the referenced object (named zone, database, database + table, or id) as the +// root. Also see spanconfig.SQLTranslator.Translate(). +// +// "full-translate": performs a full translation of the SQL zone config state +// to the implied span config state. Also see +// spanconfig.SQLTranslator.FullTranslate(). +// TODO(arul): Add a secondary tenant configuration for this test as well. +func TestSQLTranslatorDataDriven(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + + ctx := context.Background() + datadriven.Walk(t, "testdata/", func(t *testing.T, path string) { + tc := testcluster.StartTestCluster(t, 1, base.TestClusterArgs{ + ServerArgs: base.TestServerArgs{ + EnableSpanConfigs: true, + Knobs: base.TestingKnobs{ + SpanConfig: &spanconfig.TestingKnobs{ + ManagerDisableJobCreation: true, + }, + }, + }, + }) + defer tc.Stopper().Stop(ctx) + sqlDB := tc.ServerConn(0 /* idx */) + + datadriven.RunTest(t, path, func(t *testing.T, d *datadriven.TestData) string { + switch d.Cmd { + case "exec-sql": + _, err := sqlDB.Exec(d.Input) + if err != nil { + return err.Error() + } + case "query-sql": + rows, err := sqlDB.Query(d.Input) + if err != nil { + return err.Error() + } + output, err := sqlutils.RowsToDataDrivenOutput(rows) + require.NoError(t, err) + return output + case "translate": + // Parse the args to get the object ID we're interested in translating. + objID := descpb.InvalidID + if d.HasArg(namedZone) { + var zone string + d.ScanArgs(t, namedZone, &zone) + if namedZoneID, found := zonepb.NamedZones[zonepb.NamedZone(zone)]; found { + objID = descpb.ID(namedZoneID) + } else { + t.Fatalf("unkown named zone: %s", zone) + } + } else if d.HasArg(database) { + var dbName string + d.ScanArgs(t, database, &dbName) + if d.HasArg(table) { + var tbName string + d.ScanArgs(t, table, &tbName) + tableDesc := catalogkv.TestingGetTableDescriptor( + tc.Server(0).DB(), + tc.Server(0).ExecutorConfig().(sql.ExecutorConfig).Codec, + dbName, + tbName, + ) + objID = tableDesc.GetID() + } else { + dbDesc := catalogkv.TestingGetDatabaseDescriptor( + tc.Server(0).DB(), + tc.Server(0).ExecutorConfig().(sql.ExecutorConfig).Codec, + dbName, + ) + objID = dbDesc.GetID() + } + } else if d.HasArg(id) { + var scanID int + d.ScanArgs(t, id, &scanID) + objID = descpb.ID(scanID) + } else { + t.Fatal("insufficient args provided to translate") + } + sqlTranslator := tc.Server(0).SpanConfigSQLTranslator().(spanconfig.SQLTranslator) + entries, err := sqlTranslator.Translate(ctx, descpb.IDs{objID}) + if err != nil { + return err.Error() + } + require.NoError(t, err) + return datadrivenTranslationResult(entries) + case "full-translate": + sqlTranslator := tc.Server(0).SpanConfigSQLTranslator().(spanconfig.SQLTranslator) + entries, _, err := sqlTranslator.FullTranslate(ctx) + require.NoError(t, err) + return datadrivenTranslationResult(entries) + default: + t.Fatalf("unknown command: %s", d.Cmd) + } + return "" + }) + }) +} + +// Constants for data-driven args. +const ( + id = "id" + namedZone = "named-zone" + table = "table" + database = "database" +) + +// datadrivenTranslationResult constructs the datadriven output for a given +// slice of (translated) span config entries. +func datadrivenTranslationResult(entries []roachpb.SpanConfigEntry) string { + var output strings.Builder + for _, entry := range entries { + res := diffEntryAgainstRangeDefault(entry) + output.WriteString(res) + output.WriteByte('\n') + } + return output.String() +} + +// diffEntryAgainstRangeDefault computes the difference between the given config +// and RANGE DEFAULT. It then constructs a (span<->mismatching field(s)) string +// and returns it. If there config is same as RANGE DEFAULT, a (span, DEFAULT) +// string is returned instead. +func diffEntryAgainstRangeDefault(entry roachpb.SpanConfigEntry) string { + var res strings.Builder + defaultSpanConfig := zonepb.DefaultZoneConfig().AsSpanConfig() + var diffs []string + + if entry.Config.RangeMaxBytes != defaultSpanConfig.RangeMaxBytes { + diffs = append(diffs, fmt.Sprintf("range_max_bytes=%d", entry.Config.RangeMaxBytes)) + } + if entry.Config.RangeMinBytes != defaultSpanConfig.RangeMinBytes { + diffs = append(diffs, fmt.Sprintf("range_min_bytes=%d", entry.Config.RangeMinBytes)) + } + if entry.Config.GCPolicy.TTLSeconds != defaultSpanConfig.GCPolicy.TTLSeconds { + diffs = append(diffs, fmt.Sprintf("ttl_seconds=%d", entry.Config.GCPolicy.TTLSeconds)) + } + if entry.Config.GlobalReads != defaultSpanConfig.GlobalReads { + diffs = append(diffs, fmt.Sprintf("global_reads=%v", entry.Config.GlobalReads)) + } + if entry.Config.NumReplicas != defaultSpanConfig.NumReplicas { + diffs = append(diffs, fmt.Sprintf("num_replicas=%d", entry.Config.NumReplicas)) + } + if entry.Config.NumVoters != defaultSpanConfig.NumVoters { + diffs = append(diffs, fmt.Sprintf("num_voters=%d", entry.Config.NumVoters)) + } + if !reflect.DeepEqual(entry.Config.Constraints, defaultSpanConfig.Constraints) { + diffs = append(diffs, fmt.Sprintf("constraints=%v", entry.Config.Constraints)) + } + if !reflect.DeepEqual(entry.Config.VoterConstraints, defaultSpanConfig.VoterConstraints) { + diffs = append(diffs, fmt.Sprintf("voter_constraints=%v", entry.Config.VoterConstraints)) + } + if !reflect.DeepEqual(entry.Config.LeasePreferences, defaultSpanConfig.LeasePreferences) { + diffs = append(diffs, fmt.Sprintf("lease_preferences=%v", entry.Config.VoterConstraints)) + } + + if len(diffs) != 0 { + for i, diff := range diffs { + if i == 0 { + res.WriteString(fmt.Sprintf("%-30s %s", entry.Span.String(), diff)) + } else { + res.WriteByte(' ') + res.WriteString(diff) + } + } + } else { + res.WriteString(fmt.Sprintf("%-30s %s", entry.Span.String(), "DEFAULT")) + } + return res.String() +} diff --git a/pkg/ccl/spanconfigccl/main_test.go b/pkg/ccl/spanconfigccl/main_test.go new file mode 100644 index 000000000000..ac929a3c3a94 --- /dev/null +++ b/pkg/ccl/spanconfigccl/main_test.go @@ -0,0 +1,33 @@ +// Copyright 2021 The Cockroach Authors. +// +// Licensed as a CockroachDB Enterprise file under the Cockroach Community +// License (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// https://github.com/cockroachdb/cockroach/blob/master/licenses/CCL.txt + +package spanconfigccl_test + +import ( + "os" + "testing" + + "github.com/cockroachdb/cockroach/pkg/ccl/utilccl" + "github.com/cockroachdb/cockroach/pkg/security" + "github.com/cockroachdb/cockroach/pkg/security/securitytest" + "github.com/cockroachdb/cockroach/pkg/server" + "github.com/cockroachdb/cockroach/pkg/testutils/serverutils" + "github.com/cockroachdb/cockroach/pkg/testutils/testcluster" + "github.com/cockroachdb/cockroach/pkg/util/randutil" +) + +//go:generate ../../util/leaktest/add-leaktest.sh *_test.go + +func TestMain(m *testing.M) { + defer utilccl.TestingEnableEnterprise()() + security.SetAssetLoader(securitytest.EmbeddedAssets) + randutil.SeedForTests() + serverutils.InitTestServerFactory(server.TestServerFactory) + serverutils.InitTestClusterFactory(testcluster.TestClusterFactory) + os.Exit(m.Run()) +} diff --git a/pkg/ccl/spanconfigccl/testdata/databases b/pkg/ccl/spanconfigccl/testdata/databases new file mode 100644 index 000000000000..4f4410e13de3 --- /dev/null +++ b/pkg/ccl/spanconfigccl/testdata/databases @@ -0,0 +1,49 @@ +# Create a database with some tables, types, and schemas. Check that span +# configurations are as we expect. + +exec-sql +CREATE DATABASE db; +CREATE TABLE db.t1(); +CREATE TYPE db.typ AS ENUM(); +CREATE SCHEMA db.sc; +CREATE TABLE db.t2(); +---- + +query-sql +SELECT id FROM system.namespace WHERE name='t1' +---- +53 + +query-sql +SELECT id FROM system.namespace WHERE name='t2' +---- +57 + +# We only expect there to be span config entries for tables t1 and t2. +translate database=db +---- +/Table/5{3-4} DEFAULT +/Table/5{7-8} DEFAULT + +# Alter zone config fields on the database and one of the tables to ensure +# things are cascading. +exec-sql +ALTER DATABASE db CONFIGURE ZONE USING num_replicas=7; +ALTER TABLE db.t1 CONFIGURE ZONE USING num_voters=5; +---- + +translate database=db +---- +/Table/5{3-4} num_replicas=7 num_voters=5 +/Table/5{7-8} num_replicas=7 + +# Translating the tables in the database individually should result in the same +# config as above. + +translate database=db table=t1 +---- +/Table/5{3-4} num_replicas=7 num_voters=5 + +translate database=db table=t2 +---- +/Table/5{7-8} num_replicas=7 diff --git a/pkg/ccl/spanconfigccl/testdata/full_translate b/pkg/ccl/spanconfigccl/testdata/full_translate new file mode 100644 index 000000000000..624d8ee21522 --- /dev/null +++ b/pkg/ccl/spanconfigccl/testdata/full_translate @@ -0,0 +1,66 @@ +# This file tests a full reconciliation scenario when none of the named zone +# entries have been messed with and the user has created a single database +# and a single table inside that database. + +exec-sql +CREATE DATABASE db; +CREATE SCHEMA sc; +CREATE TYPE typ AS ENUM(); +CREATE TABLE db.t(); +---- + +# - User created table above, with ID 56. +# There should be no entry for IDs 52, 53, 54, and 55 as these belong to the +# database, schema, type, and type alias respectively. +# - All system tables. Note that there should be no entry for pseudo IDs or IDs +# for which no table exists. +# - NodeLivenessSpan +# - Meta ranges: min -> NodeLiveness start +# - System ranges: +# - NodeLiveness end -> TimeSeries Start +# - TimeSeries end -> System Ranges end +# - Time Series Span +full-translate +---- +/Table/5{6-7} DEFAULT +/Table/{3-4} num_replicas=5 +/Table/{4-5} num_replicas=5 +/Table/{5-6} num_replicas=5 +/Table/{6-7} num_replicas=5 +/Table/{8-9} num_replicas=5 +/Table/1{1-2} num_replicas=5 +/Table/1{2-3} num_replicas=5 +/Table/1{3-4} num_replicas=5 +/Table/1{4-5} num_replicas=5 +/Table/1{5-6} num_replicas=5 +/Table/{19-20} num_replicas=5 +/Table/2{0-1} num_replicas=5 +/Table/2{1-2} num_replicas=5 +/Table/2{3-4} num_replicas=5 +/Table/2{4-5} num_replicas=5 +/Table/2{5-6} ttl_seconds=600 num_replicas=5 +/Table/2{6-7} num_replicas=5 +/Table/2{7-8} ttl_seconds=600 num_replicas=5 +/Table/2{8-9} num_replicas=5 +/NamespaceTable/{30-Max} num_replicas=5 +/{NamespaceTable/Max-Table/32} num_replicas=5 +/Table/3{2-3} num_replicas=5 +/Table/3{3-4} num_replicas=5 +/Table/3{4-5} num_replicas=5 +/Table/3{5-6} num_replicas=5 +/Table/3{6-7} num_replicas=5 +/Table/3{7-8} num_replicas=5 +/Table/{39-40} num_replicas=5 +/Table/4{0-1} num_replicas=5 +/Table/4{1-2} num_replicas=5 +/Table/4{2-3} num_replicas=5 +/Table/4{3-4} num_replicas=5 +/Table/4{4-5} num_replicas=5 +/Table/4{5-6} ttl_seconds=7200 num_replicas=5 +/Table/4{6-7} num_replicas=5 +/Table/4{7-8} num_replicas=5 +/System/NodeLiveness{-Max} ttl_seconds=600 num_replicas=5 +/{Min-System/NodeLiveness} ttl_seconds=3600 num_replicas=5 +/System/{NodeLivenessMax-tsd} num_replicas=5 +/System{tse-/Max} num_replicas=5 +/System{/tsd-tse} DEFAULT diff --git a/pkg/ccl/spanconfigccl/testdata/full_translate_named_zones_deleted b/pkg/ccl/spanconfigccl/testdata/full_translate_named_zones_deleted new file mode 100644 index 000000000000..5095938c5860 --- /dev/null +++ b/pkg/ccl/spanconfigccl/testdata/full_translate_named_zones_deleted @@ -0,0 +1,79 @@ +# This file tests a full translation when all named zone entries have been +# removed from system.zones. The expectation is for us to still generate span +# config updates for these named zones. These should be the same as RANGE +# DEFAULT because that's what they inherit from in the absence of an explicit +# zone config. + +query-sql +SELECT id FROM system.zones +---- +0 +1 +16 +17 +22 +25 +27 +45 + +# Note that discarding RANGE_DEFAULT isn't allowed. +exec-sql +ALTER RANGE liveness CONFIGURE ZONE DISCARD; +ALTER RANGE meta CONFIGURE ZONE DISCARD; +ALTER RANGE system CONFIGURE ZONE DISCARD; +ALTER RANGE timeseries CONFIGURE ZONE DISCARD; +---- + +query-sql +SELECT id FROM system.zones +---- +0 +1 +25 +27 +45 + + +full-translate +---- +/Table/{3-4} num_replicas=5 +/Table/{4-5} num_replicas=5 +/Table/{5-6} num_replicas=5 +/Table/{6-7} num_replicas=5 +/Table/{8-9} num_replicas=5 +/Table/1{1-2} num_replicas=5 +/Table/1{2-3} num_replicas=5 +/Table/1{3-4} num_replicas=5 +/Table/1{4-5} num_replicas=5 +/Table/1{5-6} num_replicas=5 +/Table/{19-20} num_replicas=5 +/Table/2{0-1} num_replicas=5 +/Table/2{1-2} num_replicas=5 +/Table/2{3-4} num_replicas=5 +/Table/2{4-5} num_replicas=5 +/Table/2{5-6} ttl_seconds=600 num_replicas=5 +/Table/2{6-7} num_replicas=5 +/Table/2{7-8} ttl_seconds=600 num_replicas=5 +/Table/2{8-9} num_replicas=5 +/NamespaceTable/{30-Max} num_replicas=5 +/{NamespaceTable/Max-Table/32} num_replicas=5 +/Table/3{2-3} num_replicas=5 +/Table/3{3-4} num_replicas=5 +/Table/3{4-5} num_replicas=5 +/Table/3{5-6} num_replicas=5 +/Table/3{6-7} num_replicas=5 +/Table/3{7-8} num_replicas=5 +/Table/{39-40} num_replicas=5 +/Table/4{0-1} num_replicas=5 +/Table/4{1-2} num_replicas=5 +/Table/4{2-3} num_replicas=5 +/Table/4{3-4} num_replicas=5 +/Table/4{4-5} num_replicas=5 +/Table/4{5-6} ttl_seconds=7200 num_replicas=5 +/Table/4{6-7} num_replicas=5 +/Table/4{7-8} num_replicas=5 +/System/NodeLiveness{-Max} DEFAULT +/{Min-System/NodeLiveness} DEFAULT +/System/{NodeLivenessMax-tsd} DEFAULT +/System{tse-/Max} DEFAULT +/System{/tsd-tse} DEFAULT diff --git a/pkg/ccl/spanconfigccl/testdata/indexes b/pkg/ccl/spanconfigccl/testdata/indexes new file mode 100644 index 000000000000..a8d19aea1b7e --- /dev/null +++ b/pkg/ccl/spanconfigccl/testdata/indexes @@ -0,0 +1,94 @@ +# We start off by creating a simple database -> table -> index hierarchy. We set +# a zone configuration on the index but not on the table. This means the table +# has a "placeholder zone config". + +exec-sql +CREATE DATABASE db; +CREATE TABLE db.t(i INT PRIMARY KEY, j INT); +CREATE INDEX idx ON db.t (j); +ALTER DATABASE db CONFIGURE ZONE USING num_replicas=7; +ALTER INDEX db.t@idx CONFIGURE ZONE USING num_voters = 5; +---- + +query-sql +SHOW ZONE CONFIGURATION FOR DATABASE db +---- +DATABASE db ALTER DATABASE db CONFIGURE ZONE USING + range_min_bytes = 134217728, + range_max_bytes = 536870912, + gc.ttlseconds = 90000, + num_replicas = 7, + constraints = '[]', + lease_preferences = '[]' + +query-sql +SHOW ZONE CONFIGURATION FOR TABLE db.t +---- +DATABASE db ALTER DATABASE db CONFIGURE ZONE USING + range_min_bytes = 134217728, + range_max_bytes = 536870912, + gc.ttlseconds = 90000, + num_replicas = 7, + constraints = '[]', + lease_preferences = '[]' + +query-sql +SHOW ZONE CONFIGURATION FOR INDEX db.t@idx +---- +INDEX db.public.t@idx ALTER INDEX db.public.t@idx CONFIGURE ZONE USING + range_min_bytes = 134217728, + range_max_bytes = 536870912, + gc.ttlseconds = 90000, + num_replicas = 7, + num_voters = 5, + constraints = '[]', + voter_constraints = '[]', + lease_preferences = '[]' + +# First entry = primary index (table's config above) +# Second entry = index idx, so numvoters should be overridden. +# Third entry = Any future indexes that may be added to this table. Should be +# the same as the table's config. +translate database=db table=t +---- +/Table/53{-/2} num_replicas=7 +/Table/53/{2-3} num_replicas=7 num_voters=5 +/Table/5{3/3-4} num_replicas=7 + +# Configure GC ttl on the database and override it for the index. The table +# continues to hold a placeholder zone config. +exec-sql +ALTER DATABASE db CONFIGURE ZONE USING gc.ttlseconds = 3600; +ALTER INDEX db.t@idx CONFIGURE ZONE USING gc.ttlseconds = 25 +---- + +translate database=db table=t +---- +/Table/53{-/2} ttl_seconds=3600 num_replicas=7 +/Table/53/{2-3} ttl_seconds=25 num_replicas=7 num_voters=5 +/Table/5{3/3-4} ttl_seconds=3600 num_replicas=7 + +# Configure a zone config field on the table, so that it is no longer a +# placeholder zone config. +exec-sql +ALTER TABLE db.t CONFIGURE ZONE USING range_min_bytes = 1000, range_max_bytes=100000; +---- + +query-sql +SHOW ZONE CONFIGURATION FOR INDEX db.t@idx +---- +INDEX db.public.t@idx ALTER INDEX db.public.t@idx CONFIGURE ZONE USING + range_min_bytes = 1000, + range_max_bytes = 100000, + gc.ttlseconds = 25, + num_replicas = 7, + num_voters = 5, + constraints = '[]', + voter_constraints = '[]', + lease_preferences = '[]' + +translate database=db table=t +---- +/Table/53{-/2} range_max_bytes=100000 range_min_bytes=1000 ttl_seconds=3600 num_replicas=7 +/Table/53/{2-3} range_max_bytes=100000 range_min_bytes=1000 ttl_seconds=25 num_replicas=7 num_voters=5 +/Table/5{3/3-4} range_max_bytes=100000 range_min_bytes=1000 ttl_seconds=3600 num_replicas=7 diff --git a/pkg/ccl/spanconfigccl/testdata/misc b/pkg/ccl/spanconfigccl/testdata/misc new file mode 100644 index 000000000000..6ed5ae8d6af3 --- /dev/null +++ b/pkg/ccl/spanconfigccl/testdata/misc @@ -0,0 +1,55 @@ +# Miscellaneous edge case tests for the SQLTranslator. + +# Test dropped tables/databases work correctly. + +exec-sql +CREATE DATABASE db; +CREATE TABLE db.t1(); +CREATE TABLE db.t2(); +ALTER TABLE db.t1 CONFIGURE ZONE USING num_replicas=5; +---- + +translate database=db +---- +/Table/5{3-4} num_replicas=5 +/Table/5{4-5} DEFAULT + +# Drop the table. +exec-sql +DROP TABLE db.t1; +---- + +# We still should be able to generate the span configuration for it when +# starting our translation from the database. +translate database=db +---- +/Table/5{3-4} num_replicas=5 +/Table/5{4-5} DEFAULT + +# Same as above, except this time the translation starts from the table's ID. +translate id=53 +---- +/Table/5{3-4} num_replicas=5 + +# Test schemas/types don't generate a span configuration. +exec-sql +CREATE SCHEMA db.sc; +CREATE TYPE db.typ AS ENUM(); +---- + +# Schema. +translate id=55 +---- + +# Enum. +translate id=56 +---- + +# Array type alias. +translate id=57 +---- + +# Test that non-existent IDs do not generate span configurations either. + +translate id=500 +---- diff --git a/pkg/ccl/spanconfigccl/testdata/named_zones b/pkg/ccl/spanconfigccl/testdata/named_zones new file mode 100644 index 000000000000..9ef8220e53bc --- /dev/null +++ b/pkg/ccl/spanconfigccl/testdata/named_zones @@ -0,0 +1,45 @@ +# This test generates span configurations for various named zone IDs. No tests +# for RANGE DEFAULT are included here as generating span configurations for +# RANGE DEFAULT is akin to a full reconciliation. + + +# ID 18 is for the time series range which is the only named zone that doesn't +# have an entry in `system.zones` at bootstrap. It should inherit from RANGE +# DEFAULT. +query-sql +SELECT count(*) FROM system.zones WHERE id=18 +---- +0 + +translate named-zone=timeseries +---- +/System{/tsd-tse} DEFAULT + +# Adding an explicit zone configuration for the timeseries range should work +# as expected. +exec-sql +ALTER RANGE timeseries CONFIGURE ZONE USING gc.ttlseconds=1000 +---- + +translate named-zone=timeseries +---- +/System{/tsd-tse} ttl_seconds=1000 + +# Change a field on the liveness range and ensure it behaves as expected. +exec-sql +ALTER RANGE liveness CONFIGURE ZONE USING num_replicas=7; +---- + +translate named-zone=liveness +---- +/System/NodeLiveness{-Max} ttl_seconds=600 num_replicas=7 + +# We are allowed to discard the liveness range's zone configuration. The +# generated span should have the RANGE DEFAULT config. +exec-sql +ALTER RANGE liveness CONFIGURE ZONE DISCARD +---- + +translate named-zone=liveness +---- +/System/NodeLiveness{-Max} DEFAULT diff --git a/pkg/ccl/spanconfigccl/testdata/partitions_primary_index b/pkg/ccl/spanconfigccl/testdata/partitions_primary_index new file mode 100644 index 000000000000..69e0c62377fc --- /dev/null +++ b/pkg/ccl/spanconfigccl/testdata/partitions_primary_index @@ -0,0 +1,138 @@ +# This test creates partitions on a table's primary index and moves the zone +# configurations on these partitions through the steps described inline, making +# assertions along the way. + +exec-sql +CREATE DATABASE db; +CREATE TABLE db.t(i INT PRIMARY KEY, j INT) PARTITION BY LIST (i) ( + PARTITION one_two VALUES IN (1, 2), + PARTITION three_four VALUES IN (3, 4), + PARTITION default VALUES IN (default) +); +ALTER DATABASE db CONFIGURE ZONE USING num_replicas=7; +ALTER TABLE db.t CONFIGURE ZONE USING num_voters=5; +---- + +query-sql +SHOW ZONE CONFIGURATION FOR DATABASE db +---- +DATABASE db ALTER DATABASE db CONFIGURE ZONE USING + range_min_bytes = 134217728, + range_max_bytes = 536870912, + gc.ttlseconds = 90000, + num_replicas = 7, + constraints = '[]', + lease_preferences = '[]' + +query-sql +SHOW ZONE CONFIGURATION FOR TABLE db.t +---- +TABLE db.public.t ALTER TABLE db.public.t CONFIGURE ZONE USING + range_min_bytes = 134217728, + range_max_bytes = 536870912, + gc.ttlseconds = 90000, + num_replicas = 7, + num_voters = 5, + constraints = '[]', + voter_constraints = '[]', + lease_preferences = '[]' + +query-sql +SHOW ZONE CONFIGURATION FOR PARTITION one_two OF TABLE db.t +---- +TABLE db.public.t ALTER TABLE db.public.t CONFIGURE ZONE USING + range_min_bytes = 134217728, + range_max_bytes = 536870912, + gc.ttlseconds = 90000, + num_replicas = 7, + num_voters = 5, + constraints = '[]', + voter_constraints = '[]', + lease_preferences = '[]' + + +# There is no zone configuration set on any of the partitions initially. So +# there is just one entry here which covers the entire table's span. +translate database=db table=t +---- +/Table/5{3-4} num_replicas=7 num_voters=5 + + +exec-sql +ALTER PARTITION one_two OF TABLE db.t CONFIGURE ZONE USING global_reads=true +---- + +# Now that we have a zone configuration on one of the partitions, `one_two`, +# which implies two (adjacent) spans. Both these configs have global reads set +# to true. The table's spans before and after the partitions span continue to +# have the table's zone configuration. +translate database=db table=t +---- +/Table/53{-/1/1} num_replicas=7 num_voters=5 +/Table/53/1/{1-2} global_reads=true num_replicas=7 num_voters=5 +/Table/53/1/{2-3} global_reads=true num_replicas=7 num_voters=5 +/Table/5{3/1/3-4} num_replicas=7 num_voters=5 + +# Change two fields on the second partition. One of them (num_voters) is an +# override on the value set on the database's zone config. The other, gc.ttlseconds, +# is not set on either the table or the database. +exec-sql +ALTER PARTITION three_four OF TABLE db.t CONFIGURE ZONE USING gc.ttlseconds=5; +ALTER PARTITION three_four OF TABLE db.t CONFIGURE ZONE USING num_voters=3 +---- + +# We expect 2 more (adjacent) spans for the second partition now. These should +# have the correct values of gc.ttlseconds (5) and num_voters (3). +translate database=db table=t +---- +/Table/53{-/1/1} num_replicas=7 num_voters=5 +/Table/53/1/{1-2} global_reads=true num_replicas=7 num_voters=5 +/Table/53/1/{2-3} global_reads=true num_replicas=7 num_voters=5 +/Table/53/1/{3-4} ttl_seconds=5 num_replicas=7 num_voters=3 +/Table/53/1/{4-5} ttl_seconds=5 num_replicas=7 num_voters=3 +/Table/5{3/1/5-4} num_replicas=7 num_voters=5 + +exec-sql +ALTER PARTITION default OF TABLE db.t CONFIGURE ZONE USING num_voters=6 +---- + +# Now that we've set a zone configuration on the default partition we get a few +# more entries. Of note is that this default partition is on the primary index. +# This results in a span before the primary index starts +# (which will always be empty) but has the span config of the table. We also have +# a span for all other secondary indexes with the same config at the end. +# We get two more spans that subdivide the primary index as well. One before the +# start of the first partition and one after the end of the second partition. This +# second span covers the remaining primary index. Both these spans have num_voters +# set to 6, as that's what we did above. +translate database=db table=t +---- +/Table/53{-/1} num_replicas=7 num_voters=5 +/Table/53/1{-/1} num_replicas=7 num_voters=6 +/Table/53/1/{1-2} global_reads=true num_replicas=7 num_voters=5 +/Table/53/1/{2-3} global_reads=true num_replicas=7 num_voters=5 +/Table/53/1/{3-4} ttl_seconds=5 num_replicas=7 num_voters=3 +/Table/53/1/{4-5} ttl_seconds=5 num_replicas=7 num_voters=3 +/Table/53/{1/5-2} num_replicas=7 num_voters=6 +/Table/5{3/2-4} num_replicas=7 num_voters=5 + +# Discard the table's zone configuration. This essentially means that the table +# has a "placeholder" zone config (to capture partition subzone configs). +exec-sql +ALTER TABLE db.t CONFIGURE ZONE DISCARD +---- + +# The expectation here is very similar to above, except the spans before the +# primary index and for secondary indexes now inherit num_voters from the +# database. This is because we removed the explicit zone configuration on the +# table above (which is where num_voters=5 was coming from). +translate database=db table=t +---- +/Table/53{-/1} num_replicas=7 +/Table/53/1{-/1} num_replicas=7 num_voters=6 +/Table/53/1/{1-2} global_reads=true num_replicas=7 +/Table/53/1/{2-3} global_reads=true num_replicas=7 +/Table/53/1/{3-4} ttl_seconds=5 num_replicas=7 num_voters=3 +/Table/53/1/{4-5} ttl_seconds=5 num_replicas=7 num_voters=3 +/Table/53/{1/5-2} num_replicas=7 num_voters=6 +/Table/5{3/2-4} num_replicas=7 diff --git a/pkg/config/zonepb/BUILD.bazel b/pkg/config/zonepb/BUILD.bazel index c3240f84a361..3cc68c6ca75f 100644 --- a/pkg/config/zonepb/BUILD.bazel +++ b/pkg/config/zonepb/BUILD.bazel @@ -15,6 +15,7 @@ go_library( "//pkg/base", "//pkg/keys", "//pkg/roachpb:with-mocks", + "//pkg/sql/catalog/descpb", "//pkg/sql/opt/cat", "//pkg/sql/sem/tree", "//pkg/util/log", diff --git a/pkg/config/zonepb/zone.go b/pkg/config/zonepb/zone.go index 55657f3a0dbf..6f239ef57306 100644 --- a/pkg/config/zonepb/zone.go +++ b/pkg/config/zonepb/zone.go @@ -19,6 +19,7 @@ import ( "github.com/cockroachdb/cockroach/pkg/base" "github.com/cockroachdb/cockroach/pkg/keys" "github.com/cockroachdb/cockroach/pkg/roachpb" + "github.com/cockroachdb/cockroach/pkg/sql/catalog/descpb" "github.com/cockroachdb/cockroach/pkg/sql/opt/cat" "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" "github.com/cockroachdb/cockroach/pkg/util/log" @@ -26,20 +27,35 @@ import ( "github.com/gogo/protobuf/proto" ) +// NamedZone is a custom type for names used to reference ranges outside the +// SQL keyspace by zone configurations. +type NamedZone string + // Several ranges outside of the SQL keyspace are given special names so they // can be targeted by zone configs. const ( - DefaultZoneName = "default" - LivenessZoneName = "liveness" - MetaZoneName = "meta" - SystemZoneName = "system" - TimeseriesZoneName = "timeseries" - TenantsZoneName = "tenants" + DefaultZoneName NamedZone = "default" + LivenessZoneName NamedZone = "liveness" + MetaZoneName NamedZone = "meta" + SystemZoneName NamedZone = "system" + TimeseriesZoneName NamedZone = "timeseries" + TenantsZoneName NamedZone = "tenants" ) +// NamedZonesList is a list of all named zones that that reference ranges +// outside the SQL keyspace which can be referenced by zone configurations. +var NamedZonesList = [...]NamedZone{ + DefaultZoneName, + LivenessZoneName, + MetaZoneName, + SystemZoneName, + TimeseriesZoneName, + TenantsZoneName, +} + // NamedZones maps named zones to their pseudo-table ID that can be used to // install an entry into the system.zones table. -var NamedZones = map[string]uint32{ +var NamedZones = map[NamedZone]uint32{ DefaultZoneName: keys.RootNamespaceID, LivenessZoneName: keys.LivenessRangesID, MetaZoneName: keys.MetaRangesID, @@ -50,14 +66,21 @@ var NamedZones = map[string]uint32{ // NamedZonesByID is the inverse of NamedZones: it maps pseudo-table IDs to // their zone names. -var NamedZonesByID = func() map[uint32]string { - out := map[uint32]string{} +var NamedZonesByID = func() map[uint32]NamedZone { + out := map[uint32]NamedZone{} for name, id := range NamedZones { out[id] = name } return out }() +// IsNamedZoneID returns true if the given ID is one of the pseudo-table IDs +// that maps to named zones. +func IsNamedZoneID(id descpb.ID) bool { + _, ok := NamedZonesByID[(uint32(id))] + return ok +} + // MultiRegionZoneConfigFields are the fields on a zone configuration which // may be set by the system for multi-region objects". var MultiRegionZoneConfigFields = []tree.Name{ @@ -120,10 +143,10 @@ func ResolveZoneSpecifier( // - a database name; // - a table or index name. if zs.NamedZone != "" { - if zs.NamedZone == DefaultZoneName { + if NamedZone(zs.NamedZone) == DefaultZoneName { return keys.RootNamespaceID, nil } - if id, ok := NamedZones[string(zs.NamedZone)]; ok { + if id, ok := NamedZones[NamedZone(zs.NamedZone)]; ok { return id, nil } return 0, fmt.Errorf("%q is not a built-in zone", string(zs.NamedZone)) @@ -1136,7 +1159,7 @@ func (z *ZoneConfig) EnsureFullyHydrated() error { // AsSpanConfig converts a fully hydrated zone configuration to an equivalent // SpanConfig. It fatals if the zone config hasn't been fully hydrated (fields // are expected to have been cascaded through parent zone configs). -func (z *ZoneConfig) AsSpanConfig() roachpb.SpanConfig { +func (z ZoneConfig) AsSpanConfig() roachpb.SpanConfig { spanConfig, err := z.toSpanConfig() if err != nil { log.Fatalf(context.Background(), "%v", err) @@ -1214,3 +1237,13 @@ func (z *ZoneConfig) toSpanConfig() (roachpb.SpanConfig, error) { } return sc, nil } + +func init() { + if len(NamedZonesList) != len(NamedZones) { + panic(fmt.Errorf( + "NamedZonesList (%d) and NamedZones (%d) should have the same number of entries", + len(NamedZones), + len(NamedZonesList), + )) + } +} diff --git a/pkg/config/zonepb/zone_test.go b/pkg/config/zonepb/zone_test.go index 2ac12278f5b2..a0d0d5f8dd4d 100644 --- a/pkg/config/zonepb/zone_test.go +++ b/pkg/config/zonepb/zone_test.go @@ -975,13 +975,13 @@ func TestZoneSpecifiers(t *testing.T) { // Simulate exactly two named zones: one named default and one named carl. // N.B. DefaultZoneName must always exist in the mapping; it is treated // specially so that it always appears first in the lookup path. - defer func(old map[string]uint32) { NamedZones = old }(NamedZones) - NamedZones = map[string]uint32{ + defer func(old map[NamedZone]uint32) { NamedZones = old }(NamedZones) + NamedZones = map[NamedZone]uint32{ DefaultZoneName: 0, "carl": 42, } - defer func(old map[uint32]string) { NamedZonesByID = old }(NamedZonesByID) - NamedZonesByID = map[uint32]string{ + defer func(old map[uint32]NamedZone) { NamedZonesByID = old }(NamedZonesByID) + NamedZonesByID = map[uint32]NamedZone{ 0: DefaultZoneName, 42: "carl", } diff --git a/pkg/keys/spans.go b/pkg/keys/spans.go index f7de69b62043..d611b2ee8a02 100644 --- a/pkg/keys/spans.go +++ b/pkg/keys/spans.go @@ -31,6 +31,9 @@ var ( // NodeLivenessSpan holds the liveness records for nodes in the cluster. NodeLivenessSpan = roachpb.Span{Key: NodeLivenessPrefix, EndKey: NodeLivenessKeyMax} + // TimeseriesSpan holds all the timeseries data in the cluster. + TimeseriesSpan = roachpb.Span{Key: TimeseriesPrefix, EndKey: TimeseriesKeyMax} + // SystemConfigSpan is the range of system objects which will be gossiped. SystemConfigSpan = roachpb.Span{Key: SystemConfigSplitKey, EndKey: SystemConfigTableDataMax} diff --git a/pkg/roachpb/span_config.go b/pkg/roachpb/span_config.go index 546c137bb844..f3ce2a73f475 100644 --- a/pkg/roachpb/span_config.go +++ b/pkg/roachpb/span_config.go @@ -57,6 +57,8 @@ func (s *SpanConfig) TTL() time.Duration { // GetNumVoters returns the number of voting replicas as defined in the // span config. +// TODO(arul): We can get rid of this now that we're correctly populating +// numVoters when going from ZoneConfigs -> SpanConfigs. func (s *SpanConfig) GetNumVoters() int32 { if s.NumVoters != 0 { return s.NumVoters diff --git a/pkg/server/BUILD.bazel b/pkg/server/BUILD.bazel index 957f279c23a8..b347b0d55ff8 100644 --- a/pkg/server/BUILD.bazel +++ b/pkg/server/BUILD.bazel @@ -114,6 +114,7 @@ go_library( "//pkg/spanconfig/spanconfigjob", "//pkg/spanconfig/spanconfigkvaccessor", "//pkg/spanconfig/spanconfigmanager", + "//pkg/spanconfig/spanconfigsqltranslator", "//pkg/sql", "//pkg/sql/catalog/bootstrap", "//pkg/sql/catalog/catalogkeys", diff --git a/pkg/server/server_sql.go b/pkg/server/server_sql.go index 9e7202529132..aa14f3e324b1 100644 --- a/pkg/server/server_sql.go +++ b/pkg/server/server_sql.go @@ -52,6 +52,7 @@ import ( "github.com/cockroachdb/cockroach/pkg/settings/cluster" "github.com/cockroachdb/cockroach/pkg/spanconfig" "github.com/cockroachdb/cockroach/pkg/spanconfig/spanconfigmanager" + "github.com/cockroachdb/cockroach/pkg/spanconfig/spanconfigsqltranslator" "github.com/cockroachdb/cockroach/pkg/sql" "github.com/cockroachdb/cockroach/pkg/sql/catalog/catalogkeys" "github.com/cockroachdb/cockroach/pkg/sql/catalog/descs" @@ -839,6 +840,7 @@ func newSQLServer(ctx context.Context, cfg sqlServerArgs) (*SQLServer, error) { // Instantiate a span config manager. If we're the host tenant we'll // only do it if COCKROACH_EXPERIMENTAL_SPAN_CONFIGS is set. spanConfigKnobs, _ := cfg.TestingKnobs.SpanConfig.(*spanconfig.TestingKnobs) + sqlTranslator := spanconfigsqltranslator.New(execCfg, codec) spanConfigMgr = spanconfigmanager.New( cfg.db, jobRegistry, @@ -846,6 +848,7 @@ func newSQLServer(ctx context.Context, cfg sqlServerArgs) (*SQLServer, error) { cfg.stopper, cfg.Settings, cfg.spanConfigAccessor, + sqlTranslator, spanConfigKnobs, ) execCfg.SpanConfigReconciliationJobDeps = spanConfigMgr diff --git a/pkg/server/testserver.go b/pkg/server/testserver.go index 7c3aa51d4063..cca85671453a 100644 --- a/pkg/server/testserver.go +++ b/pkg/server/testserver.go @@ -43,6 +43,7 @@ import ( "github.com/cockroachdb/cockroach/pkg/server/serverpb" "github.com/cockroachdb/cockroach/pkg/server/status" "github.com/cockroachdb/cockroach/pkg/settings/cluster" + "github.com/cockroachdb/cockroach/pkg/spanconfig/spanconfigsqltranslator" "github.com/cockroachdb/cockroach/pkg/sql" "github.com/cockroachdb/cockroach/pkg/sql/catalog/descpb" "github.com/cockroachdb/cockroach/pkg/sql/pgwire" @@ -937,6 +938,14 @@ func (ts *TestServer) SpanConfigAccessor() interface{} { return ts.Server.node.spanConfigAccessor } +// SpanConfigSQLTranslator is part of TestServerInterface. +func (ts *TestServer) SpanConfigSQLTranslator() interface{} { + if ts.sqlServer.spanconfigMgr != nil { + return ts.sqlServer.spanconfigMgr.SQLTranslator + } + return spanconfigsqltranslator.NewDisabled() +} + // SQLServer is part of TestServerInterface. func (ts *TestServer) SQLServer() interface{} { return ts.PGServer().SQLServer diff --git a/pkg/spanconfig/BUILD.bazel b/pkg/spanconfig/BUILD.bazel index e949d5d82f05..b881396e7ee9 100644 --- a/pkg/spanconfig/BUILD.bazel +++ b/pkg/spanconfig/BUILD.bazel @@ -11,5 +11,7 @@ go_library( deps = [ "//pkg/base", "//pkg/roachpb:with-mocks", + "//pkg/sql/catalog/descpb", + "//pkg/util/hlc", ], ) diff --git a/pkg/spanconfig/spanconfig.go b/pkg/spanconfig/spanconfig.go index 2917bdf889da..1b9794384a9d 100644 --- a/pkg/spanconfig/spanconfig.go +++ b/pkg/spanconfig/spanconfig.go @@ -14,6 +14,8 @@ import ( "context" "github.com/cockroachdb/cockroach/pkg/roachpb" + "github.com/cockroachdb/cockroach/pkg/sql/catalog/descpb" + "github.com/cockroachdb/cockroach/pkg/util/hlc" ) // KVAccessor mediates access to KV span configurations pertaining to a given @@ -33,6 +35,21 @@ type KVAccessor interface { UpdateSpanConfigEntries(ctx context.Context, toDelete []roachpb.Span, toUpsert []roachpb.SpanConfigEntry) error } +// SQLTranslator translates SQL descriptors and their corresponding zone +// configuration state to span configurations. It merely constructs the implied +// span configuration state from SQL's perspective -- it is agnostic to the +// actual span configuration state in KV. +type SQLTranslator interface { + // Translate generates the implied span configuration state given a list of + // {descriptor, named zone} IDs. No entry is returned for an ID if it doesn't + // exist or has been dropped. + Translate(ctx context.Context, ids descpb.IDs) ([]roachpb.SpanConfigEntry, error) + // FullTranslate translates the entire SQL zone configuration state to the + // implied span configuration state. The timestamp at which such a translation + // is valid is al so returned. + FullTranslate(ctx context.Context) ([]roachpb.SpanConfigEntry, hlc.Timestamp, error) +} + // ReconciliationDependencies captures what's needed by the span config // reconciliation job to perform its task. The job is responsible for // reconciling a tenant's zone configurations with the clusters span @@ -40,11 +57,7 @@ type KVAccessor interface { type ReconciliationDependencies interface { KVAccessor - // TODO(irfansharif): We'll also want access to a "SQLWatcher", something - // that watches for changes to system.{descriptor,zones} and be responsible - // for generating corresponding span config updates. Put together, the - // reconciliation job will react to these updates by installing them into KV - // through the KVAccessor. + SQLTranslator } // Store is a data structure used to store spans and their corresponding diff --git a/pkg/spanconfig/spanconfigmanager/manager.go b/pkg/spanconfig/spanconfigmanager/manager.go index 8756e8fcf9c6..defe3b3f7cc5 100644 --- a/pkg/spanconfig/spanconfigmanager/manager.go +++ b/pkg/spanconfig/spanconfigmanager/manager.go @@ -60,6 +60,7 @@ type Manager struct { knobs *spanconfig.TestingKnobs spanconfig.KVAccessor + spanconfig.SQLTranslator } var _ spanconfig.ReconciliationDependencies = &Manager{} @@ -72,19 +73,21 @@ func New( stopper *stop.Stopper, settings *cluster.Settings, kvAccessor spanconfig.KVAccessor, + sqlTranslator spanconfig.SQLTranslator, knobs *spanconfig.TestingKnobs, ) *Manager { if knobs == nil { knobs = &spanconfig.TestingKnobs{} } return &Manager{ - db: db, - jr: jr, - ie: ie, - stopper: stopper, - settings: settings, - knobs: knobs, - KVAccessor: kvAccessor, + db: db, + jr: jr, + ie: ie, + stopper: stopper, + settings: settings, + KVAccessor: kvAccessor, + SQLTranslator: sqlTranslator, + knobs: knobs, } } diff --git a/pkg/spanconfig/spanconfigmanager/manager_test.go b/pkg/spanconfig/spanconfigmanager/manager_test.go index 7849796c6709..a8b0a7bd7ab2 100644 --- a/pkg/spanconfig/spanconfigmanager/manager_test.go +++ b/pkg/spanconfig/spanconfigmanager/manager_test.go @@ -74,6 +74,7 @@ func TestManagerConcurrentJobCreation(t *testing.T) { ts.Stopper(), ts.ClusterSettings(), ts.SpanConfigAccessor().(spanconfig.KVAccessor), + ts.SpanConfigSQLTranslator().(spanconfig.SQLTranslator), &spanconfig.TestingKnobs{ ManagerCreatedJobInterceptor: func(jobI interface{}) { job := jobI.(*jobs.Job) @@ -161,6 +162,7 @@ func TestManagerStartsJobIfFailed(t *testing.T) { ts.Stopper(), ts.ClusterSettings(), ts.SpanConfigAccessor().(spanconfig.KVAccessor), + ts.SpanConfigSQLTranslator().(spanconfig.SQLTranslator), &spanconfig.TestingKnobs{ ManagerAfterCheckedReconciliationJobExistsInterceptor: func(exists bool) { require.False(t, exists) @@ -235,6 +237,7 @@ func TestManagerCheckJobConditions(t *testing.T) { ts.Stopper(), ts.ClusterSettings(), ts.SpanConfigAccessor().(spanconfig.KVAccessor), + ts.SpanConfigSQLTranslator().(spanconfig.SQLTranslator), &spanconfig.TestingKnobs{ ManagerDisableJobCreation: true, ManagerCheckJobInterceptor: func() { diff --git a/pkg/spanconfig/spanconfigsqltranslator/BUILD.bazel b/pkg/spanconfig/spanconfigsqltranslator/BUILD.bazel new file mode 100644 index 000000000000..3f0d8527a984 --- /dev/null +++ b/pkg/spanconfig/spanconfigsqltranslator/BUILD.bazel @@ -0,0 +1,25 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "spanconfigsqltranslator", + srcs = [ + "disabled.go", + "sql_translator.go", + ], + importpath = "github.com/cockroachdb/cockroach/pkg/spanconfig/spanconfigsqltranslator", + visibility = ["//visibility:public"], + deps = [ + "//pkg/config/zonepb", + "//pkg/keys", + "//pkg/kv", + "//pkg/roachpb:with-mocks", + "//pkg/spanconfig", + "//pkg/sql", + "//pkg/sql/catalog", + "//pkg/sql/catalog/descpb", + "//pkg/sql/catalog/descs", + "//pkg/sql/sem/tree", + "//pkg/util/hlc", + "@com_github_cockroachdb_errors//:errors", + ], +) diff --git a/pkg/spanconfig/spanconfigsqltranslator/disabled.go b/pkg/spanconfig/spanconfigsqltranslator/disabled.go new file mode 100644 index 000000000000..f9ef124f46d8 --- /dev/null +++ b/pkg/spanconfig/spanconfigsqltranslator/disabled.go @@ -0,0 +1,47 @@ +// Copyright 2021 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package spanconfigsqltranslator + +import ( + "context" + + "github.com/cockroachdb/cockroach/pkg/roachpb" + "github.com/cockroachdb/cockroach/pkg/spanconfig" + "github.com/cockroachdb/cockroach/pkg/sql/catalog/descpb" + "github.com/cockroachdb/cockroach/pkg/util/hlc" + "github.com/cockroachdb/errors" +) + +// DisabledSQLTranslator is an empty implementation of spanconfig.SQLTranslator +// for testing purposes. +type DisabledSQLTranslator struct{} + +// DisabledSQLTranslator implements the spanconfig.SQLTranslator interface. +var _ spanconfig.SQLTranslator = &DisabledSQLTranslator{} + +// NewDisabled returns a new disabled SQLTranslator. +func NewDisabled() *DisabledSQLTranslator { + return &DisabledSQLTranslator{} +} + +// Translate implements the spanconfig.SQLTranslator interface. +func (d *DisabledSQLTranslator) Translate( + _ context.Context, _ descpb.IDs, +) ([]roachpb.SpanConfigEntry, error) { + return nil, errors.New("DisabledSQLTranslator is not intended for use") +} + +// FullTranslate implements the spanconfig.SQLTranslator interface. +func (d *DisabledSQLTranslator) FullTranslate( + _ context.Context, +) ([]roachpb.SpanConfigEntry, hlc.Timestamp, error) { + return nil, hlc.Timestamp{}, errors.New("DisabledSQLTranslator is not intended for use") +} diff --git a/pkg/spanconfig/spanconfigsqltranslator/sql_translator.go b/pkg/spanconfig/spanconfigsqltranslator/sql_translator.go new file mode 100644 index 000000000000..27fc9b45d6a3 --- /dev/null +++ b/pkg/spanconfig/spanconfigsqltranslator/sql_translator.go @@ -0,0 +1,391 @@ +// Copyright 2021 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package spanconfigsqltranslator + +import ( + "context" + + "github.com/cockroachdb/cockroach/pkg/config/zonepb" + "github.com/cockroachdb/cockroach/pkg/keys" + "github.com/cockroachdb/cockroach/pkg/kv" + "github.com/cockroachdb/cockroach/pkg/roachpb" + "github.com/cockroachdb/cockroach/pkg/spanconfig" + "github.com/cockroachdb/cockroach/pkg/sql" + "github.com/cockroachdb/cockroach/pkg/sql/catalog" + "github.com/cockroachdb/cockroach/pkg/sql/catalog/descpb" + "github.com/cockroachdb/cockroach/pkg/sql/catalog/descs" + "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" + "github.com/cockroachdb/cockroach/pkg/util/hlc" + "github.com/cockroachdb/errors" +) + +// SQLTranslator implements the spanconfig.SQLTranslator interface. +var _ spanconfig.SQLTranslator = &SQLTranslator{} + +// SQLTranslator is the concrete implementation of spanconfig.SQLTranslator. +type SQLTranslator struct { + execCfg *sql.ExecutorConfig + codec keys.SQLCodec +} + +// New constructs and returns a SQLTranslator. +func New(execCfg *sql.ExecutorConfig, codec keys.SQLCodec) *SQLTranslator { + return &SQLTranslator{ + execCfg: execCfg, + codec: codec, + } +} + +// FullTranslate is part of the spanconfig.SQLTranslator interface. +func (s *SQLTranslator) FullTranslate( + ctx context.Context, +) ([]roachpb.SpanConfigEntry, hlc.Timestamp, error) { + // As RANGE DEFAULT is the root of all zone configurations (including + // other named zones for the system tenant), we can construct the entire + // span configuration state by starting from RANGE DEFAULT. + return s.translate(ctx, descpb.IDs{keys.RootNamespaceID}) +} + +// Translate is part of the spanconfig.SQLTranslator interface. +func (s *SQLTranslator) Translate( + ctx context.Context, ids descpb.IDs, +) ([]roachpb.SpanConfigEntry, error) { + entries, _, err := s.translate(ctx, ids) + return entries, err +} + +// translate generates the implied span configuration state for the given IDs. +func (s *SQLTranslator) translate( + ctx context.Context, ids descpb.IDs, +) (entries []roachpb.SpanConfigEntry, timestamp hlc.Timestamp, err error) { + err = sql.DescsTxn( + ctx, s.execCfg, func( + ctx context.Context, txn *kv.Txn, descsCol *descs.Collection, + ) error { + for _, id := range ids { + idEntries, err := s.translateID(ctx, id, txn, descsCol) + if err != nil { + return err + } + entries = append(entries, idEntries...) + } + // TODO(arul): Looks like the use of this method means the transactions + // timestamp can't be pushed. Confirm that we're okay with this, but I'm + // not sure if there's a better way to do what we want to do. + timestamp = txn.CommitTimestamp() + return nil + }) + return entries, timestamp, err +} + +// translateID generates the implied span configuration state for a single ID. +func (s *SQLTranslator) translateID( + ctx context.Context, id descpb.ID, txn *kv.Txn, descsCol *descs.Collection, +) (entries []roachpb.SpanConfigEntry, err error) { + var expandedIDs descpb.IDs + if zonepb.IsNamedZoneID(id) { + entries, expandedIDs, err = s.translateNamedZoneConfig(ctx, id, txn, descsCol) + if err != nil { + return nil, err + } + } else { + // We're dealing with a SQL Object. + entries, expandedIDs, err = s.translateDescriptorIDZoneConfig(ctx, id, txn, descsCol) + if err != nil { + return nil, err + } + } + + for _, ID := range expandedIDs { + idEntries, err := s.translateID(ctx, ID, txn, descsCol) + if err != nil { + return nil, err + } + entries = append(entries, idEntries...) + } + return entries, nil +} + +// translateDescriptorIDZoneConfig generates the span configuration for an ID +// belonging to a SQL descriptor. It also returns a list of IDs below the given +// ID in the zone configuration hierarchy. This list of expandedIDs must be +// further translated. +func (s *SQLTranslator) translateDescriptorIDZoneConfig( + ctx context.Context, id descpb.ID, txn *kv.Txn, descsCol *descs.Collection, +) (entries []roachpb.SpanConfigEntry, expandedIDs descpb.IDs, err error) { + desc, err := descsCol.GetImmutableDescriptorByID(ctx, txn, id, tree.CommonLookupFlags{ + AvoidCached: true, + IncludeDropped: true, + IncludeOffline: true, + }) + if err != nil { + if errors.Is(err, catalog.ErrDescriptorNotFound) { + // The descriptor has been deleted. If this is the case then no span + // configuration exists for it. + return nil, nil, nil + } + return nil, nil, err + } + expandedIDs, err = findIDsBelowDescriptorInZoneConfigHierarchy(ctx, desc, txn, descsCol) + if err != nil { + return nil, nil, err + } + + // There's nothing to do for {Type,Schema} descriptors. + if desc.DescriptorType() == catalog.Type || desc.DescriptorType() == catalog.Schema { + return nil, nil, nil + } + + // If the descriptor is that of a table then we simply generate all span + // configs. + if desc.DescriptorType() == catalog.Table { + entries, err = s.generateSpanConfigurationsForTable(ctx, txn, desc) + if err != nil { + return nil, nil, err + } + return entries, nil, nil + } + + // The ID belongs to a database. There aren't any entries to generate for the + // database itself, but we need to pass the list of expanded IDs to the caller + // to translate further. + return nil, expandedIDs, nil +} + +// translateNamedZoneConfig generates the span configuration state for an ID +// that corresponds to a named zone. It also returns a list of expanded IDs that +// need to be further translated as they lie further down in the zone +// configuration hierarchy. This is only ever the case for RANGE DEFAULT. +func (s *SQLTranslator) translateNamedZoneConfig( + ctx context.Context, id descpb.ID, txn *kv.Txn, descsCol *descs.Collection, +) (entries []roachpb.SpanConfigEntry, expandedIDs descpb.IDs, err error) { + name, ok := zonepb.NamedZonesByID[uint32(id)] + if !ok { + return nil, nil, errors.AssertionFailedf("id %d does not belong to a named zone", id) + } + + // Named zones other than RANGE DEFAULT are not a thing for secondary tenants. + if !s.codec.ForSystemTenant() && name != zonepb.DefaultZoneName { + return nil, + nil, + errors.AssertionFailedf("secondary tenants do not have the notion of %s named zone", name) + } + + entries, err = s.generateSpanConfigurationsForNamedZone(ctx, txn, name) + if err != nil { + return nil, nil, err + } + + switch name { + case zonepb.DefaultZoneName: + default: + // No IDs lie below in the zone configuration hierarchy for named zones + // other than RANGE DEFAULT, so expanded IDs is nil here. + return entries, nil, nil + } + + // A change to RANGE DEFAULT may imply a change to every SQL object for the + // tenant. + databases, err := descsCol.GetAllDatabaseDescriptors(ctx, txn) + if err != nil { + return nil, nil, err + } + for _, dbDesc := range databases { + tableIDs, err := findIDsBelowDescriptorInZoneConfigHierarchy(ctx, dbDesc, txn, descsCol) + if err != nil { + return nil, nil, err + } + expandedIDs = append(expandedIDs, tableIDs...) + } + + // As all named zones also inherit from RANGE DEFAULT, a change to it may also + // imply a change to every other named zone. This is only a thing for the + // host tenant as named zones other than RANGE DEFAULT don't exist for + // secondary tenants. + if s.codec.ForSystemTenant() { + for _, namedZone := range zonepb.NamedZonesList { + // Add an entry for all named zones bar RANGE DEFAULT. + if namedZone != zonepb.DefaultZoneName { + expandedIDs = append(expandedIDs, descpb.ID(zonepb.NamedZones[namedZone])) + } + } + } + + return entries, expandedIDs, nil +} + +// generateSpanConfigurationsForNamedZone takes in a named zone and generates +// the span configuration for that range. +func (s *SQLTranslator) generateSpanConfigurationsForNamedZone( + ctx context.Context, txn *kv.Txn, name zonepb.NamedZone, +) ([]roachpb.SpanConfigEntry, error) { + var spans []roachpb.Span + switch name { + case zonepb.DefaultZoneName: + if !s.codec.ForSystemTenant() { + // Tenant's RANGE DEFAULT addresses the first range in the tenant's prefix. + // We always want there to be an entry for this span in + // `system.span_configurations` as this span is at the tenant boundary, + // and as such, a hard split point. + spans = append(spans, roachpb.Span{ + Key: s.codec.TenantPrefix(), + EndKey: s.codec.TenantPrefix().Next(), + }) + } + + case zonepb.MetaZoneName: + spans = append(spans, roachpb.Span{Key: keys.Meta1Span.Key, EndKey: keys.NodeLivenessSpan.Key}) + case zonepb.LivenessZoneName: + spans = append(spans, keys.NodeLivenessSpan) + case zonepb.TimeseriesZoneName: + spans = append(spans, keys.TimeseriesSpan) + case zonepb.SystemZoneName: + // Add spans for the system range without the timeseries and + // liveness ranges, which are individually captured above. + // + // Note that the NodeLivenessSpan sorts before the rest of the system + // keyspace, so the first span here starts at the end of the + // NodeLivenessSpan. + spans = append(spans, roachpb.Span{ + Key: keys.NodeLivenessSpan.EndKey, + EndKey: keys.TimeseriesSpan.Key, + }) + spans = append(spans, roachpb.Span{ + Key: keys.TimeseriesSpan.EndKey, + EndKey: keys.SystemMax, + }) + case zonepb.TenantsZoneName: // nothing to do + default: + return nil, errors.AssertionFailedf("unknown named zone config %s", name) + } + + zone, err := sql.GetHydratedZoneConfigForNamedZone(ctx, txn, s.codec, name) + if err != nil { + return nil, err + } + spanConfig := zone.AsSpanConfig() + + var entries []roachpb.SpanConfigEntry + for _, span := range spans { + entries = append(entries, roachpb.SpanConfigEntry{ + Span: span, + Config: spanConfig, + }) + } + return entries, nil +} + +// generateSpanConfigurationsForTable generates the span configurations +// corresponding to the given tableID. It uses a transactional view of +// system.zones and system.descriptors to do so. +func (s *SQLTranslator) generateSpanConfigurationsForTable( + ctx context.Context, txn *kv.Txn, desc catalog.Descriptor, +) ([]roachpb.SpanConfigEntry, error) { + if desc.DescriptorType() != catalog.Table { + return nil, errors.AssertionFailedf( + "expected table descriptor, but got descriptor of type %s", desc.DescriptorType(), + ) + } + zone, err := sql.GetHydratedZoneConfigForTable(ctx, txn, s.codec, desc.GetID()) + if err != nil { + return nil, err + } + spanConfig := zone.AsSpanConfig() + + ret := make([]roachpb.SpanConfigEntry, 0) + prevEndKey := s.codec.TablePrefix(uint32(desc.GetID())) + for i := range zone.SubzoneSpans { + // We need to prepend the tablePrefix to the spans stored inside the + // SubzoneSpans field because we store the stripped version there for + // historical reasons. + span := roachpb.Span{ + Key: append(s.codec.TablePrefix(uint32(desc.GetID())), zone.SubzoneSpans[i].Key...), + EndKey: append(s.codec.TablePrefix(uint32(desc.GetID())), zone.SubzoneSpans[i].EndKey...), + } + + { + // The zone config code sets the EndKey to be nil before storing the + // proto if it is equal to `Key.PrefixEnd()`, so we bring it back if + // required. + if zone.SubzoneSpans[i].EndKey == nil { + span.EndKey = span.Key.PrefixEnd() + } + } + + // If there is a "hole" in the spans covered by the subzones array we fill + // it using the parent zone configuration. + if !prevEndKey.Equal(span.Key) { + ret = append(ret, + roachpb.SpanConfigEntry{ + Span: roachpb.Span{Key: prevEndKey, EndKey: span.Key}, + Config: spanConfig, + }, + ) + } + + // Add an entry for the subzone. + subzoneSpanConfig := zone.Subzones[zone.SubzoneSpans[i].SubzoneIndex].Config.AsSpanConfig() + ret = append(ret, + roachpb.SpanConfigEntry{ + Span: roachpb.Span{Key: span.Key, EndKey: span.EndKey}, + Config: subzoneSpanConfig, + }, + ) + + prevEndKey = span.EndKey + } + + // If the last subzone span doesn't cover the entire table's keyspace then we + // cover the remaining key range with the table's zone configuration. + if !prevEndKey.Equal(s.codec.TablePrefix(uint32(desc.GetID())).PrefixEnd()) { + ret = append(ret, + roachpb.SpanConfigEntry{ + Span: roachpb.Span{Key: prevEndKey, EndKey: s.codec.TablePrefix(uint32(desc.GetID())).PrefixEnd()}, + Config: spanConfig, + }, + ) + } + return ret, nil +} + +// findIDsBelowDescriptorInZoneConfigHierarchy returns a list of IDs that lie +// below the given descriptor in the zone configuration hierarchy. This is all +// tableIDs for database descriptors. It is a pass through for all other +// descriptor types. +func findIDsBelowDescriptorInZoneConfigHierarchy( + ctx context.Context, desc catalog.Descriptor, txn *kv.Txn, descsCol *descs.Collection, +) (descpb.IDs, error) { + // No descriptors lie below {Table,Type,Schema} descriptors in the zone config + // hierarchy. + if desc.DescriptorType() == catalog.Table || desc.DescriptorType() == catalog.Type || desc.DescriptorType() == catalog.Schema { + return nil, nil + } + + // Now that we know the descriptor belongs to a database, the list of IDs + // below it in the zone config hierarchy is simply all the tables in the + // database. + + // There's nothing for us to do if the descriptor is offline or has been + // dropped. + if desc.Offline() || desc.Dropped() { + return nil, nil + } + + tables, err := descsCol.GetAllTableDescriptorsInDatabase(ctx, txn, desc.GetID()) + if err != nil { + return nil, err + } + ret := make(descpb.IDs, 0, len(tables)) + for _, table := range tables { + ret = append(ret, table.GetID()) + } + return ret, nil +} diff --git a/pkg/sql/catalog/descs/collection.go b/pkg/sql/catalog/descs/collection.go index b482defc6906..b83aeafd1aec 100644 --- a/pkg/sql/catalog/descs/collection.go +++ b/pkg/sql/catalog/descs/collection.go @@ -15,6 +15,7 @@ package descs import ( "bytes" "context" + "fmt" "github.com/cockroachdb/cockroach/pkg/keys" "github.com/cockroachdb/cockroach/pkg/kv" @@ -27,6 +28,7 @@ import ( "github.com/cockroachdb/cockroach/pkg/sql/catalog/hydratedtables" "github.com/cockroachdb/cockroach/pkg/sql/catalog/lease" "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" + "github.com/cockroachdb/cockroach/pkg/sql/sqlerrors" "github.com/cockroachdb/cockroach/pkg/sql/sqlliveness" "github.com/cockroachdb/cockroach/pkg/util/encoding" "github.com/cockroachdb/cockroach/pkg/util/hlc" @@ -278,6 +280,37 @@ func (tc *Collection) GetAllDatabaseDescriptors( return tc.kv.getAllDatabaseDescriptors(ctx, txn) } +// GetAllTableDescriptorsInDatabase returns all the table descriptors visible to +// the transaction inside the database referenced by the given database ID. It +// first checks the collections cached descriptors before defaulting to a key-value scan. +func (tc *Collection) GetAllTableDescriptorsInDatabase( + ctx context.Context, txn *kv.Txn, dbID descpb.ID, +) ([]catalog.TableDescriptor, error) { + // Ensure the given ID does indeed belong to a database. + found, _, err := tc.getDatabaseByID(ctx, txn, dbID, tree.DatabaseLookupFlags{ + AvoidCached: false, + }) + if err != nil { + return nil, err + } + if !found { + return nil, sqlerrors.NewUndefinedDatabaseError(fmt.Sprintf("[%d]", dbID)) + } + descs, err := tc.GetAllDescriptors(ctx, txn) + if err != nil { + return nil, err + } + var ret []catalog.TableDescriptor + for _, desc := range descs { + if desc.GetParentID() == dbID { + if table, ok := desc.(catalog.TableDescriptor); ok { + ret = append(ret, table) + } + } + } + return ret, nil +} + // GetSchemasForDatabase returns the schemas for a given database // visible by the transaction. This uses the schema cache locally // if possible, or else performs a scan on kv. diff --git a/pkg/sql/show_zone_config.go b/pkg/sql/show_zone_config.go index 16868941d533..3cec49eb5e6a 100644 --- a/pkg/sql/show_zone_config.go +++ b/pkg/sql/show_zone_config.go @@ -383,7 +383,7 @@ func ascendZoneSpecifier( if actualID == keys.RootNamespaceID { // We had to traverse to the top of the hierarchy, so we're showing the // default zone config. - zs.NamedZone = zonepb.DefaultZoneName + zs.NamedZone = tree.UnrestrictedName(zonepb.DefaultZoneName) zs.Database = "" zs.TableOrIndex = tree.TableIndexName{} // Since the default zone has no partition, we can erase the diff --git a/pkg/sql/zone_config.go b/pkg/sql/zone_config.go index e0d2dd1c24d8..d6fbf3dba99b 100644 --- a/pkg/sql/zone_config.go +++ b/pkg/sql/zone_config.go @@ -258,6 +258,93 @@ func GetZoneConfigInTxn( return zoneID, zone, subzone, nil } +// GetHydratedZoneConfigForNamedZone returns a zone config for the given named +// zone. Any missing fields are filled through the RANGE DEFAULT zone config. +func GetHydratedZoneConfigForNamedZone( + ctx context.Context, txn *kv.Txn, codec keys.SQLCodec, zoneName zonepb.NamedZone, +) (*zonepb.ZoneConfig, error) { + getKey := func(key roachpb.Key) (*roachpb.Value, error) { + kv, err := txn.Get(ctx, key) + if err != nil { + return nil, err + } + return kv.Value, nil + } + id := zonepb.NamedZones[zoneName] + zoneID, zone, _, _, err := getZoneConfig( + codec, descpb.ID(id), getKey, false /* getInheritedDefault */, false, /* mayBeTable */ + ) + if err := completeZoneConfig(zone, codec, zoneID, getKey); err != nil { + return nil, err + } + return zone, err +} + +// GetHydratedZoneConfigForTable returns a fully hydrated zone config for a +// given table ID. +func GetHydratedZoneConfigForTable( + ctx context.Context, txn *kv.Txn, codec keys.SQLCodec, id descpb.ID, +) (*zonepb.ZoneConfig, error) { + getKey := func(key roachpb.Key) (*roachpb.Value, error) { + kv, err := txn.Get(ctx, key) + if err != nil { + return nil, err + } + return kv.Value, nil + } + // TODO(zcfgs-pod): Use the descCollection to do descriptor lookups instead of this + // getKey function that `getZoneConfig` currently uses. + zoneID, zone, _, placeholder, err := getZoneConfig( + codec, id, getKey, false /* getInheritedDefault */, true, /* mayBeTable */ + ) + if err != nil { + return nil, err + } + if err := completeZoneConfig(zone, codec, zoneID, getKey); err != nil { + return nil, err + } + + // We've completely hydrated the zone config now. The only thing left to do + // is to do is hydrate the subzones, if applicable. + + // A placeholder config exists only to store subzones, so we copy over that + // information on the zone config. + if placeholder != nil { + // A placeholder config only exists for tables. Furthermore, if it exists, + // then the zone config (`zone`) above must belong to an object further up + // in the inheritance chain (such as the database or DEFAULT RANGE). As the + // subzones field is only defined if the zone config applies to a table, it + // follows that `zone` must not have any Subzones set on it. + if len(zone.Subzones) != 0 { + return nil, errors.AssertionFailedf("placeholder %v exists in conjunction with subzones on zone config %v", *zone, *placeholder) + } + zone.Subzones = placeholder.Subzones + zone.SubzoneSpans = placeholder.SubzoneSpans + } + + for i, subzone := range zone.Subzones { + // Check if a zone configuration exists for the index this subzone applies + // to by passing in a an empty partition below. + indexSubzone := zone.GetSubzone(subzone.IndexID, "" /* partition */) + // Partitions, in terms of the inheritance hierarchy, first inherit from the + // zone configuration fields on their parent index (if such a zone + // configuration exists). + // NB: If the subzone we're dealing with belongs to an index and not a + // partition, then the call below is a no-op. + if indexSubzone != nil { + zone.Subzones[i].Config.InheritFromParent(&indexSubzone.Config) + } + // After inheriting from the index's zone configuration, any fields that are + // left empty must be filled in from the table's zone configuration. Note + // that the table's zone configuration was fully hydrated above and + // inheriting from it will result in the subzone config being fully hydrated + // as well. + zone.Subzones[i].Config.InheritFromParent(zone) + } + + return zone, nil +} + func zoneSpecifierNotFoundError(zs tree.ZoneSpecifier) error { if zs.NamedZone != "" { return pgerror.Newf( diff --git a/pkg/testutils/serverutils/test_server_shim.go b/pkg/testutils/serverutils/test_server_shim.go index 7a654cd82512..a0d75bab0312 100644 --- a/pkg/testutils/serverutils/test_server_shim.go +++ b/pkg/testutils/serverutils/test_server_shim.go @@ -118,6 +118,10 @@ type TestServerInterface interface { // interface{}. SpanConfigAccessor() interface{} + // SpanConfigSQLTranslator returns the underlying spanconfig.SQLTranslator as + // an interface{}. + SpanConfigSQLTranslator() interface{} + // SQLServer returns the *sql.Server as an interface{}. SQLServer() interface{}