diff --git a/.github/workflows/build-app-pro-beta.yaml b/.github/workflows/build-app-pro-beta.yaml
index a28406aea..ed5bf4ee4 100644
--- a/.github/workflows/build-app-pro-beta.yaml
+++ b/.github/workflows/build-app-pro-beta.yaml
@@ -43,7 +43,7 @@ jobs:
repository: dbgate/dbgate-pro
token: ${{ secrets.GH_TOKEN }}
path: dbgate-pro
- ref: 3b9ca48888d17d96806820c4e54bb047c18d6278
+ ref: ca69c4857d7d93c4b066018e8a9a0a0ece2300e7
- name: Merge dbgate/dbgate-pro
run: |
mkdir ../dbgate-pro
diff --git a/.github/workflows/build-app-pro.yaml b/.github/workflows/build-app-pro.yaml
index fca1d0378..5ebb4a796 100644
--- a/.github/workflows/build-app-pro.yaml
+++ b/.github/workflows/build-app-pro.yaml
@@ -43,7 +43,7 @@ jobs:
repository: dbgate/dbgate-pro
token: ${{ secrets.GH_TOKEN }}
path: dbgate-pro
- ref: 3b9ca48888d17d96806820c4e54bb047c18d6278
+ ref: ca69c4857d7d93c4b066018e8a9a0a0ece2300e7
- name: Merge dbgate/dbgate-pro
run: |
mkdir ../dbgate-pro
diff --git a/.github/workflows/build-cloud-pro.yaml b/.github/workflows/build-cloud-pro.yaml
index ea60420e9..4bfde2ff7 100644
--- a/.github/workflows/build-cloud-pro.yaml
+++ b/.github/workflows/build-cloud-pro.yaml
@@ -39,7 +39,7 @@ jobs:
repository: dbgate/dbgate-pro
token: ${{ secrets.GH_TOKEN }}
path: dbgate-pro
- ref: 3b9ca48888d17d96806820c4e54bb047c18d6278
+ ref: ca69c4857d7d93c4b066018e8a9a0a0ece2300e7
- name: Merge dbgate/dbgate-pro
run: |
mkdir ../dbgate-pro
diff --git a/.github/workflows/build-docker-pro.yaml b/.github/workflows/build-docker-pro.yaml
index 5ddc01901..f112f3db1 100644
--- a/.github/workflows/build-docker-pro.yaml
+++ b/.github/workflows/build-docker-pro.yaml
@@ -44,7 +44,7 @@ jobs:
repository: dbgate/dbgate-pro
token: ${{ secrets.GH_TOKEN }}
path: dbgate-pro
- ref: 3b9ca48888d17d96806820c4e54bb047c18d6278
+ ref: ca69c4857d7d93c4b066018e8a9a0a0ece2300e7
- name: Merge dbgate/dbgate-pro
run: |
mkdir ../dbgate-pro
diff --git a/.github/workflows/build-npm-pro.yaml b/.github/workflows/build-npm-pro.yaml
index 0eef3850a..621723b8d 100644
--- a/.github/workflows/build-npm-pro.yaml
+++ b/.github/workflows/build-npm-pro.yaml
@@ -35,7 +35,7 @@ jobs:
repository: dbgate/dbgate-pro
token: ${{ secrets.GH_TOKEN }}
path: dbgate-pro
- ref: 3b9ca48888d17d96806820c4e54bb047c18d6278
+ ref: ca69c4857d7d93c4b066018e8a9a0a0ece2300e7
- name: Merge dbgate/dbgate-pro
run: |
mkdir ../dbgate-pro
diff --git a/.github/workflows/e2e-pro.yaml b/.github/workflows/e2e-pro.yaml
index 34f9cbfed..5e058c392 100644
--- a/.github/workflows/e2e-pro.yaml
+++ b/.github/workflows/e2e-pro.yaml
@@ -26,7 +26,7 @@ jobs:
repository: dbgate/dbgate-pro
token: ${{ secrets.GH_TOKEN }}
path: dbgate-pro
- ref: 3b9ca48888d17d96806820c4e54bb047c18d6278
+ ref: ca69c4857d7d93c4b066018e8a9a0a0ece2300e7
- name: Merge dbgate/dbgate-pro
run: |
mkdir ../dbgate-pro
diff --git a/e2e-tests/cypress/e2e/add-connection.cy.js b/e2e-tests/cypress/e2e/add-connection.cy.js
index fff8656e9..cc76c2797 100644
--- a/e2e-tests/cypress/e2e/add-connection.cy.js
+++ b/e2e-tests/cypress/e2e/add-connection.cy.js
@@ -113,6 +113,18 @@ describe('Add connection', () => {
cy.contains('performance_schema');
});
+ it('Plugin tab', () => {
+ cy.testid('WidgetIconPanel_menu').click();
+ cy.contains('Tools').click();
+ cy.contains('Manage plugins').click();
+ cy.contains('dbgate-plugin-theme-total-white').click();
+ // text from plugin markdown
+ cy.contains('Total white theme');
+ // wait for load logos
+ cy.wait(2000);
+ cy.themeshot('view-plugin-tab');
+ });
+
it('export connections', () => {
cy.testid('WidgetIconPanel_menu').click();
cy.contains('Tools').click();
diff --git a/e2e-tests/cypress/e2e/browse-data.cy.js b/e2e-tests/cypress/e2e/browse-data.cy.js
index 5a2fbcf38..a31dd4aa2 100644
--- a/e2e-tests/cypress/e2e/browse-data.cy.js
+++ b/e2e-tests/cypress/e2e/browse-data.cy.js
@@ -310,17 +310,6 @@ describe('Data browser data', () => {
cy.themeshot('search-in-connections');
});
- it('Plugin tab', () => {
- cy.testid('WidgetIconPanel_settings').click();
- cy.contains('Manage plugins').click();
- cy.contains('dbgate-plugin-theme-total-white').click();
- // text from plugin markdown
- cy.contains('Total white theme');
- // wait for load logos
- cy.wait(2000);
- cy.themeshot('view-plugin-tab');
- });
-
it('Edit mongo data JSON', () => {
// TODO FIX: Missing button+ctx menu Revert all changes, missing button+ctx menu add document
// TODO: Dark theme - not visible changed and deleted document
diff --git a/e2e-tests/cypress/e2e/charts.cy.js b/e2e-tests/cypress/e2e/charts.cy.js
index d6606ff8e..89f7bc6ff 100644
--- a/e2e-tests/cypress/e2e/charts.cy.js
+++ b/e2e-tests/cypress/e2e/charts.cy.js
@@ -163,41 +163,35 @@ describe('Charts', () => {
cy.contains('MySql-connection').click();
cy.contains('MyChinook').click();
cy.testid('WidgetIconPanel_settings').click();
- cy.contains('Settings').click();
cy.testid('SettingsModal_languageSelect').select('Deutsch');
cy.testid('ConfirmModal_okButton').click();
cy.testid('WidgetIconPanel_settings').click();
- cy.contains('Einstellungen').click();
- cy.contains('Lokalisierung');
+ cy.contains('Sprache');
cy.themeshot('switch-language-de');
cy.testid('SettingsModal_languageSelect').select('Français');
cy.testid('ConfirmModal_okButton').click();
cy.testid('WidgetIconPanel_settings').click();
- cy.contains('Paramètres').click();
- cy.contains('Localisation');
+ cy.contains('Langue');
cy.themeshot('switch-language-fr');
cy.testid('SettingsModal_languageSelect').select('Español');
cy.testid('ConfirmModal_okButton').click();
cy.testid('WidgetIconPanel_settings').click();
- cy.contains('Configuración').click();
- cy.contains('Localización');
+ cy.contains('Idioma');
cy.themeshot('switch-language-es');
cy.testid('SettingsModal_languageSelect').select('Čeština');
cy.testid('ConfirmModal_okButton').click();
cy.testid('WidgetIconPanel_settings').click();
- cy.contains('Nastavení').click();
- cy.contains('Lokalizace');
+ cy.contains('Jazyk');
cy.themeshot('switch-language-cs');
cy.testid('SettingsModal_languageSelect').select('中文');
cy.testid('ConfirmModal_okButton').click();
cy.testid('WidgetIconPanel_settings').click();
- cy.contains('设置').click();
- cy.contains('本地化');
+ cy.contains('语言');
cy.themeshot('switch-language-zh');
cy.testid('SettingsModal_languageSelect').select('English');
diff --git a/e2e-tests/cypress/e2e/multi-sql.cy.js b/e2e-tests/cypress/e2e/multi-sql.cy.js
index 9b4596570..59a537208 100644
--- a/e2e-tests/cypress/e2e/multi-sql.cy.js
+++ b/e2e-tests/cypress/e2e/multi-sql.cy.js
@@ -103,13 +103,70 @@ describe('Transactions', () => {
describe('Backup table', () => {
multiTest({ skipMongo: true }, (connectionName, databaseName, engine, options = {}) => {
+ const implicitTransactions = options.implicitTransactions ?? false;
+
cy.contains(connectionName).click();
if (databaseName) cy.contains(databaseName).click();
- cy.contains('customers').rightclick();
+ cy.contains('addresses').rightclick();
cy.contains('Create table backup').click();
cy.testid('ConfirmSqlModal_okButton').click();
- cy.contains('_customers').click();
- cy.contains('Rows: 8').should('be.visible');
+ cy.testid('app-object-group-items-table-backups').contains('addresses').click();
+ cy.contains('Rows: 12').should('be.visible');
+ cy.testid('app-object-group-items-tables').contains('addresses').click();
+
+ cy.contains('Ridgewood').click();
+ cy.testid('TableDataTab_deleteSelectedRows').click();
+ cy.contains('Rosewood').click();
+ cy.testid('TableDataTab_deleteSelectedRows').click();
+
+ cy.contains('Vermont').click();
+ cy.get('body').realType('Wermont{enter}');
+
+ cy.testid('TableDataTab_insertNewRow').click();
+ cy.get('body').realType('Modranska{enter}');
+ cy.realPress(['ArrowLeft']);
+ cy.realPress(['ArrowLeft']);
+ cy.get('body').realType('13{enter}');
+ cy.realPress(['ArrowRight']);
+ cy.get('body').realType('1{enter}');
+ cy.realPress(['ArrowRight']);
+ cy.realPress(['ArrowRight']);
+ cy.realPress(['ArrowRight']);
+ cy.get('body').realType('Prague{enter}');
+ cy.realPress(['ArrowRight']);
+ cy.get('body').realType('CZ{enter}');
+ cy.realPress(['ArrowRight']);
+ cy.get('body').realType('10000{enter}');
+ cy.realPress(['ArrowRight']);
+ cy.get('body').realType('111222333{enter}');
+
+ cy.testid('TableDataTab_save').click();
+ cy.testid('ConfirmSqlModal_okButton').click();
+ cy.contains('Rows: 11').should('be.visible'); // wait for save
+
+ cy.testid('app-object-group-items-table-backups').contains('addresses').rightclick();
+ cy.contains('restore script').click();
+ cy.contains('UPDATE'); // wait for query
+ cy.testid('QueryTab_executeButton').click();
+ cy.contains('Query execution finished');
+
+ if (implicitTransactions) {
+ cy.testid('QueryTab_commitTransactionButton').click();
+ cy.contains('Commit Transaction finished');
+ }
+
+ cy.realPress('F1');
+ cy.realType('Close all');
+ cy.realPress('Enter');
+ // cy.testid('CloseTabModal_buttonConfirm').click();
+ cy.wait(1000);
+
+ cy.testid('app-object-group-items-tables').contains('addresses').click();
+
+ // check whether data was successfully restored
+ cy.contains('Rows: 12').should('be.visible');
+ cy.contains('Ridgewood');
+ cy.contains('Vermont');
});
});
@@ -146,13 +203,14 @@ describe('Import CSV', () => {
cy.contains('Import').click();
cy.get('input[type=file]').selectFile('cypress/fixtures/customers-20.csv', { force: true });
- cy.contains('customers-20');
+ cy.testid('ImportExportConfigurator_tableMappingSection').contains('customers-20');
cy.testid('ImportExportTab_preview_content').contains('50ddd99fAdF48B3').should('be.visible');
cy.testid('ImportExportTab_executeButton').click();
- cy.contains('20 rows written').should('be.visible');
+ cy.testid('ImportExportConfigurator_tableMappingSection').contains('20 rows written').should('be.visible');
cy.testid('SqlObjectList_refreshButton').click();
+ cy.contains('Refresh DB structure (incremental)').click();
cy.testid('SqlObjectList_container').contains('customers-20').click();
cy.contains('Rows: 20').should('be.visible');
diff --git a/integration-tests/__tests__/schema-tests.spec.js b/integration-tests/__tests__/schema-tests.spec.js
index fda045c90..bf3f47f4a 100644
--- a/integration-tests/__tests__/schema-tests.spec.js
+++ b/integration-tests/__tests__/schema-tests.spec.js
@@ -28,12 +28,14 @@ describe('Schema tests', () => {
const count = schemas1.length;
expect(structure1.tables.length).toEqual(2);
await runCommandOnDriver(conn, driver, dmp => dmp.createSchema('myschema'));
- const structure2 = await driver.analyseIncremental(conn, structure1);
- const schemas2 = await driver.listSchemas(conn);
- expect(schemas2.find(x => x.schemaName == 'myschema')).toBeTruthy();
- expect(schemas2.length).toEqual(count + 1);
- expect(schemas2.find(x => x.isDefault).schemaName).toEqual(engine.defaultSchemaName);
- expect(structure2).toBeNull();
+ if (!engine.skipIncrementalAnalysis) {
+ const structure2 = await driver.analyseIncremental(conn, structure1);
+ const schemas2 = await driver.listSchemas(conn);
+ expect(schemas2.find(x => x.schemaName == 'myschema')).toBeTruthy();
+ expect(schemas2.length).toEqual(count + 1);
+ expect(schemas2.find(x => x.isDefault).schemaName).toEqual(engine.defaultSchemaName);
+ expect(structure2).toBeNull();
+ }
})
);
@@ -48,10 +50,12 @@ describe('Schema tests', () => {
expect(schemas1.find(x => x.schemaName == 'myschema')).toBeTruthy();
expect(structure1.tables.length).toEqual(2);
await runCommandOnDriver(conn, driver, dmp => dmp.dropSchema('myschema'));
- const structure2 = await driver.analyseIncremental(conn, structure1);
- const schemas2 = await driver.listSchemas(conn);
- expect(schemas2.find(x => x.schemaName == 'myschema')).toBeFalsy();
- expect(structure2).toBeNull();
+ if (!engine.skipIncrementalAnalysis) {
+ const structure2 = await driver.analyseIncremental(conn, structure1);
+ const schemas2 = await driver.listSchemas(conn);
+ expect(schemas2.find(x => x.schemaName == 'myschema')).toBeFalsy();
+ expect(structure2).toBeNull();
+ }
})
);
diff --git a/package.json b/package.json
index 04a4c1055..ed59d9ddc 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"private": true,
- "version": "6.7.2-alpha.1",
+ "version": "6.7.2-premium-beta.4",
"name": "dbgate-all",
"workspaces": [
"packages/*",
diff --git a/packages/sqltree/src/dumpSqlCommand.ts b/packages/sqltree/src/dumpSqlCommand.ts
index e8286bafb..b3b9373a4 100644
--- a/packages/sqltree/src/dumpSqlCommand.ts
+++ b/packages/sqltree/src/dumpSqlCommand.ts
@@ -1,7 +1,7 @@
import type { SqlDumper } from 'dbgate-types';
import { Command, Select, Update, Delete, Insert } from './types';
import { dumpSqlExpression } from './dumpSqlExpression';
-import { dumpSqlFromDefinition, dumpSqlSourceRef } from './dumpSqlSource';
+import { dumpSqlFromDefinition, dumpSqlSourceDef, dumpSqlSourceRef } from './dumpSqlSource';
import { dumpSqlCondition } from './dumpSqlCondition';
export function dumpSqlSelect(dmp: SqlDumper, cmd: Select) {
@@ -115,7 +115,10 @@ export function dumpSqlInsert(dmp: SqlDumper, cmd: Insert) {
cmd.fields.map(x => x.targetColumn)
);
dmp.putCollection(',', cmd.fields, x => dumpSqlExpression(dmp, x));
- if (dmp.dialect.requireFromDual) {
+ if (cmd.whereNotExistsSource) {
+ dmp.put(' ^from ');
+ dumpSqlSourceDef(dmp, cmd.whereNotExistsSource);
+ } else if (dmp.dialect.requireFromDual) {
dmp.put(' ^from ^dual ');
}
dmp.put(' ^where ^not ^exists (^select * ^from %f ^where ', cmd.targetTable);
diff --git a/packages/sqltree/src/dumpSqlExpression.ts b/packages/sqltree/src/dumpSqlExpression.ts
index aeaf3b288..f9447f792 100644
--- a/packages/sqltree/src/dumpSqlExpression.ts
+++ b/packages/sqltree/src/dumpSqlExpression.ts
@@ -2,6 +2,7 @@ import _ from 'lodash';
import type { SqlDumper } from 'dbgate-types';
import { Expression, ColumnRefExpression } from './types';
import { dumpSqlSourceRef } from './dumpSqlSource';
+import { dumpSqlSelect } from './dumpSqlCommand';
export function dumpSqlExpression(dmp: SqlDumper, expr: Expression) {
switch (expr.exprType) {
@@ -67,5 +68,11 @@ export function dumpSqlExpression(dmp: SqlDumper, expr: Expression) {
});
dmp.put(')');
break;
+
+ case 'select':
+ dmp.put('(');
+ dumpSqlSelect(dmp, expr.select);
+ dmp.put(')');
+ break;
}
}
diff --git a/packages/sqltree/src/types.ts b/packages/sqltree/src/types.ts
index bce0e61d3..ea7c5c994 100644
--- a/packages/sqltree/src/types.ts
+++ b/packages/sqltree/src/types.ts
@@ -44,6 +44,7 @@ export interface Insert {
fields: UpdateField[];
targetTable: NamedObjectInfo;
insertWhereNotExistsCondition?: Condition;
+ whereNotExistsSource?: Source;
}
export interface AllowIdentityInsert {
@@ -226,6 +227,11 @@ export interface RowNumberExpression {
orderBy: OrderByExpression[];
}
+export interface SelectExpression {
+ exprType: 'select';
+ select: Select;
+}
+
export type Expression =
| ColumnRefExpression
| ValueExpression
@@ -235,7 +241,8 @@ export type Expression =
| CallExpression
| MethodCallExpression
| TranformExpression
- | RowNumberExpression;
+ | RowNumberExpression
+ | SelectExpression;
export type OrderByExpression = Expression & { direction: 'ASC' | 'DESC' };
export type ResultField = Expression & { alias?: string };
diff --git a/packages/types/dumper.d.ts b/packages/types/dumper.d.ts
index 746cbbfc2..c5f380a22 100644
--- a/packages/types/dumper.d.ts
+++ b/packages/types/dumper.d.ts
@@ -16,6 +16,7 @@ export interface SqlDumper extends AlterProcessor {
transform(type: TransformType, dumpExpr: () => void);
createDatabase(name: string);
dropDatabase(name: string);
+ comment(value: string);
callableTemplate(func: CallableObjectInfo);
diff --git a/packages/web/src/appobj/AppObjectGroup.svelte b/packages/web/src/appobj/AppObjectGroup.svelte
index cdcc4b78c..df3eb341e 100644
--- a/packages/web/src/appobj/AppObjectGroup.svelte
+++ b/packages/web/src/appobj/AppObjectGroup.svelte
@@ -77,7 +77,7 @@
{/if}
-
+
{#each items as item}
import { copyTextToClipboard } from '../utility/clipboard';
import { _t, _tval, DefferedTranslationResult } from '../translations';
+ import sqlFormatter from 'sql-formatter';
export const extractKey = ({ schemaName, pureName }) => (schemaName ? `${schemaName}.${pureName}` : pureName);
export const createMatcher =
@@ -88,7 +89,8 @@
isRename?: boolean;
isTruncate?: boolean;
isCopyTableName?: boolean;
- isDuplicateTable?: boolean;
+ isTableBackup?: boolean;
+ isTableRestore?: boolean;
isDiagram?: boolean;
functionName?: string;
isExport?: boolean;
@@ -106,6 +108,8 @@
}
function createMenusCore(objectTypeField, driver, data): DbObjMenuItem[] {
+ const backupMatch = data.objectTypeField === 'tables' ? data.pureName.match(TABLE_BACKUP_REGEX) : null;
+
switch (objectTypeField) {
case 'tables':
return [
@@ -175,11 +179,18 @@
isCopyTableName: true,
requiresWriteAccess: false,
},
- hasPermission('dbops/table/backup') && {
- label: _t('dbObject.createTableBackup', { defaultMessage: 'Create table backup' }),
- isDuplicateTable: true,
- requiresWriteAccess: true,
- },
+ hasPermission('dbops/table/backup') &&
+ !backupMatch && {
+ label: _t('dbObject.createTableBackup', { defaultMessage: 'Create table backup' }),
+ isTableBackup: true,
+ requiresWriteAccess: true,
+ },
+ hasPermission('dbops/table/restore') &&
+ backupMatch && {
+ label: _t('dbObject.createRestoreScript', { defaultMessage: 'Create restore script' }),
+ isTableRestore: true,
+ requiresWriteAccess: true,
+ },
hasPermission('dbops/model/view') && {
label: _t('dbObject.showDiagram', { defaultMessage: 'Show diagram' }),
isDiagram: true,
@@ -637,7 +648,7 @@
});
},
});
- } else if (menu.isDuplicateTable) {
+ } else if (menu.isTableBackup) {
const driver = await getDriver();
const dmp = driver.createDumper();
const newTable = _.cloneDeep(data);
@@ -671,6 +682,25 @@
},
engine: driver.engine,
});
+ } else if (menu.isTableRestore) {
+ const backupMatch = data.objectTypeField === 'tables' ? data.pureName.match(TABLE_BACKUP_REGEX) : null;
+
+ const driver = await getDriver();
+ const dmp = driver.createDumper();
+ const db = await getDatabaseInfo(data);
+ if (db) {
+ const originalTable = db?.tables?.find(x => x.pureName == backupMatch[1] && x.schemaName == data.schemaName);
+ if (originalTable) {
+ createTableRestoreScript(data, originalTable, dmp);
+ newQuery({
+ title: _t('dbObject.restoreScript', {
+ defaultMessage: 'Restore {name} #',
+ values: { name: backupMatch[1] },
+ }),
+ initialData: sqlFormatter.format(dmp.s),
+ });
+ }
+ }
} else if (menu.isImport) {
const { conid, database } = data;
openImportExportTab({
@@ -724,9 +754,8 @@
const coreMenus = createMenusCore(objectTypeField, driver, data);
const filteredSumenus = coreMenus.map(item => {
+ if (!item) return item;
if (!item.submenu) {
- if (!item) return item;
-
return { ...item, label: _tval(item.label) };
}
return {
@@ -743,7 +772,7 @@
};
});
- const filteredNoEmptySubmenus = filteredSumenus.filter(x => !x.submenu || x.submenu.length > 0);
+ const filteredNoEmptySubmenus = _.compact(filteredSumenus).filter(x => !x.submenu || x.submenu.length > 0);
return filteredNoEmptySubmenus;
}
@@ -1008,6 +1037,8 @@
return handleDatabaseObjectClick(data, { forceNewTab, tabPreviewMode, focusTab });
}
+
+ export const TABLE_BACKUP_REGEX = /^_(.*)_(\d\d\d\d)-(\d\d)-(\d\d)-(\d\d)-(\d\d)-(\d\d)$/;
-
+
-
-
{_t('importExport.mapSourceTablesFiles', { defaultMessage: "Map source tables/files" })}
+
+
+
+ {_t('importExport.mapSourceTablesFiles', { defaultMessage: 'Map source tables/files' })}
+
{#key targetEditKey}
{#key progressHolder}
@@ -221,34 +224,34 @@
columns={[
{
fieldName: 'source',
- header: _t('importExport.source', { defaultMessage: "Source" }),
+ header: _t('importExport.source', { defaultMessage: 'Source' }),
component: SourceName,
getProps: row => ({ name: row }),
},
{
fieldName: 'action',
- header: _t('importExport.action', { defaultMessage: "Action" }),
+ header: _t('importExport.action', { defaultMessage: 'Action' }),
component: SourceAction,
getProps: row => ({ name: row, targetDbinfo }),
},
{
fieldName: 'target',
- header: _t('importExport.target', { defaultMessage: "Target" }),
+ header: _t('importExport.target', { defaultMessage: 'Target' }),
slot: 1,
},
supportsPreview && {
fieldName: 'preview',
- header: _t('importExport.preview', { defaultMessage: "Preview" }),
+ header: _t('importExport.preview', { defaultMessage: 'Preview' }),
slot: 0,
},
!!progressHolder && {
fieldName: 'status',
- header: _t('importExport.status', { defaultMessage: "Status" }),
+ header: _t('importExport.status', { defaultMessage: 'Status' }),
slot: 3,
},
{
fieldName: 'columns',
- header: _t('importExport.columns', { defaultMessage: "Columns" }),
+ header: _t('importExport.columns', { defaultMessage: 'Columns' }),
slot: 2,
},
]}
diff --git a/packages/web/src/modals/CloseTabModal.svelte b/packages/web/src/modals/CloseTabModal.svelte
index 62d67f22c..eb86cc071 100644
--- a/packages/web/src/modals/CloseTabModal.svelte
+++ b/packages/web/src/modals/CloseTabModal.svelte
@@ -44,6 +44,7 @@
{
closeCurrentModal();
onConfirm();
@@ -52,6 +53,7 @@
{
closeCurrentModal();
onCancel();
diff --git a/packages/web/src/modals/KeyboardModal.svelte b/packages/web/src/modals/KeyboardModal.svelte
index dbb2925e3..380e5cea3 100644
--- a/packages/web/src/modals/KeyboardModal.svelte
+++ b/packages/web/src/modals/KeyboardModal.svelte
@@ -39,7 +39,7 @@
- _{_t('commandModal.showKeyCombination', { defaultMessage: 'Show desired key combination and press ENTER' })}
+ {_t('commandModal.showKeyCombination', { defaultMessage: 'Show desired key combination and press ENTER' })}
diff --git a/packages/web/src/settings/GeneralSettings.svelte b/packages/web/src/settings/GeneralSettings.svelte
index acff0fedb..aa7cd0bc9 100644
--- a/packages/web/src/settings/GeneralSettings.svelte
+++ b/packages/web/src/settings/GeneralSettings.svelte
@@ -1,61 +1,27 @@
-
{_t('settings.general', { defaultMessage: 'General' })}
-{#if electron}
-
{_t('settings.appearance', { defaultMessage: 'Appearance' })}
-
{
- restartWarning = true;
- }}
- />
- {#if restartWarning}
-
-
- {_t('settings.nativeMenuRestartWarning', {
- defaultMessage: 'Native menu settings will be applied after app restart',
- })}
-
- {/if}
-{/if}
-
-{_t('settings.localization', { defaultMessage: 'Localization' })}
-
-
-{_t('settings.application', { defaultMessage: 'Application' })}
+
+
-
+
+
+
+
+ {_t('settings.appearance', { defaultMessage: 'Appearance' })}
+
+ {#if electron}
+
+ {
+ restartWarning = true;
+ }}
+ />
+ {#if restartWarning}
+
+
+ {_t('settings.nativeMenuRestartWarning', {
+ defaultMessage: 'Native menu settings will be applied after app restart',
+ })}
+
+ {/if}
+ {/if}
+
+
+
\ No newline at end of file
diff --git a/packages/web/src/settings/SettingsModal.svelte b/packages/web/src/settings/SettingsModal.svelte
deleted file mode 100644
index 981c62704..000000000
--- a/packages/web/src/settings/SettingsModal.svelte
+++ /dev/null
@@ -1,850 +0,0 @@
-
-
-
-
- {_t('settings.title', { defaultMessage: 'Settings' })}
-
-
-
-
- {#if electron}
- {_t('settings.appearance', { defaultMessage: 'Appearance' })}
- {
- restartWarning = true;
- }}
- />
- {#if restartWarning}
-
-
- {_t('settings.nativeMenuRestartWarning', {
- defaultMessage: 'Native menu settings will be applied after app restart',
- })}
-
- {/if}
- {/if}
-
-
- {_t('settings.localization', { defaultMessage: 'Localization' })}
-
-
- {
- setSelectedLanguage(e.detail);
- showModal(ConfirmModal, {
- message: _t('settings.localization.reloadWarning', {
- defaultMessage: 'Application will be reloaded to apply new language settings',
- }),
- onConfirm: () => {
- setTimeout(() => {
- internalRedirectTo(electron ? '/index.html' : '/');
- }, 100);
- },
- });
- }}
- />
-
-
- {_t('settings.dataGrid.title', { defaultMessage: 'Data grid' })}
-
- {#if isProApp()}
-
- {/if}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {_t('settings.sqlEditor', { defaultMessage: 'SQL editor' })}
-
-
-
-
-
-
-
- ({ label: mode.label, value: mode.value }))}
- value={$currentEditorKeybindigMode}
- on:change={e => ($currentEditorKeybindigMode = e.detail)}
- />
-
-
-
-
- ($currentEditorWrapEnabled = e.target.checked)}
- />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {_t('settings.connection', { defaultMessage: 'Connection' })}
-
- {
- $lockedDatabaseMode = !$lockedDatabaseMode;
- },
- }}
- >
- ($lockedDatabaseMode = e.target.checked)} />
-
-
-
-
-
-
- {_t('settings.session', { defaultMessage: 'Query sessions' })}
-
-
-
-
-
- {_t('settings.applicationTheme', { defaultMessage: 'Application theme' })}
-
- {
- if ($currentTheme) {
- $currentTheme = null;
- } else {
- $currentTheme = getSystemTheme();
- }
- },
- }}
- >
- {
- if (e.target['checked']) {
- $currentTheme = null;
- } else {
- $currentTheme = getSystemTheme();
- }
- }}
- />
-
-
-
- {#each $extensions.themes as theme}
-
- {/each}
-
-
-
- {_t('settings.appearance.moreThemes', { defaultMessage: 'More themes are available as' })}
- plugins
-
- {_t('settings.appearance.afterInstalling', {
- defaultMessage:
- 'After installing theme plugin (try search "theme" in available extensions) new themes will be available here.',
- })}
-
-
- {_t('settings.appearance.editorTheme', { defaultMessage: 'Editor theme' })}
-
-
-
-
- ({ label: theme, value: theme }))}
- value={$currentEditorTheme}
- on:change={e => ($currentEditorTheme = e.detail)}
- />
-
-
-
-
-
- x.value == $currentEditorFontSize) ? $currentEditorFontSize : 'custom'}
- on:change={e => ($currentEditorFontSize = e.detail)}
- />
-
-
-
-
-
- ($currentEditorFontSize = e.target['value'])}
- disabled={!!FONT_SIZES.find(x => x.value == $currentEditorFontSize) &&
- $currentEditorFontSize != 'custom'}
- />
-
-
-
-
-
-
-
-
-
-
-
-
-
- {_t('settings.defaultActions', { defaultMessage: 'Default Actions' })}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {_t('settings.behaviour', { defaultMessage: 'Behaviour' })}
-
-
-
-
-
-
-
- {_t('settings.behaviour.singleClickPreview', {
- defaultMessage:
- 'When you single-click or select a file in the "Tables, Views, Functions" view, it is shown in a preview mode and reuses an existing tab (preview tab). This is useful if you are quickly browsing tables and don\'t want every visited table to have its own tab. When you start editing the table or use double-click to open the table from the "Tables" view, a new tab is dedicated to that table.',
- })}
-
-
-
-
- {_t('settings.confirmations', { defaultMessage: 'Confirmations' })}
-
-
-
-
-
- {_t('settings.other', { defaultMessage: 'Other' })}
-
-
-
-
-
- {#if isProApp()}
-
- {/if}
-
-
-
- {_t('settings.other.license', { defaultMessage: 'License' })}
- {
- licenseKeyCheckResult = await apiCall('config/check-license', { licenseKey: value });
- }}
- />
- {#if licenseKeyCheckResult}
-
- {#if licenseKeyCheckResult.status == 'ok'}
-
-
- {_t('settings.other.licenseKey.valid', { defaultMessage: 'License key is valid' })}
-
- {#if licenseKeyCheckResult.validTo}
-
- {_t('settings.other.licenseKey.validTo', { defaultMessage: 'License valid to:' })}
- {licenseKeyCheckResult.validTo}
-
- {/if}
- {#if licenseKeyCheckResult.expiration}
-
- {_t('settings.other.licenseKey.expiration', { defaultMessage: 'License key expiration:' })}
- {safeFormatDate(licenseKeyCheckResult.expiration)}
-
- {/if}
- {:else if licenseKeyCheckResult.status == 'error'}
-
-
- {licenseKeyCheckResult.errorMessage ??
- _t('settings.other.licenseKey.invalid', { defaultMessage: 'License key is invalid' })}
- {#if licenseKeyCheckResult.expiration}
-
- {_t('settings.other.licenseKey.expiration', { defaultMessage: 'License key expiration:' })}
- {safeFormatDate(licenseKeyCheckResult.expiration)}
-
- {/if}
-
- {#if licenseKeyCheckResult.isExpired}
-
- {
- licenseKeyCheckResult = await apiCall('config/get-new-license', { oldLicenseKey: licenseKey });
- if (licenseKeyCheckResult.licenseKey) {
- apiCall('config/update-settings', { 'other.licenseKey': licenseKeyCheckResult.licenseKey });
- }
- }}
- />
-
- {/if}
- {/if}
-
- {/if}
-
-
-
- {_t('settings.externalTools', { defaultMessage: 'External Tools' })}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/packages/web/src/tabs/SettingsTab.svelte b/packages/web/src/tabs/SettingsTab.svelte
index e4e89c8f9..290530b96 100644
--- a/packages/web/src/tabs/SettingsTab.svelte
+++ b/packages/web/src/tabs/SettingsTab.svelte
@@ -11,7 +11,6 @@
import DefaultActionsSettings from "../settings/DefaultActionsSettings.svelte";
import BehaviourSettings from "../settings/BehaviourSettings.svelte";
import ExternalToolsSettings from "../settings/ExternalToolsSettings.svelte";
- import OtherSettings from "../settings/OtherSettings.svelte";
import LicenseSettings from "../settings/LicenseSettings.svelte";
import { isProApp } from "../utility/proTools";
import { _t } from "../translations";
@@ -20,6 +19,8 @@
import SQLEditorSettings from "../settings/SQLEditorSettings.svelte";
import AiSettingsTab from "../settings/AiSettingsTab.svelte";
+ export let selectedItem = 'general';
+
const menuItems = [
{
label: _t('settings.general', { defaultMessage: 'General' }),
@@ -98,16 +99,7 @@
props: {},
testid: 'settings-ai',
},
- {
- label: _t('settings.other', { defaultMessage: 'Other' }),
- identifier: 'other',
- component: OtherSettings,
- props: {},
- testid: 'settings-other',
- },
];
-
- let selectedItem = 'general';
diff --git a/packages/web/src/utility/tableRestoreScript.ts b/packages/web/src/utility/tableRestoreScript.ts
new file mode 100644
index 000000000..ec4f7479a
--- /dev/null
+++ b/packages/web/src/utility/tableRestoreScript.ts
@@ -0,0 +1,174 @@
+import _ from 'lodash';
+import { Condition, dumpSqlInsert, dumpSqlUpdate, Insert, Update, Delete, dumpSqlDelete } from 'dbgate-sqltree';
+import { TableInfo, SqlDumper } from 'dbgate-types';
+
+export function createTableRestoreScript(backupTable: TableInfo, originalTable: TableInfo, dmp: SqlDumper) {
+ const bothColumns = _.intersection(
+ backupTable.columns.map(x => x.columnName),
+ originalTable.columns.map(x => x.columnName)
+ );
+ const keyColumns = _.intersection(
+ originalTable.primaryKey?.columns?.map(x => x.columnName) || [],
+ backupTable.columns.map(x => x.columnName)
+ );
+ const valueColumns = _.difference(bothColumns, keyColumns);
+
+ if (keyColumns.length === 0) {
+ throw new Error('Cannot create restore script: no key columns found');
+ }
+
+ function makeColumnCond(colName: string, operator: '=' | '<>' | '<' | '>' | '<=' | '>=' = '='): Condition {
+ return {
+ conditionType: 'binary',
+ operator,
+ left: {
+ exprType: 'column',
+ columnName: colName,
+ source: { name: originalTable },
+ },
+ right: {
+ exprType: 'column',
+ columnName: colName,
+ source: { alias: 'bak' },
+ },
+ };
+ }
+
+ function makeNullNotNullCond(colName: string, isCond1: boolean): Condition {
+ return {
+ conditionType: 'and',
+ conditions: [
+ {
+ conditionType: 'isNull',
+ expr: {
+ exprType: 'column',
+ columnName: colName,
+ source: isCond1 ? { name: originalTable } : { alias: 'bak' },
+ },
+ },
+ {
+ conditionType: 'isNotNull',
+ expr: {
+ exprType: 'column',
+ columnName: colName,
+ source: isCond1 ? { alias: 'bak' } : { name: originalTable },
+ },
+ },
+ ],
+ };
+ }
+
+ function putTitle(title: string) {
+ dmp.putRaw('\n\n');
+ dmp.comment(`******************** ${title} ********************`);
+ dmp.putRaw('\n');
+ }
+
+ dmp.comment(`Restoring data into table ${originalTable.pureName} from backup table ${backupTable.pureName}`);
+ dmp.putRaw('\n');
+ dmp.comment(`Key columns: ${keyColumns.join(', ')}`);
+ dmp.putRaw('\n');
+ dmp.comment(`Value columns: ${valueColumns.join(', ')}`);
+ dmp.putRaw('\n');
+ dmp.comment(`Follows UPDATE, DELETE, INSERT statements to restore data`);
+ dmp.putRaw('\n');
+
+ if (valueColumns.length > 0) {
+ const update: Update = {
+ commandType: 'update',
+ from: { name: originalTable },
+ fields: valueColumns.map(colName => ({
+ exprType: 'select',
+ select: {
+ commandType: 'select',
+ from: { name: backupTable, alias: 'bak' },
+ columns: [
+ {
+ exprType: 'column',
+ columnName: colName,
+ source: { alias: 'bak' },
+ },
+ ],
+ where: {
+ conditionType: 'and',
+ conditions: keyColumns.map(colName => makeColumnCond(colName)),
+ },
+ },
+ targetColumn: colName,
+ })),
+ where: {
+ conditionType: 'exists',
+ subQuery: {
+ commandType: 'select',
+ from: { name: backupTable, alias: 'bak' },
+ selectAll: true,
+ where: {
+ conditionType: 'and',
+ conditions: [
+ ...keyColumns.map(keyColName => makeColumnCond(keyColName)),
+ {
+ conditionType: 'or',
+ conditions: valueColumns.flatMap(colName => [
+ makeColumnCond(colName, '<>'),
+ makeNullNotNullCond(colName, true),
+ makeNullNotNullCond(colName, false),
+ ]),
+ },
+ ],
+ },
+ },
+ },
+ };
+ putTitle('UPDATE');
+ dumpSqlUpdate(dmp, update);
+ dmp.endCommand();
+ }
+
+ const delcmd: Delete = {
+ commandType: 'delete',
+ from: { name: originalTable },
+ where: {
+ conditionType: 'notExists',
+ subQuery: {
+ commandType: 'select',
+ from: { name: backupTable, alias: 'bak' },
+ selectAll: true,
+ where: {
+ conditionType: 'and',
+ conditions: keyColumns.map(colName => makeColumnCond(colName)),
+ },
+ },
+ },
+ };
+ putTitle('DELETE');
+ dumpSqlDelete(dmp, delcmd);
+ dmp.endCommand();
+
+ const insert: Insert = {
+ commandType: 'insert',
+ targetTable: originalTable,
+ fields: bothColumns.map(colName => ({
+ targetColumn: colName,
+ exprType: 'column',
+ columnName: colName,
+ source: { alias: 'bak' },
+ })),
+ whereNotExistsSource: { name: backupTable, alias: 'bak' },
+ insertWhereNotExistsCondition: {
+ conditionType: 'and',
+ conditions: keyColumns.map(colName => makeColumnCond(colName)),
+ },
+ };
+
+ putTitle('INSERT');
+
+ const autoinc = originalTable.columns.find(x => x.autoIncrement);
+ if (autoinc) {
+ dmp.allowIdentityInsert(originalTable, true);
+ }
+ dumpSqlInsert(dmp, insert);
+ dmp.endCommand();
+ if (autoinc) {
+ dmp.allowIdentityInsert(originalTable, false);
+ }
+}
diff --git a/packages/web/src/widgets/SqlObjectList.svelte b/packages/web/src/widgets/SqlObjectList.svelte
index fa24ff1d8..8dbabeae8 100644
--- a/packages/web/src/widgets/SqlObjectList.svelte
+++ b/packages/web/src/widgets/SqlObjectList.svelte
@@ -235,7 +235,7 @@
function getAppObjectGroup(data) {
if (data.objectTypeField == 'tables') {
- if (data.pureName.match(/^_(.*)_\d\d\d\d-\d\d-\d\d-\d\d-\d\d-\d\d$/)) {
+ if (data.pureName.match(databaseObjectAppObject.TABLE_BACKUP_REGEX)) {
return _t('dbObject.tableBackups', { defaultMessage: 'Table Backups' });
}
}
@@ -286,7 +286,7 @@
icon="img alert"
/>
- {_t('common.refresh', { defaultMessage: 'Refresh' })}
+ {_t('common.refresh', { defaultMessage: 'Refresh' })}
{#if driver?.databaseEngineTypes?.includes('sql')}
runCommand('new.table')}
diff --git a/workflow-templates/includes.tpl.yaml b/workflow-templates/includes.tpl.yaml
index 3bf5a021d..dec078561 100644
--- a/workflow-templates/includes.tpl.yaml
+++ b/workflow-templates/includes.tpl.yaml
@@ -7,7 +7,7 @@ checkout-and-merge-pro:
repository: dbgate/dbgate-pro
token: ${{ secrets.GH_TOKEN }}
path: dbgate-pro
- ref: 3b9ca48888d17d96806820c4e54bb047c18d6278
+ ref: ca69c4857d7d93c4b066018e8a9a0a0ece2300e7
- name: Merge dbgate/dbgate-pro
run: |
mkdir ../dbgate-pro