feat(Firebird SQL): support to indexes and foreign keys

This commit is contained in:
Fabio Di Stasio 2022-11-08 14:05:54 +01:00
parent 76df6319c2
commit 2c8509ff41
6 changed files with 213 additions and 103 deletions

View File

@ -22,7 +22,7 @@ export const customizations: Customizations = {
schemas: false, schemas: false,
tables: true, tables: true,
views: false, views: false,
triggers: false, triggers: true,
triggerFunctions: false, triggerFunctions: false,
routines: false, routines: false,
functions: false, functions: false,

View File

@ -1,5 +1,4 @@
export default [ export default [
'PRIMARY', 'PRIMARY',
'INDEX',
'UNIQUE' 'UNIQUE'
]; ];

View File

@ -105,6 +105,7 @@ export default (connections: {[key: string]: antares.Client}) => {
break; break;
case 'pg': case 'pg':
case 'sqlite': case 'sqlite':
case 'firebird':
escapedParam = `'${params.content.replaceAll('\'', '\'\'')}'`; escapedParam = `'${params.content.replaceAll('\'', '\'\'')}'`;
break; break;
} }
@ -124,6 +125,7 @@ export default (connections: {[key: string]: antares.Client}) => {
escapedParam = `0x${fileBlob.toString('hex')}`; escapedParam = `0x${fileBlob.toString('hex')}`;
break; break;
case 'pg': case 'pg':
case 'firebird':
fileBlob = fs.readFileSync(params.content); fileBlob = fs.readFileSync(params.content);
escapedParam = `decode('${fileBlob.toString('hex')}', 'hex')`; escapedParam = `decode('${fileBlob.toString('hex')}', 'hex')`;
break; break;
@ -141,6 +143,7 @@ export default (connections: {[key: string]: antares.Client}) => {
escapedParam = '\'\''; escapedParam = '\'\'';
break; break;
case 'pg': case 'pg':
case 'firebird':
escapedParam = 'decode(\'\', \'hex\')'; escapedParam = 'decode(\'\', \'hex\')';
break; break;
case 'sqlite': case 'sqlite':
@ -158,6 +161,7 @@ export default (connections: {[key: string]: antares.Client}) => {
case 'mysql': case 'mysql':
case 'maria': case 'maria':
case 'pg': case 'pg':
case 'firebird':
escapedParam = params.content; escapedParam = params.content;
break; break;
case 'sqlite': case 'sqlite':
@ -382,7 +386,20 @@ export default (connections: {[key: string]: antares.Client}) => {
if (description) if (description)
query.select(`LEFT(${description}, 20) AS foreign_description`); query.select(`LEFT(${description}, 20) AS foreign_description`);
const results = await query.run(); const results = await query.run<{[key: string]: string}>();
const parsedResults: {[key: string]: string}[] = [];
for (const row of results.rows) {
const remappedRow: {[key: string]: string} = {};
for (const key in row)
remappedRow[key.toLowerCase()] = row[key];// Thanks Firebird -.-
parsedResults.push(remappedRow);
}
results.rows = parsedResults;
return { status: 'success', response: results }; return { status: 'success', response: results };
} }

View File

@ -6,6 +6,7 @@ import dataTypes from 'common/data-types/sqlite';
export class FirebirdSQLClient extends AntaresCore { export class FirebirdSQLClient extends AntaresCore {
private _schema?: string; private _schema?: string;
private _runningConnections: Map<string, number>;
private _connectionsToCommit: Map<string, firebird.Database>; private _connectionsToCommit: Map<string, firebird.Database>;
protected _connection?: firebird.Database; protected _connection?: firebird.Database;
_params: firebird.Options; _params: firebird.Options;
@ -69,7 +70,7 @@ export class FirebirdSQLClient extends AntaresCore {
} }
destroy () { destroy () {
return (this._connection as firebird.Database).detach(); return this._connection.detach();
} }
use (): void { use (): void {
@ -84,7 +85,11 @@ export class FirebirdSQLClient extends AntaresCore {
DESCRIPTION: string | null; DESCRIPTION: string | null;
} }
// type ShowTriggersResult = ShowTableResult interface ShowTriggersResult {
NAME: string;
RELATION: string;
SOURCE: string;
}
const { rows: databases } = await this.raw<antares.QueryResult<{ NAME: string}>>('SELECT rdb$get_context(\'SYSTEM\', \'DB_NAME\') as name FROM rdb$database'); const { rows: databases } = await this.raw<antares.QueryResult<{ NAME: string}>>('SELECT rdb$get_context(\'SYSTEM\', \'DB_NAME\') as name FROM rdb$database');
@ -93,10 +98,11 @@ export class FirebirdSQLClient extends AntaresCore {
}); });
const tablesArr: ShowTableResult[] = []; const tablesArr: ShowTableResult[] = [];
// const triggersArr: ShowTriggersResult[] = []; const triggersArr: ShowTriggersResult[] = [];
let schemaSize = 0; let schemaSize = 0;
for (const db of filteredDatabases) { // eslint-disable-next-line @typescript-eslint/no-unused-vars
for (const _db of filteredDatabases) {
// if (!schemas.has(db.name)) continue; // if (!schemas.has(db.name)) continue;
const { rows: tables } = await this.raw<antares.QueryResult<ShowTableResult>>(` const { rows: tables } = await this.raw<antares.QueryResult<ShowTableResult>>(`
@ -111,6 +117,17 @@ export class FirebirdSQLClient extends AntaresCore {
`); `);
tablesArr.push(...tables); tablesArr.push(...tables);
const { rows: triggers } = await this.raw<antares.QueryResult<ShowTriggersResult>>(`
SELECT
RDB$TRIGGER_NAME as name,
RDB$RELATION_NAME as relation,
RDB$TRIGGER_SOURCE as source
FROM RDB$TRIGGERS
WHERE RDB$SYSTEM_FLAG=0;
`);
triggersArr.push(...triggers);
} }
return filteredDatabases.map(db => { return filteredDatabases.map(db => {
@ -128,12 +145,13 @@ export class FirebirdSQLClient extends AntaresCore {
}); });
// TRIGGERS // TRIGGERS
// const remappedTriggers = triggersArr.filter(trigger => trigger.Db === db.name).map(trigger => { const remappedTriggers = triggersArr.map(trigger => {
// return { return {
// name: trigger.name, name: trigger.NAME,
// table: trigger.tbl_name table: trigger.RELATION,
// }; statement: trigger.SOURCE
// }); };
});
return { return {
name: db.name, name: db.name,
@ -141,7 +159,7 @@ export class FirebirdSQLClient extends AntaresCore {
tables: remappedTables, tables: remappedTables,
functions: [], functions: [],
procedures: [], procedures: [],
triggers: [], triggers: remappedTriggers,
schedulers: [] schedulers: []
}; };
}); });
@ -226,97 +244,111 @@ export class FirebirdSQLClient extends AntaresCore {
}); });
} }
async getTableApproximateCount ({ schema, table }: { schema: string; table: string }): Promise<number> { async getTableApproximateCount ({ table }: { schema: string; table: string }): Promise<number> {
const { rows } = await this.raw(`SELECT COUNT(*) AS count FROM "${schema}"."${table}"`); const { rows } = await this.raw(`SELECT COUNT(*) AS nRows FROM "${table}"`);
return rows.length ? rows[0].count : 0; return rows.length ? rows[0].NROWS : 0;
} }
async getTableOptions ({ table }: { table: string }) { async getTableOptions ({ table }: { table: string }) {
return { name: table }; return { name: table };
} }
async getTableIndexes ({ schema, table }: { schema: string; table: string }) { async getTableIndexes ({ table }: { schema: string; table: string }) {
interface TableColumnsResult {
type: string;
name: string;
// eslint-disable-next-line camelcase
tbl_name: string;
rootpage:4;
sql: string;
}
interface ShowIndexesResult { interface ShowIndexesResult {
seq: number; INDEX_NAME: string;
name: string; FIELD_NAME: string;
unique: 0 | 1; TABLE_NAME: string;
origin: string; INDEX_TYPE: string;
partial: 0 | 1; INDEX_UNIQUE: number;
} }
const remappedIndexes = []; const remappedIndexes = [];
const { rows: primaryKeys } = await this.raw<antares.QueryResult<TableColumnsResult>>(`SELECT * FROM "${schema}".pragma_table_info('${table}') WHERE pk != 0`);
for (const key of primaryKeys) { const { rows: indexes } = await this.raw<antares.QueryResult<ShowIndexesResult>>(`
SELECT
ix.rdb$index_name AS INDEX_NAME,
sg.rdb$field_name AS FIELD_NAME,
rc.rdb$relation_name AS TABLE_NAME,
rc.rdb$constraint_type AS INDEX_TYPE,
ix.RDB$UNIQUE_FLAG AS INDEX_UNIQUE
FROM
rdb$indices ix
LEFT JOIN rdb$index_segments sg ON ix.rdb$index_name = sg.rdb$index_name
LEFT JOIN rdb$relation_constraints rc ON rc.rdb$index_name = ix.rdb$index_name
WHERE
rc.rdb$relation_name = '${table}'
`);
for (const index of indexes) {
remappedIndexes.push({ remappedIndexes.push({
name: 'PRIMARY', name: index.INDEX_NAME.trim(),
column: key.name, column: index.FIELD_NAME.trim(),
indexType: null as never, indexType: null as never,
type: 'PRIMARY', type: index.INDEX_TYPE.trim(),
cardinality: null as never, cardinality: null as never,
comment: '', comment: '',
indexComment: '' indexComment: ''
}); });
} }
const { rows: indexes } = await this.raw<antares.QueryResult<ShowIndexesResult>>(`SELECT * FROM "${schema}".pragma_index_list('${table}');`);
for (const index of indexes) {
const { rows: details } = await this.raw(`SELECT * FROM "${schema}".pragma_index_info('${index.name}');`);
for (const detail of details) {
remappedIndexes.push({
name: index.name,
column: detail.name,
indexType: null as never,
type: index.unique === 1 ? 'UNIQUE' : 'INDEX',
cardinality: null as never,
comment: '',
indexComment: ''
});
}
}
return remappedIndexes; return remappedIndexes;
} }
async getKeyUsage ({ schema, table }: { schema: string; table: string }) { async getKeyUsage ({ schema, table }: { schema: string; table: string }) {
/* eslint-disable camelcase */ /* eslint-disable camelcase */
interface KeyResult { interface KeyResult {
from: string; PKTABLE_NAME: string;
id: number; PKCOLUMN_NAME: string;
table: string; FKTABLE_NAME: string;
to: string; FKCOLUMN_NAME: string;
on_update: string; KEY_SEQ: number;
on_delete: string; UPDATE_RULE: string;
DELETE_RULE: string;
PK_NAME: string;
FK_NAME: string;
} }
/* eslint-enable camelcase */ /* eslint-enable camelcase */
const { rows } = await this.raw<antares.QueryResult<KeyResult>>(`SELECT * FROM "${schema}".pragma_foreign_key_list('${table}');`); const { rows } = await this.raw<antares.QueryResult<KeyResult>>(`
SELECT
PK.RDB$RELATION_NAME as PKTABLE_NAME,
ISP.RDB$FIELD_NAME as PKCOLUMN_NAME,
FK.RDB$RELATION_NAME as FKTABLE_NAME,
ISF.RDB$FIELD_NAME as FKCOLUMN_NAME,
(ISP.RDB$FIELD_POSITION + 1) as KEY_SEQ,
RC.RDB$UPDATE_RULE as UPDATE_RULE,
RC.RDB$DELETE_RULE as DELETE_RULE,
PK.RDB$CONSTRAINT_NAME as PK_NAME,
FK.RDB$CONSTRAINT_NAME as FK_NAME
FROM
RDB$RELATION_CONSTRAINTS PK,
RDB$RELATION_CONSTRAINTS FK,
RDB$REF_CONSTRAINTS RC,
RDB$INDEX_SEGMENTS ISP,
RDB$INDEX_SEGMENTS ISF
WHERE FK.RDB$RELATION_NAME = '${table}'
and FK.RDB$CONSTRAINT_NAME = RC.RDB$CONSTRAINT_NAME
and PK.RDB$CONSTRAINT_NAME = RC.RDB$CONST_NAME_UQ
and ISP.RDB$INDEX_NAME = PK.RDB$INDEX_NAME
and ISF.RDB$INDEX_NAME = FK.RDB$INDEX_NAME
and ISP.RDB$FIELD_POSITION = ISF.RDB$FIELD_POSITION
ORDER BY 1, 5
`);
return rows.map(field => { return rows.map(field => {
return { return {
schema: schema, schema: schema,
table: table, table: table,
field: field.from, field: field.FKCOLUMN_NAME.trim(),
position: field.id + 1, position: field.KEY_SEQ,
constraintPosition: null, constraintPosition: null,
constraintName: field.id, constraintName: field.FK_NAME.trim(),
refSchema: schema, refSchema: schema,
refTable: field.table, refTable: field.PKTABLE_NAME.trim(),
refField: field.to, refField: field.PKCOLUMN_NAME.trim(),
onUpdate: field.on_update, onUpdate: field.UPDATE_RULE.trim(),
onDelete: field.on_delete onDelete: field.DELETE_RULE.trim()
}; };
}); });
} }
@ -582,12 +614,15 @@ export class FirebirdSQLClient extends AntaresCore {
// LIMIT // LIMIT
const limitRaw = this._query.limit ? ` first ${this._query.limit}` : ''; const limitRaw = this._query.limit ? ` first ${this._query.limit}` : '';
// OFFSET
const offsetRaw = this._query.offset ? ` skip ${this._query.offset}` : '';
// SELECT // SELECT
const selectArray = this._query.select.reduce(this._reducer, []); const selectArray = this._query.select.reduce(this._reducer, []);
let selectRaw = ''; let selectRaw = '';
if (selectArray.length) if (selectArray.length)
selectRaw = selectArray.length ? `SELECT${limitRaw||''} ${selectArray.join(', ')} ` : `SELECT${limitRaw||''} * `; selectRaw = selectArray.length ? `SELECT${limitRaw||''}${offsetRaw||''} ${selectArray.join(', ')} ` : `SELECT${limitRaw||''}${offsetRaw||''} * `;
// FROM // FROM
let fromRaw = ''; let fromRaw = '';
@ -627,10 +662,7 @@ export class FirebirdSQLClient extends AntaresCore {
const orderByArray = this._query.orderBy.reduce(this._reducer, []); const orderByArray = this._query.orderBy.reduce(this._reducer, []);
const orderByRaw = orderByArray.length ? `ORDER BY ${orderByArray.join(', ')} ` : ''; const orderByRaw = orderByArray.length ? `ORDER BY ${orderByArray.join(', ')} ` : '';
// OFFSET return `${selectRaw}${updateRaw ? 'UPDATE' : ''}${insertRaw ? 'INSERT ' : ''}${this._query.delete ? 'DELETE ' : ''}${fromRaw}${updateRaw}${whereRaw}${groupByRaw}${orderByRaw}${insertRaw}`;
const offsetRaw = this._query.offset ? `OFFSET ${this._query.offset} ` : '';
return `${selectRaw}${updateRaw ? 'UPDATE' : ''}${insertRaw ? 'INSERT ' : ''}${this._query.delete ? 'DELETE ' : ''}${fromRaw}${updateRaw}${whereRaw}${groupByRaw}${orderByRaw}${offsetRaw}${insertRaw}`;
} }
async raw<T = antares.QueryResult> (sql: string, args?: antares.QueryParams) { async raw<T = antares.QueryResult> (sql: string, args?: antares.QueryParams) {
@ -660,6 +692,7 @@ export class FirebirdSQLClient extends AntaresCore {
sql = sql.replace(/(\/\*(.|[\r\n])*?\*\/)|(--(.*|[\r\n]))/gm, '');// Remove comments sql = sql.replace(/(\/\*(.|[\r\n])*?\*\/)|(--(.*|[\r\n]))/gm, '');// Remove comments
const resultsArr = []; const resultsArr = [];
let paramsArr = [];
const queries = args.split const queries = args.split
? sql.split(/((?:[^;'"]*(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*')[^;'"]*)+)|;/gm) ? sql.split(/((?:[^;'"]*(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*')[^;'"]*)+)|;/gm)
.filter(Boolean) .filter(Boolean)
@ -690,52 +723,114 @@ export class FirebirdSQLClient extends AntaresCore {
if (!query) continue; if (!query) continue;
const timeStart = new Date(); const timeStart = new Date();
let timeStop; let timeStop;
const keysArr: antares.QueryForeign[] = []; let keysArr: antares.QueryForeign[] = [];
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const { rows, report, fields, keys, duration }: any = await new Promise((resolve, reject) => { const { rows, report, fields, keys, duration }: any = await new Promise((resolve, reject) => {
(async () => { (async () => {
let queryResult; let queryResult;
let remappedFields; let remappedFields: {
name: string;
alias: string;
orgName: string;
schema: string;
table: string;
tableAlias: string;
orgTable: string;
type: string;
length: number;
key?: string;
}[];
try { try {
queryResult = await new Promise<unknown[]>((resolve, reject) => { queryResult = await new Promise<unknown[]>((resolve, reject) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
(connection as any).query(query, [], (err: any, res: any, fields: FieldData[]) => { // <- fields is not natively typed or documented (connection as any).query(query, [], async (err: any, res: any, fields: FieldData[]) => { // <- fields is not natively typed or documented
if (err) reject(err); if (err) reject(err);
else { else {
const remappedResponse = []; const remappedResponse = [];
for (const row of res) { if (res) {
for (const key in row) { for (const row of res) {
if (Buffer.isBuffer(row[key])) for (const key in row) {
row[key] = row[key].toString('binary'); if (Buffer.isBuffer(row[key]))
else if (typeof row[key] === 'function') row[key] = row[key].toString('binary');
row[key] = row[key].toString('binary'); else if (typeof row[key] === 'function')
} row[key] = row[key].toString('binary');
}
remappedResponse.push(row); remappedResponse.push(row);
}
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any if (fields) {
remappedFields = fields.map((field: any) => { remappedFields = fields.map(field => {
return { return {
name: field.alias, name: field.alias,
alias: field.alias, alias: field.alias,
orgName: field.field, orgName: field.field,
schema: args.schema, schema: args.schema,
table: field.relation, table: field.relation,
tableAlias: field.relation, tableAlias: field.relation,
orgTable: field.relation, orgTable: field.relation,
type: this.types[field.type], type: this.types[field.type],
length: field.length length: field.length,
}; key: undefined as string
}); };
});
}
resolve(remappedResponse); resolve(remappedResponse);
} }
}); });
}); });
if (args.details) {
if (remappedFields.length) {
paramsArr = remappedFields.map(field => {
return {
table: field.orgTable,
schema: field.schema
};
}).filter((val, i, arr) => arr.findIndex(el => el.table === val.table) === i);
for (const paramObj of paramsArr) {
if (!paramObj.table || !paramObj.schema) continue;
try { // Column details
const indexes = await this.getTableIndexes(paramObj);
remappedFields = remappedFields.map(field => {
const fieldIndex = indexes.find(i => i.column === field.name);
if (fieldIndex) {
const key = fieldIndex.type === 'PRIMARY KEY' ? 'pri' : fieldIndex.type === 'UNIQUE' ? 'uni' : 'fk';
field = { ...field, key };
}
return field;
});
}
catch (err) {
if (args.autocommit) {
this._connection.detach();
this._runningConnections.delete(args.tabUid);
}
reject(err);
}
try { // Key usage (foreign keys)
const response = await this.getKeyUsage(paramObj);
keysArr = keysArr ? [...keysArr, ...response] : response;
}
catch (err) {
if (args.autocommit) {
this._connection.detach();
this._runningConnections.delete(args.tabUid);
}
reject(err);
}
}
}
}
} }
catch (err) { catch (err) {
reject(err); reject(err);
@ -744,10 +839,6 @@ export class FirebirdSQLClient extends AntaresCore {
timeStop = new Date(); timeStop = new Date();
// if (args.details) {
// }
resolve({ resolve({
duration: timeStop.getTime() - timeStart.getTime(), duration: timeStop.getTime() - timeStart.getTime(),
rows: Array.isArray(queryResult) ? queryResult.some(el => Array.isArray(el)) ? [] : queryResult : false, rows: Array.isArray(queryResult) ? queryResult.some(el => Array.isArray(el)) ? [] : queryResult : false,

View File

@ -272,6 +272,8 @@ const keyName = (key: string) => {
return 'UNIQUE'; return 'UNIQUE';
case 'mul': case 'mul':
return 'INDEX'; return 'INDEX';
case 'fk':
return 'FOREIGN KEY';
default: default:
return 'UNKNOWN ' + key; return 'UNKNOWN ' + key;
} }

View File

@ -17,6 +17,7 @@
&.key-mul, &.key-mul,
&.key-INDEX, &.key-INDEX,
&.key-fk,
&.key-KEY { &.key-KEY {
color: limegreen; color: limegreen;
} }