LCOV - code coverage report
Current view: top level - dashql-app/src/connections/catalog - catalog_sql_generator.ts Coverage Total Hit
Test: DashQL d805a4b Lines: 99.1 % 110 109
Test Date: 2026-11-09 20:22:13

            Line data    Source code
       1              : import * as flatbuffers from 'flatbuffers';
       2              : import * as dashql from '@ankoh/dashql-core';
       3              : 
       4            1 : export const DEFAULT_DATABASE_NAME = 'hyper';
       5              : 
       6              : /// Column metadata for SQL generation
       7              : export interface ColumnMetadata {
       8              :     name: string;
       9              :     ordinalPosition: number;
      10              :     dataType?: string | null;
      11              : }
      12              : 
      13            1 : export function quoteIdentifier(identifier: string): string {
      14            1 :     const escaped = identifier.replace(/"/g, '""');
      15            1 :     return `"${escaped}"`;
      16            1 : }
      17              : 
      18              : /// The type of a column of a generated CREATE TABLE.
      19              : /// A backend can name any type, such as `USER-DEFINED` or `numeric(10,2)`, and some of those aren't SQL types.
      20              : /// A quoted name always parses, and the catalog keeps its text.
      21            1 : export function quoteDataType(sqlType: string | null | undefined): string {
      22            1 :     return quoteIdentifier(sqlType?.trim() || 'unknown');
      23            1 : }
      24              : 
      25              : /// Quote a string literal
      26            1 : export function quoteLiteral(value: string): string {
      27            1 :     return `'${value.replace(/'/g, "''")}'`;
      28            1 : }
      29              : 
      30              : /// A relation of a connection
      31              : export interface RelationName {
      32              :     databaseName: string;
      33              :     /// The schema, empty if a statement didn't name one. The connection decides which schema that is, it can be a
      34              :     /// temporary schema or one of the search path, so a name without a schema matches every schema.
      35              :     schemaName: string;
      36              :     /// The table, empty for everything in the schema
      37              :     tableName: string;
      38              : }
      39              : 
      40              : /// The condition of a metadata query that selects relations
      41            1 : export function relationFilter(schemaColumn: string, tableColumn: string, relations: RelationName[]): string {
      42            1 :     const relation = (r: RelationName) => {
      43              :         const parts = [
      44            1 :             ...(r.schemaName ? [`${schemaColumn} = ${quoteLiteral(r.schemaName)}`] : []),
      45            1 :             ...(r.tableName ? [`${tableColumn} = ${quoteLiteral(r.tableName)}`] : []),
      46              :         ];
      47            1 :         return `(${parts.join(' AND ')})`;
      48            1 :     };
      49            1 :     return `(${relations.map(relation).join(' OR ')})`;
      50            1 : }
      51              : 
      52              : /// Generates a qualified table name (database.schema.table).
      53            1 : export function generateQualifiedTableName(
      54            1 :     databaseName: string | null | undefined,
      55            1 :     schemaName: string,
      56            1 :     tableName: string
      57            1 : ): string {
      58            1 :     const db = databaseName || DEFAULT_DATABASE_NAME;
      59            1 :     return `${quoteIdentifier(db)}.${quoteIdentifier(schemaName)}.${quoteIdentifier(tableName)}`;
      60            1 : }
      61              : 
      62              : /// Generates a CREATE TABLE SQL statement for a single table.
      63            1 : export function generateCreateTableSQL(
      64            1 :     databaseName: string | null | undefined,
      65            1 :     schemaName: string | null,
      66            1 :     tableName: string,
      67            1 :     columns: ColumnMetadata[]
      68            1 : ): string {
      69            1 :     const qualifiedName = schemaName == null
      70            0 :         ? quoteIdentifier(tableName)
      71            1 :         : generateQualifiedTableName(databaseName, schemaName, tableName);
      72              : 
      73              :     // Sort columns by ordinal position
      74            1 :     const sortedColumns = [...columns].sort((a, b) => a.ordinalPosition - b.ordinalPosition);
      75              : 
      76              :     // Generate column definitions
      77            1 :     const columnDefs = sortedColumns.map(col => {
      78            1 :         const colName = quoteIdentifier(col.name);
      79            1 :         const colType = quoteDataType(col.dataType);
      80            1 :         return `    ${colName} ${colType}`;
      81            1 :     }).join(',\n');
      82              : 
      83            1 :     return `CREATE TABLE ${qualifiedName} (\n${columnDefs}\n);`;
      84            1 : }
      85              : 
      86              : /// A table that a connection reported
      87              : export interface CatalogTable extends RelationName {
      88              :     columns: ColumnMetadata[];
      89              : }
      90              : 
      91              : /// The tables of a schema, sorted by name
      92            1 : export function catalogTablesOfSchema(databaseName: string, schemaName: string, tables: Map<string, ColumnMetadata[]>): CatalogTable[] {
      93            1 :     return Array.from(tables.keys()).sort().map(tableName => ({ databaseName, schemaName, tableName, columns: tables.get(tableName)! }));
      94            1 : }
      95              : 
      96              : /// The name of a relation for the core
      97            1 : export function packRelationName(relation: RelationName): dashql.buffers.analyzer.QualifiedTableNameT {
      98            1 :     return new dashql.buffers.analyzer.QualifiedTableNameT(0, relation.databaseName, relation.schemaName, relation.tableName);
      99            1 : }
     100              : 
     101              : /// A table of a catalog descriptor, its columns in the order of the connection
     102            1 : export function packCatalogTable(table: CatalogTable): dashql.buffers.analyzer.TableT {
     103            1 :     const packed = new dashql.buffers.analyzer.TableT();
     104            1 :     packed.tableName = packRelationName(table);
     105            1 :     packed.tableColumns = [...table.columns]
     106            1 :         .sort((a, b) => a.ordinalPosition - b.ordinalPosition)
     107            1 :         .map(column => new dashql.buffers.analyzer.TableColumnT(0, column.name, null, column.dataType ?? null));
     108            1 :     return packed;
     109            1 : }
     110              : 
     111              : /// A catalog descriptor of tables
     112            1 : export function packCatalogDescriptor(tables: CatalogTable[]): Uint8Array {
     113            1 :     const descriptor = new dashql.buffers.catalog.CatalogDescriptorT(tables.map(packCatalogTable), []);
     114            1 :     const builder = new flatbuffers.Builder();
     115            1 :     builder.finish(descriptor.pack(builder));
     116            1 :     return builder.asUint8Array();
     117            1 : }
     118              : 
     119              : /// The tables of a catalog descriptor
     120            1 : export function catalogTablesOfDescriptor(descriptor: dashql.buffers.catalog.CatalogDescriptorT | null): CatalogTable[] {
     121            1 :     return (descriptor?.tables ?? []).map(table => ({
     122            1 :         databaseName: String(table.tableName?.databaseName ?? ''),
     123            1 :         schemaName: String(table.tableName?.schemaName ?? ''),
     124            1 :         tableName: String(table.tableName?.tableName ?? ''),
     125            1 :         columns: table.tableColumns.map((column, ordinalPosition) => ({
     126            1 :             name: String(column.columnName ?? ''),
     127            1 :             ordinalPosition,
     128            1 :             dataType: column.columnType == null ? null : String(column.columnType),
     129            1 :         })),
     130            1 :     }));
     131            1 : }
     132              : 
     133              : /// CREATE TABLE statements of tables, the catalog of a connection on disk
     134            1 : export function generateCatalogTablesSQL(tables: CatalogTable[]): string {
     135            1 :     return tables.map(table => generateCreateTableSQL(table.databaseName, table.schemaName, table.tableName, table.columns)).join('\n\n');
     136            1 : }
     137              : 
     138              : /// The file with the relations of a connection. The catalog holds descriptors, this text is only written and read.
     139              : /// Temporary relations don't outlive the database session, a restart must not bring them back.
     140            1 : export function generateRelationsFile(descriptor: dashql.buffers.catalog.CatalogDescriptorT | null): string {
     141            1 :     const tables = catalogTablesOfDescriptor(descriptor).filter(table => !table.schemaName.toLowerCase().startsWith('pg_temp'));
     142            1 :     return `-- DashQL Connection Relations.
     143              : -- This file is auto-generated and can only be updated through a catalog refresh.
     144              : 
     145            1 : ${generateCatalogTablesSQL(tables)}
     146              : `;
     147            1 : }
     148              : 
     149              : /// What the file with the relations of a connection doesn't say
     150              : export interface CatalogStateFile {
     151              :     /// The relations of executed statements that the connection didn't answer about yet
     152              :     pending: RelationName[];
     153              :     /// The relations that the connection reported as gone, a declaration in a script doesn't provide them
     154              :     gone: RelationName[];
     155              : }
     156              : 
     157              : /// A relation name of the core
     158            1 : export function readRelationName(name: dashql.buffers.analyzer.QualifiedTableNameT, defaultDatabase = ''): RelationName {
     159            1 :     return {
     160            1 :         databaseName: String(name.databaseName ?? '') || defaultDatabase,
     161            1 :         schemaName: String(name.schemaName ?? ''),
     162            1 :         tableName: String(name.tableName ?? ''),
     163            1 :     };
     164            1 : }
     165              : 
     166              : /// Read the file with the catalog state, a file that can't be read holds nothing
     167            1 : export function parseCatalogStateFile(text: string | null): CatalogStateFile {
     168            1 :     const isRelation = (r: any): r is RelationName => typeof r?.databaseName === 'string' && typeof r?.schemaName === 'string' && typeof r?.tableName === 'string';
     169            1 :     try {
     170            1 :         const parsed = JSON.parse(text ?? '');
     171            1 :         return {
     172            1 :             pending: Array.isArray(parsed?.pending) ? parsed.pending.filter(isRelation) : [],
     173            1 :             gone: Array.isArray(parsed?.gone) ? parsed.gone.filter(isRelation) : [],
     174            1 :         };
     175            1 :     } catch {
     176            1 :         return { pending: [], gone: [] };
     177            1 :     }
     178            1 : }
     179              : 
     180            1 : export enum CatalogSource {
     181            1 :     InformationSchema = 0,
     182            1 :     PgClass = 1,
     183            1 :     SalesforceMetadataApi = 2,
     184            1 :     Hyper = 3,
     185            1 :     Unknown = 4,
     186            1 : }
     187              : 
        

Generated by: LCOV version 2.0-1