From c62aa82bef7bf59cb50dc69328ac54ad447e9f9c Mon Sep 17 00:00:00 2001 From: "turbolytics.io" Date: Sat, 10 Oct 2026 07:03:47 -0400 Subject: [PATCH] turbostats: a database counts its tables and its partitions A database section said how big the database is, but not how many tables it holds. A table partitioned by day adds a partition a day until retention drops one. When retention stops, the partition count climbs before the disk does, and nothing reported it. Control's own database holds 172 tables, 148 of them partitions under 10 parents. DatabaseResources sends table_count, the tables in the database's own schemas with partitions included, and partition_count, how many of them are partitions. Both are optional: a kind that cannot count its tables leaves both out. One round-trip test, which fails to build without the fields. The widest database bundle grows 54 bytes, to 84,091, under the 128 KiB a receiver allows. The schema is regenerated. If this is wrong, a receiver misreads the two counts. Both fields are new and optional, so a receiver that does not know them ignores them, and every existing field is unchanged. --- CHANGELOG.md | 7 +++++++ internal/turbostats/dimensional_test.go | 2 ++ turbostats/wire/database.go | 7 +++++++ turbostats/wire/database_test.go | 20 ++++++++++++++++++++ turbostats/wire/schema/bundle.schema.json | 7 +++++++ 5 files changed, 43 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6b37552..42bf6eab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ ### Added +- **TurboStats databases count their tables.** `DatabaseResources` sends + `table_count`, the tables in the database's own schemas with partitions + included, and `partition_count`, how many of them are partitions. A + table partitioned by day adds a partition a day until retention drops + one, so a partition count that keeps climbing is retention that + stopped. The widest database bundle grows 54 bytes, to 84,091. + - **TurboStats tables carry their schema and their writes.** A `DatabaseTable` sends `schema_hash` every report, a hash of its columns' names, types and nullability, and `schema_changes` in the report where diff --git a/internal/turbostats/dimensional_test.go b/internal/turbostats/dimensional_test.go index cfa66fdd..914386a7 100644 --- a/internal/turbostats/dimensional_test.go +++ b/internal/turbostats/dimensional_test.go @@ -620,6 +620,8 @@ func widestDatabase(tables, replicas, errors, queries, changes int) *wire.Databa SizeBytes: &big, OldestTransactionSeconds: &big, Memory: &wire.DatabaseMemory{SharedBuffersBytes: big}, + TableCount: &n, + PartitionCount: &n, }, Replication: &wire.DatabaseReplication{Role: "primary", LagSeconds: &secs, LastReplayedAt: &at, Upstream: host + ":65535"}, Load: &wire.DatabaseLoad{ diff --git a/turbostats/wire/database.go b/turbostats/wire/database.go index 1383198c..42ec35b3 100644 --- a/turbostats/wire/database.go +++ b/turbostats/wire/database.go @@ -137,6 +137,13 @@ type DatabaseResources struct { // one idle in transaction holds vacuum back. OldestTransactionSeconds *int64 `json:"oldest_transaction_seconds,omitempty"` Memory *DatabaseMemory `json:"memory,omitempty"` + // TableCount is the tables in the database's own schemas, partitions + // included; the system catalogs are not counted. PartitionCount is how + // many of them are partitions. A table partitioned by day adds one a + // day until retention drops one, so a PartitionCount that keeps + // climbing is retention that stopped. + TableCount *int `json:"table_count,omitempty"` + PartitionCount *int `json:"partition_count,omitempty"` } // DatabaseConnections is connections in use against the configured maximum. diff --git a/turbostats/wire/database_test.go b/turbostats/wire/database_test.go index a425505a..f495d37c 100644 --- a/turbostats/wire/database_test.go +++ b/turbostats/wire/database_test.go @@ -163,3 +163,23 @@ func TestDatabaseTable_SchemaAndWritesRoundTrip(t *testing.T) { assert.That(t, !strings.Contains(string(out), "rows_inserted")) assert.Equal(t, 60, MaxDatabaseSchemaChanges) } + +// A database counts its tables and how many of them are partitions. A +// table partitioned by day adds a partition a day until retention drops +// one, so a partition count that keeps climbing is retention that stopped. +func TestDatabaseResources_TableAndPartitionCountsRoundTrip(t *testing.T) { + coverage.Covers(t, "observability.turbostats") + raw := `{"size_bytes":247463936,"table_count":172,"partition_count":148}` + var r DatabaseResources + assert.NoError(t, json.Unmarshal([]byte(raw), &r)) + assert.Equal(t, 172, *r.TableCount) + assert.Equal(t, 148, *r.PartitionCount) + out, err := json.Marshal(r) + assert.NoError(t, err) + assert.Equal(t, raw, string(out)) + + // A kind that cannot count its tables leaves both out. + out, err = json.Marshal(DatabaseResources{}) + assert.NoError(t, err) + assert.Equal(t, `{}`, string(out)) +} diff --git a/turbostats/wire/schema/bundle.schema.json b/turbostats/wire/schema/bundle.schema.json index 34bf4a47..4460f699 100644 --- a/turbostats/wire/schema/bundle.schema.json +++ b/turbostats/wire/schema/bundle.schema.json @@ -618,6 +618,13 @@ "required": [ "shared_buffers_bytes" ] + }, + "table_count": { + "type": "integer", + "description": "TableCount is the tables in the database's own schemas, partitions\nincluded; the system catalogs are not counted. PartitionCount is how\nmany of them are partitions. A table partitioned by day adds one a\nday until retention drops one, so a PartitionCount that keeps\nclimbing is retention that stopped." + }, + "partition_count": { + "type": "integer" } }, "type": "object"