LCOV - code coverage report
Current view: top level - dashql-app/src/connections/catalog - catalog_query_information_schema.ts Coverage Total Hit
Test: DashQL 915828b Lines: 98.2 % 114 112
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, type ColumnMetadata } from './catalog_sql_generator.js';
      10              : import { beginCatalogBatch, publishCatalogBatch, type CatalogBatchOptions } from './catalog_batch.js';
      11              : 
      12              : export type InformationSchemaColumnsTable = arrow.Table<{
      13              :     table_catalog: arrow.Utf8;
      14              :     table_schema: arrow.Utf8;
      15              :     table_name: arrow.Utf8;
      16              :     column_name: arrow.Utf8;
      17              :     /// Trino types the ordinal position as bigint, its values are then BigInt
      18              :     ordinal_position: arrow.Int32 | arrow.Int64;
      19              :     is_nullable: arrow.Utf8;  // 'YES' or 'NO'
      20              :     data_type: arrow.Utf8;
      21              : }>;
      22              : 
      23              : /**
      24              :  * Generates SQL DDL from information_schema query results.
      25              :  * Builds a hierarchy of catalog -> schema -> table -> columns and generates CREATE TABLE statements.
      26              :  */
      27            1 : function generateCatalogSQLFromInformationSchema(result: InformationSchemaColumnsTable): string {
      28              :     // Build hierarchy: catalog -> schema -> table -> columns
      29            1 :     const catalogs = new Map<string, Map<string, Map<string, ColumnMetadata[]>>>();
      30              : 
      31            1 :     for (const batch of result.batches) {
      32            1 :         const colTableCatalog = batch.getChild("table_catalog")!;
      33            1 :         const colTableSchema = batch.getChild("table_schema")!;
      34            1 :         const colTableName = batch.getChild("table_name")!;
      35            1 :         const colColumnName = batch.getChild("column_name")!;
      36            1 :         const colOrdinalPos = batch.getChild("ordinal_position")!;
      37            1 :         const colDataType = batch.getChild("data_type")!;
      38              : 
      39              :         // Iterate over all rows in the batch
      40            1 :         for (let i = 0; i < batch.numRows; ++i) {
      41            1 :             const tableCatalog = colTableCatalog.at(i);
      42            1 :             const tableSchema = colTableSchema.at(i);
      43            1 :             const tableName = colTableName.at(i);
      44            1 :             const columnName = colColumnName.at(i);
      45            1 :             const ordinalPosition = colOrdinalPos.at(i);
      46            1 :             const dataType = colDataType.at(i);
      47              : 
      48            1 :             if (!tableCatalog || !tableSchema || !columnName || !tableName) {
      49            0 :                 continue;
      50              :             }
      51              : 
      52              :             // Get or create catalog entry
      53            1 :             let schemaMap = catalogs.get(tableCatalog);
      54            1 :             if (!schemaMap) {
      55            1 :                 schemaMap = new Map<string, Map<string, ColumnMetadata[]>>();
      56            1 :                 catalogs.set(tableCatalog, schemaMap);
      57            1 :             }
      58              : 
      59              :             // Get or create schema entry
      60            1 :             let tableMap = schemaMap.get(tableSchema);
      61            1 :             if (!tableMap) {
      62            1 :                 tableMap = new Map<string, ColumnMetadata[]>();
      63            1 :                 schemaMap.set(tableSchema, tableMap);
      64            1 :             }
      65              : 
      66              :             // Get or create table entry
      67            1 :             let columns = tableMap.get(tableName);
      68            1 :             if (!columns) {
      69            1 :                 columns = [];
      70            1 :                 tableMap.set(tableName, columns);
      71            1 :             }
      72              : 
      73              :             // Add column metadata
      74            1 :             columns.push({
      75            1 :                 name: columnName,
      76            1 :                 ordinalPosition: ordinalPosition != null ? Number(ordinalPosition) : columns.length,
      77            1 :                 dataType: dataType ?? null,
      78            1 :             });
      79            1 :         }
      80            1 :     }
      81              : 
      82              :     // Generate SQL for all schemas
      83            1 :     const sqlStatements: string[] = [];
      84            1 :     for (const [catalogName, catalogSchemas] of catalogs) {
      85            1 :         for (const [schemaName, schemaTables] of catalogSchemas) {
      86            1 :             const sql = generateSchemaSQL(catalogName, schemaName, schemaTables);
      87            1 :             if (sql.length > 0) {
      88            1 :                 sqlStatements.push(sql);
      89              :             }
      90            1 :         }
      91              :     }
      92              : 
      93            1 :     return sqlStatements.join('\n\n');
      94            1 : }
      95              : 
      96              : /// A catalog and the schemas of it whose columns are read, no schemas for all of them
      97              : export interface InformationSchemaScope {
      98              :     /// The catalog name
      99              :     catalog: string;
     100              :     /// The schema names, empty for all schemas of the catalog
     101              :     schemas: string[];
     102              : }
     103              : 
     104              : /// Quote an identifier
     105            1 : function quoteIdentifier(name: string): string {
     106            1 :     return `"${name.replace(/"/g, '""')}"`;
     107            1 : }
     108              : 
     109              : /// Quote a string literal
     110            1 : function quoteLiteral(value: string): string {
     111            1 :     return `'${value.replace(/'/g, "''")}'`;
     112            1 : }
     113              : 
     114              : /// The query of the columns of the scopes.
     115              : /// Every catalog has an information_schema of its own, the query reads the one of every catalog.
     116            1 : export function informationSchemaColumnsQuery(scopes: InformationSchemaScope[]): string {
     117            1 :     const select = (source: string, schemas: string[]) => `SELECT
     118              :     table_catalog,
     119              :     table_schema,
     120              :     table_name,
     121              :     column_name,
     122              :     ordinal_position,
     123              :     is_nullable,
     124              :     data_type
     125            1 : FROM ${source}${schemas.length > 0 ? `
     126            1 : WHERE table_schema IN (${schemas.map(quoteLiteral).join(', ')})` : ''}`;
     127            1 :     return scopes
     128            1 :         .map(scope => select(`${quoteIdentifier(scope.catalog)}.information_schema.columns`, scope.schemas))
     129            1 :         .join('\nUNION ALL\n');
     130            1 : }
     131              : 
     132            1 : export async function queryInformationSchema(connectionId: string, connectionDispatch: DynamicConnectionDispatch, updateId: number, scopes: InformationSchemaScope[], executor: QueryExecutor, abortSignal?: AbortSignal): Promise<InformationSchemaColumnsTable | null> {
     133            1 :     const query = informationSchemaColumnsQuery(scopes);
     134              : 
     135              :     const args: QueryExecutionArgs = {
     136            1 :         query: query,
     137            1 :         abortSignal,
     138            1 :         throwOnError: true,
     139            1 :         metadata: {
     140            1 :             queryType: QueryType.CATALOG_QUERY_INFORMATION_SCHEMA,
     141            1 :             title: "Information Schema",
     142            1 :             description: null,
     143            1 :             issuer: "Catalog Update",
     144            1 :             userProvided: false
     145            1 :         }
     146              :     };
     147            1 :     const [queryId, queryExecution] = executor(connectionId, args);
     148            1 :     connectionDispatch(connectionId, {
     149            1 :         type: CATALOG_UPDATE_REGISTER_QUERY,
     150            1 :         value: [updateId, queryId]
     151            1 :     });
     152              : 
     153            1 :     const queryResult = await queryExecution as InformationSchemaColumnsTable;
     154            1 :     return queryResult;
     155            1 : }
     156              : 
     157            1 : export async function updateInformationSchemaCatalog(
     158            1 :     connectionId: string,
     159            1 :     connectionDispatch: DynamicConnectionDispatch,
     160            1 :     updateId: number,
     161            1 :     scopes: InformationSchemaScope[],
     162            1 :     executor: QueryExecutor,
     163            1 :     catalog: dashql.DashQLCatalog,
     164            1 :     dql: dashql.DashQL,
     165            1 :     catalogRelationScript: dashql.DashQLScript,
     166            1 :     _catalogFunctionScript: dashql.DashQLScript,
     167            1 :     options: CatalogBatchOptions = {},
     168            1 : ): Promise<void> {
     169              :     // Every information_schema belongs to a catalog, without one there is nothing to read
     170            1 :     if (scopes.length === 0) {
     171            1 :         throw new Error('no catalogs to read the information_schema of');
     172              :     }
     173            1 :     const validate = beginCatalogBatch(catalog, options);
     174              :     // Query the information schema. If the query errors it throws and propagates
     175              :     // to the caller so we never overwrite the existing catalog script with partial data.
     176            1 :     const queryResult = await queryInformationSchema(connectionId, connectionDispatch, updateId, scopes, executor, options.abortSignal);
     177            1 :     validate();
     178            1 :     if (queryResult == null || queryResult.numRows === 0) {
     179            1 :         throw new Error('information_schema returned no catalog relations');
     180              :     }
     181              : 
     182              :     // Generate SQL from query results before touching the script so an empty
     183              :     // result (after row-level filtering) doesn't clobber a restored catalog.
     184            1 :     const header = generateCatalogScriptHeader(CatalogSource.InformationSchema);
     185            1 :     const catalogSQL = generateCatalogSQLFromInformationSchema(queryResult);
     186            1 :     if (catalogSQL.length === 0) {
     187            0 :         throw new Error('information_schema returned no usable catalog relations');
     188              :     }
     189              : 
     190              :     // Mark loading started
     191            1 :     connectionDispatch(connectionId, {
     192            1 :         type: CATALOG_UPDATE_SCHEMA_SCRIPT,
     193            1 :         value: [updateId]
     194            1 :     });
     195              : 
     196            1 :     await publishCatalogBatch(dql, catalog, [{
     197            1 :         script: catalogRelationScript, text: `${header}${catalogSQL}`,
     198            1 :     }], options, validate);
     199            1 : }
        

Generated by: LCOV version 2.0-1