knex-schema-inspector
Version:
Utility for extracting information about existing DB schema
334 lines (304 loc) • 10 kB
text/typescript
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;
}
}