UNPKG

knex-schema-inspector

Version:

Utility for extracting information about existing DB schema

334 lines (304 loc) 10 kB
import { Knex } from 'knex'; import { SchemaInspector } from '../types/schema-inspector'; import { Table } from '../types/table'; import { Column } from '../types/column'; import { ForeignKey } from '../types/foreign-key'; import { stripQuotes } from '../utils/strip-quotes'; /** * NOTE: Use previous optimizer for better data dictionary performance. */ const OPTIMIZER_FEATURES = '11.2.0.4'; type RawColumn = { TABLE_NAME: string; COLUMN_NAME: string; DATA_DEFAULT: any | null; DATA_TYPE: string; DATA_LENGTH: number | null; DATA_PRECISION: number | null; DATA_SCALE: number | null; NULLABLE: 'Y' | 'N'; COLUMN_COMMENT: string | null; REFERENCED_TABLE_NAME: string | null; REFERENCED_COLUMN_NAME: string | null; CONSTRAINT_TYPE: 'P' | 'U' | 'R' | null; VIRTUAL_COLUMN: 'YES' | 'NO'; IDENTITY_COLUMN: 'YES' | 'NO'; }; export function rawColumnToColumn(rawColumn: RawColumn): Column { const is_generated = rawColumn.VIRTUAL_COLUMN === 'YES'; const default_value = parseDefaultValue(rawColumn.DATA_DEFAULT); return { name: rawColumn.COLUMN_NAME, table: rawColumn.TABLE_NAME, data_type: rawColumn.DATA_TYPE, default_value: !is_generated ? default_value : null, generation_expression: is_generated ? default_value : null, max_length: rawColumn.DATA_LENGTH, numeric_precision: rawColumn.DATA_PRECISION, numeric_scale: rawColumn.DATA_SCALE, is_generated: rawColumn.VIRTUAL_COLUMN === 'YES', is_nullable: rawColumn.NULLABLE === 'Y', is_unique: rawColumn.CONSTRAINT_TYPE === 'U', is_primary_key: rawColumn.CONSTRAINT_TYPE === 'P', has_auto_increment: rawColumn.IDENTITY_COLUMN === 'YES', foreign_key_column: rawColumn.REFERENCED_COLUMN_NAME, foreign_key_table: rawColumn.REFERENCED_TABLE_NAME, comment: rawColumn.COLUMN_COMMENT, }; } export function parseDefaultValue(value: string | null): string | null { if (value === null || value.trim().toLowerCase() === 'null') return null; if (value === 'CURRENT_TIMESTAMP ') return 'CURRENT_TIMESTAMP'; return stripQuotes(value); } export default class oracleDB implements SchemaInspector { knex: Knex; constructor(knex: Knex) { this.knex = knex; } // Tables // =============================================================================================== /** * List all existing tables in the current schema/database */ async tables(): Promise<string[]> { const records = await this.knex .select<Table[]>( this.knex.raw(` /*+ OPTIMIZER_FEATURES_ENABLE('${OPTIMIZER_FEATURES}') */ "TABLE_NAME" "name" `) ) .from('USER_TABLES'); return records.map(({ name }) => name); } /** * Get the table info for a given table. If table parameter is undefined, it will return all tables * in the current schema/database */ tableInfo(): Promise<Table[]>; tableInfo(table: string): Promise<Table>; async tableInfo<T>(table?: string) { const query = this.knex .select<Table[]>( this.knex.raw(` /*+ OPTIMIZER_FEATURES_ENABLE('${OPTIMIZER_FEATURES}') */ "TABLE_NAME" "name" `) ) .from('USER_TABLES'); if (table) { return await query.andWhere({ TABLE_NAME: table }).first(); } return await query; } /** * Check if a table exists in the current schema/database */ async hasTable(table: string): Promise<boolean> { const result = await this.knex .select<{ count: 0 | 1 }>( this.knex.raw(` /*+ OPTIMIZER_FEATURES_ENABLE('${OPTIMIZER_FEATURES}') */ COUNT(*) "count" `) ) .from('USER_TABLES') .where({ TABLE_NAME: table }) .first(); return !!result?.count; } // Columns // =============================================================================================== /** * Get all the available columns in the current schema/database. Can be filtered to a specific table */ async columns(table?: string) { const query = this.knex .select<{ table: string; column: string }[]>( this.knex.raw(` /*+ OPTIMIZER_FEATURES_ENABLE('${OPTIMIZER_FEATURES}') NO_QUERY_TRANSFORMATION */ "TABLE_NAME" "table", "COLUMN_NAME" "column" `) ) .from('USER_TAB_COLS') .where({ HIDDEN_COLUMN: 'NO' }); if (table) { query.andWhere({ TABLE_NAME: table }); } return await query; } /** * Get the column info for all columns, columns in a given table, or a specific column. */ columnInfo(): Promise<Column[]>; columnInfo(table: string): Promise<Column[]>; columnInfo(table: string, column: string): Promise<Column>; async columnInfo<T>(table?: string, column?: string) { /** * NOTE: Keep in mind, this query is optimized for speed. */ const query = this.knex .with( 'uc', this.knex.raw(` SELECT /*+ MATERIALIZE */ "uc"."TABLE_NAME", "ucc"."COLUMN_NAME", "uc"."CONSTRAINT_NAME", "uc"."CONSTRAINT_TYPE", "uc"."R_CONSTRAINT_NAME", COUNT(*) OVER( PARTITION BY "uc"."CONSTRAINT_NAME" ) "CONSTRAINT_COUNT", ROW_NUMBER() OVER( PARTITION BY "uc"."TABLE_NAME", "ucc"."COLUMN_NAME" ORDER BY "uc"."CONSTRAINT_TYPE" ) "CONSTRAINT_PRIORITY" FROM "USER_CONSTRAINTS" "uc" INNER JOIN "USER_CONS_COLUMNS" "ucc" ON "uc"."CONSTRAINT_NAME" = "ucc"."CONSTRAINT_NAME" WHERE "uc"."CONSTRAINT_TYPE" IN ('P', 'U', 'R') `) ) .select( this.knex.raw(` /*+ OPTIMIZER_FEATURES_ENABLE('${OPTIMIZER_FEATURES}') */ "c"."TABLE_NAME", "c"."COLUMN_NAME", "c"."DATA_DEFAULT", "c"."DATA_TYPE", "c"."DATA_LENGTH", "c"."DATA_PRECISION", "c"."DATA_SCALE", "c"."NULLABLE", "c"."IDENTITY_COLUMN", "c"."VIRTUAL_COLUMN", "cm"."COMMENTS" "COLUMN_COMMENT", "ct"."CONSTRAINT_TYPE", "fk"."TABLE_NAME" "REFERENCED_TABLE_NAME", "fk"."COLUMN_NAME" "REFERENCED_COLUMN_NAME" FROM "USER_TAB_COLS" "c" LEFT JOIN "USER_COL_COMMENTS" "cm" ON "c"."TABLE_NAME" = "cm"."TABLE_NAME" AND "c"."COLUMN_NAME" = "cm"."COLUMN_NAME" LEFT JOIN "uc" "ct" ON "c"."TABLE_NAME" = "ct"."TABLE_NAME" AND "c"."COLUMN_NAME" = "ct"."COLUMN_NAME" AND "ct"."CONSTRAINT_COUNT" = 1 AND "ct"."CONSTRAINT_PRIORITY" = 1 LEFT JOIN "uc" "fk" ON "ct"."R_CONSTRAINT_NAME" = "fk"."CONSTRAINT_NAME" `) ) .where({ 'c.HIDDEN_COLUMN': 'NO' }); if (table) { query.andWhere({ 'c.TABLE_NAME': table }); } if (column) { const rawColumn = await query .andWhere({ 'c.COLUMN_NAME': column, }) .first(); return rawColumnToColumn(rawColumn); } const records: RawColumn[] = await query; return records.map(rawColumnToColumn); } /** * Check if a table exists in the current schema/database */ async hasColumn(table: string, column: string): Promise<boolean> { const result = await this.knex .select<{ count: 0 | 1 }>( this.knex.raw(` /*+ OPTIMIZER_FEATURES_ENABLE('${OPTIMIZER_FEATURES}') NO_QUERY_TRANSFORMATION */ COUNT(*) "count" `) ) .from('USER_TAB_COLS') .where({ TABLE_NAME: table, COLUMN_NAME: column, HIDDEN_COLUMN: 'NO', }) .first(); return !!result?.count; } /** * Get the primary key column for the given table */ async primary(table: string): Promise<string> { /** * NOTE: Keep in mind, this query is optimized for speed. */ const result = await this.knex .with( 'uc', this.knex .select(this.knex.raw(`/*+ MATERIALIZE */ "CONSTRAINT_NAME"`)) .from('USER_CONSTRAINTS') .where({ TABLE_NAME: table, CONSTRAINT_TYPE: 'P', }) ) .select( this.knex.raw(` /*+ OPTIMIZER_FEATURES_ENABLE('${OPTIMIZER_FEATURES}') */ "ucc"."COLUMN_NAME" FROM "USER_CONS_COLUMNS" "ucc" INNER JOIN "uc" "pk" ON "ucc"."CONSTRAINT_NAME" = "pk"."CONSTRAINT_NAME" `) ) .first(); return result?.COLUMN_NAME ?? null; } // Foreign Keys // =============================================================================================== async foreignKeys(table?: string): Promise<ForeignKey[]> { /** * NOTE: Keep in mind, this query is optimized for speed. */ const query = this.knex .with( 'ucc', this.knex.raw(` SELECT /*+ MATERIALIZE */ "TABLE_NAME", "COLUMN_NAME", "CONSTRAINT_NAME" FROM "USER_CONS_COLUMNS" `) ) .select( this.knex.raw(` /*+ OPTIMIZER_FEATURES_ENABLE('${OPTIMIZER_FEATURES}') */ "uc"."TABLE_NAME" "table", "fcc"."COLUMN_NAME" "column", "rcc"."TABLE_NAME" AS "foreign_key_table", "rcc"."COLUMN_NAME" AS "foreign_key_column", "uc"."CONSTRAINT_NAME" "constraint_name", NULL as "on_update", "uc"."DELETE_RULE" "on_delete" FROM "USER_CONSTRAINTS" "uc" INNER JOIN "ucc" "fcc" ON "uc"."CONSTRAINT_NAME" = "fcc"."CONSTRAINT_NAME" INNER JOIN "ucc" "rcc" ON "uc"."R_CONSTRAINT_NAME" = "rcc"."CONSTRAINT_NAME" `) ) .where({ 'uc.CONSTRAINT_TYPE': 'R' }); if (table) { query.andWhere({ 'uc.TABLE_NAME': table }); } return await query; } }