diff --git a/.github/workflows/build-app-beta.yaml b/.github/workflows/build-app-beta.yaml index c516c9475..4a4697c19 100644 --- a/.github/workflows/build-app-beta.yaml +++ b/.github/workflows/build-app-beta.yaml @@ -108,7 +108,7 @@ jobs: rm artifacts/builder-debug.yml - name: Upload artifacts - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@v4 with: name: ${{ matrix.os }} path: artifacts diff --git a/.github/workflows/build-app-pro-beta.yaml b/.github/workflows/build-app-pro-beta.yaml index 184a44d8c..9b69277d9 100644 --- a/.github/workflows/build-app-pro-beta.yaml +++ b/.github/workflows/build-app-pro-beta.yaml @@ -138,7 +138,7 @@ jobs: rm artifacts/builder-debug.yml - name: Upload artifacts - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@v4 with: name: ${{ matrix.os }} path: artifacts diff --git a/.github/workflows/build-app-pro.yaml b/.github/workflows/build-app-pro.yaml index fc2e15532..1170a7079 100644 --- a/.github/workflows/build-app-pro.yaml +++ b/.github/workflows/build-app-pro.yaml @@ -141,7 +141,7 @@ jobs: rm artifacts/builder-debug.yml - name: Upload artifacts - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@v4 with: name: ${{ matrix.os }} path: artifacts diff --git a/.github/workflows/build-app.yaml b/.github/workflows/build-app.yaml index ed5ada489..492153279 100644 --- a/.github/workflows/build-app.yaml +++ b/.github/workflows/build-app.yaml @@ -144,7 +144,7 @@ jobs: mv app/dist/dbgate-pad.xml artifacts/ || true - name: Upload artifacts - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@v4 with: name: ${{ matrix.os }} path: artifacts diff --git a/.github/workflows/build-npm.yaml b/.github/workflows/build-npm.yaml index 656e0ab75..83320018c 100644 --- a/.github/workflows/build-npm.yaml +++ b/.github/workflows/build-npm.yaml @@ -154,3 +154,8 @@ jobs: working-directory: plugins/dbgate-plugin-oracle run: | npm publish + + - name: Publish dbgate-plugin-clickhouse + working-directory: plugins/dbgate-plugin-clickhouse + run: | + npm publish diff --git a/.github/workflows/run-tests.yaml b/.github/workflows/run-tests.yaml index 7e28ddf01..ff0a610c9 100644 --- a/.github/workflows/run-tests.yaml +++ b/.github/workflows/run-tests.yaml @@ -77,6 +77,11 @@ jobs: ACCEPT_EULA: Y SA_PASSWORD: Pwd2020Db MSSQL_PID: Express - + + clickhouse: + image: bitnami/clickhouse:24.8.4 + env: + CLICKHOUSE_ADMIN_PASSWORD: Pwd2020Db + # cockroachdb: # image: cockroachdb/cockroach diff --git a/integration-tests/__tests__/alter-database.spec.js b/integration-tests/__tests__/alter-database.spec.js index b786c4718..b06dcf583 100644 --- a/integration-tests/__tests__/alter-database.spec.js +++ b/integration-tests/__tests__/alter-database.spec.js @@ -7,7 +7,9 @@ const { getAlterDatabaseScript, extendDatabaseInfo, generateDbPairingId } = requ function flatSource() { return _.flatten( - engines.map(engine => (engine.objects || []).map(object => [engine.label, object.type, object, engine])) + engines + .filter(x => !x.skipReferences) + .map(engine => (engine.objects || []).map(object => [engine.label, object.type, object, engine])) ); } @@ -41,7 +43,7 @@ async function testDatabaseDiff(conn, driver, mangle, createObject = null) { } describe('Alter database', () => { - test.each(engines.map(engine => [engine.label, engine]))( + test.each(engines.filter(x => !x.skipReferences).map(engine => [engine.label, engine]))( 'Drop referenced table - %s', testWrapper(async (conn, driver, engine) => { await testDatabaseDiff(conn, driver, db => { diff --git a/integration-tests/__tests__/alter-table.spec.js b/integration-tests/__tests__/alter-table.spec.js index 7b6f1e0b5..bab95d741 100644 --- a/integration-tests/__tests__/alter-table.spec.js +++ b/integration-tests/__tests__/alter-table.spec.js @@ -6,39 +6,44 @@ const engines = require('../engines'); const crypto = require('crypto'); const { getAlterTableScript, extendDatabaseInfo, generateDbPairingId } = require('dbgate-tools'); -function pickImportantTableInfo(table) { +function pickImportantTableInfo(engine, table) { + const props = ['columnName']; + if (!engine.skipNullability) props.push('notNull'); + if (!engine.skipAutoIncrement) props.push('autoIncrement'); return { pureName: table.pureName, - columns: table.columns - .filter(x => x.columnName != 'rowid') - .map(fp.pick(['columnName', 'notNull', 'autoIncrement'])), + columns: table.columns.filter(x => x.columnName != 'rowid').map(fp.pick(props)), }; } -function checkTableStructure(t1, t2) { +function checkTableStructure(engine, t1, t2) { // expect(t1.pureName).toEqual(t2.pureName) - expect(pickImportantTableInfo(t1)).toEqual(pickImportantTableInfo(t2)); + expect(pickImportantTableInfo(engine, t1)).toEqual(pickImportantTableInfo(engine, t2)); } -async function testTableDiff(conn, driver, mangle) { +async function testTableDiff(engine, conn, driver, mangle) { await driver.query(conn, `create table t0 (id int not null primary key)`); await driver.query( conn, `create table t1 ( col_pk int not null primary key, - col_std int null, - col_def int null default 12, - col_fk int null references t0(id), - col_idx int null, - col_uq int null unique, - col_ref int null unique + col_std int, + col_def int default 12, + ${engine.skipReferences ? '' : 'col_fk int references t0(id),'} + col_idx int, + col_uq int ${engine.skipUnique ? '' : 'unique'} , + col_ref int ${engine.skipUnique ? '' : 'unique'} )` ); - await driver.query(conn, `create index idx1 on t1(col_idx)`); + if (!engine.skipIndexes) { + await driver.query(conn, `create index idx1 on t1(col_idx)`); + } - await driver.query(conn, `create table t2 (id int not null primary key, fkval int null references t1(col_ref))`); + if (!engine.skipReferences) { + await driver.query(conn, `create table t2 (id int not null primary key, fkval int null references t1(col_ref))`); + } const tget = x => x.tables.find(y => y.pureName == 't1'); const structure1 = generateDbPairingId(extendDatabaseInfo(await driver.analyseFull(conn))); @@ -53,7 +58,7 @@ async function testTableDiff(conn, driver, mangle) { const structure2Real = extendDatabaseInfo(await driver.analyseFull(conn)); - checkTableStructure(tget(structure2Real), tget(structure2)); + checkTableStructure(engine, tget(structure2Real), tget(structure2)); // expect(stableStringify(structure2)).toEqual(stableStringify(structure2Real)); } @@ -65,14 +70,22 @@ const TESTED_COLUMNS = ['col_pk', 'col_std', 'col_def', 'col_fk', 'col_ref', 'co // const TESTED_COLUMNS = ['col_ref']; function engines_columns_source() { - return _.flatten(engines.map(engine => TESTED_COLUMNS.map(column => [engine.label, column, engine]))); + return _.flatten( + engines.map(engine => + TESTED_COLUMNS.filter(col => !col.endsWith('_pk') || !engine.skipPkColumnTesting).map(column => [ + engine.label, + column, + engine, + ]) + ) + ); } describe('Alter table', () => { test.each(engines.map(engine => [engine.label, engine]))( 'Add column - %s', testWrapper(async (conn, driver, engine) => { - await testTableDiff(conn, driver, tbl => { + await testTableDiff(engine, conn, driver, tbl => { tbl.columns.push({ columnName: 'added', dataType: 'int', @@ -87,7 +100,7 @@ describe('Alter table', () => { test.each(engines_columns_source())( 'Drop column - %s - %s', testWrapper(async (conn, driver, column, engine) => { - await testTableDiff(conn, driver, tbl => (tbl.columns = tbl.columns.filter(x => x.columnName != column))); + await testTableDiff(engine, conn, driver, tbl => (tbl.columns = tbl.columns.filter(x => x.columnName != column))); }) ); @@ -95,6 +108,7 @@ describe('Alter table', () => { 'Change nullability - %s - %s', testWrapper(async (conn, driver, column, engine) => { await testTableDiff( + engine, conn, driver, tbl => (tbl.columns = tbl.columns.map(x => (x.columnName == column ? { ...x, notNull: true } : x))) @@ -106,6 +120,7 @@ describe('Alter table', () => { 'Rename column - %s - %s', testWrapper(async (conn, driver, column, engine) => { await testTableDiff( + engine, conn, driver, tbl => (tbl.columns = tbl.columns.map(x => (x.columnName == column ? { ...x, columnName: 'col_renamed' } : x))) @@ -116,7 +131,7 @@ describe('Alter table', () => { test.each(engines.map(engine => [engine.label, engine]))( 'Drop index - %s', testWrapper(async (conn, driver, engine) => { - await testTableDiff(conn, driver, tbl => { + await testTableDiff(engine, conn, driver, tbl => { tbl.indexes = []; }); }) diff --git a/integration-tests/__tests__/data-duplicator.spec.js b/integration-tests/__tests__/data-duplicator.spec.js index 3227cd984..7d5c532e5 100644 --- a/integration-tests/__tests__/data-duplicator.spec.js +++ b/integration-tests/__tests__/data-duplicator.spec.js @@ -5,7 +5,7 @@ const dataDuplicator = require('dbgate-api/src/shell/dataDuplicator'); const { runCommandOnDriver } = require('dbgate-tools'); describe('Data duplicator', () => { - test.each(engines.map(engine => [engine.label, engine]))( + test.each(engines.filter(x => !x.skipDataDuplicator).map(engine => [engine.label, engine]))( 'Insert simple data - %s', testWrapper(async (conn, driver, engine) => { runCommandOnDriver(conn, driver, dmp => diff --git a/integration-tests/__tests__/deploy-database.spec.js b/integration-tests/__tests__/deploy-database.spec.js index 416f904f5..bcbe4e749 100644 --- a/integration-tests/__tests__/deploy-database.spec.js +++ b/integration-tests/__tests__/deploy-database.spec.js @@ -167,7 +167,7 @@ describe('Deploy database', () => { }) ); - test.each(engines.map(engine => [engine.label, engine]))( + test.each(engines.filter(x => !x.skipReferences).map(engine => [engine.label, engine]))( 'Foreign keys - %s', testWrapper(async (conn, driver, engine) => { await testDatabaseDeploy( @@ -222,7 +222,7 @@ describe('Deploy database', () => { }) ); - test.each(engines.map(engine => [engine.label, engine]))( + test.each(engines.filter(x => !x.skipDataModifications).map(engine => [engine.label, engine]))( 'Deploy preloaded data - %s', testWrapper(async (conn, driver, engine) => { await testDatabaseDeploy(conn, driver, [ @@ -251,7 +251,7 @@ describe('Deploy database', () => { }) ); - test.each(engines.map(engine => [engine.label, engine]))( + test.each(engines.filter(x => !x.skipDataModifications).map(engine => [engine.label, engine]))( 'Deploy preloaded data - update - %s', testWrapper(async (conn, driver, engine) => { await testDatabaseDeploy(conn, driver, [ diff --git a/integration-tests/__tests__/object-analyse.spec.js b/integration-tests/__tests__/object-analyse.spec.js index b088d1b62..e49e098df 100644 --- a/integration-tests/__tests__/object-analyse.spec.js +++ b/integration-tests/__tests__/object-analyse.spec.js @@ -2,7 +2,7 @@ const { testWrapper } = require('../tools'); const engines = require('../engines'); const _ = require('lodash'); -const initSql = ['CREATE TABLE t1 (id int)', 'CREATE TABLE t2 (id int)']; +const initSql = ['CREATE TABLE t1 (id int primary key)', 'CREATE TABLE t2 (id int primary key)']; function flatSource() { return _.flatten( @@ -26,9 +26,9 @@ describe('Object analyse', () => { test.each(flatSource())( 'Full analysis - %s - %s', testWrapper(async (conn, driver, type, object, engine) => { - for (const sql of initSql) await driver.query(conn, sql); + for (const sql of initSql) await driver.query(conn, sql, { discardResult: true }); - await driver.query(conn, object.create1); + await driver.query(conn, object.create1, { discardResult: true }); const structure = await driver.analyseFull(conn); expect(structure[type].length).toEqual(1); @@ -39,11 +39,11 @@ describe('Object analyse', () => { test.each(flatSource())( 'Incremental analysis - add - %s - %s', testWrapper(async (conn, driver, type, object, engine) => { - for (const sql of initSql) await driver.query(conn, sql); + for (const sql of initSql) await driver.query(conn, sql, { discardResult: true }); - await driver.query(conn, object.create2); + await driver.query(conn, object.create2, { discardResult: true }); const structure1 = await driver.analyseFull(conn); - await driver.query(conn, object.create1); + await driver.query(conn, object.create1, { discardResult: true }); const structure2 = await driver.analyseIncremental(conn, structure1); expect(structure2[type].length).toEqual(2); @@ -54,12 +54,12 @@ describe('Object analyse', () => { test.each(flatSource())( 'Incremental analysis - drop - %s - %s', testWrapper(async (conn, driver, type, object, engine) => { - for (const sql of initSql) await driver.query(conn, sql); + for (const sql of initSql) await driver.query(conn, sql, { discardResult: true }); - await driver.query(conn, object.create1); - await driver.query(conn, object.create2); + await driver.query(conn, object.create1, { discardResult: true }); + await driver.query(conn, object.create2, { discardResult: true }); const structure1 = await driver.analyseFull(conn); - await driver.query(conn, object.drop2); + await driver.query(conn, object.drop2, { discardResult: true }); const structure2 = await driver.analyseIncremental(conn, structure1); expect(structure2[type].length).toEqual(1); @@ -70,15 +70,15 @@ describe('Object analyse', () => { test.each(flatSource())( 'Create SQL - add - %s - %s', testWrapper(async (conn, driver, type, object, engine) => { - for (const sql of initSql) await driver.query(conn, sql); + for (const sql of initSql) await driver.query(conn, sql, { discardResult: true }); - await driver.query(conn, object.create1); + await driver.query(conn, object.create1, { discardResult: true }); const structure1 = await driver.analyseFull(conn); - await driver.query(conn, object.drop1); + await driver.query(conn, object.drop1, { discardResult: true }); const structure2 = await driver.analyseIncremental(conn, structure1); expect(structure2[type].length).toEqual(0); - await driver.query(conn, structure1[type][0].createSql); + await driver.query(conn, structure1[type][0].createSql, { discardResult: true }); const structure3 = await driver.analyseIncremental(conn, structure2); diff --git a/integration-tests/__tests__/query.spec.js b/integration-tests/__tests__/query.spec.js index 1336a7df6..d01648286 100644 --- a/integration-tests/__tests__/query.spec.js +++ b/integration-tests/__tests__/query.spec.js @@ -2,7 +2,11 @@ const engines = require('../engines'); const { splitQuery } = require('dbgate-query-splitter'); const { testWrapper } = require('../tools'); -const initSql = ['CREATE TABLE t1 (id int)', 'INSERT INTO t1 (id) VALUES (1)', 'INSERT INTO t1 (id) VALUES (2)']; +const initSql = [ + 'CREATE TABLE t1 (id int primary key)', + 'INSERT INTO t1 (id) VALUES (1)', + 'INSERT INTO t1 (id) VALUES (2)', +]; expect.extend({ dataRow(row, expected) { @@ -64,7 +68,7 @@ describe('Query', () => { test.each(engines.map(engine => [engine.label, engine]))( 'Simple query - %s', testWrapper(async (conn, driver, engine) => { - for (const sql of initSql) await driver.query(conn, sql); + for (const sql of initSql) await driver.query(conn, sql, { discardResult: true }); const res = await driver.query(conn, 'SELECT id FROM t1 ORDER BY id'); expect(res.columns).toEqual([ @@ -87,7 +91,7 @@ describe('Query', () => { test.each(engines.map(engine => [engine.label, engine]))( 'Simple stream query - %s', testWrapper(async (conn, driver, engine) => { - for (const sql of initSql) await driver.query(conn, sql); + for (const sql of initSql) await driver.query(conn, sql, { discardResult: true }); const results = await executeStream(driver, conn, 'SELECT id FROM t1 ORDER BY id'); expect(results.length).toEqual(1); const res = results[0]; @@ -100,7 +104,7 @@ describe('Query', () => { test.each(engines.map(engine => [engine.label, engine]))( 'More queries - %s', testWrapper(async (conn, driver, engine) => { - for (const sql of initSql) await driver.query(conn, sql); + for (const sql of initSql) await driver.query(conn, sql, { discardResult: true }); const results = await executeStream( driver, conn, @@ -124,7 +128,7 @@ describe('Query', () => { const results = await executeStream( driver, conn, - 'CREATE TABLE t1 (id int); INSERT INTO t1 (id) VALUES (1); INSERT INTO t1 (id) VALUES (2); SELECT id FROM t1 ORDER BY id; ' + 'CREATE TABLE t1 (id int primary key); INSERT INTO t1 (id) VALUES (1); INSERT INTO t1 (id) VALUES (2); SELECT id FROM t1 ORDER BY id; ' ); expect(results.length).toEqual(1); @@ -146,14 +150,15 @@ describe('Query', () => { }) ); - test.each(engines.map(engine => [engine.label, engine]))( + test.each(engines.filter(x => !x.skipDataModifications).map(engine => [engine.label, engine]))( 'Save data query - %s', testWrapper(async (conn, driver, engine) => { - for (const sql of initSql) await driver.query(conn, sql); + for (const sql of initSql) await driver.query(conn, sql, { discardResult: true }); await driver.script( conn, - 'INSERT INTO t1 (id) VALUES (3);INSERT INTO t1 (id) VALUES (4);UPDATE t1 SET id=10 WHERE id=1;DELETE FROM t1 WHERE id=2;' + 'INSERT INTO t1 (id) VALUES (3);INSERT INTO t1 (id) VALUES (4);UPDATE t1 SET id=10 WHERE id=1;DELETE FROM t1 WHERE id=2;', + { discardResult: true } ); const res = await driver.query(conn, 'SELECT COUNT(*) AS cnt FROM t1'); // console.log(res); diff --git a/integration-tests/__tests__/table-analyse.spec.js b/integration-tests/__tests__/table-analyse.spec.js index 0c17b95ee..16c7fb8d1 100644 --- a/integration-tests/__tests__/table-analyse.spec.js +++ b/integration-tests/__tests__/table-analyse.spec.js @@ -1,32 +1,37 @@ const engines = require('../engines'); const { testWrapper } = require('../tools'); -const t1Sql = 'CREATE TABLE t1 (id int not null primary key, val1 varchar(50) null)'; +const t1Sql = 'CREATE TABLE t1 (id int not null primary key, val1 varchar(50))'; const ix1Sql = 'CREATE index ix1 ON t1(val1, id)'; -const t2Sql = 'CREATE TABLE t2 (id int not null primary key, val2 varchar(50) null unique)'; +const t2Sql = engine => + `CREATE TABLE t2 (id int not null primary key, val2 varchar(50) ${engine.skipUnique ? '' : 'unique'})`; const t3Sql = 'CREATE TABLE t3 (id int not null primary key, valfk int, foreign key (valfk) references t2(id))'; // const fkSql = 'ALTER TABLE t3 ADD FOREIGN KEY (valfk) REFERENCES t2(id)' -const txMatch = (tname, vcolname, nextcol) => +const txMatch = (engine, tname, vcolname, nextcol) => expect.objectContaining({ pureName: tname, columns: [ expect.objectContaining({ columnName: 'id', - notNull: true, - dataType: expect.stringMatching(/int/i), + dataType: expect.stringMatching(/int.*/i), + ...(engine.skipNullability ? {} : { notNull: true }), }), expect.objectContaining({ columnName: vcolname, - notNull: false, - dataType: expect.stringMatching(/.*char.*\(50\)/), + ...(engine.skipNullability ? {} : { notNull: false }), + dataType: engine.skipStringLength + ? expect.stringMatching(/.*string|char.*/i) + : expect.stringMatching(/.*char.*\(50\)/i), }), ...(nextcol ? [ expect.objectContaining({ columnName: 'nextcol', - notNull: false, - dataType: expect.stringMatching(/.*char.*\(50\)/), + ...(engine.skipNullability ? {} : { notNull: false }), + dataType: engine.skipStringLength + ? expect.stringMatching(/.*string.*|char.*/i) + : expect.stringMatching(/.*char.*\(50\).*/i), }), ] : []), @@ -40,9 +45,9 @@ const txMatch = (tname, vcolname, nextcol) => }), }); -const t1Match = txMatch('t1', 'val1'); -const t2Match = txMatch('t2', 'val2'); -const t2NextColMatch = txMatch('t2', 'val2', true); +const t1Match = engine => txMatch(engine, 't1', 'val1'); +const t2Match = engine => txMatch(engine, 't2', 'val2'); +const t2NextColMatch = engine => txMatch(engine, 't2', 'val2', true); describe('Table analyse', () => { test.each(engines.map(engine => [engine.label, engine]))( @@ -53,25 +58,25 @@ describe('Table analyse', () => { const structure = await driver.analyseFull(conn); expect(structure.tables.length).toEqual(1); - expect(structure.tables[0]).toEqual(t1Match); + expect(structure.tables[0]).toEqual(t1Match(engine)); }) ); test.each(engines.map(engine => [engine.label, engine]))( 'Table add - incremental analysis - %s', testWrapper(async (conn, driver, engine) => { - await driver.query(conn, t2Sql); + await driver.query(conn, t2Sql(engine)); const structure1 = await driver.analyseFull(conn); expect(structure1.tables.length).toEqual(1); - expect(structure1.tables[0]).toEqual(t2Match); + expect(structure1.tables[0]).toEqual(t2Match(engine)); await driver.query(conn, t1Sql); const structure2 = await driver.analyseIncremental(conn, structure1); expect(structure2.tables.length).toEqual(2); - expect(structure2.tables.find(x => x.pureName == 't1')).toEqual(t1Match); - expect(structure2.tables.find(x => x.pureName == 't2')).toEqual(t2Match); + expect(structure2.tables.find(x => x.pureName == 't1')).toEqual(t1Match(engine)); + expect(structure2.tables.find(x => x.pureName == 't2')).toEqual(t2Match(engine)); }) ); @@ -79,17 +84,17 @@ describe('Table analyse', () => { 'Table remove - incremental analysis - %s', testWrapper(async (conn, driver, engine) => { await driver.query(conn, t1Sql); - await driver.query(conn, t2Sql); + await driver.query(conn, t2Sql(engine)); const structure1 = await driver.analyseFull(conn); expect(structure1.tables.length).toEqual(2); - expect(structure1.tables.find(x => x.pureName == 't1')).toEqual(t1Match); - expect(structure1.tables.find(x => x.pureName == 't2')).toEqual(t2Match); + expect(structure1.tables.find(x => x.pureName == 't1')).toEqual(t1Match(engine)); + expect(structure1.tables.find(x => x.pureName == 't2')).toEqual(t2Match(engine)); await driver.query(conn, 'DROP TABLE t2'); const structure2 = await driver.analyseIncremental(conn, structure1); expect(structure2.tables.length).toEqual(1); - expect(structure2.tables[0]).toEqual(t1Match); + expect(structure2.tables[0]).toEqual(t1Match(engine)); }) ); @@ -97,23 +102,26 @@ describe('Table analyse', () => { 'Table change - incremental analysis - %s', testWrapper(async (conn, driver, engine) => { await driver.query(conn, t1Sql); - await driver.query(conn, t2Sql); + await driver.query(conn, t2Sql(engine)); const structure1 = await driver.analyseFull(conn); if (engine.dbSnapshotBySeconds) await new Promise(resolve => setTimeout(resolve, 1100)); - await driver.query(conn, 'ALTER TABLE t2 ADD nextcol varchar(50)'); + await driver.query( + conn, + `ALTER TABLE t2 ADD ${engine.alterTableAddColumnSyntax ? 'COLUMN' : ''} nextcol varchar(50)` + ); const structure2 = await driver.analyseIncremental(conn, structure1); expect(structure2).toBeTruthy(); // if falsy, no modification is detected expect(structure2.tables.length).toEqual(2); - expect(structure2.tables.find(x => x.pureName == 't1')).toEqual(t1Match); - expect(structure2.tables.find(x => x.pureName == 't2')).toEqual(t2NextColMatch); + expect(structure2.tables.find(x => x.pureName == 't1')).toEqual(t1Match(engine)); + expect(structure2.tables.find(x => x.pureName == 't2')).toEqual(t2NextColMatch(engine)); }) ); - test.each(engines.map(engine => [engine.label, engine]))( + test.each(engines.filter(x => !x.skipIndexes).map(engine => [engine.label, engine]))( 'Index - full analysis - %s', testWrapper(async (conn, driver, engine) => { await driver.query(conn, t1Sql); @@ -128,10 +136,10 @@ describe('Table analyse', () => { }) ); - test.each(engines.map(engine => [engine.label, engine]))( + test.each(engines.filter(x => !x.skipUnique).map(engine => [engine.label, engine]))( 'Unique - full analysis - %s', testWrapper(async (conn, driver, engine) => { - await driver.query(conn, t2Sql); + await driver.query(conn, t2Sql(engine)); const structure = await driver.analyseFull(conn); const t2 = structure.tables.find(x => x.pureName == 't2'); @@ -142,10 +150,10 @@ describe('Table analyse', () => { }) ); - test.each(engines.map(engine => [engine.label, engine]))( + test.each(engines.filter(x => !x.skipReferences).map(engine => [engine.label, engine]))( 'Foreign key - full analysis - %s', testWrapper(async (conn, driver, engine) => { - await driver.query(conn, t2Sql); + await driver.query(conn, t2Sql(engine)); await driver.query(conn, t3Sql); // await driver.query(conn, fkSql); diff --git a/integration-tests/__tests__/table-create.spec.js b/integration-tests/__tests__/table-create.spec.js index 618c39ae2..471f5e544 100644 --- a/integration-tests/__tests__/table-create.spec.js +++ b/integration-tests/__tests__/table-create.spec.js @@ -62,7 +62,7 @@ describe('Table create', () => { }) ); - test.each(engines.map(engine => [engine.label, engine]))( + test.each(engines.filter(x => !x.skipIndexes).map(engine => [engine.label, engine]))( 'Table with index - %s', testWrapper(async (conn, driver, engine) => { await testTableCreate(conn, driver, { @@ -92,7 +92,7 @@ describe('Table create', () => { }) ); - test.each(engines.map(engine => [engine.label, engine]))( + test.each(engines.filter(x => !x.skipReferences).map(engine => [engine.label, engine]))( 'Table with foreign key - %s', testWrapper(async (conn, driver, engine) => { await testTableCreate(conn, driver, { @@ -122,7 +122,7 @@ describe('Table create', () => { }) ); - test.each(engines.map(engine => [engine.label, engine]))( + test.each(engines.filter(x => !x.skipUnique).map(engine => [engine.label, engine]))( 'Table with unique - %s', testWrapper(async (conn, driver, engine) => { await testTableCreate(conn, driver, { diff --git a/integration-tests/docker-compose.yaml b/integration-tests/docker-compose.yaml index 5c0d846da..7f0bc431f 100644 --- a/integration-tests/docker-compose.yaml +++ b/integration-tests/docker-compose.yaml @@ -26,15 +26,23 @@ services: # environment: # - MYSQL_ROOT_PASSWORD=Pwd2020Db - mssql: - image: mcr.microsoft.com/mssql/server + clickhouse: + image: bitnami/clickhouse:24.8.4 restart: always ports: - - 15002:1433 + - 15005:8123 environment: - - ACCEPT_EULA=Y - - SA_PASSWORD=Pwd2020Db - - MSSQL_PID=Express + - CLICKHOUSE_ADMIN_PASSWORD=Pwd2020Db + + # mssql: + # image: mcr.microsoft.com/mssql/server + # restart: always + # ports: + # - 15002:1433 + # environment: + # - ACCEPT_EULA=Y + # - SA_PASSWORD=Pwd2020Db + # - MSSQL_PID=Express # cockroachdb: # image: cockroachdb/cockroach diff --git a/integration-tests/engines.js b/integration-tests/engines.js index 196919223..564a35775 100644 --- a/integration-tests/engines.js +++ b/integration-tests/engines.js @@ -129,6 +129,30 @@ const engines = [ skipOnCI: true, objects: [views, matviews], }, + { + label: 'ClickHouse', + connection: { + engine: 'clickhouse@dbgate-plugin-clickhouse', + databaseUrl: 'http://clickhouse:8123', + password: 'Pwd2020Db', + }, + local: { + databaseUrl: 'http://localhost:15005', + }, + skipOnCI: false, + objects: [views], + skipDataModifications: true, + skipReferences: true, + skipIndexes: true, + skipNullability: true, + skipUnique: true, + skipAutoIncrement: true, + skipPkColumnTesting: true, + skipDataDuplicator: true, + skipStringLength: true, + alterTableAddColumnSyntax: true, + dbSnapshotBySeconds: true, + }, ]; const filterLocal = [ @@ -137,8 +161,9 @@ const filterLocal = [ '-MariaDB', '-PostgreSQL', '-SQL Server', - 'SQLite', + '-SQLite', '-CockroachDB', + 'ClickHouse', ]; const enginesPostgre = engines.filter(x => x.label == 'PostgreSQL'); diff --git a/integration-tests/jest.config.js b/integration-tests/jest.config.js new file mode 100644 index 000000000..83368208f --- /dev/null +++ b/integration-tests/jest.config.js @@ -0,0 +1,3 @@ +module.exports = { + setupFilesAfterEnv: ['/setupTests.js'], +}; diff --git a/integration-tests/package.json b/integration-tests/package.json index 3b0ac39f9..dcb1e67e4 100644 --- a/integration-tests/package.json +++ b/integration-tests/package.json @@ -11,12 +11,9 @@ "scripts": { "wait:local": "cross-env DEVMODE=1 LOCALTEST=1 node wait.js", "wait:ci": "cross-env DEVMODE=1 CITEST=1 node wait.js", - "test:local": "cross-env DEVMODE=1 LOCALTEST=1 jest", "test:local:path": "cross-env DEVMODE=1 LOCALTEST=1 jest --runTestsByPath __tests__/data-duplicator.spec.js", - "test:ci": "cross-env DEVMODE=1 CITEST=1 jest --runInBand --json --outputFile=result.json --testLocationInResults", - "run:local": "docker-compose down && docker-compose up -d && yarn wait:local && yarn test:local" }, "jest": { @@ -24,7 +21,7 @@ }, "devDependencies": { "cross-env": "^7.0.3", - "jest": "^27.0.1" - }, - "dependencies": {} + "jest": "^27.0.1", + "pino-pretty": "^11.2.2" + } } diff --git a/integration-tests/setupTests.js b/integration-tests/setupTests.js new file mode 100644 index 000000000..b52c4066e --- /dev/null +++ b/integration-tests/setupTests.js @@ -0,0 +1,22 @@ +const { prettyFactory } = require('pino-pretty'); + +const pretty = prettyFactory({ + colorize: true, + translateTime: 'SYS:standard', + ignore: 'pid,hostname', +}); + +global.console = { + ...console, + log: (...messages) => { + try { + const parsedMessage = JSON.parse(messages[0]); + process.stdout.write(pretty(parsedMessage)); + } catch (error) { + process.stdout.write(messages.join(' ') + '\n'); + } + }, + debug: (...messages) => { + process.stdout.write(messages.join(' ') + '\n'); + }, +}; diff --git a/package.json b/package.json index 911fb6af6..eea07773c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "5.4.4", + "version": "5.4.5-alpha.5", "name": "dbgate-all", "workspaces": [ "packages/*", diff --git a/packages/api/src/proc/serverConnectionProcess.js b/packages/api/src/proc/serverConnectionProcess.js index 66f48bcec..b946f4643 100644 --- a/packages/api/src/proc/serverConnectionProcess.js +++ b/packages/api/src/proc/serverConnectionProcess.js @@ -115,7 +115,7 @@ async function handleDatabaseOp(op, { msgid, name }) { const dmp = driver.createDumper(); dmp[op](name); logger.info({ sql: dmp.s }, 'Running script'); - await driver.query(systemConnection, dmp.s); + await driver.query(systemConnection, dmp.s, { discardResult: true }); } await handleRefresh(); diff --git a/packages/sqltree/src/dumpSqlCommand.ts b/packages/sqltree/src/dumpSqlCommand.ts index b16f3c34d..d73d7d690 100644 --- a/packages/sqltree/src/dumpSqlCommand.ts +++ b/packages/sqltree/src/dumpSqlCommand.ts @@ -62,10 +62,13 @@ export function dumpSqlSelect(dmp: SqlDumper, cmd: Select) { } export function dumpSqlUpdate(dmp: SqlDumper, cmd: Update) { - dmp.put('^update '); - dumpSqlSourceRef(dmp, cmd.from); - - dmp.put('&n^set '); + if (cmd.alterTableUpdateSyntax) { + dmp.put('^alter ^table %f &n^update ', cmd.from?.name); + } else { + dmp.put('^update '); + dumpSqlSourceRef(dmp, cmd.from); + dmp.put('&n^set '); + } dmp.put('&>'); dmp.putCollection(', ', cmd.fields, col => { dmp.put('%i=', col.targetColumn); @@ -81,8 +84,14 @@ export function dumpSqlUpdate(dmp: SqlDumper, cmd: Update) { } export function dumpSqlDelete(dmp: SqlDumper, cmd: Delete) { - dmp.put('^delete ^from '); - dumpSqlSourceRef(dmp, cmd.from); + if (cmd.alterTableDeleteSyntax) { + dmp.put('^alter ^table '); + dumpSqlSourceRef(dmp, cmd.from); + dmp.put(' ^delete '); + } else { + dmp.put('^delete ^from '); + dumpSqlSourceRef(dmp, cmd.from); + } if (cmd.where) { dmp.put('&n^where '); diff --git a/packages/sqltree/src/types.ts b/packages/sqltree/src/types.ts index 44a1e2ea7..c2fe24674 100644 --- a/packages/sqltree/src/types.ts +++ b/packages/sqltree/src/types.ts @@ -26,12 +26,17 @@ export interface Update { fields: UpdateField[]; from: FromDefinition; where?: Condition; + // ALTER TABLE xxx UPDATE col1=val1 - syntax for ClickHouse + alterTableUpdateSyntax?: boolean; } export interface Delete { commandType: 'delete'; from: FromDefinition; where?: Condition; + + // ALTER TABLE xxx DELETE - syntax for ClickHouse + alterTableDeleteSyntax?: boolean; } export interface Insert { diff --git a/packages/tools/src/SqlDumper.ts b/packages/tools/src/SqlDumper.ts index e0a72d0c7..4e0582d3f 100644 --- a/packages/tools/src/SqlDumper.ts +++ b/packages/tools/src/SqlDumper.ts @@ -246,7 +246,7 @@ export class SqlDumper implements AlterProcessor { this.putRaw(' '); this.specialColumnOptions(column); - if (includeNullable) { + if (includeNullable && !this.dialect?.specificNullabilityImplementation) { this.put(column.notNull ? '^not ^null' : '^null'); } if (includeDefault && column.defaultValue?.trim()) { @@ -294,12 +294,25 @@ export class SqlDumper implements AlterProcessor { }); this.put('&<&n)'); + + this.tableOptions(table); + this.endCommand(); (table.indexes || []).forEach(ix => { this.createIndex(ix); }); } + tableOptions(table: TableInfo) { + const options = this.driver?.dialect?.getTableFormOptions?.('sqlCreateTable') || []; + for (const option of options) { + if (table[option.name]) { + this.put('&n'); + this.put(option.sqlFormatString, table[option.name]); + } + } + } + createTablePrimaryKeyCore(table: TableInfo) { if (table.primaryKey) { this.put(',&n'); @@ -531,7 +544,9 @@ export class SqlDumper implements AlterProcessor { renameConstraint(constraint: ConstraintInfo, newName: string) {} createColumn(column: ColumnInfo, constraints: ConstraintInfo[]) { - this.put('^alter ^table %f ^add %i ', column, column.columnName); + this.put('^alter ^table %f ^add ', column); + if (this.dialect.createColumnWithColumnKeyword) this.put('^column '); + this.put(' %i ', column.columnName); this.columnDefinition(column); this.inlineConstraints(constraints); this.endCommand(); @@ -607,10 +622,8 @@ export class SqlDumper implements AlterProcessor { if (!oldTable.pairingId || !newTable.pairingId || oldTable.pairingId != newTable.pairingId) { throw new Error('Recreate is not possible: oldTable.paringId != newTable.paringId'); } - const tmpTable = `temp_${uuidv1()}`; - // console.log('oldTable', oldTable); - // console.log('newTable', newTable); + const tmpTable = `temp_${uuidv1()}`; const columnPairs = oldTable.columns .map(oldcol => ({ @@ -619,33 +632,49 @@ export class SqlDumper implements AlterProcessor { })) .filter(x => x.newcol); - this.dropConstraints(oldTable, true); - this.renameTable(oldTable, tmpTable); + if (this.driver.supportsTransactions) { + this.dropConstraints(oldTable, true); + this.renameTable(oldTable, tmpTable); - this.createTable(newTable); + this.createTable(newTable); - const autoinc = newTable.columns.find(x => x.autoIncrement); - if (autoinc) { - this.allowIdentityInsert(newTable, true); + const autoinc = newTable.columns.find(x => x.autoIncrement); + if (autoinc) { + this.allowIdentityInsert(newTable, true); + } + + this.putCmd( + '^insert ^into %f (%,i) select %,s ^from %f', + newTable, + columnPairs.map(x => x.newcol.columnName), + columnPairs.map(x => x.oldcol.columnName), + { ...oldTable, pureName: tmpTable } + ); + + if (autoinc) { + this.allowIdentityInsert(newTable, false); + } + + if (this.dialect.dropForeignKey) { + newTable.dependencies.forEach(cnt => this.createConstraint(cnt)); + } + + this.dropTable({ ...oldTable, pureName: tmpTable }); + } else { + // we have to preserve old table as long as possible + this.createTable({ ...newTable, pureName: tmpTable }); + + this.putCmd( + '^insert ^into %f (%,i) select %,s ^from %f', + { ...newTable, pureName: tmpTable }, + columnPairs.map(x => x.newcol.columnName), + columnPairs.map(x => x.oldcol.columnName), + oldTable + ); + + this.dropTable(oldTable); + this.renameTable({ ...newTable, pureName: tmpTable }, newTable.pureName); } - - this.putCmd( - '^insert ^into %f (%,i) select %,s ^from %f', - newTable, - columnPairs.map(x => x.newcol.columnName), - columnPairs.map(x => x.oldcol.columnName), - { ...oldTable, pureName: tmpTable } - ); - - if (autoinc) { - this.allowIdentityInsert(newTable, false); - } - - if (this.dialect.dropForeignKey) { - newTable.dependencies.forEach(cnt => this.createConstraint(cnt)); - } - - this.dropTable({ ...oldTable, pureName: tmpTable }); } createSqlObject(obj: SqlObjectInfo) { @@ -671,6 +700,23 @@ export class SqlDumper implements AlterProcessor { this.putCmd('^drop %s %f', this.getSqlObjectSqlName(obj.objectTypeField), obj); } + setTableOption(table: TableInfo, optionName: string, optionValue: string) { + const options = this?.dialect?.getTableFormOptions?.('sqlAlterTable'); + const option = options?.find(x => x.name == optionName && !x.disabled); + if (!option) { + return; + } + + this.setTableOptionCore(table, optionName, optionValue, option.sqlFormatString); + + this.endCommand(); + } + + setTableOptionCore(table: TableInfo, optionName: string, optionValue: string, formatString: string) { + this.put('^alter ^table %f ', table); + this.put(formatString, optionValue); + } + fillPreloadedRows( table: NamedObjectInfo, oldRows: any[], diff --git a/packages/tools/src/alterPlan.ts b/packages/tools/src/alterPlan.ts index c47ecdc95..65c432067 100644 --- a/packages/tools/src/alterPlan.ts +++ b/packages/tools/src/alterPlan.ts @@ -97,6 +97,13 @@ interface AlterOperation_FillPreloadedRows { autoIncrementColumn: string; } +interface AlterOperation_SetTableOption { + operationType: 'setTableOption'; + table: TableInfo; + optionName: string; + optionValue: string; +} + type AlterOperation = | AlterOperation_CreateColumn | AlterOperation_ChangeColumn @@ -112,7 +119,8 @@ type AlterOperation = | AlterOperation_CreateSqlObject | AlterOperation_DropSqlObject | AlterOperation_RecreateTable - | AlterOperation_FillPreloadedRows; + | AlterOperation_FillPreloadedRows + | AlterOperation_SetTableOption; export class AlterPlan { recreates = { @@ -253,6 +261,15 @@ export class AlterPlan { }); } + setTableOption(table: TableInfo, optionName: string, optionValue: string) { + this.operations.push({ + operationType: 'setTableOption', + table, + optionName, + optionValue, + }); + } + run(processor: AlterProcessor) { for (const op of this.operations) { runAlterOperation(op, processor); @@ -267,6 +284,7 @@ export class AlterPlan { : []; const constraints = _.compact([ dependencyDefinition?.includes('primaryKey') ? table.primaryKey : null, + dependencyDefinition?.includes('sortingKey') ? table.sortingKey : null, ...(dependencyDefinition?.includes('foreignKeys') ? table.foreignKeys : []), ...(dependencyDefinition?.includes('indexes') ? table.indexes : []), ...(dependencyDefinition?.includes('uniques') ? table.uniques : []), @@ -297,35 +315,40 @@ export class AlterPlan { return res; } - if (op.operationType == 'changeColumn') { - const constraints = this._getDependendColumnConstraints(op.oldObject, this.dialect.changeColumnDependencies); + for (const [testedOperationType, testedDependencies, testedObject] of [ + ['changeColumn', this.dialect.changeColumnDependencies, (op as AlterOperation_ChangeColumn).oldObject], + ['renameColumn', this.dialect.renameColumnDependencies, (op as AlterOperation_RenameColumn).object], + ]) { + if (op.operationType == testedOperationType) { + const constraints = this._getDependendColumnConstraints(testedObject as ColumnInfo, testedDependencies); - if (constraints.length > 0 && this.opts.noDropConstraint) { - return []; + if (constraints.length > 0 && this.opts.noDropConstraint) { + return []; + } + + const res: AlterOperation[] = [ + ...constraints.map(oldObject => { + const opRes: AlterOperation = { + operationType: 'dropConstraint', + oldObject, + }; + return opRes; + }), + op, + ..._.reverse([...constraints]).map(newObject => { + const opRes: AlterOperation = { + operationType: 'createConstraint', + newObject, + }; + return opRes; + }), + ]; + + if (constraints.length > 0) { + this.recreates.constraints += 1; + } + return res; } - - const res: AlterOperation[] = [ - ...constraints.map(oldObject => { - const opRes: AlterOperation = { - operationType: 'dropConstraint', - oldObject, - }; - return opRes; - }), - op, - ..._.reverse([...constraints]).map(newObject => { - const opRes: AlterOperation = { - operationType: 'createConstraint', - newObject, - }; - return opRes; - }), - ]; - - if (constraints.length > 0) { - this.recreates.constraints += 1; - } - return res; } if (op.operationType == 'dropTable') { @@ -374,7 +397,8 @@ export class AlterPlan { this._testTableRecreate(op, 'dropColumn', this.dialect.dropColumn, 'oldObject') || this._testTableRecreate(op, 'createConstraint', obj => this._canCreateConstraint(obj), 'newObject') || this._testTableRecreate(op, 'dropConstraint', obj => this._canDropConstraint(obj), 'oldObject') || - this._testTableRecreate(op, 'changeColumn', this.dialect.changeColumn, 'newObject') || [op] + this._testTableRecreate(op, 'changeColumn', this.dialect.changeColumn, 'newObject') || + this._testTableRecreate(op, 'renameColumn', true, 'object') || [op] ); }); @@ -383,6 +407,7 @@ export class AlterPlan { _canCreateConstraint(cnt: ConstraintInfo) { if (cnt.constraintType == 'primaryKey') return this.dialect.createPrimaryKey; + if (cnt.constraintType == 'sortingKey') return this.dialect.createPrimaryKey; if (cnt.constraintType == 'foreignKey') return this.dialect.createForeignKey; if (cnt.constraintType == 'index') return this.dialect.createIndex; if (cnt.constraintType == 'unique') return this.dialect.createUnique; @@ -392,6 +417,7 @@ export class AlterPlan { _canDropConstraint(cnt: ConstraintInfo) { if (cnt.constraintType == 'primaryKey') return this.dialect.dropPrimaryKey; + if (cnt.constraintType == 'sortingKey') return this.dialect.dropPrimaryKey; if (cnt.constraintType == 'foreignKey') return this.dialect.dropForeignKey; if (cnt.constraintType == 'index') return this.dialect.dropIndex; if (cnt.constraintType == 'unique') return this.dialect.dropUnique; @@ -453,7 +479,7 @@ export class AlterPlan { } } else { // @ts-ignore - const oldObject: TableInfo = op.oldObject; + const oldObject: TableInfo = op.oldObject || op.object; if (oldObject) { const recreated = recreates[`${oldObject.schemaName}||${oldObject.pureName}`]; if (recreated) { @@ -575,6 +601,9 @@ export function runAlterOperation(op: AlterOperation, processor: AlterProcessor) case 'dropSqlObject': processor.dropSqlObject(op.oldObject); break; + case 'setTableOption': + processor.setTableOption(op.table, op.optionName, op.optionValue); + break; case 'fillPreloadedRows': processor.fillPreloadedRows(op.table, op.oldRows, op.newRows, op.key, op.insertOnly, op.autoIncrementColumn); break; diff --git a/packages/tools/src/createBulkInsertStreamBase.ts b/packages/tools/src/createBulkInsertStreamBase.ts index eb16d9ffc..dcf93e40b 100644 --- a/packages/tools/src/createBulkInsertStreamBase.ts +++ b/packages/tools/src/createBulkInsertStreamBase.ts @@ -37,7 +37,8 @@ export function createBulkInsertStreamBase(driver: EngineDriver, stream, pool, n } if (options.createIfNotExists && (!structure || options.dropIfExists)) { const dmp = driver.createDumper(); - dmp.createTable(prepareTableForImport({ ...writable.structure, ...name })); + const createdTableInfo = driver.adaptTableInfo(prepareTableForImport({ ...writable.structure, ...name })); + dmp.createTable(createdTableInfo); logger.info({ sql: dmp.s }, `Creating table ${fullNameQuoted}`); await driver.script(pool, dmp.s); structure = await driver.analyseSingleTable(pool, name); diff --git a/packages/tools/src/database-info-alter-processor.ts b/packages/tools/src/database-info-alter-processor.ts index 2b8c2b23f..90031d171 100644 --- a/packages/tools/src/database-info-alter-processor.ts +++ b/packages/tools/src/database-info-alter-processor.ts @@ -11,6 +11,7 @@ import { UniqueInfo, SqlObjectInfo, NamedObjectInfo, + ColumnsConstraintInfo, } from '../../types'; export class DatabaseInfoAlterProcessor { @@ -59,6 +60,9 @@ export class DatabaseInfoAlterProcessor { case 'primaryKey': table.primaryKey = constraint as PrimaryKeyInfo; break; + case 'sortingKey': + table.sortingKey = constraint as ColumnsConstraintInfo; + break; case 'foreignKey': table.foreignKeys.push(constraint as ForeignKeyInfo); break; @@ -86,6 +90,9 @@ export class DatabaseInfoAlterProcessor { case 'primaryKey': table.primaryKey = null; break; + case 'sortingKey': + table.sortingKey = null; + break; case 'foreignKey': table.foreignKeys = table.foreignKeys.filter(x => x.constraintName != constraint.constraintName); break; @@ -129,4 +136,9 @@ export class DatabaseInfoAlterProcessor { tableInfo.preloadedRowsKey = key; tableInfo.preloadedRowsInsertOnly = insertOnly; } + + setTableOption(table: TableInfo, optionName: string, optionValue: string) { + const tableInfo = this.db.tables.find(x => x.pureName == table.pureName && x.schemaName == table.schemaName); + tableInfo[optionName] = optionValue; + } } diff --git a/packages/tools/src/diffTools.ts b/packages/tools/src/diffTools.ts index 6f939cb89..f3d85d466 100644 --- a/packages/tools/src/diffTools.ts +++ b/packages/tools/src/diffTools.ts @@ -46,6 +46,14 @@ export function generateTablePairingId(table: TableInfo): TableInfo { if (!table.pairingId) { return { ...table, + primaryKey: table.primaryKey && { + ...table.primaryKey, + pairingId: table.primaryKey.pairingId || uuidv1(), + }, + sortingKey: table.sortingKey && { + ...table.sortingKey, + pairingId: table.sortingKey.pairingId || uuidv1(), + }, columns: table.columns?.map(col => ({ ...col, pairingId: col.pairingId || uuidv1(), @@ -335,6 +343,7 @@ export function testEqualTypes(a: ColumnInfo, b: ColumnInfo, opts: DbDiffOptions function getTableConstraints(table: TableInfo) { const res = []; if (table.primaryKey) res.push(table.primaryKey); + if (table.sortingKey) res.push(table.sortingKey); if (table.foreignKeys) res.push(...table.foreignKeys); if (table.indexes) res.push(...table.indexes); if (table.uniques) res.push(...table.uniques); @@ -345,7 +354,9 @@ function getTableConstraints(table: TableInfo) { function createPairs(oldList, newList, additionalCondition = null) { const res = []; for (const a of oldList) { - const b = newList.find(x => x.pairingId == a.pairingId || (additionalCondition && additionalCondition(a, x))); + const b = newList.find( + x => (a.pairingId && x.pairingId == a.pairingId) || (additionalCondition && additionalCondition(a, x)) + ); if (b) { res.push([a, b]); } else { @@ -381,9 +392,14 @@ function planAlterTable(plan: AlterPlan, oldTable: TableInfo, newTable: TableInf const constraintPairs = createPairs( getTableConstraints(oldTable), getTableConstraints(newTable), - (a, b) => a.constraintType == 'primaryKey' && b.constraintType == 'primaryKey' + (a, b) => + (a.constraintType == 'primaryKey' && b.constraintType == 'primaryKey') || + (a.constraintType == 'sortingKey' && b.constraintType == 'sortingKey') ); - // console.log('constraintPairs SOURCE', getTableConstraints(oldTable), getTableConstraints(newTable)); + // console.log('constraintPairs OLD TABLE', oldTable); + // console.log('constraintPairs NEW TABLE', newTable); + // console.log('constraintPairs SOURCE OLD', getTableConstraints(oldTable)); + // console.log('constraintPairs SOURCE NEW', getTableConstraints(newTable)); // console.log('constraintPairs', constraintPairs); if (!opts.noDropConstraint) { @@ -407,7 +423,7 @@ function planAlterTable(plan: AlterPlan, oldTable: TableInfo, newTable: TableInf // console.log('PLAN RENAME COLUMN') plan.renameColumn(x[0], x[1].columnName); } else { - // console.log('PLAN CHANGE COLUMN') + // console.log('PLAN CHANGE COLUMN', x[0], x[1]); plan.changeColumn(x[0], x[1]); } } @@ -425,6 +441,28 @@ function planAlterTable(plan: AlterPlan, oldTable: TableInfo, newTable: TableInf constraintPairs.filter(x => x[0] == null).forEach(x => plan.createConstraint(x[1])); planTablePreload(plan, oldTable, newTable); + + planChangeTableOptions(plan, oldTable, newTable, opts); + + // console.log('oldTable', oldTable); + // console.log('newTable', newTable); + // console.log('plan.operations', plan.operations); +} + +function planChangeTableOptions(plan: AlterPlan, oldTable: TableInfo, newTable: TableInfo, opts: DbDiffOptions) { + for (const option of plan.dialect?.getTableFormOptions?.('sqlAlterTable') || []) { + if (option.disabled) { + continue; + } + const name = option.name; + if ( + oldTable[name] != newTable[name] && + (oldTable[name] || newTable[name]) && + (newTable[name] || option.allowEmptyValue) + ) { + plan.setTableOption(newTable, name, newTable[name]); + } + } } export function testEqualTables( diff --git a/packages/tools/src/driverBase.ts b/packages/tools/src/driverBase.ts index 307146c44..ebcf03cd5 100644 --- a/packages/tools/src/driverBase.ts +++ b/packages/tools/src/driverBase.ts @@ -66,20 +66,20 @@ export const driverBase = { return new this.dumperClass(this, options); }, async script(pool, sql, options: RunScriptOptions) { - if (options?.useTransaction) { + if (options?.useTransaction && this.supportsTransactions) { runCommandOnDriver(pool, this, dmp => dmp.beginTransaction()); } for (const sqlItem of splitQuery(sql, this.getQuerySplitterOptions('script'))) { try { await this.query(pool, sqlItem, { discardResult: true }); } catch (err) { - if (options?.useTransaction) { + if (options?.useTransaction && this.supportsTransactions) { runCommandOnDriver(pool, this, dmp => dmp.rollbackTransaction()); } throw err; } } - if (options?.useTransaction) { + if (options?.useTransaction && this.supportsTransactions) { runCommandOnDriver(pool, this, dmp => dmp.commitTransaction()); } }, @@ -173,4 +173,12 @@ export const driverBase = { parseSqlNull: true, parseHexAsBuffer: true, }, + + createSaveChangeSetScript(changeSet, dbinfo, defaultCreator) { + return defaultCreator(changeSet, dbinfo); + }, + + adaptTableInfo(table) { + return table; + } }; diff --git a/packages/tools/src/schemaEditorTools.ts b/packages/tools/src/schemaEditorTools.ts index 18e095198..db7519fa2 100644 --- a/packages/tools/src/schemaEditorTools.ts +++ b/packages/tools/src/schemaEditorTools.ts @@ -2,6 +2,7 @@ import uuidv1 from 'uuid/v1'; import _omit from 'lodash/omit'; import type { ColumnInfo, + ColumnsConstraintInfo, ConstraintInfo, ForeignKeyInfo, IndexInfo, @@ -195,6 +196,13 @@ export function editorAddConstraint(table: TableInfo, constraint: ConstraintInfo } as PrimaryKeyInfo; } + if (constraint.constraintType == 'sortingKey') { + res.sortingKey = { + pairingId: uuidv1(), + ...constraint, + } as ColumnsConstraintInfo; + } + if (constraint.constraintType == 'foreignKey') { res.foreignKeys = [ ...(res.foreignKeys || []), @@ -240,6 +248,13 @@ export function editorModifyConstraint(table: TableInfo, constraint: ConstraintI }; } + if (constraint.constraintType == 'sortingKey') { + res.sortingKey = { + ...res.sortingKey, + ...constraint, + }; + } + if (constraint.constraintType == 'foreignKey') { res.foreignKeys = table.foreignKeys.map(fk => fk.pairingId == constraint.pairingId ? { ...fk, ...constraint } : fk @@ -266,6 +281,10 @@ export function editorDeleteConstraint(table: TableInfo, constraint: ConstraintI res.primaryKey = null; } + if (constraint.constraintType == 'sortingKey') { + res.sortingKey = null; + } + if (constraint.constraintType == 'foreignKey') { res.foreignKeys = table.foreignKeys.filter(x => x.pairingId != constraint.pairingId); } diff --git a/packages/tools/src/structureTools.ts b/packages/tools/src/structureTools.ts index 6583d0cdd..0ef7b9aca 100644 --- a/packages/tools/src/structureTools.ts +++ b/packages/tools/src/structureTools.ts @@ -5,7 +5,7 @@ export function addTableDependencies(db: DatabaseInfo): DatabaseInfo { if (!db.tables) { return db; } - + const allForeignKeys = _flatten(db.tables.map(x => x.foreignKeys || [])); return { ...db, @@ -33,6 +33,14 @@ export function extendTableInfo(table: TableInfo): TableInfo { constraintType: 'primaryKey', } : undefined, + sortingKey: table.sortingKey + ? { + ...table.sortingKey, + pureName: table.pureName, + schemaName: table.schemaName, + constraintType: 'sortingKey', + } + : undefined, foreignKeys: (table.foreignKeys || []).map(cnt => ({ ...cnt, pureName: table.pureName, diff --git a/packages/tools/src/tableTransforms.ts b/packages/tools/src/tableTransforms.ts index d53c38570..6babccc32 100644 --- a/packages/tools/src/tableTransforms.ts +++ b/packages/tools/src/tableTransforms.ts @@ -10,6 +10,7 @@ export function prepareTableForImport(table: TableInfo): TableInfo { res.uniques = []; res.checks = []; if (res.primaryKey) res.primaryKey.constraintName = null; + res.tableEngine = null; return res; } diff --git a/packages/tools/src/yamlModelConv.ts b/packages/tools/src/yamlModelConv.ts index 3cda81c5b..4a2c8ee0e 100644 --- a/packages/tools/src/yamlModelConv.ts +++ b/packages/tools/src/yamlModelConv.ts @@ -27,6 +27,7 @@ export interface TableInfoYaml { // schema?: string; columns: ColumnInfoYaml[]; primaryKey?: string[]; + sortingKey?: string[]; insertKey?: string[]; insertOnly?: string[]; @@ -91,6 +92,9 @@ export function tableInfoToYaml(table: TableInfo): TableInfoYaml { if (tableCopy.primaryKey && !tableCopy.primaryKey['_dumped']) { res.primaryKey = tableCopy.primaryKey.columns.map(x => x.columnName); } + if (tableCopy.sortingKey && !tableCopy.sortingKey['_dumped']) { + res.sortingKey = tableCopy.sortingKey.columns.map(x => x.columnName); + } // const foreignKeys = (tableCopy.foreignKeys || []).filter(x => !x['_dumped']).map(foreignKeyInfoToYaml); return res; } @@ -132,6 +136,13 @@ export function tableInfoFromYaml(table: TableInfoYaml, allTables: TableInfoYaml columns: table.primaryKey.map(columnName => ({ columnName })), }; } + if (table.sortingKey) { + res.sortingKey = { + pureName: table.name, + constraintType: 'sortingKey', + columns: table.sortingKey.map(columnName => ({ columnName })), + }; + } res.preloadedRows = table.data; res.preloadedRowsKey = table.insertKey; res.preloadedRowsInsertOnly = table.insertOnly; diff --git a/packages/types/alter-processor.d.ts b/packages/types/alter-processor.d.ts index e2d1d6d71..fae6d4fe9 100644 --- a/packages/types/alter-processor.d.ts +++ b/packages/types/alter-processor.d.ts @@ -15,6 +15,7 @@ export interface AlterProcessor { recreateTable(oldTable: TableInfo, newTable: TableInfo); createSqlObject(obj: SqlObjectInfo); dropSqlObject(obj: SqlObjectInfo); + setTableOption(table: TableInfo, optionName: string, optionValue: string); fillPreloadedRows( table: NamedObjectInfo, oldRows: any[], diff --git a/packages/types/dbinfo.d.ts b/packages/types/dbinfo.d.ts index 73b4951f6..038ba345e 100644 --- a/packages/types/dbinfo.d.ts +++ b/packages/types/dbinfo.d.ts @@ -15,7 +15,7 @@ export interface ColumnReference { export interface ConstraintInfo extends NamedObjectInfo { pairingId?: string; constraintName?: string; - constraintType: 'primaryKey' | 'foreignKey' | 'index' | 'check' | 'unique'; + constraintType: 'primaryKey' | 'foreignKey' | 'sortingKey' | 'index' | 'check' | 'unique'; } export interface ColumnsConstraintInfo extends ConstraintInfo { @@ -49,6 +49,7 @@ export interface ColumnInfo extends NamedObjectInfo { notNull?: boolean; autoIncrement?: boolean; dataType: string; + displayedDataType?: string; precision?: number; scale?: number; length?: number; @@ -61,7 +62,7 @@ export interface ColumnInfo extends NamedObjectInfo { isUnsigned?: boolean; isZerofill?: boolean; options?: []; - canSelectMultipleOptions?: boolean, + canSelectMultipleOptions?: boolean; } export interface DatabaseObjectInfo extends NamedObjectInfo { @@ -82,6 +83,7 @@ export interface SqlObjectInfo extends DatabaseObjectInfo { export interface TableInfo extends DatabaseObjectInfo { columns: ColumnInfo[]; primaryKey?: PrimaryKeyInfo; + sortingKey?: ColumnsConstraintInfo; foreignKeys: ForeignKeyInfo[]; dependencies?: ForeignKeyInfo[]; indexes?: IndexInfo[]; @@ -91,6 +93,7 @@ export interface TableInfo extends DatabaseObjectInfo { preloadedRowsKey?: string[]; preloadedRowsInsertOnly?: string[]; tableRowCount?: number | string; + tableEngine?: string; __isDynamicStructure?: boolean; } @@ -102,10 +105,10 @@ export interface CollectionInfo extends DatabaseObjectInfo { uniqueKey?: ColumnReference[]; // partition key columns - partitionKey?: ColumnReference[] + partitionKey?: ColumnReference[]; // unique key inside partition - clusterKey?: ColumnReference[]; + clusterKey?: ColumnReference[]; } export interface ViewInfo extends SqlObjectInfo { diff --git a/packages/types/dialect.d.ts b/packages/types/dialect.d.ts index ffff7f0c2..94705d7cc 100644 --- a/packages/types/dialect.d.ts +++ b/packages/types/dialect.d.ts @@ -17,6 +17,7 @@ export interface SqlDialect { dropColumnDependencies?: string[]; changeColumnDependencies?: string[]; + renameColumnDependencies?: string[]; dropIndexContainsTableSpec?: boolean; @@ -34,6 +35,15 @@ export interface SqlDialect { createCheck?: boolean; dropCheck?: boolean; + specificNullabilityImplementation?: boolean; + omitForeignKeys?: boolean; + omitUniqueConstraints?: boolean; + omitIndexes?: boolean; + sortingKeys?: boolean; + + // syntax for create column: ALTER TABLE table ADD COLUMN column + createColumnWithColumnKeyword?: boolean; + dropReferencesWhenDropTable?: boolean; requireFromDual?: boolean; @@ -41,4 +51,11 @@ export interface SqlDialect { // create sql-tree expression createColumnViewExpression(columnName: string, dataType: string, source: { alias: string }, alias?: string): any; + + getTableFormOptions(intent: 'newTableForm' | 'editTableForm' | 'sqlCreateTable' | 'sqlAlterTable'): { + name: string; + sqlFormatString: string; + disabled?: boolean; + allowEmptyValue?: boolean; + }[]; } diff --git a/packages/types/engines.d.ts b/packages/types/engines.d.ts index 51cfac77d..1ee4b71a6 100644 --- a/packages/types/engines.d.ts +++ b/packages/types/engines.d.ts @@ -148,6 +148,7 @@ export interface EngineDriver extends FilterBehaviourProvider { profilerChartAggregateFunction?: string; profilerChartMeasures?: { label: string; field: string }[]; isElectronOnly?: boolean; + supportsTransactions?: boolean; collectionSingularLabel?: string; collectionPluralLabel?: string; @@ -222,6 +223,13 @@ export interface EngineDriver extends FilterBehaviourProvider { getCollectionExportQueryJson(collection: string, condition: any, sort?: CollectionSortDefinition): {}; getScriptTemplates(objectTypeField: keyof DatabaseInfo): { label: string; scriptTemplate: string }[]; getScriptTemplateContent(scriptTemplate: string, props: any): Promise; + createSaveChangeSetScript( + changeSet: any, + dbinfo: DatabaseInfo, + defaultCreator: (changeSet: any, dbinfo: DatabaseInfo) => any + ): any[]; + // adapts table info from different source (import, other database) to be suitable for this database + adaptTableInfo(table: TableInfo): TableInfo; analyserClass?: any; dumperClass?: any; diff --git a/packages/web/src/appobj/ColumnAppObject.svelte b/packages/web/src/appobj/ColumnAppObject.svelte index 8ee9f79db..37aa9b6f9 100644 --- a/packages/web/src/appobj/ColumnAppObject.svelte +++ b/packages/web/src/appobj/ColumnAppObject.svelte @@ -31,7 +31,7 @@ return [ { text: 'Rename column', onClick: handleRenameColumn }, { text: 'Drop column', onClick: handleDropColumn }, - { text: 'Copy name', onClick: () => navigator.clipboard.writeText(data.columnName)}, + { text: 'Copy name', onClick: () => navigator.clipboard.writeText(data.columnName) }, ]; } diff --git a/packages/web/src/appobj/DatabaseObjectAppObject.svelte b/packages/web/src/appobj/DatabaseObjectAppObject.svelte index e12c54069..4dbd0d081 100644 --- a/packages/web/src/appobj/DatabaseObjectAppObject.svelte +++ b/packages/web/src/appobj/DatabaseObjectAppObject.svelte @@ -860,6 +860,18 @@ return createDatabaseObjectMenu(data, passProps?.connection); } + function getExtInfo(data) { + const res = []; + if (data.tableRowCount != null) { + res.push(`${formatRowCount(data.tableRowCount)} rows`); + } + if (data.tableEngine) { + res.push(data.tableEngine); + } + if (res.length > 0) return res.join(', '); + return null; + } + $: isPinned = !!$pinnedTables.find(x => testEqual(data, x)); @@ -873,7 +885,7 @@ showPinnedInsteadOfUnpin={passProps?.showPinnedInsteadOfUnpin} onPin={isPinned ? null : () => pinnedTables.update(list => [...list, data])} onUnpin={isPinned ? () => pinnedTables.update(list => list.filter(x => !testEqual(x, data))) : null} - extInfo={data.tableRowCount != null ? `${formatRowCount(data.tableRowCount)} rows` : null} + extInfo={getExtInfo(data)} on:click={() => handleClick()} on:middleclick={() => handleClick(true)} on:expand diff --git a/packages/web/src/datagrid/ColumnHeaderControl.svelte b/packages/web/src/datagrid/ColumnHeaderControl.svelte index 9e367a0fa..6083c680d 100644 --- a/packages/web/src/datagrid/ColumnHeaderControl.svelte +++ b/packages/web/src/datagrid/ColumnHeaderControl.svelte @@ -88,9 +88,9 @@ {/if} - {#if _.isString(column.dataType) && !order} + {#if _.isString(column.displayedDataType || column.dataType) && !order} - {column.dataType.toLowerCase()} + {(column.displayedDataType || column.dataType).toLowerCase()} {/if} diff --git a/packages/web/src/designer/ColumnLine.svelte b/packages/web/src/designer/ColumnLine.svelte index 7128ff89c..26065169d 100644 --- a/packages/web/src/designer/ColumnLine.svelte +++ b/packages/web/src/designer/ColumnLine.svelte @@ -196,7 +196,7 @@
{#if designer?.style?.showDataType && column?.dataType}
- {column?.dataType.toLowerCase()} + {(column?.displayedDataType || column?.dataType).toLowerCase()}
{/if} {#if designer?.style?.showNullability} diff --git a/packages/web/src/elements/ColumnLabel.svelte b/packages/web/src/elements/ColumnLabel.svelte index ffb77ba86..14d74c972 100644 --- a/packages/web/src/elements/ColumnLabel.svelte +++ b/packages/web/src/elements/ColumnLabel.svelte @@ -19,6 +19,7 @@ export let columnName = ''; export let extInfo = null; export let dataType = null; + export let displayedDataType = null; export let showDataType = false; export let foreignKey; export let conid = undefined; @@ -59,7 +60,7 @@ {/if} {:else if dataType} - {dataType.toLowerCase()} + {(displayedDataType || dataType).toLowerCase()} {/if} {/if} diff --git a/packages/web/src/elements/ForeignKeyObjectListControl.svelte b/packages/web/src/elements/ForeignKeyObjectListControl.svelte index b56fb3eea..da4417c82 100644 --- a/packages/web/src/elements/ForeignKeyObjectListControl.svelte +++ b/packages/web/src/elements/ForeignKeyObjectListControl.svelte @@ -8,7 +8,7 @@ export let collection; export let title; - export let clickable; + export let clickable = false; export let onRemove = null; export let onAddNew = null; export let emptyMessage = null; diff --git a/packages/web/src/elements/ObjectFieldsEditor.svelte b/packages/web/src/elements/ObjectFieldsEditor.svelte new file mode 100644 index 000000000..caa2fbf63 --- /dev/null +++ b/packages/web/src/elements/ObjectFieldsEditor.svelte @@ -0,0 +1,68 @@ + + +
+
+ { + collapsed = !collapsed; + }} + > + + + {title} +
+ {#if !collapsed} + + + + {/if} +
+ + diff --git a/packages/web/src/elements/ObjectListControl.svelte b/packages/web/src/elements/ObjectListControl.svelte index fe8022dce..15ddaaf2a 100644 --- a/packages/web/src/elements/ObjectListControl.svelte +++ b/packages/web/src/elements/ObjectListControl.svelte @@ -10,13 +10,23 @@ export let showIfEmpty = false; export let emptyMessage = null; export let hideDisplayName = false; - export let clickable; - export let onAddNew; + export let clickable = false; + export let onAddNew = null; + + let collapsed = false; {#if collection?.length > 0 || showIfEmpty || emptyMessage}
+ { + collapsed = !collapsed; + }} + > + + {title} {#if onAddNew} Add new @@ -27,7 +37,7 @@ {emptyMessage}
{/if} - {#if collection?.length > 0 || showIfEmpty} + {#if !collapsed && (collection?.length > 0 || showIfEmpty)}
.wrapper { margin-bottom: 20px; + user-select: none; } .header { @@ -93,4 +104,13 @@ .body { margin: 20px; } + + .collapse { + cursor: pointer; + } + + .collapse:hover { + color: var(--theme-font-hover); + background: var(--theme-bg-3); + } diff --git a/packages/web/src/forms/FormArgument.svelte b/packages/web/src/forms/FormArgument.svelte index ea1bd923d..a40fcfced 100644 --- a/packages/web/src/forms/FormArgument.svelte +++ b/packages/web/src/forms/FormArgument.svelte @@ -5,11 +5,15 @@ import FormSelectField from './FormSelectField.svelte'; import FormTextField from './FormTextField.svelte'; import FormStringList from './FormStringList.svelte'; + import FormDropDownTextField from './FormDropDownTextField.svelte'; + import { getFormContext } from './FormProviderCore.svelte'; export let arg; export let namePrefix; $: name = `${namePrefix}${arg.name}`; + + const { setFieldValue } = getFormContext(); {#if arg.type == 'text'} @@ -19,14 +23,10 @@ defaultValue={arg.default} focused={arg.focused} placeholder={arg.placeholder} + disabled={arg.disabled} /> {:else if arg.type == 'stringlist'} - + {:else if arg.type == 'number'} +{:else if arg.type == 'dropdowntext'} + { + return arg.options.map(opt => ({ + text: _.isString(opt) ? opt : opt.name, + onClick: () => setFieldValue(name, _.isString(opt) ? opt : opt.value), + })); + }} + /> {/if} diff --git a/packages/web/src/tableeditor/ColumnEditorModal.svelte b/packages/web/src/tableeditor/ColumnEditorModal.svelte index 3411f40ca..00060325b 100644 --- a/packages/web/src/tableeditor/ColumnEditorModal.svelte +++ b/packages/web/src/tableeditor/ColumnEditorModal.svelte @@ -32,7 +32,9 @@ - + {#if !driver?.dialect?.specificNullabilityImplementation} + + {/if}
-
-
{constraintNameLabel}
-
- (constraintName = e.target['value'])} - focused - disabled={isReadOnly} - /> + {#if !hideConstraintName} +
+
{constraintNameLabel}
+
+ (constraintName = e.target['value'])} + focused + disabled={isReadOnly} + /> +
-
+ {/if} {#if $$slots.constraintProps} diff --git a/packages/web/src/tableeditor/PrimaryKeyEditorModal.svelte b/packages/web/src/tableeditor/PrimaryKeyEditorModal.svelte index fc86db830..4a2fb5787 100644 --- a/packages/web/src/tableeditor/PrimaryKeyEditorModal.svelte +++ b/packages/web/src/tableeditor/PrimaryKeyEditorModal.svelte @@ -4,14 +4,18 @@ export let constraintInfo; export let setTableInfo; export let tableInfo; + export let driver; + export let constraintLabel = 'primary key'; + export let constraintType = 'primaryKey'; diff --git a/packages/web/src/tableeditor/PrimaryKeyLikeListControl.svelte b/packages/web/src/tableeditor/PrimaryKeyLikeListControl.svelte new file mode 100644 index 000000000..0608f3af4 --- /dev/null +++ b/packages/web/src/tableeditor/PrimaryKeyLikeListControl.svelte @@ -0,0 +1,76 @@ + + + 0 ? addKeyConstraint : null} + hideDisplayName={driver?.dialect?.anonymousPrimaryKey} + clickable + on:clickrow={e => + showModal(PrimaryKeyEditorModal, { + constraintInfo: e.detail, + tableInfo, + setTableInfo, + constraintLabel, + constraintType, + driver, + })} + columns={[ + { + fieldName: 'columns', + header: 'Columns', + slot: 0, + }, + isWritable + ? { + fieldName: 'actions', + sortable: true, + slot: 1, + } + : null, + ]} +> + + {row?.columns.map(x => x.columnName).join(', ')} + { + e.stopPropagation(); + setTableInfo(tbl => editorDeleteConstraint(tbl, row)); + }}>Remove + diff --git a/packages/web/src/tableeditor/TableEditor.svelte b/packages/web/src/tableeditor/TableEditor.svelte index bb469e6db..2e65924f5 100644 --- a/packages/web/src/tableeditor/TableEditor.svelte +++ b/packages/web/src/tableeditor/TableEditor.svelte @@ -30,7 +30,7 @@ icon: 'icon add-key', toolbar: true, isRelatedToTab: true, - testEnabled: () => getCurrentEditor()?.getIsWritable(), + testEnabled: () => getCurrentEditor()?.getIsWritable() && !getCurrentEditor()?.getDialect()?.omitForeignKeys, onClick: () => getCurrentEditor().addForeignKey(), }); @@ -41,7 +41,7 @@ icon: 'icon add-key', toolbar: true, isRelatedToTab: true, - testEnabled: () => getCurrentEditor()?.getIsWritable(), + testEnabled: () => getCurrentEditor()?.getIsWritable() && !getCurrentEditor()?.getDialect()?.omitIndexes, onClick: () => getCurrentEditor().addIndex(), }); @@ -52,7 +52,7 @@ icon: 'icon add-key', toolbar: true, isRelatedToTab: true, - testEnabled: () => getCurrentEditor()?.getIsWritable(), + testEnabled: () => getCurrentEditor()?.getIsWritable() && !getCurrentEditor()?.getDialect()?.omitUniqueConstraints, onClick: () => getCurrentEditor().addUnique(), }); @@ -81,6 +81,8 @@ import IndexEditorModal from './IndexEditorModal.svelte'; import PrimaryKeyEditorModal from './PrimaryKeyEditorModal.svelte'; import UniqueEditorModal from './UniqueEditorModal.svelte'; + import ObjectFieldsEditor from '../elements/ObjectFieldsEditor.svelte'; + import PrimaryKeyLikeListControl from './PrimaryKeyLikeListControl.svelte'; export const activator = createActivator('TableEditor', true); @@ -88,6 +90,7 @@ export let setTableInfo; export let dbInfo; export let driver; + export let resetCounter; $: isWritable = !!setTableInfo; @@ -95,6 +98,10 @@ return isWritable; } + export function getDialect() { + return driver?.dialect; + } + export function addColumn() { showModal(ColumnEditorModal, { setTableInfo, @@ -115,6 +122,7 @@ showModal(PrimaryKeyEditorModal, { setTableInfo, tableInfo, + driver, }); } @@ -143,7 +151,6 @@ } $: columns = tableInfo?.columns; - $: primaryKey = tableInfo?.primaryKey; $: foreignKeys = tableInfo?.foreignKeys; $: dependencies = tableInfo?.dependencies; $: indexes = tableInfo?.indexes; @@ -153,9 +160,29 @@ tableInfo; invalidateCommands(); } + + $: tableFormOptions = driver?.dialect?.getTableFormOptions?.(tableInfo?.objectId ? 'editTableForm' : 'newTableForm');
+ {#if tableFormOptions} + {#key resetCounter} + x.name) + )} + onChangeValues={vals => { + if (!_.isEmpty(vals)) { + setTableInfo(tbl => ({ ...tbl, ...vals })); + } + }} + /> + {/key} + {/if} + ({ ...x, ordinal: index + 1 }))} title={`Columns (${columns?.length || 0})`} @@ -164,7 +191,7 @@ on:clickrow={e => showModal(ColumnEditorModal, { columnInfo: e.detail, tableInfo, setTableInfo, driver })} onAddNew={isWritable ? addColumn : null} columns={[ - { + !driver?.dialect?.specificNullabilityImplementation && { fieldName: 'notNull', header: 'Nullability', sortable: true, @@ -239,124 +266,109 @@ - 0 ? addPrimaryKey : null} - clickable - on:clickrow={e => showModal(PrimaryKeyEditorModal, { constraintInfo: e.detail, tableInfo, setTableInfo })} - columns={[ - { - fieldName: 'columns', - header: 'Columns', - slot: 0, - }, - isWritable - ? { - fieldName: 'actions', - sortable: true, - slot: 1, - } - : null, - ]} - > - - {row?.columns.map(x => x.columnName).join(', ')} - { - e.stopPropagation(); - setTableInfo(tbl => editorDeleteConstraint(tbl, row)); - }}>Remove - + - 0 ? addIndex : null} - title={`Indexes (${indexes?.length || 0})`} - emptyMessage={isWritable ? 'No index defined' : null} - clickable - on:clickrow={e => showModal(IndexEditorModal, { constraintInfo: e.detail, tableInfo, setTableInfo })} - columns={[ - { - fieldName: 'columns', - header: 'Columns', - slot: 0, - }, - { - fieldName: 'unique', - header: 'Unique', - slot: 1, - }, - isWritable - ? { - fieldName: 'actions', - sortable: true, - slot: 2, - } - : null, - ]} - > - - {row?.columns.map(x => x.columnName).join(', ')} - {row?.isUnique ? 'YES' : 'NO'} - { - e.stopPropagation(); - setTableInfo(tbl => editorDeleteConstraint(tbl, row)); - }}>Remove - + {#if driver?.dialect?.sortingKeys} + + {/if} - 0 ? addUnique : null} - title={`Unique constraints (${uniques?.length || 0})`} - emptyMessage={isWritable ? 'No unique defined' : null} - clickable - on:clickrow={e => showModal(UniqueEditorModal, { constraintInfo: e.detail, tableInfo, setTableInfo })} - columns={[ - { - fieldName: 'columns', - header: 'Columns', - slot: 0, - }, - isWritable - ? { - fieldName: 'actions', - sortable: true, - slot: 1, - } - : null, - ]} - > - - {row?.columns.map(x => x.columnName).join(', ')} - { - e.stopPropagation(); - setTableInfo(tbl => editorDeleteConstraint(tbl, row)); - }}>Remove 0 ? addIndex : null} + title={`Indexes (${indexes?.length || 0})`} + emptyMessage={isWritable ? 'No index defined' : null} + clickable + on:clickrow={e => showModal(IndexEditorModal, { constraintInfo: e.detail, tableInfo, setTableInfo })} + columns={[ + { + fieldName: 'columns', + header: 'Columns', + slot: 0, + }, + { + fieldName: 'unique', + header: 'Unique', + slot: 1, + }, + isWritable + ? { + fieldName: 'actions', + sortable: true, + slot: 2, + } + : null, + ]} > - + + {row?.columns.map(x => x.columnName).join(', ')} + {row?.isUnique ? 'YES' : 'NO'} + { + e.stopPropagation(); + setTableInfo(tbl => editorDeleteConstraint(tbl, row)); + }}>Remove + + {/if} - 0 ? addForeignKey : null} - title={`Foreign keys (${foreignKeys?.length || 0})`} - emptyMessage={isWritable ? 'No foreign key defined' : null} - clickable - onRemove={row => setTableInfo(tbl => editorDeleteConstraint(tbl, row))} - on:clickrow={e => showModal(ForeignKeyEditorModal, { constraintInfo: e.detail, tableInfo, setTableInfo, dbInfo })} - /> - + {#if !driver?.dialect?.omitUniqueConstraints} + 0 ? addUnique : null} + title={`Unique constraints (${uniques?.length || 0})`} + emptyMessage={isWritable ? 'No unique defined' : null} + clickable + on:clickrow={e => showModal(UniqueEditorModal, { constraintInfo: e.detail, tableInfo, setTableInfo })} + columns={[ + { + fieldName: 'columns', + header: 'Columns', + slot: 0, + }, + isWritable + ? { + fieldName: 'actions', + sortable: true, + slot: 1, + } + : null, + ]} + > + + {row?.columns.map(x => x.columnName).join(', ')} + { + e.stopPropagation(); + setTableInfo(tbl => editorDeleteConstraint(tbl, row)); + }}>Remove + + {/if} + + {#if !driver?.dialect?.omitForeignKeys} + 0 ? addForeignKey : null} + title={`Foreign keys (${foreignKeys?.length || 0})`} + emptyMessage={isWritable ? 'No foreign key defined' : null} + clickable + onRemove={row => setTableInfo(tbl => editorDeleteConstraint(tbl, row))} + on:clickrow={e => showModal(ForeignKeyEditorModal, { constraintInfo: e.detail, tableInfo, setTableInfo, dbInfo })} + /> + + {/if}