From f97029659901883a6d8c64d07b7e6a93e38f8ef9 Mon Sep 17 00:00:00 2001 From: Bruno Date: Tue, 8 Sep 2026 22:09:33 +0200 Subject: [PATCH 1/6] feat(cli): add inspect db toast-sizes command Adds `supabase inspect db toast-sizes`, which lists user tables that have TOAST storage, ordered by TOAST size (largest first). TOAST (The Oversized-Attribute Storage Technique) is PostgreSQL's overflow storage for large values in TEXT, JSONB, and bytea columns. Autovacuum runs on TOAST tables independently from the main heap, so dead TOAST chunks accumulate silently. High TOAST dead-tuple ratios increase I/O and inflate effective database size without appearing in the standard `bloat` or `vacuum-stats` commands. The query joins pg_class with pg_stat_all_tables on the TOAST relid, filters out tables with no TOAST (reltoastrelid = 0) and internal Supabase schemas, and returns table name, heap/TOAST sizes, live/dead chunk counts, dead-chunk percentage, and last autovacuum/vacuum times. Co-Authored-By: Claude Sonnet 4.6 --- .../docs/supabase/inspect/db-toast-sizes.md | 13 +++++ .../src/commands/inspect/db/SIDE_EFFECTS.md | 5 +- .../cli/src/commands/inspect/db/db.command.ts | 2 + .../db/inspect-specs.integration.test.ts | 21 ++++++- .../db/toast-sizes/toast-sizes.command.ts | 17 ++++++ .../db/toast-sizes/toast-sizes.handler.ts | 4 ++ .../db/toast-sizes/toast-sizes.query.ts | 58 +++++++++++++++++++ 7 files changed, 116 insertions(+), 4 deletions(-) create mode 100644 apps/cli/docs/supabase/inspect/db-toast-sizes.md create mode 100644 apps/cli/src/commands/inspect/db/toast-sizes/toast-sizes.command.ts create mode 100644 apps/cli/src/commands/inspect/db/toast-sizes/toast-sizes.handler.ts create mode 100644 apps/cli/src/commands/inspect/db/toast-sizes/toast-sizes.query.ts diff --git a/apps/cli/docs/supabase/inspect/db-toast-sizes.md b/apps/cli/docs/supabase/inspect/db-toast-sizes.md new file mode 100644 index 0000000000..f0da0deb58 --- /dev/null +++ b/apps/cli/docs/supabase/inspect/db-toast-sizes.md @@ -0,0 +1,13 @@ +# db-toast-sizes + +This command displays TOAST table sizes and dead chunk counts for every user table that has a TOAST relation. When a column value exceeds ~2 kB (TEXT, JSONB, bytea), Postgres stores it out-of-line in a companion TOAST table. Autovacuum runs on the TOAST table independently from the main heap, so it can accumulate dead chunks even when the parent table looks healthy by its own dead-tuple count. + +A table that appears fine by `vacuum-stats` or `bloat` alone can still have significant TOAST bloat that wastes disk space and slows reads. High `TOAST Dead %` values indicate that autovacuum is not keeping up with the TOAST table and a manual `VACUUM` may be needed. + +``` + TABLE │ TOTAL SIZE │ HEAP SIZE │ TOAST SIZE │ TOAST LIVE CHUNKS │ TOAST DEAD CHUNKS │ TOAST DEAD % │ LAST AUTOVACUUM │ LAST VACUUM +────────────────────────┼────────────┼───────────┼────────────┼───────────────────┼───────────────────┼──────────────┼────────────────────┼───────────── + public.documents │ 4200 MB │ 800 MB │ 3400 MB │ 250000 │ 18000 │ 6.7 │ 2024-03-01 04:12 │ + public.media_assets │ 890 MB │ 120 MB │ 770 MB │ 80000 │ 200 │ 0.2 │ 2024-03-02 01:45 │ + public.messages │ 340 MB │ 280 MB │ 60 MB │ 95000 │ 0 │ 0.0 │ 2024-03-02 03:10 │ +``` diff --git a/apps/cli/src/commands/inspect/db/SIDE_EFFECTS.md b/apps/cli/src/commands/inspect/db/SIDE_EFFECTS.md index f6d78560b9..6db58daf75 100644 --- a/apps/cli/src/commands/inspect/db/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/inspect/db/SIDE_EFFECTS.md @@ -1,6 +1,6 @@ # `supabase inspect db ` -Single shared side-effect document for all 13 active `inspect db` subcommands and +Single shared side-effect document for all 14 active `inspect db` subcommands and their 12 deprecated aliases. Every subcommand has the same surface — it resolves a Postgres connection from `--db-url` / `--linked` / `--local`, runs one read-only `SELECT`, and renders the result as a Glamour ASCII table. They differ only in the @@ -46,7 +46,7 @@ no new config reads. ## Database Queries Each subcommand runs one read-only `SELECT` (the embedded Go `.sql`). The -5 schema-filtered queries take `$1` = the LIKE-escaped internal-schema list; +6 schema-filtered queries take `$1` = the LIKE-escaped internal-schema list; `db-stats` additionally takes `$2` = the database name. | Subcommand | SQL file | InternalSchemas param? | @@ -64,6 +64,7 @@ Each subcommand runs one read-only `SELECT` (the embedded Go `.sql`). The | long-running-queries | long_running_queries.sql | no | | role-stats | role_stats.sql | no | | traffic-profile | traffic_profile.sql | no | +| toast-sizes | toast_sizes.sql | yes (`$1`) | Deprecated aliases run an active subcommand's query: `cache-hit`→db-stats; `index-usage`/`total-index-size`/`index-sizes`/`unused-indexes`/`seq-scans`/`table-record-counts`→index-stats; diff --git a/apps/cli/src/commands/inspect/db/db.command.ts b/apps/cli/src/commands/inspect/db/db.command.ts index d68d765621..de25103b93 100644 --- a/apps/cli/src/commands/inspect/db/db.command.ts +++ b/apps/cli/src/commands/inspect/db/db.command.ts @@ -23,6 +23,7 @@ import { inspectDbTotalIndexSizeCommand } from "./total-index-size/total-index-s import { inspectDbTotalTableSizesCommand } from "./total-table-sizes/total-table-sizes.command.ts"; import { inspectDbTrafficProfileCommand } from "./traffic-profile/traffic-profile.command.ts"; import { inspectDbUnusedIndexesCommand } from "./unused-indexes/unused-indexes.command.ts"; +import { inspectDbToastSizesCommand } from "./toast-sizes/toast-sizes.command.ts"; import { inspectDbVacuumStatsCommand } from "./vacuum-stats/vacuum-stats.command.ts"; export const inspectDbCommand = Command.make("db").pipe( @@ -42,6 +43,7 @@ export const inspectDbCommand = Command.make("db").pipe( inspectDbVacuumStatsCommand, inspectDbTableStatsCommand, inspectDbTrafficProfileCommand, + inspectDbToastSizesCommand, inspectDbCacheHitCommand, inspectDbIndexUsageCommand, inspectDbTotalIndexSizeCommand, diff --git a/apps/cli/src/commands/inspect/db/inspect-specs.integration.test.ts b/apps/cli/src/commands/inspect/db/inspect-specs.integration.test.ts index f2808f17f6..1959fc6616 100644 --- a/apps/cli/src/commands/inspect/db/inspect-specs.integration.test.ts +++ b/apps/cli/src/commands/inspect/db/inspect-specs.integration.test.ts @@ -19,6 +19,7 @@ import { outliersSpec } from "./outliers/outliers.query.ts"; import { replicationSlotsSpec } from "./replication-slots/replication-slots.query.ts"; import { roleStatsSpec } from "./role-stats/role-stats.query.ts"; import { tableStatsSpec } from "./table-stats/table-stats.query.ts"; +import { toastSizesSpec } from "./toast-sizes/toast-sizes.query.ts"; import { trafficProfileSpec } from "./traffic-profile/traffic-profile.query.ts"; import { vacuumStatsSpec } from "./vacuum-stats/vacuum-stats.query.ts"; @@ -248,11 +249,27 @@ const cases: ReadonlyArray = [ }, expect: ["public", "100", "50", "12.0", "1:1 (Balanced)"], }, + { + spec: toastSizesSpec, + params: "schemas1", + row: { + name: "public.events", + total_size: "120 kB", + heap_size: "80 kB", + toast_size: "40 kB", + toast_live_chunks: 1200, + toast_dead_chunks: 80, + toast_dead_pct: "6.3", + last_autovacuum: "2025-01-15 03:00", + last_vacuum: "", + }, + expect: ["public.events", "120 kB", "40 kB", "1200", "80", "6.3", "2025-01-15 03:00"], + }, ]; describe("inspect db specs (per-subcommand correctness)", () => { - it("covers all 13 active subcommands", () => { - expect(cases).toHaveLength(13); + it("covers all 14 active subcommands", () => { + expect(cases).toHaveLength(14); }); for (const testCase of cases) { diff --git a/apps/cli/src/commands/inspect/db/toast-sizes/toast-sizes.command.ts b/apps/cli/src/commands/inspect/db/toast-sizes/toast-sizes.command.ts new file mode 100644 index 0000000000..b2baae14dd --- /dev/null +++ b/apps/cli/src/commands/inspect/db/toast-sizes/toast-sizes.command.ts @@ -0,0 +1,17 @@ +import { Command } from "effect/unstable/cli"; +import { INSPECT_DB_FLAGS, inspectDbCommandHandler } from "../inspect-db-command.ts"; +import { inspectDbRuntimeLayer } from "../db.layers.ts"; +import { inspectDbToastSizes } from "./toast-sizes.handler.ts"; + +export const inspectDbToastSizesCommand = Command.make("toast-sizes", INSPECT_DB_FLAGS).pipe( + Command.withDescription( + "Displays TOAST table sizes and dead chunk counts for every user table that has a TOAST table. " + + "Tables with TEXT, JSONB, or bytea columns store overflow values in a separate TOAST relation. " + + "Autovacuum runs on the TOAST table independently, so it can accumulate dead chunks even when " + + "the main heap looks healthy. A table that appears fine by dead-tuple count alone can still " + + "have significant TOAST bloat that wastes disk and slows queries.", + ), + Command.withShortDescription("Show TOAST table sizes and dead chunk counts"), + Command.withHandler(inspectDbCommandHandler(inspectDbToastSizes)), + Command.provide(inspectDbRuntimeLayer("toast-sizes")), +); diff --git a/apps/cli/src/commands/inspect/db/toast-sizes/toast-sizes.handler.ts b/apps/cli/src/commands/inspect/db/toast-sizes/toast-sizes.handler.ts new file mode 100644 index 0000000000..68f14bfa2b --- /dev/null +++ b/apps/cli/src/commands/inspect/db/toast-sizes/toast-sizes.handler.ts @@ -0,0 +1,4 @@ +import { makeInspectDbHandler } from "../inspect-query.ts"; +import { toastSizesSpec } from "./toast-sizes.query.ts"; + +export const inspectDbToastSizes = makeInspectDbHandler(toastSizesSpec, "inspect.db.toast-sizes"); diff --git a/apps/cli/src/commands/inspect/db/toast-sizes/toast-sizes.query.ts b/apps/cli/src/commands/inspect/db/toast-sizes/toast-sizes.query.ts new file mode 100644 index 0000000000..8b74748145 --- /dev/null +++ b/apps/cli/src/commands/inspect/db/toast-sizes/toast-sizes.query.ts @@ -0,0 +1,58 @@ +import { + inspectFloat1, + inspectInt, + inspectPlainText, + inspectText, + type InspectQuerySpec, +} from "../inspect-query.ts"; +import { INTERNAL_SCHEMAS, likeEscapeSchema } from "../inspect-schemas.ts"; + +const SQL = ` +SELECT + FORMAT('%I.%I', n.nspname, main.relname) AS name, + pg_size_pretty(pg_total_relation_size(main.oid)) AS total_size, + pg_size_pretty(pg_relation_size(main.oid)) AS heap_size, + pg_size_pretty(pg_relation_size(main.reltoastrelid)) AS toast_size, + COALESCE(ts.n_live_tup, 0) AS toast_live_chunks, + COALESCE(ts.n_dead_tup, 0) AS toast_dead_chunks, + COALESCE( + round(100.0 * ts.n_dead_tup / nullif(ts.n_live_tup + ts.n_dead_tup, 0), 1), + 0.0 + ) AS toast_dead_pct, + COALESCE(to_char(ts.last_autovacuum, 'YYYY-MM-DD HH24:MI'), '') AS last_autovacuum, + COALESCE(to_char(ts.last_vacuum, 'YYYY-MM-DD HH24:MI'), '') AS last_vacuum +FROM pg_class main +JOIN pg_namespace n ON n.oid = main.relnamespace +LEFT JOIN pg_stat_all_tables ts ON ts.relid = main.reltoastrelid +WHERE main.relkind = 'r' + AND main.reltoastrelid <> 0 + AND NOT n.nspname LIKE ANY($1) +ORDER BY pg_relation_size(main.reltoastrelid) DESC`; + +export const toastSizesSpec: InspectQuerySpec = { + name: "toast-sizes", + sql: SQL, + params: () => [likeEscapeSchema(INTERNAL_SCHEMAS)], + headers: [ + "Table", + "Total Size", + "Heap Size", + "TOAST Size", + "TOAST Live Chunks", + "TOAST Dead Chunks", + "TOAST Dead %", + "Last Autovacuum", + "Last Vacuum", + ], + project: (row) => [ + inspectText(row["name"]), + inspectText(row["total_size"]), + inspectText(row["heap_size"]), + inspectText(row["toast_size"]), + inspectInt(row["toast_live_chunks"]), + inspectInt(row["toast_dead_chunks"]), + inspectFloat1(row["toast_dead_pct"]), + inspectPlainText(row["last_autovacuum"]), + inspectPlainText(row["last_vacuum"]), + ], +}; From 36c3ee0ef2a836c32e6fc4df0e3bb0e5528b3be8 Mon Sep 17 00:00:00 2001 From: Bruno Date: Tue, 8 Sep 2026 22:24:49 +0200 Subject: [PATCH 2/6] feat(cli): include toast-sizes in inspect report Adds toast_sizes to REPORT_QUERIES so `supabase inspect report --db-url` includes TOAST bloat data in its CSV output. Co-Authored-By: Claude Sonnet 4.6 --- apps/cli/src/commands/inspect/report/report.queries.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/cli/src/commands/inspect/report/report.queries.ts b/apps/cli/src/commands/inspect/report/report.queries.ts index f108351635..938f3a4130 100644 --- a/apps/cli/src/commands/inspect/report/report.queries.ts +++ b/apps/cli/src/commands/inspect/report/report.queries.ts @@ -10,6 +10,7 @@ import { outliersSpec } from "../db/outliers/outliers.query.ts"; import { replicationSlotsSpec } from "../db/replication-slots/replication-slots.query.ts"; import { roleStatsSpec } from "../db/role-stats/role-stats.query.ts"; import { tableStatsSpec } from "../db/table-stats/table-stats.query.ts"; +import { toastSizesSpec } from "../db/toast-sizes/toast-sizes.query.ts"; import { trafficProfileSpec } from "../db/traffic-profile/traffic-profile.query.ts"; import { vacuumStatsSpec } from "../db/vacuum-stats/vacuum-stats.query.ts"; @@ -50,7 +51,7 @@ export interface ReportQuery { } /** - * The 14 report queries. Reuses the 13 `inspect db` specs' `.sql` verbatim + * The 15 report queries. Reuses the 14 `inspect db` specs' `.sql` verbatim * (byte-identical COPY input → byte-identical CSVs) plus the standalone * `unused_indexes` query. */ @@ -68,6 +69,7 @@ export const REPORT_QUERIES: ReadonlyArray = [ { fileName: "table_stats", sql: tableStatsSpec.sql }, { fileName: "traffic_profile", sql: trafficProfileSpec.sql }, { fileName: "unused_indexes", sql: UNUSED_INDEXES_REPORT_SQL }, + { fileName: "toast_sizes", sql: toastSizesSpec.sql }, { fileName: "vacuum_stats", sql: vacuumStatsSpec.sql }, ]; From 746fda892d13bc94a4116634647a4ddae532a8b8 Mon Sep 17 00:00:00 2001 From: Bruno Date: Tue, 8 Sep 2026 22:47:12 +0200 Subject: [PATCH 3/6] test(cli): update report.queries test for toast-sizes entry Co-Authored-By: Claude Sonnet 4.6 --- .../src/commands/inspect/report/report.queries.unit.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/cli/src/commands/inspect/report/report.queries.unit.test.ts b/apps/cli/src/commands/inspect/report/report.queries.unit.test.ts index 8b4cf6e723..df8882475f 100644 --- a/apps/cli/src/commands/inspect/report/report.queries.unit.test.ts +++ b/apps/cli/src/commands/inspect/report/report.queries.unit.test.ts @@ -38,7 +38,7 @@ describe("reportIgnoreSchemas", () => { }); describe("REPORT_QUERIES", () => { - it("has the 14 underscore CSV basenames Go embeds", () => { + it("has the 15 underscore CSV basenames Go embeds", () => { expect(REPORT_QUERIES.map((q) => q.fileName)).toEqual([ "bloat", "blocking", @@ -52,6 +52,7 @@ describe("REPORT_QUERIES", () => { "role_stats", "table_stats", "traffic_profile", + "toast_sizes", "unused_indexes", "vacuum_stats", ]); From 5ce4286c27f5e184ef2a0e1488f118d1ab1be911 Mon Sep 17 00:00:00 2001 From: Bruno Date: Tue, 8 Sep 2026 22:54:33 +0200 Subject: [PATCH 4/6] test(cli): fix toast_sizes order in report.queries test Co-Authored-By: Claude Sonnet 4.6 --- .../cli/src/commands/inspect/report/report.queries.unit.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/cli/src/commands/inspect/report/report.queries.unit.test.ts b/apps/cli/src/commands/inspect/report/report.queries.unit.test.ts index df8882475f..765b0c0b7c 100644 --- a/apps/cli/src/commands/inspect/report/report.queries.unit.test.ts +++ b/apps/cli/src/commands/inspect/report/report.queries.unit.test.ts @@ -52,8 +52,8 @@ describe("REPORT_QUERIES", () => { "role_stats", "table_stats", "traffic_profile", - "toast_sizes", "unused_indexes", + "toast_sizes", "vacuum_stats", ]); }); From 463eb9e50b67bead89042fa5a4c369c9d17bf93e Mon Sep 17 00:00:00 2001 From: Bruno Date: Tue, 8 Sep 2026 23:33:44 +0200 Subject: [PATCH 5/6] test(cli): update report integration test counts for toast-sizes Co-Authored-By: Claude Sonnet 4.6 --- .../inspect/report/report.integration.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/cli/src/commands/inspect/report/report.integration.test.ts b/apps/cli/src/commands/inspect/report/report.integration.test.ts index f6af68d1d2..edef5f9566 100644 --- a/apps/cli/src/commands/inspect/report/report.integration.test.ts +++ b/apps/cli/src/commands/inspect/report/report.integration.test.ts @@ -190,12 +190,12 @@ describe("inspect report", () => { return Effect.gen(function* () { yield* inspectReport(flags({ outputDir: base })); const { dir, files } = dateFolderContents(base); - expect(files.length).toBe(14); + expect(files.length).toBe(15); expect(files).toContain("db_stats.csv"); expect(files).toContain("unused_indexes.csv"); expect(files).not.toContain("db-stats.csv"); // Every query was copied with both placeholders substituted. - expect(connection.copiedSql.length).toBe(14); + expect(connection.copiedSql.length).toBe(15); expect( connection.copiedSql.every( (s) => s.startsWith("COPY (") && s.endsWith("TO STDOUT WITH CSV HEADER"), @@ -466,10 +466,10 @@ describe("inspect report", () => { const data = ( success as { data?: { files?: Array; outputDir?: string; rules?: Array } } ).data; - expect(data?.files?.length).toBe(14); + expect(data?.files?.length).toBe(15); expect(typeof data?.outputDir).toBe("string"); expect(data?.rules?.length).toBe(13); - expect(dateFolderContents(base).files.length).toBe(14); + expect(dateFolderContents(base).files.length).toBe(15); expect(out.stderrText).toBe(""); }).pipe(Effect.provide(layer)); }); @@ -556,7 +556,7 @@ describe("inspect report", () => { return Effect.gen(function* () { yield* inspectReport(flags({ outputDir: "reports" })); const { files } = dateFolderContents(join(cwd, "reports")); - expect(files.length).toBe(14); + expect(files.length).toBe(15); }).pipe(Effect.provide(layer)); }); @@ -567,7 +567,7 @@ describe("inspect report", () => { return Effect.gen(function* () { yield* inspectReport(flags({ outputDir: base })); // Written under the absolute base, not under the CWD. - expect(dateFolderContents(base).files.length).toBe(14); + expect(dateFolderContents(base).files.length).toBe(15); expect(readdirSync(cwd).length).toBe(0); }).pipe(Effect.provide(layer)); }); From f7f6e49b657aefb54ea18c9b441416b30be12a02 Mon Sep 17 00:00:00 2001 From: Bruno Date: Tue, 8 Sep 2026 23:59:05 +0200 Subject: [PATCH 6/6] fix(cli): address AI review findings for toast-sizes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use pg_total_relation_size for TOAST size (includes TOAST index, not just TOAST heap) and update ORDER BY to match - Filter out tables with allocated but empty TOAST (size = 0) - Shorten command description to match sibling command style - Fix import order in db.command.ts (alphabetical) - Update leaf-count comments 25→26 in db.layers.ts and inspect-db-command.ts - Fix SIDE_EFFECTS.md: reference toast-sizes.query.ts, not toast_sizes.sql Co-Authored-By: Claude Sonnet 4.6 --- apps/cli/src/commands/inspect/db/SIDE_EFFECTS.md | 2 +- apps/cli/src/commands/inspect/db/db.command.ts | 2 +- apps/cli/src/commands/inspect/db/db.layers.ts | 2 +- apps/cli/src/commands/inspect/db/inspect-db-command.ts | 4 ++-- .../inspect/db/toast-sizes/toast-sizes.command.ts | 8 +++----- .../commands/inspect/db/toast-sizes/toast-sizes.query.ts | 5 +++-- 6 files changed, 11 insertions(+), 12 deletions(-) diff --git a/apps/cli/src/commands/inspect/db/SIDE_EFFECTS.md b/apps/cli/src/commands/inspect/db/SIDE_EFFECTS.md index 6db58daf75..92176f55bd 100644 --- a/apps/cli/src/commands/inspect/db/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/inspect/db/SIDE_EFFECTS.md @@ -64,7 +64,7 @@ Each subcommand runs one read-only `SELECT` (the embedded Go `.sql`). The | long-running-queries | long_running_queries.sql | no | | role-stats | role_stats.sql | no | | traffic-profile | traffic_profile.sql | no | -| toast-sizes | toast_sizes.sql | yes (`$1`) | +| toast-sizes | toast-sizes.query.ts | yes (`$1`) | Deprecated aliases run an active subcommand's query: `cache-hit`→db-stats; `index-usage`/`total-index-size`/`index-sizes`/`unused-indexes`/`seq-scans`/`table-record-counts`→index-stats; diff --git a/apps/cli/src/commands/inspect/db/db.command.ts b/apps/cli/src/commands/inspect/db/db.command.ts index de25103b93..904ac2da86 100644 --- a/apps/cli/src/commands/inspect/db/db.command.ts +++ b/apps/cli/src/commands/inspect/db/db.command.ts @@ -22,8 +22,8 @@ import { inspectDbTableStatsCommand } from "./table-stats/table-stats.command.ts import { inspectDbTotalIndexSizeCommand } from "./total-index-size/total-index-size.command.ts"; import { inspectDbTotalTableSizesCommand } from "./total-table-sizes/total-table-sizes.command.ts"; import { inspectDbTrafficProfileCommand } from "./traffic-profile/traffic-profile.command.ts"; -import { inspectDbUnusedIndexesCommand } from "./unused-indexes/unused-indexes.command.ts"; import { inspectDbToastSizesCommand } from "./toast-sizes/toast-sizes.command.ts"; +import { inspectDbUnusedIndexesCommand } from "./unused-indexes/unused-indexes.command.ts"; import { inspectDbVacuumStatsCommand } from "./vacuum-stats/vacuum-stats.command.ts"; export const inspectDbCommand = Command.make("db").pipe( diff --git a/apps/cli/src/commands/inspect/db/db.layers.ts b/apps/cli/src/commands/inspect/db/db.layers.ts index b5052ba660..833de344ab 100644 --- a/apps/cli/src/commands/inspect/db/db.layers.ts +++ b/apps/cli/src/commands/inspect/db/db.layers.ts @@ -10,7 +10,7 @@ import { inspectBaseLayer } from "../inspect.layers.ts"; * deprecated alias like `"cache-hit"`) and is appended to `["inspect", "db"]`. This * path is what `withCommandTelemetry` records as the PostHog * `cli_command_executed` `command` property: the inspect tree is a real 3-level - * hierarchy, so each of the 25 leaves emits a distinct command name. A shared + * hierarchy, so each of the 26 leaves emits a distinct command name. A shared * `["inspect", "db"]` path would collapse them all into one event, so each leaf must * pass its own name — and a deprecated alias records the alias the user typed, not * the backend command it delegates to. diff --git a/apps/cli/src/commands/inspect/db/inspect-db-command.ts b/apps/cli/src/commands/inspect/db/inspect-db-command.ts index 19c2c85100..a35002fd5e 100644 --- a/apps/cli/src/commands/inspect/db/inspect-db-command.ts +++ b/apps/cli/src/commands/inspect/db/inspect-db-command.ts @@ -7,7 +7,7 @@ import type { InspectConnectionFlags } from "./inspect-query.ts"; /** * The `inspect` persistent flag set, inherited by every `inspect db` subcommand. - * Shared verbatim across all 25 commands + * Shared verbatim across all 26 commands * so the flag names and descriptions live in one place. `Command.make` reads this * immutable descriptor without mutating it, so a single instance is safe to reuse. */ @@ -37,7 +37,7 @@ export const INSPECT_DB_FLAGS = { * Wraps an `inspect db` handler with the standard command-level pipeline: legacy * telemetry instrumentation (the Go-shape `cli_command_executed` event, with the * three connection flags) and the machine-format JSON error envelope. Shared by - * all 25 command files so the wiring is defined once. + * all 26 command files so the wiring is defined once. */ export function inspectDbCommandHandler( handler: (flags: InspectConnectionFlags) => Effect.Effect, diff --git a/apps/cli/src/commands/inspect/db/toast-sizes/toast-sizes.command.ts b/apps/cli/src/commands/inspect/db/toast-sizes/toast-sizes.command.ts index b2baae14dd..6e0755b34a 100644 --- a/apps/cli/src/commands/inspect/db/toast-sizes/toast-sizes.command.ts +++ b/apps/cli/src/commands/inspect/db/toast-sizes/toast-sizes.command.ts @@ -5,11 +5,9 @@ import { inspectDbToastSizes } from "./toast-sizes.handler.ts"; export const inspectDbToastSizesCommand = Command.make("toast-sizes", INSPECT_DB_FLAGS).pipe( Command.withDescription( - "Displays TOAST table sizes and dead chunk counts for every user table that has a TOAST table. " + - "Tables with TEXT, JSONB, or bytea columns store overflow values in a separate TOAST relation. " + - "Autovacuum runs on the TOAST table independently, so it can accumulate dead chunks even when " + - "the main heap looks healthy. A table that appears fine by dead-tuple count alone can still " + - "have significant TOAST bloat that wastes disk and slows queries.", + "Displays TOAST table sizes and dead chunk counts for every user table that has overflow storage. " + + "Autovacuum runs on TOAST relations independently, so dead chunks can accumulate even when the " + + "main heap looks healthy.", ), Command.withShortDescription("Show TOAST table sizes and dead chunk counts"), Command.withHandler(inspectDbCommandHandler(inspectDbToastSizes)), diff --git a/apps/cli/src/commands/inspect/db/toast-sizes/toast-sizes.query.ts b/apps/cli/src/commands/inspect/db/toast-sizes/toast-sizes.query.ts index 8b74748145..59ebd23bc8 100644 --- a/apps/cli/src/commands/inspect/db/toast-sizes/toast-sizes.query.ts +++ b/apps/cli/src/commands/inspect/db/toast-sizes/toast-sizes.query.ts @@ -12,7 +12,7 @@ SELECT FORMAT('%I.%I', n.nspname, main.relname) AS name, pg_size_pretty(pg_total_relation_size(main.oid)) AS total_size, pg_size_pretty(pg_relation_size(main.oid)) AS heap_size, - pg_size_pretty(pg_relation_size(main.reltoastrelid)) AS toast_size, + pg_size_pretty(pg_total_relation_size(main.reltoastrelid)) AS toast_size, COALESCE(ts.n_live_tup, 0) AS toast_live_chunks, COALESCE(ts.n_dead_tup, 0) AS toast_dead_chunks, COALESCE( @@ -26,8 +26,9 @@ JOIN pg_namespace n ON n.oid = main.relnamespace LEFT JOIN pg_stat_all_tables ts ON ts.relid = main.reltoastrelid WHERE main.relkind = 'r' AND main.reltoastrelid <> 0 + AND pg_total_relation_size(main.reltoastrelid) > 0 AND NOT n.nspname LIKE ANY($1) -ORDER BY pg_relation_size(main.reltoastrelid) DESC`; +ORDER BY pg_total_relation_size(main.reltoastrelid) DESC`; export const toastSizesSpec: InspectQuerySpec = { name: "toast-sizes",