Files
windmill/cli/src/commands/datatable/virtual_catalog.ts
T
Diego Imbert 28c8b5c60f feat: CLI datatable serve / psql (#9267)
* feat(cli): add datatable list and run commands

* feat(cli): render datatable query results as a table

* feat(cli): serve datatables as a postgres-wire endpoint

* feat(cli): add 'datatable psql' to launch psql against the proxy

* feat(cli): route datatable serve by client-supplied database name

* override database list + password option

* fix: support extended queries in datatable serve

* fix: correct cloud size threshold log and parse CLI descriptions with parens/trailing comma

* refactor: extract raw_output envelope encoding into pg_raw_output module

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-05-20 16:56:50 +00:00

76 lines
2.9 KiB
TypeScript

const CURRENT_DATABASE_QUERY_RE = /\b(?:pg_catalog\.)?current_database\s*\(\s*\)/i;
const CURRENT_DATABASE_RE = /\b(?:pg_catalog\.)?current_database\s*\(\s*\)/gi;
const PG_DATABASE_RE = /\b(?:pg_catalog\.)?pg_database\b/i;
const PG_DATABASE_FROM_OR_JOIN_RE =
/(\b(?:from|join)\s+(?:only\s+)?)(?:pg_catalog\.)?pg_database\b/gi;
const PG_DATABASE_COMMA_JOIN_RE = /(,\s*)(?:pg_catalog\.)?pg_database\b/gi;
const PG_SIZE_PRETTY_DATABASE_SIZE_RE =
/\b(?:pg_catalog\.)?pg_size_pretty\s*\(\s*(?:pg_catalog\.)?pg_database_size\s*\((?:[^()]|\([^()]*\))*\)\s*\)/gi;
const PG_DATABASE_SIZE_RE =
/\b(?:pg_catalog\.)?pg_database_size\s*\((?:[^()]|\([^()]*\))*\)/gi;
const HAS_DATABASE_PRIVILEGE_RE =
/\b(?:pg_catalog\.)?has_database_privilege\s*\((?:[^()]|\([^()]*\))*\)/gi;
const PG_DATABASE_COLLATION_VERSION_RE =
/\b(?:pg_catalog\.)?pg_database_collation_actual_version\s*\((?:[^()]|\([^()]*\))*\)/gi;
export function queryTouchesVirtualDatabaseCatalog(query: string): boolean {
return CURRENT_DATABASE_QUERY_RE.test(query) || PG_DATABASE_RE.test(query);
}
export function virtualizeDatabaseCatalogQuery(
query: string,
currentDatabaseName: string,
databaseNames: string[],
): string {
let rewritten = query.replace(
CURRENT_DATABASE_RE,
sqlStringLiteral(currentDatabaseName),
);
if (!PG_DATABASE_RE.test(query)) {
return rewritten;
}
const relation = buildVirtualPgDatabaseRelation(databaseNames);
rewritten = rewritten
.replace(PG_SIZE_PRETTY_DATABASE_SIZE_RE, "NULL::text")
.replace(PG_DATABASE_SIZE_RE, "NULL::bigint")
.replace(HAS_DATABASE_PRIVILEGE_RE, "true")
.replace(PG_DATABASE_COLLATION_VERSION_RE, "NULL::text")
.replace(PG_DATABASE_FROM_OR_JOIN_RE, `$1${relation}`)
.replace(PG_DATABASE_COMMA_JOIN_RE, `$1${relation}`);
return rewritten;
}
function buildVirtualPgDatabaseRelation(databaseNames: string[]): string {
const names = Array.from(new Set(databaseNames));
const arrayLiteral = names.map(sqlStringLiteral).join(", ");
return `(
SELECT
(20000 + src.ordinality)::oid AS oid,
src.datname,
COALESCE((SELECT oid FROM pg_catalog.pg_roles WHERE rolname = CURRENT_USER LIMIT 1), 10::oid) AS datdba,
pg_char_to_encoding(current_setting('server_encoding')) AS encoding,
'c'::"char" AS datlocprovider,
false AS datistemplate,
true AS datallowconn,
-1 AS datconnlimit,
'0'::xid AS datfrozenxid,
'1'::xid AS datminmxid,
COALESCE((SELECT oid FROM pg_catalog.pg_tablespace WHERE spcname = 'pg_default' LIMIT 1), 1663::oid) AS dattablespace,
current_setting('lc_collate') AS datcollate,
current_setting('lc_ctype') AS datctype,
NULL::text AS daticulocale,
NULL::text AS daticurules,
NULL::text AS datcollversion,
NULL::aclitem[] AS datacl
FROM unnest(ARRAY[${arrayLiteral}]::text[]) WITH ORDINALITY AS src(datname, ordinality)
)`;
}
function sqlStringLiteral(value: string): string {
return `'${value.replaceAll("'", "''")}'`;
}