Compare commits

...

51 Commits

Author SHA1 Message Date
Stela Augustinova 4682255d5f Refactor SQL editor settings layout and update word wrap field type 2025-12-15 13:02:11 +01:00
Stela Augustinova a503898b21 Refactor SQL editor to integrate word wrap settings and remove redundant options in QueryTab 2025-12-15 12:44:04 +01:00
Stela Augustinova 21352dae07 Revert "Implement word wrap feature in SQL editor and settings"
This reverts commit 28aa86f0aa.
2025-12-15 12:37:15 +01:00
Stela Augustinova 28aa86f0aa Implement word wrap feature in SQL editor and settings 2025-12-14 17:26:25 +01:00
SPRINX0\prochazka a71129df4b SYNC: AI assistant 2025-12-10 07:13:16 +00:00
SPRINX0\prochazka de6acfa1ce Revert "Revert "MPR archive""
This reverts commit ccf075dc65.
2025-12-10 07:48:30 +01:00
SPRINX0\prochazka ccf075dc65 Revert "MPR archive"
This reverts commit 391d04b45c.
2025-12-10 07:36:03 +01:00
SPRINX0\prochazka 1d8ac3cf86 Revert "MPR advanced exports"
This reverts commit 864797fc99.
2025-12-10 07:36:03 +01:00
Jan Prochazka 7a8ff89c5c Merge pull request #1293 from dbgate/feature/FK-test
Feature/fk test
2025-12-09 16:22:29 +01:00
CI workflows eda70def2a chore: auto-update github workflows 2025-12-09 15:02:50 +00:00
CI workflows 08fd75edc7 Update pro ref 2025-12-09 15:02:30 +00:00
Jan Prochazka 15ea53864f SYNC: Merge pull request #18 from dbgate/feature/translation5 2025-12-09 15:02:19 +00:00
Jan Prochazka 377cd64556 Revert "try to comment out earlier patch"
This reverts commit 955ca99cf3.
2025-12-09 12:56:47 +01:00
Stela Augustinova b37744d574 Merge pull request #1296 from dbgate/feature/map-autodetect-lat-lon
Feature/map autodetect lat lon
2025-12-09 11:01:49 +01:00
Stela Augustinova a7f21fe0c6 Merge pull request #1292 from dbgate/feature/table-cell-data-view
Feature/table cell data view
2025-12-09 11:00:19 +01:00
Jan Prochazka 955ca99cf3 try to comment out earlier patch 2025-12-09 10:46:25 +01:00
Jan Prochazka 98f5bb4124 sanitize constraints 2025-12-09 10:45:38 +01:00
Jan Prochazka b3943f005d alter table fixed 2025-12-08 17:35:29 +01:00
Jan Prochazka 8d4178b984 grouped table recreates 2025-12-08 16:57:09 +01:00
Stela Augustinova 2a88ed38c4 Added translation tags to TableCellView component, updated decimal handling 2025-12-08 16:45:18 +01:00
Jan Prochazka 52dce7dfd3 disabled grouping recreate table OPs 2025-12-08 16:41:55 +01:00
Stela Augustinova 6ebee92542 Merge branch 'master' into feature/table-cell-data-view 2025-12-08 16:07:55 +01:00
Jan Prochazka 1b5646f526 Revert "fix: correct reference from wholeNewDb to wholeOldDb in AlterPlan class"
This reverts commit 12e6afbaad.
2025-12-08 16:05:50 +01:00
Jan Prochazka 7024e4b40d Merge pull request #1289 from dbgate/feature/postgresql-decimal
Postgresql decimal
2025-12-08 15:42:19 +01:00
Jan Prochazka bc2e27d7da Merge pull request #1290 from david-pivonka/feature/table-cell-data-view
Add Table format to Cell data view sidebar
2025-12-08 15:35:35 +01:00
Jan Prochazka 189da2bfe2 Merge pull request #1291 from david-pivonka/fix/map-autodetect-lat-lon
Improve Map view lat/lon field autodetection
2025-12-08 15:33:58 +01:00
Stela Augustinova 12e6afbaad fix: correct reference from wholeNewDb to wholeOldDb in AlterPlan class 2025-12-08 15:27:45 +01:00
David Pivoňka 142ebe3d27 Fix scrolling in Table - Row view
Use absolute positioning pattern for proper scrolling behavior
when many columns are displayed.
2025-12-08 15:23:59 +01:00
Stela Augustinova 7579f6e42a fix: comment out incremental analysis in testTableDiff and correct clickhouse image reference 2025-12-08 15:19:27 +01:00
David Pivoňka 38c25cae74 Add multi-row selection support with bulk editing
- Show "(Multiple values)" when selected rows have different values
- Allow bulk editing: changes apply to all selected rows
- Rename format to "Table - Row" for clarity
2025-12-08 15:01:02 +01:00
David Pivoňka 408496eb7c Improve Map view lat/lon field autodetection
Prioritize field names that are more likely to be actual latitude/longitude
fields instead of randomly selecting the first numeric field containing
"lat" or "lon" in its name.
2025-12-08 14:42:56 +01:00
Jan Prochazka 4d61c74a8b fixed tes on CI 2025-12-08 14:34:45 +01:00
David Pivoňka 190c610466 Add column filter/search to Table cell data view
Adds a search input at the top of the Table view that filters columns
by name with regex support.
2025-12-08 14:31:38 +01:00
Jan Prochazka 85b7e3ebe3 fixed sqlite test folder 2025-12-08 14:18:28 +01:00
David Pivoňka d220525ac7 Use braces for isChangedRef.get() blocks to match codebase style 2025-12-08 13:47:35 +01:00
David Pivoňka 5e4a631ff2 Remove comments and apply early return pattern 2025-12-08 13:43:41 +01:00
David Pivoňka 9099ce42b9 Add Table format to Cell data view sidebar
Adds a new "Table" format option to the Cell data view widget that
displays the selected row as a vertical list with column names above
values, inspired by TablePlus.

Features:
- Shows all columns from the selected row in grid display order
- Inline editing support for regular values (double-click to edit)
- JSON values open Edit Cell modal on double-click
- Open-in-new button for JSON values to view in JSON tab
2025-12-08 13:37:55 +01:00
Jan Prochazka df226fea22 import test - greater timeout 2025-12-08 13:12:08 +01:00
Jan Prochazka 851d2e9151 fixed double drop constraint 2025-12-08 13:05:38 +01:00
SPRINX0\prochazka 89121a2608 handled UTF-8 BOM in CSV input 2025-12-04 16:44:08 +01:00
SPRINX0\prochazka 23cf264d4d fix 2025-12-04 16:29:06 +01:00
Stela Augustinova b3130225b5 Filter out primary key columns in nullability change tests 2025-12-04 14:53:08 +01:00
Stela Augustinova 65512defed Merge branch 'master' into feature/FK-test 2025-12-04 14:36:14 +01:00
Stela Augustinova 3b1c8748f1 Add createForeignKeyFore method to handle foreign key creation (Oracle) 2025-12-04 14:34:26 +01:00
Stela Augustinova aba660eddb Fix nullability filter in alter table tests 2025-12-04 14:08:22 +01:00
Stela Augustinova 137eac7dbf Add dropColumnDependencies property to dialect configuration 2025-12-04 14:07:55 +01:00
Stela Augustinova fdbd08f511 Added FK constraint type for sqlite 2025-12-04 13:00:06 +01:00
Stela Augustinova ace1cec1f6 Delete FK from drop column 2025-12-04 10:00:48 +01:00
Stela Augustinova fa5fda0c3b Incremental analysis 2025-12-03 15:31:05 +01:00
Stela Augustinova 251609e274 Update foreign key references when dropping or renaming columns in alter table tests 2025-12-03 15:21:12 +01:00
Stela Augustinova c0287e49d8 FK test 2025-12-03 14:28:58 +01:00
35 changed files with 849 additions and 169 deletions
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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
+2 -2
View File
@@ -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');
});
});
+32 -10
View File
@@ -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)
}));
}
);
})
);
+1 -1
View File
@@ -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
+3 -1
View File
@@ -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 {
+75 -9
View File
@@ -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
View File
@@ -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>
+1 -1
View File
@@ -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>
+4 -1
View File
@@ -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(),
+49 -32
View File
@@ -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;
});
+1 -1
View File
@@ -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