Merge pull request #141 from Fabio286/feat/sqlite-implementation

feat:sqlite implementation
This commit is contained in:
Fabio Di Stasio 2021-11-19 16:11:00 +01:00 committed by GitHub
commit 7d345cf795
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 1213 additions and 101 deletions

View File

@ -3,7 +3,8 @@
"UI", "UI",
"core", "core",
"MySQL", "MySQL",
"PostgreSQL" "PostgreSQL",
"SQLite"
], ],
"svg.preview.background": "transparent" "svg.preview.background": "transparent"
} }

View File

@ -14,7 +14,7 @@
"build": "cross-env NODE_ENV=production npm run compile", "build": "cross-env NODE_ENV=production npm run compile",
"build:local": "npm run build && electron-builder", "build:local": "npm run build && electron-builder",
"build:appx": "npm run build:local -- --win appx", "build:appx": "npm run build:local -- --win appx",
"rebuild:electron": "npm run postinstall && electron-rebuild", "rebuild:electron": "npm run postinstall",
"release": "standard-version", "release": "standard-version",
"release:pre": "npm run release -- --prerelease alpha", "release:pre": "npm run release -- --prerelease alpha",
"postinstall": "electron-builder install-app-deps", "postinstall": "electron-builder install-app-deps",
@ -30,6 +30,7 @@
"appId": "com.fabio286.antares", "appId": "com.fabio286.antares",
"artifactName": "${productName}-${version}-${os}_${arch}.${ext}", "artifactName": "${productName}-${version}-${os}_${arch}.${ext}",
"asar": true, "asar": true,
"buildDependenciesFromSource": true,
"directories": { "directories": {
"output": "build", "output": "build",
"buildResources": "assets" "buildResources": "assets"
@ -104,6 +105,7 @@
"@electron/remote": "^2.0.1", "@electron/remote": "^2.0.1",
"@mdi/font": "^6.1.95", "@mdi/font": "^6.1.95",
"ace-builds": "^1.4.13", "ace-builds": "^1.4.13",
"better-sqlite3": "^7.4.4",
"electron-log": "^4.4.1", "electron-log": "^4.4.1",
"electron-store": "^8.0.1", "electron-store": "^8.0.1",
"electron-updater": "^4.3.9", "electron-updater": "^4.3.9",
@ -134,7 +136,6 @@
"electron": "^15.3.0", "electron": "^15.3.0",
"electron-builder": "^22.13.1", "electron-builder": "^22.13.1",
"electron-devtools-installer": "^3.2.0", "electron-devtools-installer": "^3.2.0",
"electron-rebuild": "^3.2.3",
"eslint": "^7.32.0", "eslint": "^7.32.0",
"eslint-config-standard": "^16.0.3", "eslint-config-standard": "^16.0.3",
"eslint-plugin-import": "^2.24.2", "eslint-plugin-import": "^2.24.2",

View File

@ -8,6 +8,9 @@ module.exports = {
collations: false, collations: false,
engines: false, engines: false,
connectionSchema: false, connectionSchema: false,
sslConnection: false,
sshConnection: false,
fileConnection: false,
// Tools // Tools
processesList: false, processesList: false,
usersManagement: false, usersManagement: false,
@ -33,7 +36,11 @@ module.exports = {
schedulerAdd: false, schedulerAdd: false,
databaseEdit: false, databaseEdit: false,
schemaEdit: false, schemaEdit: false,
schemaDrop: false,
tableSettings: false, tableSettings: false,
tableOptions: false,
tableArray: false,
tableRealCount: false,
viewSettings: false, viewSettings: false,
triggerSettings: false, triggerSettings: false,
triggerFunctionSettings: false, triggerFunctionSettings: false,
@ -45,14 +52,13 @@ module.exports = {
sortableFields: false, sortableFields: false,
unsigned: false, unsigned: false,
nullable: false, nullable: false,
nullablePrimary: false,
zerofill: false, zerofill: false,
tableOptions: false,
autoIncrement: false, autoIncrement: false,
comment: false, comment: false,
collation: false, collation: false,
definer: false, definer: false,
onUpdate: false, onUpdate: false,
tableArray: false,
viewAlgorithm: false, viewAlgorithm: false,
viewSqlSecurity: false, viewSqlSecurity: false,
viewUpdateOption: false, viewUpdateOption: false,
@ -76,5 +82,6 @@ module.exports = {
triggerFunctionSql: false, triggerFunctionSql: false,
triggerFunctionlanguages: false, triggerFunctionlanguages: false,
parametersLength: false, parametersLength: false,
languages: false languages: false,
readOnlyMode: false
}; };

View File

@ -1,5 +1,6 @@
module.exports = { module.exports = {
maria: require('./mysql'), maria: require('./mysql'),
mysql: require('./mysql'), mysql: require('./mysql'),
pg: require('./postgresql') pg: require('./postgresql'),
sqlite: require('./sqlite')
}; };

View File

@ -10,6 +10,8 @@ module.exports = {
connectionSchema: true, connectionSchema: true,
collations: true, collations: true,
engines: true, engines: true,
sslConnection: true,
sshConnection: true,
// Tools // Tools
processesList: true, processesList: true,
// Structure // Structure
@ -30,6 +32,7 @@ module.exports = {
functionAdd: true, functionAdd: true,
schedulerAdd: true, schedulerAdd: true,
schemaEdit: true, schemaEdit: true,
schemaDrop: true,
tableSettings: true, tableSettings: true,
viewSettings: true, viewSettings: true,
triggerSettings: true, triggerSettings: true,

View File

@ -8,6 +8,8 @@ module.exports = {
defaultDatabase: 'postgres', defaultDatabase: 'postgres',
// Core // Core
database: true, database: true,
sslConnection: true,
sshConnection: true,
// Tools // Tools
processesList: true, processesList: true,
// Structure // Structure
@ -26,6 +28,7 @@ module.exports = {
triggerFunctionAdd: true, triggerFunctionAdd: true,
routineAdd: true, routineAdd: true,
functionAdd: true, functionAdd: true,
schemaDrop: true,
databaseEdit: false, databaseEdit: false,
tableSettings: true, tableSettings: true,
viewSettings: true, viewSettings: true,

View File

@ -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
};

View File

@ -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
}
]
}
];

View File

@ -0,0 +1,5 @@
module.exports = [
'PRIMARY',
'INDEX',
'UNIQUE'
];

View File

@ -9,12 +9,16 @@ export default connections => {
port: +conn.port, port: +conn.port,
user: conn.user, user: conn.user,
password: conn.password, password: conn.password,
application_name: 'Antares SQL' application_name: 'Antares SQL',
readonly: conn.readonly
}; };
if (conn.database) if (conn.database)
params.database = conn.database; params.database = conn.database;
if (conn.databasePath)
params.databasePath = conn.databasePath;
if (conn.ssl) { if (conn.ssl) {
params.ssl = { params.ssl = {
key: conn.key ? fs.readFileSync(conn.key) : null, key: conn.key ? fs.readFileSync(conn.key) : null,
@ -62,12 +66,16 @@ export default connections => {
port: +conn.port, port: +conn.port,
user: conn.user, user: conn.user,
password: conn.password, password: conn.password,
application_name: 'Antares SQL' application_name: 'Antares SQL',
readonly: conn.readonly
}; };
if (conn.database) if (conn.database)
params.database = conn.database; params.database = conn.database;
if (conn.databasePath)
params.databasePath = conn.databasePath;
if (conn.schema) if (conn.schema)
params.schema = conn.schema; params.schema = conn.schema;

View File

@ -102,6 +102,9 @@ export default (connections) => {
case 'pg': case 'pg':
escapedParam = `'${params.content.replaceAll('\'', '\'\'')}'`; escapedParam = `'${params.content.replaceAll('\'', '\'\'')}'`;
break; break;
case 'sqlite':
escapedParam = `'${params.content.replaceAll('\'', '\'\'')}'`;
break;
} }
} }
else if (ARRAY.includes(params.type)) else if (ARRAY.includes(params.type))
@ -122,6 +125,10 @@ export default (connections) => {
fileBlob = fs.readFileSync(params.content); fileBlob = fs.readFileSync(params.content);
escapedParam = `decode('${fileBlob.toString('hex')}', 'hex')`; escapedParam = `decode('${fileBlob.toString('hex')}', 'hex')`;
break; break;
case 'sqlite':
fileBlob = fs.readFileSync(params.content);
escapedParam = `X'${fileBlob.toString('hex')}'`;
break;
} }
reload = true; reload = true;
} }
@ -134,6 +141,9 @@ export default (connections) => {
case 'pg': case 'pg':
escapedParam = 'decode(\'\', \'hex\')'; escapedParam = 'decode(\'\', \'hex\')';
break; break;
case 'sqlite':
escapedParam = 'X\'\'';
break;
} }
} }
} }

View File

@ -1,6 +1,7 @@
'use strict'; 'use strict';
import { MySQLClient } from './clients/MySQLClient'; import { MySQLClient } from './clients/MySQLClient';
import { PostgreSQLClient } from './clients/PostgreSQLClient'; import { PostgreSQLClient } from './clients/PostgreSQLClient';
import { SQLiteClient } from './clients/SQLiteClient';
const queryLogger = sql => { const queryLogger = sql => {
// Remove comments, newlines and multiple spaces // Remove comments, newlines and multiple spaces
@ -37,6 +38,8 @@ export class ClientsFactory {
return new MySQLClient(args); return new MySQLClient(args);
case 'pg': case 'pg':
return new PostgreSQLClient(args); return new PostgreSQLClient(args);
case 'sqlite':
return new SQLiteClient(args);
default: default:
throw new Error(`Unknown database client: ${args.client}`); throw new Error(`Unknown database client: ${args.client}`);
} }

View File

@ -571,7 +571,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* CREATE DATABASE * CREATE DATABASE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async createSchema (params) { async createSchema (params) {
@ -581,7 +581,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* ALTER DATABASE * ALTER DATABASE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async alterSchema (params) { async alterSchema (params) {
@ -591,7 +591,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* DROP DATABASE * DROP DATABASE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async dropSchema (params) { async dropSchema (params) {
@ -631,7 +631,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* DROP VIEW * DROP VIEW
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async dropView (params) { async dropView (params) {
@ -642,7 +642,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* ALTER VIEW * ALTER VIEW
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async alterView (params) { async alterView (params) {
@ -663,7 +663,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* CREATE VIEW * CREATE VIEW
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async createView (params) { async createView (params) {
@ -696,7 +696,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* DROP TRIGGER * DROP TRIGGER
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async dropTrigger (params) { async dropTrigger (params) {
@ -707,7 +707,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* ALTER TRIGGER * ALTER TRIGGER
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async alterTrigger (params) { async alterTrigger (params) {
@ -729,7 +729,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* CREATE TRIGGER * CREATE TRIGGER
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async createTrigger (params) { async createTrigger (params) {
@ -803,7 +803,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* DROP PROCEDURE * DROP PROCEDURE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async dropRoutine (params) { async dropRoutine (params) {
@ -814,7 +814,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* ALTER PROCEDURE * ALTER PROCEDURE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async alterRoutine (params) { async alterRoutine (params) {
@ -836,7 +836,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* CREATE PROCEDURE * CREATE PROCEDURE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async createRoutine (params) { async createRoutine (params) {
@ -930,7 +930,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* DROP FUNCTION * DROP FUNCTION
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async dropFunction (params) { async dropFunction (params) {
@ -941,7 +941,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* ALTER FUNCTION * ALTER FUNCTION
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async alterFunction (params) { async alterFunction (params) {
@ -963,7 +963,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* CREATE FUNCTION * CREATE FUNCTION
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async createFunction (params) { async createFunction (params) {
@ -1024,7 +1024,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* DROP EVENT * DROP EVENT
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async dropEvent (params) { async dropEvent (params) {
@ -1035,7 +1035,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* ALTER EVENT * ALTER EVENT
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async alterEvent (params) { async alterEvent (params) {
@ -1061,7 +1061,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* CREATE EVENT * CREATE EVENT
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async createEvent (params) { async createEvent (params) {
@ -1205,7 +1205,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* CREATE TABLE * CREATE TABLE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async createTable (params) { async createTable (params) {
@ -1267,7 +1267,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* ALTER TABLE * ALTER TABLE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async alterTable (params) { async alterTable (params) {
@ -1402,7 +1402,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* DUPLICATE TABLE * DUPLICATE TABLE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async duplicateTable (params) { async duplicateTable (params) {
@ -1413,7 +1413,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* TRUNCATE TABLE * TRUNCATE TABLE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async truncateTable (params) { async truncateTable (params) {
@ -1424,7 +1424,7 @@ export class MySQLClient extends AntaresCore {
/** /**
* DROP TABLE * DROP TABLE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async dropTable (params) { async dropTable (params) {

View File

@ -506,7 +506,7 @@ export class PostgreSQLClient extends AntaresCore {
/** /**
* CREATE SCHEMA * CREATE SCHEMA
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async createSchema (params) { async createSchema (params) {
@ -516,7 +516,7 @@ export class PostgreSQLClient extends AntaresCore {
/** /**
* ALTER DATABASE * ALTER DATABASE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async alterSchema (params) { async alterSchema (params) {
@ -526,7 +526,7 @@ export class PostgreSQLClient extends AntaresCore {
/** /**
* DROP DATABASE * DROP DATABASE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async dropSchema (params) { async dropSchema (params) {
@ -558,7 +558,7 @@ export class PostgreSQLClient extends AntaresCore {
/** /**
* DROP VIEW * DROP VIEW
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof PostgreSQLClient * @memberof PostgreSQLClient
*/ */
async dropView (params) { async dropView (params) {
@ -569,7 +569,7 @@ export class PostgreSQLClient extends AntaresCore {
/** /**
* ALTER VIEW * ALTER VIEW
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof PostgreSQLClient * @memberof PostgreSQLClient
*/ */
async alterView (params) { async alterView (params) {
@ -585,7 +585,7 @@ export class PostgreSQLClient extends AntaresCore {
/** /**
* CREATE VIEW * CREATE VIEW
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof PostgreSQLClient * @memberof PostgreSQLClient
*/ */
async createView (params) { async createView (params) {
@ -640,7 +640,7 @@ export class PostgreSQLClient extends AntaresCore {
/** /**
* DROP TRIGGER * DROP TRIGGER
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof PostgreSQLClient * @memberof PostgreSQLClient
*/ */
async dropTrigger (params) { async dropTrigger (params) {
@ -652,7 +652,7 @@ export class PostgreSQLClient extends AntaresCore {
/** /**
* ALTER TRIGGER * ALTER TRIGGER
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof PostgreSQLClient * @memberof PostgreSQLClient
*/ */
async alterTrigger (params) { async alterTrigger (params) {
@ -674,7 +674,7 @@ export class PostgreSQLClient extends AntaresCore {
/** /**
* CREATE TRIGGER * CREATE TRIGGER
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof PostgreSQLClient * @memberof PostgreSQLClient
*/ */
async createTrigger (params) { async createTrigger (params) {
@ -1086,7 +1086,7 @@ export class PostgreSQLClient extends AntaresCore {
/** /**
* CREATE TABLE * CREATE TABLE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof PostgreSQLClient * @memberof PostgreSQLClient
*/ */
async createTable (params) { async createTable (params) {
@ -1144,7 +1144,7 @@ export class PostgreSQLClient extends AntaresCore {
/** /**
* ALTER TABLE * ALTER TABLE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof PostgreSQLClient * @memberof PostgreSQLClient
*/ */
async alterTable (params) { async alterTable (params) {
@ -1290,7 +1290,7 @@ export class PostgreSQLClient extends AntaresCore {
/** /**
* DUPLICATE TABLE * DUPLICATE TABLE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof PostgreSQLClient * @memberof PostgreSQLClient
*/ */
async duplicateTable (params) { async duplicateTable (params) {
@ -1301,7 +1301,7 @@ export class PostgreSQLClient extends AntaresCore {
/** /**
* TRUNCATE TABLE * TRUNCATE TABLE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof PostgreSQLClient * @memberof PostgreSQLClient
*/ */
async truncateTable (params) { async truncateTable (params) {
@ -1312,7 +1312,7 @@ export class PostgreSQLClient extends AntaresCore {
/** /**
* DROP TABLE * DROP TABLE
* *
* @returns {Array.<Object>} parameters * @returns {Promise<null>}
* @memberof PostgreSQLClient * @memberof PostgreSQLClient
*/ */
async dropTable (params) { async dropTable (params) {

View File

@ -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.<Object>} 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.<Object>} 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.<Object>} parameters
* @memberof SQLiteClient
*/
async dropView (params) {
const sql = `DROP VIEW "${params.schema}"."${params.view}"`;
return await this.raw(sql);
}
/**
* ALTER VIEW
*
* @returns {Array.<Object>} 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.<Object>} 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.<Object>} 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.<Object>} parameters
* @memberof SQLiteClient
*/
async dropTrigger (params) {
const sql = `DROP TRIGGER \`${params.schema}\`.\`${params.trigger}\``;
return await this.raw(sql);
}
/**
* ALTER TRIGGER
*
* @returns {Array.<Object>} 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.<Object>} 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.<Object>} collations list
* @memberof SQLiteClient
*/
async getCollations () {
return [];
}
/**
* SHOW VARIABLES
*
* @returns {Array.<Object>} variables list
* @memberof SQLiteClient
*/
async getVariables () {
return [];
}
/**
* SHOW ENGINES
*
* @returns {Array.<Object>} engines list
* @memberof SQLiteClient
*/
async getEngines () {
return {
name: 'SQLite',
support: 'YES',
comment: '',
isDefault: true
};
}
/**
* SHOW VARIABLES LIKE '%vers%'
*
* @returns {Array.<Object>} 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<null>}
* @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<null>}
* @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<null>}
* @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<null>}
* @memberof SQLiteClient
*/
async truncateTable (params) {
const sql = `DELETE FROM "${params.schema}"."${params.table}"`;
return await this.raw(sql);
}
/**
* DROP TABLE
*
* @returns {Promise<null>}
* @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;
}
}

View File

@ -107,14 +107,14 @@ else {
mainWindow = await createMainWindow(); mainWindow = await createMainWindow();
createAppMenu(); createAppMenu();
if (isDevelopment) // if (isDevelopment)
mainWindow.webContents.openDevTools(); // mainWindow.webContents.openDevTools();
process.on('uncaughtException', (error) => { process.on('uncaughtException', error => {
mainWindow.webContents.send('unhandled-exception', error); mainWindow.webContents.send('unhandled-exception', error);
}); });
process.on('unhandledRejection', (error) => { process.on('unhandledRejection', error => {
mainWindow.webContents.send('unhandled-exception', error); mainWindow.webContents.send('unhandled-exception', error);
}); });
}); });

View File

@ -11,6 +11,7 @@
<a class="tab-link">{{ $t('word.general') }}</a> <a class="tab-link">{{ $t('word.general') }}</a>
</li> </li>
<li <li
v-if="customizations.sslConnection"
class="tab-item c-hand" class="tab-item c-hand"
:class="{'active': selectedTab === 'ssl'}" :class="{'active': selectedTab === 'ssl'}"
@click="selectTab('ssl')" @click="selectTab('ssl')"
@ -18,6 +19,7 @@
<a class="tab-link">{{ $t('word.ssl') }}</a> <a class="tab-link">{{ $t('word.ssl') }}</a>
</li> </li>
<li <li
v-if="customizations.sshConnection"
class="tab-item c-hand" class="tab-item c-hand"
:class="{'active': selectedTab === 'ssh'}" :class="{'active': selectedTab === 'ssh'}"
@click="selectTab('ssh')" @click="selectTab('ssh')"
@ -49,25 +51,17 @@
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<select v-model="connection.client" class="form-select"> <select v-model="connection.client" class="form-select">
<option value="mysql"> <option
MySQL v-for="client in clients"
:key="client.slug"
:value="client.slug"
>
{{ client.name }}
</option> </option>
<option value="maria">
MariaDB
</option>
<option value="pg">
PostgreSQL
</option>
<!-- <option value="mssql">
Microsoft SQL
</option>
<option value="oracledb">
Oracle DB
</option> -->
</select> </select>
</div> </div>
</div> </div>
<div class="form-group columns"> <div v-if="!customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.hostName') }}/IP</label> <label class="form-label">{{ $t('word.hostName') }}/IP</label>
</div> </div>
@ -79,7 +73,20 @@
> >
</div> </div>
</div> </div>
<div class="form-group columns"> <div v-if="customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.database') }}</label>
</div>
<div class="column col-8 col-sm-12">
<BaseUploadInput
:value="connection.databasePath"
:message="$t('word.browse')"
@clear="pathClear('databasePath')"
@change="pathSelection($event, 'databasePath')"
/>
</div>
</div>
<div v-if="!customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.port') }}</label> <label class="form-label">{{ $t('word.port') }}</label>
</div> </div>
@ -105,7 +112,7 @@
> >
</div> </div>
</div> </div>
<div class="form-group columns"> <div v-if="!customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.user') }}</label> <label class="form-label">{{ $t('word.user') }}</label>
</div> </div>
@ -118,7 +125,7 @@
> >
</div> </div>
</div> </div>
<div class="form-group columns"> <div v-if="!customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.password') }}</label> <label class="form-label">{{ $t('word.password') }}</label>
</div> </div>
@ -144,7 +151,15 @@
> >
</div> </div>
</div> </div>
<div class="form-group columns"> <div v-if="customizations.readOnlyMode" class="form-group columns">
<div class="column col-4 col-sm-12" />
<div class="column col-8 col-sm-12">
<label class="form-checkbox form-inline">
<input v-model="connection.readonly" type="checkbox"><i class="form-icon" /> {{ $t('message.readOnlyMode') }}
</label>
</div>
</div>
<div v-if="!customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12" /> <div class="column col-4 col-sm-12" />
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<label class="form-checkbox form-inline"> <label class="form-checkbox form-inline">
@ -369,15 +384,23 @@ export default {
}, },
data () { data () {
return { return {
clients: [
{ name: 'MySQL', slug: 'mysql' },
{ name: 'MariaDB', slug: 'maria' },
{ name: 'PostgreSQL', slug: 'pg' },
{ name: 'SQLite', slug: 'sqlite' }
],
connection: { connection: {
name: '', name: '',
client: 'mysql', client: 'mysql',
host: '127.0.0.1', host: '127.0.0.1',
database: null, database: null,
databasePath: '',
port: null, port: null,
user: null, user: null,
password: '', password: '',
ask: false, ask: false,
readonly: false,
uid: uidGen('C'), uid: uidGen('C'),
ssl: false, ssl: false,
cert: '', cert: '',

View File

@ -11,6 +11,7 @@
<a class="tab-link">{{ $t('word.general') }}</a> <a class="tab-link">{{ $t('word.general') }}</a>
</li> </li>
<li <li
v-if="customizations.sslConnection"
class="tab-item c-hand" class="tab-item c-hand"
:class="{'active': selectedTab === 'ssl'}" :class="{'active': selectedTab === 'ssl'}"
@click="selectTab('ssl')" @click="selectTab('ssl')"
@ -18,6 +19,7 @@
<a class="tab-link">{{ $t('word.ssl') }}</a> <a class="tab-link">{{ $t('word.ssl') }}</a>
</li> </li>
<li <li
v-if="customizations.sshConnection"
class="tab-item c-hand" class="tab-item c-hand"
:class="{'active': selectedTab === 'ssh'}" :class="{'active': selectedTab === 'ssh'}"
@click="selectTab('ssh')" @click="selectTab('ssh')"
@ -49,19 +51,17 @@
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<select v-model="localConnection.client" class="form-select"> <select v-model="localConnection.client" class="form-select">
<option value="mysql"> <option
MySQL v-for="client in clients"
</option> :key="client.slug"
<option value="maria"> :value="client.slug"
MariaDB >
</option> {{ client.name }}
<option value="pg">
PostgreSQL
</option> </option>
</select> </select>
</div> </div>
</div> </div>
<div class="form-group columns"> <div v-if="!customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.hostName') }}/IP</label> <label class="form-label">{{ $t('word.hostName') }}/IP</label>
</div> </div>
@ -73,7 +73,20 @@
> >
</div> </div>
</div> </div>
<div class="form-group columns"> <div v-if="customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.database') }}</label>
</div>
<div class="column col-8 col-sm-12">
<BaseUploadInput
:value="localConnection.databasePath"
:message="$t('word.browse')"
@clear="pathClear('databasePath')"
@change="pathSelection($event, 'databasePath')"
/>
</div>
</div>
<div v-if="!customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.port') }}</label> <label class="form-label">{{ $t('word.port') }}</label>
</div> </div>
@ -99,7 +112,7 @@
> >
</div> </div>
</div> </div>
<div class="form-group columns"> <div v-if="!customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.user') }}</label> <label class="form-label">{{ $t('word.user') }}</label>
</div> </div>
@ -112,7 +125,7 @@
> >
</div> </div>
</div> </div>
<div class="form-group columns"> <div v-if="!customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.password') }}</label> <label class="form-label">{{ $t('word.password') }}</label>
</div> </div>
@ -138,7 +151,15 @@
> >
</div> </div>
</div> </div>
<div class="form-group columns"> <div v-if="customizations.readOnlyMode" class="form-group columns">
<div class="column col-4 col-sm-12" />
<div class="column col-8 col-sm-12">
<label class="form-checkbox form-inline">
<input v-model="localConnection.readonly" type="checkbox"><i class="form-icon" /> {{ $t('message.readOnlyMode') }}
</label>
</div>
</div>
<div v-if="!customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12" /> <div class="column col-4 col-sm-12" />
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<label class="form-checkbox form-inline"> <label class="form-checkbox form-inline">
@ -374,6 +395,12 @@ export default {
}, },
data () { data () {
return { return {
clients: [
{ name: 'MySQL', slug: 'mysql' },
{ name: 'MariaDB', slug: 'maria' },
{ name: 'PostgreSQL', slug: 'pg' },
{ name: 'SQLite', slug: 'sqlite' }
],
isConnecting: false, isConnecting: false,
isTesting: false, isTesting: false,
isAsking: false, isAsking: false,
@ -383,7 +410,7 @@ export default {
}, },
computed: { computed: {
customizations () { customizations () {
return customizations[this.connection.client]; return customizations[this.localConnection.client];
}, },
isBusy () { isBusy () {
return this.isConnecting || this.isTesting; return this.isConnecting || this.isTesting;

View File

@ -13,6 +13,7 @@
<span class="workspace-explorebar-title">{{ connectionName }}</span> <span class="workspace-explorebar-title">{{ connectionName }}</span>
<span v-if="workspace.connectionStatus === 'connected'" class="workspace-explorebar-tools"> <span v-if="workspace.connectionStatus === 'connected'" class="workspace-explorebar-tools">
<i <i
v-if="customizations.schemas"
class="mdi mdi-18px mdi-database-plus c-hand mr-2" class="mdi mdi-18px mdi-database-plus c-hand mr-2"
:title="$t('message.createNewSchema')" :title="$t('message.createNewSchema')"
@click="showNewDBModal" @click="showNewDBModal"

View File

@ -43,7 +43,7 @@
<span v-html="highlightWord(table.name)" /> <span v-html="highlightWord(table.name)" />
</a> </a>
<div <div
v-if="table.type === 'table'" v-if="table.type === 'table' && table.size !== false"
class="table-size tooltip tooltip-left mr-1" class="table-size tooltip tooltip-left mr-1"
:data-tooltip="formatBytes(table.size)" :data-tooltip="formatBytes(table.size)"
> >

View File

@ -65,7 +65,11 @@
> >
<span class="d-flex"><i class="mdi mdi-18px mdi-database-edit text-light pr-1" /> {{ $t('word.edit') }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-database-edit text-light pr-1" /> {{ $t('word.edit') }}</span>
</div> </div>
<div class="context-element" @click="showDeleteModal"> <div
v-if="workspace.customizations.schemaDrop"
class="context-element"
@click="showDeleteModal"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-database-remove text-light pr-1" /> {{ $t('word.delete') }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-database-remove text-light pr-1" /> {{ $t('word.delete') }}</span>
</div> </div>

View File

@ -506,6 +506,12 @@ export default {
uid: this.connection.uid, uid: this.connection.uid,
schema: this.schema, schema: this.schema,
table: this.table, table: this.table,
tableStructure: {
name: this.localOptions.name,
fields: this.localFields,
foreigns: this.localKeyUsage,
indexes: this.localIndexes
},
additions, additions,
changes, changes,
deletions, deletions,

View File

@ -367,7 +367,8 @@ export default {
getWorkspace: 'workspaces/getWorkspace' getWorkspace: 'workspaces/getWorkspace'
}), }),
localLength () { localLength () {
return this.localRow.numLength || this.localRow.charLength || this.localRow.datePrecision || this.localRow.numPrecision || 0; const localLength = this.localRow.numLength || this.localRow.charLength || this.localRow.datePrecision || this.localRow.numPrecision || 0;
return localLength === true ? null : localLength;
}, },
fieldType () { fieldType () {
const fieldType = this.dataTypes.reduce((acc, group) => [...acc, ...group.types], []).filter(type => const fieldType = this.dataTypes.reduce((acc, group) => [...acc, ...group.types], []).filter(type =>
@ -391,7 +392,7 @@ export default {
return this.indexes.some(index => ['PRIMARY', 'UNIQUE'].includes(index.type)); return this.indexes.some(index => ['PRIMARY', 'UNIQUE'].includes(index.type));
}, },
isNullable () { isNullable () {
return !this.indexes.some(index => ['PRIMARY'].includes(index.type)); return this.customizations.nullablePrimary || !this.indexes.some(index => ['PRIMARY'].includes(index.type));
}, },
isInDataTypes () { isInDataTypes () {
let typeNames = []; let typeNames = [];

View File

@ -93,7 +93,7 @@
<div v-if="resultsCount"> <div v-if="resultsCount">
{{ $t('word.results') }}: <b>{{ resultsCount.toLocaleString() }}</b> {{ $t('word.results') }}: <b>{{ resultsCount.toLocaleString() }}</b>
</div> </div>
<div v-if="affectedCount"> <div v-if="affectedCount !== null">
{{ $t('message.affectedRows') }}: <b>{{ affectedCount }}</b> {{ $t('message.affectedRows') }}: <b>{{ affectedCount }}</b>
</div> </div>
<div class="input-group" :title="$t('word.schema')"> <div class="input-group" :title="$t('word.schema')">
@ -170,7 +170,7 @@ export default {
selectedSchema: null, selectedSchema: null,
resultsCount: 0, resultsCount: 0,
durationsCount: 0, durationsCount: 0,
affectedCount: 0, affectedCount: null,
editorHeight: 200, editorHeight: 200,
isHistoryOpen: false isHistoryOpen: false
}; };
@ -255,9 +255,14 @@ export default {
if (status === 'success') { if (status === 'success') {
this.results = Array.isArray(response) ? response : [response]; this.results = Array.isArray(response) ? response : [response];
this.resultsCount += this.results.reduce((acc, curr) => acc + (curr.rows ? curr.rows.length : 0), 0); this.resultsCount = this.results.reduce((acc, curr) => acc + (curr.rows ? curr.rows.length : 0), 0);
this.durationsCount += this.results.reduce((acc, curr) => acc + curr.duration, 0); this.durationsCount = this.results.reduce((acc, curr) => acc + curr.duration, 0);
this.affectedCount += this.results.reduce((acc, curr) => acc + (curr.report ? curr.report.affectedRows : 0), 0); this.affectedCount = this.results
.filter(result => result.report !== null)
.reduce((acc, curr) => {
if (acc === null) acc = 0;
return acc + (curr.report ? curr.report.affectedRows : 0);
}, null);
this.updateTabContent({ this.updateTabContent({
uid: this.connection.uid, uid: this.connection.uid,
@ -285,7 +290,7 @@ export default {
this.results = []; this.results = [];
this.resultsCount = 0; this.resultsCount = 0;
this.durationsCount = 0; this.durationsCount = 0;
this.affectedCount = 0; this.affectedCount = null;
}, },
resize (e) { resize (e) {
const el = this.$refs.queryEditor.$el; const el = this.$refs.queryEditor.$el;

View File

@ -120,7 +120,12 @@
{{ $t('word.results') }}: <b>{{ results[0].rows.length | localeString }}</b> {{ $t('word.results') }}: <b>{{ results[0].rows.length | localeString }}</b>
</div> </div>
<div v-if="hasApproximately || (page > 1 && approximateCount)"> <div v-if="hasApproximately || (page > 1 && approximateCount)">
{{ $t('word.total') }}: <b :title="$t('word.approximately')"> {{ approximateCount | localeString }}</b> {{ $t('word.total') }}: <b
:title="!customizations.tableRealCount ? $t('word.approximately') : ''"
>
<span v-if="!customizations.tableRealCount"></span>
{{ approximateCount | localeString }}
</b>
</div> </div>
<div class="d-flex" :title="$t('word.schema')"> <div class="d-flex" :title="$t('word.schema')">
<i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b> <i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b>
@ -231,6 +236,9 @@ export default {
workspace () { workspace () {
return this.getWorkspace(this.connection.uid); return this.getWorkspace(this.connection.uid);
}, },
customizations () {
return this.workspace.customizations;
},
isTable () { isTable () {
return !!this.workspace.breadcrumbs.table; return !!this.workspace.breadcrumbs.table;
}, },

View File

@ -250,7 +250,8 @@ module.exports = {
searchForQueries: 'Search for queries', searchForQueries: 'Search for queries',
killProcess: 'Kill process', killProcess: 'Kill process',
closeTab: 'Close tab', closeTab: 'Close tab',
goToDownloadPage: 'Go to download page' goToDownloadPage: 'Go to download page',
readOnlyMode: 'Read-only mode'
}, },
faker: { faker: {
address: 'Address', address: 'Address',

View File

Before

Width:  |  Height:  |  Size: 1004 B

After

Width:  |  Height:  |  Size: 1004 B

View File

@ -17,10 +17,12 @@
( (
"char": $string-color, "char": $string-color,
"varchar": $string-color, "varchar": $string-color,
"longvarchar": $string-color,
"text": $string-color, "text": $string-color,
"tinytext": $string-color, "tinytext": $string-color,
"mediumtext": $string-color, "mediumtext": $string-color,
"longtext": $string-color, "longtext": $string-color,
"string": $string-color,
"json": $string-color, "json": $string-color,
"name": $string-color, "name": $string-color,
"character": $string-color, "character": $string-color,
@ -50,6 +52,7 @@
"oid": $number-color, "oid": $number-color,
"xid": $number-color, "xid": $number-color,
"money": $number-color, "money": $number-color,
"number": $number-color,
"datetime": $date-color, "datetime": $date-color,
"date": $date-color, "date": $date-color,
"time": $date-color, "time": $date-color,

View File

@ -20,6 +20,10 @@
background-image: url("../images/svg/pg.svg"); background-image: url("../images/svg/pg.svg");
} }
&.dbi-sqlite {
background-image: url("../images/svg/sqlite.svg");
}
&.dbi-oracledb { &.dbi-oracledb {
background-image: url("../images/svg/oracledb.svg"); background-image: url("../images/svg/oracledb.svg");
} }

View File

@ -24,12 +24,24 @@ export default {
getConnections: state => state.connections, getConnections: state => state.connections,
getConnectionName: state => uid => { getConnectionName: state => uid => {
const connection = state.connections.filter(connection => connection.uid === uid)[0]; const connection = state.connections.filter(connection => connection.uid === uid)[0];
if (!connection) return ''; let connectionName = '';
return connection.name
? connection.name if (connection.name)
: connection.ask connectionName = connection.name;
? `${connection.host}:${connection.port}` else if (connection.ask)
: `${connection.user + '@'}${connection.host}:${connection.port}`; connectionName = `${connection.host}:${connection.port}`;
else if (connection.databasePath) {
let string = connection.databasePath.split(/[/\\]+/).pop();
if (string.length >= 30)
string = `...${string.slice(-30)}`;
connectionName = string;
}
else
connectionName = `${connection.user + '@'}${connection.host}:${connection.port}`;
return connectionName;
} }
}, },
mutations: { mutations: {

View File

@ -412,6 +412,11 @@ export default {
indexTypes = require('common/index-types/postgresql'); indexTypes = require('common/index-types/postgresql');
customizations = require('common/customizations/postgresql'); customizations = require('common/customizations/postgresql');
break; break;
case 'sqlite':
dataTypes = require('common/data-types/sqlite');
indexTypes = require('common/index-types/sqlite');
customizations = require('common/customizations/sqlite');
break;
} }
const { status, response: version } = await Schema.getVersion(connection.uid); const { status, response: version } = await Schema.getVersion(connection.uid);