Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/providers/mysql.md
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,12 @@ outright on Doris for a filter the statement does not need ([#573](https://githu
and the narrowest fix changes only what a grammar refuses. `getOverview()` also costs one round trip
fewer than before, reading uptime and connections out of the same result set.

**Database size is absent, never zeroed, when it is not measured.** `getOverview()` sizes the
database with `SUM(DATA_LENGTH + INDEX_LENGTH)` over `information_schema.tables`. A missing result
row, or a row without the `size_bytes` column, is no measurement at all: `databaseSizeBytes` is
omitted and `databaseSize` stays `"N/A"`. Only a returned SQL `NULL` — an empty database — is a
measured zero, and that reading is published as `0`/`"0 B"`.

**Graceful degradation — note the *different* failure modes:**
- `getHealth()` slow-queries: the digest rows, or **an empty list** — never a placeholder row, and
on this path **the reason is dropped**. It used to answer a single fabricated row
Expand Down
6 changes: 6 additions & 0 deletions docs/providers/postgres.md
Original file line number Diff line number Diff line change
Expand Up @@ -713,6 +713,12 @@ base) fans these out in parallel.
`getTableStats()` / `getIndexStats()` accept an optional `{ schema }` filter; with none they cover
all user schemas.

**Database size is absent, never zeroed, when it is not measured.** `getOverview()` sizes the
database with `pg_database_size`; a missing result row, or a row without `database_size_bytes`, is
no measurement at all, so `databaseSizeBytes` is omitted and `databaseSize` stays `"N/A"`. Only a
returned SQL `NULL` — an empty database — is a measured zero, and that reading is published as
`0`/`"0 B"`.

### 7.1 When the cache hit ratio is not measurable

The ratio comes from `pg_statio_user_tables`, and there are two ordinary states in which that view
Expand Down
8 changes: 5 additions & 3 deletions src/lib/db/providers/sql/mysql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
} from "../../types";
import { DatabaseConfigError, ConnectionError, QueryError, mapDatabaseError } from "../../errors";
import { formatBytes } from "../../utils/pool-manager";
import { measuredNullableAggregate } from "../../utils/measured-aggregate";
import { CACHE_HIT_RATIO_UNAVAILABLE, formatCacheHitRatio, measuredNumber } from "@/lib/monitoring-cache-ratio";

/**
Expand Down Expand Up @@ -1292,7 +1293,8 @@ export class MySQLProvider extends SQLBaseProvider {

// Get database size
const [sizeRows] = await runStatement(conn, OVERVIEW_DATABASE_SIZE_SQL, [this.config.database]);
const databaseSizeBytes = parseInt(sizeRows[0]?.size_bytes || "0");
const databaseSizeBytes = measuredNullableAggregate(sizeRows[0], "size_bytes");
const databaseSize = databaseSizeBytes === undefined ? "N/A" : formatBytes(databaseSizeBytes);

// Get table and index count
const [countRows] = await runStatement(conn, OVERVIEW_OBJECT_COUNTS_SQL, [this.config.database]);
Expand All @@ -1308,8 +1310,8 @@ export class MySQLProvider extends SQLBaseProvider {
...(uptimeSeconds === undefined ? {} : { startTime: new Date(Date.now() - uptimeSeconds * 1000) }),
...(activeConnections === undefined ? {} : { activeConnections }),
maxConnections,
databaseSize: formatBytes(databaseSizeBytes),
databaseSizeBytes,
databaseSize,
...(databaseSizeBytes === undefined ? {} : { databaseSizeBytes }),
tableCount: parseInt(tableCountRows[0]?.cnt || "0"),
indexCount: parseInt(countRows[0]?.index_count || "0"),
};
Expand Down
7 changes: 4 additions & 3 deletions src/lib/db/providers/sql/postgres.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
import { assertReadOnlyBudget, measureResultBytes } from "./read-only-budget";
import { postgresColumnTypes } from "./column-types";
import { formatBytes } from "../../utils/pool-manager";
import { measuredNullableAggregate } from "../../utils/measured-aggregate";
import { CACHE_HIT_RATIO_UNAVAILABLE, formatCacheHitRatio, measuredNumber } from "@/lib/monitoring-cache-ratio";

// ============================================================================
Expand Down Expand Up @@ -1813,8 +1814,8 @@ export class PostgresProvider extends SQLBaseProvider {
let databaseSizeBytes: number | undefined;
try {
const sizeRes = await client.query(OVERVIEW_SIZE_SQL, [this.config.database]);
databaseSize = sizeRes.rows[0].database_size || "0 bytes";
databaseSizeBytes = parseInt(sizeRes.rows[0].database_size_bytes || "0");
databaseSizeBytes = measuredNullableAggregate(sizeRes.rows[0], "database_size_bytes");
if (databaseSizeBytes !== undefined) databaseSize = formatBytes(databaseSizeBytes);
} catch {
databaseSize = "N/A";
databaseSizeBytes = undefined;
Expand Down Expand Up @@ -1842,7 +1843,7 @@ export class PostgresProvider extends SQLBaseProvider {
activeConnections,
maxConnections,
databaseSize,
databaseSizeBytes,
...(databaseSizeBytes === undefined ? {} : { databaseSizeBytes }),
tableCount,
indexCount,
};
Expand Down
72 changes: 72 additions & 0 deletions tests/integration/db/mysql-provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1560,6 +1560,78 @@ describe("MySQLProvider", () => {
expect(overview.startTime).toBeInstanceOf(Date);
});

test("a size result without the expected column leaves overview size absent", async () => {
mockExecuteFn = (sql: string) => {
const lower = sql.toLowerCase();
if (lower.includes("information_schema.tables") && lower.includes("sum(data_length") && !lower.includes("table_name")) {
return Promise.resolve([[{ size_mb: "12.50", name: "testdb" }], []]);
}
return defaultMockExecute(sql);
};

provider = new MySQLProvider(makeMySQLConfig());
await provider.connect();
const overview = await provider.getOverview();

expect("databaseSizeBytes" in overview).toBe(false);
expect(overview.databaseSize).toBe("N/A");
});

test("a size read with no result row leaves overview size absent", async () => {
mockExecuteFn = (sql: string) => {
const lower = sql.toLowerCase();
if (lower.includes("information_schema.tables") && lower.includes("sum(data_length") && !lower.includes("table_name")) {
return Promise.resolve([[], []]);
}
return defaultMockExecute(sql);
};

provider = new MySQLProvider(makeMySQLConfig());
await provider.connect();
const overview = await provider.getOverview();

expect("databaseSizeBytes" in overview).toBe(false);
expect(overview.databaseSize).toBe("N/A");
});

test("a non-finite size leaves overview size absent", async () => {
mockExecuteFn = (sql: string) => {
const lower = sql.toLowerCase();
if (lower.includes("information_schema.tables") && lower.includes("sum(data_length") && !lower.includes("table_name")) {
return Promise.resolve([[{ size_mb: "12.50", size_bytes: Number.POSITIVE_INFINITY, name: "testdb" }], []]);
}
return defaultMockExecute(sql);
};

provider = new MySQLProvider(makeMySQLConfig());
await provider.connect();
const overview = await provider.getOverview();

expect("databaseSizeBytes" in overview).toBe(false);
expect(overview.databaseSize).toBe("N/A");
});

test("a database that measures zero bytes keeps its measured zero size", async () => {
// The anti-vacuity twin of the tests above: `SUM(DATA_LENGTH + INDEX_LENGTH)`
// returns NULL over an empty schema, and that returned null aggregate is a
// measured zero the provider must keep publishing - never an absence.
mockExecuteFn = (sql: string) => {
const lower = sql.toLowerCase();
if (lower.includes("information_schema.tables") && lower.includes("sum(data_length") && !lower.includes("table_name")) {
return Promise.resolve([[{ size_mb: "0.00", size_bytes: null, name: "testdb" }], []]);
}
return defaultMockExecute(sql);
};

provider = new MySQLProvider(makeMySQLConfig());
await provider.connect();
const overview = await provider.getOverview();

expect("databaseSizeBytes" in overview).toBe(true);
expect(overview.databaseSizeBytes).toBe(0);
expect(overview.databaseSize).toBe("0 B");
});

test("does not call a MariaDB server MySQL", async () => {
mockExecuteFn = mariaDBMockExecute;

Expand Down
80 changes: 80 additions & 0 deletions tests/integration/db/postgres-provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2349,6 +2349,86 @@
// 90061 seconds = 1d 1h 1m
expect(overview.uptime).toBe("1d 1h 1m");
});

test("a size result without the expected column leaves overview size absent", async () => {
mockQueryFn = async (sql: string, params?: unknown[]) => {
const normalized = sql.trim().toLowerCase();
if (normalized.includes("pg_database_size") && normalized.includes("database_size_bytes")) {
return Promise.resolve({ rows: [{ database_size: "512 MB" }], fields: [], rowCount: 1 });
}
return defaultMockQuery(sql, params);

Check failure on line 2359 in tests/integration/db/postgres-provider.test.ts

View workflow job for this annotation

GitHub Actions / Engine Smoke - Build Payload

Expected 1 arguments, but got 2.
};

provider = new PostgresProvider(makePgConfig());
await provider.connect();
const overview = await provider.getOverview();

expect("databaseSizeBytes" in overview).toBe(false);
expect(overview.databaseSize).toBe("N/A");
});

test("a size read with no result row leaves overview size absent", async () => {
mockQueryFn = async (sql: string, params?: unknown[]) => {
const normalized = sql.trim().toLowerCase();
if (normalized.includes("pg_database_size") && normalized.includes("database_size_bytes")) {
return Promise.resolve({ rows: [], fields: [], rowCount: 0 });
}
return defaultMockQuery(sql, params);

Check failure on line 2376 in tests/integration/db/postgres-provider.test.ts

View workflow job for this annotation

GitHub Actions / Engine Smoke - Build Payload

Expected 1 arguments, but got 2.
};

provider = new PostgresProvider(makePgConfig());
await provider.connect();
const overview = await provider.getOverview();

expect("databaseSizeBytes" in overview).toBe(false);
expect(overview.databaseSize).toBe("N/A");
});

test("a non-finite size leaves overview size absent", async () => {
mockQueryFn = async (sql: string, params?: unknown[]) => {
const normalized = sql.trim().toLowerCase();
if (normalized.includes("pg_database_size") && normalized.includes("database_size_bytes")) {
return Promise.resolve({
rows: [{ database_size: "512 MB", database_size_bytes: Number.POSITIVE_INFINITY }],
fields: [],
rowCount: 1,
});
}
return defaultMockQuery(sql, params);

Check failure on line 2397 in tests/integration/db/postgres-provider.test.ts

View workflow job for this annotation

GitHub Actions / Engine Smoke - Build Payload

Expected 1 arguments, but got 2.
};

provider = new PostgresProvider(makePgConfig());
await provider.connect();
const overview = await provider.getOverview();

expect("databaseSizeBytes" in overview).toBe(false);
expect(overview.databaseSize).toBe("N/A");
});

test("a database that measures zero bytes keeps its measured zero size", async () => {
// The anti-vacuity twin of the tests above: `pg_database_size($1)` answers NULL
// when the aggregate has nothing to measure, and that returned null aggregate is
// a measured zero the provider must keep publishing - never an absence.
mockQueryFn = async (sql: string, params?: unknown[]) => {
const normalized = sql.trim().toLowerCase();
if (normalized.includes("pg_database_size") && normalized.includes("database_size_bytes")) {
return Promise.resolve({
rows: [{ database_size: null, database_size_bytes: null }],
fields: [],
rowCount: 1,
});
}
return defaultMockQuery(sql, params);

Check failure on line 2421 in tests/integration/db/postgres-provider.test.ts

View workflow job for this annotation

GitHub Actions / Engine Smoke - Build Payload

Expected 1 arguments, but got 2.
};

provider = new PostgresProvider(makePgConfig());
await provider.connect();
const overview = await provider.getOverview();

expect("databaseSizeBytes" in overview).toBe(true);
expect(overview.databaseSizeBytes).toBe(0);
expect(overview.databaseSize).toBe("0 B");
});
});

// --------------------------------------------------------------------------
Expand Down
Loading