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 :
|