UNPKG

@dbml/connector

Version:

This package was created to fetch the schema JSON from many kind of databases.

393 lines (337 loc) 11.1 kB
/* eslint-disable camelcase */ // // Description: // Snowflake info schema lacks reference direction for relationships. // Constraints and uniqueness cannot be auto-generated from Snowflake info schema. // Snowflake does not support ENUMs. // Snowflake does not support indexes for standard tables. // import snowflake from 'snowflake-sdk'; import type { Connection, LogLevel, ConnectionOptions } from 'snowflake-sdk'; import { parseSchema } from '../utils/parseSchema'; import { DatabaseSchema, DefaultInfo, DefaultType, Field, FieldsDictionary, Index, IndexesDictionary, Table, TableConstraintsDictionary, } from './types'; type ConstraintRow = { schemaName: string; tableName: string; constraintName: string; columnNames: string[]; type: string; primary?: boolean; unique?: boolean; }; type GeneratedIndexes = { indexes: IndexesDictionary; tableConstraints: TableConstraintsDictionary; }; const parseConnectionString = (connectionString: string): Record<string, string> => { const params: Record<string, string> = {}; const regex = /([^;=]+)=([^;]*)/g; let match; while ((match = regex.exec(connectionString)) !== null) { params[match[1].trim()] = match[2].trim(); } return params; } const connect = async (connection: Connection): Promise<void> => { return new Promise((resolve, reject) => { console.log('Attempting to connect...'); connection.connect((err) => { if (err) { reject(err); } else { resolve(); } }); }); } const executeQuery = (connection: Connection, sqlText: string): Promise<any[]> => { return new Promise((resolve, reject) => { console.log('Executing query...'); connection.execute({ sqlText: sqlText, complete: (err, stmt, rows) => { if (err) { reject(err); } else { console.log('Query executed successfully.'); if (rows) { resolve(rows); } } } }); }); } const isLogLevel = (value: string): value is LogLevel => { return ['ERROR', 'WARN', 'INFO', 'DEBUG', 'TRACE'].includes(value as LogLevel) } const connectToSnowflake = async (config: Record<string, string>): Promise<Connection> => { const logLevel = isLogLevel(config.LOG_LEVEL) ? config.LOG_LEVEL : 'INFO'; const isDebugMode = config.IS_DEBUG_MODE === 'true'; snowflake.configure({ logLevel, additionalLogToConsole: isDebugMode, }); // https://docs.snowflake.com/en/developer-guide/node-js/nodejs-driver-options#authentication-options let authConfig = {}; switch (config.AUTHENTICATOR) { case 'SNOWFLAKE_JWT': // https://docs.snowflake.com/en/developer-guide/node-js/nodejs-driver-authenticate#label-nodejs-key-pair-authentication // Support private key file authConfig = { privateKeyPath: config.PRIVATE_KEY_PATH, privateKeyPass: config.PASSPHRASE, }; break; case 'SNOWFLAKE': case undefined: authConfig = { password: config.PWD, }; break; default: throw new Error('Unsupported authenticator'); } // "SERVER=myaccount.snowflakecomputing.com;UID=myusername;PWD=mypassword;DATABASE=mydatabase;WAREHOUSE=mywarehouse;ROLE=myrole"; const connectionOptions: ConnectionOptions = { authenticator: config.AUTHENTICATOR, account: config.SERVER, username: config.UID, database: config.DATABASE, warehouse: config.WAREHOUSE, sfRetryMaxLoginRetries: 3, timeout: 10000, ...authConfig, }; const connection = snowflake.createConnection(connectionOptions); // Connect to Snowflake await connect(connection); // Execute the query await executeQuery(connection, 'SELECT CURRENT_VERSION();'); return connection; }; const convertQueryBoolean = (val: string | null) => val === 'YES'; const getFieldType = (data_type: string, character_maximum_length: number, numeric_precision: number, numeric_scale: number): string => { if (character_maximum_length) { return `${data_type}(${character_maximum_length})`; } if (numeric_precision && numeric_scale) { return `${data_type}(${numeric_precision},${numeric_scale})`; } return data_type; }; const getDbdefault = (column_default: string, default_type: DefaultType): DefaultInfo => { if (default_type === 'string') { return { type: default_type, value: column_default.slice(1, -1), }; } return { type: default_type, value: column_default, }; }; const generateField = (row: Record<string, any>): Field => { const { COLUMN_NAME: column_name, DATA_TYPE: data_type, CHARACTER_MAXIMUM_LENGTH: character_maximum_length, NUMERIC_PRECISION: numeric_precision, NUMERIC_SCALE: numeric_scale, IDENTITY_INCREMENT: identity_increment, IS_NULLABLE: is_nullable, COLUMN_DEFAULT: column_default, DEFAULT_TYPE: default_type, COLUMN_COMMENT: column_comment, } = row; const dbdefault = column_default ? getDbdefault(column_default, default_type) : null; const fieldType = { type_name: getFieldType(data_type, character_maximum_length, numeric_precision, numeric_scale), schemaName: null, }; return { name: column_name, type: fieldType, dbdefault, not_null: !convertQueryBoolean(is_nullable), increment: !!identity_increment, note: column_comment ? { value: column_comment } : { value: '' }, }; }; const generateTablesAndFields = async (conn: Connection, schemas: string[]): Promise<{ tables: Table[], fields: FieldsDictionary, }> => { const fields: FieldsDictionary = {}; const schemaSql = schemas.length > 0 ? `AND c.table_schema IN (${schemas.map((schema) => `'${schema}'`).join(',')})` : ''; const tablesAndFieldsSql = ` SELECT c.table_schema, c.table_name, c.column_name, c.data_type, c.character_maximum_length, c.numeric_precision, c.numeric_scale, c.is_nullable, c.column_default, CASE WHEN c.column_default IS NULL THEN NULL WHEN c.column_default LIKE '''%' THEN 'string' WHEN LOWER(c.column_default) = 'true' OR LOWER(c.column_default) = 'false' THEN 'boolean' WHEN c.column_default REGEXP '^-?[0-9]+(\\.[0-9]+)?$' THEN 'number' ELSE 'expression' END AS default_type, t.comment AS table_comment, c.comment AS column_comment, c.identity_increment FROM information_schema.columns c JOIN information_schema.tables t ON c.table_name = t.table_name AND c.table_schema = t.table_schema WHERE t.table_type = 'BASE TABLE' AND t.table_schema NOT IN ('INFORMATION_SCHEMA') ${schemaSql} ORDER BY c.table_schema, t.created, c.ordinal_position; `; const tablesAndFieldsResult = await executeQuery(conn, tablesAndFieldsSql); const tables = tablesAndFieldsResult.reduce((acc: Record<string, Table>, row: Record<string, string>) => { const { TABLE_SCHEMA: table_schema, TABLE_NAME: table_name, TABLE_COMMENT: table_comment } = row; const key = `${table_schema}.${table_name}`; if (!acc[key]) { acc[key] = { name: table_name, schemaName: table_schema, note: table_comment ? { value: table_comment } : { value: '' }, }; } if (!fields[key]) fields[key] = []; const field = generateField(row); fields[key].push(field); return acc; }, {}); return { tables: Object.values(tables), fields, }; }; const createConstraintKeysMap = (keys: Record<string, string>[], schemas: string[], constraintType: 'primary' | 'unique'): Record<string, ConstraintRow> => { return keys.reduce((acc: Record<string, ConstraintRow>, row: Record<string, string>) => { const { schema_name, table_name, column_name, constraint_name } = row; const selectedSchema = schemas.length > 0 ? schemas.includes(schema_name) : true; if (!selectedSchema) { return acc; } const key = `${schema_name}.${table_name}.${constraint_name}`; if (acc[key]) { const columnNames = acc[key].columnNames; columnNames.push(column_name); return acc; } acc[key] = { schemaName: schema_name, tableName: table_name, constraintName: constraint_name, columnNames: [column_name], type: '', }; acc[key][constraintType] = true; return acc; }, {}); }; const generateIndexes = async (conn: Connection, databaseName: string, schemas: string[]) => { const getPrimaryKeysSql = ` SHOW PRIMARY KEYS IN DATABASE ${databaseName}; `; const getUniqueKeysSql = ` SHOW UNIQUE KEYS IN DATABASE ${databaseName}; `; const primaryKeys = await executeQuery(conn, getPrimaryKeysSql); const uniqueKeys = await executeQuery(conn, getUniqueKeysSql); const primaryKeysByConstraint: Record<string, ConstraintRow> = createConstraintKeysMap(primaryKeys, schemas, 'primary'); const uniqueKeysByConstraint: Record<string, ConstraintRow> = createConstraintKeysMap(uniqueKeys, schemas, 'unique'); const allConstraints: ConstraintRow[] = [Object.values(primaryKeysByConstraint), Object.values(uniqueKeysByConstraint)].flat(); const { indexes, tableConstraints } = allConstraints.reduce((acc: GeneratedIndexes, row: ConstraintRow): GeneratedIndexes => { const { schemaName, tableName, constraintName, columnNames, type, primary, unique } = row; const key = `${schemaName}.${tableName}`; if (columnNames.length < 2) { acc.tableConstraints[key] = { [columnNames[0]]: { pk: primary, unique, }, }; return acc; } const index: Index = { name: constraintName, type, unique, pk: primary, columns: columnNames.map((columnName) => ({ type: 'column', value: columnName, })), }; if (acc.indexes[key]) { acc.indexes[key].push(index); } else { acc.indexes[key] = [index]; } return acc; }, { indexes: {}, tableConstraints: {}}); return { indexes, tableConstraints, }; }; const fetchSchemaJson = async (connection: string): Promise<DatabaseSchema> => { const config = parseConnectionString(connection); const conn = await connectToSnowflake(config); if (conn instanceof Error) { throw conn; } // Schemas: schema1,schema2,schema3 const schemas = config.SCHEMAS ? parseSchema(config.SCHEMAS) : []; const databaseName = config.DATABASE; const tablesAndFieldsRes = generateTablesAndFields(conn, schemas); const indexesRes = generateIndexes(conn, databaseName, schemas); const res = await Promise.all([ tablesAndFieldsRes, indexesRes, ]); conn.destroy((err) => { if (err) { throw err; } else { console.log('Connection destroyed successfully.'); } }); const { tables, fields } = res[0]; const { indexes, tableConstraints } = res[1]; return { tables, fields, refs: [], enums: [], indexes, tableConstraints, }; }; export { fetchSchemaJson, };