feat: mysql export for trigger, views, schedulers, functions and routines

This commit is contained in:
Giulio Ganci 2021-10-31 17:22:59 +01:00
parent 0de2321920
commit b2a5b40c03
7 changed files with 545 additions and 159 deletions

View File

@ -167,14 +167,12 @@ export default connections => {
} }
}); });
ipcMain.handle('export', async (event, { uid, ...rest }) => { ipcMain.handle('export', async (event, { uid, type, tables, ...rest }) => {
if (exporter !== null) return; if (exporter !== null) return;
const type = connections[uid]._client;
switch (type) { switch (type) {
case 'mysql': case 'mysql':
exporter = new MysqlExporter(connections[uid], rest); exporter = new MysqlExporter(connections[uid], tables, rest);
break; break;
default: default:
return { return {

View File

@ -3,8 +3,9 @@ import path from 'path';
import EventEmitter from 'events'; import EventEmitter from 'events';
export class BaseExporter extends EventEmitter { export class BaseExporter extends EventEmitter {
constructor (options) { constructor (tables, options) {
super(); super();
this._tables = tables;
this._options = options; this._options = options;
this._isCancelled = false; this._isCancelled = false;
this._outputStream = fs.createWriteStream(this._options.outputFile, { this._outputStream = fs.createWriteStream(this._options.outputFile, {

View File

@ -13,28 +13,36 @@ export default class MysqlExporter extends SqlExporter {
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
SET NAMES utf8mb4; SET NAMES utf8mb4;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE='NO_AUTO_VALUE_ON_ZERO', SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;`; /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;`;
return dump; return dump;
} }
async getFooter () { async getFooter () {
const footer = await super.getFooter();
return `/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; return `/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; /*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;`; /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
${footer}
`;
} }
async getCreateTable (tableName) { async getCreateTable (tableName) {
const { rows } = await this._client.raw(`SHOW CREATE TABLE \`${this.schemaName}\`.\`${tableName}\``); const { rows } = await this._client.raw(
`SHOW CREATE TABLE \`${this.schemaName}\`.\`${tableName}\``
);
if (rows.length !== 1) if (rows.length !== 1) return '';
return '';
return rows[0]['Create Table'] + ';'; const col = 'Create View' in rows[0] ? 'Create View' : 'Create Table';
return rows[0][col] + ';';
} }
getDropTable (tableName) { getDropTable (tableName) {
@ -45,54 +53,82 @@ SET NAMES utf8mb4;
let rowCount = 0; let rowCount = 0;
let sqlStr = ''; let sqlStr = '';
const countResults = await this._client.raw(`SELECT COUNT(1) as count FROM \`${this.schemaName}\`.\`${tableName}\``); const countResults = await this._client.raw(
if (countResults.rows.length === 1) `SELECT COUNT(1) as count FROM \`${this.schemaName}\`.\`${tableName}\``
rowCount = countResults.rows[0].count; );
if (countResults.rows.length === 1) rowCount = countResults.rows[0].count;
if (rowCount > 0) { if (rowCount > 0) {
const columns = await this._client.getTableColumns({ table: tableName, schema: this.schemaName }); let queryLength = 0;
let rowsWritten = 0;
const { sqlInsertDivider, sqlInsertAfter } = this._options;
const columns = await this._client.getTableColumns({
table: tableName,
schema: this.schemaName
});
const columnNames = columns.map(col => '`' + col.name + '`'); const columnNames = columns.map(col => '`' + col.name + '`');
const insertStmt = `INSERT INTO \`${tableName}\` (${columnNames.join(', ')}) VALUES`; const insertStmt = `INSERT INTO \`${tableName}\` (${columnNames.join(
', '
)}) VALUES`;
const tableResult = await this._client.raw(`SELECT ${columnNames.join(', ')} FROM \`${this.schemaName}\`.\`${tableName}\``); const tableResult = await this._client.raw(
`SELECT ${columnNames.join(', ')} FROM \`${
this.schemaName
}\`.\`${tableName}\``
);
sqlStr += `LOCK TABLES \`${tableName}\` WRITE;\n`; sqlStr += `LOCK TABLES \`${tableName}\` WRITE;\n`;
sqlStr += `/*!40000 ALTER TABLE \`${tableName}\` DISABLE KEYS */;`; sqlStr += `/*!40000 ALTER TABLE \`${tableName}\` DISABLE KEYS */;`;
sqlStr += '\n\n'; sqlStr += '\n\n';
sqlStr += insertStmt; sqlStr += insertStmt;
sqlStr += '\n';
for (const row of tableResult.rows) { for (const rowIndex in tableResult.rows) {
sqlStr += '\t('; const row = tableResult.rows[rowIndex];
let sqlInsertString = '';
if (
(sqlInsertDivider === 'bytes' &&
queryLength >= sqlInsertAfter * 1024) ||
(sqlInsertDivider === 'rows' && rowsWritten === sqlInsertAfter)
) {
sqlInsertString += `;\n${insertStmt}\n\t(`;
queryLength = 0;
rowsWritten = 0;
}
else if (parseInt(rowIndex) === 0) sqlInsertString += '\n\t(';
else sqlInsertString += ',\n\t(';
for (const i in columns) { for (const i in columns) {
const column = columns[i]; const column = columns[i];
const val = row[column.name]; const val = row[column.name];
if (val === null) if (val === null) sqlInsertString += 'NULL';
sqlStr += 'NULL'; else if (BIT.includes(column.type)) {
sqlInsertString += `b'${hexToBinary(
else if (BIT.includes(column.type)) Buffer.from(val).toString('hex')
sqlStr += `b'${hexToBinary(Buffer.from(val).toString('hex'))}'`; )}'`;
}
else if (BLOB.includes(column.type)) else if (BLOB.includes(column.type))
sqlStr += `X'${val.toString('hex').toUpperCase()}'`; sqlInsertString += `X'${val.toString('hex').toUpperCase()}'`;
else if (val === '') sqlInsertString += '\'\'';
else if (val === '') else {
sqlStr += '\'\''; sqlInsertString +=
typeof val === 'string' ? this.escapeAndQuote(val) : val;
else
sqlStr += typeof val === 'string' ? this.escapeAndQuote(val) : val;
if (parseInt(i) !== columns.length - 1)
sqlStr += ', ';
} }
sqlStr += '),\n'; if (parseInt(i) !== columns.length - 1) sqlInsertString += ', ';
} }
sqlStr += '\n'; sqlInsertString += ')';
sqlStr += sqlInsertString;
queryLength += sqlInsertString.length;
rowsWritten++;
}
sqlStr += ';\n\n';
sqlStr += `/*!40000 ALTER TABLE \`${tableName}\` ENABLE KEYS */;\n`; sqlStr += `/*!40000 ALTER TABLE \`${tableName}\` ENABLE KEYS */;\n`;
sqlStr += 'UNLOCK TABLES;'; sqlStr += 'UNLOCK TABLES;';
@ -101,6 +137,173 @@ SET NAMES utf8mb4;
return sqlStr; return sqlStr;
} }
async getViews () {
const { rows: views } = await this._client.raw(
`SHOW TABLE STATUS FROM \`${this.schemaName}\` WHERE Comment = 'VIEW'`
);
let sqlString = '';
for (const view of views) {
sqlString += `DROP VIEW IF EXISTS \`${view.Name}\`;\n`;
const viewSyntax = await this.getCreateTable(view.Name);
sqlString += viewSyntax.replaceAll('`' + this.schemaName + '`.', '');
sqlString += '\n';
}
return sqlString;
}
async getTriggers () {
const { rows: triggers } = await this._client.raw(
`SHOW TRIGGERS FROM \`${this.schemaName}\``
);
const generatedTables = this._tables
.filter(t => t.includeStructure)
.map(t => t.table);
let sqlString = '';
for (const trigger of triggers) {
const {
Trigger: name,
Timing: timing,
Event: event,
Table: table,
Statement: statement,
sql_mode: sqlMode
} = trigger;
if (!generatedTables.includes(table)) continue;
const definer = this.getEscapedDefiner(trigger.Definer);
sqlString += '/*!50003 SET @OLD_SQL_MODE=@@SQL_MODE*/;;\n';
sqlString += `/*!50003 SET SQL_MODE="${sqlMode}" */;\n`;
sqlString += 'DELIMITER ;;\n';
sqlString += '/*!50003 CREATE*/ ';
sqlString += `/*!50017 DEFINER=${definer}*/ `;
sqlString += `/*!50003 TRIGGER \`${name}\` ${timing} ${event} ON ${table} FOR EACH ROW ${statement}*/;;\n`;
sqlString += 'DELIMITER ;\n';
sqlString += '/*!50003 SET SQL_MODE=@OLD_SQL_MODE */;\n\n';
}
return sqlString;
}
async getSchedulers () {
const { rows: schedulers } = await this._client.raw(
`SELECT *, EVENT_SCHEMA AS \`Db\`, EVENT_NAME AS \`Name\` FROM information_schema.\`EVENTS\` WHERE EVENT_SCHEMA = '${this.schemaName}'`
);
let sqlString = '';
for (const scheduler of schedulers) {
const {
EVENT_NAME: name,
SQL_MODE: sqlMode,
EVENT_TYPE: type,
INTERVAL_VALUE: intervalValue,
INTERVAL_FIELD: intervalField,
STARTS: starts,
ENDS: ends,
EXECUTE_AT: at,
ON_COMPLETION: onCompletion,
STATUS: status,
EVENT_DEFINITION: definition
} = scheduler;
const definer = this.getEscapedDefiner(scheduler.DEFINER);
const comment = this.escapeAndQuote(scheduler.EVENT_COMMENT);
sqlString += `/*!50106 DROP EVENT IF EXISTS \`${name}\` */;\n`;
sqlString += '/*!50003 SET @OLD_SQL_MODE=@@SQL_MODE*/;;\n';
sqlString += `/*!50003 SET SQL_MODE='${sqlMode}' */;\n`;
sqlString += 'DELIMITER ;;\n';
sqlString += '/*!50106 CREATE*/ ';
sqlString += `/*!50117 DEFINER=${definer}*/ `;
sqlString += `/*!50106 EVENT \`${name}\` ON SCHEDULE `;
if (type === 'RECURRING') {
sqlString += `EVERY ${intervalValue} ${intervalField} STARTS '${starts}' `;
if (ends) sqlString += `ENDS '${ends}' `;
}
else sqlString += `AT '${at}' `;
sqlString += `ON COMPLETION ${onCompletion} ${
status === 'disabled' ? 'DISABLE' : 'ENABLE'
} COMMENT ${comment || '\'\''} DO ${definition}*/;;\n`;
sqlString += 'DELIMITER ;\n';
sqlString += '/*!50003 SET SQL_MODE=@OLD_SQL_MODE*/;;\n';
}
return sqlString;
}
async getFunctions () {
const { rows: functions } = await this._client.raw(
`SHOW FUNCTION STATUS WHERE \`Db\` = '${this.schemaName}';`
);
let sqlString = '';
for (const func of functions) {
sqlString += await this.getRoutineSyntax(
func.Name,
func.Type,
func.Definer
);
}
return sqlString;
}
async getRoutines () {
const { rows: routines } = await this._client.raw(
`SHOW PROCEDURE STATUS WHERE \`Db\` = '${this.schemaName}';`
);
let sqlString = '';
for (const routine of routines) {
sqlString += await this.getRoutineSyntax(
routine.Name,
routine.Type,
routine.Definer
);
}
return sqlString;
}
async getRoutineSyntax (name, type, definer) {
const { rows: routines } = await this._client.raw(
`SHOW CREATE ${type} \`${this.schemaName}\`.\`${name}\``
);
if (routines.length === 0) return '';
const routine = routines[0];
const { sql_mode: sqlMode, 'Create Function': createProcedure } = routine;
const startOffset = createProcedure.indexOf(type);
const procedureBody = createProcedure.substring(startOffset);
let sqlString = 'DELIMITER ;;\n';
sqlString = `/*!50003 DROP ${type} IF EXISTS ${name}*/;;\n`;
sqlString += '/*!50003 SET @OLD_SQL_MODE=@@SQL_MODE*/;;\n';
sqlString += `/*!50003 SET SQL_MODE="${sqlMode}"*/;;\n`;
sqlString += `/*!50003 CREATE*/ /*!50020 DEFINER=${definer}*/ /*!50003 ${procedureBody}*/;;\n`;
sqlString += '/*!50003 SET SQL_MODE=@OLD_SQL_MODE*/;;\n';
sqlString += 'DELIMITER ;\n';
return sqlString;
}
getEscapedDefiner (definer) {
return definer
.split('@')
.map(part => '`' + part + '`')
.join('@');
}
escapeAndQuote (value) { escapeAndQuote (value) {
if (!value) return null; if (!value) return null;
return `'${value.replaceAll(/'/g, '\'\'')}'`; return `'${value.replaceAll(/'/g, '\'\'')}'`;

View File

@ -3,8 +3,8 @@ import moment from 'moment';
import { BaseExporter } from '../BaseExporter'; import { BaseExporter } from '../BaseExporter';
export class SqlExporter extends BaseExporter { export class SqlExporter extends BaseExporter {
constructor (client, options) { constructor (client, tables, options) {
super(options); super(tables, options);
this._client = client; this._client = client;
this._commentChar = '#'; this._commentChar = '#';
} }
@ -23,8 +23,15 @@ export class SqlExporter extends BaseExporter {
} }
async dump () { async dump () {
const { includes } = this._options;
const extraItems = Object.keys(includes).filter(key => includes[key]);
const totalTableToProcess = this._tables.filter(
t => t.includeStructure || t.includeContent || t.includeDropStatement
).length;
const processingItemCount = totalTableToProcess + extraItems.length;
const exportState = { const exportState = {
totalItems: this._options.items.length, totalItems: processingItemCount,
currentItemIndex: 0, currentItemIndex: 0,
currentItem: '', currentItem: '',
op: '' op: ''
@ -34,13 +41,16 @@ export class SqlExporter extends BaseExporter {
this.writeString(header); this.writeString(header);
this.writeString('\n\n\n'); this.writeString('\n\n\n');
for (const item of this._options.items) { for (const item of this._tables) {
// user abort operation // user abort operation
if (this.isCancelled) if (this.isCancelled) return;
return;
// skip item if not set to output any detail for them // skip item if not set to output any detail for them
if (!item.includeStructure && !item.includeContent && !item.includeDropStatement) if (
!item.includeStructure &&
!item.includeContent &&
!item.includeDropStatement
)
continue; continue;
exportState.currentItemIndex++; exportState.currentItemIndex++;
@ -49,7 +59,9 @@ export class SqlExporter extends BaseExporter {
this.emitUpdate(exportState); this.emitUpdate(exportState);
const tableHeader = this.buildComment(`Dump of table ${item.table}\n------------------------------------------------------------`); const tableHeader = this.buildComment(
`Dump of table ${item.table}\n------------------------------------------------------------`
);
this.writeString(tableHeader); this.writeString(tableHeader);
this.writeString('\n\n'); this.writeString('\n\n');
@ -79,12 +91,38 @@ export class SqlExporter extends BaseExporter {
this.writeString('\n\n'); this.writeString('\n\n');
} }
for (const item of extraItems) {
const processingMethod = `get${item.charAt(0).toUpperCase() +
item.slice(1)}`;
exportState.currentItemIndex++;
exportState.currentItem = item;
exportState.op = 'PROCESSING';
this.emitUpdate(exportState);
if (this[processingMethod]) {
const data = await this[processingMethod]();
if (data !== '') {
const header =
this.buildComment(
`Dump of ${item}\n------------------------------------------------------------`
) + '\n\n';
this.writeString(header);
this.writeString(data);
this.writeString('\n\n');
}
}
}
const footer = await this.getFooter(); const footer = await this.getFooter();
this.writeString(footer); this.writeString(footer);
} }
buildComment (text) { buildComment (text) {
return text.split('\n').map(txt => `${this._commentChar} ${txt}`).join('\n'); return text
.split('\n')
.map(txt => `${this._commentChar} ${txt}`)
.join('\n');
} }
async getSqlHeader () { async getSqlHeader () {
@ -105,11 +143,13 @@ Generation time: ${moment().format()}
} }
async getFooter () { async getFooter () {
return ''; return this.buildComment(`Dump completed on ${moment().format()}`);
} }
getCreateTable (tableName) { getCreateTable (tableName) {
throw new Error('Sql Exporter must implement the "getCreateTable" method'); throw new Error(
'Sql Exporter must implement the "getCreateTable" method'
);
} }
getDropTable (tableName) { getDropTable (tableName) {
@ -117,6 +157,8 @@ Generation time: ${moment().format()}
} }
getTableInsert (tableName) { getTableInsert (tableName) {
throw new Error('Sql Exporter must implement the "getTableInsert" method'); throw new Error(
'Sql Exporter must implement the "getTableInsert" method'
);
} }
} }

View File

@ -37,46 +37,58 @@
</fieldset> </fieldset>
</div> </div>
</div> </div>
</div>
<div class="columns export-options">
<div class="column col-8 left">
<div class="columns mb-2"> <div class="columns mb-2">
<div class="column col-auto d-flex p-0 text-italic "> <div class="column col-auto d-flex text-italic ">
<i class="mdi mdi-file-document-outline mr-2" /> <i class="mdi mdi-file-document-outline mr-2" />
{{ filename }} {{ filename }}
</div> </div>
<div class="column col-auto col-ml-auto p-0"> <div class="column col-auto col-ml-auto ">
<button class="btn btn-dark btn-sm" @click="uncheckAllTables"> <button class="btn btn-dark btn-sm" @click="refresh">
<i class="mdi mdi-database-refresh" />
</button>
<button
class="btn btn-dark btn-sm"
:disabled="isRefreshing"
@click="uncheckAllTables"
>
<i class="mdi mdi-file-tree-outline" /> <i class="mdi mdi-file-tree-outline" />
</button> </button>
<button class="btn btn-dark btn-sm" @click="checkAllTables"> <button
class="btn btn-dark btn-sm"
:disabled="isRefreshing"
@click="checkAllTables"
>
<i class="mdi mdi-file-tree" /> <i class="mdi mdi-file-tree" />
</button> </button>
</div> </div>
</div> </div>
</div>
<div class="workspace-query-results"> <div class="workspace-query-results">
<div ref="table" class="table table-hover"> <div ref="table" class="table table-hover">
<div class="thead"> <div class="thead">
<div class="tr"> <div class="tr">
<div class="th c-hand" style="width: 50%;"> <div class="th c-hand" style="width: 50%;">
<div class="table-column-title"> <div class="table-column-title">
<span>Table</span> <span>{{ $t('word.table') }}</span>
</div> </div>
</div> </div>
<div class="th c-hand"> <div class="th c-hand text-center">
<div class="table-column-title"> <div class="table-column-title">
<span>Structure</span> <span>{{ $t('word.structure') }}</span>
</div> </div>
</div> </div>
<div class="th c-hand"> <div class="th c-hand text-center">
<div class="table-column-title"> <div class="table-column-title">
<span>Content</span> <span>{{ $t('word.content') }}</span>
</div> </div>
</div> </div>
<div class="th c-hand"> <div class="th c-hand text-center">
<div class="table-column-title"> <div class="table-column-title">
<span>Drop</span> <span>{{ $t('word.drop') }}</span>
</div> </div>
</div> </div>
</div> </div>
@ -91,24 +103,24 @@
<div class="td"> <div class="td">
{{ item.table }} {{ item.table }}
</div> </div>
<div class="td"> <div class="td text-center">
<label class="form-checkbox m-0 px-2"> <label class="form-checkbox m-0 px-2 form-inline">
<input <input
v-model="item.includeStructure" v-model="item.includeStructure"
type="checkbox" type="checkbox"
><i class="form-icon" /> ><i class="form-icon" />
</label> </label>
</div> </div>
<div class="td"> <div class="td text-center">
<label class="form-checkbox m-0 px-2"> <label class="form-checkbox m-0 px-2 form-inline">
<input <input
v-model="item.includeContent" v-model="item.includeContent"
type="checkbox" type="checkbox"
><i class="form-icon" /> ><i class="form-icon" />
</label> </label>
</div> </div>
<div class="td"> <div class="td text-center">
<label class="form-checkbox m-0 px-2"> <label class="form-checkbox m-0 px-2 form-inline">
<input <input
v-model="item.includeDropStatement" v-model="item.includeDropStatement"
type="checkbox" type="checkbox"
@ -119,29 +131,74 @@
</div> </div>
</div> </div>
</div> </div>
<div v-if="progressPercentage > 0"> </div>
<div class="column col-4">
<h4>
{{ $t('word.options') }}
</h4>
<span>{{ $t('word.includes') }}:</span>
<label
v-for="(_, key) in options.includes"
:key="key"
class="form-checkbox"
>
<input v-model="options.includes[key]" type="checkbox"><i class="form-icon" /> {{ $t(`word.${key}`) }}
</label>
<div class="mt-4 mb-2">
{{ $t('message.newInserStmtEvery') }}:
</div>
<div class="columns">
<div class="column col-6">
<input
v-model.number="options.sqlInsertAfter"
type="number"
class="form-input"
value="250"
>
</div>
<div class="column col-6">
<select v-model="options.sqlInsertDivider" class="form-select">
<option value="bytes">
KiB
</option>
<option value="rows">
{{ $t('word.rows') }}
</option>
</select>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer columns">
<div class="column col modal-progress-wrapper text-left">
<div v-if="progressPercentage > 0" class="export-progress">
<span class="progress-status">
{{ progressPercentage }}% - {{ progressStatus }}
</span>
<progress <progress
class="progress" class="progress d-block"
:value="progressPercentage" :value="progressPercentage"
max="100" max="100"
/> />
<p class="empty-subtitle">
{{ progressPercentage }}% - {{ progressStatus }}
</p>
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="column col-auto px-0">
<button
class="btn btn-primary mr-2"
:class="{'loading': isExporting}"
:disabled="isExporting"
@click.stop="startExport"
>
{{ $t('word.export') }}
</button>
<button class="btn btn-link" @click.stop="closeModal"> <button class="btn btn-link" @click.stop="closeModal">
{{ $t('word.close') }} {{ $t('word.close') }}
</button> </button>
<button
class="btn btn-primary mr-2"
:class="{'loading': isExporting}"
:disabled="isExporting || isRefreshing"
autofocus
@click.prevent="startExport"
>
{{ $t('word.export') }}
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -151,6 +208,7 @@
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import { mapActions, mapGetters } from 'vuex'; import { mapActions, mapGetters } from 'vuex';
import moment from 'moment'; import moment from 'moment';
import customizations from 'common/customizations';
import Application from '@/ipc-api/Application'; import Application from '@/ipc-api/Application';
import Schema from '@/ipc-api/Schema'; import Schema from '@/ipc-api/Schema';
@ -163,9 +221,15 @@ export default {
data () { data () {
return { return {
isExporting: false, isExporting: false,
isRefreshing: false,
progressPercentage: 0, progressPercentage: 0,
progressStatus: '', progressStatus: '',
tables: [], tables: [],
options: {
includes: {},
sqlInsertAfter: 250,
sqlInsertDivider: 'bytes'
},
basePath: '' basePath: ''
}; };
}, },
@ -194,7 +258,7 @@ export default {
} }
}, },
async created () { async created () {
await this.refreshSchema({ uid: this.currentWorkspace.uid, schema: this.selectedSchema }); await this.refresh();
window.addEventListener('keydown', this.onKey); window.addEventListener('keydown', this.onKey);
this.basePath = await Application.getDownloadPathDirectory(); this.basePath = await Application.getDownloadPathDirectory();
this.tables = this.schemaItems.map(item => ({ this.tables = this.schemaItems.map(item => ({
@ -204,6 +268,14 @@ export default {
includeDropStatement: true includeDropStatement: true
})); }));
const structure = ['views', 'triggers', 'routines', 'functions', 'schedulers', 'triggerFunctions'];
structure.forEach(feat => {
const val = customizations[this.currentWorkspace.client][feat];
if (val)
this.$set(this.options.includes, feat, true);
});
ipcRenderer.on('export-progress', this.updateProgress); ipcRenderer.on('export-progress', this.updateProgress);
}, },
beforeDestroy () { beforeDestroy () {
@ -216,18 +288,20 @@ export default {
}), }),
async startExport () { async startExport () {
this.isExporting = true; this.isExporting = true;
const { uid } = this.currentWorkspace; const { uid, client } = this.currentWorkspace;
const params = { const params = {
uid, uid,
type: client,
schema: this.selectedSchema, schema: this.selectedSchema,
outputFile: this.dumpFilePath, outputFile: this.dumpFilePath,
items: [...this.tables] tables: [...this.tables],
...this.options
}; };
const result = await Schema.export(params); const result = await Schema.export(params);
if (result) { if (result) {
if (result.status === 'success') if (result.status === 'success')
this.progressStatus = result.response.cancelled ? 'Aborted' : 'Completed!'; this.progressStatus = result.response.cancelled ? this.$t('word.aborted') : this.$t('word.completed');
else else
this.progressStatus = result.response; this.progressStatus = result.response;
@ -237,7 +311,17 @@ export default {
}, },
updateProgress (event, state) { updateProgress (event, state) {
this.progressPercentage = Number((state.currentItemIndex / state.totalItems * 100).toFixed(1)); this.progressPercentage = Number((state.currentItemIndex / state.totalItems * 100).toFixed(1));
this.progressStatus = state.op + ' ' + state.currentItem; switch (state.op) {
case 'PROCESSING':
this.progressStatus = this.$t('message.processingTableExport', { table: state.currentItem });
break;
case 'FETCH':
this.progressStatus = this.$t('message.fechingTableExport', { table: state.currentItem });
break;
case 'WRITE':
this.progressStatus = this.$t('message.writingTableExport', { table: state.currentItem });
break;
}
}, },
async closeModal () { async closeModal () {
let willClose = true; let willClose = true;
@ -261,6 +345,11 @@ export default {
uncheckAllTables () { uncheckAllTables () {
this.tables = this.tables.map(item => ({ table: item.table, includeStructure: false, includeContent: false, includeDropStatement: false })); this.tables = this.tables.map(item => ({ table: item.table, includeStructure: false, includeContent: false, includeDropStatement: false }));
}, },
async refresh () {
this.isRefreshing = true;
await this.refreshSchema({ uid: this.currentWorkspace.uid, schema: this.selectedSchema });
this.isRefreshing = false;
},
async openPathDialog () { async openPathDialog () {
const result = await Application.showOpenDialog({ properties: ['openDirectory'] }); const result = await Application.showOpenDialog({ properties: ['openDirectory'] });
if (result && !result.canceled) if (result && !result.canceled)
@ -271,9 +360,19 @@ export default {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.workspace-query-results { .export-options {
flex: 1 1 auto; flex: 1;
overflow: hidden;
.left {
display: flex;
flex-direction: column;
flex: 1;
}
}
.workspace-query-results {
flex: 1 0 1px;
.table { .table {
width: 100% !important; width: 100% !important;
} }
@ -292,13 +391,22 @@ export default {
.modal-container { .modal-container {
max-width: 800px; max-width: 800px;
}
.modal-body { .modal-body {
height: 60vh; max-height: 60vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.modal-footer {
display: flex;
} }
} }
.progress-status {
font-style: italic;
font-size: 80%;
}
</style> </style>

View File

@ -122,7 +122,17 @@ module.exports = {
select: 'Select', select: 'Select',
passphrase: 'Passphrase', passphrase: 'Passphrase',
filter: 'Filter', filter: 'Filter',
change: 'Change' change: 'Change',
views: 'Views',
triggers: 'Triggers',
routines: 'Routines',
functions: 'Functions',
schedulers: 'Schedulers',
includes: 'Includes',
drop: 'Drop',
rows: 'Rows',
completed: 'Completed',
aborted: 'Aborted'
}, },
message: { message: {
appWelcome: 'Welcome to Antares SQL Client!', appWelcome: 'Welcome to Antares SQL Client!',
@ -145,10 +155,12 @@ module.exports = {
downloadingUpdate: 'Downloading update', downloadingUpdate: 'Downloading update',
updateDownloaded: 'Update downloaded', updateDownloaded: 'Update downloaded',
restartToInstall: 'Restart Antares to install', restartToInstall: 'Restart Antares to install',
unableEditFieldWithoutPrimary: 'Unable to edit a field without a primary key in resultset', unableEditFieldWithoutPrimary:
'Unable to edit a field without a primary key in resultset',
editCell: 'Edit cell', editCell: 'Edit cell',
deleteRows: 'Delete row | Delete {count} rows', deleteRows: 'Delete row | Delete {count} rows',
confirmToDeleteRows: 'Do you confirm to delete one row? | Do you confirm to delete {count} rows?', confirmToDeleteRows:
'Do you confirm to delete one row? | Do you confirm to delete {count} rows?',
notificationsTimeout: 'Notifications timeout', notificationsTimeout: 'Notifications timeout',
uploadFile: 'Upload file', uploadFile: 'Upload file',
addNewRow: 'Add new row', addNewRow: 'Add new row',
@ -176,7 +188,8 @@ module.exports = {
deleteTable: 'Delete table', deleteTable: 'Delete table',
emptyCorfirm: 'Do you confirm to empty', emptyCorfirm: 'Do you confirm to empty',
unsavedChanges: 'Unsaved changes', unsavedChanges: 'Unsaved changes',
discardUnsavedChanges: 'You have some unsaved changes. Closing this tab these changes will be discarded.', discardUnsavedChanges:
'You have some unsaved changes. Closing this tab these changes will be discarded.',
thereAreNoIndexes: 'There are no indexes', thereAreNoIndexes: 'There are no indexes',
thereAreNoForeign: 'There are no foreign keys', thereAreNoForeign: 'There are no foreign keys',
createNewForeign: 'Create new foreign key', createNewForeign: 'Create new foreign key',
@ -249,7 +262,11 @@ module.exports = {
killProcess: 'Kill process', killProcess: 'Kill process',
closeTab: 'Close tab', closeTab: 'Close tab',
exportSchema: 'Export schema', exportSchema: 'Export schema',
directoryPath: 'Directory path' directoryPath: 'Directory path',
newInserStmtEvery: 'New INSERT statement every',
processingTableExport: 'Processing {table}',
fechingTableExport: 'Fetching {table} data',
writingTableExport: 'Writing {table} data'
}, },
faker: { faker: {
address: 'Address', address: 'Address',

View File

@ -122,7 +122,17 @@ module.exports = {
select: 'Seleziona', select: 'Seleziona',
passphrase: 'Passphrase', passphrase: 'Passphrase',
filter: 'Filtra', filter: 'Filtra',
change: 'Cambia' change: 'Cambia',
views: 'Viste',
triggers: 'Trigger',
routines: 'Routine',
functions: 'Function',
schedulers: 'Scheduler',
includes: 'Includi',
drop: 'Drop',
rows: 'Righe',
completed: 'Completato',
aborted: 'Annullato'
}, },
message: { message: {
appWelcome: 'Benvenuto in Antares SQL Client!', appWelcome: 'Benvenuto in Antares SQL Client!',
@ -145,10 +155,12 @@ module.exports = {
downloadingUpdate: 'Download dell\'aggiornamento', downloadingUpdate: 'Download dell\'aggiornamento',
updateDownloaded: 'Aggiornamento scaricato', updateDownloaded: 'Aggiornamento scaricato',
restartToInstall: 'Riavvia Antares per installare l\'aggiornamento', restartToInstall: 'Riavvia Antares per installare l\'aggiornamento',
unableEditFieldWithoutPrimary: 'Impossibile modificare il campo senza una primary key nel resultset', unableEditFieldWithoutPrimary:
'Impossibile modificare il campo senza una primary key nel resultset',
editCell: 'Modifica cella', editCell: 'Modifica cella',
deleteRows: 'Elimina riga | Elimina {count} righe', deleteRows: 'Elimina riga | Elimina {count} righe',
confirmToDeleteRows: 'Confermi di voler cancellare una riga? | Confermi di voler cancellare {count} righe?', confirmToDeleteRows:
'Confermi di voler cancellare una riga? | Confermi di voler cancellare {count} righe?',
notificationsTimeout: 'Timeout Notifiche', notificationsTimeout: 'Timeout Notifiche',
uploadFile: 'Carica file', uploadFile: 'Carica file',
addNewRow: 'Aggiungi nuova riga', addNewRow: 'Aggiungi nuova riga',
@ -176,7 +188,8 @@ module.exports = {
deleteTable: 'Cancella tabella', deleteTable: 'Cancella tabella',
emptyCorfirm: 'Confermi di voler svuotare', emptyCorfirm: 'Confermi di voler svuotare',
unsavedChanges: 'Modifiche non salvate', unsavedChanges: 'Modifiche non salvate',
discardUnsavedChanges: 'Hai modifiche non salvate. Lasciando questa scheda le modifiche saranno scartate.', discardUnsavedChanges:
'Hai modifiche non salvate. Lasciando questa scheda le modifiche saranno scartate.',
thereAreNoIndexes: 'Non ci sono indici', thereAreNoIndexes: 'Non ci sono indici',
thereAreNoForeign: 'Non ci sono chiavi esterne', thereAreNoForeign: 'Non ci sono chiavi esterne',
createNewForeign: 'Crea nuova chiave esterna', createNewForeign: 'Crea nuova chiave esterna',
@ -236,7 +249,11 @@ module.exports = {
noSchema: 'Nessuno schema', noSchema: 'Nessuno schema',
restorePreviourSession: 'Ripristina sessione precedente', restorePreviourSession: 'Ripristina sessione precedente',
exportSchema: 'Esporta schema', exportSchema: 'Esporta schema',
directoryPath: 'Percorso directory' directoryPath: 'Percorso directory',
newInserStmtEvery: 'Nuova istruzione INSERT ogni',
processingTableExport: 'Processo {table}',
fechingTableExport: 'Ricavo i dati {table}',
writingTableExport: 'Scrittura dati {table}'
}, },
faker: { faker: {
address: 'Indirizzo', address: 'Indirizzo',