fix(databases): properly display row count pills for un-analyzed empty postgres tables
This commit is contained in:
@@ -43,10 +43,12 @@ const SSH_TIMEOUT_MS = 8_000;
|
||||
/** Find the running container for a Coolify database uuid. */
|
||||
async function resolveDbContainer(dbUuid: string): Promise<string> {
|
||||
const containers = await listContainersForApp(dbUuid);
|
||||
const running = containers.find(c => /up /i.test(c.status));
|
||||
const running = containers.find((c) => /up /i.test(c.status));
|
||||
const target = running ?? containers[0];
|
||||
if (!target) {
|
||||
throw new Error(`No container found for database ${dbUuid}. Database may be stopped.`);
|
||||
throw new Error(
|
||||
`No container found for database ${dbUuid}. Database may be stopped.`,
|
||||
);
|
||||
}
|
||||
return target.name;
|
||||
}
|
||||
@@ -85,7 +87,9 @@ async function pgListTables(container: string): Promise<IntrospectedTable[]> {
|
||||
AND n.nspname NOT LIKE 'pg_%'
|
||||
ORDER BY n.nspname, c.relname
|
||||
LIMIT ${MAX_TABLES + 1};
|
||||
`.replace(/\s+/g, " ").trim();
|
||||
`
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
|
||||
const all: IntrospectedTable[] = [];
|
||||
const multi = dbs.length > 1;
|
||||
@@ -93,7 +97,9 @@ async function pgListTables(container: string): Promise<IntrospectedTable[]> {
|
||||
if (all.length >= MAX_TABLES) break;
|
||||
const cmd =
|
||||
`docker exec ${sq(container)} bash -c ` +
|
||||
sq(`psql -U "$POSTGRES_USER" -d ${sqInner(db)} -tAF '|' -c ${sqInner(tablesSql)}`);
|
||||
sq(
|
||||
`psql -U "$POSTGRES_USER" -d ${sqInner(db)} -tAF '|' -c ${sqInner(tablesSql)}`,
|
||||
);
|
||||
const res = await runOnCoolifyHost(cmd, { timeoutMs: SSH_TIMEOUT_MS });
|
||||
if (res.code !== 0) {
|
||||
// Skip dbs the role can't connect to (e.g. owned by another user).
|
||||
@@ -104,11 +110,17 @@ async function pgListTables(container: string): Promise<IntrospectedTable[]> {
|
||||
if (!trimmed) continue;
|
||||
const [schema, name, rowsStr] = trimmed.split("|");
|
||||
if (!schema || !name) continue;
|
||||
const approxRows = Number(rowsStr);
|
||||
let approxRows = Number(rowsStr);
|
||||
// Postgres 14+ sets reltuples to -1 if the table has never been analyzed
|
||||
if (approxRows === -1) approxRows = 0;
|
||||
|
||||
all.push({
|
||||
schema: multi ? `${db}.${schema}` : schema,
|
||||
name,
|
||||
approxRows: Number.isFinite(approxRows) && approxRows >= 0 ? approxRows : undefined,
|
||||
approxRows:
|
||||
Number.isFinite(approxRows) && approxRows >= 0
|
||||
? approxRows
|
||||
: undefined,
|
||||
});
|
||||
if (all.length >= MAX_TABLES) break;
|
||||
}
|
||||
@@ -127,12 +139,14 @@ async function pgListDatabases(container: string): Promise<string[]> {
|
||||
sq(`psql -U "$POSTGRES_USER" -d postgres -tA -c ${sqInner(sql)}`);
|
||||
const res = await runOnCoolifyHost(cmd, { timeoutMs: SSH_TIMEOUT_MS });
|
||||
if (res.code !== 0) {
|
||||
throw new Error(`psql -l exited ${res.code}: ${res.stderr.trim() || "(no stderr)"}`);
|
||||
throw new Error(
|
||||
`psql -l exited ${res.code}: ${res.stderr.trim() || "(no stderr)"}`,
|
||||
);
|
||||
}
|
||||
return res.stdout
|
||||
.split("\n")
|
||||
.map(l => l.trim())
|
||||
.filter(name => name && /^[A-Za-z0-9_]+$/.test(name));
|
||||
.map((l) => l.trim())
|
||||
.filter((name) => name && /^[A-Za-z0-9_]+$/.test(name));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -173,14 +187,21 @@ async function pgPreviewTable(
|
||||
|
||||
const cmd =
|
||||
`docker exec ${sq(container)} bash -c ` +
|
||||
sq(`psql -U "$POSTGRES_USER" -d ${sqInner(database)} -tA -c ${sqInner(sql)}`);
|
||||
sq(
|
||||
`psql -U "$POSTGRES_USER" -d ${sqInner(database)} -tA -c ${sqInner(sql)}`,
|
||||
);
|
||||
|
||||
const res = await runOnCoolifyHost(cmd, { timeoutMs: SSH_TIMEOUT_MS });
|
||||
if (res.code !== 0) {
|
||||
throw new Error(`psql exited ${res.code}: ${res.stderr.trim() || "(no stderr)"}`);
|
||||
throw new Error(
|
||||
`psql exited ${res.code}: ${res.stderr.trim() || "(no stderr)"}`,
|
||||
);
|
||||
}
|
||||
|
||||
const lines = res.stdout.split("\n").map(l => l.trim()).filter(Boolean);
|
||||
const lines = res.stdout
|
||||
.split("\n")
|
||||
.map((l) => l.trim())
|
||||
.filter(Boolean);
|
||||
const truncated = lines.length > MAX_ROWS;
|
||||
const rowJson = lines.slice(0, MAX_ROWS);
|
||||
|
||||
@@ -189,7 +210,7 @@ async function pgPreviewTable(
|
||||
for (const line of rowJson) {
|
||||
try {
|
||||
const obj = JSON.parse(line) as Record<string, unknown>;
|
||||
Object.keys(obj).forEach(k => columnSet.add(k));
|
||||
Object.keys(obj).forEach((k) => columnSet.add(k));
|
||||
parsed.push(obj);
|
||||
} catch {
|
||||
// Skip non-JSON lines (shouldn't happen with -tA, but be safe).
|
||||
@@ -199,10 +220,10 @@ async function pgPreviewTable(
|
||||
// Stable column order: order they appeared in the first row, then
|
||||
// any extras at the end.
|
||||
const firstRowKeys = parsed[0] ? Object.keys(parsed[0]) : [];
|
||||
const extras = [...columnSet].filter(k => !firstRowKeys.includes(k));
|
||||
const extras = [...columnSet].filter((k) => !firstRowKeys.includes(k));
|
||||
const columns = [...firstRowKeys, ...extras];
|
||||
|
||||
const rows = parsed.map(row => {
|
||||
const rows = parsed.map((row) => {
|
||||
const out: Record<string, string> = {};
|
||||
for (const col of columns) {
|
||||
out[col] = formatCell(row[col]);
|
||||
@@ -228,7 +249,9 @@ function formatCell(v: unknown): string {
|
||||
}
|
||||
if (typeof v === "number" || typeof v === "boolean") return String(v);
|
||||
const json = JSON.stringify(v);
|
||||
return json.length > MAX_CELL_CHARS ? json.slice(0, MAX_CELL_CHARS) + "…" : json;
|
||||
return json.length > MAX_CELL_CHARS
|
||||
? json.slice(0, MAX_CELL_CHARS) + "…"
|
||||
: json;
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user