diff --git a/docs/generated/sql/functions.md b/docs/generated/sql/functions.md index 60fa8277c01f..75af62f6b930 100644 --- a/docs/generated/sql/functions.md +++ b/docs/generated/sql/functions.md @@ -1279,6 +1279,12 @@ the locality flag on node startup. Returns an error if no region is set.
crdb_internal.scan(start_key: bytes, end_key: bytes) → tuple{bytes AS key, bytes AS value, string AS ts}
Returns the raw keys and values with their timestamp from the specified span
crdb_internal.tenant_span_stats() → tuple{int AS database_id, int AS table_id, int AS range_count, int AS approximate_disk_bytes, int AS live_bytes, int AS total_bytes, float AS live_percentage}
Returns statistics (range count, disk size, live range bytes, total range bytes, live range byte percentage) for all of the tenant’s tables.
+crdb_internal.tenant_span_stats(database_id: int) → tuple{int AS database_id, int AS table_id, int AS range_count, int AS approximate_disk_bytes, int AS live_bytes, int AS total_bytes, float AS live_percentage}
Returns statistics (range count, disk size, live range bytes, total range bytes, live range byte percentage) for tables of the provided database id.
+crdb_internal.tenant_span_stats(database_id: int, table_id: int) → tuple{int AS database_id, int AS table_id, int AS range_count, int AS approximate_disk_bytes, int AS live_bytes, int AS total_bytes, float AS live_percentage}
Returns statistics (range count, disk size, live range bytes, total range bytes, live range byte percentage) for the provided table id.
+crdb_internal.testing_callback(name: string) → int
For internal CRDB testing only. The function calls a callback identified by name
registered with the server by the test.
crdb_internal.unary_table() → tuple
Produces a virtual table containing a single row with no values.
diff --git a/pkg/ccl/logictestccl/tests/3node-tenant/generated_test.go b/pkg/ccl/logictestccl/tests/3node-tenant/generated_test.go index 8cc9c5bb192f..0af11926fbaf 100644 --- a/pkg/ccl/logictestccl/tests/3node-tenant/generated_test.go +++ b/pkg/ccl/logictestccl/tests/3node-tenant/generated_test.go @@ -1928,6 +1928,13 @@ func TestTenantLogic_tenant_slow_repro( runLogicTest(t, "tenant_slow_repro") } +func TestTenantLogic_tenant_span_stats( + t *testing.T, +) { + defer leaktest.AfterTest(t)() + runLogicTest(t, "tenant_span_stats") +} + func TestTenantLogic_time( t *testing.T, ) { diff --git a/pkg/sql/authorization.go b/pkg/sql/authorization.go index bf0b3ae8cdf6..3c5f052aff52 100644 --- a/pkg/sql/authorization.go +++ b/pkg/sql/authorization.go @@ -907,6 +907,8 @@ func (p *planner) HasOwnershipOnSchema( return hasOwnership, nil } +// HasViewActivityOrViewActivityRedactedRole implements the AuthorizationAccessor interface. +// Requires a valid transaction to be open. func (p *planner) HasViewActivityOrViewActivityRedactedRole(ctx context.Context) (bool, error) { if hasAdmin, err := p.HasAdminRole(ctx); err != nil { return hasAdmin, err diff --git a/pkg/sql/faketreeeval/evalctx.go b/pkg/sql/faketreeeval/evalctx.go index ad7dc1b226ab..7439c9506fee 100644 --- a/pkg/sql/faketreeeval/evalctx.go +++ b/pkg/sql/faketreeeval/evalctx.go @@ -476,6 +476,20 @@ func (ep *DummyEvalPlanner) GetRangeDescByID( return } +// SpanStats is part of the eval.Planner interface. +func (ep *DummyEvalPlanner) SpanStats( + context.Context, roachpb.RKey, roachpb.RKey, +) (stats *roachpb.SpanStatsResponse, err error) { + return +} + +// GetDetailsForSpanStats is part of the eval.Planner interface. +func (ep *DummyEvalPlanner) GetDetailsForSpanStats( + context.Context, int, int, +) (it eval.InternalRows, err error) { + return +} + // DummyPrivilegedAccessor implements the tree.PrivilegedAccessor interface by returning errors. type DummyPrivilegedAccessor struct{} @@ -532,6 +546,13 @@ func (ep *DummySessionAccessor) HasRoleOption( return false, errors.WithStack(errEvalSessionVar) } +// HasViewActivityOrViewActivityRedactedRole is part of the eval.SessionAccessor interface. +func (ep *DummySessionAccessor) HasViewActivityOrViewActivityRedactedRole( + context.Context, +) (bool, error) { + return false, errors.WithStack(errEvalSessionVar) +} + // DummyClientNoticeSender implements the eval.ClientNoticeSender interface. type DummyClientNoticeSender struct{} diff --git a/pkg/sql/logictest/testdata/logic_test/tenant_span_stats b/pkg/sql/logictest/testdata/logic_test/tenant_span_stats new file mode 100644 index 000000000000..3a3558e54dab --- /dev/null +++ b/pkg/sql/logictest/testdata/logic_test/tenant_span_stats @@ -0,0 +1,130 @@ +# LogicTest: 3node-tenant + +# Create a second database. +statement ok +CREATE DATABASE a + +# Create a table for database. +statement ok +CREATE TABLE a.b (id INT PRIMARY KEY) + +# Insert data into the table +statement ok +INSERT INTO a.b SELECT generate_series(1, 10) + +# Create a second table for database. +statement ok +CREATE TABLE a.c (id INT PRIMARY KEY) + +# SELECT * FROM crdb_internal.tenant_span_stats: span stats for all of the tenant's tables. +# Assert the schema. +query IIIIIIR colnames +SELECT * FROM crdb_internal.tenant_span_stats() LIMIT 0 +---- +database_id table_id range_count approximate_disk_bytes live_bytes total_bytes live_percentage + +# SELECT DISTINCT(database_id) FROM crdb_internal.tenant_span_stats: +# Assert that we are collecting span stats for tables across multiple databases. +query I colnames +SELECT DISTINCT(database_id) FROM crdb_internal.tenant_span_stats() +---- +database_id +1 +106 + +# SELECT database_id, table_id FROM crdb_internal.tenant_span_stats(106): +# Assert that we are collecting span stats scoped to the provided database id. +query II colnames +SELECT database_id, table_id FROM crdb_internal.tenant_span_stats(106) +---- +database_id table_id +106 108 +106 109 + +# SELECT database_id, table_id FROM crdb_internal.tenant_span_stats(106, 108): +# Assert that we are collecting span stats scoped to the provided database/table id combo. +query II colnames +SELECT database_id, table_id FROM crdb_internal.tenant_span_stats(106, 108) +---- +database_id table_id +106 108 + +# Assert that we cannot provide an invalid database id. +query error pq: provided database id must be greater than or equal to 1 +SELECT database_id, table_id FROM crdb_internal.tenant_span_stats(0) + +# Assert that we cannot provide an invalid table id. +query error pq: provided table id must be greater than or equal to 1 +SELECT database_id, table_id FROM crdb_internal.tenant_span_stats(1, -1) + +# Assert that we cannot provide an invalid database id with a valid table id. +query error pq: provided database id must be greater than or equal to 1 +SELECT database_id, table_id FROM crdb_internal.tenant_span_stats(-1, 1) + +# SELECT * FROM crdb_internal.tenant_span_stats(1000): +# Assert that we get empty rows for a database id that does not correspond to a database. +query IIIIIIR colnames +SELECT * FROM crdb_internal.tenant_span_stats(1000) +---- +database_id table_id range_count approximate_disk_bytes live_bytes total_bytes live_percentage + +# SELECT * FROM crdb_internal.tenant_span_stats(1, 1000): +# Assert that we get empty rows for a table id that does not correspond to a table. +query IIIIIIR colnames +SELECT * FROM crdb_internal.tenant_span_stats(1, 1000) +---- +database_id table_id range_count approximate_disk_bytes live_bytes total_bytes live_percentage + +# Select everything from a table to have 'live bytes'. +statement ok +SELECT * FROM a.b; + +# Assert that we are collecting non-zero span stats scoped to the provided database/table id combo. +query IIBBBB colnames +SELECT database_id, table_id, range_count > 0 as range_count, live_bytes > 0 as live_bytes, total_bytes > 0 as total_bytes, live_percentage > 0 as live_percentage FROM crdb_internal.tenant_span_stats(106, 108) +---- +database_id table_id range_count live_bytes total_bytes live_percentage +106 108 true true true true + +# Create a second user without VIEWACTIVITY permission. +statement ok +CREATE USER testuser2 + +# Switch to user2 +user testuser2 + +# Assert that the user2 doesn't have permission to use this builtin. +query error pq: user needs ADMIN role or the VIEWACTIVITY/VIEWACTIVITYREDACTED permission to view span statistics +SELECT * FROM crdb_internal.tenant_span_stats() LIMIT 0 + +# Switch to root +user root + +# Grant VIEWACTIVITY permission to second user. +statement ok +ALTER ROLE testuser2 WITH VIEWACTIVITY + +user testuser2 + +# Assert that the user2 has permission to use this builtin. +query IIIIIIR colnames +SELECT * FROM crdb_internal.tenant_span_stats() LIMIT 0 +---- +database_id table_id range_count approximate_disk_bytes live_bytes total_bytes live_percentage + +# Switch to root +user root + +# Remove VIEWACTIVITY permission from second user. +statement ok +ALTER ROLE testuser2 WITH NOVIEWACTIVITY + +# Grant VIEWACTIVITYREDACTED permission to second user. +statement ok +ALTER ROLE testuser2 WITH VIEWACTIVITYREDACTED + +# Assert that the user2 has permission to use this builtin. +query IIIIIIR colnames +SELECT * FROM crdb_internal.tenant_span_stats() LIMIT 0 +---- +database_id table_id range_count approximate_disk_bytes live_bytes total_bytes live_percentage diff --git a/pkg/sql/planner.go b/pkg/sql/planner.go index 5defe723bcaa..806b01a90145 100644 --- a/pkg/sql/planner.go +++ b/pkg/sql/planner.go @@ -20,6 +20,7 @@ import ( "github.com/cockroachdb/cockroach/pkg/keyvisualizer" "github.com/cockroachdb/cockroach/pkg/kv" "github.com/cockroachdb/cockroach/pkg/repstream" + "github.com/cockroachdb/cockroach/pkg/roachpb" "github.com/cockroachdb/cockroach/pkg/security/username" "github.com/cockroachdb/cockroach/pkg/server/serverpb" "github.com/cockroachdb/cockroach/pkg/spanconfig" @@ -834,3 +835,46 @@ func (p *planner) GetReplicationStreamManager( func (p *planner) GetStreamIngestManager(ctx context.Context) (eval.StreamIngestManager, error) { return repstream.GetStreamIngestManager(ctx, p.EvalContext(), p.InternalSQLTxn()) } + +// SpanStats returns a stats for the given span of keys. +func (p *planner) SpanStats( + ctx context.Context, startKey roachpb.RKey, endKey roachpb.RKey, +) (*roachpb.SpanStatsResponse, error) { + req := &roachpb.SpanStatsRequest{ + NodeID: "0", + StartKey: startKey, + EndKey: endKey, + } + return p.ExecCfg().TenantStatusServer.SpanStats(ctx, req) +} + +// GetDetailsForSpanStats ensures that the given database and table id exist. +// No rows will be returned for database/table ids that do not correspond to an actual +// database/table. +func (p *planner) GetDetailsForSpanStats( + ctx context.Context, dbId int, tableId int, +) (eval.InternalRows, error) { + query := `SELECT parent_id, table_id FROM crdb_internal.tables` + var args []interface{} + + if tableId != 0 { + query += ` WHERE parent_id = $1 AND table_id = $2` + args = append(args, dbId, tableId) + } else if dbId != 0 { + query += ` WHERE parent_id = $1` + args = append(args, dbId) + } else { + // Some tables belonging to crdb_internal.tables are not affiliated with a database + // and have a parent_id of 0 (usually crdb_internal or pg catalog tables), which aren't useful to the user. + query += ` WHERE parent_id != $1` + args = append(args, dbId) + } + + return p.QueryIteratorEx( + ctx, + "crdb_internal.database_span_stats", + sessiondata.NoSessionDataOverride, + query, + args..., + ) +} diff --git a/pkg/sql/sem/builtins/fixed_oids.go b/pkg/sql/sem/builtins/fixed_oids.go index 852696068d9b..a98f7bbc606b 100644 --- a/pkg/sql/sem/builtins/fixed_oids.go +++ b/pkg/sql/sem/builtins/fixed_oids.go @@ -2335,6 +2335,9 @@ var builtinOidsArray = []string{ 2361: `geography(geometry: geometry) -> geography`, 2362: `geography(geography: geography) -> geography`, 2363: `geography(bytes: bytes) -> geography`, + 2364: `crdb_internal.tenant_span_stats() -> tuple{int AS database_id, int AS table_id, int AS range_count, int AS approximate_disk_bytes, int AS live_bytes, int AS total_bytes, float AS live_percentage}`, + 2365: `crdb_internal.tenant_span_stats(database_id: int) -> tuple{int AS database_id, int AS table_id, int AS range_count, int AS approximate_disk_bytes, int AS live_bytes, int AS total_bytes, float AS live_percentage}`, + 2366: `crdb_internal.tenant_span_stats(database_id: int, table_id: int) -> tuple{int AS database_id, int AS table_id, int AS range_count, int AS approximate_disk_bytes, int AS live_bytes, int AS total_bytes, float AS live_percentage}`, } var builtinOidsBySignature map[string]oid.Oid diff --git a/pkg/sql/sem/builtins/generator_builtins.go b/pkg/sql/sem/builtins/generator_builtins.go index 961a25224bf0..d5287c37856c 100644 --- a/pkg/sql/sem/builtins/generator_builtins.go +++ b/pkg/sql/sem/builtins/generator_builtins.go @@ -576,6 +576,37 @@ The last argument is a JSONB object containing the following optional fields: volatility.Volatile, ), ), + "crdb_internal.tenant_span_stats": makeBuiltin(genProps(), + // Tenant overload + makeGeneratorOverload( + tree.ParamTypes{}, + tableSpanStatsGeneratorType, + makeTableSpanStatsGenerator, + "Returns statistics (range count, disk size, live range bytes, total range bytes, live range byte percentage) for all of the tenant's tables.", + volatility.Stable, + ), + // Database overload + makeGeneratorOverload( + tree.ParamTypes{ + {Name: "database_id", Typ: types.Int}, + }, + tableSpanStatsGeneratorType, + makeTableSpanStatsGenerator, + "Returns statistics (range count, disk size, live range bytes, total range bytes, live range byte percentage) for tables of the provided database id.", + volatility.Stable, + ), + // Table overload + makeGeneratorOverload( + tree.ParamTypes{ + {Name: "database_id", Typ: types.Int}, + {Name: "table_id", Typ: types.Int}, + }, + tableSpanStatsGeneratorType, + makeTableSpanStatsGenerator, + "Returns statistics (range count, disk size, live range bytes, total range bytes, live range byte percentage) for the provided table id.", + volatility.Stable, + ), + ), } var decodePlanGistGeneratorType = types.String @@ -2895,3 +2926,110 @@ func makeIdentGenerator( count: count, }, nil } + +type tableSpanStatsIterator struct { + it eval.InternalRows + codec keys.SQLCodec + p eval.Planner + currDbId int + currTableId int + currStatsResponse *roachpb.SpanStatsResponse + singleTableReq bool +} + +func newTableSpanStatsIterator(eval *eval.Context, dbId int, tableId int) *tableSpanStatsIterator { + return &tableSpanStatsIterator{codec: eval.Codec, p: eval.Planner, currDbId: dbId, currTableId: tableId, singleTableReq: tableId != 0} +} + +// Start implements the tree.ValueGenerator interface. +func (tssi *tableSpanStatsIterator) Start(ctx context.Context, _ *kv.Txn) error { + var err error = nil + tssi.it, err = tssi.p.GetDetailsForSpanStats(ctx, tssi.currDbId, tssi.currTableId) + return err +} + +// Next implements the tree.ValueGenerator interface. +func (tssi *tableSpanStatsIterator) Next(ctx context.Context) (bool, error) { + if tssi.it == nil { + return false, errors.AssertionFailedf("Start must be called before Next") + } + next, err := tssi.it.Next(ctx) + if err != nil || !next { + return false, err + } + // Pull the current row. + row := tssi.it.Cur() + tssi.currDbId = int(tree.MustBeDInt(row[0])) + tssi.currTableId = int(tree.MustBeDInt(row[1])) + + // Set our current stats response. + startKey := roachpb.RKey(tssi.codec.TablePrefix(uint32(tssi.currTableId))) + tssi.currStatsResponse, err = tssi.p.SpanStats(ctx, startKey, startKey.PrefixEnd()) + if err != nil { + return false, err + } + return next, err +} + +// Values implements the tree.ValueGenerator interface. +func (tssi *tableSpanStatsIterator) Values() (tree.Datums, error) { + liveBytes := tssi.currStatsResponse.TotalStats.LiveBytes + totalBytes := tssi.currStatsResponse.TotalStats.KeyBytes + + tssi.currStatsResponse.TotalStats.ValBytes + + tssi.currStatsResponse.TotalStats.RangeKeyBytes + + tssi.currStatsResponse.TotalStats.RangeValBytes + livePercentage := float64(0) + if totalBytes > 0 { + livePercentage = float64(liveBytes) / float64(totalBytes) + } + return []tree.Datum{ + tree.NewDInt(tree.DInt(tssi.currDbId)), + tree.NewDInt(tree.DInt(tssi.currTableId)), + tree.NewDInt(tree.DInt(tssi.currStatsResponse.RangeCount)), + tree.NewDInt(tree.DInt(tssi.currStatsResponse.ApproximateDiskBytes)), + tree.NewDInt(tree.DInt(liveBytes)), + tree.NewDInt(tree.DInt(totalBytes)), + tree.NewDFloat(tree.DFloat(livePercentage)), + }, nil +} + +// Close implements the tree.ValueGenerator interface. +func (tssi *tableSpanStatsIterator) Close(_ context.Context) {} + +// ResolvedType implements the tree.ValueGenerator interface. +func (tssi *tableSpanStatsIterator) ResolvedType() *types.T { + return tableSpanStatsGeneratorType +} + +var tableSpanStatsGeneratorType = types.MakeLabeledTuple( + []*types.T{types.Int, types.Int, types.Int, types.Int, types.Int, types.Int, types.Float}, + []string{"database_id", "table_id", "range_count", "approximate_disk_bytes", "live_bytes", "total_bytes", "live_percentage"}, +) + +func makeTableSpanStatsGenerator( + ctx context.Context, evalCtx *eval.Context, args tree.Datums, +) (eval.ValueGenerator, error) { + // The user must have ADMIN role or VIEWACTIVITY/VIEWACTIVITYREDACTED permission to use this builtin. + hasViewActivity, err := evalCtx.SessionAccessor.HasViewActivityOrViewActivityRedactedRole(ctx) + if err != nil { + return nil, err + } + if !hasViewActivity { + return nil, pgerror.Newf(pgcode.InsufficientPrivilege, "user needs ADMIN role or the VIEWACTIVITY/VIEWACTIVITYREDACTED permission to view span statistics") + } + dbId := 0 + tableId := 0 + if len(args) > 0 { + dbId = int(tree.MustBeDInt(args[0])) + if dbId <= 0 { + return nil, errors.New("provided database id must be greater than or equal to 1") + } + } + if len(args) > 1 { + tableId = int(tree.MustBeDInt(args[1])) + if tableId <= 0 { + return nil, errors.New("provided table id must be greater than or equal to 1") + } + } + return newTableSpanStatsIterator(evalCtx, dbId, tableId), nil +} diff --git a/pkg/sql/sem/eval/deps.go b/pkg/sql/sem/eval/deps.go index 9b4999bb2820..4e008145c29a 100644 --- a/pkg/sql/sem/eval/deps.go +++ b/pkg/sql/sem/eval/deps.go @@ -371,6 +371,10 @@ type Planner interface { // GetRangeDescByID gets the RangeDescriptor by the specified RangeID. GetRangeDescByID(context.Context, roachpb.RangeID) (roachpb.RangeDescriptor, error) + + SpanStats(context.Context, roachpb.RKey, roachpb.RKey) (*roachpb.SpanStatsResponse, error) + + GetDetailsForSpanStats(ctx context.Context, dbId int, tableId int) (InternalRows, error) } // InternalRows is an iterator interface that's exposed by the internal @@ -433,6 +437,10 @@ type SessionAccessor interface { // HasRoleOption returns nil iff the current session user has the specified // role option. HasRoleOption(ctx context.Context, roleOption roleoption.Option) (bool, error) + + // HasViewActivityOrViewActivityRedactedRole returns true iff the current session user has the + // VIEWACTIVITY or VIEWACTIVITYREDACTED permission. + HasViewActivityOrViewActivityRedactedRole(ctx context.Context) (bool, error) } // PreparedStatementState is a limited interface that exposes metadata about