Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4682255d5f | |||
| a503898b21 | |||
| 21352dae07 | |||
| 28aa86f0aa | |||
| a71129df4b | |||
| de6acfa1ce | |||
| ccf075dc65 | |||
| 1d8ac3cf86 | |||
| 7a8ff89c5c | |||
| eda70def2a | |||
| 08fd75edc7 | |||
| 15ea53864f | |||
| 377cd64556 | |||
| b37744d574 | |||
| a7f21fe0c6 | |||
| 955ca99cf3 | |||
| 98f5bb4124 | |||
| b3943f005d | |||
| 8d4178b984 | |||
| 2a88ed38c4 | |||
| 52dce7dfd3 | |||
| 6ebee92542 | |||
| 1b5646f526 | |||
| 7024e4b40d | |||
| bc2e27d7da | |||
| 189da2bfe2 | |||
| 12e6afbaad | |||
| 142ebe3d27 | |||
| 7579f6e42a | |||
| 38c25cae74 | |||
| 408496eb7c | |||
| 4d61c74a8b | |||
| 190c610466 | |||
| 85b7e3ebe3 | |||
| d220525ac7 | |||
| 5e4a631ff2 | |||
| 9099ce42b9 | |||
| df226fea22 | |||
| 851d2e9151 | |||
| 89121a2608 | |||
| 23cf264d4d | |||
| b3130225b5 | |||
| 65512defed | |||
| 3b1c8748f1 | |||
| aba660eddb | |||
| 137eac7dbf | |||
| fdbd08f511 | |||
| ace1cec1f6 | |||
| fa5fda0c3b | |||
| 251609e274 | |||
| c0287e49d8 |
@@ -43,7 +43,7 @@ jobs:
|
||||
repository: dbgate/dbgate-pro
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
path: dbgate-pro
|
||||
ref: ae1fcf6e61c6f7dfbb21005daa259c68e899a80a
|
||||
ref: 4401bfe2a52019448fd14a999eb26bc4e7b5341f
|
||||
- name: Merge dbgate/dbgate-pro
|
||||
run: |
|
||||
mkdir ../dbgate-pro
|
||||
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
repository: dbgate/dbgate-pro
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
path: dbgate-pro
|
||||
ref: ae1fcf6e61c6f7dfbb21005daa259c68e899a80a
|
||||
ref: 4401bfe2a52019448fd14a999eb26bc4e7b5341f
|
||||
- name: Merge dbgate/dbgate-pro
|
||||
run: |
|
||||
mkdir ../dbgate-pro
|
||||
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
repository: dbgate/dbgate-pro
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
path: dbgate-pro
|
||||
ref: ae1fcf6e61c6f7dfbb21005daa259c68e899a80a
|
||||
ref: 4401bfe2a52019448fd14a999eb26bc4e7b5341f
|
||||
- name: Merge dbgate/dbgate-pro
|
||||
run: |
|
||||
mkdir ../dbgate-pro
|
||||
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
repository: dbgate/dbgate-pro
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
path: dbgate-pro
|
||||
ref: ae1fcf6e61c6f7dfbb21005daa259c68e899a80a
|
||||
ref: 4401bfe2a52019448fd14a999eb26bc4e7b5341f
|
||||
- name: Merge dbgate/dbgate-pro
|
||||
run: |
|
||||
mkdir ../dbgate-pro
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
repository: dbgate/dbgate-pro
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
path: dbgate-pro
|
||||
ref: ae1fcf6e61c6f7dfbb21005daa259c68e899a80a
|
||||
ref: 4401bfe2a52019448fd14a999eb26bc4e7b5341f
|
||||
- name: Merge dbgate/dbgate-pro
|
||||
run: |
|
||||
mkdir ../dbgate-pro
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
repository: dbgate/dbgate-pro
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
path: dbgate-pro
|
||||
ref: ae1fcf6e61c6f7dfbb21005daa259c68e899a80a
|
||||
ref: 4401bfe2a52019448fd14a999eb26bc4e7b5341f
|
||||
- name: Merge dbgate/dbgate-pro
|
||||
run: |
|
||||
mkdir ../dbgate-pro
|
||||
|
||||
+1
-1
@@ -79,7 +79,7 @@ Builds:
|
||||
- ADDED: SQL AI assistant - powered by database chat, could help you to write SQL queries (Premium)
|
||||
- ADDED: Explain SQL error (powered by AI) (Premium)
|
||||
- ADDED: Database chat (and SQL AI Assistant) now supports showing charts (Premium)
|
||||
- FIXED: Fxied editing new files and roles (Team Premium)
|
||||
- FIXED: Fixed editing new files and roles (Team Premium)
|
||||
- FIXED: Connection to standalone database could be now pinned
|
||||
- FIXED: Cannot open up large JSON file #1215
|
||||
|
||||
|
||||
@@ -237,7 +237,7 @@ describe('Import CSV - source error', () => {
|
||||
cy.testid('ImportExportTab_preview_content').contains('Invalid Closing Quote').should('be.visible');
|
||||
|
||||
cy.testid('ImportExportTab_executeButton').click();
|
||||
cy.testid('ImportExportConfigurator_errorInfoIcon_customers-20-err').click();
|
||||
cy.testid('ImportExportConfigurator_errorInfoIcon_customers-20-err', { timeout: 10000 }).click();
|
||||
|
||||
cy.testid('ErrorMessageModal_message').contains('Invalid Closing Quote').should('be.visible');
|
||||
});
|
||||
@@ -256,7 +256,7 @@ describe('Import CSV - target error', () => {
|
||||
cy.contains('customers-20');
|
||||
cy.testid('ImportExportConfigurator_targetName_customers-20').clear().type('system."]`');
|
||||
cy.testid('ImportExportTab_executeButton').click();
|
||||
cy.testid('ImportExportConfigurator_errorInfoIcon_customers-20').click();
|
||||
cy.testid('ImportExportConfigurator_errorInfoIcon_customers-20', { timeout: 10000 }).click();
|
||||
cy.testid('ErrorMessageModal_message').should('be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,13 +26,15 @@ function pickImportantTableInfo(engine, table) {
|
||||
.map(props =>
|
||||
_.omitBy(props, (v, k) => k == 'defaultValue' && v == 'NULL' && engine.setNullDefaultInsteadOfDrop)
|
||||
),
|
||||
// foreignKeys: table.foreignKeys
|
||||
// .sort((a, b) => a.refTableName.localeCompare(b.refTableName))
|
||||
// .map(fk => ({
|
||||
// constraintType: fk.constraintType,
|
||||
// refTableName: fk.refTableName,
|
||||
// columns: fk.columns.map(col => ({ columnName: col.columnName, refColumnName: col.refColumnName })),
|
||||
// })),
|
||||
|
||||
// TODO:
|
||||
foreignKeys: table.foreignKeys
|
||||
.sort((a, b) => a.refTableName.localeCompare(b.refTableName))
|
||||
.map(fk => ({
|
||||
constraintType: fk.constraintType,
|
||||
refTableName: fk.refTableName,
|
||||
columns: fk.columns.map(col => ({ columnName: col.columnName, refColumnName: col.refColumnName })),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -103,6 +105,7 @@ async function testTableDiff(engine, conn, driver, mangle, changedTable = 't1')
|
||||
|
||||
await driver.script(conn, sql);
|
||||
|
||||
// TODO:
|
||||
// if (!engine.skipIncrementalAnalysis) {
|
||||
// const structure2RealIncremental = await driver.analyseIncremental(conn, structure1Source);
|
||||
// checkTableStructure(engine, tget(structure2RealIncremental), tget(structure2));
|
||||
@@ -116,6 +119,7 @@ async function testTableDiff(engine, conn, driver, mangle, changedTable = 't1')
|
||||
|
||||
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_fk'];
|
||||
// const TESTED_COLUMNS = ['col_idx'];
|
||||
// const TESTED_COLUMNS = ['col_def'];
|
||||
// const TESTED_COLUMNS = ['col_std'];
|
||||
@@ -179,11 +183,25 @@ describe('Alter table', () => {
|
||||
)(
|
||||
'Drop column - %s - %s',
|
||||
testWrapper(async (conn, driver, column, engine) => {
|
||||
await testTableDiff(engine, conn, driver, tbl => (tbl.columns = tbl.columns.filter(x => x.columnName != column)));
|
||||
await testTableDiff(engine, conn, driver,
|
||||
tbl => {
|
||||
tbl.columns = tbl.columns.filter(x => x.columnName != column);
|
||||
tbl.foreignKeys = tbl.foreignKeys
|
||||
.map(fk => ({
|
||||
...fk,
|
||||
columns: fk.columns.filter(col => col.columnName != column)
|
||||
}))
|
||||
.filter(fk => fk.columns.length > 0);
|
||||
}
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
test.each(createEnginesColumnsSource(engines.filter(x => !x.skipNullable && !x.skipChangeNullability)))(
|
||||
test.each(
|
||||
createEnginesColumnsSource(engines.filter(x => !x.skipNullability && !x.skipChangeNullability)).filter(
|
||||
([_label, col]) => !col.endsWith('_pk')
|
||||
)
|
||||
)(
|
||||
'Change nullability - %s - %s',
|
||||
testWrapper(async (conn, driver, column, engine) => {
|
||||
await testTableDiff(
|
||||
@@ -202,7 +220,11 @@ describe('Alter table', () => {
|
||||
engine,
|
||||
conn,
|
||||
driver,
|
||||
tbl => (tbl.columns = tbl.columns.map(x => (x.columnName == column ? { ...x, columnName: 'col_renamed' } : x)))
|
||||
tbl => {
|
||||
tbl.columns = tbl.columns.map(x => (x.columnName == column ? { ...x, columnName: 'col_renamed' } : x));
|
||||
tbl.foreignKeys = tbl.foreignKeys.map(fk => ({...fk, columns: fk.columns.map(col => col.columnName == column ? { ...col, columnName: 'col_renamed' } : col)
|
||||
}));
|
||||
}
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
@@ -44,7 +44,7 @@ services:
|
||||
# - 15942:9042
|
||||
#
|
||||
# clickhouse:
|
||||
# image: bitnami/clickhouse:24.8.4
|
||||
# image: bitnamilegacy/clickhouse:24.8.4
|
||||
# restart: always
|
||||
# ports:
|
||||
# - 15005:8123
|
||||
|
||||
@@ -22,7 +22,9 @@ async function connect(engine, database) {
|
||||
if (engine.generateDbFile) {
|
||||
const conn = await driver.connect({
|
||||
...connection,
|
||||
databaseFile: (engine.databaseFileLocationOnServer ?? 'dbtemp/') + database,
|
||||
databaseFile:
|
||||
(engine.databaseFileLocationOnServer ?? (process.env.CITEST ? 'dbtemp/' : 'integration-tests/dbtemp/')) +
|
||||
database,
|
||||
});
|
||||
return conn;
|
||||
} else {
|
||||
|
||||
@@ -26,6 +26,7 @@ import _isDate from 'lodash/isDate';
|
||||
import _isArray from 'lodash/isArray';
|
||||
import _isPlainObject from 'lodash/isPlainObject';
|
||||
import _keys from 'lodash/keys';
|
||||
import _cloneDeep from 'lodash/cloneDeep';
|
||||
import uuidv1 from 'uuid/v1';
|
||||
|
||||
export class SqlDumper implements AlterProcessor {
|
||||
@@ -667,6 +668,68 @@ export class SqlDumper implements AlterProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
sanitizeTableConstraints(table: TableInfo): TableInfo {
|
||||
// Create a deep copy of the table
|
||||
const sanitized = _cloneDeep(table);
|
||||
|
||||
// Get the set of existing column names
|
||||
const existingColumns = new Set(sanitized.columns.map(col => col.columnName));
|
||||
|
||||
// Filter primary key columns to only include existing columns
|
||||
if (sanitized.primaryKey) {
|
||||
const validPkColumns = sanitized.primaryKey.columns.filter(col => existingColumns.has(col.columnName));
|
||||
if (validPkColumns.length === 0) {
|
||||
// If no valid columns remain, remove the primary key entirely
|
||||
sanitized.primaryKey = null;
|
||||
} else if (validPkColumns.length < sanitized.primaryKey.columns.length) {
|
||||
// Update primary key with only valid columns
|
||||
sanitized.primaryKey = {
|
||||
...sanitized.primaryKey,
|
||||
columns: validPkColumns
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Filter sorting key columns to only include existing columns
|
||||
if (sanitized.sortingKey) {
|
||||
const validSkColumns = sanitized.sortingKey.columns.filter(col => existingColumns.has(col.columnName));
|
||||
if (validSkColumns.length === 0) {
|
||||
sanitized.sortingKey = null;
|
||||
} else if (validSkColumns.length < sanitized.sortingKey.columns.length) {
|
||||
sanitized.sortingKey = {
|
||||
...sanitized.sortingKey,
|
||||
columns: validSkColumns
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Filter foreign keys to only include those with all columns present
|
||||
if (sanitized.foreignKeys) {
|
||||
sanitized.foreignKeys = sanitized.foreignKeys.filter(fk =>
|
||||
fk.columns.every(col => existingColumns.has(col.columnName))
|
||||
);
|
||||
}
|
||||
|
||||
// Filter indexes to only include those with all columns present
|
||||
if (sanitized.indexes) {
|
||||
sanitized.indexes = sanitized.indexes.filter(idx =>
|
||||
idx.columns.every(col => existingColumns.has(col.columnName))
|
||||
);
|
||||
}
|
||||
|
||||
// Filter unique constraints to only include those with all columns present
|
||||
if (sanitized.uniques) {
|
||||
sanitized.uniques = sanitized.uniques.filter(uq =>
|
||||
uq.columns.every(col => existingColumns.has(col.columnName))
|
||||
);
|
||||
}
|
||||
|
||||
// Filter dependencies (references from other tables) - these should remain as-is
|
||||
// since they don't affect the CREATE TABLE statement for this table
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
recreateTable(oldTable: TableInfo, newTable: TableInfo) {
|
||||
if (!oldTable.pairingId || !newTable.pairingId || oldTable.pairingId != newTable.pairingId) {
|
||||
throw new Error('Recreate is not possible: oldTable.paringId != newTable.paringId');
|
||||
@@ -681,48 +744,51 @@ export class SqlDumper implements AlterProcessor {
|
||||
}))
|
||||
.filter(x => x.newcol);
|
||||
|
||||
// Create a sanitized version of newTable with constraints that only reference existing columns
|
||||
const sanitizedNewTable = this.sanitizeTableConstraints(newTable);
|
||||
|
||||
if (this.driver.supportsTransactions) {
|
||||
this.dropConstraints(oldTable, true);
|
||||
this.renameTable(oldTable, tmpTable);
|
||||
|
||||
this.createTable(newTable);
|
||||
this.createTable(sanitizedNewTable);
|
||||
|
||||
const autoinc = newTable.columns.find(x => x.autoIncrement);
|
||||
const autoinc = sanitizedNewTable.columns.find(x => x.autoIncrement);
|
||||
if (autoinc) {
|
||||
this.allowIdentityInsert(newTable, true);
|
||||
this.allowIdentityInsert(sanitizedNewTable, true);
|
||||
}
|
||||
|
||||
this.putCmd(
|
||||
'^insert ^into %f (%,i) select %,i ^from %f',
|
||||
newTable,
|
||||
sanitizedNewTable,
|
||||
columnPairs.map(x => x.newcol.columnName),
|
||||
columnPairs.map(x => x.oldcol.columnName),
|
||||
{ ...oldTable, pureName: tmpTable }
|
||||
);
|
||||
|
||||
if (autoinc) {
|
||||
this.allowIdentityInsert(newTable, false);
|
||||
this.allowIdentityInsert(sanitizedNewTable, false);
|
||||
}
|
||||
|
||||
if (this.dialect.dropForeignKey) {
|
||||
newTable.dependencies.forEach(cnt => this.createConstraint(cnt));
|
||||
sanitizedNewTable.dependencies.forEach(cnt => this.createConstraint(cnt));
|
||||
}
|
||||
|
||||
this.dropTable({ ...oldTable, pureName: tmpTable });
|
||||
} else {
|
||||
// we have to preserve old table as long as possible
|
||||
this.createTable({ ...newTable, pureName: tmpTable });
|
||||
this.createTable({ ...sanitizedNewTable, pureName: tmpTable });
|
||||
|
||||
this.putCmd(
|
||||
'^insert ^into %f (%,i) select %,s ^from %f',
|
||||
{ ...newTable, pureName: tmpTable },
|
||||
{ ...sanitizedNewTable, pureName: tmpTable },
|
||||
columnPairs.map(x => x.newcol.columnName),
|
||||
columnPairs.map(x => x.oldcol.columnName),
|
||||
oldTable
|
||||
);
|
||||
|
||||
this.dropTable(oldTable);
|
||||
this.renameTable({ ...newTable, pureName: tmpTable }, newTable.pureName);
|
||||
this.renameTable({ ...sanitizedNewTable, pureName: tmpTable }, newTable.pureName);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+121
-45
@@ -91,8 +91,8 @@ interface AlterOperation_RenameConstraint {
|
||||
}
|
||||
interface AlterOperation_RecreateTable {
|
||||
operationType: 'recreateTable';
|
||||
table: TableInfo;
|
||||
operations: AlterOperation[];
|
||||
oldTable: TableInfo;
|
||||
newTable: TableInfo;
|
||||
}
|
||||
interface AlterOperation_FillPreloadedRows {
|
||||
operationType: 'fillPreloadedRows';
|
||||
@@ -249,11 +249,11 @@ export class AlterPlan {
|
||||
});
|
||||
}
|
||||
|
||||
recreateTable(table: TableInfo, operations: AlterOperation[]) {
|
||||
recreateTable(oldTable: TableInfo, newTable: TableInfo) {
|
||||
this.operations.push({
|
||||
operationType: 'recreateTable',
|
||||
table,
|
||||
operations,
|
||||
oldTable,
|
||||
newTable,
|
||||
});
|
||||
this.recreates.tables += 1;
|
||||
}
|
||||
@@ -337,7 +337,13 @@ export class AlterPlan {
|
||||
return opRes;
|
||||
}),
|
||||
op,
|
||||
];
|
||||
].filter(op => {
|
||||
// filter duplicated drops
|
||||
const existingDrop = this.operations.find(
|
||||
o => o.operationType == 'dropConstraint' && o.oldObject === op['oldObject']
|
||||
);
|
||||
return existingDrop == null;
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -498,53 +504,121 @@ export class AlterPlan {
|
||||
return [];
|
||||
}
|
||||
|
||||
const table = this.wholeNewDb.tables.find(
|
||||
const oldTable = this.wholeOldDb.tables.find(
|
||||
x => x.pureName == op[objectField].pureName && x.schemaName == op[objectField].schemaName
|
||||
);
|
||||
const newTable = this.wholeNewDb.tables.find(
|
||||
x => x.pureName == op[objectField].pureName && x.schemaName == op[objectField].schemaName
|
||||
);
|
||||
this.recreates.tables += 1;
|
||||
return [
|
||||
{
|
||||
operationType: 'recreateTable',
|
||||
table,
|
||||
operations: [op],
|
||||
oldTable,
|
||||
newTable,
|
||||
// operations: [op],
|
||||
},
|
||||
];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
_groupTableRecreations(): AlterOperation[] {
|
||||
const res = [];
|
||||
const recreates = {};
|
||||
_removeRecreatedTableAlters(): AlterOperation[] {
|
||||
const res: AlterOperation[] = [];
|
||||
const recreates = new Set<string>();
|
||||
for (const op of this.operations) {
|
||||
if (op.operationType == 'recreateTable' && op.table) {
|
||||
const existingRecreate = recreates[`${op.table.schemaName}||${op.table.pureName}`];
|
||||
if (existingRecreate) {
|
||||
existingRecreate.operations.push(...op.operations);
|
||||
} else {
|
||||
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 || op.object;
|
||||
if (oldObject) {
|
||||
const recreated = recreates[`${oldObject.schemaName}||${oldObject.pureName}`];
|
||||
if (recreated) {
|
||||
recreated.operations.push(op);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
res.push(op);
|
||||
if (op.operationType == 'recreateTable' && op.oldTable && op.newTable) {
|
||||
const key = `${op.oldTable.schemaName}||${op.oldTable.pureName}`;
|
||||
recreates.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
for (const op of this.operations) {
|
||||
switch (op.operationType) {
|
||||
case 'createColumn':
|
||||
case 'createConstraint':
|
||||
{
|
||||
const key = `${op.newObject.schemaName}||${op.newObject.pureName}`;
|
||||
if (recreates.has(key)) {
|
||||
// skip create inside recreated table
|
||||
continue;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'dropColumn':
|
||||
case 'dropConstraint':
|
||||
case 'changeColumn':
|
||||
{
|
||||
const key = `${op.oldObject.schemaName}||${op.oldObject.pureName}`;
|
||||
if (recreates.has(key)) {
|
||||
// skip drop/change inside recreated table
|
||||
continue;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'renameColumn':
|
||||
{
|
||||
const key = `${op.object.schemaName}||${op.object.pureName}`;
|
||||
if (recreates.has(key)) {
|
||||
// skip rename inside recreated table
|
||||
continue;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
res.push(op);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
_groupTableRecreations(): AlterOperation[] {
|
||||
const res = [];
|
||||
const recreates = new Set<string>();
|
||||
for (const op of this.operations) {
|
||||
if (op.operationType == 'recreateTable' && op.oldTable && op.newTable) {
|
||||
const key = `${op.oldTable.schemaName}||${op.oldTable.pureName}`;
|
||||
if (recreates.has(key)) {
|
||||
// prevent duplicate recreates
|
||||
continue;
|
||||
}
|
||||
recreates.add(key);
|
||||
}
|
||||
|
||||
res.push(op);
|
||||
}
|
||||
return res;
|
||||
|
||||
// const res = [];
|
||||
// const recreates = {};
|
||||
// for (const op of this.operations) {
|
||||
// if (op.operationType == 'recreateTable' && op.table) {
|
||||
// const existingRecreate = recreates[`${op.table.schemaName}||${op.table.pureName}`];
|
||||
// if (existingRecreate) {
|
||||
// existingRecreate.operations.push(...op.operations);
|
||||
// } else {
|
||||
// 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 || op.object;
|
||||
// if (oldObject) {
|
||||
// const recreated = recreates[`${oldObject.schemaName}||${oldObject.pureName}`];
|
||||
// if (recreated) {
|
||||
// recreated.operations.push(op);
|
||||
// continue;
|
||||
// }
|
||||
// }
|
||||
// res.push(op);
|
||||
// }
|
||||
// }
|
||||
// return res;
|
||||
}
|
||||
|
||||
_moveForeignKeysToLast(): AlterOperation[] {
|
||||
if (!this.dialect.createForeignKey) {
|
||||
return this.operations;
|
||||
@@ -611,6 +685,8 @@ export class AlterPlan {
|
||||
|
||||
// console.log('*****************OPERATIONS3', this.operations);
|
||||
|
||||
this.operations = this._removeRecreatedTableAlters();
|
||||
|
||||
this.operations = this._moveForeignKeysToLast();
|
||||
|
||||
// console.log('*****************OPERATIONS4', this.operations);
|
||||
@@ -673,16 +749,16 @@ export function runAlterOperation(op: AlterOperation, processor: AlterProcessor)
|
||||
break;
|
||||
case 'recreateTable':
|
||||
{
|
||||
const oldTable = generateTablePairingId(op.table);
|
||||
const newTable = _.cloneDeep(oldTable);
|
||||
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(oldTable, newTable);
|
||||
// const oldTable = generateTablePairingId(op.table);
|
||||
// const newTable = _.cloneDeep(oldTable);
|
||||
// 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.oldTable, op.newTable);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -467,14 +467,12 @@ await dbgateApi.executeQuery(${JSON.stringify(
|
||||
|
||||
{ divider: true },
|
||||
isSqlOrDoc &&
|
||||
isProApp() &&
|
||||
!connection.isReadOnly &&
|
||||
hasPermission(`dbops/import`) && {
|
||||
onClick: handleImport,
|
||||
text: _t('database.import', { defaultMessage: 'Import' }),
|
||||
},
|
||||
isSqlOrDoc &&
|
||||
isProApp() &&
|
||||
hasPermission(`dbops/export`) && {
|
||||
onClick: handleExport,
|
||||
text: _t('database.export', { defaultMessage: 'Export' }),
|
||||
|
||||
@@ -0,0 +1,356 @@
|
||||
<script lang="ts">
|
||||
import _ from 'lodash';
|
||||
import { tick } from 'svelte';
|
||||
import CellValue from '../datagrid/CellValue.svelte';
|
||||
import { isJsonLikeLongString, safeJsonParse, parseCellValue, stringifyCellValue } from 'dbgate-tools';
|
||||
import keycodes from '../utility/keycodes';
|
||||
import createRef from '../utility/createRef';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
import EditCellDataModal from '../modals/EditCellDataModal.svelte';
|
||||
import ShowFormButton from '../formview/ShowFormButton.svelte';
|
||||
import { openJsonDocument } from '../tabs/JsonTab.svelte';
|
||||
import SearchBoxWrapper from '../elements/SearchBoxWrapper.svelte';
|
||||
import SearchInput from '../elements/SearchInput.svelte';
|
||||
import CloseSearchButton from '../buttons/CloseSearchButton.svelte';
|
||||
import { _t } from '../translations'
|
||||
|
||||
export let selection;
|
||||
|
||||
$: firstSelection = selection?.[0];
|
||||
$: rowData = firstSelection?.rowData;
|
||||
$: editable = firstSelection?.editable;
|
||||
$: editorTypes = firstSelection?.editorTypes;
|
||||
$: columns = selection?.columns || [];
|
||||
$: realColumnUniqueNames = selection?.realColumnUniqueNames || [];
|
||||
$: setCellValue = selection?.setCellValue;
|
||||
|
||||
$: uniqueRows = _.uniqBy(selection || [], 'row');
|
||||
$: isMultipleRows = uniqueRows.length > 1;
|
||||
|
||||
function areValuesEqual(val1, val2) {
|
||||
if (val1 === val2) return true;
|
||||
if (val1 == null && val2 == null) return true;
|
||||
if (val1 == null || val2 == null) return false;
|
||||
return _.isEqual(val1, val2);
|
||||
}
|
||||
|
||||
function getFieldValue(colName) {
|
||||
if (!isMultipleRows) return { value: rowData?.[colName], hasMultipleValues: false };
|
||||
|
||||
const values = uniqueRows.map(sel => sel.rowData?.[colName]);
|
||||
const firstValue = values[0];
|
||||
const allSame = values.every(v => areValuesEqual(v, firstValue));
|
||||
|
||||
return allSame ? { value: firstValue, hasMultipleValues: false } : { value: null, hasMultipleValues: true };
|
||||
}
|
||||
|
||||
let filter = '';
|
||||
|
||||
$: orderedFields = realColumnUniqueNames
|
||||
.map(colName => {
|
||||
const col = columns.find(c => c.uniqueName === colName);
|
||||
if (!col) return null;
|
||||
const { value, hasMultipleValues } = getFieldValue(colName);
|
||||
return {
|
||||
columnName: col.columnName || colName,
|
||||
uniqueName: colName,
|
||||
value,
|
||||
hasMultipleValues,
|
||||
col,
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
$: filteredFields = orderedFields.filter(field => {
|
||||
if (!filter) return true;
|
||||
try {
|
||||
const regex = new RegExp(filter, 'i');
|
||||
return regex.test(field.columnName);
|
||||
} catch (e) {
|
||||
return field.columnName.toLowerCase().includes(filter.toLowerCase());
|
||||
}
|
||||
});
|
||||
|
||||
let editingColumn = null;
|
||||
let editValue = '';
|
||||
let domEditor = null;
|
||||
const isChangedRef = createRef(false);
|
||||
|
||||
function isJsonValue(value) {
|
||||
if (_.isPlainObject(value) && !(value?.type == 'Buffer' && _.isArray(value.data)) && !value.$oid && !value.$bigint && !value.$decimal) {
|
||||
return true;
|
||||
}
|
||||
if (_.isArray(value)) return true;
|
||||
if (typeof value !== 'string') return false;
|
||||
if (!isJsonLikeLongString(value)) return false;
|
||||
const parsed = safeJsonParse(value);
|
||||
return parsed !== null && (_.isPlainObject(parsed) || _.isArray(parsed));
|
||||
}
|
||||
|
||||
function getJsonObject(value) {
|
||||
if (_.isPlainObject(value) || _.isArray(value)) return value;
|
||||
if (typeof value === 'string') return safeJsonParse(value);
|
||||
return null;
|
||||
}
|
||||
|
||||
function handleDoubleClick(field) {
|
||||
if (!editable || !setCellValue) return;
|
||||
if (isJsonValue(field.value) && !field.hasMultipleValues) {
|
||||
openEditModal(field);
|
||||
return;
|
||||
}
|
||||
startEditing(field);
|
||||
}
|
||||
|
||||
function startEditing(field) {
|
||||
if (!editable || !setCellValue) return;
|
||||
editingColumn = field.uniqueName;
|
||||
editValue = field.hasMultipleValues ? '' : stringifyCellValue(field.value, 'inlineEditorIntent', editorTypes).value;
|
||||
isChangedRef.set(false);
|
||||
tick().then(() => {
|
||||
if (!domEditor) return;
|
||||
domEditor.focus();
|
||||
if (!field.hasMultipleValues) domEditor.select();
|
||||
});
|
||||
}
|
||||
|
||||
function handleKeyDown(event, field) {
|
||||
switch (event.keyCode) {
|
||||
case keycodes.escape:
|
||||
isChangedRef.set(false);
|
||||
editingColumn = null;
|
||||
break;
|
||||
case keycodes.enter:
|
||||
if (isChangedRef.get()) {
|
||||
saveValue(field);
|
||||
}
|
||||
editingColumn = null;
|
||||
event.preventDefault();
|
||||
break;
|
||||
case keycodes.tab:
|
||||
if (isChangedRef.get()) {
|
||||
saveValue(field);
|
||||
}
|
||||
editingColumn = null;
|
||||
event.preventDefault();
|
||||
moveToNextField(field, event.shiftKey);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function moveToNextField(field, reverse) {
|
||||
const currentIndex = filteredFields.findIndex(f => f.uniqueName === field.uniqueName);
|
||||
const nextIndex = reverse ? currentIndex - 1 : currentIndex + 1;
|
||||
if (nextIndex < 0 || nextIndex >= filteredFields.length) return;
|
||||
|
||||
tick().then(() => {
|
||||
const nextField = filteredFields[nextIndex];
|
||||
if (isJsonValue(nextField.value)) {
|
||||
openEditModal(nextField);
|
||||
} else {
|
||||
startEditing(nextField);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleSearchKeyDown(e) {
|
||||
if (e.keyCode === keycodes.backspace && (e.metaKey || e.ctrlKey)) {
|
||||
filter = '';
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
function handleBlur(field) {
|
||||
if (isChangedRef.get()) {
|
||||
saveValue(field);
|
||||
}
|
||||
editingColumn = null;
|
||||
}
|
||||
|
||||
function saveValue(field) {
|
||||
if (!setCellValue) return;
|
||||
const parsedValue = parseCellValue(editValue, editorTypes);
|
||||
setCellValue(field.uniqueName, parsedValue);
|
||||
isChangedRef.set(false);
|
||||
}
|
||||
|
||||
function openEditModal(field) {
|
||||
if (!setCellValue) return;
|
||||
showModal(EditCellDataModal, {
|
||||
value: field.value,
|
||||
dataEditorTypesBehaviour: editorTypes,
|
||||
onSave: value => setCellValue(field.uniqueName, value),
|
||||
});
|
||||
}
|
||||
|
||||
function openJsonInNewTab(field) {
|
||||
const jsonObj = getJsonObject(field.value);
|
||||
if (jsonObj) openJsonDocument(jsonObj, undefined, true);
|
||||
}
|
||||
|
||||
function getJsonParsedValue(value) {
|
||||
if (editorTypes?.explicitDataType) return null;
|
||||
if (!isJsonLikeLongString(value)) return null;
|
||||
return safeJsonParse(value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="outer">
|
||||
<div class="content">
|
||||
{#if rowData}
|
||||
<div class="search-wrapper" on:keydown={handleSearchKeyDown}>
|
||||
<SearchBoxWrapper noMargin>
|
||||
<SearchInput placeholder={_t('tableCell.filterColumns', { defaultMessage: "Filter columns (regex)" })} bind:value={filter} />
|
||||
<CloseSearchButton bind:filter />
|
||||
</SearchBoxWrapper>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="inner">
|
||||
{#if !rowData}
|
||||
<div class="no-data">{_t('tableCell.noDataSelected', { defaultMessage: "No data selected" })}</div>
|
||||
{:else}
|
||||
{#each filteredFields as field (field.uniqueName)}
|
||||
<div class="field">
|
||||
<div class="field-name">{field.columnName}</div>
|
||||
<div
|
||||
class="field-value"
|
||||
class:editable
|
||||
on:dblclick={() => handleDoubleClick(field)}
|
||||
>
|
||||
{#if editingColumn === field.uniqueName}
|
||||
<div class="editor-wrapper">
|
||||
<input
|
||||
type="text"
|
||||
bind:this={domEditor}
|
||||
bind:value={editValue}
|
||||
on:input={() => isChangedRef.set(true)}
|
||||
on:keydown={e => handleKeyDown(e, field)}
|
||||
on:blur={() => handleBlur(field)}
|
||||
class="inline-editor"
|
||||
/>
|
||||
{#if editable && !field.hasMultipleValues}
|
||||
<ShowFormButton
|
||||
icon="icon edit"
|
||||
on:click={() => {
|
||||
editingColumn = null;
|
||||
openEditModal(field);
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if field.hasMultipleValues}
|
||||
<span class="multiple-values">({_t('tableCell.multipleValues', { defaultMessage: "Multiple values" })})</span>
|
||||
{:else}
|
||||
<CellValue
|
||||
{rowData}
|
||||
value={field.value}
|
||||
jsonParsedValue={getJsonParsedValue(field.value)}
|
||||
{editorTypes}
|
||||
/>
|
||||
{#if isJsonValue(field.value)}
|
||||
<ShowFormButton
|
||||
icon="icon open-in-new"
|
||||
on:click={() => openJsonInNewTab(field)}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.outer {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.search-wrapper {
|
||||
padding: 4px 4px 0 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.inner {
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
color: var(--theme-font-3);
|
||||
font-style: italic;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.field {
|
||||
margin-bottom: 8px;
|
||||
border: 1px solid var(--theme-border);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.field-name {
|
||||
background: var(--theme-bg-1);
|
||||
padding: 4px 8px;
|
||||
font-weight: 500;
|
||||
font-size: 11px;
|
||||
color: var(--theme-font-2);
|
||||
border-bottom: 1px solid var(--theme-border);
|
||||
}
|
||||
|
||||
.field-value {
|
||||
padding: 6px 8px;
|
||||
background: var(--theme-bg-0);
|
||||
min-height: 20px;
|
||||
word-break: break-all;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.field-value.editable {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.field-value.editable:hover {
|
||||
background: var(--theme-bg-hover);
|
||||
}
|
||||
|
||||
.editor-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.inline-editor {
|
||||
flex: 1;
|
||||
border: none;
|
||||
outline: none;
|
||||
background: var(--theme-bg-0);
|
||||
color: var(--theme-font-1);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.inline-editor:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.multiple-values {
|
||||
color: var(--theme-font-3);
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
@@ -703,7 +703,7 @@ registerCommand({
|
||||
name: __t('command.database.export', { defaultMessage: 'Export database' }),
|
||||
toolbar: true,
|
||||
icon: 'icon export',
|
||||
testEnabled: () => getCurrentDatabase() != null && hasPermission(`dbops/export`) && isProApp(),
|
||||
testEnabled: () => getCurrentDatabase() != null && hasPermission(`dbops/export`),
|
||||
onClick: () => {
|
||||
openImportExportTab({
|
||||
targetStorageType: getDefaultFileFormat(getExtensions()).storageType,
|
||||
|
||||
@@ -1258,9 +1258,27 @@
|
||||
condition: display?.getChangeSetCondition(rowData),
|
||||
insertedRowIndex: grider?.getInsertedRowIndex(row),
|
||||
rowStatus: grider.getRowStatus(row),
|
||||
onSetValue: value => grider.setCellValue(row, column, value),
|
||||
editable: grider.editable,
|
||||
editorTypes: display?.driver?.dataEditorTypesBehaviour,
|
||||
};
|
||||
})
|
||||
.filter(x => x.column);
|
||||
|
||||
res.columns = columns;
|
||||
res.realColumnUniqueNames = realColumnUniqueNames;
|
||||
|
||||
if (res.length > 0) {
|
||||
const uniqueRowIndices = _.uniq(res.map(x => x.row));
|
||||
res.setCellValue = (columnName, value) => {
|
||||
grider.beginUpdate();
|
||||
for (const row of uniqueRowIndices) {
|
||||
grider.setCellValue(row, columnName, value);
|
||||
}
|
||||
grider.endUpdate();
|
||||
};
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,44 @@
|
||||
<script lang="ts" context="module">
|
||||
const LAT_PRIORITY_PATTERNS = [
|
||||
/^lat$/i,
|
||||
/^latitude$/i,
|
||||
/latitude$/i,
|
||||
/lat$/i,
|
||||
/latitude/i,
|
||||
/lat/i,
|
||||
];
|
||||
|
||||
const LON_PRIORITY_PATTERNS = [
|
||||
/^lon$/i,
|
||||
/^lng$/i,
|
||||
/^longitude$/i,
|
||||
/longitude$/i,
|
||||
/lon$/i,
|
||||
/lng$/i,
|
||||
/longitude/i,
|
||||
/lon|lng/i,
|
||||
];
|
||||
|
||||
function getFieldName(fieldPath) {
|
||||
return fieldPath.split('.').pop() || fieldPath;
|
||||
}
|
||||
|
||||
function getFieldPriority(fieldPath, patterns) {
|
||||
const name = getFieldName(fieldPath);
|
||||
for (let i = 0; i < patterns.length; i++) {
|
||||
if (patterns[i].test(name)) return i;
|
||||
}
|
||||
return patterns.length;
|
||||
}
|
||||
|
||||
function sortByPriorityThenLength(paths, patterns) {
|
||||
return paths.sort((a, b) => {
|
||||
const priorityDiff = getFieldPriority(a, patterns) - getFieldPriority(b, patterns);
|
||||
if (priorityDiff !== 0) return priorityDiff;
|
||||
return getFieldName(a).length - getFieldName(b).length;
|
||||
});
|
||||
}
|
||||
|
||||
function findLatLonPaths(obj, attrTest, res = [], prefix = '') {
|
||||
for (const key of Object.keys(obj)) {
|
||||
if (attrTest(key, obj[key])) {
|
||||
@@ -10,11 +50,15 @@
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
export function findLatPaths(obj) {
|
||||
return findLatLonPaths(obj, x => x.toLowerCase()?.includes('lat'));
|
||||
const paths = findLatLonPaths(obj, x => x.toLowerCase()?.includes('lat'));
|
||||
return sortByPriorityThenLength(paths, LAT_PRIORITY_PATTERNS);
|
||||
}
|
||||
|
||||
export function findLonPaths(obj) {
|
||||
return findLatLonPaths(obj, x => x.toLowerCase()?.includes('lon') || x.toLowerCase()?.includes('lng'));
|
||||
const paths = findLatLonPaths(obj, x => x.toLowerCase()?.includes('lon') || x.toLowerCase()?.includes('lng'));
|
||||
return sortByPriorityThenLength(paths, LON_PRIORITY_PATTERNS);
|
||||
}
|
||||
export function findAllObjectPaths(obj) {
|
||||
return findLatLonPaths(obj, (_k, v) => v != null && !_.isNaN(Number(v)));
|
||||
|
||||
@@ -95,7 +95,6 @@
|
||||
title: _t('common.exportDatabase', { defaultMessage: 'Export database' }),
|
||||
description: _t('newObject.exportDescription', { defaultMessage: 'Export to file like CSV, JSON, Excel, or other DB' }),
|
||||
command: 'database.export',
|
||||
isProFeature: true,
|
||||
testid: 'NewObjectModal_databaseExport',
|
||||
disabledMessage: _t('newObject.exportDisabled', { defaultMessage: 'Export is not available for current database' }),
|
||||
},
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import ManagerInnerContainer from '../elements/ManagerInnerContainer.svelte';
|
||||
import FontIcon from '../icons/FontIcon.svelte';
|
||||
import PerspectiveFiltersColumn from './PerspectiveFiltersColumn.svelte';
|
||||
import { _t } from '../translations';
|
||||
|
||||
export let managerSize;
|
||||
export let config: PerspectiveConfig;
|
||||
@@ -25,8 +26,8 @@
|
||||
<ManagerInnerContainer width={managerSize} isFlex={filterCount == 0}>
|
||||
{#if filterCount == 0}
|
||||
<div class="msg">
|
||||
<div class="mb-3 bold">No Filters defined</div>
|
||||
<div><FontIcon icon="img info" /> Use context menu, command "Add to filter" in table or in tree</div>
|
||||
<div class="mb-3 bold">{_t('perspective.noFiltersDefined', { defaultMessage: "No Filters defined" })}</div>
|
||||
<div><FontIcon icon="img info" /> {_t('perspective.useContextMenuAddToFilter', { defaultMessage: 'Use context menu, command "Add to filter" in table or in tree' })}</div>
|
||||
</div>
|
||||
{:else}
|
||||
{#each config.nodes as nodeConfig}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
|
||||
registerCommand({
|
||||
id: 'perspective.openJson',
|
||||
category: 'Perspective',
|
||||
name: 'Open JSON',
|
||||
category: __t('command.perspective', { defaultMessage: 'Perspective' }),
|
||||
name: __t('command.perspective.openJson', { defaultMessage: 'Open JSON' }),
|
||||
isRelatedToTab: true,
|
||||
testEnabled: () => getCurrentEditor()?.openJsonEnabled(),
|
||||
onClick: () => getCurrentEditor().openJson(),
|
||||
@@ -40,6 +40,7 @@
|
||||
import openNewTab from '../utility/openNewTab';
|
||||
import { getFilterValueExpression } from 'dbgate-filterparser';
|
||||
import StatusBarTabItem from '../widgets/StatusBarTabItem.svelte';
|
||||
import { __t } from '../translations';
|
||||
|
||||
const TABS_BY_FIELD = {
|
||||
tables: {
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
|
||||
registerCommand({
|
||||
id: 'perspective.customJoin',
|
||||
category: 'Perspective',
|
||||
name: 'Custom join',
|
||||
category: __t('perspective.category', { defaultMessage: 'Perspective' }),
|
||||
name: __t('perspective.customJoin', { defaultMessage: 'Custom join' }),
|
||||
keyText: 'CtrlOrCommand+J',
|
||||
isRelatedToTab: true,
|
||||
icon: 'icon custom-join',
|
||||
@@ -65,6 +65,7 @@
|
||||
import FontIcon from '../icons/FontIcon.svelte';
|
||||
import InlineButton from '../buttons/InlineButton.svelte';
|
||||
import { usePerspectiveDataPatterns } from '../utility/usePerspectiveDataPatterns';
|
||||
import { _t, __t } from '../translations';
|
||||
|
||||
const dbg = debug('dbgate:PerspectiveView');
|
||||
|
||||
@@ -168,7 +169,7 @@
|
||||
<HorizontalSplitter initialValue={getInitialManagerSize()} bind:size={managerSize} allowCollapseChild1>
|
||||
<div class="left" slot="1">
|
||||
<WidgetColumnBar>
|
||||
<WidgetColumnBarItem title="Choose data" name="perspectiveTree" height={'70%'}>
|
||||
<WidgetColumnBarItem title={_t('perspective.chooseData', { defaultMessage: "Choose data" })} name="perspectiveTree" height={'70%'}>
|
||||
{#if tempRoot && tempRoot != root}
|
||||
<div class="temp-root">
|
||||
<div>
|
||||
@@ -184,7 +185,7 @@
|
||||
{/if}
|
||||
|
||||
<SearchBoxWrapper>
|
||||
<SearchInput placeholder="Search column or table" bind:value={filter} />
|
||||
<SearchInput placeholder={_t('perspective.searchColumnOrTable', { defaultMessage: "Search column or table" })} bind:value={filter} />
|
||||
<CloseSearchButton bind:filter />
|
||||
</SearchBoxWrapper>
|
||||
|
||||
@@ -195,7 +196,7 @@
|
||||
</ManagerInnerContainer>
|
||||
</WidgetColumnBarItem>
|
||||
|
||||
<WidgetColumnBarItem title="Filters" name="tableFilters">
|
||||
<WidgetColumnBarItem title={_t('perspective.filters', { defaultMessage: "Filters" })} name="tableFilters">
|
||||
<PerspectiveFilters {managerSize} {config} {setConfig} {conid} {database} {driver} {root} />
|
||||
</WidgetColumnBarItem>
|
||||
</WidgetColumnBar>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
import useEffect from '../utility/useEffect';
|
||||
import { getContext } from 'svelte';
|
||||
import { mountCodeCompletion } from './codeCompletion';
|
||||
import { getCurrentSettings } from '../stores';
|
||||
import { currentEditorWrapEnabled, getCurrentSettings } from '../stores';
|
||||
export let engine = null;
|
||||
export let conid = null;
|
||||
export let database = null;
|
||||
@@ -29,6 +29,8 @@
|
||||
mode = engineToMode[match ? match[1] : engine] || 'sql';
|
||||
}
|
||||
|
||||
$: enableWrap = $currentEditorWrapEnabled || false;
|
||||
|
||||
export function getEditor(): ace.Editor {
|
||||
return domEditor.getEditor();
|
||||
}
|
||||
@@ -63,5 +65,6 @@
|
||||
options={{
|
||||
...$$props.options,
|
||||
enableBasicAutocompletion: true,
|
||||
wrap: enableWrap,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
<div class="wrapper">
|
||||
<div class="heading">{_t('settings.sqlEditor', { defaultMessage: 'SQL editor' })}</div>
|
||||
|
||||
<div class="flex">
|
||||
<div class="col-3">
|
||||
<FormSelectField
|
||||
label={_t('settings.sqlEditor.sqlCommandsCase', { defaultMessage: 'SQL commands case' })}
|
||||
name="sqlEditor.sqlCommandsCase"
|
||||
@@ -28,8 +26,6 @@
|
||||
]}
|
||||
data-testid="SQLEditorSettings_sqlCommandsCase"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<FormFieldTemplateLarge
|
||||
label={_t('settings.editor.keybinds', { defaultMessage: 'Editor keybinds' })}
|
||||
type="combo"
|
||||
@@ -42,53 +38,49 @@
|
||||
on:change={e => ($currentEditorKeybindigMode = e.detail)}
|
||||
/>
|
||||
</FormFieldTemplateLarge>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<FormFieldTemplateLarge
|
||||
label={_t('settings.editor.wordWrap', { defaultMessage: 'Enable word wrap' })}
|
||||
type="combo"
|
||||
type="checkbox"
|
||||
>
|
||||
<CheckboxField
|
||||
checked={$currentEditorWrapEnabled}
|
||||
on:change={e => ($currentEditorWrapEnabled = e.target.checked)}
|
||||
/>
|
||||
</FormFieldTemplateLarge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormTextField
|
||||
name="sqlEditor.limitRows"
|
||||
label={_t('settings.sqlEditor.limitRows', { defaultMessage: 'Return only N rows from query' })}
|
||||
placeholder={_t('settings.sqlEditor.limitRowsPlaceholder', { defaultMessage: '(No rows limit)' })}
|
||||
/>
|
||||
<FormTextField
|
||||
name="sqlEditor.limitRows"
|
||||
label={_t('settings.sqlEditor.limitRows', { defaultMessage: 'Return only N rows from query' })}
|
||||
placeholder={_t('settings.sqlEditor.limitRowsPlaceholder', { defaultMessage: '(No rows limit)' })}
|
||||
/>
|
||||
|
||||
<FormCheckboxField
|
||||
name="sqlEditor.showTableAliasesInCodeCompletion"
|
||||
label={_t('settings.sqlEditor.showTableAliasesInCodeCompletion', {
|
||||
defaultMessage: 'Show table aliases in code completion',
|
||||
})}
|
||||
defaultValue={false}
|
||||
/>
|
||||
<FormCheckboxField
|
||||
name="sqlEditor.showTableAliasesInCodeCompletion"
|
||||
label={_t('settings.sqlEditor.showTableAliasesInCodeCompletion', {
|
||||
defaultMessage: 'Show table aliases in code completion',
|
||||
})}
|
||||
defaultValue={false}
|
||||
/>
|
||||
|
||||
<FormCheckboxField
|
||||
name="sqlEditor.disableSplitByEmptyLine"
|
||||
label={_t('settings.sqlEditor.disableSplitByEmptyLine', { defaultMessage: 'Disable split by empty line' })}
|
||||
defaultValue={false}
|
||||
/>
|
||||
<FormCheckboxField
|
||||
name="sqlEditor.disableSplitByEmptyLine"
|
||||
label={_t('settings.sqlEditor.disableSplitByEmptyLine', { defaultMessage: 'Disable split by empty line' })}
|
||||
defaultValue={false}
|
||||
/>
|
||||
|
||||
<FormCheckboxField
|
||||
name="sqlEditor.disableExecuteCurrentLine"
|
||||
label={_t('settings.sqlEditor.disableExecuteCurrentLine', {
|
||||
defaultMessage: 'Disable current line execution (Execute current)',
|
||||
})}
|
||||
defaultValue={false}
|
||||
/>
|
||||
<FormCheckboxField
|
||||
name="sqlEditor.disableExecuteCurrentLine"
|
||||
label={_t('settings.sqlEditor.disableExecuteCurrentLine', {
|
||||
defaultMessage: 'Disable current line execution (Execute current)',
|
||||
})}
|
||||
defaultValue={false}
|
||||
/>
|
||||
|
||||
<FormCheckboxField
|
||||
name="sqlEditor.hideColumnsPanel"
|
||||
label={_t('settings.sqlEditor.hideColumnsPanel', { defaultMessage: 'Hide Columns/Filters panel by default' })}
|
||||
defaultValue={false}
|
||||
/>
|
||||
<FormCheckboxField
|
||||
name="sqlEditor.hideColumnsPanel"
|
||||
label={_t('settings.sqlEditor.hideColumnsPanel', { defaultMessage: 'Hide Columns/Filters panel by default' })}
|
||||
defaultValue={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@@ -102,4 +94,8 @@ defaultValue={false}
|
||||
.wrapper :global(input){
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.wrapper :global(select) {
|
||||
max-width: 400px;
|
||||
}
|
||||
</style>
|
||||
@@ -16,7 +16,7 @@
|
||||
registerCommand({
|
||||
id: 'jsonl.save',
|
||||
group: 'save',
|
||||
category: 'JSON Lines editor',
|
||||
category: __t('command.jsonl', { defaultMessage: 'JSON Lines editor' }),
|
||||
name: __t('command.jsonl.save', { defaultMessage: 'Save' }),
|
||||
toolbar: true,
|
||||
isRelatedToTab: true,
|
||||
@@ -27,7 +27,7 @@
|
||||
|
||||
registerCommand({
|
||||
id: 'jsonl.preview',
|
||||
category: 'JSON Lines editor',
|
||||
category: __t('command.jsonl', { defaultMessage: 'JSON Lines editor' }),
|
||||
name: __t('command.jsonl.preview', { defaultMessage: 'Preview' }),
|
||||
icon: 'icon preview',
|
||||
keyText: 'F5',
|
||||
@@ -37,7 +37,7 @@
|
||||
|
||||
registerCommand({
|
||||
id: 'jsonl.previewNewTab',
|
||||
category: 'JSON Lines editor',
|
||||
category: __t('command.jsonl', { defaultMessage: 'JSON Lines editor' }),
|
||||
name: __t('command.jsonl.previewNewTab', { defaultMessage: 'Preview in new tab' }),
|
||||
icon: 'icon preview',
|
||||
testEnabled: () => getCurrentEditor() != null,
|
||||
@@ -46,7 +46,7 @@
|
||||
|
||||
registerCommand({
|
||||
id: 'jsonl.closePreview',
|
||||
category: 'JSON Lines editor',
|
||||
category: __t('command.jsonl', { defaultMessage: 'JSON Lines editor' }),
|
||||
name: __t('command.jsonl.closePreview', { defaultMessage: 'Close preview' }),
|
||||
icon: 'icon close',
|
||||
testEnabled: () => getCurrentEditor()?.isPreview(),
|
||||
|
||||
@@ -170,6 +170,9 @@
|
||||
import QueryAiAssistant from '../ai/QueryAiAssistant.svelte';
|
||||
import { getCurrentSettings } from '../stores';
|
||||
import { Messages } from 'openai/resources/chat/completions';
|
||||
import WidgetColumnBar from '../widgets/WidgetColumnBar.svelte';
|
||||
import WidgetsInnerContainer from '../widgets/WidgetsInnerContainer.svelte';
|
||||
import WidgetColumnBarItem from '../widgets/WidgetColumnBarItem.svelte';
|
||||
|
||||
export let tabid;
|
||||
export let conid;
|
||||
@@ -717,9 +720,6 @@
|
||||
...driver?.getQuerySplitterOptions('editor'),
|
||||
splitByEmptyLine: !$settingsValue?.['sqlEditor.disableSplitByEmptyLine'],
|
||||
}}
|
||||
options={{
|
||||
wrap: enableWrap,
|
||||
}}
|
||||
value={$editorState.value || ''}
|
||||
menu={createMenu()}
|
||||
on:input={e => {
|
||||
@@ -791,33 +791,44 @@
|
||||
</VerticalSplitter>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="2">
|
||||
<QueryAiAssistant
|
||||
bind:this={domAiAssistant}
|
||||
{conid}
|
||||
{database}
|
||||
{driver}
|
||||
onClose={() => {
|
||||
isAiAssistantVisible = false;
|
||||
}}
|
||||
text={$editorValue}
|
||||
getLine={() => domEditor.getEditor().getSelectionRange().start.row}
|
||||
onInsertAtCursor={text => {
|
||||
const editor = domEditor.getEditor();
|
||||
editor.session.insert(editor.getCursorPosition(), text);
|
||||
domEditor?.getEditor()?.focus();
|
||||
}}
|
||||
getTextOrSelectedText={() => domEditor.getEditor().getSelectedText() || $editorValue}
|
||||
onSetSelectedText={text => {
|
||||
const editor = domEditor.getEditor();
|
||||
if (editor.getSelectedText()) {
|
||||
const range = editor.selection.getRange();
|
||||
editor.session.replace(range, text);
|
||||
} else {
|
||||
editor.setValue(text);
|
||||
}
|
||||
}}
|
||||
{tabid}
|
||||
/>
|
||||
<WidgetColumnBar>
|
||||
<WidgetColumnBarItem
|
||||
title={_t('query.AiAssistant', { defaultMessage: 'AI Assistant' })}
|
||||
onClose={() => {
|
||||
isAiAssistantVisible = false;
|
||||
}}
|
||||
>
|
||||
<WidgetsInnerContainer skipDefineWidth flexContainer>
|
||||
<QueryAiAssistant
|
||||
bind:this={domAiAssistant}
|
||||
{conid}
|
||||
{database}
|
||||
{driver}
|
||||
onClose={() => {
|
||||
isAiAssistantVisible = false;
|
||||
}}
|
||||
text={$editorValue}
|
||||
getLine={() => domEditor.getEditor().getSelectionRange().start.row}
|
||||
onInsertAtCursor={text => {
|
||||
const editor = domEditor.getEditor();
|
||||
editor.session.insert(editor.getCursorPosition(), text);
|
||||
domEditor?.getEditor()?.focus();
|
||||
}}
|
||||
getTextOrSelectedText={() => domEditor.getEditor().getSelectedText() || $editorValue}
|
||||
onSetSelectedText={text => {
|
||||
const editor = domEditor.getEditor();
|
||||
if (editor.getSelectedText()) {
|
||||
const range = editor.selection.getRange();
|
||||
editor.session.replace(range, text);
|
||||
} else {
|
||||
editor.setValue(text);
|
||||
}
|
||||
}}
|
||||
{tabid}
|
||||
/>
|
||||
</WidgetsInnerContainer>
|
||||
</WidgetColumnBarItem>
|
||||
</WidgetColumnBar>
|
||||
</svelte:fragment>
|
||||
</HorizontalSplitter>
|
||||
<svelte:fragment slot="toolstrip">
|
||||
@@ -839,11 +850,17 @@
|
||||
},
|
||||
})}
|
||||
>
|
||||
{queryRowsLimit ? _t('query.limitRows', { defaultMessage: 'Limit {queryRowsLimit} rows', values: { queryRowsLimit } }) : _t('query.unlimitedRows', { defaultMessage: 'Unlimited rows' })}</ToolStripButton
|
||||
{queryRowsLimit
|
||||
? _t('query.limitRows', { defaultMessage: 'Limit {queryRowsLimit} rows', values: { queryRowsLimit } })
|
||||
: _t('query.unlimitedRows', { defaultMessage: 'Unlimited rows' })}</ToolStripButton
|
||||
>
|
||||
{/if}
|
||||
{#if resultCount == 1}
|
||||
<ToolStripExportButton command="jslTableGrid.export" {quickExportHandlerRef} label={_t('export.result', { defaultMessage: 'Export result' })} />
|
||||
<ToolStripExportButton
|
||||
command="jslTableGrid.export"
|
||||
{quickExportHandlerRef}
|
||||
label={_t('export.result', { defaultMessage: 'Export result' })}
|
||||
/>
|
||||
{/if}
|
||||
<ToolStripDropDownButton
|
||||
menu={() =>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { isProApp } from './proTools';
|
||||
export function createQuickExportMenuItems(handler: (fmt: QuickExportDefinition) => Function, advancedExportMenuItem) {
|
||||
const extensions = getExtensions();
|
||||
return [
|
||||
isProApp() && {
|
||||
{
|
||||
text: _t('export.exportAdvanced', { defaultMessage : 'Export advanced...'}),
|
||||
...advancedExportMenuItem,
|
||||
},
|
||||
|
||||
@@ -14,6 +14,12 @@
|
||||
component: TextCellViewNoWrap,
|
||||
single: false,
|
||||
},
|
||||
{
|
||||
type: 'table',
|
||||
title: 'Table - Row',
|
||||
component: TableCellView,
|
||||
single: false,
|
||||
},
|
||||
{
|
||||
type: 'json',
|
||||
title: 'Json',
|
||||
@@ -92,6 +98,7 @@
|
||||
import JsonRowView from '../celldata/JsonRowView.svelte';
|
||||
import MapCellView from '../celldata/MapCellView.svelte';
|
||||
import PictureCellView from '../celldata/PictureCellView.svelte';
|
||||
import TableCellView from '../celldata/TableCellView.svelte';
|
||||
import TextCellViewNoWrap from '../celldata/TextCellViewNoWrap.svelte';
|
||||
import TextCellViewWrap from '../celldata/TextCellViewWrap.svelte';
|
||||
import ErrorInfo from '../elements/ErrorInfo.svelte';
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
import { getLocalStorage, setLocalStorage } from '../utility/storageCache';
|
||||
|
||||
export let title;
|
||||
export let name;
|
||||
export let skip = false;
|
||||
export let positiveCondition = true;
|
||||
export let height = null;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
export let hideContent = false;
|
||||
export let fixedWidth = 0;
|
||||
export let skipDefineWidth = false;
|
||||
export let flexContainer = false;
|
||||
|
||||
export function scrollTop() {
|
||||
domDiv.scrollTop = 0;
|
||||
@@ -14,6 +15,7 @@
|
||||
on:drop
|
||||
bind:this={domDiv}
|
||||
class:hideContent
|
||||
class:flexContainer
|
||||
class:leftFixedWidth={!fixedWidth && !skipDefineWidth}
|
||||
data-testid={$$props['data-testid']}
|
||||
style:width={fixedWidth ? `${fixedWidth}px` : undefined}
|
||||
@@ -35,4 +37,8 @@
|
||||
div.hideContent {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
div.flexContainer {
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,6 +6,56 @@ const lineReader = require('line-reader');
|
||||
|
||||
let dbgateApi;
|
||||
|
||||
class StripUtf8BomTransform extends stream.Transform {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
this._checkedBOM = false;
|
||||
this._pending = Buffer.alloc(0); // store initial bytes until we know if BOM is present
|
||||
}
|
||||
|
||||
_transform(chunk, encoding, callback) {
|
||||
if (this._checkedBOM) {
|
||||
// We already handled BOM decision, just pass through
|
||||
this.push(chunk);
|
||||
return callback();
|
||||
}
|
||||
|
||||
// Accumulate into pending until we can decide
|
||||
this._pending = Buffer.concat([this._pending, chunk]);
|
||||
|
||||
if (this._pending.length < 3) {
|
||||
// Still don't know if it's BOM or not (need at least 3 bytes)
|
||||
return callback();
|
||||
}
|
||||
|
||||
// Now we can check the first 3 bytes
|
||||
const BOM = [0xef, 0xbb, 0xbf];
|
||||
const hasBom = this._pending[0] === BOM[0] && this._pending[1] === BOM[1] && this._pending[2] === BOM[2];
|
||||
|
||||
if (hasBom) {
|
||||
// Drop the BOM, push the rest
|
||||
this.push(this._pending.slice(3));
|
||||
} else {
|
||||
// No BOM, push everything as-is
|
||||
this.push(this._pending);
|
||||
}
|
||||
|
||||
this._pending = Buffer.alloc(0);
|
||||
this._checkedBOM = true;
|
||||
callback();
|
||||
}
|
||||
|
||||
_flush(callback) {
|
||||
// Stream ended but we never had enough bytes to decide (length < 3)
|
||||
if (!this._checkedBOM && this._pending.length > 0) {
|
||||
// If it's less than 3 bytes, it can't be a UTF-8 BOM, so just pass it through
|
||||
this.push(this._pending);
|
||||
}
|
||||
this._pending = Buffer.alloc(0);
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
function readFirstLine(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
lineReader.open(file, (err, reader) => {
|
||||
@@ -95,7 +145,7 @@ async function reader({ fileName, encoding = 'utf-8', header = true, delimiter,
|
||||
});
|
||||
const fileStream = fs.createReadStream(downloadedFile, encoding);
|
||||
const csvPrepare = new CsvPrepareStream({ header });
|
||||
return [fileStream, csvStream, csvPrepare];
|
||||
return [fileStream, new StripUtf8BomTransform(), csvStream, csvPrepare];
|
||||
// fileStream.pipe(csvStream);
|
||||
// csvStream.pipe(csvPrepare);
|
||||
// return csvPrepare;
|
||||
|
||||
@@ -46,6 +46,8 @@ const dialect = {
|
||||
dropReferencesWhenDropTable: false,
|
||||
requireStandaloneSelectForScopeIdentity: true,
|
||||
|
||||
dropColumnDependencies: ['dependencies'],
|
||||
|
||||
columnProperties: {
|
||||
columnComment: true,
|
||||
isUnsigned: true,
|
||||
|
||||
@@ -64,6 +64,21 @@ class Dumper extends SqlDumper {
|
||||
this.putCmd('^alter ^table %f ^rename ^column %i ^to %i', column, column.columnName, newcol);
|
||||
}
|
||||
|
||||
createForeignKeyFore(fk) {
|
||||
if (fk.constraintName != null && !this.dialect.anonymousForeignKey) {
|
||||
this.put('^constraint %i ', fk.constraintName);
|
||||
}
|
||||
this.put(
|
||||
'^foreign ^key (%,i) ^references %f (%,i)',
|
||||
fk.columns.map(x => x.columnName),
|
||||
{ schemaName: fk.refSchemaName, pureName: fk.refTableName },
|
||||
fk.columns.map(x => x.refColumnName)
|
||||
);
|
||||
if (fk.deleteAction && fk.deleteAction.toUpperCase() !== 'NO ACTION') {
|
||||
this.put(' ^on ^delete %k', fk.deleteAction);
|
||||
}
|
||||
}
|
||||
|
||||
// dropTable(obj, options = {}) {
|
||||
// this.put('^drop ^table');
|
||||
// if (options.testIfExists) this.put(' ^if ^exists');
|
||||
|
||||
@@ -129,6 +129,7 @@ class Analyser extends DatabaseAnalyser {
|
||||
updateAction: fkcol.on_update,
|
||||
deleteAction: fkcol.on_delete,
|
||||
constraintName: `FK_${tableName}_${fkcol.id}`,
|
||||
constraintType: 'foreignKey',
|
||||
};
|
||||
return fk;
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@ checkout-and-merge-pro:
|
||||
repository: dbgate/dbgate-pro
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
path: dbgate-pro
|
||||
ref: ae1fcf6e61c6f7dfbb21005daa259c68e899a80a
|
||||
ref: 4401bfe2a52019448fd14a999eb26bc4e7b5341f
|
||||
- name: Merge dbgate/dbgate-pro
|
||||
run: |
|
||||
mkdir ../dbgate-pro
|
||||
|
||||
Reference in New Issue
Block a user