diff --git a/.vscode/settings.json b/.vscode/settings.json index 5b0c8130..649205af 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,7 +3,8 @@ "UI", "core", "MySQL", - "PostgreSQL" + "PostgreSQL", + "SQLite" ], "svg.preview.background": "transparent" } \ No newline at end of file diff --git a/package.json b/package.json index 55db2660..ffbe3441 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "build": "cross-env NODE_ENV=production npm run compile", "build:local": "npm run build && electron-builder", "build:appx": "npm run build:local -- --win appx", - "rebuild:electron": "npm run postinstall && electron-rebuild", + "rebuild:electron": "npm run postinstall", "release": "standard-version", "release:pre": "npm run release -- --prerelease alpha", "postinstall": "electron-builder install-app-deps", @@ -30,6 +30,7 @@ "appId": "com.fabio286.antares", "artifactName": "${productName}-${version}-${os}_${arch}.${ext}", "asar": true, + "buildDependenciesFromSource": true, "directories": { "output": "build", "buildResources": "assets" @@ -104,6 +105,7 @@ "@electron/remote": "^2.0.1", "@mdi/font": "^6.1.95", "ace-builds": "^1.4.13", + "better-sqlite3": "^7.4.4", "electron-log": "^4.4.1", "electron-store": "^8.0.1", "electron-updater": "^4.3.9", @@ -134,7 +136,6 @@ "electron": "^15.3.0", "electron-builder": "^22.13.1", "electron-devtools-installer": "^3.2.0", - "electron-rebuild": "^3.2.3", "eslint": "^7.32.0", "eslint-config-standard": "^16.0.3", "eslint-plugin-import": "^2.24.2", diff --git a/src/common/customizations/defaults.js b/src/common/customizations/defaults.js index 062836a2..e41ce30b 100644 --- a/src/common/customizations/defaults.js +++ b/src/common/customizations/defaults.js @@ -8,6 +8,9 @@ module.exports = { collations: false, engines: false, connectionSchema: false, + sslConnection: false, + sshConnection: false, + fileConnection: false, // Tools processesList: false, usersManagement: false, @@ -33,7 +36,11 @@ module.exports = { schedulerAdd: false, databaseEdit: false, schemaEdit: false, + schemaDrop: false, tableSettings: false, + tableOptions: false, + tableArray: false, + tableRealCount: false, viewSettings: false, triggerSettings: false, triggerFunctionSettings: false, @@ -45,14 +52,13 @@ module.exports = { sortableFields: false, unsigned: false, nullable: false, + nullablePrimary: false, zerofill: false, - tableOptions: false, autoIncrement: false, comment: false, collation: false, definer: false, onUpdate: false, - tableArray: false, viewAlgorithm: false, viewSqlSecurity: false, viewUpdateOption: false, @@ -76,5 +82,6 @@ module.exports = { triggerFunctionSql: false, triggerFunctionlanguages: false, parametersLength: false, - languages: false + languages: false, + readOnlyMode: false }; diff --git a/src/common/customizations/index.js b/src/common/customizations/index.js index 304889a4..931e18f2 100644 --- a/src/common/customizations/index.js +++ b/src/common/customizations/index.js @@ -1,5 +1,6 @@ module.exports = { maria: require('./mysql'), mysql: require('./mysql'), - pg: require('./postgresql') + pg: require('./postgresql'), + sqlite: require('./sqlite') }; diff --git a/src/common/customizations/mysql.js b/src/common/customizations/mysql.js index 673adea2..7496eb62 100644 --- a/src/common/customizations/mysql.js +++ b/src/common/customizations/mysql.js @@ -10,6 +10,8 @@ module.exports = { connectionSchema: true, collations: true, engines: true, + sslConnection: true, + sshConnection: true, // Tools processesList: true, // Structure @@ -30,6 +32,7 @@ module.exports = { functionAdd: true, schedulerAdd: true, schemaEdit: true, + schemaDrop: true, tableSettings: true, viewSettings: true, triggerSettings: true, diff --git a/src/common/customizations/postgresql.js b/src/common/customizations/postgresql.js index b3547ccc..078ce79e 100644 --- a/src/common/customizations/postgresql.js +++ b/src/common/customizations/postgresql.js @@ -8,6 +8,8 @@ module.exports = { defaultDatabase: 'postgres', // Core database: true, + sslConnection: true, + sshConnection: true, // Tools processesList: true, // Structure @@ -26,6 +28,7 @@ module.exports = { triggerFunctionAdd: true, routineAdd: true, functionAdd: true, + schemaDrop: true, databaseEdit: false, tableSettings: true, viewSettings: true, diff --git a/src/common/customizations/sqlite.js b/src/common/customizations/sqlite.js new file mode 100644 index 00000000..d72c5103 --- /dev/null +++ b/src/common/customizations/sqlite.js @@ -0,0 +1,27 @@ +module.exports = { + // Core + fileConnection: true, + // Structure + schemas: true, + tables: true, + views: true, + triggers: true, + // Settings + elementsWrapper: '', + stringsWrapper: '"', + tableAdd: true, + viewAdd: true, + triggerAdd: true, + schemaEdit: false, + tableSettings: true, + tableRealCount: true, + viewSettings: true, + triggerSettings: true, + indexes: true, + foreigns: true, + sortableFields: true, + nullable: true, + nullablePrimary: true, + triggerSql: 'BEGIN\r\n\r\nEND', + readOnlyMode: true +}; diff --git a/src/common/data-types/sqlite.js b/src/common/data-types/sqlite.js new file mode 100644 index 00000000..c7ac6b32 --- /dev/null +++ b/src/common/data-types/sqlite.js @@ -0,0 +1,137 @@ +module.exports = [ + { + group: 'integer', + types: [ + { + name: 'INT', + length: true, + collation: false, + unsigned: true, + zerofill: true + }, + { + name: 'INTEGER', + length: true, + collation: false, + unsigned: true, + zerofill: true + }, + { + name: 'BIGINT', + length: true, + collation: false, + unsigned: true, + zerofill: true + }, + { + name: 'NUMERIC', + length: true, + collation: false, + unsigned: true, + zerofill: true + }, + { + name: 'BOOLEAN', + length: false, + collation: false, + unsigned: true, + zerofill: true + } + ] + }, + { + group: 'float', + types: [ + { + name: 'FLOAT', + length: true, + collation: false, + unsigned: false, + zerofill: false + }, + { + name: 'REAL', + length: true, + collation: false, + unsigned: false, + zerofill: false + } + ] + }, + { + group: 'string', + types: [ + { + name: 'CHAR', + length: true, + collation: true, + unsigned: false, + zerofill: false + }, + { + name: 'VARCHAR', + length: true, + collation: true, + unsigned: false, + zerofill: false + }, + { + name: 'TEXT', + length: true, + collation: true, + unsigned: false, + zerofill: false + } + ] + }, + { + group: 'binary', + types: [ + { + name: 'BLOB', + length: true, + collation: false, + unsigned: false, + zerofill: false + } + ] + }, + { + group: 'time', + types: [ + { + name: 'DATE', + length: false, + collation: false, + unsigned: false, + zerofill: false + }, + { + name: 'TIME', + length: true, + collation: false, + unsigned: false, + zerofill: false + }, + { + name: 'DATETIME', + length: true, + collation: false, + unsigned: false, + zerofill: false + } + ] + }, + { + group: 'other', + types: [ + { + name: 'NONE', + length: false, + collation: false, + unsigned: false, + zerofill: false + } + ] + } +]; diff --git a/src/common/index-types/sqlite.js b/src/common/index-types/sqlite.js new file mode 100644 index 00000000..edc2f2a3 --- /dev/null +++ b/src/common/index-types/sqlite.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 aa2c1378..5ac0aac5 100644 --- a/src/main/ipc-handlers/connection.js +++ b/src/main/ipc-handlers/connection.js @@ -9,12 +9,16 @@ export default connections => { port: +conn.port, user: conn.user, password: conn.password, - application_name: 'Antares SQL' + application_name: 'Antares SQL', + readonly: conn.readonly }; if (conn.database) params.database = conn.database; + if (conn.databasePath) + params.databasePath = conn.databasePath; + if (conn.ssl) { params.ssl = { key: conn.key ? fs.readFileSync(conn.key) : null, @@ -62,12 +66,16 @@ export default connections => { port: +conn.port, user: conn.user, password: conn.password, - application_name: 'Antares SQL' + application_name: 'Antares SQL', + readonly: conn.readonly }; if (conn.database) params.database = conn.database; + if (conn.databasePath) + params.databasePath = conn.databasePath; + if (conn.schema) params.schema = conn.schema; diff --git a/src/main/ipc-handlers/tables.js b/src/main/ipc-handlers/tables.js index 3032febc..61400e32 100644 --- a/src/main/ipc-handlers/tables.js +++ b/src/main/ipc-handlers/tables.js @@ -102,6 +102,9 @@ export default (connections) => { case 'pg': escapedParam = `'${params.content.replaceAll('\'', '\'\'')}'`; break; + case 'sqlite': + escapedParam = `'${params.content.replaceAll('\'', '\'\'')}'`; + break; } } else if (ARRAY.includes(params.type)) @@ -122,6 +125,10 @@ export default (connections) => { fileBlob = fs.readFileSync(params.content); escapedParam = `decode('${fileBlob.toString('hex')}', 'hex')`; break; + case 'sqlite': + fileBlob = fs.readFileSync(params.content); + escapedParam = `X'${fileBlob.toString('hex')}'`; + break; } reload = true; } @@ -134,6 +141,9 @@ export default (connections) => { case 'pg': escapedParam = 'decode(\'\', \'hex\')'; break; + case 'sqlite': + escapedParam = 'X\'\''; + break; } } } diff --git a/src/main/libs/ClientsFactory.js b/src/main/libs/ClientsFactory.js index 54211908..6543eaff 100644 --- a/src/main/libs/ClientsFactory.js +++ b/src/main/libs/ClientsFactory.js @@ -1,6 +1,7 @@ 'use strict'; import { MySQLClient } from './clients/MySQLClient'; import { PostgreSQLClient } from './clients/PostgreSQLClient'; +import { SQLiteClient } from './clients/SQLiteClient'; const queryLogger = sql => { // Remove comments, newlines and multiple spaces @@ -37,6 +38,8 @@ export class ClientsFactory { return new MySQLClient(args); case 'pg': return new PostgreSQLClient(args); + case 'sqlite': + return new SQLiteClient(args); default: 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 47757c88..7d10ce3e 100644 --- a/src/main/libs/clients/MySQLClient.js +++ b/src/main/libs/clients/MySQLClient.js @@ -571,7 +571,7 @@ export class MySQLClient extends AntaresCore { /** * CREATE DATABASE * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async createSchema (params) { @@ -581,7 +581,7 @@ export class MySQLClient extends AntaresCore { /** * ALTER DATABASE * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async alterSchema (params) { @@ -591,7 +591,7 @@ export class MySQLClient extends AntaresCore { /** * DROP DATABASE * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async dropSchema (params) { @@ -631,7 +631,7 @@ export class MySQLClient extends AntaresCore { /** * DROP VIEW * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async dropView (params) { @@ -642,7 +642,7 @@ export class MySQLClient extends AntaresCore { /** * ALTER VIEW * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async alterView (params) { @@ -663,7 +663,7 @@ export class MySQLClient extends AntaresCore { /** * CREATE VIEW * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async createView (params) { @@ -696,7 +696,7 @@ export class MySQLClient extends AntaresCore { /** * DROP TRIGGER * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async dropTrigger (params) { @@ -707,7 +707,7 @@ export class MySQLClient extends AntaresCore { /** * ALTER TRIGGER * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async alterTrigger (params) { @@ -729,7 +729,7 @@ export class MySQLClient extends AntaresCore { /** * CREATE TRIGGER * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async createTrigger (params) { @@ -803,7 +803,7 @@ export class MySQLClient extends AntaresCore { /** * DROP PROCEDURE * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async dropRoutine (params) { @@ -814,7 +814,7 @@ export class MySQLClient extends AntaresCore { /** * ALTER PROCEDURE * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async alterRoutine (params) { @@ -836,7 +836,7 @@ export class MySQLClient extends AntaresCore { /** * CREATE PROCEDURE * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async createRoutine (params) { @@ -930,7 +930,7 @@ export class MySQLClient extends AntaresCore { /** * DROP FUNCTION * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async dropFunction (params) { @@ -941,7 +941,7 @@ export class MySQLClient extends AntaresCore { /** * ALTER FUNCTION * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async alterFunction (params) { @@ -963,7 +963,7 @@ export class MySQLClient extends AntaresCore { /** * CREATE FUNCTION * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async createFunction (params) { @@ -1024,7 +1024,7 @@ export class MySQLClient extends AntaresCore { /** * DROP EVENT * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async dropEvent (params) { @@ -1035,7 +1035,7 @@ export class MySQLClient extends AntaresCore { /** * ALTER EVENT * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async alterEvent (params) { @@ -1061,7 +1061,7 @@ export class MySQLClient extends AntaresCore { /** * CREATE EVENT * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async createEvent (params) { @@ -1205,7 +1205,7 @@ export class MySQLClient extends AntaresCore { /** * CREATE TABLE * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async createTable (params) { @@ -1267,7 +1267,7 @@ export class MySQLClient extends AntaresCore { /** * ALTER TABLE * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async alterTable (params) { @@ -1402,7 +1402,7 @@ export class MySQLClient extends AntaresCore { /** * DUPLICATE TABLE * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async duplicateTable (params) { @@ -1413,7 +1413,7 @@ export class MySQLClient extends AntaresCore { /** * TRUNCATE TABLE * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async truncateTable (params) { @@ -1424,7 +1424,7 @@ export class MySQLClient extends AntaresCore { /** * DROP TABLE * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async dropTable (params) { diff --git a/src/main/libs/clients/PostgreSQLClient.js b/src/main/libs/clients/PostgreSQLClient.js index 760368d2..6a68948f 100644 --- a/src/main/libs/clients/PostgreSQLClient.js +++ b/src/main/libs/clients/PostgreSQLClient.js @@ -506,7 +506,7 @@ export class PostgreSQLClient extends AntaresCore { /** * CREATE SCHEMA * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async createSchema (params) { @@ -516,7 +516,7 @@ export class PostgreSQLClient extends AntaresCore { /** * ALTER DATABASE * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async alterSchema (params) { @@ -526,7 +526,7 @@ export class PostgreSQLClient extends AntaresCore { /** * DROP DATABASE * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async dropSchema (params) { @@ -558,7 +558,7 @@ export class PostgreSQLClient extends AntaresCore { /** * DROP VIEW * - * @returns {Array.} parameters + * @returns {Promise} * @memberof PostgreSQLClient */ async dropView (params) { @@ -569,7 +569,7 @@ export class PostgreSQLClient extends AntaresCore { /** * ALTER VIEW * - * @returns {Array.} parameters + * @returns {Promise} * @memberof PostgreSQLClient */ async alterView (params) { @@ -585,7 +585,7 @@ export class PostgreSQLClient extends AntaresCore { /** * CREATE VIEW * - * @returns {Array.} parameters + * @returns {Promise} * @memberof PostgreSQLClient */ async createView (params) { @@ -640,7 +640,7 @@ export class PostgreSQLClient extends AntaresCore { /** * DROP TRIGGER * - * @returns {Array.} parameters + * @returns {Promise} * @memberof PostgreSQLClient */ async dropTrigger (params) { @@ -652,7 +652,7 @@ export class PostgreSQLClient extends AntaresCore { /** * ALTER TRIGGER * - * @returns {Array.} parameters + * @returns {Promise} * @memberof PostgreSQLClient */ async alterTrigger (params) { @@ -674,7 +674,7 @@ export class PostgreSQLClient extends AntaresCore { /** * CREATE TRIGGER * - * @returns {Array.} parameters + * @returns {Promise} * @memberof PostgreSQLClient */ async createTrigger (params) { @@ -1086,7 +1086,7 @@ export class PostgreSQLClient extends AntaresCore { /** * CREATE TABLE * - * @returns {Array.} parameters + * @returns {Promise} * @memberof PostgreSQLClient */ async createTable (params) { @@ -1144,7 +1144,7 @@ export class PostgreSQLClient extends AntaresCore { /** * ALTER TABLE * - * @returns {Array.} parameters + * @returns {Promise} * @memberof PostgreSQLClient */ async alterTable (params) { @@ -1290,7 +1290,7 @@ export class PostgreSQLClient extends AntaresCore { /** * DUPLICATE TABLE * - * @returns {Array.} parameters + * @returns {Promise} * @memberof PostgreSQLClient */ async duplicateTable (params) { @@ -1301,7 +1301,7 @@ export class PostgreSQLClient extends AntaresCore { /** * TRUNCATE TABLE * - * @returns {Array.} parameters + * @returns {Promise} * @memberof PostgreSQLClient */ async truncateTable (params) { @@ -1312,7 +1312,7 @@ export class PostgreSQLClient extends AntaresCore { /** * DROP TABLE * - * @returns {Array.} parameters + * @returns {Promise} * @memberof PostgreSQLClient */ async dropTable (params) { diff --git a/src/main/libs/clients/SQLiteClient.js b/src/main/libs/clients/SQLiteClient.js new file mode 100644 index 00000000..d7d6bfcb --- /dev/null +++ b/src/main/libs/clients/SQLiteClient.js @@ -0,0 +1,806 @@ +'use strict'; +import sqlite from 'better-sqlite3'; +import { AntaresCore } from '../AntaresCore'; +import dataTypes from 'common/data-types/mysql'; +import { NUMBER, FLOAT, TIME, DATETIME } from 'common/fieldTypes'; + +export class SQLiteClient extends AntaresCore { + constructor (args) { + super(args); + + this._schema = null; + } + + _getTypeInfo (type) { + return dataTypes + .reduce((acc, group) => [...acc, ...group.types], []) + .filter(_type => _type.name === type.toUpperCase())[0]; + } + + /** + * @memberof SQLiteClient + */ + async connect () { + this._connection = sqlite(this._params.databasePath, { + fileMustExist: true, + readonly: this._params.readonly + }); + } + + /** + * @memberof SQLiteClient + */ + destroy () {} + + /** + * Executes an USE query + * + * @memberof SQLiteClient + */ + use () {} + + /** + * @param {Array} schemas list + * @returns {Array.} databases scructure + * @memberof SQLiteClient + */ + async getStructure (schemas) { + const { rows: databases } = await this.raw('SELECT * FROM pragma_database_list'); + + const filteredDatabases = databases; + + const tablesArr = []; + const triggersArr = []; + let schemaSize = 0; + + for (const db of filteredDatabases) { + if (!schemas.has(db.name)) continue; + + let { rows: tables } = await this.raw(` + SELECT * + FROM "${db.name}".sqlite_master + WHERE type IN ('table', 'view') + AND name NOT LIKE 'sqlite_%' + ORDER BY name + `); + if (tables.length) { + tables = tables.map(table => { + table.Db = db.name; + return table; + }); + tablesArr.push(...tables); + } + + let { rows: triggers } = await this.raw(`SELECT * FROM "${db.name}".sqlite_master WHERE type='trigger'`); + if (triggers.length) { + triggers = triggers.map(trigger => { + trigger.Db = db.name; + return trigger; + }); + triggersArr.push(...triggers); + } + } + + return filteredDatabases.map(db => { + if (schemas.has(db.name)) { + // TABLES + const remappedTables = tablesArr.filter(table => table.Db === db.name).map(table => { + const tableSize = 0; + schemaSize += tableSize; + + return { + name: table.name, + type: table.type, + rows: false, + size: false + }; + }); + + // TRIGGERS + const remappedTriggers = triggersArr.filter(trigger => trigger.Db === db.name).map(trigger => { + return { + name: trigger.name, + table: trigger.tbl_name + }; + }); + + return { + name: db.name, + size: schemaSize, + tables: remappedTables, + functions: [], + procedures: [], + triggers: remappedTriggers, + schedulers: [] + }; + } + else { + return { + name: db.name, + size: 0, + tables: [], + functions: [], + procedures: [], + triggers: [], + schedulers: [] + }; + } + }); + } + + /** + * @param {Object} params + * @param {String} params.schema + * @param {String} params.table + * @returns {Object} table scructure + * @memberof SQLiteClient + */ + async getTableColumns ({ schema, table }) { + const { rows: fields } = await this.raw(`SELECT * FROM "${schema}".pragma_table_info('${table}')`); + + return fields.map(field => { + const [type, length] = field.type.includes('(') + ? field.type.replace(')', '').split('(').map(el => { + if (!isNaN(el)) el = +el; + return el; + }) + : [field.type, null]; + + return { + name: field.name, + key: null, + type: type.trim(), + schema: schema, + table: table, + numPrecision: [...NUMBER, ...FLOAT].includes(type) ? length : null, + datePrecision: null, + charLength: ![...NUMBER, ...FLOAT].includes(type) ? length : null, + nullable: !field.notnull, + unsigned: null, + zerofill: null, + order: field.cid + 1, + default: field.dflt_value, + charset: null, + collation: null, + autoIncrement: false, + onUpdate: null, + comment: '' + }; + }); + } + + /** + * @param {Object} params + * @param {String} params.schema + * @param {String} params.table + * @returns {Object} table row count + * @memberof SQLiteClient + */ + async getTableApproximateCount ({ schema, table }) { + const { rows } = await this.raw(`SELECT COUNT(*) AS count FROM "${schema}"."${table}"`); + + return rows.length ? rows[0].count : 0; + } + + /** + * @param {Object} params + * @param {String} params.schema + * @param {String} params.table + * @returns {Object} table options + * @memberof SQLiteClient + */ + async getTableOptions ({ schema, table }) { + return { name: table }; + } + + /** + * @param {Object} params + * @param {String} params.schema + * @param {String} params.table + * @returns {Object} table indexes + * @memberof SQLiteClient + */ + async getTableIndexes ({ schema, table }) { + const remappedIndexes = []; + const { rows: primaryKeys } = await this.raw(`SELECT * FROM "${schema}".pragma_table_info('${table}') WHERE pk != 0`); + + for (const key of primaryKeys) { + remappedIndexes.push({ + name: 'PRIMARY', + column: key.name, + indexType: null, + type: 'PRIMARY', + cardinality: null, + comment: '', + indexComment: '' + }); + } + + const { rows: indexes } = await this.raw(`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, + type: index.unique === 1 ? 'UNIQUE' : 'INDEX', + cardinality: null, + comment: '', + indexComment: '' + }); + } + } + + return remappedIndexes; + } + + /** + * @param {Object} params + * @param {String} params.schema + * @param {String} params.table + * @returns {Object} table key usage + * @memberof SQLiteClient + */ + async getKeyUsage ({ schema, table }) { + const { rows } = await this.raw(`SELECT * FROM "${schema}".pragma_foreign_key_list('${table}');`); + + return rows.map(field => { + return { + schema: schema, + table: table, + field: field.from, + position: field.id + 1, + constraintPosition: null, + constraintName: field.id, + refSchema: schema, + refTable: field.table, + refField: field.to, + onUpdate: field.on_update, + onDelete: field.on_delete + }; + }); + } + + async getUsers () {} + + /** + * SHOW CREATE VIEW + * + * @returns {Array.} view informations + * @memberof SQLiteClient + */ + async getViewInformations ({ schema, view }) { + const sql = `SELECT "sql" FROM "${schema}".sqlite_master WHERE "type"='view' AND name='${view}'`; + const results = await this.raw(sql); + + return results.rows.map(row => { + return { + sql: row.sql.match(/(?<=AS ).*?$/gs)[0], + name: view + }; + })[0]; + } + + /** + * DROP VIEW + * + * @returns {Array.} parameters + * @memberof SQLiteClient + */ + async dropView (params) { + const sql = `DROP VIEW "${params.schema}"."${params.view}"`; + return await this.raw(sql); + } + + /** + * ALTER VIEW + * + * @returns {Array.} parameters + * @memberof SQLiteClient + */ + async alterView (params) { + const { view } = params; + try { + await this.dropView({ schema: view.schema, view: view.oldName }); + await this.createView(view); + } + catch (err) { + return Promise.reject(err); + } + } + + /** + * CREATE VIEW + * + * @returns {Array.} parameters + * @memberof SQLiteClient + */ + async createView (params) { + const sql = `CREATE VIEW "${params.schema}"."${params.name}" AS ${params.sql}`; + return await this.raw(sql); + } + + /** + * SHOW CREATE TRIGGER + * + * @returns {Array.} view informations + * @memberof SQLiteClient + */ + async getTriggerInformations ({ schema, trigger }) { + const sql = `SELECT "sql" FROM "${schema}".sqlite_master WHERE "type"='trigger' AND name='${trigger}'`; + const results = await this.raw(sql); + + return results.rows.map(row => { + return { + sql: row.sql.match(/(BEGIN|begin)(.*)(END|end)/gs)[0], + name: trigger, + table: row.sql.match(/(?<=ON `).*?(?=`)/gs)[0], + activation: row.sql.match(/(BEFORE|AFTER)/gs)[0], + event: row.sql.match(/(INSERT|UPDATE|DELETE)/gs)[0] + }; + })[0]; + } + + /** + * DROP TRIGGER + * + * @returns {Array.} parameters + * @memberof SQLiteClient + */ + async dropTrigger (params) { + const sql = `DROP TRIGGER \`${params.schema}\`.\`${params.trigger}\``; + return await this.raw(sql); + } + + /** + * ALTER TRIGGER + * + * @returns {Array.} parameters + * @memberof SQLiteClient + */ + 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({ schema: trigger.schema, trigger: tempTrigger.name }); + await this.dropTrigger({ schema: trigger.schema, trigger: trigger.oldName }); + await this.createTrigger(trigger); + } + catch (err) { + return Promise.reject(err); + } + } + + /** + * CREATE TRIGGER + * + * @returns {Array.} parameters + * @memberof SQLiteClient + */ + async createTrigger (params) { + const sql = `CREATE ${params.definer ? `DEFINER=${params.definer} ` : ''}TRIGGER \`${params.schema}\`.\`${params.name}\` ${params.activation} ${params.event} ON \`${params.table}\` FOR EACH ROW ${params.sql}`; + return await this.raw(sql, { split: false }); + } + + /** + * SHOW COLLATION + * + * @returns {Array.} collations list + * @memberof SQLiteClient + */ + async getCollations () { + return []; + } + + /** + * SHOW VARIABLES + * + * @returns {Array.} variables list + * @memberof SQLiteClient + */ + async getVariables () { + return []; + } + + /** + * SHOW ENGINES + * + * @returns {Array.} engines list + * @memberof SQLiteClient + */ + async getEngines () { + return { + name: 'SQLite', + support: 'YES', + comment: '', + isDefault: true + }; + } + + /** + * SHOW VARIABLES LIKE '%vers%' + * + * @returns {Array.} version parameters + * @memberof SQLiteClient + */ + async getVersion () { + const os = require('os'); + const sql = 'SELECT sqlite_version() AS version'; + const { rows } = await this.raw(sql); + + return { + number: rows[0].version, + name: 'SQLite', + arch: process.arch, + os: `${os.type()} ${os.release()}` + }; + } + + async getProcesses () {} + + async killProcess () {} + + /** + * CREATE TABLE + * + * @returns {Promise} + * @memberof SQLiteClient + */ + async createTable (params) { + const { + schema, + fields, + foreigns, + indexes, + options + } = params; + const newColumns = []; + const newIndexes = []; + const manageIndexes = []; + const newForeigns = []; + + let sql = `CREATE TABLE "${schema}"."${options.name}"`; + + // ADD FIELDS + fields.forEach(field => { + const typeInfo = this._getTypeInfo(field.type); + const length = typeInfo?.length ? field.enumValues || field.numLength || field.charLength || field.datePrecision : false; + + newColumns.push(`"${field.name}" + ${field.type.toUpperCase()}${length && length !== true ? `(${length})` : ''} + ${field.unsigned ? 'UNSIGNED' : ''} + ${field.nullable ? 'NULL' : 'NOT NULL'} + ${field.autoIncrement ? 'AUTO_INCREMENT' : ''} + ${field.default ? `DEFAULT ${field.default}` : ''} + ${field.onUpdate ? `ON UPDATE ${field.onUpdate}` : ''}`); + }); + + // ADD INDEX + indexes.forEach(index => { + const fields = index.fields.map(field => `"${field}"`).join(','); + const type = index.type; + + if (type === 'PRIMARY') + newIndexes.push(`PRIMARY KEY (${fields})`); + else + manageIndexes.push(`CREATE ${type === 'UNIQUE' ? type : ''} INDEX "${index.name}" ON "${options.name}" (${fields})`); + }); + + // ADD FOREIGN KEYS + foreigns.forEach(foreign => { + newForeigns.push(`CONSTRAINT "${foreign.constraintName}" FOREIGN KEY ("${foreign.field}") REFERENCES "${foreign.refTable}" ("${foreign.refField}") ON UPDATE ${foreign.onUpdate} ON DELETE ${foreign.onDelete}`); + }); + + sql = `${sql} (${[...newColumns, ...newIndexes, ...newForeigns].join(', ')})`; + if (manageIndexes.length) sql = `${sql}; ${manageIndexes.join(';')}`; + + return await this.raw(sql); + } + + /** + * ALTER TABLE + * + * @returns {Promise} + * @memberof SQLiteClient + */ + async alterTable (params) { + try { + await this.raw('BEGIN TRANSACTION'); + await this.raw('PRAGMA foreign_keys = 0'); + + const tmpName = `Antares_${params.table}_tmp`; + await this.raw(`CREATE TABLE "${tmpName}" AS SELECT * FROM "${params.table}"`); + await this.dropTable(params); + + const createTableParams = { + schema: params.schema, + fields: params.tableStructure.fields, + foreigns: params.tableStructure.foreigns, + indexes: params.tableStructure.indexes.filter(index => !index.name.includes('sqlite_autoindex')), + options: { name: params.tableStructure.name } + }; + await this.createTable(createTableParams); + const insertFields = createTableParams.fields + .filter(field => { + return ( + params.additions.every(add => add.name !== field.name) && + params.deletions.every(del => del.name !== field.name) + ); + }) + .reduce((acc, curr) => { + acc.push(`"${curr.name}"`); + return acc; + }, []); + + const selectFields = insertFields.map(field => { + const renamedField = params.changes.find(change => `"${change.name}"` === field); + if (renamedField) + return `"${renamedField.orgName}"`; + return field; + }); + + await this.raw(`INSERT INTO "${createTableParams.options.name}" (${insertFields.join(',')}) SELECT ${selectFields.join(',')} FROM "${tmpName}"`); + + await this.dropTable({ schema: params.schema, table: tmpName }); + await this.raw('PRAGMA foreign_keys = 1'); + await this.raw('COMMIT'); + } + catch (err) { + await this.raw('ROLLBACK'); + return Promise.reject(err); + } + } + + /** + * DUPLICATE TABLE + * + * @returns {Promise} + * @memberof SQLiteClient + */ + async duplicateTable (params) { // TODO: retrive table informations and create a copy + const sql = `CREATE TABLE "${params.schema}"."${params.table}_copy" AS SELECT * FROM "${params.schema}"."${params.table}"`; + return await this.raw(sql); + } + + /** + * TRUNCATE TABLE + * + * @returns {Promise} + * @memberof SQLiteClient + */ + async truncateTable (params) { + const sql = `DELETE FROM "${params.schema}"."${params.table}"`; + return await this.raw(sql); + } + + /** + * DROP TABLE + * + * @returns {Promise} + * @memberof SQLiteClient + */ + async dropTable (params) { + const sql = `DROP TABLE "${params.schema}"."${params.table}"`; + return await this.raw(sql); + } + + /** + * @returns {String} SQL string + * @memberof SQLiteClient + */ + 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, []) + ?.map(clausole => clausole.replace('= null', 'IS NULL')); + 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]); + 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(', ')} ` : ''; + + // OFFSET + const offsetRaw = this._query.offset.length ? `OFFSET ${this._query.offset.join(', ')} ` : ''; + + return `${selectRaw}${updateRaw ? 'UPDATE' : ''}${insertRaw ? 'INSERT ' : ''}${this._query.delete ? 'DELETE ' : ''}${fromRaw}${updateRaw}${whereRaw}${groupByRaw}${orderByRaw}${limitRaw}${offsetRaw}${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 SQLiteClient + */ + async raw (sql, args) { + if (process.env.NODE_ENV === 'development') this._logger(sql);// TODO: replace BLOB content with a placeholder + + args = { + nest: false, + details: false, + split: true, + comments: true, + ...args + }; + + if (!args.comments) + sql = sql.replace(/(\/\*(.|[\r\n])*?\*\/)|(--(.*|[\r\n]))/gm, '');// Remove comments + + const resultsArr = []; + let paramsArr = []; + const queries = args.split + ? sql.split(/((?:[^;'"]*(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*')[^;'"]*)+)|;/gm) + .filter(Boolean) + .map(q => q.trim()) + : [sql]; + const connection = this._connection; + + for (const query of queries) { + if (!query) continue; + const timeStart = new Date(); + let timeStop; + const keysArr = []; + + const { rows, report, fields, keys, duration } = await new Promise((resolve, reject) => { + (async () => { + let queryResult; + let affectedRows; + let fields; + const detectedTypes = {}; + + try { + const stmt = connection.prepare(query); + + if (stmt.reader) { + queryResult = stmt.all(); + fields = stmt.columns(); + + if (queryResult.length) { + fields.forEach(field => { + detectedTypes[field.name] = typeof queryResult[0][field.name]; + }); + } + } + else { + const info = queryResult = stmt.run(); + affectedRows = info.changes; + } + } + catch (err) { + reject(err); + } + + timeStop = new Date(); + + let remappedFields = fields + ? fields.map(field => { + let [parsedType, length] = field.type?.includes('(') + ? field.type.replace(')', '').split('(').map(el => { + if (!isNaN(el)) + el = +el; + else + el = el.trim(); + return el; + }) + : [field.type, null]; + + if ([...TIME, ...DATETIME].includes(parsedType)) { + const firstNotNull = queryResult.find(res => res[field.name] !== null); + if (firstNotNull[field.name].includes('.')) + length = firstNotNull[field.name].split('.').pop().length; + } + + return { + name: field.name, + alias: field.name, + orgName: field.column, + schema: field.database, + table: field.table, + tableAlias: field.table, + orgTable: field.table, + type: field.type !== null ? parsedType : detectedTypes[field.name], + length + }; + }).filter(Boolean) + : []; + + if (args.details) { + paramsArr = remappedFields.map(field => { + return { + table: field.table, + schema: field.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 { + 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) { + // const length = detailedField.numPrecision || detailedField.charLength || detailedField.datePrecision || null; + // field = { ...field, ...detailedField, length }; + // } + + if (fieldIndex) { + const key = fieldIndex.type === 'PRIMARY' ? 'pri' : fieldIndex.type === 'UNIQUE' ? 'uni' : 'mul'; + field = { ...field, key }; + }; + } + + return field; + }); + } + catch (err) { + reject(err); + } + } + } + + resolve({ + duration: timeStop - timeStart, + rows: Array.isArray(queryResult) ? queryResult.some(el => Array.isArray(el)) ? [] : queryResult : false, + report: affectedRows !== undefined ? { affectedRows } : null, + fields: remappedFields, + keys: keysArr + }); + })(); + }); + + resultsArr.push({ rows, report, fields, keys, duration }); + } + + return resultsArr.length === 1 ? resultsArr[0] : resultsArr; + } +} diff --git a/src/main/main.js b/src/main/main.js index dc742bfa..639d3c39 100644 --- a/src/main/main.js +++ b/src/main/main.js @@ -107,14 +107,14 @@ else { mainWindow = await createMainWindow(); createAppMenu(); - if (isDevelopment) - mainWindow.webContents.openDevTools(); + // if (isDevelopment) + // mainWindow.webContents.openDevTools(); - process.on('uncaughtException', (error) => { + process.on('uncaughtException', error => { mainWindow.webContents.send('unhandled-exception', error); }); - process.on('unhandledRejection', (error) => { + process.on('unhandledRejection', error => { mainWindow.webContents.send('unhandled-exception', error); }); }); diff --git a/src/renderer/components/WorkspaceAddConnectionPanel.vue b/src/renderer/components/WorkspaceAddConnectionPanel.vue index e8802235..8d6b144c 100644 --- a/src/renderer/components/WorkspaceAddConnectionPanel.vue +++ b/src/renderer/components/WorkspaceAddConnectionPanel.vue @@ -11,6 +11,7 @@ {{ $t('word.general') }}
  • {{ $t('word.ssl') }}
  • -
    +
    @@ -79,7 +73,20 @@ >
    -
    +
    +
    + +
    +
    + +
    +
    +
    @@ -105,7 +112,7 @@ >
    -
    +
    @@ -118,7 +125,7 @@ >
    -
    +
    @@ -144,7 +151,15 @@ >
    -
    +
    +
    +
    + +
    +
    +
  • {{ $t('word.ssl') }}
  • -
    +
    @@ -73,7 +73,20 @@ >
    -
    +
    +
    + +
    +
    + +
    +
    +
    @@ -99,7 +112,7 @@ >
    -
    +
    @@ -112,7 +125,7 @@ >
    -
    +
    @@ -138,7 +151,15 @@ >
    -
    +
    +
    +
    + +
    +
    +