diff --git a/packages/api/src/controllers/databaseConnections.js b/packages/api/src/controllers/databaseConnections.js index 016078e0..02ec95c7 100644 --- a/packages/api/src/controllers/databaseConnections.js +++ b/packages/api/src/controllers/databaseConnections.js @@ -171,11 +171,11 @@ module.exports = { }, runScript_meta: true, - async runScript({ conid, database, sql }, req) { + async runScript({ conid, database, sql, useTransaction }, req) { testConnectionPermission(conid, req); logger.info({ conid, database, sql }, 'Processing script'); const opened = await this.ensureOpened(conid, database); - const res = await this.sendRequest(opened, { msgtype: 'runScript', sql }); + const res = await this.sendRequest(opened, { msgtype: 'runScript', sql, useTransaction }); return res; }, diff --git a/packages/api/src/proc/databaseConnectionProcess.js b/packages/api/src/proc/databaseConnectionProcess.js index cc94ebe8..9684cfe1 100644 --- a/packages/api/src/proc/databaseConnectionProcess.js +++ b/packages/api/src/proc/databaseConnectionProcess.js @@ -158,12 +158,12 @@ function resolveAnalysedPromises() { afterAnalyseCallbacks = []; } -async function handleRunScript({ msgid, sql }, skipReadonlyCheck = false) { +async function handleRunScript({ msgid, sql, useTransaction }, skipReadonlyCheck = false) { await waitConnected(); const driver = requireEngineDriver(storedConnection); try { if (!skipReadonlyCheck) ensureExecuteCustomScript(driver); - await driver.script(systemConnection, sql); + await driver.script(systemConnection, sql, { useTransaction }); process.send({ msgtype: 'response', msgid }); } catch (err) { process.send({ msgtype: 'response', msgid, errorMessage: err.message }); diff --git a/packages/tools/src/SqlDumper.ts b/packages/tools/src/SqlDumper.ts index 82af25c6..84a025cf 100644 --- a/packages/tools/src/SqlDumper.ts +++ b/packages/tools/src/SqlDumper.ts @@ -533,6 +533,10 @@ export class SqlDumper implements AlterProcessor { this.putCmd('^commit'); } + rollbackTransaction() { + this.putCmd('^rollback'); + } + alterProlog() {} alterEpilog() {} diff --git a/packages/tools/src/driverBase.ts b/packages/tools/src/driverBase.ts index 76d2dc20..7887f4b6 100644 --- a/packages/tools/src/driverBase.ts +++ b/packages/tools/src/driverBase.ts @@ -2,6 +2,7 @@ import _compact from 'lodash/compact'; import { SqlDumper } from './SqlDumper'; import { splitQuery } from 'dbgate-query-splitter'; import { dumpSqlSelect } from 'dbgate-sqltree'; +import { EngineDriver, RunScriptOptions } from 'dbgate-types'; const dialect = { limitSelect: true, @@ -19,6 +20,12 @@ const dialect = { defaultSchemaName: null, }; +export async function runCommandOnDriver(pool, driver: EngineDriver, cmd: (dmp: SqlDumper) => void) { + const dmp = driver.createDumper(); + cmd(dmp as any); + await driver.query(pool, dmp.s, { discardResult: true }); +} + export const driverBase = { analyserClass: null, dumperClass: SqlDumper, @@ -41,12 +48,25 @@ export const driverBase = { const analyser = new this.analyserClass(pool, this, version); return analyser.incrementalAnalysis(structure); }, - createDumper(options = null) { + createDumper(options = null): SqlDumper { return new this.dumperClass(this, options); }, - async script(pool, sql) { + async script(pool, sql, options: RunScriptOptions) { + if (options?.useTransaction) { + runCommandOnDriver(pool, this, dmp => dmp.beginTransaction()); + } for (const sqlItem of splitQuery(sql, this.getQuerySplitterOptions('script'))) { - await this.query(pool, sqlItem, { discardResult: true }); + try { + await this.query(pool, sqlItem, { discardResult: true }); + } catch (err) { + if (options?.useTransaction) { + runCommandOnDriver(pool, this, dmp => dmp.rollbackTransaction()); + } + throw err; + } + } + if (options?.useTransaction) { + runCommandOnDriver(pool, this, dmp => dmp.commitTransaction()); } }, getNewObjectTemplates() { @@ -113,5 +133,5 @@ export const driverBase = { return this.readQuery(pool, dmp.s, structure); }, showConnectionField: (field, values) => false, - showConnectionTab: (field) => true, + showConnectionTab: field => true, }; diff --git a/packages/types/engines.d.ts b/packages/types/engines.d.ts index 2c4e5610..5c2f4943 100644 --- a/packages/types/engines.d.ts +++ b/packages/types/engines.d.ts @@ -12,6 +12,10 @@ export interface StreamOptions { info?: (info) => void; } +export interface RunScriptOptions { + useTransaction: boolean; +} + export interface QueryOptions { discardResult?: boolean; } @@ -130,7 +134,7 @@ export interface EngineDriver { createDatabase(pool: any, name: string): Promise; dropDatabase(pool: any, name: string): Promise; getQuerySplitterOptions(usage: 'stream' | 'script' | 'editor'): any; - script(pool: any, sql: string): Promise; + script(pool: any, sql: string, options?: RunScriptOptions): Promise; getNewObjectTemplates(): NewObjectTemplate[]; // direct call of pool method, only some methods could be supported, on only some drivers callMethod(pool, method, args); diff --git a/packages/web/src/tabs/TableDataTab.svelte b/packages/web/src/tabs/TableDataTab.svelte index ee081d40..ce63bba6 100644 --- a/packages/web/src/tabs/TableDataTab.svelte +++ b/packages/web/src/tabs/TableDataTab.svelte @@ -121,7 +121,7 @@ const [changeSetStore, dispatchChangeSet] = createUndoReducer(createChangeSet()); async function handleConfirmSql(sql) { - const resp = await apiCall('database-connections/run-script', { conid, database, sql }); + const resp = await apiCall('database-connections/run-script', { conid, database, sql, useTransaction: true }); const { errorMessage } = resp || {}; if (errorMessage) { showModal(ErrorMessageModal, { title: 'Error when saving', message: errorMessage });