Compare commits
116 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3895c6bb47 | |||
| 2e03056a15 | |||
| eaa5970a0f | |||
| e79e19c614 | |||
| 0ef5ac04d8 | |||
| 2cb3a6b446 | |||
| d75397d793 | |||
| 5b58ed9c26 | |||
| f4c39bbf3c | |||
| e2ce349a30 | |||
| 04a6540890 | |||
| b3b7d021c5 | |||
| b396c8f820 | |||
| 8cc8f9f0b9 | |||
| 3bbe06a55b | |||
| dfe37496f2 | |||
| 3fe13f0443 | |||
| a5b5f36298 | |||
| b9e2e51bd7 | |||
| 10e63f3e77 | |||
| 820044b489 | |||
| 89c904abc1 | |||
| c5a3ee01ee | |||
| a5cc99005a | |||
| 77ebf0051c | |||
| d5cee7b35b | |||
| 3ac96c4ae4 | |||
| 60545674c5 | |||
| b5c313e517 | |||
| e3bdad6d77 | |||
| 7453afa684 | |||
| 86eca6bc7e | |||
| 3c0bc69662 | |||
| 2baf9a1446 | |||
| 296038a3de | |||
| 71e1ea5736 | |||
| 32d05edb6a | |||
| ba608ff438 | |||
| 5d79b687d5 | |||
| ad1ec70e94 | |||
| 7e73f81d4e | |||
| 9a0ae06c87 | |||
| 902fbbf29a | |||
| a0df43484a | |||
| d2317ab908 | |||
| fa733e2285 | |||
| 134737d4a8 | |||
| def3141b6d | |||
| 057afe2bd5 | |||
| 40203f2823 | |||
| 9d711e45f9 | |||
| 35eb5716a5 | |||
| 7a10b85b4c | |||
| 7a2e86246d | |||
| 331b275105 | |||
| 3791fd568c | |||
| 6ed9bb0258 | |||
| e66f2fcd2d | |||
| 1250f5d25a | |||
| 4a3ef70979 | |||
| bf29ee430e | |||
| c7f1e75d22 | |||
| 0886712714 | |||
| f415c8bfe9 | |||
| 4c1ac0757c | |||
| 67a793038b | |||
| 05a65dab3c | |||
| 0d61e43431 | |||
| b6195603e8 | |||
| 6db306cb0c | |||
| 48140348e0 | |||
| 989574bb52 | |||
| 8f3c479642 | |||
| 4db464772e | |||
| 039d3b4058 | |||
| b99e3ed177 | |||
| 6519ba95bc | |||
| 6f22932b16 | |||
| bf725dd563 | |||
| dea6700a25 | |||
| b8ccae570e | |||
| 17fc6ccc2e | |||
| 112f310d13 | |||
| 8874589ed0 | |||
| f7621ae336 | |||
| b4cc211763 | |||
| 38263de9f1 | |||
| 260e3c50df | |||
| 57327623d1 | |||
| ff9f1d85ab | |||
| 661775d5af | |||
| dd1088d02d | |||
| 8d265ad6d2 | |||
| 346c530f76 | |||
| 870e3ad666 | |||
| e31ff5960c | |||
| 3fa71cc94a | |||
| f5ea87da7b | |||
| 643695bd2b | |||
| 697a9438c6 | |||
| 3ad665f80b | |||
| 7847eaa64d | |||
| bb870ec90f | |||
| 9959e61b35 | |||
| 92ead26873 | |||
| afb27ec989 | |||
| ff30b6511c | |||
| 56306aeaec | |||
| c2c354044a | |||
| ceb216df8c | |||
| b2994ede8c | |||
| 0b87c5085c | |||
| 2305d086cc | |||
| 6c3c1b377e | |||
| 07c4c89720 | |||
| 87efb92f07 |
@@ -3,6 +3,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
|
||||
jobs:
|
||||
test-runner:
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
# ChangeLog
|
||||
|
||||
### 4.2.4 - to be released
|
||||
### 4.2.6
|
||||
- Fixed MongoDB import
|
||||
- Configurable thousands separator #136
|
||||
- Using case insensitive text search in postgres
|
||||
|
||||
### 4.2.5
|
||||
- FIXED: Fixed crash when using large model on some installations
|
||||
- FIXED: Postgre SQL CREATE function
|
||||
- FIXED: Analysing of MySQL when modifyDate is not known
|
||||
|
||||
### 4.2.4
|
||||
- ADDED: Query history
|
||||
- ADDED: One-click exports in desktop app
|
||||
- ADDED: JSON array export
|
||||
- FIXED: Procedures in PostgreSQL #122
|
||||
- ADDED: Support of materialized views for PostgreSQL #123
|
||||
- ADDED: Integration tests
|
||||
|
||||
@@ -64,7 +64,7 @@ There are many database managers now, so why DbGate?
|
||||
* Backend - NodeJs, ExpressJs, socket.io, database connection drivers
|
||||
* JavaScript + TypeScript
|
||||
* App - electron
|
||||
* Platform independed - will run as web application in single docker container on server, or as application using Electron platform on Linux, Windows and Mac
|
||||
* Platform independent - will run as web application in single docker container on server, or as application using Electron platform on Linux, Windows and Mac
|
||||
|
||||
## Plugins
|
||||
Plugins are standard NPM packages published on [npmjs.com](https://www.npmjs.com).
|
||||
|
||||
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
@@ -45,7 +45,7 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"icon": "icon.png",
|
||||
"icon": "icons/",
|
||||
"category": "Development",
|
||||
"synopsis": "Database manager for SQL Server, MySQL, PostgreSQL, MongoDB and SQLite",
|
||||
"publish": [
|
||||
@@ -103,4 +103,4 @@
|
||||
"optionalDependencies": {
|
||||
"msnodesqlv8": "^2.0.10"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
const stableStringify = require('json-stable-stringify');
|
||||
const _ = require('lodash');
|
||||
const fp = require('lodash/fp');
|
||||
const uuidv1 = require('uuid/v1');
|
||||
const { testWrapper } = require('../tools');
|
||||
const engines = require('../engines');
|
||||
const { getAlterTableScript, extendDatabaseInfo, generateDbPairingId } = require('dbgate-tools');
|
||||
|
||||
function pickImportantTableInfo(table) {
|
||||
return {
|
||||
pureName: table.pureName,
|
||||
columns: table.columns
|
||||
.filter(x => x.columnName != 'rowid')
|
||||
.map(fp.pick(['columnName', 'notNull', 'autoIncrement'])),
|
||||
};
|
||||
}
|
||||
|
||||
function checkTableStructure(t1, t2) {
|
||||
// expect(t1.pureName).toEqual(t2.pureName)
|
||||
expect(pickImportantTableInfo(t1)).toEqual(pickImportantTableInfo(t2));
|
||||
}
|
||||
|
||||
async function testTableDiff(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
|
||||
)`
|
||||
);
|
||||
|
||||
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))`);
|
||||
|
||||
const tget = x => x.tables.find(y => y.pureName == 't1');
|
||||
const structure1 = generateDbPairingId(extendDatabaseInfo(await driver.analyseFull(conn)));
|
||||
let structure2 = _.cloneDeep(structure1);
|
||||
mangle(tget(structure2));
|
||||
structure2 = extendDatabaseInfo(structure2);
|
||||
|
||||
const sql = getAlterTableScript(tget(structure1), tget(structure2), {}, structure2, driver);
|
||||
console.log('RUNNING ALTER SQL', driver.engine, ':', sql);
|
||||
|
||||
await driver.script(conn, sql);
|
||||
|
||||
const structure2Real = extendDatabaseInfo(await driver.analyseFull(conn));
|
||||
|
||||
checkTableStructure(tget(structure2Real), tget(structure2));
|
||||
// expect(stableStringify(structure2)).toEqual(stableStringify(structure2Real));
|
||||
}
|
||||
|
||||
const TESTED_COLUMNS = ['col_pk', 'col_std', 'col_def', 'col_fk', 'col_ref', 'col_idx', 'col_uq'];
|
||||
// const TESTED_COLUMNS = ['col_pk'];
|
||||
// const TESTED_COLUMNS = ['col_idx'];
|
||||
// const TESTED_COLUMNS = ['col_def'];
|
||||
// const TESTED_COLUMNS = ['col_std'];
|
||||
|
||||
function engines_columns_source() {
|
||||
return _.flatten(engines.map(engine => TESTED_COLUMNS.map(column => [engine.label, column, engine])));
|
||||
}
|
||||
|
||||
describe('Alter processor', () => {
|
||||
test.each(engines.map(engine => [engine.label, engine]))(
|
||||
'Add column - %s',
|
||||
testWrapper(async (conn, driver, engine) => {
|
||||
await testTableDiff(conn, driver, tbl => {
|
||||
tbl.columns.push({
|
||||
columnName: 'added',
|
||||
dataType: 'int',
|
||||
pairingId: uuidv1(),
|
||||
notNull: false,
|
||||
autoIncrement: false,
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
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)));
|
||||
})
|
||||
);
|
||||
|
||||
test.each(engines_columns_source())(
|
||||
'Change nullability - %s - %s',
|
||||
testWrapper(async (conn, driver, column, engine) => {
|
||||
await testTableDiff(
|
||||
conn,
|
||||
driver,
|
||||
tbl => (tbl.columns = tbl.columns.map(x => (x.columnName == column ? { ...x, notNull: true } : x)))
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
test.each(engines_columns_source())(
|
||||
'Rename column - %s - %s',
|
||||
testWrapper(async (conn, driver, column, engine) => {
|
||||
await testTableDiff(
|
||||
conn,
|
||||
driver,
|
||||
tbl => (tbl.columns = tbl.columns.map(x => (x.columnName == column ? { ...x, columnName: 'col_renamed' } : x)))
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
test.each(engines.map(engine => [engine.label, engine]))(
|
||||
'Drop index - %s',
|
||||
testWrapper(async (conn, driver, engine) => {
|
||||
await testTableDiff(conn, driver, tbl => {
|
||||
tbl.indexes = [];
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -2,7 +2,10 @@ const engines = require('../engines');
|
||||
const { testWrapper } = require('../tools');
|
||||
|
||||
const t1Sql = 'CREATE TABLE t1 (id int not null primary key, val1 varchar(50) null)';
|
||||
const t2Sql = 'CREATE TABLE t2 (id int not null primary key, val2 varchar(50) null)';
|
||||
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 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) =>
|
||||
expect.objectContaining({
|
||||
@@ -98,7 +101,7 @@ describe('Table analyse', () => {
|
||||
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)');
|
||||
const structure2 = await driver.analyseIncremental(conn, structure1);
|
||||
|
||||
@@ -109,4 +112,53 @@ describe('Table analyse', () => {
|
||||
expect(structure2.tables.find(x => x.pureName == 't2')).toEqual(t2NextColMatch);
|
||||
})
|
||||
);
|
||||
|
||||
test.each(engines.map(engine => [engine.label, engine]))(
|
||||
'Index - full analysis - %s',
|
||||
testWrapper(async (conn, driver, engine) => {
|
||||
await driver.query(conn, t1Sql);
|
||||
await driver.query(conn, ix1Sql);
|
||||
const structure = await driver.analyseFull(conn);
|
||||
|
||||
const t1 = structure.tables.find(x => x.pureName == 't1');
|
||||
expect(t1.indexes.length).toEqual(1);
|
||||
expect(t1.indexes[0].columns.length).toEqual(2);
|
||||
expect(t1.indexes[0].columns[0]).toEqual(expect.objectContaining({ columnName: 'val1' }));
|
||||
expect(t1.indexes[0].columns[1]).toEqual(expect.objectContaining({ columnName: 'id' }));
|
||||
})
|
||||
);
|
||||
|
||||
test.each(engines.map(engine => [engine.label, engine]))(
|
||||
'Unique - full analysis - %s',
|
||||
testWrapper(async (conn, driver, engine) => {
|
||||
await driver.query(conn, t2Sql);
|
||||
const structure = await driver.analyseFull(conn);
|
||||
|
||||
const t2 = structure.tables.find(x => x.pureName == 't2');
|
||||
// const indexesAndUniques = [...t2.uniques, ...t2.indexes];
|
||||
expect(t2.uniques.length).toEqual(1);
|
||||
expect(t2.uniques[0].columns.length).toEqual(1);
|
||||
expect(t2.uniques[0].columns[0]).toEqual(expect.objectContaining({ columnName: 'val2' }));
|
||||
})
|
||||
);
|
||||
|
||||
test.each(engines.map(engine => [engine.label, engine]))(
|
||||
'Foreign key - full analysis - %s',
|
||||
testWrapper(async (conn, driver, engine) => {
|
||||
await driver.query(conn, t2Sql);
|
||||
await driver.query(conn, t3Sql);
|
||||
// await driver.query(conn, fkSql);
|
||||
|
||||
const structure = await driver.analyseFull(conn);
|
||||
|
||||
const t3 = structure.tables.find(x => x.pureName == 't3');
|
||||
console.log('T3', t3.foreignKeys[0].columns);
|
||||
expect(t3.foreignKeys.length).toEqual(1);
|
||||
expect(t3.foreignKeys[0].columns.length).toEqual(1);
|
||||
expect(t3.foreignKeys[0]).toEqual(expect.objectContaining({ refTableName: 't2' }));
|
||||
expect(t3.foreignKeys[0].columns[0]).toEqual(
|
||||
expect.objectContaining({ columnName: 'valfk', refColumnName: 'id' })
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
const _ = require('lodash');
|
||||
const fp = require('lodash/fp');
|
||||
const engines = require('../engines');
|
||||
const { testWrapper } = require('../tools');
|
||||
const { extendDatabaseInfo } = require('dbgate-tools');
|
||||
|
||||
function createExpector(value) {
|
||||
return _.cloneDeepWith(value, x => {
|
||||
if (_.isPlainObject(x)) {
|
||||
return expect.objectContaining(_.mapValues(x, y => createExpector(y)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function omitTableSpecificInfo(table) {
|
||||
return {
|
||||
...table,
|
||||
columns: table.columns.map(fp.omit(['dataType'])),
|
||||
};
|
||||
}
|
||||
|
||||
function checkTableStructure2(t1, t2) {
|
||||
// expect(t1.pureName).toEqual(t2.pureName)
|
||||
expect(t2).toEqual(createExpector(omitTableSpecificInfo(t1)));
|
||||
}
|
||||
|
||||
async function testTableCreate(conn, driver, table) {
|
||||
await driver.query(conn, `create table t0 (id int not null primary key)`);
|
||||
|
||||
const dmp = driver.createDumper();
|
||||
const table1 = {
|
||||
...table,
|
||||
pureName: 'tested',
|
||||
};
|
||||
dmp.createTable(table1);
|
||||
|
||||
console.log('RUNNING CREATE SQL', driver.engine, ':', dmp.s);
|
||||
await driver.script(conn, dmp.s);
|
||||
|
||||
const db = extendDatabaseInfo(await driver.analyseFull(conn));
|
||||
const table2 = db.tables.find(x => x.pureName == 'tested');
|
||||
|
||||
checkTableStructure2(table1, table2);
|
||||
}
|
||||
|
||||
describe('Table create', () => {
|
||||
test.each(engines.map(engine => [engine.label, engine]))(
|
||||
'Simple table - %s',
|
||||
testWrapper(async (conn, driver, engine) => {
|
||||
await testTableCreate(conn, driver, {
|
||||
columns: [
|
||||
{
|
||||
columnName: 'col1',
|
||||
dataType: 'int',
|
||||
notNull: true,
|
||||
},
|
||||
],
|
||||
primaryKey: {
|
||||
columns: [{ columnName: 'col1' }],
|
||||
},
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
test.each(engines.map(engine => [engine.label, engine]))(
|
||||
'Table with index - %s',
|
||||
testWrapper(async (conn, driver, engine) => {
|
||||
await testTableCreate(conn, driver, {
|
||||
columns: [
|
||||
{
|
||||
columnName: 'col1',
|
||||
dataType: 'int',
|
||||
notNull: true,
|
||||
},
|
||||
{
|
||||
columnName: 'col2',
|
||||
dataType: 'int',
|
||||
notNull: true,
|
||||
},
|
||||
],
|
||||
primaryKey: {
|
||||
columns: [{ columnName: 'col1' }],
|
||||
},
|
||||
indexes: [
|
||||
{
|
||||
constraintName: 'ix1',
|
||||
pureName: 'tested',
|
||||
columns: [{ columnName: 'col2' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
test.each(engines.map(engine => [engine.label, engine]))(
|
||||
'Table with foreign key - %s',
|
||||
testWrapper(async (conn, driver, engine) => {
|
||||
await testTableCreate(conn, driver, {
|
||||
columns: [
|
||||
{
|
||||
columnName: 'col1',
|
||||
dataType: 'int',
|
||||
notNull: true,
|
||||
},
|
||||
{
|
||||
columnName: 'col2',
|
||||
dataType: 'int',
|
||||
notNull: true,
|
||||
},
|
||||
],
|
||||
primaryKey: {
|
||||
columns: [{ columnName: 'col1' }],
|
||||
},
|
||||
foreignKeys: [
|
||||
{
|
||||
pureName: 'tested',
|
||||
refTableName: 't0',
|
||||
columns: [{ columnName: 'col2', refColumnName: 'id' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
test.each(engines.map(engine => [engine.label, engine]))(
|
||||
'Table with unique - %s',
|
||||
testWrapper(async (conn, driver, engine) => {
|
||||
await testTableCreate(conn, driver, {
|
||||
columns: [
|
||||
{
|
||||
columnName: 'col1',
|
||||
dataType: 'int',
|
||||
notNull: true,
|
||||
},
|
||||
{
|
||||
columnName: 'col2',
|
||||
dataType: 'int',
|
||||
notNull: true,
|
||||
},
|
||||
],
|
||||
primaryKey: {
|
||||
columns: [{ columnName: 'col1' }],
|
||||
},
|
||||
uniques: [
|
||||
{
|
||||
pureName: 'tested',
|
||||
columns: [{ columnName: 'col2' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -54,6 +54,15 @@ const engines = [
|
||||
drop1: 'DROP PROCEDURE obj1',
|
||||
drop2: 'DROP PROCEDURE obj2',
|
||||
},
|
||||
{
|
||||
type: 'functions',
|
||||
create1:
|
||||
'CREATE FUNCTION obj1() returns int LANGUAGE plpgsql AS $$ declare res integer; begin select count(*) into res from t1; return res; end; $$',
|
||||
create2:
|
||||
'CREATE FUNCTION obj2() returns int LANGUAGE plpgsql AS $$ declare res integer; begin select count(*) into res from t2; return res; end; $$',
|
||||
drop1: 'DROP FUNCTION obj1',
|
||||
drop2: 'DROP FUNCTION obj2',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -105,4 +114,15 @@ const engines = [
|
||||
},
|
||||
];
|
||||
|
||||
module.exports = process.env.CITEST ? engines.filter(x => !x.skipOnCI) : engines;
|
||||
const filterLocal = [
|
||||
// filter local testing
|
||||
'MySQL',
|
||||
'PostgreSQL',
|
||||
'SQL Server',
|
||||
'SQLite',
|
||||
'CockroachDB',
|
||||
];
|
||||
|
||||
module.exports = process.env.CITEST
|
||||
? engines.filter(x => !x.skipOnCI)
|
||||
: engines.filter(x => filterLocal.find(y => x.label == y));
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
|
||||
"run:local": "docker-compose down && docker-compose up -d && yarn wait:local && yarn test:local"
|
||||
},
|
||||
"jest": {
|
||||
"testTimeout": 5000
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "^7.0.3",
|
||||
"jest": "^27.0.1"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"version": "4.2.4-beta.2",
|
||||
"version": "4.2.8-beta.3",
|
||||
"name": "dbgate-all",
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
|
||||
@@ -8,6 +8,7 @@ const consoleObjectWriter = require('./consoleObjectWriter');
|
||||
const jsonLinesWriter = require('./jsonLinesWriter');
|
||||
const jsonArrayWriter = require('./jsonArrayWriter');
|
||||
const jsonLinesReader = require('./jsonLinesReader');
|
||||
const sqlDataWriter = require('./sqlDataWriter');
|
||||
const jslDataReader = require('./jslDataReader');
|
||||
const archiveWriter = require('./archiveWriter');
|
||||
const archiveReader = require('./archiveReader');
|
||||
@@ -29,6 +30,7 @@ const dbgateApi = {
|
||||
jsonLinesWriter,
|
||||
jsonArrayWriter,
|
||||
jsonLinesReader,
|
||||
sqlDataWriter,
|
||||
fakeObjectReader,
|
||||
consoleObjectWriter,
|
||||
jslDataReader,
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
const fs = require('fs');
|
||||
const stream = require('stream');
|
||||
const path = require('path');
|
||||
const { driverBase } = require('dbgate-tools');
|
||||
const requireEngineDriver = require('../utility/requireEngineDriver');
|
||||
|
||||
class SqlizeStream extends stream.Transform {
|
||||
constructor({ fileName }) {
|
||||
super({ objectMode: true });
|
||||
this.wasHeader = false;
|
||||
this.tableName = path.parse(fileName).name;
|
||||
this.driver = driverBase;
|
||||
}
|
||||
_transform(chunk, encoding, done) {
|
||||
let skip = false;
|
||||
if (!this.wasHeader) {
|
||||
if (
|
||||
chunk.__isStreamHeader ||
|
||||
// TODO remove isArray test
|
||||
Array.isArray(chunk.columns)
|
||||
) {
|
||||
skip = true;
|
||||
this.tableName = chunk.pureName;
|
||||
if (chunk.engine) {
|
||||
// @ts-ignore
|
||||
this.driver = requireEngineDriver(chunk.engine) || driverBase;
|
||||
}
|
||||
}
|
||||
this.wasHeader = true;
|
||||
}
|
||||
if (!skip) {
|
||||
const dmp = this.driver.createDumper();
|
||||
dmp.put(
|
||||
'^insert ^into %f (%,i) ^values (%,v);\n',
|
||||
{ pureName: this.tableName },
|
||||
Object.keys(chunk),
|
||||
Object.values(chunk)
|
||||
);
|
||||
this.push(dmp.s);
|
||||
}
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
||||
async function sqlDataWriter({ fileName, driver, encoding = 'utf-8' }) {
|
||||
console.log(`Writing file ${fileName}`);
|
||||
const stringify = new SqlizeStream({ fileName });
|
||||
const fileStream = fs.createWriteStream(fileName, encoding);
|
||||
stringify.pipe(fileStream);
|
||||
stringify['finisher'] = fileStream;
|
||||
return stringify;
|
||||
}
|
||||
|
||||
module.exports = sqlDataWriter;
|
||||
@@ -75,6 +75,7 @@ export abstract class GridDisplay {
|
||||
}
|
||||
changeSetKeyFields: string[] = null;
|
||||
sortable = false;
|
||||
groupable = false;
|
||||
filterable = false;
|
||||
editable = false;
|
||||
isLoadedCorrectly = true;
|
||||
|
||||
@@ -35,6 +35,7 @@ export class TableGridDisplay extends GridDisplay {
|
||||
this.columns = this.getDisplayColumns(this.table, []);
|
||||
this.filterable = true;
|
||||
this.sortable = true;
|
||||
this.groupable = true;
|
||||
this.editable = true;
|
||||
this.supportsReload = true;
|
||||
this.baseTable = this.table;
|
||||
|
||||
@@ -17,6 +17,7 @@ export class ViewGridDisplay extends GridDisplay {
|
||||
this.columns = this.getDisplayColumns(view);
|
||||
this.filterable = true;
|
||||
this.sortable = true;
|
||||
this.groupable = true;
|
||||
this.editable = false;
|
||||
this.supportsReload = true;
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ export function dumpSqlCondition(dmp: SqlDumper, condition: Condition) {
|
||||
break;
|
||||
case 'like':
|
||||
dumpSqlExpression(dmp, condition.left);
|
||||
dmp.put(' ^like ');
|
||||
dmp.put(dmp.dialect.ilike ? ' ^ilike ' : ' ^like ');
|
||||
dumpSqlExpression(dmp, condition.right);
|
||||
break;
|
||||
case 'notLike':
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"typescript": "^3.7.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.21"
|
||||
"lodash": "^4.17.21",
|
||||
"uuid": "^3.4.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@ import _groupBy from 'lodash/groupBy';
|
||||
import _pick from 'lodash/pick';
|
||||
import _compact from 'lodash/compact';
|
||||
|
||||
const STRUCTURE_FIELDS = ['tables', 'collections', 'views', 'matviews', 'functions', 'procedures', 'triggers'];
|
||||
|
||||
const fp_pick = arg => array => _pick(array, arg);
|
||||
|
||||
export class DatabaseAnalyser {
|
||||
structure: DatabaseInfo;
|
||||
modifications: DatabaseModification[];
|
||||
@@ -23,8 +26,20 @@ export class DatabaseAnalyser {
|
||||
|
||||
async _computeSingleObjectId() {}
|
||||
|
||||
addEngineField(db: DatabaseInfo) {
|
||||
if (!this.driver?.engine) return;
|
||||
for (const field of STRUCTURE_FIELDS) {
|
||||
if (!db[field]) continue;
|
||||
for (const item of db[field]) {
|
||||
item.engine = this.driver.engine;
|
||||
}
|
||||
}
|
||||
db.engine = this.driver.engine;
|
||||
return db;
|
||||
}
|
||||
|
||||
async fullAnalysis() {
|
||||
const res = await this._runAnalysis();
|
||||
const res = this.addEngineField(await this._runAnalysis());
|
||||
// console.log('FULL ANALYSIS', res);
|
||||
return res;
|
||||
}
|
||||
@@ -33,7 +48,7 @@ export class DatabaseAnalyser {
|
||||
// console.log('Analysing SINGLE OBJECT', name, typeField);
|
||||
this.singleObjectFilter = { ...name, typeField };
|
||||
await this._computeSingleObjectId();
|
||||
const res = await this._runAnalysis();
|
||||
const res = this.addEngineField(await this._runAnalysis());
|
||||
// console.log('SINGLE OBJECT RES', res);
|
||||
const obj =
|
||||
res[typeField]?.length == 1
|
||||
@@ -50,11 +65,11 @@ export class DatabaseAnalyser {
|
||||
if (this.modifications == null) {
|
||||
// modifications not implemented, perform full analysis
|
||||
this.structure = null;
|
||||
return this._runAnalysis();
|
||||
return this.addEngineField(await this._runAnalysis());
|
||||
}
|
||||
if (this.modifications.length == 0) return null;
|
||||
console.log('DB modifications detected:', this.modifications);
|
||||
return this.mergeAnalyseResult(await this._runAnalysis());
|
||||
return this.addEngineField(this.mergeAnalyseResult(await this._runAnalysis()));
|
||||
}
|
||||
|
||||
mergeAnalyseResult(newlyAnalysed) {
|
||||
@@ -66,7 +81,7 @@ export class DatabaseAnalyser {
|
||||
}
|
||||
|
||||
const res = {};
|
||||
for (const field of ['tables', 'collections', 'views', 'matviews', 'functions', 'procedures', 'triggers']) {
|
||||
for (const field of STRUCTURE_FIELDS) {
|
||||
const removedIds = this.modifications
|
||||
.filter(x => x.action == 'remove' && x.objectTypeField == field)
|
||||
.map(x => x.objectId);
|
||||
|
||||
@@ -15,12 +15,14 @@ import {
|
||||
IndexInfo,
|
||||
UniqueInfo,
|
||||
CheckInfo,
|
||||
AlterProcessor,
|
||||
} from 'dbgate-types';
|
||||
import _isString from 'lodash/isString';
|
||||
import _isNumber from 'lodash/isNumber';
|
||||
import _isDate from 'lodash/isDate';
|
||||
import uuidv1 from 'uuid/v1';
|
||||
|
||||
export class SqlDumper {
|
||||
export class SqlDumper implements AlterProcessor {
|
||||
s = '';
|
||||
driver: EngineDriver;
|
||||
dialect: SqlDialect;
|
||||
@@ -227,30 +229,25 @@ export class SqlDumper {
|
||||
table.primaryKey.columns.map(x => x.columnName)
|
||||
);
|
||||
}
|
||||
if (table.foreignKeys) {
|
||||
table.foreignKeys.forEach(fk => {
|
||||
this.put(',&n');
|
||||
this.createForeignKeyFore(fk);
|
||||
});
|
||||
}
|
||||
// foreach (var cnt in table.Uniques)
|
||||
// {
|
||||
// if (!first) this.put(", &n");
|
||||
// first = false;
|
||||
// CreateUniqueCore(cnt);
|
||||
// }
|
||||
// foreach (var cnt in table.Checks)
|
||||
// {
|
||||
// if (!first) this.put(", &n");
|
||||
// first = false;
|
||||
// CreateCheckCore(cnt);
|
||||
// }
|
||||
|
||||
(table.foreignKeys || []).forEach(fk => {
|
||||
this.put(',&n');
|
||||
this.createForeignKeyFore(fk);
|
||||
});
|
||||
(table.uniques || []).forEach(uq => {
|
||||
this.put(',&n');
|
||||
this.createUniqueCore(uq);
|
||||
});
|
||||
(table.checks || []).forEach(chk => {
|
||||
this.put(',&n');
|
||||
this.createCheckCore(chk);
|
||||
});
|
||||
|
||||
this.put('&<&n)');
|
||||
this.endCommand();
|
||||
// foreach (var ix in table.Indexes)
|
||||
// {
|
||||
// CreateIndex(ix);
|
||||
// }
|
||||
(table.indexes || []).forEach(ix => {
|
||||
this.createIndex(ix);
|
||||
});
|
||||
}
|
||||
|
||||
createForeignKeyFore(fk: ForeignKeyInfo) {
|
||||
@@ -349,14 +346,52 @@ export class SqlDumper {
|
||||
changeTriggerSchema(obj: TriggerInfo, newSchema: string) {}
|
||||
renameTrigger(obj: TriggerInfo, newSchema: string) {}
|
||||
|
||||
dropConstraint(cnt: ConstraintInfo) {
|
||||
dropConstraintCore(cnt: ConstraintInfo) {
|
||||
this.putCmd('^alter ^table %f ^drop ^constraint %i', cnt, cnt.constraintName);
|
||||
}
|
||||
dropConstraint(cnt: ConstraintInfo) {
|
||||
switch (cnt.constraintType) {
|
||||
case 'primaryKey':
|
||||
this.dropPrimaryKey(cnt as PrimaryKeyInfo);
|
||||
break;
|
||||
case 'foreignKey':
|
||||
this.dropForeignKey(cnt as ForeignKeyInfo);
|
||||
break;
|
||||
case 'unique':
|
||||
this.dropUnique(cnt as UniqueInfo);
|
||||
break;
|
||||
case 'check':
|
||||
this.dropCheck(cnt as CheckInfo);
|
||||
break;
|
||||
case 'index':
|
||||
this.dropIndex(cnt as IndexInfo);
|
||||
break;
|
||||
}
|
||||
}
|
||||
createConstraint(cnt: ConstraintInfo) {
|
||||
switch (cnt.constraintType) {
|
||||
case 'primaryKey':
|
||||
this.createPrimaryKey(cnt as PrimaryKeyInfo);
|
||||
break;
|
||||
case 'foreignKey':
|
||||
this.createForeignKey(cnt as ForeignKeyInfo);
|
||||
break;
|
||||
case 'unique':
|
||||
this.createUnique(cnt as UniqueInfo);
|
||||
break;
|
||||
case 'check':
|
||||
this.createCheck(cnt as CheckInfo);
|
||||
break;
|
||||
case 'index':
|
||||
this.createIndex(cnt as IndexInfo);
|
||||
break;
|
||||
}
|
||||
}
|
||||
dropForeignKey(fk: ForeignKeyInfo) {
|
||||
if (this.dialect.explicitDropConstraint) {
|
||||
this.putCmd('^alter ^table %f ^drop ^foreign ^key %i', fk, fk.constraintName);
|
||||
} else {
|
||||
this.dropConstraint(fk);
|
||||
this.dropConstraintCore(fk);
|
||||
}
|
||||
}
|
||||
createForeignKey(fk: ForeignKeyInfo) {
|
||||
@@ -368,7 +403,7 @@ export class SqlDumper {
|
||||
if (this.dialect.explicitDropConstraint) {
|
||||
this.putCmd('^alter ^table %f ^drop ^primary ^key', pk);
|
||||
} else {
|
||||
this.dropConstraint(pk);
|
||||
this.dropConstraintCore(pk);
|
||||
}
|
||||
}
|
||||
createPrimaryKey(pk: PrimaryKeyInfo) {
|
||||
@@ -380,11 +415,26 @@ export class SqlDumper {
|
||||
);
|
||||
}
|
||||
|
||||
dropIndex(ix: IndexInfo) {}
|
||||
createIndex(ix: IndexInfo) {}
|
||||
dropIndex(ix: IndexInfo) {
|
||||
this.put('^drop ^index %i', ix.constraintName);
|
||||
if (this.dialect.dropIndexContainsTableSpec) {
|
||||
this.put(' ^on %f', ix);
|
||||
}
|
||||
this.endCommand();
|
||||
}
|
||||
createIndex(ix: IndexInfo) {
|
||||
this.put('^create');
|
||||
if (ix.isUnique) this.put(' ^unique');
|
||||
this.put(' ^index %i &n^on %f (&>&n', ix.constraintName, ix);
|
||||
this.putCollection(',&n', ix.columns, col => {
|
||||
this.put('%i %k', col.columnName, col.isDescending == true ? 'DESC' : 'ASC');
|
||||
});
|
||||
this.put('&<&n)');
|
||||
this.endCommand();
|
||||
}
|
||||
|
||||
dropUnique(uq: UniqueInfo) {
|
||||
this.dropConstraint(uq);
|
||||
this.dropConstraintCore(uq);
|
||||
}
|
||||
createUniqueCore(uq: UniqueInfo) {
|
||||
this.put(
|
||||
@@ -401,7 +451,7 @@ export class SqlDumper {
|
||||
}
|
||||
|
||||
dropCheck(ch: CheckInfo) {
|
||||
this.dropConstraint(ch);
|
||||
this.dropConstraintCore(ch);
|
||||
}
|
||||
|
||||
createCheckCore(ch: CheckInfo) {
|
||||
@@ -416,8 +466,8 @@ export class SqlDumper {
|
||||
|
||||
renameConstraint(constraint: ConstraintInfo, newName: string) {}
|
||||
|
||||
createColumn(table: TableInfo, column: ColumnInfo, constraints: ConstraintInfo[]) {
|
||||
this.put('^alter ^table %f ^add %i ', table, column.columnName);
|
||||
createColumn(column: ColumnInfo, constraints: ConstraintInfo[]) {
|
||||
this.put('^alter ^table %f ^add %i ', column, column.columnName);
|
||||
this.columnDefinition(column);
|
||||
this.inlineConstraints(constraints);
|
||||
this.endCommand();
|
||||
@@ -443,7 +493,7 @@ export class SqlDumper {
|
||||
|
||||
changeColumn(oldcol: ColumnInfo, newcol: ColumnInfo, constraints: ConstraintInfo[]) {}
|
||||
|
||||
dropTable(obj: TableInfo, { testIfExists = false }) {
|
||||
dropTable(obj: TableInfo, { testIfExists = false } = {}) {
|
||||
this.putCmd('^drop ^table %f', obj);
|
||||
}
|
||||
|
||||
@@ -469,4 +519,61 @@ export class SqlDumper {
|
||||
truncateTable(name: NamedObjectInfo) {
|
||||
this.putCmd('^delete ^from %f', name);
|
||||
}
|
||||
|
||||
dropConstraints(table: TableInfo, dropReferences = false) {
|
||||
if (dropReferences && this.dialect.dropForeignKey) {
|
||||
table.dependencies.forEach(cnt => this.dropConstraint(cnt));
|
||||
}
|
||||
if (this.dialect.dropIndex) {
|
||||
table.indexes.forEach(cnt => this.dropIndex(cnt));
|
||||
}
|
||||
if (this.dialect.dropForeignKey) {
|
||||
table.foreignKeys.forEach(cnt => this.dropForeignKey(cnt));
|
||||
}
|
||||
if (this.dialect.dropPrimaryKey && table.primaryKey) {
|
||||
this.dropPrimaryKey(table.primaryKey);
|
||||
}
|
||||
}
|
||||
|
||||
recreateTable(oldTable: TableInfo, newTable: TableInfo) {
|
||||
if (oldTable.pairingId != newTable.pairingId) {
|
||||
throw new Error('Recreate is not possible: oldTable.paringId != newTable.paringId');
|
||||
}
|
||||
const tmpTable = `temp_${uuidv1()}`;
|
||||
|
||||
const columnPairs = oldTable.columns
|
||||
.map(oldcol => ({
|
||||
oldcol,
|
||||
newcol: newTable.columns.find(x => x.pairingId == oldcol.pairingId),
|
||||
}))
|
||||
.filter(x => x.newcol);
|
||||
|
||||
this.dropConstraints(oldTable, true);
|
||||
this.renameTable(oldTable, tmpTable);
|
||||
|
||||
this.createTable(newTable);
|
||||
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,380 @@
|
||||
import _ from 'lodash';
|
||||
import {
|
||||
AlterProcessor,
|
||||
ColumnInfo,
|
||||
ConstraintInfo,
|
||||
DatabaseInfo,
|
||||
NamedObjectInfo,
|
||||
SqlDialect,
|
||||
TableInfo,
|
||||
} from '../../types';
|
||||
import { DatabaseInfoAlterProcessor } from './database-info-alter-processor';
|
||||
import { DatabaseAnalyser } from './DatabaseAnalyser';
|
||||
|
||||
interface AlterOperation_CreateTable {
|
||||
operationType: 'createTable';
|
||||
newObject: TableInfo;
|
||||
}
|
||||
|
||||
interface AlterOperation_DropTable {
|
||||
operationType: 'dropTable';
|
||||
oldObject: TableInfo;
|
||||
}
|
||||
|
||||
interface AlterOperation_RenameTable {
|
||||
operationType: 'renameTable';
|
||||
object: TableInfo;
|
||||
newName: string;
|
||||
}
|
||||
|
||||
interface AlterOperation_CreateColumn {
|
||||
operationType: 'createColumn';
|
||||
newObject: ColumnInfo;
|
||||
}
|
||||
|
||||
interface AlterOperation_ChangeColumn {
|
||||
operationType: 'changeColumn';
|
||||
oldObject: ColumnInfo;
|
||||
newObject: ColumnInfo;
|
||||
}
|
||||
|
||||
interface AlterOperation_RenameColumn {
|
||||
operationType: 'renameColumn';
|
||||
object: ColumnInfo;
|
||||
newName: string;
|
||||
}
|
||||
|
||||
interface AlterOperation_DropColumn {
|
||||
operationType: 'dropColumn';
|
||||
oldObject: ColumnInfo;
|
||||
}
|
||||
|
||||
interface AlterOperation_CreateConstraint {
|
||||
operationType: 'createConstraint';
|
||||
newObject: ConstraintInfo;
|
||||
}
|
||||
|
||||
interface AlterOperation_ChangeConstraint {
|
||||
operationType: 'changeConstraint';
|
||||
oldObject: ConstraintInfo;
|
||||
newObject: ConstraintInfo;
|
||||
}
|
||||
|
||||
interface AlterOperation_DropConstraint {
|
||||
operationType: 'dropConstraint';
|
||||
oldObject: ConstraintInfo;
|
||||
}
|
||||
|
||||
interface AlterOperation_RenameConstraint {
|
||||
operationType: 'renameConstraint';
|
||||
object: ConstraintInfo;
|
||||
newName: string;
|
||||
}
|
||||
interface AlterOperation_RecreateTable {
|
||||
operationType: 'recreateTable';
|
||||
table: TableInfo;
|
||||
operations: AlterOperation[];
|
||||
}
|
||||
|
||||
type AlterOperation =
|
||||
| AlterOperation_CreateColumn
|
||||
| AlterOperation_ChangeColumn
|
||||
| AlterOperation_DropColumn
|
||||
| AlterOperation_CreateConstraint
|
||||
| AlterOperation_ChangeConstraint
|
||||
| AlterOperation_DropConstraint
|
||||
| AlterOperation_CreateTable
|
||||
| AlterOperation_DropTable
|
||||
| AlterOperation_RenameTable
|
||||
| AlterOperation_RenameColumn
|
||||
| AlterOperation_RenameConstraint
|
||||
| AlterOperation_RecreateTable;
|
||||
|
||||
export class AlterPlan {
|
||||
public operations: AlterOperation[] = [];
|
||||
constructor(public db: DatabaseInfo, public dialect: SqlDialect) {}
|
||||
|
||||
createTable(table: TableInfo) {
|
||||
this.operations.push({
|
||||
operationType: 'createTable',
|
||||
newObject: table,
|
||||
});
|
||||
}
|
||||
|
||||
dropTable(table: TableInfo) {
|
||||
this.operations.push({
|
||||
operationType: 'dropTable',
|
||||
oldObject: table,
|
||||
});
|
||||
}
|
||||
|
||||
createColumn(column: ColumnInfo) {
|
||||
this.operations.push({
|
||||
operationType: 'createColumn',
|
||||
newObject: column,
|
||||
});
|
||||
}
|
||||
|
||||
changeColumn(oldColumn: ColumnInfo, newColumn: ColumnInfo) {
|
||||
this.operations.push({
|
||||
operationType: 'changeColumn',
|
||||
oldObject: oldColumn,
|
||||
newObject: newColumn,
|
||||
});
|
||||
}
|
||||
|
||||
dropColumn(column: ColumnInfo) {
|
||||
this.operations.push({
|
||||
operationType: 'dropColumn',
|
||||
oldObject: column,
|
||||
});
|
||||
}
|
||||
|
||||
createConstraint(constraint: ConstraintInfo) {
|
||||
this.operations.push({
|
||||
operationType: 'createConstraint',
|
||||
newObject: constraint,
|
||||
});
|
||||
}
|
||||
|
||||
changeConstraint(oldConstraint: ConstraintInfo, newConstraint: ConstraintInfo) {
|
||||
this.operations.push({
|
||||
operationType: 'changeConstraint',
|
||||
oldObject: oldConstraint,
|
||||
newObject: newConstraint,
|
||||
});
|
||||
}
|
||||
|
||||
dropConstraint(constraint: ConstraintInfo) {
|
||||
this.operations.push({
|
||||
operationType: 'dropConstraint',
|
||||
oldObject: constraint,
|
||||
});
|
||||
}
|
||||
|
||||
renameTable(table: TableInfo, newName: string) {
|
||||
this.operations.push({
|
||||
operationType: 'renameTable',
|
||||
object: table,
|
||||
newName,
|
||||
});
|
||||
}
|
||||
|
||||
renameColumn(column: ColumnInfo, newName: string) {
|
||||
this.operations.push({
|
||||
operationType: 'renameColumn',
|
||||
object: column,
|
||||
newName,
|
||||
});
|
||||
}
|
||||
|
||||
renameConstraint(constraint: ConstraintInfo, newName: string) {
|
||||
this.operations.push({
|
||||
operationType: 'renameConstraint',
|
||||
object: constraint,
|
||||
newName,
|
||||
});
|
||||
}
|
||||
|
||||
recreateTable(table: TableInfo, operations: AlterOperation[]) {
|
||||
this.operations.push({
|
||||
operationType: 'recreateTable',
|
||||
table,
|
||||
operations,
|
||||
});
|
||||
}
|
||||
|
||||
run(processor: AlterProcessor) {
|
||||
for (const op of this.operations) {
|
||||
runAlterOperation(op, processor);
|
||||
}
|
||||
}
|
||||
|
||||
_addLogicalDependencies(): AlterOperation[] {
|
||||
const lists = this.operations.map(op => {
|
||||
if (op.operationType == 'dropColumn') {
|
||||
const table = this.db.tables.find(
|
||||
x => x.pureName == op.oldObject.pureName && x.schemaName == op.oldObject.schemaName
|
||||
);
|
||||
const deletedFks = this.dialect.dropColumnDependencies?.includes('dependencies')
|
||||
? table.dependencies.filter(fk => fk.columns.find(col => col.refColumnName == op.oldObject.columnName))
|
||||
: [];
|
||||
|
||||
const deletedConstraints = _.compact([
|
||||
this.dialect.dropColumnDependencies?.includes('primaryKey') ? table.primaryKey : null,
|
||||
...(this.dialect.dropColumnDependencies?.includes('foreignKeys') ? table.foreignKeys : []),
|
||||
...(this.dialect.dropColumnDependencies?.includes('indexes') ? table.indexes : []),
|
||||
...(this.dialect.dropColumnDependencies?.includes('uniques') ? table.uniques : []),
|
||||
]).filter(cnt => cnt.columns.find(col => col.columnName == op.oldObject.columnName));
|
||||
|
||||
// console.log('deletedConstraints', deletedConstraints);
|
||||
|
||||
const res: AlterOperation[] = [
|
||||
...[...deletedFks, ...deletedConstraints].map(oldObject => {
|
||||
const opRes: AlterOperation = {
|
||||
operationType: 'dropConstraint',
|
||||
oldObject,
|
||||
};
|
||||
return opRes;
|
||||
}),
|
||||
op,
|
||||
];
|
||||
return res;
|
||||
}
|
||||
return [op];
|
||||
});
|
||||
|
||||
return _.flatten(lists);
|
||||
}
|
||||
|
||||
_transformToImplementedOps(): AlterOperation[] {
|
||||
const lists = this.operations.map(op => {
|
||||
return (
|
||||
this._testTableRecreate(op, 'createColumn', this.dialect.createColumn, 'newObject') ||
|
||||
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') || [op]
|
||||
);
|
||||
});
|
||||
|
||||
return _.flatten(lists);
|
||||
}
|
||||
|
||||
_canCreateConstraint(cnt: ConstraintInfo) {
|
||||
if (cnt.constraintType == 'primaryKey') 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;
|
||||
if (cnt.constraintType == 'check') return this.dialect.createCheck;
|
||||
return null;
|
||||
}
|
||||
|
||||
_canDropConstraint(cnt: ConstraintInfo) {
|
||||
if (cnt.constraintType == 'primaryKey') 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;
|
||||
if (cnt.constraintType == 'check') return this.dialect.dropCheck;
|
||||
return null;
|
||||
}
|
||||
|
||||
_testTableRecreate(
|
||||
op: AlterOperation,
|
||||
operationType: string,
|
||||
isAllowed: boolean | Function,
|
||||
objectField: string
|
||||
): AlterOperation[] | null {
|
||||
if (op.operationType == operationType) {
|
||||
if (_.isFunction(isAllowed)) {
|
||||
if (isAllowed(op[objectField])) return null;
|
||||
} else {
|
||||
if (isAllowed) return null;
|
||||
}
|
||||
|
||||
// console.log('*****************RECREATED NEEDED', op, operationType, isAllowed);
|
||||
// console.log(this.dialect);
|
||||
const table = this.db.tables.find(
|
||||
x => x.pureName == op[objectField].pureName && x.schemaName == op[objectField].schemaName
|
||||
);
|
||||
return [
|
||||
{
|
||||
operationType: 'recreateTable',
|
||||
table,
|
||||
operations: [op],
|
||||
},
|
||||
];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
_groupTableRecreations(): AlterOperation[] {
|
||||
const res = [];
|
||||
const recreates = {};
|
||||
for (const op of this.operations) {
|
||||
if (op.operationType == 'recreateTable') {
|
||||
const recreate = {
|
||||
...op,
|
||||
operations: [...op.operations],
|
||||
};
|
||||
res.push(recreate);
|
||||
recreates[`${op.table.schemaName}||${op.table.pureName}`] = recreate;
|
||||
} else {
|
||||
// @ts-ignore
|
||||
const oldObject: TableInfo = op.oldObject;
|
||||
if (oldObject) {
|
||||
const recreated = recreates[`${oldObject.schemaName}||${oldObject.pureName}`];
|
||||
if (recreated) {
|
||||
recreated.operations.push(op);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
res.push(op);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
transformPlan() {
|
||||
// console.log('*****************OPERATIONS0', this.operations);
|
||||
|
||||
this.operations = this._addLogicalDependencies();
|
||||
|
||||
// console.log('*****************OPERATIONS1', this.operations);
|
||||
|
||||
this.operations = this._transformToImplementedOps();
|
||||
|
||||
// console.log('*****************OPERATIONS2', this.operations);
|
||||
|
||||
this.operations = this._groupTableRecreations();
|
||||
|
||||
// console.log('*****************OPERATIONS3', this.operations);
|
||||
}
|
||||
}
|
||||
|
||||
export function runAlterOperation(op: AlterOperation, processor: AlterProcessor) {
|
||||
switch (op.operationType) {
|
||||
case 'createTable':
|
||||
processor.createTable(op.newObject);
|
||||
break;
|
||||
case 'changeColumn':
|
||||
processor.changeColumn(op.oldObject, op.newObject);
|
||||
break;
|
||||
case 'createColumn':
|
||||
processor.createColumn(op.newObject, []);
|
||||
break;
|
||||
case 'dropColumn':
|
||||
processor.dropColumn(op.oldObject);
|
||||
break;
|
||||
case 'changeConstraint':
|
||||
processor.changeConstraint(op.oldObject, op.newObject);
|
||||
break;
|
||||
case 'createConstraint':
|
||||
processor.createConstraint(op.newObject);
|
||||
break;
|
||||
case 'dropConstraint':
|
||||
processor.dropConstraint(op.oldObject);
|
||||
break;
|
||||
case 'renameColumn':
|
||||
processor.renameColumn(op.object, op.newName);
|
||||
break;
|
||||
case 'renameTable':
|
||||
processor.renameTable(op.object, op.newName);
|
||||
break;
|
||||
case 'renameConstraint':
|
||||
processor.renameConstraint(op.object, op.newName);
|
||||
break;
|
||||
case 'recreateTable':
|
||||
{
|
||||
const newTable = _.cloneDeep(op.table);
|
||||
const newDb = DatabaseAnalyser.createEmptyStructure();
|
||||
newDb.tables.push(newTable);
|
||||
// console.log('////////////////////////////newTable1', newTable);
|
||||
op.operations.forEach(child => runAlterOperation(child, new DatabaseInfoAlterProcessor(newDb)));
|
||||
// console.log('////////////////////////////op.operations', op.operations);
|
||||
// console.log('////////////////////////////op.table', op.table);
|
||||
// console.log('////////////////////////////newTable2', newTable);
|
||||
processor.recreateTable(op.table, newTable);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import {
|
||||
ColumnInfo,
|
||||
ConstraintInfo,
|
||||
DatabaseInfo,
|
||||
ForeignKeyInfo,
|
||||
PrimaryKeyInfo,
|
||||
TableInfo,
|
||||
IndexInfo,
|
||||
CheckInfo,
|
||||
UniqueInfo,
|
||||
} from '../../types';
|
||||
|
||||
export class DatabaseInfoAlterProcessor {
|
||||
constructor(public db: DatabaseInfo) {}
|
||||
|
||||
createTable(table: TableInfo) {
|
||||
this.db.tables.push(table);
|
||||
}
|
||||
|
||||
dropTable(table: TableInfo) {
|
||||
this.db.tables = this.db.tables.filter(x => x.pureName != table.pureName && x.schemaName != table.schemaName);
|
||||
}
|
||||
|
||||
createColumn(column: ColumnInfo) {
|
||||
const table = this.db.tables.find(x => x.pureName == column.pureName && x.schemaName == column.schemaName);
|
||||
table.columns.push(column);
|
||||
}
|
||||
|
||||
changeColumn(oldColumn: ColumnInfo, newColumn: ColumnInfo) {
|
||||
const table = this.db.tables.find(x => x.pureName == oldColumn.pureName && x.schemaName == oldColumn.schemaName);
|
||||
table.columns = table.columns.map(x => (x.columnName == oldColumn.columnName ? newColumn : x));
|
||||
}
|
||||
|
||||
dropColumn(column: ColumnInfo) {
|
||||
const table = this.db.tables.find(x => x.pureName == column.pureName && x.schemaName == column.schemaName);
|
||||
table.columns = table.columns.filter(x => x.columnName != column.columnName);
|
||||
}
|
||||
|
||||
createConstraint(constraint: ConstraintInfo) {
|
||||
const table = this.db.tables.find(x => x.pureName == constraint.pureName && x.schemaName == constraint.schemaName);
|
||||
switch (constraint.constraintType) {
|
||||
case 'primaryKey':
|
||||
table.primaryKey = constraint as PrimaryKeyInfo;
|
||||
break;
|
||||
case 'foreignKey':
|
||||
table.foreignKeys.push(constraint as ForeignKeyInfo);
|
||||
break;
|
||||
case 'index':
|
||||
table.indexes.push(constraint as IndexInfo);
|
||||
break;
|
||||
case 'unique':
|
||||
table.uniques.push(constraint as UniqueInfo);
|
||||
break;
|
||||
case 'check':
|
||||
table.checks.push(constraint as CheckInfo);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
changeConstraint(oldConstraint: ConstraintInfo, newConstraint: ConstraintInfo) {
|
||||
const table = this.db.tables.find(
|
||||
x => x.pureName == oldConstraint.pureName && x.schemaName == oldConstraint.schemaName
|
||||
);
|
||||
}
|
||||
|
||||
dropConstraint(constraint: ConstraintInfo) {
|
||||
const table = this.db.tables.find(x => x.pureName == constraint.pureName && x.schemaName == constraint.schemaName);
|
||||
switch (constraint.constraintType) {
|
||||
case 'primaryKey':
|
||||
table.primaryKey = null;
|
||||
break;
|
||||
case 'foreignKey':
|
||||
table.foreignKeys = table.foreignKeys.filter(x => x.constraintName != constraint.constraintName);
|
||||
break;
|
||||
case 'index':
|
||||
table.indexes = table.indexes.filter(x => x.constraintName != constraint.constraintName);
|
||||
break;
|
||||
case 'unique':
|
||||
table.uniques = table.uniques.filter(x => x.constraintName != constraint.constraintName);
|
||||
break;
|
||||
case 'check':
|
||||
table.checks = table.checks.filter(x => x.constraintName != constraint.constraintName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
renameTable(table: TableInfo, newName: string) {
|
||||
this.db.tables.find(x => x.pureName == table.pureName && x.schemaName == table.schemaName).pureName = newName;
|
||||
}
|
||||
|
||||
renameColumn(column: ColumnInfo, newName: string) {
|
||||
const table = this.db.tables.find(x => x.pureName == column.pureName && x.schemaName == column.schemaName);
|
||||
table.columns.find(x => x.columnName == column.columnName).columnName = newName;
|
||||
}
|
||||
|
||||
renameConstraint(constraint: ConstraintInfo, newName: string) {}
|
||||
|
||||
recreateTable(oldTable: TableInfo, newTable: TableInfo) {
|
||||
throw new Error('recreateTable not implemented for DatabaseInfoAlterProcessor');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
import { ColumnInfo, DatabaseInfo, EngineDriver, NamedObjectInfo, TableInfo } from 'dbgate-types';
|
||||
import uuidv1 from 'uuid/v1';
|
||||
import { AlterPlan } from './alterPlan';
|
||||
|
||||
type DbDiffSchemaMode = 'strict' | 'ignore' | 'ignoreImplicit';
|
||||
|
||||
export interface DbDiffOptions {
|
||||
allowRecreateTable?: boolean;
|
||||
allowRecreateConstraint?: boolean;
|
||||
allowRecreateSpecificObject?: boolean;
|
||||
allowPairRenamedTables?: boolean;
|
||||
|
||||
ignoreCase?: boolean;
|
||||
schemaMode?: DbDiffSchemaMode;
|
||||
leftImplicitSchema?: string;
|
||||
rightImplicitSchema?: string;
|
||||
}
|
||||
|
||||
export function generateTablePairingId(table: TableInfo): TableInfo {
|
||||
if (!table) return table;
|
||||
if (!table.pairingId) {
|
||||
return {
|
||||
...table,
|
||||
columns: table.columns.map(col => ({
|
||||
...col,
|
||||
pairingId: col.pairingId || uuidv1(),
|
||||
})),
|
||||
foreignKeys: table.foreignKeys.map(cnt => ({
|
||||
...cnt,
|
||||
pairingId: cnt.pairingId || uuidv1(),
|
||||
})),
|
||||
checks: table.checks.map(cnt => ({
|
||||
...cnt,
|
||||
pairingId: cnt.pairingId || uuidv1(),
|
||||
})),
|
||||
indexes: table.indexes.map(cnt => ({
|
||||
...cnt,
|
||||
pairingId: cnt.pairingId || uuidv1(),
|
||||
})),
|
||||
pairingId: table.pairingId || uuidv1(),
|
||||
};
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
export function generateDbPairingId(db: DatabaseInfo): DatabaseInfo {
|
||||
if (!db) return db;
|
||||
return {
|
||||
...db,
|
||||
tables: (db.tables || []).map(generateTablePairingId),
|
||||
};
|
||||
}
|
||||
|
||||
function testEqualNames(a: string, b: string, opts: DbDiffOptions) {
|
||||
if (opts.ignoreCase) return a.toLowerCase() == b.toLowerCase();
|
||||
return a == b;
|
||||
}
|
||||
|
||||
function testEqualSchemas(lschema: string, rschema: string, opts: DbDiffOptions) {
|
||||
if (opts.schemaMode == 'ignore') lschema = null;
|
||||
if (opts.schemaMode == 'ignoreImplicit' && lschema == opts.leftImplicitSchema) lschema = null;
|
||||
if (opts.schemaMode == 'ignore') rschema = null;
|
||||
if (opts.schemaMode == 'ignoreImplicit' && rschema == opts.rightImplicitSchema) rschema = null;
|
||||
return testEqualNames(lschema, rschema, opts);
|
||||
}
|
||||
|
||||
function testEqualFullNames(lft: NamedObjectInfo, rgt: NamedObjectInfo, opts: DbDiffOptions) {
|
||||
if (lft == null || rgt == null) return lft == rgt;
|
||||
return testEqualSchemas(lft.schemaName, rgt.schemaName, opts) && testEqualNames(lft.pureName, rgt.pureName, opts);
|
||||
}
|
||||
|
||||
export function testEqualColumns(
|
||||
a: ColumnInfo,
|
||||
b: ColumnInfo,
|
||||
checkName: boolean,
|
||||
checkDefault: boolean,
|
||||
opts: DbDiffOptions = {}
|
||||
) {
|
||||
if (checkName && !testEqualNames(a.columnName, b.columnName, opts)) {
|
||||
// opts.DiffLogger.Trace("Column, different name: {0}; {1}", a, b);
|
||||
return false;
|
||||
}
|
||||
//if (!DbDiffTool.EqualFullNames(a.Domain, b.Domain, opts))
|
||||
//{
|
||||
// opts.DiffLogger.Trace("Column {0}, {1}: different domain: {2}; {3}", a, b, a.Domain, b.Domain);
|
||||
// return false;
|
||||
//}
|
||||
if (a.computedExpression != b.computedExpression) {
|
||||
// opts.DiffLogger.Trace(
|
||||
// 'Column {0}, {1}: different computed expression: {2}; {3}',
|
||||
// a,
|
||||
// b,
|
||||
// a.ComputedExpression,
|
||||
// b.ComputedExpression
|
||||
// );
|
||||
return false;
|
||||
}
|
||||
if (a.computedExpression != null) {
|
||||
return true;
|
||||
}
|
||||
if (checkDefault) {
|
||||
if (a.defaultValue == null) {
|
||||
if (a.defaultValue != b.defaultValue) {
|
||||
// opts.DiffLogger.Trace(
|
||||
// 'Column {0}, {1}: different default values: {2}; {3}',
|
||||
// a,
|
||||
// b,
|
||||
// a.DefaultValue,
|
||||
// b.DefaultValue
|
||||
// );
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (a.defaultValue != b.defaultValue) {
|
||||
// opts.DiffLogger.Trace(
|
||||
// 'Column {0}, {1}: different default values: {2}; {3}',
|
||||
// a,
|
||||
// b,
|
||||
// a.DefaultValue,
|
||||
// b.DefaultValue
|
||||
// );
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (a.defaultConstraint != b.defaultConstraint) {
|
||||
// opts.DiffLogger.Trace(
|
||||
// 'Column {0}, {1}: different default constraint names: {2}; {3}',
|
||||
// a,
|
||||
// b,
|
||||
// a.DefaultConstraint,
|
||||
// b.DefaultConstraint
|
||||
// );
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (a.notNull != b.notNull) {
|
||||
// opts.DiffLogger.Trace('Column {0}, {1}: different nullable: {2}; {3}', a, b, a.NotNull, b.NotNull);
|
||||
return false;
|
||||
}
|
||||
if (a.autoIncrement != b.autoIncrement) {
|
||||
// opts.DiffLogger.Trace('Column {0}, {1}: different autoincrement: {2}; {3}', a, b, a.AutoIncrement, b.AutoIncrement);
|
||||
return false;
|
||||
}
|
||||
if (a.isSparse != b.isSparse) {
|
||||
// opts.DiffLogger.Trace('Column {0}, {1}: different is_sparse: {2}; {3}', a, b, a.IsSparse, b.IsSparse);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!testEqualTypes(a, b, opts)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
//var btype = b.DataType;
|
||||
//var atype = a.DataType;
|
||||
//if (pairing != null && pairing.Target != null && pairing.Source.Dialect != null)
|
||||
//{
|
||||
// btype = pairing.Source.Dialect.MigrateDataType(b, btype, pairing.Source.Dialect.GetDefaultMigrationProfile(), null);
|
||||
// btype = pairing.Source.Dialect.GenericTypeToSpecific(btype).ToGenericType();
|
||||
|
||||
// // normalize type
|
||||
// atype = pairing.Source.Dialect.GenericTypeToSpecific(atype).ToGenericType();
|
||||
//}
|
||||
//if (!EqualTypes(atype, btype, opts))
|
||||
//{
|
||||
// opts.DiffLogger.Trace("Column {0}, {1}: different types: {2}; {3}", a, b, a.DataType, b.DataType);
|
||||
// return false;
|
||||
//}
|
||||
//if (!opts.IgnoreColumnCollation && a.Collation != b.Collation)
|
||||
//{
|
||||
// opts.DiffLogger.Trace("Column {0}, {1}: different collations: {2}; {3}", a, b, a.Collation, b.Collation);
|
||||
// return false;
|
||||
//}
|
||||
//if (!opts.IgnoreColumnCharacterSet && a.CharacterSet != b.CharacterSet)
|
||||
//{
|
||||
// opts.DiffLogger.Trace("Column {0}, {1}: different character sets: {2}; {3}", a, b, a.CharacterSet, b.CharacterSet);
|
||||
// return false;
|
||||
//}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function testEqualTypes(a: ColumnInfo, b: ColumnInfo, opts: DbDiffOptions = {}) {
|
||||
if (a.dataType != b.dataType) {
|
||||
// opts.DiffLogger.Trace("Column {0}, {1}: different types: {2}; {3}", a, b, a.DataType, b.DataType);
|
||||
return false;
|
||||
}
|
||||
|
||||
//if (a.Length != b.Length)
|
||||
//{
|
||||
// opts.DiffLogger.Trace("Column {0}, {1}: different lengths: {2}; {3}", a, b, a.Length, b.Length);
|
||||
// return false;
|
||||
//}
|
||||
|
||||
//if (a.Precision != b.Precision)
|
||||
//{
|
||||
// opts.DiffLogger.Trace("Column {0}, {1}: different lengths: {2}; {3}", a, b, a.Precision, b.Precision);
|
||||
// return false;
|
||||
//}
|
||||
|
||||
//if (a.Scale != b.Scale)
|
||||
//{
|
||||
// opts.DiffLogger.Trace("Column {0}, {1}: different scale: {2}; {3}", a, b, a.Scale, b.Scale);
|
||||
// return false;
|
||||
//}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function getTableConstraints(table: TableInfo) {
|
||||
const res = [];
|
||||
if (table.primaryKey) res.push(table.primaryKey);
|
||||
if (table.foreignKeys) res.push(...table.foreignKeys);
|
||||
if (table.indexes) res.push(...table.indexes);
|
||||
if (table.checks) res.push(...table.checks);
|
||||
return res;
|
||||
}
|
||||
|
||||
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)));
|
||||
if (b) {
|
||||
res.push([a, b]);
|
||||
} else {
|
||||
res.push([a, null]);
|
||||
}
|
||||
}
|
||||
for (const b of newList) {
|
||||
if (!res.find(x => x[1] == b)) {
|
||||
res.push([null, b]);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function planAlterTable(plan: AlterPlan, oldTable: TableInfo, newTable: TableInfo, opts: DbDiffOptions) {
|
||||
// if (oldTable.primaryKey)
|
||||
|
||||
const columnPairs = createPairs(oldTable.columns, newTable.columns);
|
||||
const constraintPairs = createPairs(
|
||||
getTableConstraints(oldTable),
|
||||
getTableConstraints(newTable),
|
||||
(a, b) => a.constraintType == 'primaryKey' && b.constraintType == 'primaryKey'
|
||||
);
|
||||
|
||||
constraintPairs.filter(x => x[1] == null).forEach(x => plan.dropConstraint(x[0]));
|
||||
columnPairs.filter(x => x[1] == null).forEach(x => plan.dropColumn(x[0]));
|
||||
|
||||
if (!testEqualFullNames(oldTable, newTable, opts)) {
|
||||
plan.renameTable(oldTable, newTable.pureName);
|
||||
}
|
||||
|
||||
columnPairs.filter(x => x[0] == null).forEach(x => plan.createColumn(x[1]));
|
||||
|
||||
columnPairs
|
||||
.filter(x => x[0] && x[1])
|
||||
.forEach(x => {
|
||||
if (!testEqualColumns(x[0], x[1], true, true, opts)) {
|
||||
if (testEqualColumns(x[0], x[1], false, true, opts)) {
|
||||
// console.log('PLAN RENAME COLUMN')
|
||||
plan.renameColumn(x[0], x[1].columnName);
|
||||
} else {
|
||||
// console.log('PLAN CHANGE COLUMN')
|
||||
plan.changeColumn(x[0], x[1]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
constraintPairs.filter(x => x[0] == null).forEach(x => plan.createConstraint(x[1]));
|
||||
}
|
||||
|
||||
export function createAlterTablePlan(
|
||||
oldTable: TableInfo,
|
||||
newTable: TableInfo,
|
||||
opts: DbDiffOptions,
|
||||
db: DatabaseInfo,
|
||||
driver: EngineDriver
|
||||
): AlterPlan {
|
||||
const plan = new AlterPlan(db, driver.dialect);
|
||||
if (oldTable == null) {
|
||||
plan.createTable(newTable);
|
||||
} else {
|
||||
planAlterTable(plan, oldTable, newTable, opts);
|
||||
}
|
||||
plan.transformPlan();
|
||||
return plan;
|
||||
}
|
||||
|
||||
export function getAlterTableScript(
|
||||
oldTable: TableInfo,
|
||||
newTable: TableInfo,
|
||||
opts: DbDiffOptions,
|
||||
db: DatabaseInfo,
|
||||
driver: EngineDriver
|
||||
): string {
|
||||
const plan = createAlterTablePlan(oldTable, newTable, opts, db, driver);
|
||||
const dmp = driver.createDumper();
|
||||
plan.run(dmp);
|
||||
return dmp.s;
|
||||
}
|
||||
@@ -11,3 +11,5 @@ export * from './SqlGenerator';
|
||||
export * from './structureTools';
|
||||
export * from './settingsExtractors';
|
||||
export * from './filterName';
|
||||
export * from './diffTools';
|
||||
export * from './schemaEditorTools';
|
||||
|
||||
@@ -21,6 +21,13 @@ export function fullNameToString({ schemaName, pureName }) {
|
||||
return pureName;
|
||||
}
|
||||
|
||||
export function fullNameToLabel({ schemaName, pureName }) {
|
||||
if (schemaName) {
|
||||
return `${schemaName}.${pureName}`;
|
||||
}
|
||||
return pureName;
|
||||
}
|
||||
|
||||
export function quoteFullName(dialect, { schemaName, pureName }) {
|
||||
if (schemaName) return `${dialect.quoteIdentifier(schemaName)}.${dialect.quoteIdentifier(pureName)}`;
|
||||
return `${dialect.quoteIdentifier(pureName)}`;
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
import uuidv1 from 'uuid/v1';
|
||||
import _omit from 'lodash/omit';
|
||||
import { ColumnInfo, ConstraintInfo, ForeignKeyInfo, PrimaryKeyInfo, TableInfo } from 'dbgate-types';
|
||||
|
||||
export interface EditorColumnInfo extends ColumnInfo {
|
||||
isPrimaryKey?: boolean;
|
||||
}
|
||||
|
||||
export function fillEditorColumnInfo(column: ColumnInfo, table: TableInfo): EditorColumnInfo {
|
||||
return {
|
||||
isPrimaryKey: !!(table.primaryKey && table.primaryKey.columns.find(x => x.columnName == column.columnName)),
|
||||
...column,
|
||||
};
|
||||
}
|
||||
|
||||
function processPrimaryKey(table: TableInfo, oldColumn: EditorColumnInfo, newColumn: EditorColumnInfo): TableInfo {
|
||||
if (!oldColumn?.isPrimaryKey && newColumn?.isPrimaryKey) {
|
||||
let primaryKey = table.primaryKey;
|
||||
if (!primaryKey) {
|
||||
primaryKey = {
|
||||
constraintType: 'primaryKey',
|
||||
pureName: table.pureName,
|
||||
schemaName: table.schemaName,
|
||||
columns: [],
|
||||
};
|
||||
}
|
||||
return {
|
||||
...table,
|
||||
primaryKey: {
|
||||
...primaryKey,
|
||||
columns: [
|
||||
...primaryKey.columns,
|
||||
{
|
||||
columnName: newColumn.columnName,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (oldColumn?.isPrimaryKey && !newColumn?.isPrimaryKey) {
|
||||
let primaryKey = table.primaryKey;
|
||||
if (primaryKey) {
|
||||
primaryKey = {
|
||||
...primaryKey,
|
||||
columns: table.primaryKey.columns.filter(x => x.columnName != oldColumn.columnName),
|
||||
};
|
||||
if (primaryKey.columns.length == 0) {
|
||||
return {
|
||||
...table,
|
||||
primaryKey: null,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...table,
|
||||
primaryKey,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
export function editorAddColumn(table: TableInfo, column: EditorColumnInfo): TableInfo {
|
||||
let res = {
|
||||
...table,
|
||||
columns: [...table.columns, { ...column, pairingId: uuidv1() }],
|
||||
};
|
||||
|
||||
res = processPrimaryKey(res, null, column);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
export function editorModifyColumn(table: TableInfo, column: EditorColumnInfo): TableInfo {
|
||||
const oldColumn = table?.columns?.find(x => x.pairingId == column.pairingId);
|
||||
|
||||
let res = {
|
||||
...table,
|
||||
columns: table.columns.map(col => (col.pairingId == column.pairingId ? _omit(column, ['isPrimaryKey']) : col)),
|
||||
};
|
||||
res = processPrimaryKey(res, fillEditorColumnInfo(oldColumn, table), column);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
export function editorDeleteColumn(table: TableInfo, column: EditorColumnInfo): TableInfo {
|
||||
let res = {
|
||||
...table,
|
||||
columns: table.columns.filter(col => col.pairingId != column.pairingId),
|
||||
};
|
||||
|
||||
res = processPrimaryKey(res, column, null);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
export function editorAddConstraint(table: TableInfo, constraint: ConstraintInfo): TableInfo {
|
||||
const res = {
|
||||
...table,
|
||||
};
|
||||
|
||||
if (constraint.constraintType == 'primaryKey') {
|
||||
res.primaryKey = {
|
||||
pairingId: uuidv1(),
|
||||
...constraint,
|
||||
} as PrimaryKeyInfo;
|
||||
}
|
||||
|
||||
if (constraint.constraintType == 'foreignKey') {
|
||||
res.foreignKeys = [
|
||||
...(res.foreignKeys || []),
|
||||
{
|
||||
pairingId: uuidv1(),
|
||||
...constraint,
|
||||
} as ForeignKeyInfo,
|
||||
];
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
export function editorModifyConstraint(table: TableInfo, constraint: ConstraintInfo): TableInfo {
|
||||
const res = {
|
||||
...table,
|
||||
};
|
||||
|
||||
if (constraint.constraintType == 'primaryKey') {
|
||||
res.primaryKey = {
|
||||
...res.primaryKey,
|
||||
...constraint,
|
||||
};
|
||||
}
|
||||
|
||||
if (constraint.constraintType == 'foreignKey') {
|
||||
res.foreignKeys = table.foreignKeys.map(fk =>
|
||||
fk.pairingId == constraint.pairingId ? { ...fk, ...constraint } : fk
|
||||
);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
export function editorDeleteConstraint(table: TableInfo, constraint: ConstraintInfo): TableInfo {
|
||||
const res = {
|
||||
...table,
|
||||
};
|
||||
|
||||
if (constraint.constraintType == 'primaryKey') {
|
||||
res.primaryKey = null;
|
||||
}
|
||||
|
||||
if (constraint.constraintType == 'foreignKey') {
|
||||
res.foreignKeys = table.foreignKeys.filter(x => x.pairingId != constraint.pairingId);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { ColumnInfo, ConstraintInfo, TableInfo } from './dbinfo';
|
||||
|
||||
export interface AlterProcessor {
|
||||
createTable(table: TableInfo);
|
||||
dropTable(table: TableInfo);
|
||||
createColumn(column: ColumnInfo, constraints: ConstraintInfo[]);
|
||||
changeColumn(oldColumn: ColumnInfo, newColumn: ColumnInfo, constraints?: ConstraintInfo[]);
|
||||
dropColumn(column: ColumnInfo);
|
||||
createConstraint(constraint: ConstraintInfo);
|
||||
changeConstraint(oldConstraint: ConstraintInfo, newConstraint: ConstraintInfo);
|
||||
dropConstraint(constraint: ConstraintInfo);
|
||||
renameTable(table: TableInfo, newName: string);
|
||||
renameColumn(column: ColumnInfo, newName: string);
|
||||
renameConstraint(constraint: ConstraintInfo, newName: string);
|
||||
recreateTable(oldTable: TableInfo, newTable: TableInfo);
|
||||
}
|
||||
@@ -6,10 +6,13 @@ export interface NamedObjectInfo {
|
||||
export interface ColumnReference {
|
||||
columnName: string;
|
||||
refColumnName?: string;
|
||||
isIncludedColumn?: boolean;
|
||||
isDescending?: boolean;
|
||||
}
|
||||
|
||||
export interface ConstraintInfo extends NamedObjectInfo {
|
||||
constraintName: string;
|
||||
pairingId?: string;
|
||||
constraintName?: string;
|
||||
constraintType: 'primaryKey' | 'foreignKey' | 'index' | 'check' | 'unique';
|
||||
}
|
||||
|
||||
@@ -38,6 +41,7 @@ export interface CheckInfo extends ConstraintInfo {
|
||||
}
|
||||
|
||||
export interface ColumnInfo extends NamedObjectInfo {
|
||||
pairingId?: string;
|
||||
columnName: string;
|
||||
notNull: boolean;
|
||||
autoIncrement: boolean;
|
||||
@@ -53,6 +57,7 @@ export interface ColumnInfo extends NamedObjectInfo {
|
||||
}
|
||||
|
||||
export interface DatabaseObjectInfo extends NamedObjectInfo {
|
||||
pairingId?: string;
|
||||
objectId?: string;
|
||||
createDate?: string;
|
||||
modifyDate?: string;
|
||||
@@ -103,4 +108,5 @@ export interface DatabaseInfoObjects {
|
||||
|
||||
export interface DatabaseInfo extends DatabaseInfoObjects {
|
||||
schemas: SchemaInfo[];
|
||||
engine?: string;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export interface SqlDialect {
|
||||
rangeSelect?: boolean;
|
||||
limitSelect?: boolean;
|
||||
ilike?: boolean;
|
||||
rowNumberOverPaging?: boolean;
|
||||
stringEscapeChar: string;
|
||||
offsetFetchRangeSyntax?: boolean;
|
||||
@@ -10,4 +11,22 @@ export interface SqlDialect {
|
||||
anonymousPrimaryKey?: boolean;
|
||||
enableConstraintsPerTable?: boolean;
|
||||
nosql?: boolean; // mongo
|
||||
|
||||
dropColumnDependencies?: string[];
|
||||
changeColumnDependencies?: string[];
|
||||
|
||||
dropIndexContainsTableSpec?: boolean;
|
||||
|
||||
createColumn?: boolean;
|
||||
dropColumn?: boolean;
|
||||
createIndex?: boolean;
|
||||
dropIndex?: boolean;
|
||||
createForeignKey?: boolean;
|
||||
dropForeignKey?: boolean;
|
||||
createPrimaryKey?: boolean;
|
||||
dropPrimaryKey?: boolean;
|
||||
createUnique?: boolean;
|
||||
dropUnique?: boolean;
|
||||
createCheck?: boolean;
|
||||
dropCheck?: boolean;
|
||||
}
|
||||
|
||||
@@ -42,3 +42,4 @@ export * from './dialect';
|
||||
export * from './dumper';
|
||||
export * from './dbtypes';
|
||||
export * from './extensions';
|
||||
export * from './alter-processor';
|
||||
|
||||
@@ -55,6 +55,10 @@ body {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.col-10 {
|
||||
flex-basis: 83.3333%;
|
||||
max-width: 83.3333%;
|
||||
}
|
||||
.col-9 {
|
||||
flex-basis: 75%;
|
||||
max-width: 75%;
|
||||
@@ -63,10 +67,18 @@ body {
|
||||
flex-basis: 66.6667%;
|
||||
max-width: 66.6667%;
|
||||
}
|
||||
.col-7 {
|
||||
flex-basis: 58.3333%;
|
||||
max-width: 58.3333%;
|
||||
}
|
||||
.col-6 {
|
||||
flex-basis: 50%;
|
||||
max-width: 50%;
|
||||
}
|
||||
.col-5 {
|
||||
flex-basis: 41.6667%;
|
||||
max-width: 41.6667%;
|
||||
}
|
||||
.col-4 {
|
||||
flex-basis: 33.3333%;
|
||||
max-width: 33.3333%;
|
||||
@@ -75,6 +87,10 @@ body {
|
||||
flex-basis: 25%;
|
||||
max-width: 25%;
|
||||
}
|
||||
.col-2 {
|
||||
flex-basis: 16.6666%;
|
||||
max-width: 16.6666%;
|
||||
}
|
||||
|
||||
.largeFormMarker input[type='text'] {
|
||||
width: 100%;
|
||||
|
||||
@@ -38,6 +38,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseUp(e) {
|
||||
if (e.button == 1) {
|
||||
dispatch('middleclick');
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
function setChecked(value) {
|
||||
if (!value && isChecked) {
|
||||
checkedObjectsStore.update(x => x.filter(y => module.extractKey(data) != module.extractKey(y)));
|
||||
@@ -53,6 +61,7 @@
|
||||
class:isBold
|
||||
draggable={true}
|
||||
on:click={handleClick}
|
||||
on:mouseup={handleMouseUp}
|
||||
use:contextMenu={disableContextMenu ? null : menu}
|
||||
on:dragstart={e => {
|
||||
e.dataTransfer.setData('app_object_drag_data', JSON.stringify(data));
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#each filtered as item (module.extractKey(item.data))}
|
||||
{#each filtered as item}
|
||||
<AppObjectListItem
|
||||
{...$$restProps}
|
||||
{module}
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
</script>
|
||||
|
||||
{#if groupFunc}
|
||||
{#each _.keys(groups) as group (group)}
|
||||
{#each _.keys(groups) as group}
|
||||
<AppObjectGroup
|
||||
{group}
|
||||
{module}
|
||||
@@ -52,7 +52,7 @@
|
||||
/>
|
||||
{/each}
|
||||
{:else}
|
||||
{#each filtered as data (module.extractKey(data))}
|
||||
{#each filtered as data}
|
||||
<AppObjectListItem
|
||||
{module}
|
||||
{subItemsComponent}
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
text: 'Connect',
|
||||
onClick: handleConnect,
|
||||
},
|
||||
{ onClick: handleNewQuery, text: 'New query' },
|
||||
{ onClick: handleNewQuery, text: 'New query', isNewQuery: true },
|
||||
$openedConnections.includes(data._id) &&
|
||||
data.status && {
|
||||
text: 'Refresh',
|
||||
@@ -190,4 +190,9 @@
|
||||
on:click={handleConnect}
|
||||
on:click
|
||||
on:expand
|
||||
on:middleclick={() => {
|
||||
_.flattenDeep(getContextMenu())
|
||||
.find(x => x.isNewQuery)
|
||||
.onClick();
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
};
|
||||
|
||||
return [
|
||||
{ onClick: handleNewQuery, text: 'New query' },
|
||||
{ onClick: handleNewQuery, text: 'New query', isNewQuery: true },
|
||||
{ onClick: handleImport, text: 'Import' },
|
||||
{ onClick: handleExport, text: 'Export' },
|
||||
{ onClick: handleSqlGenerator, text: 'SQL Generator' },
|
||||
@@ -68,7 +68,7 @@
|
||||
<script lang="ts">
|
||||
import getConnectionLabel from '../utility/getConnectionLabel';
|
||||
|
||||
import _ from 'lodash';
|
||||
import _, { find } from 'lodash';
|
||||
import ImportExportModal from '../modals/ImportExportModal.svelte';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
import SqlGeneratorModal from '../modals/SqlGeneratorModal.svelte';
|
||||
@@ -93,5 +93,10 @@
|
||||
isBold={_.get($currentDatabase, 'connection._id') == _.get(data.connection, '_id') &&
|
||||
_.get($currentDatabase, 'name') == data.name}
|
||||
on:click={() => ($currentDatabase = data)}
|
||||
on:middleclick={() => {
|
||||
createMenu()
|
||||
.find(x => x.isNewQuery)
|
||||
.onClick();
|
||||
}}
|
||||
menu={createMenu}
|
||||
/>
|
||||
|
||||
@@ -328,7 +328,6 @@
|
||||
{ forceNewTab }
|
||||
);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -351,7 +350,7 @@
|
||||
|
||||
export let data;
|
||||
|
||||
function handleClick() {
|
||||
function handleClick(forceNewTab = false) {
|
||||
const { schemaName, pureName, conid, database, objectTypeField } = data;
|
||||
|
||||
openDatabaseObjectDetail(
|
||||
@@ -364,7 +363,7 @@
|
||||
database,
|
||||
objectTypeField,
|
||||
},
|
||||
false,
|
||||
forceNewTab,
|
||||
null
|
||||
);
|
||||
|
||||
@@ -508,7 +507,6 @@
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<AppObjectCore
|
||||
@@ -518,6 +516,7 @@
|
||||
title={data.schemaName ? `${data.schemaName}.${data.pureName}` : data.pureName}
|
||||
icon={icons[data.objectTypeField]}
|
||||
menu={createMenu}
|
||||
on:click={handleClick}
|
||||
on:click={() => handleClick()}
|
||||
on:middleclick={() => handleClick(true)}
|
||||
on:expand
|
||||
/>
|
||||
|
||||
@@ -51,11 +51,11 @@
|
||||
}
|
||||
|
||||
:global(.theme-type-dark) .inner {
|
||||
--json-tree-string-color: #efc5c5;
|
||||
--json-tree-symbol-color: #efc5c5;
|
||||
--json-tree-boolean-color: #a6b3f5;
|
||||
--json-tree-function-color: #a6b3f5;
|
||||
--json-tree-number-color: #bfbdf2;
|
||||
--json-tree-string-color: #ffc5c5;
|
||||
--json-tree-symbol-color: #ffc5c5;
|
||||
--json-tree-boolean-color: #b6c3ff;
|
||||
--json-tree-function-color: #b6c3ff;
|
||||
--json-tree-number-color: #bfbdff;
|
||||
--json-tree-label-color: #e9aaed;
|
||||
--json-tree-arrow-color: #d4d4d4;
|
||||
--json-tree-null-color: #dcdcdc;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { currentTheme, extensions, getVisibleToolbar, visibleToolbar } from '../stores';
|
||||
import { currentDatabase, currentTheme, extensions, getVisibleToolbar, visibleToolbar } from '../stores';
|
||||
import registerCommand from './registerCommand';
|
||||
import { derived, get } from 'svelte/store';
|
||||
import { ThemeDefinition } from 'dbgate-types';
|
||||
@@ -17,6 +17,7 @@ import { getDefaultFileFormat } from '../plugins/fileformats';
|
||||
import { getCurrentConfig, getCurrentDatabase } from '../stores';
|
||||
import './recentDatabaseSwitch';
|
||||
import hasPermission from '../utility/hasPermission';
|
||||
import _ from 'lodash';
|
||||
|
||||
const electron = getElectron();
|
||||
|
||||
@@ -104,6 +105,37 @@ registerCommand({
|
||||
},
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'new.table',
|
||||
category: 'New',
|
||||
icon: 'img table',
|
||||
name: 'Table',
|
||||
toolbar: true,
|
||||
toolbarName: 'New table',
|
||||
onClick: () => {
|
||||
const $currentDatabase = get(currentDatabase);
|
||||
const connection = _.get($currentDatabase, 'connection') || {};
|
||||
const database = _.get($currentDatabase, 'name');
|
||||
|
||||
openNewTab(
|
||||
{
|
||||
title: 'Table #',
|
||||
icon: 'img table',
|
||||
tabComponent: 'TableStructureTab',
|
||||
props: {
|
||||
conid: connection._id,
|
||||
database,
|
||||
},
|
||||
},
|
||||
{
|
||||
editor: {
|
||||
columns: [],
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'new.markdown',
|
||||
category: 'New',
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import ColumnLabel from '../elements/ColumnLabel.svelte';
|
||||
import { isTypeDateTime } from 'dbgate-tools';
|
||||
import { openDatabaseObjectDetail } from '../appobj/DatabaseObjectAppObject.svelte';
|
||||
import { copyTextToClipboard } from '../utility/clipboard';
|
||||
|
||||
export let column;
|
||||
export let conid = undefined;
|
||||
@@ -27,19 +28,20 @@
|
||||
|
||||
function getMenu() {
|
||||
return [
|
||||
{ onClick: () => setSort('ASC'), text: 'Sort ascending' },
|
||||
{ onClick: () => setSort('DESC'), text: 'Sort descending' },
|
||||
setSort && { onClick: () => setSort('ASC'), text: 'Sort ascending' },
|
||||
setSort && { onClick: () => setSort('DESC'), text: 'Sort descending' },
|
||||
{ onClick: () => copyTextToClipboard(column.columnName), text: 'Copy column name' },
|
||||
|
||||
column.foreignKey && [{ divider: true }, { onClick: openReferencedTable, text: column.foreignKey.refTableName }],
|
||||
|
||||
{ divider: true },
|
||||
{ onClick: () => setGrouping('GROUP'), text: 'Group by' },
|
||||
{ onClick: () => setGrouping('MAX'), text: 'MAX' },
|
||||
{ onClick: () => setGrouping('MIN'), text: 'MIN' },
|
||||
{ onClick: () => setGrouping('SUM'), text: 'SUM' },
|
||||
{ onClick: () => setGrouping('AVG'), text: 'AVG' },
|
||||
{ onClick: () => setGrouping('COUNT'), text: 'COUNT' },
|
||||
{ onClick: () => setGrouping('COUNT DISTINCT'), text: 'COUNT DISTINCT' },
|
||||
setGrouping && { divider: true },
|
||||
setGrouping && { onClick: () => setGrouping('GROUP'), text: 'Group by' },
|
||||
setGrouping && { onClick: () => setGrouping('MAX'), text: 'MAX' },
|
||||
setGrouping && { onClick: () => setGrouping('MIN'), text: 'MIN' },
|
||||
setGrouping && { onClick: () => setGrouping('SUM'), text: 'SUM' },
|
||||
setGrouping && { onClick: () => setGrouping('AVG'), text: 'AVG' },
|
||||
setGrouping && { onClick: () => setGrouping('COUNT'), text: 'COUNT' },
|
||||
setGrouping && { onClick: () => setGrouping('COUNT DISTINCT'), text: 'COUNT DISTINCT' },
|
||||
|
||||
isTypeDateTime(column.dataType) && [
|
||||
{ divider: true },
|
||||
|
||||
@@ -66,8 +66,6 @@
|
||||
import _ from 'lodash';
|
||||
import registerCommand from '../commands/registerCommand';
|
||||
import { registerMenu } from '../utility/contextMenu';
|
||||
import { useSettings } from '../utility/metadataLoaders';
|
||||
import { getCurrentSettings } from '../stores';
|
||||
import { getBoolSettingsValue } from '../settings/settingsTools';
|
||||
|
||||
export let config;
|
||||
|
||||
@@ -12,14 +12,31 @@
|
||||
return value;
|
||||
}
|
||||
|
||||
const dateTimeRegex = /^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d\d\d)?Z?$/;
|
||||
// const dateTimeRegex = /^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d\d\d)?Z?$/;
|
||||
const dateTimeRegex = /^([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(([Zz])|()|([\+|\-]([01][0-9]|2[0-3]):[0-5][0-9]))$/;
|
||||
|
||||
function formatNumber(value) {
|
||||
if (value >= 10000 || value <= -10000) {
|
||||
if (getBoolSettingsValue('dataGrid.thousandsSeparator', false)) {
|
||||
return value.toLocaleString();
|
||||
} else {
|
||||
return value.toString();
|
||||
}
|
||||
}
|
||||
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
function formatDateTime(testedString) {
|
||||
const m = testedString.match(dateTimeRegex);
|
||||
return `${m[1]}-${m[2]}-${m[3]} ${m[4]}:${m[5]}:${m[6]}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import moment from 'moment';
|
||||
import _ from 'lodash';
|
||||
import { isTypeLogical } from 'dbgate-tools';
|
||||
import ShowFormButton from '../formview/ShowFormButton.svelte';
|
||||
import { getBoolSettingsValue } from '../settings/settingsTools';
|
||||
|
||||
export let rowIndex;
|
||||
export let col;
|
||||
@@ -66,34 +83,18 @@
|
||||
{:else if value === undefined}
|
||||
<span class="null">(No field)</span>
|
||||
{:else if _.isDate(value)}
|
||||
{moment(value).format('YYYY-MM-DD HH:mm:ss')}
|
||||
{value.toString()}
|
||||
{:else if value === true}
|
||||
{#if isDynamicStructure}
|
||||
<span class="value">true</span>
|
||||
{:else}
|
||||
1
|
||||
{/if}
|
||||
<span class="value">true</span>
|
||||
{:else if value === false}
|
||||
{#if isDynamicStructure}
|
||||
<span class="value">false</span>
|
||||
{:else}
|
||||
0
|
||||
{/if}
|
||||
<span class="value">false</span>
|
||||
{:else if _.isNumber(value)}
|
||||
{#if value >= 10000 || value <= -10000}
|
||||
{#if isDynamicStructure}
|
||||
<span class="value">{value.toLocaleString()}</span>
|
||||
{:else}
|
||||
{value.toLocaleString()}
|
||||
{/if}
|
||||
{:else if isDynamicStructure}
|
||||
<span class="value">{value.toString()}</span>
|
||||
{:else}
|
||||
{value.toString()}
|
||||
{/if}
|
||||
<span class="value">{formatNumber(value)}</span>
|
||||
{:else if _.isString(value)}
|
||||
{#if dateTimeRegex.test(value)}
|
||||
{moment(value).format('YYYY-MM-DD HH:mm:ss')}
|
||||
<span class="value">
|
||||
{formatDateTime(value)}
|
||||
</span>
|
||||
{:else}
|
||||
{highlightSpecialCharacters(value)}
|
||||
{/if}
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
name: 'Set NULL',
|
||||
keyText: 'Ctrl+0',
|
||||
testEnabled: () => getCurrentDataGrid()?.getGrider()?.editable,
|
||||
onClick: () => getCurrentDataGrid().setNull(),
|
||||
onClick: () => getCurrentDataGrid().setFixedValue(null),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
@@ -171,7 +171,6 @@
|
||||
if (_.isPlainObject(value) || _.isArray(value)) return JSON.stringify(value);
|
||||
return value;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -310,10 +309,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
export function setNull() {
|
||||
export function setFixedValue(value) {
|
||||
grider.beginUpdate();
|
||||
selectedCells.filter(isRegularCell).forEach(cell => {
|
||||
setCellValue(cell, null);
|
||||
setCellValue(cell, value);
|
||||
});
|
||||
grider.endUpdate();
|
||||
}
|
||||
@@ -666,43 +665,52 @@
|
||||
|
||||
function handleGridWheel(event) {
|
||||
if (event.shiftKey) {
|
||||
let newFirstVisibleColumnScrollIndex = firstVisibleColumnScrollIndex;
|
||||
if (event.deltaY > 0) {
|
||||
newFirstVisibleColumnScrollIndex++;
|
||||
}
|
||||
if (event.deltaY < 0) {
|
||||
newFirstVisibleColumnScrollIndex--;
|
||||
}
|
||||
if (newFirstVisibleColumnScrollIndex > maxScrollColumn) {
|
||||
newFirstVisibleColumnScrollIndex = maxScrollColumn;
|
||||
}
|
||||
if (newFirstVisibleColumnScrollIndex < 0) {
|
||||
newFirstVisibleColumnScrollIndex = 0;
|
||||
}
|
||||
firstVisibleColumnScrollIndex = newFirstVisibleColumnScrollIndex;
|
||||
|
||||
domHorizontalScroll.scroll(newFirstVisibleColumnScrollIndex);
|
||||
scrollHorizontal(event.deltaY, event.deltaX);
|
||||
} else {
|
||||
let newFirstVisibleRowScrollIndex = firstVisibleRowScrollIndex;
|
||||
if (event.deltaY > 0) {
|
||||
newFirstVisibleRowScrollIndex += wheelRowCount;
|
||||
}
|
||||
if (event.deltaY < 0) {
|
||||
newFirstVisibleRowScrollIndex -= wheelRowCount;
|
||||
}
|
||||
let rowCount = grider.rowCount;
|
||||
if (newFirstVisibleRowScrollIndex + visibleRowCountLowerBound > rowCount) {
|
||||
newFirstVisibleRowScrollIndex = rowCount - visibleRowCountLowerBound + 1;
|
||||
}
|
||||
if (newFirstVisibleRowScrollIndex < 0) {
|
||||
newFirstVisibleRowScrollIndex = 0;
|
||||
}
|
||||
firstVisibleRowScrollIndex = newFirstVisibleRowScrollIndex;
|
||||
|
||||
domVerticalScroll.scroll(newFirstVisibleRowScrollIndex);
|
||||
scrollHorizontal(event.deltaX, event.deltaY);
|
||||
scrollVertical(event.deltaX, event.deltaY);
|
||||
}
|
||||
}
|
||||
|
||||
function scrollVertical(deltaX, deltaY) {
|
||||
let newFirstVisibleRowScrollIndex = firstVisibleRowScrollIndex;
|
||||
if (deltaY > 0 && deltaX === -0) {
|
||||
newFirstVisibleRowScrollIndex += wheelRowCount;
|
||||
} else if (deltaY < 0 && deltaX === -0) {
|
||||
newFirstVisibleRowScrollIndex -= wheelRowCount;
|
||||
}
|
||||
|
||||
let rowCount = grider.rowCount;
|
||||
if (newFirstVisibleRowScrollIndex + visibleRowCountLowerBound > rowCount) {
|
||||
newFirstVisibleRowScrollIndex = rowCount - visibleRowCountLowerBound + 1;
|
||||
}
|
||||
if (newFirstVisibleRowScrollIndex < 0) {
|
||||
newFirstVisibleRowScrollIndex = 0;
|
||||
}
|
||||
|
||||
firstVisibleRowScrollIndex = newFirstVisibleRowScrollIndex;
|
||||
domVerticalScroll.scroll(newFirstVisibleRowScrollIndex);
|
||||
}
|
||||
|
||||
function scrollHorizontal(deltaX, deltaY) {
|
||||
let newFirstVisibleColumnScrollIndex = firstVisibleColumnScrollIndex;
|
||||
if (deltaX > 0 && deltaY === -0) {
|
||||
newFirstVisibleColumnScrollIndex++;
|
||||
} else if (deltaX < 0 && deltaY === -0) {
|
||||
newFirstVisibleColumnScrollIndex--;
|
||||
}
|
||||
|
||||
if (newFirstVisibleColumnScrollIndex > maxScrollColumn) {
|
||||
newFirstVisibleColumnScrollIndex = maxScrollColumn;
|
||||
}
|
||||
if (newFirstVisibleColumnScrollIndex < 0) {
|
||||
newFirstVisibleColumnScrollIndex = 0;
|
||||
}
|
||||
|
||||
firstVisibleColumnScrollIndex = newFirstVisibleColumnScrollIndex;
|
||||
domHorizontalScroll.scroll(newFirstVisibleColumnScrollIndex);
|
||||
}
|
||||
|
||||
function getSelectedRowIndexes() {
|
||||
if (selectedCells.find(x => x[0] == 'header')) return _.range(0, grider.rowCount);
|
||||
return _.uniq((selectedCells || []).map(x => x[0])).filter(x => _.isNumber(x));
|
||||
@@ -733,6 +741,7 @@
|
||||
!event.altKey &&
|
||||
((event.keyCode >= keycodes.a && event.keyCode <= keycodes.z) ||
|
||||
(event.keyCode >= keycodes.n0 && event.keyCode <= keycodes.n9) ||
|
||||
(event.keyCode >= keycodes.numPad0 && event.keyCode <= keycodes.numPad9) ||
|
||||
event.keyCode == keycodes.dash)
|
||||
) {
|
||||
// @ts-ignore
|
||||
@@ -979,7 +988,6 @@
|
||||
);
|
||||
|
||||
const menu = getContextMenu();
|
||||
|
||||
</script>
|
||||
|
||||
{#if !display || (!isDynamicStructure && (!columns || columns.length == 0))}
|
||||
@@ -1000,7 +1008,13 @@
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="container" bind:clientWidth={containerWidth} bind:clientHeight={containerHeight} use:contextMenu={menu}>
|
||||
<div
|
||||
class="container"
|
||||
bind:clientWidth={containerWidth}
|
||||
bind:clientHeight={containerHeight}
|
||||
use:contextMenu={menu}
|
||||
on:wheel={handleGridWheel}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
class="focus-field"
|
||||
@@ -1018,7 +1032,6 @@
|
||||
on:mousedown={handleGridMouseDown}
|
||||
on:mousemove={handleGridMouseMove}
|
||||
on:mouseup={handleGridMouseUp}
|
||||
on:wheel={handleGridWheel}
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -1050,7 +1063,7 @@
|
||||
// @ts-ignore
|
||||
display.resizeColumn(col.uniqueName, col.width, e.detail);
|
||||
}}
|
||||
setGrouping={display.sortable ? groupFunc => display.setGrouping(col.uniqueName, groupFunc) : null}
|
||||
setGrouping={display.groupable ? groupFunc => display.setGrouping(col.uniqueName, groupFunc) : null}
|
||||
grouping={display.getGrouping(col.uniqueName)}
|
||||
/>
|
||||
</td>
|
||||
@@ -1186,5 +1199,4 @@
|
||||
right: 40px;
|
||||
bottom: 20px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -7,11 +7,14 @@
|
||||
|
||||
export let collection;
|
||||
export let title;
|
||||
export let clickable;
|
||||
</script>
|
||||
|
||||
<ObjectListControl
|
||||
{collection}
|
||||
{title}
|
||||
{clickable}
|
||||
on:clickrow
|
||||
columns={[
|
||||
{
|
||||
fieldName: 'baseColumns',
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
import TableControl from './TableControl.svelte';
|
||||
|
||||
export let title;
|
||||
export let nameComponent;
|
||||
export let collection;
|
||||
export let columns;
|
||||
export let showIfEmpty = false;
|
||||
export let clickable;
|
||||
|
||||
</script>
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
},
|
||||
...columns,
|
||||
]}
|
||||
{clickable}
|
||||
on:clickrow
|
||||
>
|
||||
<svelte:fragment slot="-1" let:row>
|
||||
<slot name="name" {row} />
|
||||
@@ -76,4 +78,5 @@
|
||||
.body {
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
|
||||
export let label;
|
||||
export let name;
|
||||
export let disabled;
|
||||
export let disabled = false;
|
||||
export let templateProps = {};
|
||||
|
||||
const { template, setFieldValue, values } = getFormContext();
|
||||
|
||||
</script>
|
||||
|
||||
<svelte:component
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { getFormContext } from './FormProviderCore.svelte';
|
||||
import FormDropDownTextFieldRaw from './FormDropDownTextFieldRaw.svelte';
|
||||
|
||||
export let label;
|
||||
export let name;
|
||||
export let templateProps = {};
|
||||
|
||||
const { template } = getFormContext();
|
||||
</script>
|
||||
|
||||
<svelte:component this={template} type="text" {label} {...templateProps}>
|
||||
<FormDropDownTextFieldRaw {name} {...$$restProps} />
|
||||
</svelte:component>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import FontIcon from '../icons/FontIcon.svelte';
|
||||
|
||||
import InlineButton from '../elements/InlineButton.svelte';
|
||||
|
||||
import { getFormContext } from './FormProviderCore.svelte';
|
||||
import TextField from './TextField.svelte';
|
||||
import DropDownButton from '../elements/DropDownButton.svelte';
|
||||
|
||||
export let name;
|
||||
export let disabled = false;
|
||||
export let defaultValue;
|
||||
export let menu;
|
||||
|
||||
const { values, setFieldValue } = getFormContext();
|
||||
|
||||
let showPassword = false;
|
||||
|
||||
$: value = $values[name];
|
||||
$: isCrypted = value && value.startsWith('crypt:');
|
||||
|
||||
</script>
|
||||
|
||||
<div class="flex">
|
||||
<TextField
|
||||
{...$$restProps}
|
||||
value={$values[name] ?? defaultValue}
|
||||
on:input={e => setFieldValue(name, e.target['value'])}
|
||||
/>
|
||||
<DropDownButton {menu} {disabled} />
|
||||
</div>
|
||||
@@ -17,6 +17,7 @@
|
||||
const filePaths = electron.remote.dialog.showOpenDialogSync(electron.remote.getCurrentWindow(), {
|
||||
defaultPath: values[name],
|
||||
properties: ['showHiddenFiles'],
|
||||
filters: [{ name: 'All Files', extensions: ['*'] }],
|
||||
});
|
||||
const filePath = filePaths && filePaths[0];
|
||||
if (filePath) setFieldValue(name, filePath);
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
export let value;
|
||||
export let isNative = false;
|
||||
export let isMulti = false;
|
||||
export let notSelected = null;
|
||||
|
||||
let listOpen = false;
|
||||
let isFocused = false;
|
||||
@@ -25,6 +26,11 @@
|
||||
dispatch('change', e.target['value']);
|
||||
}}
|
||||
>
|
||||
{#if notSelected}
|
||||
<option value="" selected={!value}>
|
||||
{_.isString(notSelected) ? notSelected : '(not selected)'}
|
||||
</option>
|
||||
{/if}
|
||||
{#each _.compact(options) as x (x.value)}
|
||||
<option value={x.value} selected={value == x.value}>
|
||||
{x.label}
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
name: 'Set NULL',
|
||||
keyText: 'Ctrl+0',
|
||||
testEnabled: () => getCurrentDataForm() != null,
|
||||
onClick: () => getCurrentDataForm().setNull(),
|
||||
onClick: () => getCurrentDataForm().setFixedValue(null),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
@@ -247,9 +247,9 @@
|
||||
// if (onSave) onSave();
|
||||
// }
|
||||
|
||||
export function setNull() {
|
||||
export function setFixedValue(value) {
|
||||
if (isDataCell(currentCell)) {
|
||||
setCellValue(currentCell, null);
|
||||
setCellValue(currentCell, value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +70,8 @@
|
||||
'icon menu-right': 'mdi mdi-menu-right',
|
||||
'icon plugin': 'mdi mdi-toy-brick',
|
||||
'icon menu': 'mdi mdi-menu',
|
||||
'icon add-column': 'mdi mdi-table-column-plus-after',
|
||||
'icon add-key': 'mdi mdi-key-plus',
|
||||
|
||||
'img ok': 'mdi mdi-check-circle color-icon-green',
|
||||
'img ok-inv': 'mdi mdi-check-circle color-icon-inv-green',
|
||||
@@ -113,6 +115,7 @@
|
||||
'img filter': 'mdi mdi-filter',
|
||||
'img group': 'mdi mdi-group',
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<span class={iconNames[icon] || icon} {title} on:click />
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import clickOutside from '../utility/clickOutside';
|
||||
import keycodes from '../utility/keycodes';
|
||||
import { onMount } from 'svelte';
|
||||
import { currentDropDownMenu } from '../stores';
|
||||
|
||||
export let fullScreen = false;
|
||||
export let noPadding = false;
|
||||
@@ -16,6 +17,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
function handleClickOutside() {
|
||||
if ($currentDropDownMenu) return;
|
||||
handleCloseModal();
|
||||
}
|
||||
|
||||
function handleEscape(e) {
|
||||
if (e.keyCode == keycodes.escape) {
|
||||
handleCloseModal();
|
||||
@@ -28,12 +34,13 @@
|
||||
if (oldFocus) oldFocus.focus();
|
||||
};
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<!-- The Modal -->
|
||||
<div id="myModal" class="bglayer">
|
||||
<!-- Modal content -->
|
||||
<div class="window" class:fullScreen class:simple use:clickOutside on:clickOutside={handleCloseModal}>
|
||||
<div class="window" class:fullScreen class:simple use:clickOutside on:clickOutside={handleClickOutside}>
|
||||
{#if $$slots.header}
|
||||
<div class="header" class:fullScreen>
|
||||
<div><slot name="header" /></div>
|
||||
@@ -153,4 +160,5 @@
|
||||
border-top: 1px solid var(--theme-border);
|
||||
background-color: var(--theme-bg-modalheader);
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -15,6 +15,13 @@ const jsonFormat = {
|
||||
writerFunc: 'jsonArrayWriter',
|
||||
};
|
||||
|
||||
const sqlFormat = {
|
||||
storageType: 'sql',
|
||||
extension: 'sql',
|
||||
name: 'SQL',
|
||||
writerFunc: 'sqlDataWriter',
|
||||
};
|
||||
|
||||
const jsonlQuickExport = {
|
||||
label: 'JSON lines',
|
||||
extension: 'jsonl',
|
||||
@@ -37,8 +44,19 @@ const jsonQuickExport = {
|
||||
}),
|
||||
};
|
||||
|
||||
const sqlQuickExport = {
|
||||
label: 'SQL',
|
||||
extension: 'sql',
|
||||
createWriter: fileName => ({
|
||||
functionName: 'sqlDataWriter',
|
||||
props: {
|
||||
fileName,
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
export function buildFileFormats(plugins): FileFormatDefinition[] {
|
||||
const res = [jsonlFormat, jsonFormat];
|
||||
const res = [jsonlFormat, jsonFormat, sqlFormat];
|
||||
for (const { content } of plugins) {
|
||||
const { fileFormats } = content;
|
||||
if (fileFormats) res.push(...fileFormats);
|
||||
@@ -47,7 +65,7 @@ export function buildFileFormats(plugins): FileFormatDefinition[] {
|
||||
}
|
||||
|
||||
export function buildQuickExports(plugins): QuickExportDefinition[] {
|
||||
const res = [jsonQuickExport, jsonlQuickExport];
|
||||
const res = [jsonQuickExport, jsonlQuickExport, sqlQuickExport];
|
||||
for (const { content } of plugins) {
|
||||
if (content.quickExports) res.push(...content.quickExports);
|
||||
}
|
||||
|
||||
@@ -69,11 +69,16 @@
|
||||
.main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
position: relative;
|
||||
overflow-y: scroll;
|
||||
background-color: var(--theme-bg-0);
|
||||
}
|
||||
table {
|
||||
flex: 1;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
border-spacing: 0;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
@@ -46,6 +46,8 @@
|
||||
/>
|
||||
<FormCheckboxField name="dataGrid.showHintColumns" label="Show foreign key hints" defaultValue={true} />
|
||||
|
||||
<FormCheckboxField name="dataGrid.thousandsSeparator" label="Use thousands separator for numbers" />
|
||||
|
||||
<div class="heading">Connection</div>
|
||||
<FormCheckboxField
|
||||
name="connection.autoRefresh"
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
<script lang="ts">
|
||||
import uuidv1 from 'uuid/v1';
|
||||
import FormStyledButton from '../elements/FormStyledButton.svelte';
|
||||
|
||||
import FormSelectField from '../forms/FormSelectField.svelte';
|
||||
import FormTextField from '../forms/FormTextField.svelte';
|
||||
import FormCheckboxField from '../forms/FormCheckboxField.svelte';
|
||||
|
||||
import FormProvider from '../forms/FormProvider.svelte';
|
||||
import FormSubmit from '../forms/FormSubmit.svelte';
|
||||
import ModalBase from '../modals/ModalBase.svelte';
|
||||
import { closeCurrentModal } from '../modals/modalTools';
|
||||
import ElectronFilesInput from '../impexp/ElectronFilesInput.svelte';
|
||||
import DropDownButton from '../elements/DropDownButton.svelte';
|
||||
import DataTypeEditor from './DataTypeEditor.svelte';
|
||||
import { editorAddColumn, editorDeleteColumn, editorModifyColumn, fillEditorColumnInfo } from 'dbgate-tools';
|
||||
|
||||
export let columnInfo;
|
||||
export let setTableInfo;
|
||||
export let tableInfo;
|
||||
export let onAddNext;
|
||||
|
||||
</script>
|
||||
|
||||
<FormProvider initialValues={fillEditorColumnInfo(columnInfo, tableInfo)}>
|
||||
<ModalBase {...$$restProps}>
|
||||
<svelte:fragment slot="header"
|
||||
>{columnInfo ? 'Edit column' : `Add column ${(tableInfo?.columns || []).length + 1}`}</svelte:fragment
|
||||
>
|
||||
|
||||
<FormTextField name="columnName" label="Column name" focused />
|
||||
<DataTypeEditor />
|
||||
|
||||
<FormCheckboxField name="notNull" label="NOT NULL" />
|
||||
<FormCheckboxField name="isPrimaryKey" label="Is Primary Key" />
|
||||
<FormCheckboxField name="autoIncrement" label="Is Autoincrement" />
|
||||
<FormTextField name="defaultValue" label="Default value" />
|
||||
<FormTextField name="computedExpression" label="Computed expression" />
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<FormSubmit
|
||||
value={columnInfo ? 'Save' : 'Save and next'}
|
||||
on:click={e => {
|
||||
closeCurrentModal();
|
||||
if (columnInfo) {
|
||||
setTableInfo(tbl => editorModifyColumn(tbl, e.detail));
|
||||
} else {
|
||||
setTableInfo(tbl => editorAddColumn(tbl, e.detail));
|
||||
if (onAddNext) onAddNext();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{#if !columnInfo}
|
||||
<FormStyledButton
|
||||
type="button"
|
||||
value="Save"
|
||||
on:click={e => {
|
||||
closeCurrentModal();
|
||||
setTableInfo(tbl => editorAddColumn(tbl, e.detail));
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<FormStyledButton type="button" value="Close" on:click={closeCurrentModal} />
|
||||
{#if columnInfo}
|
||||
<FormStyledButton
|
||||
type="button"
|
||||
value="Remove"
|
||||
on:click={() => {
|
||||
closeCurrentModal();
|
||||
setTableInfo(tbl => editorDeleteColumn(tbl, columnInfo));
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</ModalBase>
|
||||
</FormProvider>
|
||||
@@ -0,0 +1,165 @@
|
||||
<script lang="ts">
|
||||
import FormStyledButton from '../elements/FormStyledButton.svelte';
|
||||
import uuidv1 from 'uuid/v1';
|
||||
|
||||
import FormSelectField from '../forms/FormSelectField.svelte';
|
||||
import FormTextField from '../forms/FormTextField.svelte';
|
||||
import FormCheckboxField from '../forms/FormCheckboxField.svelte';
|
||||
|
||||
import FormProvider from '../forms/FormProvider.svelte';
|
||||
import FormSubmit from '../forms/FormSubmit.svelte';
|
||||
import ModalBase from '../modals/ModalBase.svelte';
|
||||
import { closeCurrentModal } from '../modals/modalTools';
|
||||
import ElectronFilesInput from '../impexp/ElectronFilesInput.svelte';
|
||||
import DropDownButton from '../elements/DropDownButton.svelte';
|
||||
import DataTypeEditor from './DataTypeEditor.svelte';
|
||||
import { editorAddConstraint, editorDeleteConstraint, editorModifyConstraint } from 'dbgate-tools';
|
||||
import TextField from '../forms/TextField.svelte';
|
||||
import SelectField from '../forms/SelectField.svelte';
|
||||
|
||||
export let constraintInfo;
|
||||
export let setTableInfo;
|
||||
export let tableInfo;
|
||||
export let constraintLabel;
|
||||
export let constraintType;
|
||||
|
||||
let constraintName = constraintInfo?.constraintName;
|
||||
let columns = constraintInfo?.columns || [];
|
||||
|
||||
function getConstraint() {
|
||||
return {
|
||||
pairingId: uuidv1(),
|
||||
...constraintInfo,
|
||||
columns,
|
||||
pureName: tableInfo.pureName,
|
||||
schemaName: tableInfo.schemaName,
|
||||
constraintName,
|
||||
constraintType,
|
||||
};
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<ModalBase {...$$restProps}>
|
||||
<svelte:fragment slot="header"
|
||||
>{constraintInfo ? `Edit ${constraintLabel}` : `Add ${constraintLabel}`}</svelte:fragment
|
||||
>
|
||||
|
||||
<div class="largeFormMarker">
|
||||
<div class="row">
|
||||
<div class="label col-3">Constraint name</div>
|
||||
<div class="col-9">
|
||||
<TextField value={constraintName} on:input={e => (constraintName = e.target['value'])} focused />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#each columns as column, index}
|
||||
<div class="row">
|
||||
<div class="label col-3">Column {index + 1}</div>
|
||||
<div class="col-6">
|
||||
{#key column.columnName}
|
||||
<SelectField
|
||||
value={column.columnName}
|
||||
isNative
|
||||
options={tableInfo.columns.map(col => ({
|
||||
label: col.columnName,
|
||||
value: col.columnName,
|
||||
}))}
|
||||
on:change={e => {
|
||||
if (e.detail) {
|
||||
columns = columns.map((col, i) => (i == index ? { columnName: e.detail } : col));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
<div class="col-3 button">
|
||||
<FormStyledButton
|
||||
value="Delete"
|
||||
on:click={e => {
|
||||
const x = [...columns];
|
||||
x.splice(index, 1);
|
||||
columns = x;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<div class="row">
|
||||
<div class="label col-3">Add new column</div>
|
||||
<div class="col-9">
|
||||
{#key columns.length}
|
||||
<SelectField
|
||||
placeholder="Select column"
|
||||
value={''}
|
||||
on:change={e => {
|
||||
if (e.detail)
|
||||
columns = [
|
||||
...columns,
|
||||
{
|
||||
columnName: e.detail,
|
||||
},
|
||||
];
|
||||
}}
|
||||
isNative
|
||||
options={[
|
||||
{
|
||||
label: 'Choose column',
|
||||
value: '',
|
||||
},
|
||||
...tableInfo.columns.map(col => ({
|
||||
label: col.columnName,
|
||||
value: col.columnName,
|
||||
})),
|
||||
]}
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<FormStyledButton
|
||||
value={'Save'}
|
||||
on:click={() => {
|
||||
closeCurrentModal();
|
||||
if (constraintInfo) {
|
||||
setTableInfo(tbl => editorModifyConstraint(tbl, getConstraint()));
|
||||
} else {
|
||||
setTableInfo(tbl => editorAddConstraint(tbl, getConstraint()));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormStyledButton type="button" value="Close" on:click={closeCurrentModal} />
|
||||
{#if constraintInfo}
|
||||
<FormStyledButton
|
||||
type="button"
|
||||
value="Remove"
|
||||
on:click={() => {
|
||||
closeCurrentModal();
|
||||
setTableInfo(tbl => editorDeleteConstraint(tbl, constraintInfo));
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</ModalBase>
|
||||
|
||||
<style>
|
||||
.row {
|
||||
margin: var(--dim-large-form-margin);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.row .label {
|
||||
white-space: nowrap;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.button {
|
||||
align-self: center;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import FormDropDownTextField from '../forms/FormDropDownTextField.svelte';
|
||||
import { getFormContext } from '../forms/FormProviderCore.svelte';
|
||||
|
||||
const { values, setFieldValue } = getFormContext();
|
||||
|
||||
$: dataTypes = ['int', 'nvarchar(250)', 'datetime', 'numeric(10,2)', 'float'];
|
||||
|
||||
function createDataTypesMenu() {
|
||||
return dataTypes.map(type => ({
|
||||
text: type,
|
||||
onClick: () => setFieldValue('dataType', type),
|
||||
}));
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<FormDropDownTextField name="dataType" label="Data type" menu={createDataTypesMenu} />
|
||||
@@ -0,0 +1,239 @@
|
||||
<script lang="ts">
|
||||
import FormStyledButton from '../elements/FormStyledButton.svelte';
|
||||
import uuidv1 from 'uuid/v1';
|
||||
|
||||
import FormSelectField from '../forms/FormSelectField.svelte';
|
||||
import FormTextField from '../forms/FormTextField.svelte';
|
||||
import FormCheckboxField from '../forms/FormCheckboxField.svelte';
|
||||
|
||||
import FormProvider from '../forms/FormProvider.svelte';
|
||||
import FormSubmit from '../forms/FormSubmit.svelte';
|
||||
import ModalBase from '../modals/ModalBase.svelte';
|
||||
import { closeCurrentModal } from '../modals/modalTools';
|
||||
import ElectronFilesInput from '../impexp/ElectronFilesInput.svelte';
|
||||
import DropDownButton from '../elements/DropDownButton.svelte';
|
||||
import DataTypeEditor from './DataTypeEditor.svelte';
|
||||
import {
|
||||
editorAddConstraint,
|
||||
editorDeleteConstraint,
|
||||
editorModifyConstraint,
|
||||
fullNameFromString,
|
||||
fullNameToLabel,
|
||||
fullNameToString,
|
||||
} from 'dbgate-tools';
|
||||
import TextField from '../forms/TextField.svelte';
|
||||
import SelectField from '../forms/SelectField.svelte';
|
||||
import _ from 'lodash';
|
||||
|
||||
export let constraintInfo;
|
||||
export let setTableInfo;
|
||||
export let tableInfo;
|
||||
export let dbInfo;
|
||||
|
||||
const foreignKeyActions = ['noAction', 'cascade', 'restrict', 'setNull'];
|
||||
const foreignKeyActionsOptions = foreignKeyActions.map(x => ({
|
||||
label: _.startCase(x),
|
||||
value: x,
|
||||
}));
|
||||
|
||||
let constraintName = constraintInfo?.constraintName;
|
||||
let columns = constraintInfo?.columns || [];
|
||||
let refTableName = constraintInfo?.refTableName;
|
||||
let refSchemaName = constraintInfo?.refSchemaName;
|
||||
let deleteAction = constraintInfo?.deleteAction ? _.camelCase(constraintInfo?.deleteAction) : null;
|
||||
let updateAction = constraintInfo?.updateAction ? _.camelCase(constraintInfo?.updateAction) : null;
|
||||
|
||||
$: refTableInfo = dbInfo?.tables?.find(x => x.pureName == refTableName && x.schemaName == refSchemaName);
|
||||
|
||||
function getConstraint() {
|
||||
return {
|
||||
pairingId: uuidv1(),
|
||||
...constraintInfo,
|
||||
columns,
|
||||
pureName: tableInfo.pureName,
|
||||
schemaName: tableInfo.schemaName,
|
||||
constraintName,
|
||||
constraintType: 'foreignKey',
|
||||
refTableName,
|
||||
refSchemaName,
|
||||
deleteAction: _.startCase(deleteAction).toUpperCase(),
|
||||
updateAction: _.startCase(updateAction).toUpperCase(),
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModalBase {...$$restProps}>
|
||||
<svelte:fragment slot="header">{constraintInfo ? `Edit foreign key` : `Add foreign key`}</svelte:fragment>
|
||||
|
||||
<div class="largeFormMarker">
|
||||
<div class="row">
|
||||
<div class="label col-3">Constraint name</div>
|
||||
<div class="col-9">
|
||||
<TextField value={constraintName} on:input={e => (constraintName = e.target['value'])} focused />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="label col-3">Referenced table</div>
|
||||
<div class="col-9">
|
||||
<SelectField
|
||||
value={fullNameToString({ pureName: refTableName, schemaName: refSchemaName })}
|
||||
isNative
|
||||
notSelected
|
||||
options={(dbInfo?.tables || []).map(tbl => ({
|
||||
label: fullNameToLabel(tbl),
|
||||
value: fullNameToString(tbl),
|
||||
}))}
|
||||
on:change={e => {
|
||||
if (e.detail) {
|
||||
const name = fullNameFromString(e.detail);
|
||||
refTableName = name.pureName;
|
||||
refSchemaName = name.schemaName;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="label col-3">On update action</div>
|
||||
<div class="col-9">
|
||||
<SelectField
|
||||
value={updateAction}
|
||||
isNative
|
||||
notSelected
|
||||
options={foreignKeyActionsOptions}
|
||||
on:change={e => {
|
||||
updateAction = e.detail || null;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="label col-3">On delete action</div>
|
||||
<div class="col-9">
|
||||
<SelectField
|
||||
value={deleteAction}
|
||||
isNative
|
||||
notSelected
|
||||
options={foreignKeyActionsOptions}
|
||||
on:change={e => {
|
||||
deleteAction = e.detail || null;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-5 mr-1">
|
||||
Base column - {tableInfo.pureName}
|
||||
</div>
|
||||
<div class="col-5 ml-1">
|
||||
Ref column - {refTableName || '(table not set)'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#each columns as column, index}
|
||||
<div class="row">
|
||||
<div class="col-5 mr-1">
|
||||
{#key column.columnName}
|
||||
<SelectField
|
||||
value={column.columnName}
|
||||
isNative
|
||||
notSelected
|
||||
options={tableInfo.columns.map(col => ({
|
||||
label: col.columnName,
|
||||
value: col.columnName,
|
||||
}))}
|
||||
on:change={e => {
|
||||
if (e.detail) {
|
||||
columns = columns.map((col, i) => (i == index ? { ...col, columnName: e.detail } : col));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
<div class="col-5 ml-1">
|
||||
{#key column.refColumnName}
|
||||
<SelectField
|
||||
value={column.refColumnName}
|
||||
isNative
|
||||
notSelected
|
||||
options={(refTableInfo?.columns || []).map(col => ({
|
||||
label: col.columnName,
|
||||
value: col.columnName,
|
||||
}))}
|
||||
on:change={e => {
|
||||
if (e.detail) {
|
||||
columns = columns.map((col, i) => (i == index ? { ...col, refColumnName: e.detail } : col));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
<div class="col-2 button">
|
||||
<FormStyledButton
|
||||
value="Delete"
|
||||
on:click={e => {
|
||||
const x = [...columns];
|
||||
x.splice(index, 1);
|
||||
columns = x;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<FormStyledButton
|
||||
type="button"
|
||||
value="Add column"
|
||||
on:click={() => {
|
||||
columns = [...columns, {}];
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<FormStyledButton
|
||||
value={'Save'}
|
||||
on:click={() => {
|
||||
closeCurrentModal();
|
||||
if (constraintInfo) {
|
||||
setTableInfo(tbl => editorModifyConstraint(tbl, getConstraint()));
|
||||
} else {
|
||||
setTableInfo(tbl => editorAddConstraint(tbl, getConstraint()));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormStyledButton type="button" value="Close" on:click={closeCurrentModal} />
|
||||
{#if constraintInfo}
|
||||
<FormStyledButton
|
||||
type="button"
|
||||
value="Remove"
|
||||
on:click={() => {
|
||||
closeCurrentModal();
|
||||
setTableInfo(tbl => editorDeleteConstraint(tbl, constraintInfo));
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</ModalBase>
|
||||
|
||||
<style>
|
||||
.row {
|
||||
margin: var(--dim-large-form-margin);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.row .label {
|
||||
white-space: nowrap;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.button {
|
||||
align-self: center;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import ColumnsConstraintEditorModal from './ColumnsConstraintEditorModal.svelte';
|
||||
|
||||
export let constraintInfo;
|
||||
export let setTableInfo;
|
||||
export let tableInfo;
|
||||
|
||||
</script>
|
||||
|
||||
<ColumnsConstraintEditorModal
|
||||
{...$$restProps}
|
||||
constraintLabel="primary key"
|
||||
constraintType="primaryKey"
|
||||
{constraintInfo}
|
||||
{setTableInfo}
|
||||
{tableInfo}
|
||||
/>
|
||||
@@ -0,0 +1,195 @@
|
||||
<script lang="ts" context="module">
|
||||
const getCurrentEditor = () => getActiveComponent('TableEditor');
|
||||
|
||||
registerCommand({
|
||||
id: 'tableEditor.addColumn',
|
||||
category: 'Table editor',
|
||||
name: 'Add column',
|
||||
icon: 'icon add-column',
|
||||
toolbar: true,
|
||||
isRelatedToTab: true,
|
||||
testEnabled: () => getCurrentEditor()?.writable(),
|
||||
onClick: () => getCurrentEditor().addColumn(),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'tableEditor.addPrimaryKey',
|
||||
category: 'Table editor',
|
||||
name: 'Add primary key',
|
||||
icon: 'icon add-key',
|
||||
toolbar: true,
|
||||
isRelatedToTab: true,
|
||||
testEnabled: () => getCurrentEditor()?.allowAddPrimaryKey(),
|
||||
onClick: () => getCurrentEditor().addPrimaryKey(),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'tableEditor.addForeignKey',
|
||||
category: 'Table editor',
|
||||
name: 'Add foreign key',
|
||||
icon: 'icon add-key',
|
||||
toolbar: true,
|
||||
isRelatedToTab: true,
|
||||
testEnabled: () => getCurrentEditor()?.writable(),
|
||||
onClick: () => getCurrentEditor().addForeignKey(),
|
||||
});
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import _ from 'lodash';
|
||||
import { tick } from 'svelte';
|
||||
import invalidateCommands from '../commands/invalidateCommands';
|
||||
import registerCommand from '../commands/registerCommand';
|
||||
|
||||
import ColumnLabel from '../elements/ColumnLabel.svelte';
|
||||
import ConstraintLabel from '../elements/ConstraintLabel.svelte';
|
||||
import ForeignKeyObjectListControl from '../elements/ForeignKeyObjectListControl.svelte';
|
||||
|
||||
import ObjectListControl from '../elements/ObjectListControl.svelte';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
import useEditorData from '../query/useEditorData';
|
||||
import createActivator, { getActiveComponent } from '../utility/createActivator';
|
||||
|
||||
import { useDbCore } from '../utility/metadataLoaders';
|
||||
import ColumnEditorModal from './ColumnEditorModal.svelte';
|
||||
import ForeignKeyEditorModal from './ForeignKeyEditorModal.svelte';
|
||||
import PrimaryKeyEditorModal from './PrimaryKeyEditorModal.svelte';
|
||||
|
||||
export const activator = createActivator('TableEditor', true);
|
||||
|
||||
export let tableInfo;
|
||||
export let setTableInfo;
|
||||
export let dbInfo;
|
||||
|
||||
export function writable() {
|
||||
return !!setTableInfo;
|
||||
}
|
||||
|
||||
export function addColumn() {
|
||||
showModal(ColumnEditorModal, {
|
||||
setTableInfo,
|
||||
tableInfo,
|
||||
onAddNext: async () => {
|
||||
await tick();
|
||||
addColumn();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function allowAddPrimaryKey() {
|
||||
return writable() && !tableInfo.primaryKey;
|
||||
}
|
||||
|
||||
export function addPrimaryKey() {
|
||||
showModal(PrimaryKeyEditorModal, {
|
||||
setTableInfo,
|
||||
tableInfo,
|
||||
});
|
||||
}
|
||||
|
||||
export function addForeignKey() {
|
||||
showModal(ForeignKeyEditorModal, {
|
||||
setTableInfo,
|
||||
tableInfo,
|
||||
dbInfo,
|
||||
});
|
||||
}
|
||||
|
||||
$: columns = tableInfo?.columns;
|
||||
$: primaryKey = tableInfo?.primaryKey;
|
||||
$: foreignKeys = tableInfo?.foreignKeys;
|
||||
$: dependencies = tableInfo?.dependencies;
|
||||
|
||||
$: {
|
||||
tableInfo;
|
||||
invalidateCommands();
|
||||
}
|
||||
|
||||
$: console.log('tableInfo', tableInfo);
|
||||
</script>
|
||||
|
||||
<div class="wrapper">
|
||||
<ObjectListControl
|
||||
collection={columns?.map((x, index) => ({ ...x, ordinal: index + 1 }))}
|
||||
title="Columns"
|
||||
clickable={writable()}
|
||||
on:clickrow={e => showModal(ColumnEditorModal, { columnInfo: e.detail, tableInfo, setTableInfo })}
|
||||
columns={[
|
||||
{
|
||||
fieldName: 'notNull',
|
||||
header: 'Not NULL',
|
||||
sortable: true,
|
||||
slot: 0,
|
||||
},
|
||||
{
|
||||
fieldName: 'dataType',
|
||||
header: 'Data Type',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
fieldName: 'defaultValue',
|
||||
header: 'Default value',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
fieldName: 'isSparse',
|
||||
header: 'Is Sparse',
|
||||
sortable: true,
|
||||
slot: 1,
|
||||
},
|
||||
{
|
||||
fieldName: 'computedExpression',
|
||||
header: 'Computed Expression',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
fieldName: 'isPersisted',
|
||||
header: 'Is Persisted',
|
||||
sortable: true,
|
||||
slot: 2,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<svelte:fragment slot="0" let:row>{row?.notNull ? 'YES' : 'NO'}</svelte:fragment>
|
||||
<svelte:fragment slot="1" let:row>{row?.isSparse ? 'YES' : 'NO'}</svelte:fragment>
|
||||
<svelte:fragment slot="2" let:row>{row?.isPersisted ? 'YES' : 'NO'}</svelte:fragment>
|
||||
<svelte:fragment slot="name" let:row><ColumnLabel {...row} forceIcon /></svelte:fragment>
|
||||
</ObjectListControl>
|
||||
|
||||
<ObjectListControl
|
||||
collection={_.compact([primaryKey])}
|
||||
title="Primary key"
|
||||
clickable={writable()}
|
||||
on:clickrow={e => showModal(PrimaryKeyEditorModal, { constraintInfo: e.detail, tableInfo, setTableInfo })}
|
||||
columns={[
|
||||
{
|
||||
fieldName: 'columns',
|
||||
header: 'Columns',
|
||||
slot: 0,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<svelte:fragment slot="name" let:row><ConstraintLabel {...row} /></svelte:fragment>
|
||||
<svelte:fragment slot="0" let:row>{row?.columns.map(x => x.columnName).join(', ')}</svelte:fragment>
|
||||
</ObjectListControl>
|
||||
|
||||
<ForeignKeyObjectListControl
|
||||
collection={foreignKeys}
|
||||
title="Foreign keys"
|
||||
clickable={writable()}
|
||||
on:clickrow={e => showModal(ForeignKeyEditorModal, { constraintInfo: e.detail, tableInfo, setTableInfo, dbInfo })}
|
||||
/>
|
||||
<ForeignKeyObjectListControl collection={dependencies} title="Dependencies" />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.wrapper {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: var(--theme-bg-0);
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -4,6 +4,8 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { generateTablePairingId } from 'dbgate-tools';
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
import ColumnLabel from '../elements/ColumnLabel.svelte';
|
||||
@@ -11,8 +13,10 @@
|
||||
import ForeignKeyObjectListControl from '../elements/ForeignKeyObjectListControl.svelte';
|
||||
|
||||
import ObjectListControl from '../elements/ObjectListControl.svelte';
|
||||
import useEditorData from '../query/useEditorData';
|
||||
import TableEditor from '../tableeditor/TableEditor.svelte';
|
||||
|
||||
import { useDbCore } from '../utility/metadataLoaders';
|
||||
import { useDatabaseInfo, useDbCore } from '../utility/metadataLoaders';
|
||||
|
||||
export let tabid;
|
||||
export let conid;
|
||||
@@ -22,86 +26,22 @@
|
||||
export let objectTypeField = 'tables';
|
||||
|
||||
$: tableInfo = useDbCore({ conid, database, schemaName, pureName, objectTypeField });
|
||||
$: dbInfo = useDatabaseInfo({ conid, database });
|
||||
$: tableInfoWithPairingId = $tableInfo ? generateTablePairingId($tableInfo) : null;
|
||||
|
||||
$: columns = $tableInfo?.columns;
|
||||
$: primaryKey = $tableInfo?.primaryKey;
|
||||
$: foreignKeys = $tableInfo?.foreignKeys;
|
||||
$: dependencies = $tableInfo?.dependencies;
|
||||
const { editorState, editorValue, setEditorData } = useEditorData({ tabid });
|
||||
|
||||
$: showTable = $editorValue || tableInfoWithPairingId;
|
||||
</script>
|
||||
|
||||
<div class="wrapper">
|
||||
<ObjectListControl
|
||||
collection={columns?.map((x, index) => ({ ...x, ordinal: index + 1 }))}
|
||||
title="Columns"
|
||||
columns={[
|
||||
{
|
||||
fieldName: 'notNull',
|
||||
header: 'Not NULL',
|
||||
sortable: true,
|
||||
slot: 0,
|
||||
},
|
||||
{
|
||||
fieldName: 'dataType',
|
||||
header: 'Data Type',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
fieldName: 'defaultValue',
|
||||
header: 'Default value',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
fieldName: 'isSparse',
|
||||
header: 'Is Sparse',
|
||||
sortable: true,
|
||||
slot: 1,
|
||||
},
|
||||
{
|
||||
fieldName: 'computedExpression',
|
||||
header: 'Computed Expression',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
fieldName: 'isPersisted',
|
||||
header: 'Is Persisted',
|
||||
sortable: true,
|
||||
slot: 2,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<svelte:fragment slot="0" let:row>{row?.notNull ? 'YES' : 'NO'}</svelte:fragment>
|
||||
<svelte:fragment slot="1" let:row>{row?.isSparse ? 'YES' : 'NO'}</svelte:fragment>
|
||||
<svelte:fragment slot="2" let:row>{row?.isPersisted ? 'YES' : 'NO'}</svelte:fragment>
|
||||
<svelte:fragment slot="name" let:row><ColumnLabel {...row} forceIcon /></svelte:fragment>
|
||||
</ObjectListControl>
|
||||
|
||||
<ObjectListControl
|
||||
collection={_.compact([primaryKey])}
|
||||
title="Primary key"
|
||||
columns={[
|
||||
{
|
||||
fieldName: 'columns',
|
||||
header: 'Columns',
|
||||
slot: 0,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<svelte:fragment slot="name" let:row><ConstraintLabel {...row} /></svelte:fragment>
|
||||
<svelte:fragment slot="0" let:row>{row?.columns.map(x => x.columnName).join(', ')}</svelte:fragment>
|
||||
</ObjectListControl>
|
||||
|
||||
<ForeignKeyObjectListControl collection={foreignKeys} title="Foreign keys" />
|
||||
<ForeignKeyObjectListControl collection={dependencies} title="Dependencies" />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.wrapper {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: var(--theme-bg-0);
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
<TableEditor
|
||||
tableInfo={showTable}
|
||||
dbInfo={$dbInfo}
|
||||
setTableInfo={objectTypeField == 'tables'
|
||||
? tableInfoUpdater =>
|
||||
setEditorData(tbl => {
|
||||
if (tbl) return tableInfoUpdater(tbl);
|
||||
return tableInfoUpdater(tableInfoWithPairingId);
|
||||
})
|
||||
: null}
|
||||
/>
|
||||
|
||||
@@ -63,11 +63,11 @@ export function closeSnackbar(snackId: string) {
|
||||
// message: 'Test snackbar',
|
||||
// allowClose: true,
|
||||
// });
|
||||
showSnackbar({
|
||||
icon: 'img ok',
|
||||
message: 'Auto close',
|
||||
autoClose: true,
|
||||
});
|
||||
// showSnackbar({
|
||||
// icon: 'img ok',
|
||||
// message: 'Auto close',
|
||||
// autoClose: true,
|
||||
// });
|
||||
// showSnackbar({
|
||||
// icon: 'img warn',
|
||||
// message: 'Buttons',
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
<script lang="ts" context="module">
|
||||
function generateObjectList(seed = 0) {
|
||||
const counts = [1000, 1200, 1100, 2100, 720];
|
||||
const schemas = ['A', 'dev', 'public', 'dbo'];
|
||||
const types = ['tables', 'views', 'functions', 'procedures', 'matviews', 'triggers'];
|
||||
const res = _.range(1, counts[seed % counts.length]).map(i => ({
|
||||
pureName: `name ${i}`,
|
||||
schemaName: schemas[i % schemas.length],
|
||||
objectTypeField: types[i % types.length],
|
||||
}));
|
||||
return res;
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import InlineButton from '../elements/InlineButton.svelte';
|
||||
import SearchInput from '../elements/SearchInput.svelte';
|
||||
@@ -12,7 +26,7 @@
|
||||
import ErrorInfo from '../elements/ErrorInfo.svelte';
|
||||
import axiosInstance from '../utility/axiosInstance';
|
||||
import LoadingInfo from '../elements/LoadingInfo.svelte';
|
||||
import { getObjectTypeFieldLabel } from '../utility/common';
|
||||
import { getObjectTypeFieldLabel } from '../utility/common';
|
||||
|
||||
export let conid;
|
||||
export let database;
|
||||
@@ -33,6 +47,10 @@ import { getObjectTypeFieldLabel } from '../utility/common';
|
||||
)
|
||||
);
|
||||
|
||||
// let generateIndex = 0;
|
||||
// setInterval(() => (generateIndex += 1), 2000);
|
||||
// $: objectList = generateObjectList(generateIndex);
|
||||
|
||||
const handleRefreshDatabase = () => {
|
||||
axiosInstance.post('database-connections/refresh', { conid, database });
|
||||
};
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
if (tab.props && tab.props.conid && tab.props.database) return tab.props.database;
|
||||
if (tab.props && tab.props.conid) {
|
||||
const connection = connectionList?.find(x => x._id == tab.props.conid);
|
||||
if (connection) return getConnectionLabel(connection.displayName, { allowExplicitDatabase: false });
|
||||
if (connection) return getConnectionLabel(connection, { allowExplicitDatabase: false });
|
||||
return '???';
|
||||
}
|
||||
if (tab.props && tab.props.archiveFolder) return tab.props.archiveFolder;
|
||||
|
||||
@@ -11,3 +11,16 @@ index ee20a17..7b6fff8 100644
|
||||
}
|
||||
function destroy_each(iterations, detaching) {
|
||||
for (let i = 0; i < iterations.length; i += 1) {
|
||||
diff --git a/node_modules/svelte/internal/index.mjs b/node_modules/svelte/internal/index.mjs
|
||||
index 4146e56..f5f00c5 100644
|
||||
--- a/node_modules/svelte/internal/index.mjs
|
||||
+++ b/node_modules/svelte/internal/index.mjs
|
||||
@@ -196,7 +196,7 @@ function insert(target, node, anchor) {
|
||||
target.insertBefore(node, anchor || null);
|
||||
}
|
||||
function detach(node) {
|
||||
- node.parentNode.removeChild(node);
|
||||
+ if (node.parentNode) node.parentNode.removeChild(node);
|
||||
}
|
||||
function destroy_each(iterations, detaching) {
|
||||
for (let i = 0; i < iterations.length; i += 1) {
|
||||
|
||||
@@ -6,7 +6,7 @@ const Analyser = require('./Analyser');
|
||||
const MongoClient = require('mongodb').MongoClient;
|
||||
const ObjectId = require('mongodb').ObjectId;
|
||||
const Cursor = require('mongodb').Cursor;
|
||||
const { createBulkInsertStream } = require('./createBulkInsertStream');
|
||||
const createBulkInsertStream = require('./createBulkInsertStream');
|
||||
|
||||
function readCursor(cursor, options) {
|
||||
return new Promise((resolve) => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
const _isString = require('lodash/isString');
|
||||
const { driverBase } = global.DBGATE_TOOLS;
|
||||
const Dumper = require('./Dumper');
|
||||
const { noSplitSplitterOptions } = require('dbgate-query-splitter/lib/options');
|
||||
@@ -5,7 +6,7 @@ const { noSplitSplitterOptions } = require('dbgate-query-splitter/lib/options');
|
||||
const mongoIdRegex = /^[0-9a-f]{24}$/;
|
||||
|
||||
function getConditionPreview(condition) {
|
||||
if (condition && _.isString(condition._id) && condition._id.match(mongoIdRegex)) {
|
||||
if (condition && _isString(condition._id) && condition._id.match(mongoIdRegex)) {
|
||||
return `{ _id: ObjectId('${condition._id}') }`;
|
||||
}
|
||||
return JSON.stringify(condition);
|
||||
|
||||
@@ -32,6 +32,8 @@ function getColumnInfo({
|
||||
charMaxLength,
|
||||
numericPrecision,
|
||||
numericScale,
|
||||
defaultValue,
|
||||
defaultConstraint,
|
||||
}) {
|
||||
let fullDataType = dataType;
|
||||
if (charMaxLength && isTypeString(dataType)) fullDataType = `${dataType}(${charMaxLength})`;
|
||||
@@ -42,6 +44,8 @@ function getColumnInfo({
|
||||
dataType: fullDataType,
|
||||
notNull: !isNullable,
|
||||
autoIncrement: !!isIdentity,
|
||||
defaultValue,
|
||||
defaultConstraint,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -51,6 +55,7 @@ class MsSqlAnalyser extends DatabaseAnalyser {
|
||||
}
|
||||
|
||||
createQuery(resFileName, typeFields) {
|
||||
if (!sql[resFileName]) throw new Error(`Missing analyse file ${resFileName}`);
|
||||
return super.createQuery(sql[resFileName], typeFields);
|
||||
}
|
||||
|
||||
@@ -67,6 +72,8 @@ class MsSqlAnalyser extends DatabaseAnalyser {
|
||||
const pkColumnsRows = await this.driver.query(this.pool, this.createQuery('primaryKeys', ['tables']));
|
||||
const fkColumnsRows = await this.driver.query(this.pool, this.createQuery('foreignKeys', ['tables']));
|
||||
const schemaRows = await this.driver.query(this.pool, this.createQuery('getSchemas'));
|
||||
const indexesRows = await this.driver.query(this.pool, this.createQuery('indexes', ['tables']));
|
||||
const indexcolsRows = await this.driver.query(this.pool, this.createQuery('indexcols', ['tables']));
|
||||
|
||||
const schemas = schemaRows.rows;
|
||||
|
||||
@@ -88,15 +95,35 @@ class MsSqlAnalyser extends DatabaseAnalyser {
|
||||
|
||||
const tables = tablesRows.rows.map(row => ({
|
||||
...row,
|
||||
contentHash: row.modifyDate.toISOString(),
|
||||
contentHash: row.modifyDate && row.modifyDate.toISOString(),
|
||||
columns: columnsRows.rows.filter(col => col.objectId == row.objectId).map(getColumnInfo),
|
||||
primaryKey: DatabaseAnalyser.extractPrimaryKeys(row, pkColumnsRows.rows),
|
||||
foreignKeys: DatabaseAnalyser.extractForeignKeys(row, fkColumnsRows.rows),
|
||||
indexes: indexesRows.rows
|
||||
.filter(idx => idx.object_id == row.objectId && !idx.is_unique_constraint)
|
||||
.map(idx => ({
|
||||
..._.pick(idx, ['constraintName', 'indexType', 'isUnique']),
|
||||
columns: indexcolsRows.rows
|
||||
.filter(col => col.object_id == idx.object_id && col.index_id == idx.index_id)
|
||||
.map(col => ({
|
||||
..._.pick(col, ['columnName', 'isDescending', 'isIncludedColumn']),
|
||||
})),
|
||||
})),
|
||||
uniques: indexesRows.rows
|
||||
.filter(idx => idx.object_id == row.objectId && idx.is_unique_constraint)
|
||||
.map(idx => ({
|
||||
..._.pick(idx, ['constraintName']),
|
||||
columns: indexcolsRows.rows
|
||||
.filter(col => col.object_id == idx.object_id && col.index_id == idx.index_id)
|
||||
.map(col => ({
|
||||
..._.pick(col, ['columnName']),
|
||||
})),
|
||||
})),
|
||||
}));
|
||||
|
||||
const views = viewsRows.rows.map(row => ({
|
||||
...row,
|
||||
contentHash: row.modifyDate.toISOString(),
|
||||
contentHash: row.modifyDate && row.modifyDate.toISOString(),
|
||||
createSql: getCreateSql(row),
|
||||
columns: viewColumnRows.rows.filter(col => col.objectId == row.objectId).map(getColumnInfo),
|
||||
}));
|
||||
@@ -105,7 +132,7 @@ class MsSqlAnalyser extends DatabaseAnalyser {
|
||||
.filter(x => x.sqlObjectType.trim() == 'P')
|
||||
.map(row => ({
|
||||
...row,
|
||||
contentHash: row.modifyDate.toISOString(),
|
||||
contentHash: row.modifyDate && row.modifyDate.toISOString(),
|
||||
createSql: getCreateSql(row),
|
||||
}));
|
||||
|
||||
@@ -113,7 +140,7 @@ class MsSqlAnalyser extends DatabaseAnalyser {
|
||||
.filter(x => ['FN', 'IF', 'TF'].includes(x.sqlObjectType.trim()))
|
||||
.map(row => ({
|
||||
...row,
|
||||
contentHash: row.modifyDate.toISOString(),
|
||||
contentHash: row.modifyDate && row.modifyDate.toISOString(),
|
||||
createSql: getCreateSql(row),
|
||||
}));
|
||||
|
||||
@@ -137,7 +164,7 @@ class MsSqlAnalyser extends DatabaseAnalyser {
|
||||
|
||||
res[field].push({
|
||||
objectId,
|
||||
contentHash: modifyDate.toISOString(),
|
||||
contentHash: modifyDate && modifyDate.toISOString(),
|
||||
schemaName,
|
||||
pureName,
|
||||
});
|
||||
|
||||
@@ -8,6 +8,8 @@ const views = require('./views');
|
||||
const programmables = require('./programmables');
|
||||
const viewColumns = require('./viewColumns');
|
||||
const getSchemas = require('./getSchemas');
|
||||
const indexes = require('./indexes');
|
||||
const indexcols = require('./indexcols');
|
||||
|
||||
module.exports = {
|
||||
columns,
|
||||
@@ -20,4 +22,6 @@ module.exports = {
|
||||
programmables,
|
||||
viewColumns,
|
||||
getSchemas,
|
||||
indexes,
|
||||
indexcols,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
module.exports = `
|
||||
select
|
||||
c.object_id, c.index_id, c.column_id,
|
||||
col.name as columnName,
|
||||
c.is_descending_key as isDescending, c.is_included_column as isIncludedColumn
|
||||
from sys.index_columns c
|
||||
inner join sys.columns col on c.object_id = col.object_id and c.column_id = col.column_id
|
||||
where c.object_id =OBJECT_ID_CONDITION
|
||||
order by c.key_ordinal
|
||||
`;
|
||||
@@ -0,0 +1,14 @@
|
||||
module.exports = `
|
||||
select i.object_id, i.name as constraintName, i.type_desc as indexType, i.is_unique as isUnique,i.index_id, i.is_unique_constraint from sys.indexes i
|
||||
where i.is_primary_key=0
|
||||
and i.is_hypothetical=0 and indexproperty(i.object_id, i.name, 'IsStatistics') = 0
|
||||
and objectproperty(i.object_id, 'IsUserTable') = 1
|
||||
and i.index_id between 1 and 254
|
||||
|
||||
--and i.name not in
|
||||
-- (select o.name from sysobjects o
|
||||
-- where o.parent_obj = i.object_id
|
||||
-- and objectproperty(o.id, N'isConstraint') = 1.0)
|
||||
|
||||
and i.object_id =OBJECT_ID_CONDITION
|
||||
`;
|
||||
@@ -1,4 +1,4 @@
|
||||
const { SqlDumper } = global.DBGATE_TOOLS;
|
||||
const { SqlDumper, testEqualColumns } = global.DBGATE_TOOLS;
|
||||
|
||||
class MsSqlDumper extends SqlDumper {
|
||||
autoIncrement() {
|
||||
@@ -65,6 +65,13 @@ class MsSqlDumper extends SqlDumper {
|
||||
super.dropTable(obj, options);
|
||||
}
|
||||
|
||||
dropColumn(column) {
|
||||
if (column.defaultConstraint) {
|
||||
this.putCmd('^alter ^table %f ^drop ^constraint %i', column, column.defaultConstraint);
|
||||
}
|
||||
super.dropColumn(column);
|
||||
}
|
||||
|
||||
dropDefault(col) {
|
||||
if (col.defaultConstraint) {
|
||||
this.putCmd('^alter ^table %f ^drop ^constraint %i', col, col.defaultConstraint);
|
||||
@@ -88,6 +95,21 @@ class MsSqlDumper extends SqlDumper {
|
||||
this.putCmd("^execute sp_rename '%f.%i', '%s', 'COLUMN'", column, column.columnName, newcol);
|
||||
}
|
||||
|
||||
changeColumn(oldcol, newcol, constraints) {
|
||||
if (testEqualColumns(oldcol, newcol, false, false)) {
|
||||
this.dropDefault(oldcol);
|
||||
if (oldcol.columnName != newcol.columnName) this.renameColumn(oldcol, newcol.columnName);
|
||||
this.createDefault(oldcol);
|
||||
} else {
|
||||
this.dropDefault(oldcol);
|
||||
if (oldcol.columnName != newcol.columnName) this.renameColumn(oldcol, newcol.columnName);
|
||||
this.put('^alter ^table %f ^alter ^column %i ', oldcol, oldcol.columnName, newcol.columnName);
|
||||
this.columnDefinition(newcol, { includeDefault: false });
|
||||
this.endCommand();
|
||||
this.createDefault(oldcol);
|
||||
}
|
||||
}
|
||||
|
||||
renameConstraint(cnt, newname) {
|
||||
if (cnt.constraintType == 'index')
|
||||
this.putCmd("^execute sp_rename '%f.%i', '%s', 'INDEX'", cnt, cnt.constraintName, newname);
|
||||
|
||||
@@ -12,10 +12,26 @@ const dialect = {
|
||||
fallbackDataType: 'nvarchar(max)',
|
||||
explicitDropConstraint: false,
|
||||
enableConstraintsPerTable: true,
|
||||
dropColumnDependencies: ['default', 'dependencies', 'indexes', 'primaryKey', 'foreignKeys', 'uniques'],
|
||||
changeColumnDependencies: ['indexes'],
|
||||
anonymousPrimaryKey: false,
|
||||
dropIndexContainsTableSpec: true,
|
||||
quoteIdentifier(s) {
|
||||
return `[${s}]`;
|
||||
},
|
||||
|
||||
createColumn: true,
|
||||
dropColumn: true,
|
||||
createIndex: true,
|
||||
dropIndex: true,
|
||||
createForeignKey: true,
|
||||
dropForeignKey: true,
|
||||
createPrimaryKey: true,
|
||||
dropPrimaryKey: true,
|
||||
createUnique: true,
|
||||
dropUnique: true,
|
||||
createCheck: true,
|
||||
dropCheck: true,
|
||||
};
|
||||
|
||||
/** @type {import('dbgate-types').EngineDriver} */
|
||||
|
||||
@@ -21,7 +21,7 @@ function getColumnInfo({
|
||||
fullDataType = `${dataType}(${numericPrecision},${numericScale})`;
|
||||
return {
|
||||
notNull: !isNullable || isNullable == 'NO' || isNullable == 'no',
|
||||
autoIncrement: extra && extra.toLowerCase().includes('auto_increment'),
|
||||
autoIncrement: !!(extra && extra.toLowerCase().includes('auto_increment')),
|
||||
columnName,
|
||||
dataType: fullDataType,
|
||||
defaultValue,
|
||||
@@ -74,20 +74,52 @@ class Analyser extends DatabaseAnalyser {
|
||||
);
|
||||
|
||||
const viewTexts = await this.getViewTexts(views.rows.map(x => x.pureName));
|
||||
const indexes = await this.driver.query(this.pool, this.createQuery('indexes', ['tables']));
|
||||
const uniqueNames = await this.driver.query(this.pool, this.createQuery('uniqueNames', ['tables']));
|
||||
|
||||
return {
|
||||
tables: tables.rows.map(table => ({
|
||||
...table,
|
||||
objectId: table.pureName,
|
||||
contentHash: table.modifyDate.toISOString(),
|
||||
contentHash: table.modifyDate && table.modifyDate.toISOString(),
|
||||
columns: columns.rows.filter(col => col.pureName == table.pureName).map(getColumnInfo),
|
||||
primaryKey: DatabaseAnalyser.extractPrimaryKeys(table, pkColumns.rows),
|
||||
foreignKeys: DatabaseAnalyser.extractForeignKeys(table, fkColumns.rows),
|
||||
indexes: _.uniqBy(
|
||||
indexes.rows.filter(
|
||||
idx =>
|
||||
idx.tableName == table.pureName && !uniqueNames.rows.find(x => x.constraintName == idx.constraintName)
|
||||
),
|
||||
'constraintName'
|
||||
).map(idx => ({
|
||||
..._.pick(idx, ['constraintName', 'indexType']),
|
||||
isUnique: !idx.nonUnique,
|
||||
columns: indexes.rows
|
||||
.filter(col => col.tableName == idx.tableName && col.constraintName == idx.constraintName)
|
||||
.map(col => ({
|
||||
..._.pick(col, ['columnName']),
|
||||
})),
|
||||
})),
|
||||
|
||||
uniques: _.uniqBy(
|
||||
indexes.rows.filter(
|
||||
idx =>
|
||||
idx.tableName == table.pureName && uniqueNames.rows.find(x => x.constraintName == idx.constraintName)
|
||||
),
|
||||
'constraintName'
|
||||
).map(idx => ({
|
||||
..._.pick(idx, ['constraintName']),
|
||||
columns: indexes.rows
|
||||
.filter(col => col.tableName == idx.tableName && col.constraintName == idx.constraintName)
|
||||
.map(col => ({
|
||||
..._.pick(col, ['columnName']),
|
||||
})),
|
||||
})),
|
||||
})),
|
||||
views: views.rows.map(view => ({
|
||||
...view,
|
||||
objectId: view.pureName,
|
||||
contentHash: view.modifyDate.toISOString(),
|
||||
contentHash: view.modifyDate && view.modifyDate.toISOString(),
|
||||
columns: columns.rows.filter(col => col.pureName == view.pureName).map(getColumnInfo),
|
||||
createSql: viewTexts[view.pureName],
|
||||
requiresFormat: true,
|
||||
@@ -98,7 +130,7 @@ class Analyser extends DatabaseAnalyser {
|
||||
.map(x => ({
|
||||
...x,
|
||||
objectId: x.pureName,
|
||||
contentHash: x.modifyDate.toISOString(),
|
||||
contentHash: x.modifyDate && x.modifyDate.toISOString(),
|
||||
})),
|
||||
functions: programmables.rows
|
||||
.filter(x => x.objectType == 'FUNCTION')
|
||||
@@ -106,7 +138,7 @@ class Analyser extends DatabaseAnalyser {
|
||||
.map(x => ({
|
||||
...x,
|
||||
objectId: x.pureName,
|
||||
contentHash: x.modifyDate.toISOString(),
|
||||
contentHash: x.modifyDate && x.modifyDate.toISOString(),
|
||||
})),
|
||||
};
|
||||
}
|
||||
@@ -128,14 +160,14 @@ class Analyser extends DatabaseAnalyser {
|
||||
.map(x => ({
|
||||
...x,
|
||||
objectId: x.pureName,
|
||||
contentHash: x.modifyDate.toISOString(),
|
||||
contentHash: x.modifyDate && x.modifyDate.toISOString(),
|
||||
})),
|
||||
views: tableModificationsQueryData.rows
|
||||
.filter(x => x.objectType == 'VIEW')
|
||||
.map(x => ({
|
||||
...x,
|
||||
objectId: x.pureName,
|
||||
contentHash: x.modifyDate.toISOString(),
|
||||
contentHash: x.modifyDate && x.modifyDate.toISOString(),
|
||||
})),
|
||||
procedures: procedureModificationsQueryData.rows.map(x => ({
|
||||
contentHash: x.Modified,
|
||||
|
||||
@@ -4,9 +4,11 @@ const primaryKeys = require('./primaryKeys');
|
||||
const foreignKeys = require('./foreignKeys');
|
||||
const tableModifications = require('./tableModifications');
|
||||
const views = require('./views');
|
||||
const indexes = require('./indexes');
|
||||
const programmables = require('./programmables');
|
||||
const procedureModifications = require('./procedureModifications');
|
||||
const functionModifications = require('./functionModifications');
|
||||
const uniqueNames = require('./uniqueNames');
|
||||
|
||||
module.exports = {
|
||||
columns,
|
||||
@@ -18,4 +20,6 @@ module.exports = {
|
||||
programmables,
|
||||
procedureModifications,
|
||||
functionModifications,
|
||||
indexes,
|
||||
uniqueNames,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
module.exports = `
|
||||
SELECT
|
||||
INDEX_NAME AS constraintName,
|
||||
TABLE_NAME AS tableName,
|
||||
COLUMN_NAME AS columnName,
|
||||
INDEX_TYPE AS indexType,
|
||||
NON_UNIQUE AS nonUnique
|
||||
FROM INFORMATION_SCHEMA.STATISTICS
|
||||
WHERE TABLE_SCHEMA = '#DATABASE#' AND TABLE_NAME =OBJECT_ID_CONDITION AND INDEX_NAME != 'PRIMARY'
|
||||
ORDER BY SEQ_IN_INDEX
|
||||
`;
|
||||
@@ -0,0 +1,5 @@
|
||||
module.exports = `
|
||||
select CONSTRAINT_NAME as constraintName
|
||||
from information_schema.TABLE_CONSTRAINTS
|
||||
where CONSTRAINT_SCHEMA = '#DATABASE#' and constraint_type = 'UNIQUE'
|
||||
`;
|
||||
@@ -32,7 +32,7 @@ class Dumper extends SqlDumper {
|
||||
|
||||
changeColumn(oldcol, newcol, constraints) {
|
||||
this.put('^alter ^table %f ^change ^column %i %i ', oldcol, oldcol.columnName, newcol.columnName);
|
||||
this.columnDefinition(newcol, true, true, true);
|
||||
this.columnDefinition(newcol);
|
||||
this.inlineConstraints(constraints);
|
||||
this.endCommand();
|
||||
}
|
||||
|
||||
@@ -13,6 +13,20 @@ const dialect = {
|
||||
quoteIdentifier(s) {
|
||||
return '`' + s + '`';
|
||||
},
|
||||
|
||||
createColumn: true,
|
||||
dropColumn: true,
|
||||
createIndex: true,
|
||||
dropIndex: true,
|
||||
createForeignKey: true,
|
||||
dropForeignKey: true,
|
||||
createPrimaryKey: true,
|
||||
dropPrimaryKey: true,
|
||||
dropIndexContainsTableSpec: true,
|
||||
createUnique: true,
|
||||
dropUnique: true,
|
||||
createCheck: true,
|
||||
dropCheck: true,
|
||||
};
|
||||
|
||||
const mysqlDriverBase = {
|
||||
|
||||
@@ -25,11 +25,13 @@ function getColumnInfo({
|
||||
if (char_max_length && isTypeString(normDataType)) fullDataType = `${normDataType}(${char_max_length})`;
|
||||
if (numeric_precision && numeric_ccale && isTypeNumeric(normDataType))
|
||||
fullDataType = `${normDataType}(${numeric_precision},${numeric_ccale})`;
|
||||
const autoIncrement = !!(default_value && default_value.startsWith('nextval('));
|
||||
return {
|
||||
columnName: column_name,
|
||||
dataType: fullDataType,
|
||||
notNull: !is_nullable || is_nullable == 'NO' || is_nullable == 'no',
|
||||
defaultValue: default_value,
|
||||
defaultValue: autoIncrement ? undefined : default_value,
|
||||
autoIncrement,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -39,7 +41,9 @@ class Analyser extends DatabaseAnalyser {
|
||||
}
|
||||
|
||||
createQuery(resFileName, typeFields) {
|
||||
return super.createQuery(sql[resFileName], typeFields);
|
||||
return super
|
||||
.createQuery(sql[resFileName], typeFields)
|
||||
.replace('#REFTABLECOND#', this.driver.__analyserInternals.refTableCond);
|
||||
}
|
||||
|
||||
async _computeSingleObjectId() {
|
||||
@@ -63,6 +67,9 @@ class Analyser extends DatabaseAnalyser {
|
||||
? await this.driver.query(this.pool, this.createQuery('matviewColumns', ['matviews']))
|
||||
: null;
|
||||
const routines = await this.driver.query(this.pool, this.createQuery('routines', ['procedures', 'functions']));
|
||||
const indexes = await this.driver.query(this.pool, this.createQuery('indexes', ['tables']));
|
||||
const indexcols = await this.driver.query(this.pool, this.createQuery('indexcols', ['tables']));
|
||||
const uniqueNames = await this.driver.query(this.pool, this.createQuery('uniqueNames', ['tables']));
|
||||
|
||||
return {
|
||||
tables: tables.rows.map(table => {
|
||||
@@ -102,6 +109,43 @@ class Analyser extends DatabaseAnalyser {
|
||||
refSchemaName: x.ref_schema_name,
|
||||
}))
|
||||
),
|
||||
indexes: indexes.rows
|
||||
.filter(
|
||||
x =>
|
||||
x.table_name == table.pure_name &&
|
||||
x.schema_name == table.schema_name &&
|
||||
!uniqueNames.rows.find(y => y.constraint_name == x.index_name)
|
||||
)
|
||||
.map(idx => ({
|
||||
constraintName: idx.index_name,
|
||||
isUnique: idx.is_unique,
|
||||
columns: _.compact(
|
||||
idx.indkey
|
||||
.split(' ')
|
||||
.map(colid => indexcols.rows.find(col => col.oid == idx.oid && col.attnum == colid))
|
||||
.map(col => ({
|
||||
columnName: col.column_name,
|
||||
}))
|
||||
),
|
||||
})),
|
||||
uniques: indexes.rows
|
||||
.filter(
|
||||
x =>
|
||||
x.table_name == table.pure_name &&
|
||||
x.schema_name == table.schema_name &&
|
||||
uniqueNames.rows.find(y => y.constraint_name == x.index_name)
|
||||
)
|
||||
.map(idx => ({
|
||||
constraintName: idx.index_name,
|
||||
columns: _.compact(
|
||||
idx.indkey
|
||||
.split(' ')
|
||||
.map(colid => indexcols.rows.find(col => col.oid == idx.oid && col.attnum == colid))
|
||||
.map(col => ({
|
||||
columnName: col.column_name,
|
||||
}))
|
||||
),
|
||||
})),
|
||||
};
|
||||
}),
|
||||
views: views.rows.map(view => ({
|
||||
@@ -139,6 +183,7 @@ class Analyser extends DatabaseAnalyser {
|
||||
.filter(x => x.object_type == 'FUNCTION')
|
||||
.map(func => ({
|
||||
objectId: `functions:${func.schema_name}.${func.pure_name}`,
|
||||
createSql: `CREATE FUNCTION "${func.schema_name}"."${func.pure_name}"() RETURNS ${func.data_type} LANGUAGE ${func.language}\nAS\n$$\n${func.definition}\n$$`,
|
||||
pureName: func.pure_name,
|
||||
schemaName: func.schema_name,
|
||||
contentHash: func.hash_code,
|
||||
|
||||
@@ -74,7 +74,7 @@ const drivers = driverBases.map(driverBase => ({
|
||||
}
|
||||
const res = await client.query({ text: sql, rowMode: 'array' });
|
||||
const columns = extractPostgresColumns(res);
|
||||
return { rows: res.rows.map(row => zipDataRow(row, columns)), columns };
|
||||
return { rows: (res.rows || []).map(row => zipDataRow(row, columns)), columns };
|
||||
},
|
||||
stream(client, sql, options) {
|
||||
const query = new pg.Query({
|
||||
|
||||
@@ -12,7 +12,7 @@ select
|
||||
refcol.column_name as "ref_column_name"
|
||||
from information_schema.referential_constraints fk
|
||||
inner join information_schema.table_constraints base on fk.constraint_name = base.constraint_name and fk.constraint_schema = base.constraint_schema
|
||||
inner join information_schema.table_constraints ref on fk.unique_constraint_name = ref.constraint_name and fk.unique_constraint_schema = ref.constraint_schema
|
||||
inner join information_schema.table_constraints ref on fk.unique_constraint_name = ref.constraint_name and fk.unique_constraint_schema = ref.constraint_schema #REFTABLECOND#
|
||||
inner join information_schema.key_column_usage basecol on base.table_name = basecol.table_name and base.constraint_name = basecol.constraint_name
|
||||
inner join information_schema.key_column_usage refcol on ref.table_name = refcol.table_name and ref.constraint_name = refcol.constraint_name and basecol.ordinal_position = refcol.ordinal_position
|
||||
where
|
||||
|
||||
@@ -10,6 +10,9 @@ const matviews = require('./matviews');
|
||||
const routines = require('./routines');
|
||||
const routineModifications = require('./routineModifications');
|
||||
const matviewColumns = require('./matviewColumns');
|
||||
const indexes = require('./indexes');
|
||||
const indexcols = require('./indexcols');
|
||||
const uniqueNames = require('./uniqueNames');
|
||||
|
||||
module.exports = {
|
||||
columns,
|
||||
@@ -24,4 +27,7 @@ module.exports = {
|
||||
matviews,
|
||||
matviewModifications,
|
||||
matviewColumns,
|
||||
indexes,
|
||||
indexcols,
|
||||
uniqueNames,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
module.exports = `
|
||||
select
|
||||
a.attname as "column_name",
|
||||
a.attnum as "attnum",
|
||||
a.attrelid as "oid"
|
||||
from
|
||||
pg_class t,
|
||||
pg_class i,
|
||||
pg_attribute a,
|
||||
pg_index ix,
|
||||
pg_namespace c
|
||||
where
|
||||
t.oid = ix.indrelid
|
||||
and a.attnum = ANY(ix.indkey)
|
||||
and a.attrelid = t.oid
|
||||
and i.oid = ix.indexrelid
|
||||
and t.relkind = 'r'
|
||||
and ix.indisprimary = false
|
||||
and t.relnamespace = c.oid
|
||||
and c.nspname != 'pg_catalog'
|
||||
and ('tables:' || c.nspname || '.' || t.relname) =OBJECT_ID_CONDITION
|
||||
order by
|
||||
t.relname
|
||||
`;
|
||||
@@ -0,0 +1,25 @@
|
||||
module.exports = `
|
||||
select
|
||||
t.relname as "table_name",
|
||||
c.nspname as "schema_name",
|
||||
i.relname as "index_name",
|
||||
ix.indisprimary as "is_primary",
|
||||
ix.indisunique as "is_unique",
|
||||
ix.indkey as "indkey",
|
||||
t.oid as "oid"
|
||||
from
|
||||
pg_class t,
|
||||
pg_class i,
|
||||
pg_index ix,
|
||||
pg_namespace c
|
||||
where
|
||||
t.oid = ix.indrelid
|
||||
and i.oid = ix.indexrelid
|
||||
and t.relkind = 'r'
|
||||
and ix.indisprimary = false
|
||||
and t.relnamespace = c.oid
|
||||
and c.nspname != 'pg_catalog'
|
||||
and ('tables:' || c.nspname || '.' || t.relname) =OBJECT_ID_CONDITION
|
||||
order by
|
||||
t.relname
|
||||
`;
|
||||
@@ -5,6 +5,7 @@ select
|
||||
routine_definition as "definition",
|
||||
md5(routine_definition) as "hash_code",
|
||||
routine_type as "object_type",
|
||||
data_type as "data_type",
|
||||
external_language as "language"
|
||||
from
|
||||
information_schema.routines where routine_schema != 'information_schema' and routine_schema != 'pg_catalog'
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = `
|
||||
select conname as "constraint_name" from pg_constraint where contype = 'u'
|
||||
`;
|
||||
@@ -1,4 +1,4 @@
|
||||
const { SqlDumper } = global.DBGATE_TOOLS;
|
||||
const { SqlDumper, testEqualTypes } = global.DBGATE_TOOLS;
|
||||
|
||||
class Dumper extends SqlDumper {
|
||||
/** @param type {import('dbgate-types').TransformType} */
|
||||
@@ -41,7 +41,7 @@ class Dumper extends SqlDumper {
|
||||
dropTable(obj, options = {}) {
|
||||
this.put('^drop ^table');
|
||||
if (options.testIfExists) this.put(' ^if ^exists');
|
||||
this.put(' %f', obj.FullName);
|
||||
this.put(' %f', obj);
|
||||
this.endCommand();
|
||||
}
|
||||
|
||||
@@ -61,6 +61,37 @@ class Dumper extends SqlDumper {
|
||||
}
|
||||
super.columnDefinition(col, options);
|
||||
}
|
||||
|
||||
changeColumn(oldcol, newcol, constraints) {
|
||||
if (oldcol.columnName != newcol.columnName) {
|
||||
this.putCmd('^alter ^table %f ^rename ^column %i ^to %i', oldcol, oldcol.columnName, newcol.columnName);
|
||||
}
|
||||
if (!testEqualTypes(oldcol, newcol)) {
|
||||
this.putCmd('^alter ^table %f ^alter ^column %i ^type %s', oldcol, oldcol.columnName, newcol.dataType);
|
||||
}
|
||||
if (oldcol.notNull != newcol.notNull) {
|
||||
if (newcol.notNull) this.putCmd('^alter ^table %f ^alter ^column %i ^set ^not ^null', newcol, newcol.columnName);
|
||||
else this.putCmd('^alter ^table %f ^alter ^column %i ^drop ^not ^null', newcol, newcol.columnName);
|
||||
}
|
||||
if (oldcol.defaultValue != newcol.defaultValue) {
|
||||
if (newcol.defaultValue == null) {
|
||||
this.putCmd('^alter ^table %f ^alter ^column %i ^drop ^default', newcol, newcol.columnName);
|
||||
} else {
|
||||
this.putCmd(
|
||||
'^alter ^table %f ^alter ^column %i ^set ^default %s',
|
||||
newcol,
|
||||
newcol.columnName,
|
||||
newcol.defaultValue
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
putValue(value) {
|
||||
if (value === true) this.putRaw('true');
|
||||
else if (value === false) this.putRaw('false');
|
||||
else super.putValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Dumper;
|
||||
|
||||
@@ -5,15 +5,30 @@ const { postgreSplitterOptions } = require('dbgate-query-splitter/lib/options');
|
||||
/** @type {import('dbgate-types').SqlDialect} */
|
||||
const dialect = {
|
||||
rangeSelect: true,
|
||||
ilike: true,
|
||||
// stringEscapeChar: '\\',
|
||||
stringEscapeChar: "'",
|
||||
fallbackDataType: 'varchar',
|
||||
anonymousPrimaryKey: true,
|
||||
enableConstraintsPerTable: true,
|
||||
dropColumnDependencies: ['dependencies'],
|
||||
quoteIdentifier(s) {
|
||||
return '"' + s + '"';
|
||||
},
|
||||
stringAgg: true,
|
||||
|
||||
createColumn: true,
|
||||
dropColumn: true,
|
||||
createIndex: true,
|
||||
dropIndex: true,
|
||||
createForeignKey: true,
|
||||
dropForeignKey: true,
|
||||
createPrimaryKey: true,
|
||||
dropPrimaryKey: true,
|
||||
createUnique: true,
|
||||
dropUnique: true,
|
||||
createCheck: true,
|
||||
dropCheck: true,
|
||||
};
|
||||
|
||||
const postgresDriverBase = {
|
||||
@@ -23,6 +38,10 @@ const postgresDriverBase = {
|
||||
showConnectionField: (field, values) =>
|
||||
['server', 'port', 'user', 'password', 'defaultDatabase', 'singleDatabase'].includes(field),
|
||||
getQuerySplitterOptions: () => postgreSplitterOptions,
|
||||
|
||||
__analyserInternals: {
|
||||
refTableCond: '',
|
||||
},
|
||||
};
|
||||
|
||||
/** @type {import('dbgate-types').EngineDriver} */
|
||||
@@ -46,6 +65,11 @@ const cockroachDriver = {
|
||||
dialect: {
|
||||
...dialect,
|
||||
materializedViews: true,
|
||||
dropColumnDependencies: ['primaryKey', 'dependencies'],
|
||||
dropPrimaryKey: false,
|
||||
},
|
||||
__analyserInternals: {
|
||||
refTableCond: 'and fk.referenced_table_name = ref.table_name',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
const _ = require('lodash');
|
||||
const { DatabaseAnalyser } = require('dbgate-tools');
|
||||
|
||||
const indexcolsQuery = `
|
||||
SELECT
|
||||
m.name as tableName,
|
||||
il.name as constraintName,
|
||||
il."unique" as isUnique,
|
||||
ii.name as columnName,
|
||||
il.origin
|
||||
FROM sqlite_schema AS m,
|
||||
pragma_index_list(m.name) AS il,
|
||||
pragma_index_info(il.name) AS ii
|
||||
WHERE m.type='table' AND il.origin <> 'pk'
|
||||
ORDER BY ii.seqno
|
||||
`;
|
||||
|
||||
class Analyser extends DatabaseAnalyser {
|
||||
constructor(pool, driver) {
|
||||
super(pool, driver);
|
||||
@@ -51,6 +65,8 @@ class Analyser extends DatabaseAnalyser {
|
||||
createSql: x.sql,
|
||||
}));
|
||||
|
||||
const indexcols = await this.driver.query(this.pool, indexcolsQuery);
|
||||
|
||||
for (const tableName of this.getRequestedObjectPureNames(
|
||||
'tables',
|
||||
tables.map((x) => x.name)
|
||||
@@ -67,6 +83,29 @@ class Analyser extends DatabaseAnalyser {
|
||||
autoIncrement: tableSqls[tableName].toLowerCase().includes('autoincrement') && !!col.pk,
|
||||
}));
|
||||
|
||||
const indexNames = _.uniq(
|
||||
indexcols.rows.filter((x) => x.tableName == tableName && x.origin == 'c').map((x) => x.constraintName)
|
||||
);
|
||||
|
||||
tableObj.indexes = indexNames.map((idx) => ({
|
||||
constraintName: idx,
|
||||
isUnique: !!indexcols.rows.find((x) => x.tableName == tableName && x.constraintName == idx).isUnique,
|
||||
columns: indexcols.rows
|
||||
.filter((x) => x.tableName == tableName && x.constraintName == idx)
|
||||
.map(({ columnName }) => ({ columnName })),
|
||||
}));
|
||||
|
||||
const uniqueNames = _.uniq(
|
||||
indexcols.rows.filter((x) => x.tableName == tableName && x.origin == 'u').map((x) => x.constraintName)
|
||||
);
|
||||
|
||||
tableObj.uniques = uniqueNames.map((idx) => ({
|
||||
constraintName: idx,
|
||||
columns: indexcols.rows
|
||||
.filter((x) => x.tableName == tableName && x.constraintName == idx)
|
||||
.map(({ columnName }) => ({ columnName })),
|
||||
}));
|
||||
|
||||
const pkColumns = info.rows
|
||||
.filter((x) => x.pk)
|
||||
.map((col) => ({
|
||||
@@ -95,8 +134,6 @@ class Analyser extends DatabaseAnalyser {
|
||||
};
|
||||
return fk;
|
||||
});
|
||||
|
||||
// console.log(info);
|
||||
}
|
||||
|
||||
for (const viewName of this.getRequestedObjectPureNames(
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
const { SqlDumper } = global.DBGATE_TOOLS;
|
||||
|
||||
class Dumper extends SqlDumper {}
|
||||
class Dumper extends SqlDumper {
|
||||
renameColumn(column, newcol) {
|
||||
this.putCmd('^alter ^table %f ^rename ^column %i ^to %i', column, column.columnName, newcol);
|
||||
}
|
||||
|
||||
renameTable(obj, newname) {
|
||||
this.putCmd('^alter ^table %f ^rename ^to %i', obj, newname);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Dumper;
|
||||
|
||||
@@ -14,11 +14,22 @@ const dialect = {
|
||||
limitSelect: true,
|
||||
rangeSelect: true,
|
||||
offsetFetchRangeSyntax: false,
|
||||
explicitDropConstraint: true,
|
||||
stringEscapeChar: "'",
|
||||
fallbackDataType: 'nvarchar(max)',
|
||||
dropColumnDependencies: ['indexes', 'primaryKey', 'uniques'],
|
||||
quoteIdentifier(s) {
|
||||
return `[${s}]`;
|
||||
},
|
||||
|
||||
createColumn: true,
|
||||
dropColumn: true,
|
||||
createIndex: true,
|
||||
dropIndex: true,
|
||||
createForeignKey: false,
|
||||
dropForeignKey: false,
|
||||
createPrimaryKey: false,
|
||||
dropPrimaryKey: false,
|
||||
};
|
||||
|
||||
/** @type {import('dbgate-types').EngineDriver} */
|
||||
|
||||