From d892fa6fb3e86fbb96887d8eb67319ae855260a1 Mon Sep 17 00:00:00 2001 From: Fabio Di Stasio Date: Tue, 16 Mar 2021 18:42:03 +0100 Subject: [PATCH] feat(PostgreSQL): partial postgre implementation --- package.json | 1 + src/common/customizations/defaults.js | 36 + src/common/customizations/mysql.js | 36 + src/common/customizations/postgresql.js | 28 + src/common/data-types/postgresql.js | 297 ++++ src/common/db-properties/mysql.js | 1 - src/common/fieldTypes.js | 69 +- src/common/index-types/postgresql.js | 5 + src/main/ipc-handlers/connection.js | 12 +- src/main/ipc-handlers/database.js | 14 +- src/main/ipc-handlers/tables.js | 18 +- src/main/libs/ClientsFactory.js | 5 +- src/main/libs/clients/MySQLClient.js | 40 +- src/main/libs/clients/PostgreSQLClient.js | 1319 +++++++++++++++++ .../components/ModalAskParameters.vue | 7 +- .../components/ModalEditConnection.vue | 6 +- src/renderer/components/ModalEditDatabase.vue | 4 +- src/renderer/components/ModalFakerRows.vue | 7 +- .../components/ModalNewConnection.vue | 7 +- src/renderer/components/ModalNewDatabase.vue | 20 +- src/renderer/components/ModalNewTableRow.vue | 7 +- src/renderer/components/Workspace.vue | 11 +- .../components/WorkspaceExploreBar.vue | 2 +- .../WorkspaceExploreBarDatabaseContext.vue | 48 +- .../WorkspacePropsFunctionParamsModal.vue | 7 +- .../WorkspacePropsRoutineParamsModal.vue | 7 +- .../components/WorkspacePropsTableRow.vue | 10 +- .../components/WorkspaceQueryTableRow.vue | 7 +- src/renderer/i18n/en-US.js | 6 +- src/renderer/scss/_data-types.scss | 27 + src/renderer/scss/_variables.scss | 1 + .../store/modules/workspaces.store.js | 11 +- 32 files changed, 2003 insertions(+), 73 deletions(-) create mode 100644 src/common/customizations/defaults.js create mode 100644 src/common/customizations/mysql.js create mode 100644 src/common/customizations/postgresql.js create mode 100644 src/common/data-types/postgresql.js delete mode 100644 src/common/db-properties/mysql.js create mode 100644 src/common/index-types/postgresql.js create mode 100644 src/main/libs/clients/PostgreSQLClient.js diff --git a/package.json b/package.json index acb27254..f16735cb 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "moment": "^2.29.1", "mssql": "^6.2.3", "mysql2": "^2.2.5", + "node-sql-parser": "^3.1.0", "pg": "^8.5.1", "source-map-support": "^0.5.16", "spectre.css": "^0.5.9", diff --git a/src/common/customizations/defaults.js b/src/common/customizations/defaults.js new file mode 100644 index 00000000..9a53c36a --- /dev/null +++ b/src/common/customizations/defaults.js @@ -0,0 +1,36 @@ +module.exports = { + // Core + collations: false, + engines: false, + // Tools + processesList: false, + usersManagement: false, + variables: false, + // Structure + databases: true, + tables: false, + views: false, + triggers: false, + routines: false, + functions: false, + schedulers: false, + // Settings + tableAdd: false, + viewAdd: false, + triggerAdd: false, + routineAdd: false, + functionAdd: false, + schedulerAdd: false, + databaseEdit: false, + schemaEdit: false, + tableSettings: false, + viewSettings: false, + triggerSettings: false, + routineSettings: false, + functionSettings: false, + schedulerSettings: false, + indexes: false, + foreigns: false, + sortableFields: false, + zerofill: false +}; diff --git a/src/common/customizations/mysql.js b/src/common/customizations/mysql.js new file mode 100644 index 00000000..d01906db --- /dev/null +++ b/src/common/customizations/mysql.js @@ -0,0 +1,36 @@ +const defaults = require('./defaults'); + +module.exports = { + ...defaults, + // Core + collations: true, + engines: true, + // Tools + processesList: true, + // Structure + schemas: true, + tables: true, + views: true, + triggers: true, + routines: true, + functions: true, + schedulers: true, + // Settings + tableAdd: true, + viewAdd: true, + triggerAdd: true, + routineAdd: true, + functionAdd: true, + schedulerAdd: true, + schemaEdit: true, + tableSettings: true, + viewSettings: true, + triggerSettings: true, + routineSettings: true, + functionSettings: true, + schedulerSettings: true, + indexes: true, + foreigns: true, + sortableFields: true, + zerofill: true +}; diff --git a/src/common/customizations/postgresql.js b/src/common/customizations/postgresql.js new file mode 100644 index 00000000..79ac60d1 --- /dev/null +++ b/src/common/customizations/postgresql.js @@ -0,0 +1,28 @@ +const defaults = require('./defaults'); + +module.exports = { + ...defaults, + // Core + collations: false, + engines: false, + // Tools + processesList: true, + // Structure + tables: true, + views: true, + triggers: true, + routines: true, + functions: true, + schedulers: false, + // Settings + databaseEdit: false, + tableSettings: false, + viewSettings: false, + triggerSettings: false, + routineSettings: false, + functionSettings: false, + schedulerSettings: false, + indexes: true, + foreigns: true, + sortableFields: false +}; diff --git a/src/common/data-types/postgresql.js b/src/common/data-types/postgresql.js new file mode 100644 index 00000000..73a74e50 --- /dev/null +++ b/src/common/data-types/postgresql.js @@ -0,0 +1,297 @@ +module.exports = [ + { + group: 'integer', + types: [ + { + name: 'SMALLINT', + length: true, + unsigned: true + }, + { + name: 'INTEGER', + length: true, + unsigned: true + }, + { + name: 'BIGINT', + length: true, + unsigned: true + }, + { + name: 'DECIMAL', + length: true, + unsigned: true + }, + { + name: 'NUMERIC', + length: true, + unsigned: true + }, + { + name: 'SMALLSERIAL', + length: true, + unsigned: true + }, + { + name: 'SERIAL', + length: true, + unsigned: true + }, + { + name: 'BIGSERIAL', + length: true, + unsigned: true + } + ] + }, + { + group: 'float', + types: [ + { + name: 'REAL', + length: true, + unsigned: true + }, + { + name: 'DOUBLE PRECISION', + length: true, + unsigned: true + } + ] + }, + { + group: 'monetary', + types: [ + { + name: 'money', + length: true, + unsigned: true + } + ] + }, + { + group: 'string', + types: [ + { + name: 'CHARACTER VARYING', + length: true, + unsigned: false + }, + { + name: 'CHAR', + length: false, + unsigned: false + }, + { + name: 'CHARACTER', + length: false, + unsigned: false + }, + { + name: 'TEXT', + length: false, + unsigned: false + }, + { + name: '"CHAR"', + length: false, + unsigned: false + }, + { + name: 'NAME', + length: false, + unsigned: false + } + ] + }, + { + group: 'binary', + types: [ + { + name: 'BYTEA', + length: true, + unsigned: false + } + ] + }, + { + group: 'time', + types: [ + { + name: 'TIMESTAMP', + length: false, + unsigned: false + }, + { + name: 'TIMESTAMP WITH TIME ZONE', + length: false, + unsigned: false + }, + { + name: 'DATE', + length: true, + unsigned: false + }, + { + name: 'TIME', + length: true, + unsigned: false + }, + { + name: 'TIME WITH TIME ZONE', + length: true, + unsigned: false + }, + { + name: 'INTERVAL', + length: false, + unsigned: false + } + ] + }, + { + group: 'boolean', + types: [ + { + name: 'BOOLEAN', + length: false, + unsigned: false + } + ] + }, + { + group: 'geometric', + types: [ + { + name: 'POINT', + length: false, + unsigned: false + }, + { + name: 'LINE', + length: false, + unsigned: false + }, + { + name: 'LSEG', + length: false, + unsigned: false + }, + { + name: 'BOX', + length: false, + unsigned: false + }, + { + name: 'PATH', + length: false, + unsigned: false + }, + { + name: 'POLYGON', + length: false, + unsigned: false + }, + { + name: 'CIRCLE', + length: false, + unsigned: false + } + ] + }, + { + group: 'network', + types: [ + { + name: 'CIDR', + length: false, + unsigned: false + }, + { + name: 'INET', + length: false, + unsigned: false + }, + { + name: 'MACADDR', + length: false, + unsigned: false + }, + { + name: 'MACADDR8', + length: false, + unsigned: false + } + ] + }, + { + group: 'bit', + types: [ + { + name: 'BIT', + length: false, + unsigned: false + }, + { + name: 'BIT VARYING', + length: false, + unsigned: false + } + ] + }, + { + group: 'text search', + types: [ + { + name: 'TSVECTOR', + length: false, + unsigned: false + }, + { + name: 'TSQUERY', + length: false, + unsigned: false + } + ] + }, + { + group: 'uuid', + types: [ + { + name: 'UUID', + length: false, + unsigned: false + } + ] + }, + { + group: 'xml', + types: [ + { + name: 'XML', + length: false, + unsigned: false + } + ] + }, + { + group: 'json', + types: [ + { + name: 'JSON', + length: false, + unsigned: false + }, + { + name: 'JSONB', + length: false, + unsigned: false + }, + { + name: 'JSONPATH', + length: false, + unsigned: false + } + ] + } +]; diff --git a/src/common/db-properties/mysql.js b/src/common/db-properties/mysql.js deleted file mode 100644 index e0a30c5d..00000000 --- a/src/common/db-properties/mysql.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = []; diff --git a/src/common/fieldTypes.js b/src/common/fieldTypes.js index 9b32c087..066ecb1b 100644 --- a/src/common/fieldTypes.js +++ b/src/common/fieldTypes.js @@ -1,13 +1,66 @@ -export const TEXT = ['CHAR', 'VARCHAR']; -export const LONG_TEXT = ['TEXT', 'MEDIUMTEXT', 'LONGTEXT']; +export const TEXT = [ + 'CHAR', + 'VARCHAR', + 'CHARACTER', + 'CHARACTER VARYING' +]; +export const LONG_TEXT = [ + 'TEXT', + 'MEDIUMTEXT', + 'LONGTEXT', + 'ARRAY', + 'ANYARRAY' +]; -export const NUMBER = ['INT', 'TINYINT', 'SMALLINT', 'MEDIUMINT', 'BIGINT', 'DECIMAL', 'NEWDECIMAL', 'BOOL']; -export const FLOAT = ['FLOAT', 'DOUBLE']; +export const NUMBER = [ + 'INT', + 'TINYINT', + 'SMALLINT', + 'MEDIUMINT', + 'BIGINT', + 'DECIMAL', + 'NUMERIC', + 'INTEGER', + 'SMALLSERIAL', + 'SERIAL', + 'BIGSERIAL', + 'OID', + 'XID' +]; + +export const FLOAT = [ + 'FLOAT', + 'DOUBLE', + 'REAL', + 'DOUBLE PRECISION', + 'MONEY' +]; + +export const BOOLEAN = [ + 'BOOL', + 'BOOLEAN' +]; export const DATE = ['DATE']; -export const TIME = ['TIME']; -export const DATETIME = ['DATETIME', 'TIMESTAMP']; +export const TIME = [ + 'TIME', + 'TIME WITH TIME ZONE' +]; +export const DATETIME = [ + 'DATETIME', + 'TIMESTAMP', + 'TIMESTAMP WITH TIME ZONE' +]; -export const BLOB = ['BLOB', 'TINYBLOB', 'MEDIUMBLOB', 'LONGBLOB']; +export const BLOB = [ + 'BLOB', + 'TINYBLOB', + 'MEDIUMBLOB', + 'LONGBLOB', + 'BYTEA' +]; -export const BIT = ['BIT']; +export const BIT = [ + 'BIT', + 'BIT VARYING' +]; diff --git a/src/common/index-types/postgresql.js b/src/common/index-types/postgresql.js new file mode 100644 index 00000000..edc2f2a3 --- /dev/null +++ b/src/common/index-types/postgresql.js @@ -0,0 +1,5 @@ +module.exports = [ + 'PRIMARY', + 'INDEX', + 'UNIQUE' +]; diff --git a/src/main/ipc-handlers/connection.js b/src/main/ipc-handlers/connection.js index 8df983a7..77a6de2a 100644 --- a/src/main/ipc-handlers/connection.js +++ b/src/main/ipc-handlers/connection.js @@ -59,13 +59,13 @@ export default connections => { }; } - const connection = ClientsFactory.getConnection({ - client: conn.client, - params, - poolSize: 1 - }); - try { + const connection = ClientsFactory.getConnection({ + client: conn.client, + params, + poolSize: 1 + }); + await connection.connect(); const structure = await connection.getStructure(new Set()); diff --git a/src/main/ipc-handlers/database.js b/src/main/ipc-handlers/database.js index acbe8630..d4a6f0ad 100644 --- a/src/main/ipc-handlers/database.js +++ b/src/main/ipc-handlers/database.js @@ -4,8 +4,7 @@ import { ipcMain } from 'electron'; export default connections => { ipcMain.handle('create-database', async (event, params) => { try { - const query = `CREATE DATABASE \`${params.name}\` COLLATE ${params.collation}`; - await connections[params.uid].raw(query); + await connections[params.uid].createDatabase(params); return { status: 'success' }; } @@ -16,8 +15,7 @@ export default connections => { ipcMain.handle('update-database', async (event, params) => { try { - const query = `ALTER DATABASE \`${params.name}\` COLLATE ${params.collation}`; - await connections[params.uid].raw(query); + await connections[params.uid].alterDatabase(params); return { status: 'success' }; } @@ -28,8 +26,7 @@ export default connections => { ipcMain.handle('delete-database', async (event, params) => { try { - const query = `DROP DATABASE \`${params.database}\``; - await connections[params.uid].raw(query); + await connections[params.uid].dropDatabase(params); return { status: 'success' }; } @@ -38,10 +35,9 @@ export default connections => { } }); - ipcMain.handle('get-database-collation', async (event, params) => { // TODO: move to mysql class + ipcMain.handle('get-database-collation', async (event, params) => { try { - const query = `SELECT \`DEFAULT_COLLATION_NAME\` FROM \`information_schema\`.\`SCHEMATA\` WHERE \`SCHEMA_NAME\`='${params.database}'`; - const collation = await connections[params.uid].raw(query); + const collation = await connections[params.uid].getDatabaseCollation(params); return { status: 'success', response: collation.rows.length ? collation.rows[0].DEFAULT_COLLATION_NAME : '' }; } diff --git a/src/main/ipc-handlers/tables.js b/src/main/ipc-handlers/tables.js index c6e063e7..10f87a83 100644 --- a/src/main/ipc-handlers/tables.js +++ b/src/main/ipc-handlers/tables.js @@ -176,19 +176,19 @@ export default (connections) => { if (params.row[key] === null) escapedParam = 'NULL'; else if ([...NUMBER, ...FLOAT].includes(type)) - escapedParam = params.row[key]; + escapedParam = +params.row[key]; else if ([...TEXT, ...LONG_TEXT].includes(type)) - escapedParam = `"${sqlEscaper(params.row[key])}"`; + escapedParam = `'${sqlEscaper(params.row[key])}'`; else if (BLOB.includes(type)) { if (params.row[key]) { const fileBlob = fs.readFileSync(params.row[key]); escapedParam = `0x${fileBlob.toString('hex')}`; } else - escapedParam = '""'; + escapedParam = '\'\''; } else - escapedParam = `"${sqlEscaper(params.row[key])}"`; + escapedParam = `'${sqlEscaper(params.row[key])}'`; insertObj[key] = escapedParam; } @@ -225,19 +225,19 @@ export default (connections) => { else if ([...NUMBER, ...FLOAT].includes(type)) escapedParam = params.row[key].value; else if ([...TEXT, ...LONG_TEXT].includes(type)) - escapedParam = `"${sqlEscaper(params.row[key].value)}"`; + escapedParam = `'${sqlEscaper(params.row[key].value)}'`; else if (BLOB.includes(type)) { if (params.row[key].value) { const fileBlob = fs.readFileSync(params.row[key].value); escapedParam = `0x${fileBlob.toString('hex')}`; } else - escapedParam = '""'; + escapedParam = '\'\''; } else if (BIT.includes(type)) escapedParam = `b'${sqlEscaper(params.row[key].value)}'`; else - escapedParam = `"${sqlEscaper(params.row[key].value)}"`; + escapedParam = `'${sqlEscaper(params.row[key].value)}'`; insertObj[key] = escapedParam; } @@ -261,10 +261,10 @@ export default (connections) => { if (typeof fakeValue === 'string') { if (params.row[key].length) fakeValue = fakeValue.substr(0, params.row[key].length); - fakeValue = `"${sqlEscaper(fakeValue)}"`; + fakeValue = `'${sqlEscaper(fakeValue)}'`; } else if ([...DATE, ...DATETIME].includes(type)) - fakeValue = `"${moment(fakeValue).format('YYYY-MM-DD HH:mm:ss.SSSSSS')}"`; + fakeValue = `'${moment(fakeValue).format('YYYY-MM-DD HH:mm:ss.SSSSSS')}'`; insertObj[key] = fakeValue; } diff --git a/src/main/libs/ClientsFactory.js b/src/main/libs/ClientsFactory.js index 6b9293c0..cb087382 100644 --- a/src/main/libs/ClientsFactory.js +++ b/src/main/libs/ClientsFactory.js @@ -1,5 +1,6 @@ 'use strict'; import { MySQLClient } from './clients/MySQLClient'; +import { PostgreSQLClient } from './clients/PostgreSQLClient'; export class ClientsFactory { /** @@ -20,8 +21,10 @@ export class ClientsFactory { case 'mysql': case 'maria': return new MySQLClient(args); + case 'pg': + return new PostgreSQLClient(args); default: - return new Error(`Unknown database client: ${args.client}`); + throw new Error(`Unknown database client: ${args.client}`); } } } diff --git a/src/main/libs/clients/MySQLClient.js b/src/main/libs/clients/MySQLClient.js index 5c47ab98..1d791709 100644 --- a/src/main/libs/clients/MySQLClient.js +++ b/src/main/libs/clients/MySQLClient.js @@ -404,6 +404,44 @@ export class MySQLClient extends AntaresCore { }); } + /** + * CREATE DATABASE + * + * @returns {Array.} parameters + * @memberof MySQLClient + */ + async createDatabase (params) { + return await this.raw(`CREATE DATABASE \`${params.name}\` COLLATE ${params.collation}`); + } + + /** + * ALTER DATABASE + * + * @returns {Array.} parameters + * @memberof MySQLClient + */ + async alterDatabase (params) { + return await this.raw(`ALTER DATABASE \`${params.name}\` COLLATE ${params.collation}`); + } + + /** + * DROP DATABASE + * + * @returns {Array.} parameters + * @memberof MySQLClient + */ + async dropDatabase (params) { + return await this.raw(`DROP DATABASE \`${params.database}\``); + } + + /** + * @returns {Array.} parameters + * @memberof MySQLClient + */ + async getDatabaseCollation (params) { + return await this.raw(`SELECT \`DEFAULT_COLLATION_NAME\` FROM \`information_schema\`.\`SCHEMATA\` WHERE \`SCHEMA_NAME\`='${params.database}'`); + } + /** * SHOW CREATE VIEW * @@ -1281,7 +1319,7 @@ export class MySQLClient extends AntaresCore { const response = await this.getTableColumns(paramObj); remappedFields = remappedFields.map(field => { const detailedField = response.find(f => f.name === field.name); - if (detailedField && field.orgTable === paramObj.table && field.schema === paramObj.schema && detailedField.name === field.orgName) + if (detailedField && field.orgTable === paramObj.table && field.schema === paramObj.schema) field = { ...detailedField, ...field }; return field; }); diff --git a/src/main/libs/clients/PostgreSQLClient.js b/src/main/libs/clients/PostgreSQLClient.js new file mode 100644 index 00000000..f8285244 --- /dev/null +++ b/src/main/libs/clients/PostgreSQLClient.js @@ -0,0 +1,1319 @@ +'use strict'; +import { Pool, Client, types } from 'pg'; +import { Parser } from 'node-sql-parser'; +import { AntaresCore } from '../AntaresCore'; +import dataTypes from 'common/data-types/postgresql'; + +export class PostgreSQLClient extends AntaresCore { + constructor (args) { + super(args); + + this._schema = null; + + this.types = {}; + for (const key in types.builtins) + this.types[types.builtins[key]] = key; + } + + _getType (field) { + let name = this.types[field.columnType]; + let length = field.columnLength; + + if (['DATE', 'TIME', 'YEAR', 'DATETIME'].includes(name)) + length = field.decimals; + + if (name === 'TIMESTAMP') + length = 0; + + if (field.charsetNr === 63) { // if binary + if (name === 'CHAR') + name = 'BINARY'; + else if (name === 'VARCHAR') + name = 'VARBINARY'; + } + + if (name === 'BLOB') { + switch (length) { + case 765: + name = 'TYNITEXT'; + break; + case 196605: + name = 'TEXT'; + break; + case 50331645: + name = 'MEDIUMTEXT'; + break; + case 4294967295: + name = field.charsetNr === 63 ? 'LONGBLOB' : 'LONGTEXT'; + break; + case 255: + name = 'TINYBLOB'; + break; + case 65535: + name = 'BLOB'; + break; + case 16777215: + name = 'MEDIUMBLOB'; + break; + default: + name = field.charsetNr === 63 ? 'BLOB' : 'TEXT'; + } + } + + return { name, length }; + } + + _getTypeInfo (type) { + return dataTypes + .reduce((acc, group) => [...acc, ...group.types], []) + .filter(_type => _type.name === type.toUpperCase())[0]; + } + + /** + * @memberof PostgreSQLClient + */ + async connect () { + if (!this._poolSize) { + const client = new Client(this._params); + this._connection = client.connect(); + } + else { + const pool = new Pool({ ...this._params, max: this._poolSize }); + this._connection = pool; + } + } + + /** + * @memberof PostgreSQLClient + */ + destroy () { + this._connection.end(); + } + + /** + * Executes an USE query + * + * @param {String} schema + * @memberof PostgreSQLClient + */ + use (schema) { + this._schema = schema; + return this.raw(`SET search_path TO '${schema}', '$user'`); + } + + /** + * @param {Array} schemas list + * @returns {Array.} databases scructure + * @memberof PostgreSQLClient + */ + async getStructure (schemas) { + const { rows: databases } = await this.raw('SELECT schema_name AS database FROM information_schema.schemata ORDER BY schema_name'); + const { rows: functions } = await this.raw('SELECT * FROM information_schema.routines WHERE routine_type = \'FUNCTION\''); + const { rows: procedures } = await this.raw('SELECT * FROM information_schema.routines WHERE routine_type = \'PROCEDURE\''); + + const tablesArr = []; + const triggersArr = []; + + for (const db of databases) { + if (!schemas.has(db.database)) continue; + + let { rows: tables } = await this.raw(` + SELECT *, + pg_table_size(QUOTE_IDENT(t.TABLE_SCHEMA) || '.' || QUOTE_IDENT(t.TABLE_NAME))::bigint AS data_length, + pg_relation_size(QUOTE_IDENT(t.TABLE_SCHEMA) || '.' || QUOTE_IDENT(t.TABLE_NAME))::bigint AS index_length, + c.reltuples, obj_description(c.oid) AS comment + FROM "information_schema"."tables" AS t + LEFT JOIN "pg_namespace" n ON t.table_schema = n.nspname + LEFT JOIN "pg_class" c ON n.oid = c.relnamespace AND c.relname=t.table_name + WHERE t."table_schema" = '${db.database}' + ORDER BY table_name + `); + + if (tables.length) { + tables = tables.map(table => { + table.Db = db.database; + return table; + }); + tablesArr.push(...tables); + } + + let { rows: triggers } = await this.raw(` + SELECT event_object_schema AS table_schema, + event_object_table AS table_name, + trigger_schema, + trigger_name, + string_agg(event_manipulation, ',') AS event, + action_timing AS activation, + action_condition AS condition, + action_statement AS definition + FROM information_schema.triggers + WHERE trigger_schema = '${db.database}' + GROUP BY 1,2,3,4,6,7,8 + ORDER BY table_schema, + table_name + `); + + if (triggers.length) { + triggers = triggers.map(trigger => { + trigger.Db = db.database; + return trigger; + }); + triggersArr.push(...triggers); + } + } + + return databases.map(db => { + if (schemas.has(db.database)) { + // TABLES + const remappedTables = tablesArr.filter(table => table.Db === db.database).map(table => { + return { + name: table.table_name, + type: table.table_type === 'VIEW' ? 'view' : 'table', + rows: table.reltuples, + size: +table.data_length + +table.index_length, + collation: table.Collation, + comment: table.comment, + engine: '' + }; + }); + + // PROCEDURES + const remappedProcedures = procedures.filter(procedure => procedure.Db === db.database).map(procedure => { + return { + name: procedure.Name, + type: procedure.Type, + definer: procedure.Definer, + created: procedure.Created, + updated: procedure.Modified, + comment: procedure.Comment, + charset: procedure.character_set_client, + security: procedure.Security_type + }; + }); + + // FUNCTIONS + const remappedFunctions = functions.filter(func => func.Db === db.database).map(func => { + return { + name: func.routine_name, + type: func.routine_type, + definer: null, // func.Definer, + created: null, // func.Created, + updated: null, // func.Modified, + comment: null, // func.Comment, + charset: null, // func.character_set_client, + security: func.security_type + }; + }); + // TRIGGERS + const remappedTriggers = triggersArr.filter(trigger => trigger.Db === db.database).map(trigger => { + return { + name: trigger.trigger_name, + timing: trigger.activation, + definer: trigger.definition, // ??? + event: trigger.event, + table: trigger.table_trigger, + sqlMode: trigger.sql_mode + }; + }); + + return { + name: db.database, + tables: remappedTables, + functions: remappedFunctions, + procedures: remappedProcedures, + triggers: remappedTriggers, + schedulers: [] + }; + } + else { + return { + name: db.database, + tables: [], + functions: [], + procedures: [], + triggers: [], + schedulers: [] + }; + } + }); + } + + /** + * @param {Object} params + * @param {String} params.schema + * @param {String} params.table + * @returns {Object} table scructure + * @memberof PostgreSQLClient + */ + async getTableColumns ({ schema, table }) { + const { rows } = await this + .select('*') + .schema('information_schema') + .from('columns') + .where({ table_schema: `= '${schema}'`, table_name: `= '${table}'` }) + .orderBy({ ordinal_position: 'ASC' }) + .run(); + + return rows.map(field => { + return { + name: field.column_name, + key: null, + type: field.data_type.toUpperCase(), + schema: field.table_schema, + table: field.table_name, + numPrecision: field.numeric_precision, + datePrecision: field.datetime_precision, + charLength: field.character_maximum_length, + nullable: field.is_nullable.includes('YES'), + unsigned: null, + zerofill: null, + order: field.ordinal_position, + default: field.column_default, + charset: field.character_set_name, + collation: field.collation_name, + autoIncrement: null, + onUpdate: null, + comment: '' + }; + }); + } + + /** + * @param {Object} params + * @param {String} params.schema + * @param {String} params.table + * @returns {Object} table indexes + * @memberof PostgreSQLClient + */ + async getTableIndexes ({ schema, table }) { + const { rows } = await this.raw(`WITH ndx_list AS ( + SELECT pg_index.indexrelid, pg_class.oid + FROM pg_index, pg_class + WHERE pg_class.relname = '${table}' AND pg_class.oid = pg_index.indrelid), ndx_cols AS ( + SELECT pg_class.relname, UNNEST(i.indkey) AS col_ndx, CASE i.indisprimary WHEN TRUE THEN 'PRIMARY' ELSE CASE i.indisunique WHEN TRUE THEN 'UNIQUE' ELSE 'KEY' END END AS CONSTRAINT_TYPE, pg_class.oid + FROM pg_class + JOIN pg_index i ON (pg_class.oid = i.indexrelid) + JOIN ndx_list ON (pg_class.oid = ndx_list.indexrelid) + WHERE pg_table_is_visible(pg_class.oid)) + SELECT ndx_cols.relname AS CONSTRAINT_NAME, ndx_cols.CONSTRAINT_TYPE, a.attname AS COLUMN_NAME + FROM pg_attribute a + JOIN ndx_cols ON (a.attnum = ndx_cols.col_ndx) + JOIN ndx_list ON (ndx_list.oid = a.attrelid AND ndx_list.indexrelid = ndx_cols.oid) + `); + + return rows.map(row => { + return { + name: row.constraint_name, + column: row.column_name, + indexType: null, + type: row.constraint_type + }; + }); + } + + /** + * @param {Object} params + * @param {String} params.schema + * @param {String} params.table + * @returns {Object} table key usage + * @memberof PostgreSQLClient + */ + async getKeyUsage ({ schema, table }) { + const { rows } = await this + .select('*') + .schema('information_schema') + .from('key_column_usage') + .where({ TABLE_SCHEMA: `= '${schema}'`, TABLE_NAME: `= '${table}'` }) + .run(); + + const { rows: extras } = await this + .select('*') + .schema('information_schema') + .from('referential_constraints') + .where({ constraint_schema: `= '${schema}'`, constraint_name: `= '${table}'` }) + .run(); + + return rows.map(field => { + const extra = extras.find(x => x.CONSTRAINT_NAME === field.CONSTRAINT_NAME); + return { + schema: field.TABLE_SCHEMA, + table: field.TABLE_NAME, + field: field.COLUMN_NAME, + position: field.ORDINAL_POSITION, + constraintPosition: field.POSITION_IN_UNIQUE_CONSTRAINT, + constraintName: field.CONSTRAINT_NAME, + refSchema: field.REFERENCED_TABLE_SCHEMA, + refTable: field.REFERENCED_TABLE_NAME, + refField: field.REFERENCED_COLUMN_NAME, + onUpdate: extra ? extra.UPDATE_RULE : '', + onDelete: extra ? extra.DELETE_RULE : '' + }; + }); + } + + /** + * SELECT * FROM pg_catalog.pg_user + * + * @returns {Array.} users list + * @memberof PostgreSQLClient + */ + async getUsers () { + const { rows } = await this.raw('SELECT * FROM pg_catalog.pg_user'); + + return rows.map(row => { + return { + name: row.username, + host: row.host, + password: row.passwd + }; + }); + } + + /** + * CREATE SCHEMA + * + * @returns {Array.} parameters + * @memberof MySQLClient + */ + async createDatabase (params) { + return await this.raw(`CREATE SCHEMA "${params.name}"`); + } + + /** + * ALTER DATABASE + * + * @returns {Array.} parameters + * @memberof MySQLClient + */ + async alterDatabase (params) { + return await this.raw(`ALTER SCHEMA "${params.name}"`); + } + + /** + * DROP DATABASE + * + * @returns {Array.} parameters + * @memberof MySQLClient + */ + async dropDatabase (params) { + return await this.raw(`DROP SCHEMA "${params.database}"`); + } + + /** + * SHOW CREATE VIEW + * + * @returns {Array.} view informations + * @memberof PostgreSQLClient + */ + async getViewInformations ({ schema, view }) { + const sql = `SHOW CREATE VIEW \`${schema}\`.\`${view}\``; + const results = await this.raw(sql); + + return results.rows.map(row => { + return { + algorithm: row['Create View'].match(/(?<=CREATE ALGORITHM=).*?(?=\s)/gs)[0], + definer: row['Create View'].match(/(?<=DEFINER=).*?(?=\s)/gs)[0], + security: row['Create View'].match(/(?<=SQL SECURITY ).*?(?=\s)/gs)[0], + updateOption: row['Create View'].match(/(?<=WITH ).*?(?=\s)/gs) ? row['Create View'].match(/(?<=WITH ).*?(?=\s)/gs)[0] : '', + sql: row['Create View'].match(/(?<=AS ).*?$/gs)[0], + name: row.View + }; + })[0]; + } + + /** + * DROP VIEW + * + * @returns {Array.} parameters + * @memberof PostgreSQLClient + */ + async dropView (params) { + const sql = `DROP VIEW \`${params.view}\``; + return await this.raw(sql); + } + + /** + * ALTER VIEW + * + * @returns {Array.} parameters + * @memberof PostgreSQLClient + */ + async alterView (params) { + const { view } = params; + let sql = `ALTER ALGORITHM = ${view.algorithm}${view.definer ? ` DEFINER=${view.definer}` : ''} SQL SECURITY ${view.security} VIEW \`${view.oldName}\` AS ${view.sql} ${view.updateOption ? `WITH ${view.updateOption} CHECK OPTION` : ''}`; + + if (view.name !== view.oldName) + sql += `; RENAME TABLE \`${view.oldName}\` TO \`${view.name}\``; + + return await this.raw(sql); + } + + /** + * CREATE VIEW + * + * @returns {Array.} parameters + * @memberof PostgreSQLClient + */ + async createView (view) { + const sql = `CREATE ALGORITHM = ${view.algorithm} ${view.definer ? `DEFINER=${view.definer} ` : ''}SQL SECURITY ${view.security} VIEW \`${view.name}\` AS ${view.sql} ${view.updateOption ? `WITH ${view.updateOption} CHECK OPTION` : ''}`; + return await this.raw(sql); + } + + /** + * SHOW CREATE TRIGGER + * + * @returns {Array.} view informations + * @memberof PostgreSQLClient + */ + async getTriggerInformations ({ schema, trigger }) { + const sql = `SHOW CREATE TRIGGER \`${schema}\`.\`${trigger}\``; + const results = await this.raw(sql); + + return results.rows.map(row => { + return { + definer: row['SQL Original Statement'].match(/(?<=DEFINER=).*?(?=\s)/gs)[0], + sql: row['SQL Original Statement'].match(/(BEGIN|begin)(.*)(END|end)/gs)[0], + name: row.Trigger, + table: row['SQL Original Statement'].match(/(?<=ON `).*?(?=`)/gs)[0], + event1: row['SQL Original Statement'].match(/(BEFORE|AFTER)/gs)[0], + event2: row['SQL Original Statement'].match(/(INSERT|UPDATE|DELETE)/gs)[0] + }; + })[0]; + } + + /** + * DROP TRIGGER + * + * @returns {Array.} parameters + * @memberof PostgreSQLClient + */ + async dropTrigger (params) { + const sql = `DROP TRIGGER \`${params.trigger}\``; + return await this.raw(sql); + } + + /** + * ALTER TRIGGER + * + * @returns {Array.} parameters + * @memberof PostgreSQLClient + */ + async alterTrigger (params) { + const { trigger } = params; + const tempTrigger = Object.assign({}, trigger); + tempTrigger.name = `Antares_${tempTrigger.name}_tmp`; + + try { + await this.createTrigger(tempTrigger); + await this.dropTrigger({ trigger: tempTrigger.name }); + await this.dropTrigger({ trigger: trigger.oldName }); + await this.createTrigger(trigger); + } + catch (err) { + return Promise.reject(err); + } + } + + /** + * CREATE TRIGGER + * + * @returns {Array.} parameters + * @memberof PostgreSQLClient + */ + async createTrigger (trigger) { + const sql = `CREATE ${trigger.definer ? `DEFINER=${trigger.definer} ` : ''}TRIGGER \`${trigger.name}\` ${trigger.event1} ${trigger.event2} ON \`${trigger.table}\` FOR EACH ROW ${trigger.sql}`; + return await this.raw(sql, { split: false }); + } + + /** + * SHOW CREATE PROCEDURE + * + * @returns {Array.} view informations + * @memberof PostgreSQLClient + */ + async getRoutineInformations ({ schema, routine }) { + const sql = `SHOW CREATE PROCEDURE \`${schema}\`.\`${routine}\``; + const results = await this.raw(sql); + + return results.rows.map(row => { + if (!row['Create Procedure']) { + return { + definer: null, + sql: '', + parameters: [], + name: row.Procedure, + comment: '', + security: 'DEFINER', + deterministic: false, + dataAccess: 'CONTAINS SQL' + }; + } + + const parameters = row['Create Procedure'] + .match(/(\([^()]*(?:(?:\([^()]*\))[^()]*)*\)\s*)/s)[0] + .replaceAll('\r', '') + .replaceAll('\t', '') + .slice(1, -1) + .split(',') + .map(el => { + const param = el.split(' '); + const type = param[2] ? param[2].replace(')', '').split('(') : ['', null]; + return { + name: param[1] ? param[1].replaceAll('`', '') : '', + type: type[0].replaceAll('\n', ''), + length: +type[1] ? +type[1].replace(/\D/g, '') : '', + context: param[0] ? param[0].replace('\n', '') : '' + }; + }).filter(el => el.name); + + let dataAccess = 'CONTAINS SQL'; + if (row['Create Procedure'].includes('NO SQL')) + dataAccess = 'NO SQL'; + if (row['Create Procedure'].includes('READS SQL DATA')) + dataAccess = 'READS SQL DATA'; + if (row['Create Procedure'].includes('MODIFIES SQL DATA')) + dataAccess = 'MODIFIES SQL DATA'; + + return { + definer: row['Create Procedure'].match(/(?<=DEFINER=).*?(?=\s)/gs)[0], + sql: row['Create Procedure'].match(/(BEGIN|begin)(.*)(END|end)/gs)[0], + parameters: parameters || [], + name: row.Procedure, + comment: row['Create Procedure'].match(/(?<=COMMENT ').*?(?=')/gs) ? row['Create Procedure'].match(/(?<=COMMENT ').*?(?=')/gs)[0] : '', + security: row['Create Procedure'].includes('SQL SECURITY INVOKER') ? 'INVOKER' : 'DEFINER', + deterministic: row['Create Procedure'].includes('DETERMINISTIC'), + dataAccess + }; + })[0]; + } + + /** + * DROP PROCEDURE + * + * @returns {Array.} parameters + * @memberof PostgreSQLClient + */ + async dropRoutine (params) { + const sql = `DROP PROCEDURE \`${params.routine}\``; + return await this.raw(sql); + } + + /** + * ALTER PROCEDURE + * + * @returns {Array.} parameters + * @memberof PostgreSQLClient + */ + async alterRoutine (params) { + const { routine } = params; + const tempProcedure = Object.assign({}, routine); + tempProcedure.name = `Antares_${tempProcedure.name}_tmp`; + + try { + await this.createRoutine(tempProcedure); + await this.dropRoutine({ routine: tempProcedure.name }); + await this.dropRoutine({ routine: routine.oldName }); + await this.createRoutine(routine); + } + catch (err) { + return Promise.reject(err); + } + } + + /** + * CREATE PROCEDURE + * + * @returns {Array.} parameters + * @memberof PostgreSQLClient + */ + async createRoutine (routine) { + const parameters = 'parameters' in routine + ? routine.parameters.reduce((acc, curr) => { + acc.push(`${curr.context} \`${curr.name}\` ${curr.type}${curr.length ? `(${curr.length})` : ''}`); + return acc; + }, []).join(',') + : ''; + + const sql = `CREATE ${routine.definer ? `DEFINER=${routine.definer} ` : ''}PROCEDURE \`${routine.name}\`(${parameters}) + LANGUAGE SQL + ${routine.deterministic ? 'DETERMINISTIC' : 'NOT DETERMINISTIC'} + ${routine.dataAccess} + SQL SECURITY ${routine.security} + COMMENT '${routine.comment}' + ${routine.sql}`; + + return await this.raw(sql, { split: false }); + } + + /** + * SHOW CREATE FUNCTION + * + * @returns {Array.} view informations + * @memberof PostgreSQLClient + */ + async getFunctionInformations ({ schema, func }) { + const sql = `SHOW CREATE FUNCTION \`${schema}\`.\`${func}\``; + const results = await this.raw(sql); + + return results.rows.map(row => { + if (!row['Create Function']) { + return { + definer: null, + sql: '', + parameters: [], + name: row.Procedure, + comment: '', + security: 'DEFINER', + deterministic: false, + dataAccess: 'CONTAINS SQL', + returns: 'INT', + returnsLength: null + }; + } + + const parameters = row['Create Function'] + .match(/(\([^()]*(?:(?:\([^()]*\))[^()]*)*\)\s*)/s)[0] + .replaceAll('\r', '') + .replaceAll('\t', '') + .slice(1, -1) + .split(',') + .map(el => { + const param = el.split(' '); + const type = param[1] ? param[1].replace(')', '').split('(') : ['', null]; + + return { + name: param[0] ? param[0].replaceAll('`', '') : '', + type: type[0], + length: +type[1] ? +type[1].replace(/\D/g, '') : '' + }; + }).filter(el => el.name); + + let dataAccess = 'CONTAINS SQL'; + if (row['Create Function'].includes('NO SQL')) + dataAccess = 'NO SQL'; + if (row['Create Function'].includes('READS SQL DATA')) + dataAccess = 'READS SQL DATA'; + if (row['Create Function'].includes('MODIFIES SQL DATA')) + dataAccess = 'MODIFIES SQL DATA'; + + const output = row['Create Function'].match(/(?<=RETURNS ).*?(?=\s)/gs).length ? row['Create Function'].match(/(?<=RETURNS ).*?(?=\s)/gs)[0].replace(')', '').split('(') : ['', null]; + + return { + definer: row['Create Function'].match(/(?<=DEFINER=).*?(?=\s)/gs)[0], + sql: row['Create Function'].match(/(BEGIN|begin)(.*)(END|end)/gs)[0], + parameters: parameters || [], + name: row.Function, + comment: row['Create Function'].match(/(?<=COMMENT ').*?(?=')/gs) ? row['Create Function'].match(/(?<=COMMENT ').*?(?=')/gs)[0] : '', + security: row['Create Function'].includes('SQL SECURITY INVOKER') ? 'INVOKER' : 'DEFINER', + deterministic: row['Create Function'].includes('DETERMINISTIC'), + dataAccess, + returns: output[0].toUpperCase(), + returnsLength: +output[1] + }; + })[0]; + } + + /** + * DROP FUNCTION + * + * @returns {Array.} parameters + * @memberof PostgreSQLClient + */ + async dropFunction (params) { + const sql = `DROP FUNCTION \`${params.func}\``; + return await this.raw(sql); + } + + /** + * ALTER FUNCTION + * + * @returns {Array.} parameters + * @memberof PostgreSQLClient + */ + async alterFunction (params) { + const { func } = params; + const tempProcedure = Object.assign({}, func); + tempProcedure.name = `Antares_${tempProcedure.name}_tmp`; + + try { + await this.createFunction(tempProcedure); + await this.dropFunction({ func: tempProcedure.name }); + await this.dropFunction({ func: func.oldName }); + await this.createFunction(func); + } + catch (err) { + return Promise.reject(err); + } + } + + /** + * CREATE FUNCTION + * + * @returns {Array.} parameters + * @memberof PostgreSQLClient + */ + async createFunction (func) { + const parameters = func.parameters.reduce((acc, curr) => { + acc.push(`\`${curr.name}\` ${curr.type}${curr.length ? `(${curr.length})` : ''}`); + return acc; + }, []).join(','); + + const sql = `CREATE ${func.definer ? `DEFINER=${func.definer} ` : ''}FUNCTION \`${func.name}\`(${parameters}) RETURNS ${func.returns}${func.returnsLength ? `(${func.returnsLength})` : ''} + LANGUAGE SQL + ${func.deterministic ? 'DETERMINISTIC' : 'NOT DETERMINISTIC'} + ${func.dataAccess} + SQL SECURITY ${func.security} + COMMENT '${func.comment}' + ${func.sql}`; + + return await this.raw(sql, { split: false }); + } + + /** + * SHOW CREATE EVENT + * + * @returns {Array.} view informations + * @memberof PostgreSQLClient + */ + async getEventInformations ({ schema, scheduler }) { + const sql = `SHOW CREATE EVENT \`${schema}\`.\`${scheduler}\``; + const results = await this.raw(sql); + + return results.rows.map(row => { + const schedule = row['Create Event']; + const execution = schedule.includes('EVERY') ? 'EVERY' : 'ONCE'; + const every = execution === 'EVERY' ? row['Create Event'].match(/(?<=EVERY )(\s*([^\s]+)){0,2}/gs)[0].replaceAll('\'', '').split(' ') : []; + const starts = execution === 'EVERY' && schedule.includes('STARTS') ? schedule.match(/(?<=STARTS ').*?(?='\s)/gs)[0] : ''; + const ends = execution === 'EVERY' && schedule.includes('ENDS') ? schedule.match(/(?<=ENDS ').*?(?='\s)/gs)[0] : ''; + const at = execution === 'ONCE' && schedule.includes('AT') ? schedule.match(/(?<=AT ').*?(?='\s)/gs)[0] : ''; + + return { + definer: row['Create Event'].match(/(?<=DEFINER=).*?(?=\s)/gs)[0], + sql: row['Create Event'].match(/(?<=DO )(.*)/gs)[0], + name: row.Event, + comment: row['Create Event'].match(/(?<=COMMENT ').*?(?=')/gs) ? row['Create Event'].match(/(?<=COMMENT ').*?(?=')/gs)[0] : '', + state: row['Create Event'].includes('ENABLE') ? 'ENABLE' : row['Create Event'].includes('DISABLE ON SLAVE') ? 'DISABLE ON SLAVE' : 'DISABLE', + preserve: row['Create Event'].includes('ON COMPLETION PRESERVE'), + execution, + every, + starts, + ends, + at + }; + })[0]; + } + + /** + * DROP EVENT + * + * @returns {Array.} parameters + * @memberof PostgreSQLClient + */ + async dropEvent (params) { + const sql = `DROP EVENT \`${params.scheduler}\``; + return await this.raw(sql); + } + + /** + * ALTER EVENT + * + * @returns {Array.} parameters + * @memberof PostgreSQLClient + */ + async alterEvent (params) { + const { scheduler } = params; + + if (scheduler.execution === 'EVERY' && scheduler.every[0].includes('-')) + scheduler.every[0] = `'${scheduler.every[0]}'`; + + const sql = `ALTER ${scheduler.definer ? ` DEFINER=${scheduler.definer}` : ''} EVENT \`${scheduler.oldName}\` + ON SCHEDULE + ${scheduler.execution === 'EVERY' + ? `EVERY ${scheduler.every.join(' ')}${scheduler.starts ? ` STARTS '${scheduler.starts}'` : ''}${scheduler.ends ? ` ENDS '${scheduler.ends}'` : ''}` + : `AT '${scheduler.at}'`} + ON COMPLETION${!scheduler.preserve ? ' NOT' : ''} PRESERVE + ${scheduler.name !== scheduler.oldName ? `RENAME TO \`${scheduler.name}\`` : ''} + ${scheduler.state} + COMMENT '${scheduler.comment}' + DO ${scheduler.sql}`; + + return await this.raw(sql, { split: false }); + } + + /** + * CREATE EVENT + * + * @returns {Array.} parameters + * @memberof PostgreSQLClient + */ + async createEvent (scheduler) { + const sql = `CREATE ${scheduler.definer ? ` DEFINER=${scheduler.definer}` : ''} EVENT \`${scheduler.name}\` + ON SCHEDULE + ${scheduler.execution === 'EVERY' + ? `EVERY ${scheduler.every.join(' ')}${scheduler.starts ? ` STARTS '${scheduler.starts}'` : ''}${scheduler.ends ? ` ENDS '${scheduler.ends}'` : ''}` + : `AT '${scheduler.at}'`} + ON COMPLETION${!scheduler.preserve ? ' NOT' : ''} PRESERVE + ${scheduler.state} + COMMENT '${scheduler.comment}' + DO ${scheduler.sql}`; + + return await this.raw(sql, { split: false }); + } + + /** + * SELECT * FROM pg_collation + * + * @returns {Array.} collations list + * @memberof PostgreSQLClient + */ + async getCollations () { + return []; + } + + /** + * SHOW ALL + * + * @returns {Array.} variables list + * @memberof PostgreSQLClient + */ + async getVariables () { + const sql = 'SHOW ALL'; + const results = await this.raw(sql); + + return results.rows.map(row => { + return { + name: row.name, + value: row.setting + }; + }); + } + + /** + * SHOW ENGINES + * + * @returns {Array.} engines list + * @memberof PostgreSQLClient + */ + async getEngines () { + return { + name: 'PostgreSQL', + support: 'YES', + comment: '', + isDefault: true + }; + } + + /** + * SHOW VARIABLES LIKE '%vers%' + * + * @returns {Array.} version parameters + * @memberof PostgreSQLClient + */ + async getVersion () { + const sql = 'SELECT version()'; + const { rows } = await this.raw(sql); + const infos = rows[0].version.split(','); + + return { + number: infos[0].split(' ')[1], + name: infos[0].split(' ')[0], + arch: infos[1], + os: infos[2] + }; + // return rows.reduce((acc, curr) => { + // switch (curr.Variable_name) { + // case 'version': + // acc.number = curr.Value.split('-')[0]; + // break; + // case 'version_comment': + // acc.name = curr.Value.replace('(GPL)', ''); + // break; + // case 'version_compile_machine': + // acc.arch = curr.Value; + // break; + // case 'version_compile_os': + // acc.os = curr.Value; + // break; + // } + // return acc; + // }, {}); + } + + async getProcesses () { + const sql = 'SELECT "pid", "usename", "client_addr", "datname", application_name , EXTRACT(EPOCH FROM CURRENT_TIMESTAMP - "query_start")::INTEGER, "state", "query" FROM "pg_stat_activity"'; + + const { rows } = await this.raw(sql); + + return rows.map(row => { + return { + id: row.pid, + user: row.usename, + host: row.client_addr, + database: row.datname, + application: row.application_name, + time: row.date_part, + state: row.state, + info: row.query + }; + }); + } + + /** + * CREATE TABLE + * + * @returns {Array.} parameters + * @memberof PostgreSQLClient + */ + async createTable (params) { + const { + name, + collation, + comment, + engine + } = params; + + const sql = `CREATE TABLE \`${name}\` (\`${name}_ID\` INT NULL) COMMENT='${comment}', COLLATE='${collation}', ENGINE=${engine}`; + + return await this.raw(sql); + } + + /** + * ALTER TABLE + * + * @returns {Array.} parameters + * @memberof PostgreSQLClient + */ + async alterTable (params) { + const { + table, + additions, + deletions, + changes, + indexChanges, + foreignChanges, + options + } = params; + + let sql = `ALTER TABLE \`${table}\` `; + const alterColumns = []; + + // OPTIONS + if ('comment' in options) alterColumns.push(`COMMENT='${options.comment}'`); + if ('engine' in options) alterColumns.push(`ENGINE=${options.engine}`); + if ('autoIncrement' in options) alterColumns.push(`AUTO_INCREMENT=${+options.autoIncrement}`); + if ('collation' in options) alterColumns.push(`COLLATE='${options.collation}'`); + + // ADD FIELDS + additions.forEach(addition => { + const typeInfo = this._getTypeInfo(addition.type); + const length = typeInfo.length ? addition.numLength || addition.charLength || addition.datePrecision : false; + + alterColumns.push(`ADD COLUMN \`${addition.name}\` + ${addition.type.toUpperCase()}${length ? `(${length})` : ''} + ${addition.unsigned ? 'UNSIGNED' : ''} + ${addition.zerofill ? 'ZEROFILL' : ''} + ${addition.nullable ? 'NULL' : 'NOT NULL'} + ${addition.autoIncrement ? 'AUTO_INCREMENT' : ''} + ${addition.default ? `DEFAULT ${addition.default}` : ''} + ${addition.comment ? `COMMENT '${addition.comment}'` : ''} + ${addition.collation ? `COLLATE ${addition.collation}` : ''} + ${addition.onUpdate ? `ON UPDATE ${addition.onUpdate}` : ''} + ${addition.after ? `AFTER \`${addition.after}\`` : 'FIRST'}`); + }); + + // ADD INDEX + indexChanges.additions.forEach(addition => { + const fields = addition.fields.map(field => `\`${field}\``).join(','); + let type = addition.type; + + if (type === 'PRIMARY') + alterColumns.push(`ADD PRIMARY KEY (${fields})`); + else { + if (type === 'UNIQUE') + type = 'UNIQUE INDEX'; + + alterColumns.push(`ADD ${type} \`${addition.name}\` (${fields})`); + } + }); + + // ADD FOREIGN KEYS + foreignChanges.additions.forEach(addition => { + alterColumns.push(`ADD CONSTRAINT \`${addition.constraintName}\` FOREIGN KEY (\`${addition.field}\`) REFERENCES \`${addition.refTable}\` (\`${addition.refField}\`) ON UPDATE ${addition.onUpdate} ON DELETE ${addition.onDelete}`); + }); + + // CHANGE FIELDS + changes.forEach(change => { + const typeInfo = this._getTypeInfo(change.type); + const length = typeInfo.length ? change.numLength || change.charLength || change.datePrecision : false; + + alterColumns.push(`CHANGE COLUMN \`${change.orgName}\` \`${change.name}\` + ${change.type.toUpperCase()}${length ? `(${length})` : ''} + ${change.unsigned ? 'UNSIGNED' : ''} + ${change.zerofill ? 'ZEROFILL' : ''} + ${change.nullable ? 'NULL' : 'NOT NULL'} + ${change.autoIncrement ? 'AUTO_INCREMENT' : ''} + ${change.default ? `DEFAULT ${change.default}` : ''} + ${change.comment ? `COMMENT '${change.comment}'` : ''} + ${change.collation ? `COLLATE ${change.collation}` : ''} + ${change.onUpdate ? `ON UPDATE ${change.onUpdate}` : ''} + ${change.after ? `AFTER \`${change.after}\`` : 'FIRST'}`); + }); + + // CHANGE INDEX + indexChanges.changes.forEach(change => { + if (change.oldType === 'PRIMARY') + alterColumns.push('DROP PRIMARY KEY'); + else + alterColumns.push(`DROP INDEX \`${change.oldName}\``); + + const fields = change.fields.map(field => `\`${field}\``).join(','); + let type = change.type; + + if (type === 'PRIMARY') + alterColumns.push(`ADD PRIMARY KEY (${fields})`); + else { + if (type === 'UNIQUE') + type = 'UNIQUE INDEX'; + + alterColumns.push(`ADD ${type} \`${change.name}\` (${fields})`); + } + }); + + // CHANGE FOREIGN KEYS + foreignChanges.changes.forEach(change => { + alterColumns.push(`DROP FOREIGN KEY \`${change.oldName}\``); + alterColumns.push(`ADD CONSTRAINT \`${change.constraintName}\` FOREIGN KEY (\`${change.field}\`) REFERENCES \`${change.refTable}\` (\`${change.refField}\`) ON UPDATE ${change.onUpdate} ON DELETE ${change.onDelete}`); + }); + + // DROP FIELDS + deletions.forEach(deletion => { + alterColumns.push(`DROP COLUMN \`${deletion.name}\``); + }); + + // DROP INDEX + indexChanges.deletions.forEach(deletion => { + if (deletion.type === 'PRIMARY') + alterColumns.push('DROP PRIMARY KEY'); + else + alterColumns.push(`DROP INDEX \`${deletion.name}\``); + }); + + // DROP FOREIGN KEYS + foreignChanges.deletions.forEach(deletion => { + alterColumns.push(`DROP FOREIGN KEY \`${deletion.constraintName}\``); + }); + + sql += alterColumns.join(', '); + + // RENAME + if (options.name) sql += `; RENAME TABLE \`${table}\` TO \`${options.name}\``; + + return await this.raw(sql); + } + + /** + * TRUNCATE TABLE + * + * @returns {Array.} parameters + * @memberof PostgreSQLClient + */ + async truncateTable (params) { + const sql = `TRUNCATE TABLE ${params.table}`; + return await this.raw(sql); + } + + /** + * DROP TABLE + * + * @returns {Array.} parameters + * @memberof PostgreSQLClient + */ + async dropTable (params) { + const sql = `DROP TABLE ${params.table}`; + return await this.raw(sql); + } + + /** + * @returns {String} SQL string + * @memberof PostgreSQLClient + */ + getSQL () { + // SELECT + const selectArray = this._query.select.reduce(this._reducer, []); + let selectRaw = ''; + + if (selectArray.length) + selectRaw = selectArray.length ? `SELECT ${selectArray.join(', ')} ` : 'SELECT * '; + + // FROM + let fromRaw = ''; + + if (!this._query.update.length && !Object.keys(this._query.insert).length && !!this._query.from) + fromRaw = 'FROM'; + else if (Object.keys(this._query.insert).length) + fromRaw = 'INTO'; + + fromRaw += this._query.from ? ` ${this._query.schema ? `${this._query.schema}.` : ''}${this._query.from} ` : ''; + + // WHERE + const whereArray = this._query.where.reduce(this._reducer, []); + const whereRaw = whereArray.length ? `WHERE ${whereArray.join(' AND ')} ` : ''; + + // UPDATE + const updateArray = this._query.update.reduce(this._reducer, []); + const updateRaw = updateArray.length ? `SET ${updateArray.join(', ')} ` : ''; + + // INSERT + let insertRaw = ''; + + if (this._query.insert.length) { + const fieldsList = Object.keys(this._query.insert[0]).map(f => `"${f}"`); + const rowsList = this._query.insert.map(el => `(${Object.values(el).join(', ')})`); + + insertRaw = `(${fieldsList.join(', ')}) VALUES ${rowsList.join(', ')} `; + } + + // GROUP BY + const groupByArray = this._query.groupBy.reduce(this._reducer, []); + const groupByRaw = groupByArray.length ? `GROUP BY ${groupByArray.join(', ')} ` : ''; + + // ORDER BY + const orderByArray = this._query.orderBy.reduce(this._reducer, []); + const orderByRaw = orderByArray.length ? `ORDER BY ${orderByArray.join(', ')} ` : ''; + + // LIMIT + const limitRaw = this._query.limit.length ? `LIMIT ${this._query.limit.join(', ')} ` : ''; + + return `${selectRaw}${updateRaw ? 'UPDATE' : ''}${insertRaw ? 'INSERT ' : ''}${this._query.delete ? 'DELETE ' : ''}${fromRaw}${updateRaw}${whereRaw}${groupByRaw}${orderByRaw}${limitRaw}${insertRaw}`; + } + + /** + * @param {string} sql raw SQL query + * @param {object} args + * @param {boolean} args.nest + * @param {boolean} args.details + * @param {boolean} args.split + * @returns {Promise} + * @memberof PostgreSQLClient + */ + async raw (sql, args) { + args = { + nest: false, + details: false, + split: true, + ...args + }; + const resultsArr = []; + let paramsArr = []; + const queries = args.split ? sql.split(';') : [sql]; + + if (process.env.NODE_ENV === 'development') this._logger(sql);// TODO: replace BLOB content with a placeholder + + for (const query of queries) { + if (!query) continue; + + const timeStart = new Date(); + let timeStop; + let keysArr = []; + + const { rows, report, fields, keys, duration } = await new Promise((resolve, reject) => { + this._connection.query({ text: query }, async (err, res) => { + timeStop = new Date(); + + if (err) + reject(err); + else { + const { rows, fields } = res; + const queryResult = rows; + const parser = new Parser(); + let ast; + try { + ast = parser.astify(query); + } + catch (err) { + + } + + let remappedFields = fields + ? fields.map(field => { + if (!field || Array.isArray(field)) + return false; + + return { + name: field.name, + alias: field.name, + schema: ast && ast.from ? ast.from[0].db : this._schema, + table: ast && ast.from ? ast.from[0].table : null, + tableAlias: ast && ast.from ? ast.from[0].as : null, + orgTable: ast && ast.from ? ast.from[0].table : null, + type: this.types[field.dataTypeID], + length: null// type.length + }; + }).filter(Boolean) + : []; + + if (args.details) { + let cachedTable; + + if (remappedFields.length) { + paramsArr = remappedFields.map(field => { + if (field.table) cachedTable = field.table;// Needed for some queries on information_schema + return { + table: field.table || cachedTable, + schema: field.schema || 'INFORMATION_SCHEMA' + }; + }).filter((val, i, arr) => arr.findIndex(el => el.schema === val.schema && el.table === val.table) === i); + + for (const paramObj of paramsArr) { + if (!paramObj.table || !paramObj.schema) continue; + + try { // Column details + const columns = await this.getTableColumns(paramObj); + const indexes = await this.getTableIndexes(paramObj); + + remappedFields = remappedFields.map(field => { + const detailedField = columns.find(f => f.name === field.name); + const fieldIndex = indexes.find(i => i.column === field.name); + if (field.table === paramObj.table && field.schema === paramObj.schema) { + if (detailedField) field = { ...field, ...detailedField }; + if (fieldIndex) { + const key = fieldIndex.type === 'PRIMARY' ? 'pri' : fieldIndex.type === 'UNIQUE' ? 'uni' : 'mul'; + field = { ...field, key }; + }; + } + + return field; + }); + } + catch (err) { + reject(err); + } + + try { // Key usage (foreign keys) + const response = await this.getKeyUsage(paramObj); + keysArr = keysArr ? [...keysArr, ...response] : response; + } + catch (err) { + reject(err); + } + } + } + } + + resolve({ + duration: timeStop - timeStart, + rows: Array.isArray(queryResult) ? queryResult.some(el => Array.isArray(el)) ? [] : queryResult : false, + report: !Array.isArray(queryResult) ? queryResult : false, + fields: remappedFields, + keys: keysArr + }); + } + }); + }); + + resultsArr.push({ rows, report, fields, keys, duration }); + } + + return resultsArr.length === 1 ? resultsArr[0] : resultsArr; + } +} diff --git a/src/renderer/components/ModalAskParameters.vue b/src/renderer/components/ModalAskParameters.vue index 3ca76c5d..108138dd 100644 --- a/src/renderer/components/ModalAskParameters.vue +++ b/src/renderer/components/ModalAskParameters.vue @@ -30,7 +30,7 @@ class="form-input" type="text" > - + {{ parameter.type }} {{ parameter.length | wrapNumber }} @@ -75,6 +75,11 @@ export default { window.removeEventListener('keydown', this.onKey); }, methods: { + typeClass (type) { + if (type) + return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`; + return ''; + }, runRoutine () { const valArr = Object.keys(this.values).reduce((acc, curr) => { const value = isNaN(this.values[curr]) ? `"${this.values[curr]}"` : this.values[curr]; diff --git a/src/renderer/components/ModalEditConnection.vue b/src/renderer/components/ModalEditConnection.vue index 59af5d2f..f431cf48 100644 --- a/src/renderer/components/ModalEditConnection.vue +++ b/src/renderer/components/ModalEditConnection.vue @@ -59,12 +59,12 @@ + diff --git a/src/renderer/components/ModalEditDatabase.vue b/src/renderer/components/ModalEditDatabase.vue index 2adb103d..8f12ba75 100644 --- a/src/renderer/components/ModalEditDatabase.vue +++ b/src/renderer/components/ModalEditDatabase.vue @@ -5,7 +5,7 @@ diff --git a/src/renderer/components/ModalFakerRows.vue b/src/renderer/components/ModalFakerRows.vue index b5df15ee..0e14f800 100644 --- a/src/renderer/components/ModalFakerRows.vue +++ b/src/renderer/components/ModalFakerRows.vue @@ -34,7 +34,7 @@ :field-obj="localRow[field.name]" :value.sync="localRow[field.name]" > - + {{ field.type }} {{ fieldLength(field) | wrapNumber }} -
+
@@ -49,7 +49,11 @@