diff --git a/integration-tests/__tests__/alter-database.spec.js b/integration-tests/__tests__/alter-database.spec.js index b06dcf583..90a80f15e 100644 --- a/integration-tests/__tests__/alter-database.spec.js +++ b/integration-tests/__tests__/alter-database.spec.js @@ -67,3 +67,4 @@ describe('Alter database', () => { }) ); }); + diff --git a/integration-tests/__tests__/schema-tests.spec.js b/integration-tests/__tests__/schema-tests.spec.js new file mode 100644 index 000000000..5f5b4f04e --- /dev/null +++ b/integration-tests/__tests__/schema-tests.spec.js @@ -0,0 +1,86 @@ +const stableStringify = require('json-stable-stringify'); +const _ = require('lodash'); +const fp = require('lodash/fp'); +const { testWrapper } = require('../tools'); +const engines = require('../engines'); +const { runCommandOnDriver } = require('dbgate-tools'); + +async function baseStructure(conn, driver) { + await driver.query(conn, `create table t1 (id int not null primary key)`); + + await driver.query( + conn, + `create table t2 ( + id int not null primary key, + t1_id int + )` + ); +} + +describe('Schema tests', () => { + test.each(engines.filter(x => x.supportSchemas).map(engine => [engine.label, engine]))( + 'Create schema - %s', + testWrapper(async (conn, driver, engine) => { + await baseStructure(conn, driver); + const structure1 = await driver.analyseFull(conn); + expect(structure1.schemas.find(x => x.schemaName == 'myschema')).toBeFalsy(); + const count = structure1.schemas.length; + expect(structure1.tables.length).toEqual(2); + await runCommandOnDriver(conn, driver, dmp => dmp.createSchema('myschema')); + const structure2 = await driver.analyseIncremental(conn, structure1); + expect(structure2.schemas.find(x => x.schemaName == 'myschema')).toBeTruthy(); + expect(structure2.tables.length).toEqual(2); + expect(structure2.schemas.length).toEqual(count + 1); + + const structure3 = await driver.analyseIncremental(conn, structure2); + expect(structure3).toBeNull(); + }) + ); + + test.each(engines.filter(x => x.supportSchemas).map(engine => [engine.label, engine]))( + 'Drop schema - %s', + testWrapper(async (conn, driver, engine) => { + await baseStructure(conn, driver); + await runCommandOnDriver(conn, driver, dmp => dmp.createSchema('myschema')); + + const structure1 = await driver.analyseFull(conn); + expect(structure1.schemas.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); + expect(structure2.schemas.find(x => x.schemaName == 'myschema')).toBeFalsy(); + expect(structure2.tables.length).toEqual(2); + + const structure3 = await driver.analyseIncremental(conn, structure2); + expect(structure3).toBeNull(); + }) + ); + + test.each(engines.filter(x => x.supportSchemas).map(engine => [engine.label, engine]))( + 'Create table - keep schemas - %s', + testWrapper(async (conn, driver, engine) => { + await baseStructure(conn, driver); + const structure1 = await driver.analyseFull(conn); + const count = structure1.schemas.length; + expect(structure1.tables.length).toEqual(2); + await driver.query(conn, `create table t3 (id int not null primary key)`); + const structure2 = await driver.analyseIncremental(conn, structure1); + expect(structure2.tables.length).toEqual(3); + expect(structure2.schemas.length).toEqual(count); + }) + ); +}); + +describe('Base analyser test', () => { + test.each(engines.map(engine => [engine.label, engine]))( + 'Structure without change - %s', + testWrapper(async (conn, driver, engine) => { + await baseStructure(conn, driver); + + const structure1 = await driver.analyseFull(conn); + expect(structure1.tables.length).toEqual(2); + const structure2 = await driver.analyseIncremental(conn, structure1); + expect(structure2).toBeNull(); + }) + ); +}); diff --git a/integration-tests/docker-compose.yaml b/integration-tests/docker-compose.yaml index 7f0bc431f..4e347bf9a 100644 --- a/integration-tests/docker-compose.yaml +++ b/integration-tests/docker-compose.yaml @@ -1,12 +1,12 @@ version: '3' services: - # postgres: - # image: postgres - # restart: always - # environment: - # POSTGRES_PASSWORD: Pwd2020Db - # ports: - # - 15000:5432 + postgres: + image: postgres + restart: always + environment: + POSTGRES_PASSWORD: Pwd2020Db + ports: + - 15000:5432 # mariadb: # image: mariadb @@ -26,13 +26,13 @@ services: # environment: # - MYSQL_ROOT_PASSWORD=Pwd2020Db - clickhouse: - image: bitnami/clickhouse:24.8.4 - restart: always - ports: - - 15005:8123 - environment: - - CLICKHOUSE_ADMIN_PASSWORD=Pwd2020Db + # clickhouse: + # image: bitnami/clickhouse:24.8.4 + # restart: always + # ports: + # - 15005:8123 + # environment: + # - CLICKHOUSE_ADMIN_PASSWORD=Pwd2020Db # mssql: # image: mcr.microsoft.com/mssql/server diff --git a/integration-tests/engines.js b/integration-tests/engines.js index 564a35775..380e8c3fd 100644 --- a/integration-tests/engines.js +++ b/integration-tests/engines.js @@ -81,6 +81,7 @@ const engines = [ drop2: 'DROP FUNCTION obj2', }, ], + supportSchemas: true, }, { label: 'SQL Server', @@ -105,6 +106,7 @@ const engines = [ drop2: 'DROP PROCEDURE obj2', }, ], + supportSchemas: true, }, { label: 'SQLite', @@ -159,11 +161,11 @@ const filterLocal = [ // filter local testing '-MySQL', '-MariaDB', - '-PostgreSQL', + 'PostgreSQL', '-SQL Server', '-SQLite', '-CockroachDB', - 'ClickHouse', + '-ClickHouse', ]; const enginesPostgre = engines.filter(x => x.label == 'PostgreSQL'); diff --git a/packages/tools/src/DatabaseAnalyser.ts b/packages/tools/src/DatabaseAnalyser.ts index 255de678b..9ebceaac4 100644 --- a/packages/tools/src/DatabaseAnalyser.ts +++ b/packages/tools/src/DatabaseAnalyser.ts @@ -5,6 +5,7 @@ import _pick from 'lodash/pick'; import _compact from 'lodash/compact'; import { getLogger } from './getLogger'; import { type Logger } from 'pinomin'; +import stableStringify from 'json-stable-stringify'; const logger = getLogger('dbAnalyser'); @@ -70,7 +71,10 @@ export class DatabaseAnalyser { async fullAnalysis() { const res = this.addEngineField(await this._runAnalysis()); // console.log('FULL ANALYSIS', res); - return res; + return { + ...res, + schemas: await this.readSchemaList(), + }; } async singleObjectAnalysis(name, typeField) { @@ -87,6 +91,10 @@ export class DatabaseAnalyser { return obj; } + async readSchemaList() { + return undefined; + } + async incrementalAnalysis(structure) { this.structure = structure; @@ -99,22 +107,35 @@ export class DatabaseAnalyser { const structureModifications = modifications.filter(x => x.action != 'setTableRowCounts'); const setTableRowCounts = modifications.find(x => x.action == 'setTableRowCounts'); - let structureWithRowCounts = null; + let structureUpdated = null; if (setTableRowCounts) { const newStructure = mergeTableRowCounts(structure, setTableRowCounts.rowCounts); if (areDifferentRowCounts(structure, newStructure)) { - structureWithRowCounts = newStructure; + structureUpdated = newStructure; } } + const schemas = await this.readSchemaList(); + const areSchemasDifferent = stableStringify(schemas) != stableStringify(this.structure.schemas); + if (areSchemasDifferent) { + structureUpdated = { + ...structure, + ...structureUpdated, + schemas, + }; + } + if (structureModifications.length == 0) { - return structureWithRowCounts ? this.addEngineField(structureWithRowCounts) : null; + return structureUpdated ? this.addEngineField(structureUpdated) : null; } this.modifications = structureModifications; - if (structureWithRowCounts) this.structure = structureWithRowCounts; + if (structureUpdated) this.structure = structureUpdated; logger.info({ modifications: this.modifications }, 'DB modifications detected:'); - return this.addEngineField(this.mergeAnalyseResult(await this._runAnalysis())); + return { + ...this.addEngineField(this.mergeAnalyseResult(await this._runAnalysis())), + schemas, + }; } mergeAnalyseResult(newlyAnalysed) { diff --git a/packages/tools/src/SqlDumper.ts b/packages/tools/src/SqlDumper.ts index 4e0582d3f..3fe9e6256 100644 --- a/packages/tools/src/SqlDumper.ts +++ b/packages/tools/src/SqlDumper.ts @@ -214,6 +214,14 @@ export class SqlDumper implements AlterProcessor { this.putCmd('^drop ^database %i', name); } + createSchema(name: string) { + this.putCmd('^create ^schema %i', name); + } + + dropSchema(name: string) { + this.putCmd('^drop ^schema %i', name); + } + specialColumnOptions(column) {} selectScopeIdentity(table: TableInfo) {} diff --git a/packages/tools/src/driverBase.ts b/packages/tools/src/driverBase.ts index ebcf03cd5..251ea87b9 100644 --- a/packages/tools/src/driverBase.ts +++ b/packages/tools/src/driverBase.ts @@ -84,7 +84,17 @@ export const driverBase = { } }, async operation(pool, operation, options: RunScriptOptions) { - throw new Error('Operation not defined in target driver'); + const { type } = operation; + switch (type) { + case 'createSchema': + await runCommandOnDriver(pool, this, dmp => dmp.createSchema(operation.schemaName)); + break; + case 'dropSchema': + await runCommandOnDriver(pool, this, dmp => dmp.dropSchema(operation.schemaName)); + break; + default: + throw new Error(`Operation type ${type} not supported`); + } }, getNewObjectTemplates() { if (this.databaseEngineTypes.includes('sql')) { @@ -180,5 +190,5 @@ export const driverBase = { adaptTableInfo(table) { return table; - } + }, }; diff --git a/packages/web/src/appobj/DatabaseAppObject.svelte b/packages/web/src/appobj/DatabaseAppObject.svelte index 32368740a..b817f6974 100644 --- a/packages/web/src/appobj/DatabaseAppObject.svelte +++ b/packages/web/src/appobj/DatabaseAppObject.svelte @@ -56,27 +56,7 @@ }; const handleNewTable = () => { - const tooltip = `${getConnectionLabel(connection)}\n${name}`; - openNewTab( - { - title: 'Table #', - tooltip, - icon: 'img table-structure', - tabComponent: 'TableStructureTab', - props: { - conid: connection._id, - database: name, - }, - }, - { - editor: { - columns: [], - }, - }, - { - forceNewTab: true, - } - ); + newTable(connection, name); }; const handleDropDatabase = () => { @@ -410,6 +390,7 @@ import NewCollectionModal from '../modals/NewCollectionModal.svelte'; import hasPermission from '../utility/hasPermission'; import { openImportExportTab } from '../utility/importExportTools'; + import newTable from '../tableeditor/newTable'; export let data; export let passProps; diff --git a/packages/web/src/appobj/DatabaseObjectAppObject.svelte b/packages/web/src/appobj/DatabaseObjectAppObject.svelte index 0f7944169..5a16cfef5 100644 --- a/packages/web/src/appobj/DatabaseObjectAppObject.svelte +++ b/packages/web/src/appobj/DatabaseObjectAppObject.svelte @@ -877,7 +877,7 @@ {...$$restProps} module={$$props.module} {data} - title={data.schemaName ? `${data.schemaName}.${data.pureName}` : data.pureName} + title={data.schemaName && !passProps?.hideSchemaName ? `${data.schemaName}.${data.pureName}` : data.pureName} icon={databaseObjectIcons[data.objectTypeField]} menu={createMenu} showPinnedInsteadOfUnpin={passProps?.showPinnedInsteadOfUnpin} diff --git a/packages/web/src/commands/stdCommands.ts b/packages/web/src/commands/stdCommands.ts index 9a0192541..70282233d 100644 --- a/packages/web/src/commands/stdCommands.ts +++ b/packages/web/src/commands/stdCommands.ts @@ -44,6 +44,7 @@ import NewCollectionModal from '../modals/NewCollectionModal.svelte'; import ConfirmModal from '../modals/ConfirmModal.svelte'; import localforage from 'localforage'; import { openImportExportTab } from '../utility/importExportTools'; +import newTable from '../tableeditor/newTable'; // function themeCommand(theme: ThemeDefinition) { // return { @@ -253,26 +254,7 @@ registerCommand({ const $currentDatabase = get(currentDatabase); const connection = _.get($currentDatabase, 'connection') || {}; const database = _.get($currentDatabase, 'name'); - - openNewTab( - { - title: 'Table #', - icon: 'img table-structure', - tabComponent: 'TableStructureTab', - props: { - conid: connection._id, - database, - }, - }, - { - editor: { - columns: [], - }, - }, - { - forceNewTab: true, - } - ); + newTable(connection, database); }, }); diff --git a/packages/web/src/elements/ObjectFieldsEditor.svelte b/packages/web/src/elements/ObjectFieldsEditor.svelte index caa2fbf63..337625f1a 100644 --- a/packages/web/src/elements/ObjectFieldsEditor.svelte +++ b/packages/web/src/elements/ObjectFieldsEditor.svelte @@ -4,12 +4,15 @@ import FormArgumentList from '../forms/FormArgumentList.svelte'; import { writable } from 'svelte/store'; import FormProviderCore from '../forms/FormProviderCore.svelte'; - import createRef from '../utility/createRef'; + import FormTextField from '../forms/FormTextField.svelte'; + import FormSelectField from '../forms/FormSelectField.svelte'; export let title; export let fieldDefinitions; export let values; export let onChangeValues; + export let pureNameTitle = null; + export let schemaList = null; let collapsed = false; @@ -32,6 +35,17 @@ {#if !collapsed} + {#if schemaList?.length > 0} + ({ label: x.schemaName, value: x.schemaName }))} + /> + {/if} + {#if pureNameTitle} + + {/if} {/if} diff --git a/packages/web/src/forms/SelectField.svelte b/packages/web/src/forms/SelectField.svelte index eadd937d4..a3826861b 100644 --- a/packages/web/src/forms/SelectField.svelte +++ b/packages/web/src/forms/SelectField.svelte @@ -11,6 +11,7 @@ export let isMulti = false; export let notSelected = null; export let defaultValue = ''; + export let selectClass = ''; let listOpen = false; let isFocused = false; @@ -23,6 +24,7 @@ {#if isNative}