LCOV - code coverage report
Current view: top level - dashql-app/src/agent - agent_catalog.ts Coverage Total Hit
Test: DashQL d805a4b Lines: 100.0 % 73 73
Test Date: 2026-11-09 20:22:13

            Line data    Source code
       1              : import * as core from '@ankoh/dashql-core';
       2              : 
       3              : import type { Logger } from '../platform/logger/logger.js';
       4              : import type { ConnectionState } from '../connections/connection_state.js';
       5              : import type { SessionScripts } from '../session/scripts/session_scripts.js';
       6              : import { catalogTablesOfDescriptor, generateCatalogTablesSQL, quoteIdentifier } from '../connections/catalog/catalog_sql_generator.js';
       7              : import { isSystemSchema } from '../connections/catalog/system_schemas.js';
       8              : 
       9            1 : const LOG_CTX = 'agent_catalog';
      10              : 
      11              : /// The schema context is cut off after this many characters to keep the output of `dashql agent catalog` readable
      12            1 : const SCHEMA_CONTEXT_BUDGET = 100_000;
      13              : 
      14              : /// The relations of a connection, split by owner
      15              : interface ConnectionRelations {
      16              :     /// The statements that define relations of user schemas
      17              :     statements: string[];
      18              :     /// The number of skipped relations of system schemas
      19              :     systemRelations: number;
      20              : }
      21              : 
      22              : /// The statements of the relations of user schemas of a connection
      23            1 : function describeConnectionRelations(descriptor: core.buffers.catalog.CatalogDescriptorT | null): ConnectionRelations {
      24            1 :     const relations: ConnectionRelations = { statements: [], systemRelations: 0 };
      25            1 :     for (const table of catalogTablesOfDescriptor(descriptor)) {
      26            1 :         if (isSystemSchema(table.schemaName)) {
      27            1 :             relations.systemRelations += 1;
      28            1 :             continue;
      29            1 :         }
      30            1 :         relations.statements.push(generateCatalogTablesSQL([table]));
      31            1 :     }
      32            1 :     return relations;
      33            1 : }
      34              : 
      35              : /// Render the tables that the session scripts on a connection define as CREATE TABLE statements without types.
      36              : /// Queries only see the tables of scripts on their own connection.
      37            1 : function describeSessionTables(sessionScripts: SessionScripts, connectionId: string): string[] {
      38            1 :     const statements: string[] = [];
      39            1 :     for (const script of Object.values(sessionScripts.scripts)) {
      40            1 :         const catalog = script.connection.catalog;
      41            1 :         if (script.connection.connectionId !== connectionId || catalog == null) continue;
      42            1 :         let entries: core.FlatBufferPtr<core.buffers.catalog.CatalogEntries>;
      43            1 :         try {
      44            1 :             entries = catalog.describeEntriesOf(script.scriptSession.getCatalogEntryId());
      45            1 :         } catch {
      46              :             // The script is not loaded into the catalog
      47            1 :             continue;
      48            1 :         }
      49            1 :         const reader = entries.read();
      50            1 :         for (let i = 0; i < reader.entriesLength(); ++i) {
      51            1 :             const entry = reader.entries(i)!;
      52            1 :             for (let j = 0; j < entry.schemasLength(); ++j) {
      53            1 :                 const schema = entry.schemas(j)!;
      54            1 :                 for (let k = 0; k < schema.tablesLength(); ++k) {
      55            1 :                     const table = schema.tables(k)!;
      56            1 :                     const columns: string[] = [];
      57            1 :                     for (let l = 0; l < table.columnsLength(); ++l) {
      58            1 :                         columns.push(quoteIdentifier(table.columns(l)!.columnName() ?? ''));
      59              :                     }
      60            1 :                     statements.push(`CREATE TABLE ${quoteIdentifier(table.tableName() ?? '')} (${columns.join(', ')});`);
      61            1 :                 }
      62            1 :             }
      63            1 :         }
      64            1 :     }
      65            1 :     return statements;
      66            1 : }
      67              : 
      68              : /// Build the schema that the model may query on a connection: the user relations of the connection and the tables of
      69              : /// the session scripts on it.
      70              : /// The schema is cut off at a statement boundary once it exceeds the budget.
      71            1 : export function buildSchemaContext(connectionId: string, conn: ConnectionState | null, sessionScripts: SessionScripts, logger: Logger): string {
      72            1 :     const relations = describeConnectionRelations(conn?.catalog.readDescriptor(conn.catalogRelationEntryId) ?? null);
      73            1 :     const sessionTables = describeSessionTables(sessionScripts, connectionId);
      74              : 
      75            1 :     const statements = [...relations.statements];
      76            1 :     if (sessionTables.length > 0) {
      77            1 :         statements.push('-- Tables defined by the session, column types are unknown', ...sessionTables);
      78              :     }
      79            1 :     let schema = '';
      80            1 :     let included = 0;
      81            1 :     for (const statement of statements) {
      82            1 :         if (schema.length + statement.length + 1 > SCHEMA_CONTEXT_BUDGET) {
      83            1 :             break;
      84              :         }
      85            1 :         schema += schema.length > 0 ? `\n${statement}` : statement;
      86            1 :         ++included;
      87            1 :     }
      88            1 :     const truncated = included < statements.length;
      89            1 :     if (truncated) {
      90            1 :         schema += '\n-- The schema is truncated';
      91            1 :         logger.warn('Truncated the schema for the agent', {
      92            1 :             budget: String(SCHEMA_CONTEXT_BUDGET),
      93            1 :             included: String(included),
      94            1 :             total: String(statements.length),
      95            1 :         }, LOG_CTX);
      96            1 :     }
      97            1 :     logger.debug('Built the schema for the agent', {
      98            1 :         connectionRelations: String(relations.statements.length),
      99            1 :         skippedSystemRelations: String(relations.systemRelations),
     100            1 :         sessionTables: String(sessionTables.length),
     101            1 :         characters: String(schema.length),
     102            1 :         truncated: String(truncated),
     103            1 :     }, LOG_CTX);
     104            1 :     return schema;
     105            1 : }
        

Generated by: LCOV version 2.0-1