LCOV - code coverage report
Current view: top level - dashql-app/src/connections/catalog - catalog_query_pg_attribute.ts Coverage Total Hit
Test: DashQL 915828b Lines: 98.7 % 158 156
Test Date: 2026-11-06 19:30:32

            Line data    Source code
       1              : import * as arrow from "apache-arrow";
       2              : import * as dashql from '@ankoh/dashql-core';
       3              : 
       4              : import { QueryExecutor } from '../query_executor.js';
       5              : import { QueryExecutionArgs } from '../query_execution_args.js';
       6              : import { DynamicConnectionDispatch } from '../connection_registry.js';
       7              : import { CATALOG_UPDATE_SCHEMA_SCRIPT, CATALOG_UPDATE_REGISTER_QUERY } from "../connection_state.js";
       8              : import { QueryType } from '../../query/query_execution_state.js';
       9              : import { generateSchemaSQL, generateCatalogScriptHeader, CatalogSource, quoteIdentifier, type ColumnMetadata } from './catalog_sql_generator.js';
      10              : import { generateFunctionScriptHeader } from './catalog_function_sql_generator.js';
      11              : import { queryPgProc, generateCatalogSQLFromPgProc } from './catalog_query_pg_proc.js';
      12              : import { type LoggerLike } from '../../platform/logger/logger.js';
      13              : import { beginCatalogBatch, publishCatalogBatch, type CatalogBatchEntry, type CatalogBatchOptions } from './catalog_batch.js';
      14              : 
      15            1 : const LOG_CTX = "catalog_pg";
      16            1 : export const CATALOG_QUERY_READ_TIMEOUT_MS = 60_000;
      17              : 
      18              : export type PgAttributeColumnsTable = arrow.Table<{
      19              :     table_schema: arrow.Utf8;
      20              :     table_name: arrow.Utf8;
      21              :     column_name: arrow.Utf8;
      22              :     ordinal_position: arrow.Int32;
      23              :     data_type: arrow.Utf8;
      24              :     is_nullable: arrow.Utf8;
      25              :     numeric_precision: arrow.Int32;
      26              :     numeric_scale: arrow.Int32;
      27              : }>;
      28              : 
      29              : /**
      30              :  * Generates SQL DDL from pg_attribute query results.
      31              :  * Builds a hierarchy of schema -> table -> columns and generates CREATE TABLE statements.
      32              :  */
      33            1 : export function generateCatalogSQLFromPgAttribute(result: PgAttributeColumnsTable, databaseName: string): string {
      34              :     // Build hierarchy: schema -> table -> columns
      35            1 :     const schemas = new Map<string, Map<string, ColumnMetadata[]>>();
      36              : 
      37            1 :     for (const batch of result.batches) {
      38            1 :         const colTableSchema = batch.getChild("table_schema")!;
      39            1 :         const colTableName = batch.getChild("table_name")!;
      40            1 :         const colColumnName = batch.getChild("column_name")!;
      41            1 :         const colOrdinalPos = batch.getChild("ordinal_position")!;
      42            1 :         const colDataType = batch.getChild("data_type")!;
      43              : 
      44              :         // Iterate over all rows in the batch
      45            1 :         for (let i = 0; i < batch.numRows; ++i) {
      46            1 :             const tableSchema = colTableSchema.at(i);
      47            1 :             const tableName = colTableName.at(i);
      48            1 :             const columnName = colColumnName.at(i);
      49            1 :             const ordinalPosition = colOrdinalPos.at(i);
      50            1 :             const dataType = colDataType.at(i);
      51              : 
      52            1 :             if (!tableSchema || !columnName || !tableName) {
      53            1 :                 continue;
      54              :             }
      55              : 
      56              :             // Get or create schema entry
      57            1 :             let tableMap = schemas.get(tableSchema);
      58            1 :             if (!tableMap) {
      59            1 :                 tableMap = new Map<string, ColumnMetadata[]>();
      60            1 :                 schemas.set(tableSchema, tableMap);
      61            1 :             }
      62              : 
      63              :             // Get or create table entry
      64            1 :             let columns = tableMap.get(tableName);
      65            1 :             if (!columns) {
      66            1 :                 columns = [];
      67            1 :                 tableMap.set(tableName, columns);
      68            1 :             }
      69              : 
      70              :             // Add column metadata
      71            1 :             columns.push({
      72            1 :                 name: columnName,
      73            1 :                 ordinalPosition: ordinalPosition ?? columns.length,
      74            1 :                 dataType: dataType ?? null,
      75            1 :             });
      76            1 :         }
      77            1 :     }
      78              : 
      79              :     // Generate SQL for all schemas
      80            1 :     const sqlStatements: string[] = [];
      81            1 :     for (const [schemaName, schemaTables] of schemas) {
      82            1 :         const sql = generateSchemaSQL(databaseName, schemaName, schemaTables);
      83            1 :         if (sql.length > 0) {
      84            1 :             sqlStatements.push(sql);
      85              :         }
      86            1 :     }
      87              : 
      88            1 :     return sqlStatements.join('\n\n');
      89            1 : }
      90              : 
      91            1 : export async function queryPgAttribute(
      92            1 :     connectionId: string,
      93            1 :     connectionDispatch: DynamicConnectionDispatch,
      94            1 :     updateId: number,
      95            1 :     databaseName: string,
      96            1 :     schemaNames: string[],
      97            1 :     executor: QueryExecutor,
      98            1 :     queryDatabaseName: string | null = null,
      99            1 :     abortSignal?: AbortSignal,
     100            1 : ): Promise<PgAttributeColumnsTable | null> {
     101            1 :     const pgCatalog = queryDatabaseName == null
     102            1 :         ? 'pg_catalog'
     103            1 :         : `${quoteIdentifier(queryDatabaseName)}.${quoteIdentifier('pg_catalog')}`;
     104            1 :     const schemaFilter = schemaNames.length > 0
     105            1 :         ? `AND n.nspname IN (${schemaNames.map(name => `'${name.replace(/'/g, "''")}'`).join(', ')})`
     106            1 :         : '';
     107              :     const query = `
     108              :         SELECT
     109              :             n.nspname AS table_schema,
     110              :             c.relname AS table_name,
     111              :             a.attname AS column_name,
     112              :             a.attnum AS ordinal_position,
     113              :             t.typname AS data_type,
     114              :             CASE
     115              :                 WHEN a.attnotnull THEN 'NO'
     116              :                 ELSE 'YES'
     117              :             END AS is_nullable,
     118              :             CASE
     119              :                 WHEN a.atttypid = ANY (ARRAY[21, 23, 20]) THEN a.atttypmod - 4
     120              :                 WHEN a.atttypid = 1700 THEN ((a.atttypmod - 4) >> 16) & 65535
     121              :                 ELSE NULL
     122              :             END AS numeric_precision,
     123              :             CASE
     124              :                 WHEN a.atttypid = 1700 THEN (a.atttypmod - 4) & 65535
     125              :                 ELSE NULL
     126              :             END AS numeric_scale
     127            1 :         FROM ${pgCatalog}.pg_class c
     128            1 :         JOIN ${pgCatalog}.pg_namespace n ON n.oid = c.relnamespace
     129            1 :         JOIN ${pgCatalog}.pg_attribute a ON a.attrelid = c.oid
     130            1 :         JOIN ${pgCatalog}.pg_type t ON t.oid = a.atttypid
     131            1 :         LEFT JOIN ${pgCatalog}.pg_attrdef d ON d.adrelid = c.oid AND d.adnum = a.attnum
     132              :         WHERE c.relkind IN ('r', 'v', 'm', 'f', 'p')
     133              :           AND a.attnum > 0
     134              :           AND NOT a.attisdropped
     135            1 :         ${schemaFilter}
     136              :         ORDER BY n.nspname, c.relname, a.attnum
     137              :     `;
     138              : 
     139              :     const args: QueryExecutionArgs = {
     140            1 :         query: query,
     141            1 :         abortSignal,
     142            1 :         readTimeoutMs: CATALOG_QUERY_READ_TIMEOUT_MS,
     143            1 :         throwOnError: true,
     144            1 :         metadata: {
     145            1 :             queryType: QueryType.CATALOG_QUERY_PG_ATTRIBUTE,
     146            1 :             title: "Query Postgres Schema Columns",
     147            1 :             description: null,
     148            1 :             issuer: "Catalog Update",
     149            1 :             userProvided: false
     150            1 :         }
     151              :     };
     152            1 :     const [queryId, queryExecution] = executor(connectionId, args);
     153            1 :     connectionDispatch(connectionId, {
     154            1 :         type: CATALOG_UPDATE_REGISTER_QUERY,
     155            1 :         value: [updateId, queryId]
     156            1 :     });
     157              : 
     158            1 :     const queryResult = await queryExecution as PgAttributeColumnsTable;
     159            1 :     return queryResult;
     160            1 : }
     161              : 
     162            1 : export async function updatePgSchemaScript(
     163            1 :     logger: LoggerLike,
     164            1 :     connectionId: string,
     165            1 :     connectionDispatch: DynamicConnectionDispatch,
     166            1 :     updateId: number,
     167            1 :     databaseName: string,
     168            1 :     schemaNames: string[],
     169            1 :     executor: QueryExecutor,
     170            1 :     catalog: dashql.DashQLCatalog,
     171            1 :     catalogRelationScript: dashql.DashQLScript,
     172            1 :     options: CatalogBatchOptions = {},
     173            1 : ): Promise<void> {
     174            1 :     const validate = beginCatalogBatch(catalog, options);
     175            1 :     const queryResult = await queryPgAttribute(connectionId, connectionDispatch, updateId, databaseName, schemaNames, executor, null, options.abortSignal);
     176            1 :     validate();
     177            1 :     if (queryResult == null || queryResult.numRows === 0) {
     178            1 :         throw new Error('pg_attribute returned no catalog relations');
     179              :     }
     180              : 
     181            1 :     const header = generateCatalogScriptHeader(CatalogSource.PgClass);
     182            1 :     const catalogSQL = generateCatalogSQLFromPgAttribute(queryResult, databaseName);
     183            1 :     if (catalogSQL.length === 0) {
     184            0 :         throw new Error('pg_attribute returned no usable catalog relations');
     185              :     }
     186              : 
     187            1 :     connectionDispatch(connectionId, {
     188            1 :         type: CATALOG_UPDATE_SCHEMA_SCRIPT,
     189            1 :         value: [updateId]
     190            1 :     });
     191              : 
     192            1 :     const [relations] = await publishCatalogBatch(catalogRelationScript.ptr.api, catalog, [{
     193            1 :         script: catalogRelationScript, text: `${header}${catalogSQL}`,
     194            1 :     }], options, validate);
     195            1 :     logger.info("Collected tables from pg_attribute", {
     196            1 :         "updateId": updateId.toString(),
     197            1 :         "tables": relations.tables.toString(),
     198            1 :     }, LOG_CTX);
     199            1 : }
     200              : 
     201            1 : export async function updatePgCatalog(
     202            1 :     logger: LoggerLike,
     203            1 :     connectionId: string,
     204            1 :     connectionDispatch: DynamicConnectionDispatch,
     205            1 :     updateId: number,
     206            1 :     databaseName: string,
     207            1 :     schemaNames: string[],
     208            1 :     executor: QueryExecutor,
     209            1 :     catalog: dashql.DashQLCatalog,
     210            1 :     dql: dashql.DashQL,
     211            1 :     catalogRelationScript: dashql.DashQLScript,
     212            1 :     catalogFunctionScript: dashql.DashQLScript,
     213            1 :     options: CatalogBatchOptions = {},
     214            1 : ): Promise<void> {
     215            1 :     const validate = beginCatalogBatch(catalog, options);
     216            1 :     const [relationsResult, functionsResult] = await Promise.all([
     217            1 :         queryPgAttribute(connectionId, connectionDispatch, updateId, databaseName, schemaNames, executor, null, options.abortSignal),
     218            1 :         queryPgProc(connectionId, connectionDispatch, updateId, executor, options.abortSignal),
     219            1 :     ]);
     220            1 :     validate();
     221            1 :     if (relationsResult == null || relationsResult.numRows === 0) {
     222            0 :         throw new Error('pg_attribute returned no catalog relations');
     223              :     }
     224            1 :     const relationsSQL = generateCatalogSQLFromPgAttribute(relationsResult, databaseName);
     225            1 :     if (!relationsSQL) throw new Error('pg_attribute returned no usable catalog relations');
     226            1 :     const entries: CatalogBatchEntry[] = [{
     227            1 :         script: catalogRelationScript,
     228            1 :         text: `${generateCatalogScriptHeader(CatalogSource.PgClass)}${relationsSQL}`,
     229            1 :     }];
     230            1 :     if (functionsResult == null) throw new Error('pg_proc returned no function metadata result');
     231            1 :     entries.push({
     232            1 :         script: catalogFunctionScript,
     233            1 :         text: `${generateFunctionScriptHeader(CatalogSource.PgClass)}${generateCatalogSQLFromPgProc(functionsResult, databaseName, logger)}`,
     234            1 :     });
     235            1 :     connectionDispatch(connectionId, { type: CATALOG_UPDATE_SCHEMA_SCRIPT, value: [updateId] });
     236            1 :     const counts = await publishCatalogBatch(dql, catalog, entries, options, validate);
     237            1 :     logger.info('Collected PostgreSQL catalog', {
     238            1 :         updateId: updateId.toString(),
     239            1 :         tables: counts[0].tables.toString(),
     240            1 :         functions: counts[1].functions.toString(),
     241            1 :     }, LOG_CTX);
     242            1 : }
        

Generated by: LCOV version 2.0-1