diff --git a/packages/datalib/src/ChangeSet.ts b/packages/datalib/src/ChangeSet.ts index fd37c153..19c4186a 100644 --- a/packages/datalib/src/ChangeSet.ts +++ b/packages/datalib/src/ChangeSet.ts @@ -1,4 +1,5 @@ import _ from 'lodash'; +import { Command, Insert, Update, Delete, UpdateField, Condition } from '@dbgate/sqltree'; export interface ChangeSetItem { pureName: string; @@ -100,3 +101,81 @@ export function setChangeSetValue( ], }; } + +function extractFields(item: ChangeSetItem): UpdateField[] { + return _.keys(item.fields).map(targetColumn => ({ + targetColumn, + exprType: 'value', + value: item.fields[targetColumn], + })); +} + +function insertToSql(item: ChangeSetItem): Insert { + return { + targetTable: { + pureName: item.pureName, + schemaName: item.schemaName, + }, + commandType: 'insert', + fields: extractFields(item), + }; +} + +function extractCondition(item: ChangeSetItem): Condition { + return { + conditionType: 'and', + conditions: _.keys(item.condition).map(columnName => ({ + conditionType: 'binary', + operator: '=', + left: { + exprType: 'column', + columnName, + source: { + name: { + pureName: item.pureName, + schemaName: item.schemaName, + }, + }, + }, + right: { + exprType: 'value', + value: item.condition[columnName], + }, + })), + }; +} + +function updateToSql(item: ChangeSetItem): Update { + return { + from: { + name: { + pureName: item.pureName, + schemaName: item.schemaName, + }, + }, + commandType: 'update', + fields: extractFields(item), + where: extractCondition(item), + }; +} + +function deleteToSql(item: ChangeSetItem): Delete { + return { + from: { + name: { + pureName: item.pureName, + schemaName: item.schemaName, + }, + }, + commandType: 'delete', + where: extractCondition(item), + }; +} + +export function changeSetToSql(changeSet: ChangeSet): Command[] { + return [ + ...changeSet.inserts.map(insertToSql), + ...changeSet.updates.map(updateToSql), + ...changeSet.deletes.map(deleteToSql), + ]; +} diff --git a/packages/datalib/src/GridDisplay.ts b/packages/datalib/src/GridDisplay.ts index 45a16583..e29ed479 100644 --- a/packages/datalib/src/GridDisplay.ts +++ b/packages/datalib/src/GridDisplay.ts @@ -1,6 +1,6 @@ import _ from 'lodash'; import { GridConfig, GridCache, GridConfigColumns } from './GridConfig'; -import { ForeignKeyInfo, TableInfo, ColumnInfo, DbType } from '@dbgate/types'; +import { ForeignKeyInfo, TableInfo, ColumnInfo, DbType, EngineDriver } from '@dbgate/types'; import { parseFilter, getFilterType } from '@dbgate/filterparser'; import { filterName } from './filterName'; import { Select, Expression } from '@dbgate/sqltree'; @@ -44,12 +44,14 @@ export abstract class GridDisplay { protected setConfig: (config: GridConfig) => void, public cache: GridCache, protected setCache: (config: GridCache) => void, - protected getTableInfo: ({ schemaName, pureName }) => Promise + protected getTableInfo: ({ schemaName, pureName }) => Promise, + public driver: EngineDriver ) {} abstract getPageQuery(offset: number, count: number): string; columns: DisplayColumn[]; baseTable?: TableInfo; changeSetKeyFields: string[] = null; + setColumnVisibility(uniquePath: string[], isVisible: boolean) { const uniqueName = uniquePath.join('.'); if (uniquePath.length == 1) { @@ -60,6 +62,10 @@ export abstract class GridDisplay { } } + get engine() { + return this.driver.engine; + } + reload() { this.setCache({ ...this.cache, diff --git a/packages/datalib/src/TableGridDisplay.ts b/packages/datalib/src/TableGridDisplay.ts index acc85335..2a2561d6 100644 --- a/packages/datalib/src/TableGridDisplay.ts +++ b/packages/datalib/src/TableGridDisplay.ts @@ -7,19 +7,21 @@ import { GridConfig, GridCache } from './GridConfig'; export class TableGridDisplay extends GridDisplay { constructor( public table: TableInfo, - public driver: EngineDriver, + driver: EngineDriver, config: GridConfig, setConfig: (config: GridConfig) => void, cache: GridCache, setCache: (config: GridCache) => void, getTableInfo: ({ schemaName, pureName }) => Promise ) { - super(config, setConfig, cache, setCache, getTableInfo); + super(config, setConfig, cache, setCache, getTableInfo, driver); this.columns = this.getDisplayColumns(table, []); this.baseTable = table; - this.changeSetKeyFields = table.primaryKey - ? table.primaryKey.columns.map(x => x.columnName) - : table.columns.map(x => x.columnName); + if (table && table.columns) { + this.changeSetKeyFields = table.primaryKey + ? table.primaryKey.columns.map(x => x.columnName) + : table.columns.map(x => x.columnName); + } } createSelect() { diff --git a/packages/engines/mssql/index.js b/packages/engines/mssql/index.js index ebf1f80b..92d21891 100644 --- a/packages/engines/mssql/index.js +++ b/packages/engines/mssql/index.js @@ -57,7 +57,8 @@ const driver = { createDumper() { return new MsSqlDumper(this); }, - dialect + dialect, + engine: 'mssql', }; module.exports = driver; diff --git a/packages/engines/mysql/index.js b/packages/engines/mysql/index.js index ebc59399..4962d452 100644 --- a/packages/engines/mysql/index.js +++ b/packages/engines/mysql/index.js @@ -52,7 +52,8 @@ const driver = { createDumper() { return new MySqlDumper(this); }, - dialect + dialect, + engine: 'mysql', }; module.exports = driver; diff --git a/packages/engines/postgres/index.js b/packages/engines/postgres/index.js index b3f4133e..b1aa3c6a 100644 --- a/packages/engines/postgres/index.js +++ b/packages/engines/postgres/index.js @@ -40,6 +40,7 @@ const driver = { return rows; }, dialect, + engine: 'postgres', }; module.exports = driver; diff --git a/packages/sqltree/src/dumpSqlCommand.ts b/packages/sqltree/src/dumpSqlCommand.ts index a3ca19c1..356d4acf 100644 --- a/packages/sqltree/src/dumpSqlCommand.ts +++ b/packages/sqltree/src/dumpSqlCommand.ts @@ -1,62 +1,111 @@ import { SqlDumper } from '@dbgate/types'; -import { Command, Select } from './types'; +import { Command, Select, Update, Delete, Insert } from './types'; import { dumpSqlExpression } from './dumpSqlExpression'; -import { dumpSqlFromDefinition } from './dumpSqlSource'; +import { dumpSqlFromDefinition, dumpSqlSourceRef } from './dumpSqlSource'; import { dumpSqlCondition } from './dumpSqlCondition'; -export function dumpSqlSelect(dmp: SqlDumper, select: Select) { +export function dumpSqlSelect(dmp: SqlDumper, cmd: Select) { dmp.put('^select '); - if (select.topRecords) { - dmp.put('^top %s ', select.topRecords); + if (cmd.topRecords) { + dmp.put('^top %s ', cmd.topRecords); } - if (select.distinct) { + if (cmd.distinct) { dmp.put('^distinct '); } - if (select.selectAll) { + if (cmd.selectAll) { dmp.put('* '); } - if (select.columns) { - if (select.selectAll) dmp.put('&n,'); + if (cmd.columns) { + if (cmd.selectAll) dmp.put('&n,'); dmp.put('&>&n'); - dmp.putCollection(',&n', select.columns, fld => { + dmp.putCollection(',&n', cmd.columns, fld => { dumpSqlExpression(dmp, fld); if (fld.alias) dmp.put(' ^as %i', fld.alias); }); dmp.put('&n&<'); } dmp.put('^from '); - dumpSqlFromDefinition(dmp, select.from); - if (select.where) { + dumpSqlFromDefinition(dmp, cmd.from); + if (cmd.where) { dmp.put('&n^where '); - dumpSqlCondition(dmp, select.where); + dumpSqlCondition(dmp, cmd.where); dmp.put('&n'); } - if (select.groupBy) { + if (cmd.groupBy) { dmp.put('&n^group ^by '); - dmp.putCollection(', ', select.groupBy, expr => dumpSqlExpression(dmp, expr)); + dmp.putCollection(', ', cmd.groupBy, expr => dumpSqlExpression(dmp, expr)); dmp.put('&n'); } - if (select.orderBy) { + if (cmd.orderBy) { dmp.put('&n^order ^by '); - dmp.putCollection(', ', select.orderBy, expr => { + dmp.putCollection(', ', cmd.orderBy, expr => { dumpSqlExpression(dmp, expr); dmp.put(' %k', expr.direction); }); dmp.put('&n'); } - if (select.range) { + if (cmd.range) { if (dmp.dialect.offsetFetchRangeSyntax) { - dmp.put('^offset %s ^rows ^fetch ^next %s ^rows ^only', select.range.offset, select.range.limit); + dmp.put('^offset %s ^rows ^fetch ^next %s ^rows ^only', cmd.range.offset, cmd.range.limit); } else { - dmp.put('^limit %s ^offset %s ', select.range.limit, select.range.offset); + dmp.put('^limit %s ^offset %s ', cmd.range.limit, cmd.range.offset); } } } -export function dumpSqlCommand(dmp: SqlDumper, command: Command) { - switch (command.commandType) { +export function dumpSqlUpdate(dmp: SqlDumper, cmd: Update) { + dmp.put('^update '); + dumpSqlSourceRef(dmp, cmd.from); + + dmp.put('&n^set '); + dmp.put('&>'); + dmp.putCollection(', ', cmd.fields, col => { + dmp.put('%i=', col.targetColumn); + dumpSqlExpression(dmp, col); + }); + dmp.put('&<'); + + if (cmd.where) { + dmp.put('&n^where '); + dumpSqlCondition(dmp, cmd.where); + dmp.put('&n'); + } +} + +export function dumpSqlDelete(dmp: SqlDumper, cmd: Delete) { + dmp.put('^delete '); + dumpSqlSourceRef(dmp, cmd.from); + + if (cmd.where) { + dmp.put('&n^where '); + dumpSqlCondition(dmp, cmd.where); + dmp.put('&n'); + } +} + +export function dumpSqlInsert(dmp: SqlDumper, cmd: Insert) { + dmp.put( + '^insert ^into %f (%,i) ^values (', + cmd.targetTable, + cmd.fields.map(x => x.targetColumn) + ); + dmp.putCollection(',', cmd.fields, x => dumpSqlExpression(dmp, x)); + dmp.put(')'); +} + +export function dumpSqlCommand(dmp: SqlDumper, cmd: Command) { + switch (cmd.commandType) { case 'select': - dumpSqlSelect(dmp, command); + dumpSqlSelect(dmp, cmd); + break; + case 'update': + dumpSqlUpdate(dmp, cmd); + break; + case 'delete': + dumpSqlDelete(dmp, cmd); + break; + case 'insert': + dumpSqlInsert(dmp, cmd); break; } } diff --git a/packages/sqltree/src/index.ts b/packages/sqltree/src/index.ts index 65e17d74..d40c0596 100644 --- a/packages/sqltree/src/index.ts +++ b/packages/sqltree/src/index.ts @@ -1,5 +1,5 @@ export * from './types'; export * from './dumpSqlCommand'; -export * from './treeToSql'; +export * from './utility'; export * from './dumpSqlSource'; export * from './dumpSqlCondition'; diff --git a/packages/sqltree/src/treeToSql.ts b/packages/sqltree/src/treeToSql.ts deleted file mode 100644 index 8475f1a4..00000000 --- a/packages/sqltree/src/treeToSql.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { EngineDriver, SqlDumper } from '@dbgate/types'; - -export function treeToSql(driver: EngineDriver, object: T, func: (dmp: SqlDumper, obj: T) => void) { - const dmp = driver.createDumper(); - func(dmp, object); - return dmp.s; -} diff --git a/packages/sqltree/src/types.ts b/packages/sqltree/src/types.ts index dee0698d..67d7ad10 100644 --- a/packages/sqltree/src/types.ts +++ b/packages/sqltree/src/types.ts @@ -18,7 +18,31 @@ export interface Select { where?: Condition; } -export type Command = Select; +export type UpdateField = Expression & { targetColumn: string }; + +export interface Update { + commandType: 'update'; + fields: UpdateField[]; + from: FromDefinition; + where?: Condition; +} + +export interface Delete { + commandType: 'delete'; + from: FromDefinition; + where?: Condition; +} + +export interface Insert { + commandType: 'insert'; + fields: UpdateField[]; + targetTable: { + schemaName: string; + pureName: string; + }; +} + +export type Command = Select | Update | Delete | Insert; // export interface Condition { // conditionType: "eq" | "not" | "binary"; diff --git a/packages/sqltree/src/utility.ts b/packages/sqltree/src/utility.ts new file mode 100644 index 00000000..26cacf51 --- /dev/null +++ b/packages/sqltree/src/utility.ts @@ -0,0 +1,18 @@ +import { EngineDriver, SqlDumper } from '@dbgate/types'; +import { Command } from './types'; +import { dumpSqlCommand } from './dumpSqlCommand'; + +export function treeToSql(driver: EngineDriver, object: T, func: (dmp: SqlDumper, obj: T) => void) { + const dmp = driver.createDumper(); + func(dmp, object); + return dmp.s; +} + +export function scriptToSql(driver: EngineDriver, script: Command[]): string { + const dmp = driver.createDumper(); + for (const cmd of script) { + dumpSqlCommand(dmp, cmd); + dmp.endCommand(); + } + return dmp.s; +} diff --git a/packages/types/engines.d.ts b/packages/types/engines.d.ts index 527e08fb..dc634388 100644 --- a/packages/types/engines.d.ts +++ b/packages/types/engines.d.ts @@ -4,6 +4,7 @@ import { SqlDumper } from "./dumper"; import { DatabaseInfo } from "./dbinfo"; export interface EngineDriver { + engine: string; connect(nativeModules, { server, port, user, password, database }): any; query(pool: any, sql: string): Promise; getVersion(pool: any): Promise<{ version: string }>; diff --git a/packages/web/src/appobj/columnAppObject.js b/packages/web/src/appobj/columnAppObject.js index 8b2eddc5..00217caa 100644 --- a/packages/web/src/appobj/columnAppObject.js +++ b/packages/web/src/appobj/columnAppObject.js @@ -1,11 +1,4 @@ -import React from 'react'; import { ColumnIcon, SequenceIcon } from '../icons'; -import { DropDownMenuItem } from '../modals/DropDownMenu'; -import showModal from '../modals/showModal'; -import ConnectionModal from '../modals/ConnectionModal'; -import axios from '../utility/axios'; -import { openNewTab } from '../utility/common'; -import { useSetOpenedTabs } from '../utility/globalState'; /** @param columnProps {import('@dbgate/types').ColumnInfo} */ function getColumnIcon(columnProps) { diff --git a/packages/web/src/appobj/constraintAppObject.js b/packages/web/src/appobj/constraintAppObject.js index 3e53371d..f68583d4 100644 --- a/packages/web/src/appobj/constraintAppObject.js +++ b/packages/web/src/appobj/constraintAppObject.js @@ -1,11 +1,4 @@ -import React from 'react'; import { PrimaryKeyIcon, ForeignKeyIcon } from '../icons'; -import { DropDownMenuItem } from '../modals/DropDownMenu'; -import showModal from '../modals/showModal'; -import ConnectionModal from '../modals/ConnectionModal'; -import axios from '../utility/axios'; -import { openNewTab } from '../utility/common'; -import { useSetOpenedTabs } from '../utility/globalState'; /** @param props {import('@dbgate/types').ConstraintInfo} */ function getConstraintIcon(props) { diff --git a/packages/web/src/appobj/tableAppObject.js b/packages/web/src/appobj/tableAppObject.js index b8ba53e7..954d39d5 100644 --- a/packages/web/src/appobj/tableAppObject.js +++ b/packages/web/src/appobj/tableAppObject.js @@ -1,11 +1,7 @@ import React from 'react'; import { TableIcon } from '../icons'; import { DropDownMenuItem } from '../modals/DropDownMenu'; -import showModal from '../modals/showModal'; -import ConnectionModal from '../modals/ConnectionModal'; -import axios from '../utility/axios'; import { openNewTab } from '../utility/common'; -import { useSetOpenedTabs } from '../utility/globalState'; import getConnectionInfo from '../utility/getConnectionInfo'; import fullDisplayName from '../utility/fullDisplayName'; diff --git a/packages/web/src/datagrid/DataGridCore.js b/packages/web/src/datagrid/DataGridCore.js index cfadd66c..a3223184 100644 --- a/packages/web/src/datagrid/DataGridCore.js +++ b/packages/web/src/datagrid/DataGridCore.js @@ -24,6 +24,10 @@ import keycodes from '../utility/keycodes'; import InplaceEditor from './InplaceEditor'; import DataGridRow from './DataGridRow'; import { countColumnSizes, countVisibleRealColumns } from './gridutil'; +import useModalState from '../modals/useModalState'; +import ConfirmSqlModal from '../modals/ConfirmSqlModal'; +import { changeSetToSql } from '@dbgate/datalib'; +import { scriptToSql } from '@dbgate/sqltree'; const GridContainer = styled.div` position: absolute; @@ -162,6 +166,8 @@ export default function DataGridCore(props) { const [tableBodyRef] = useDimensions(); const [containerRef, { height: containerHeight, width: containerWidth }] = useDimensions(); const [tableRef, { height: tableHeight, width: tableWidth }, tableElement] = useDimensions(); + const confirmSqlModalState = useModalState(); + const [confirmSql, setConfirmSql] = React.useState(''); const columnSizes = React.useMemo(() => countColumnSizes(loadedRows, columns, containerWidth, display), [ loadedRows, @@ -221,18 +227,20 @@ export default function DataGridCore(props) { [columnSizes, firstVisibleColumnScrollIndex, gridScrollAreaWidth, columns] ); - const cellIsSelected = React.useCallback((row, col) => { - const [currentRow, currentCol] = currentCell; - if (row == currentRow && col == currentCol) return true; - for (const [selectedRow, selectedCol] of selectedCells) { - if (row == selectedRow && col == selectedCol) return true; - if (selectedRow == 'header' && col == selectedCol) return true; - if (row == selectedRow && selectedCol == 'header') return true; - if (selectedRow == 'header' && selectedCol == 'header') return true; - } - return false; - }, [currentCell, selectedCells]); - + const cellIsSelected = React.useCallback( + (row, col) => { + const [currentRow, currentCol] = currentCell; + if (row == currentRow && col == currentCol) return true; + for (const [selectedRow, selectedCol] of selectedCells) { + if (row == selectedRow && col == selectedCol) return true; + if (selectedRow == 'header' && col == selectedCol) return true; + if (row == selectedRow && selectedCol == 'header') return true; + if (selectedRow == 'header' && selectedCol == 'header') return true; + } + return false; + }, + [currentCell, selectedCells] + ); if (!loadedRows || !columns) return null; const rowCountNewIncluded = loadedRows.length; @@ -297,6 +305,13 @@ export default function DataGridCore(props) { setvScrollValueToSetDate(new Date()); } + function handleSave() { + const script = changeSetToSql(changeSet); + const sql = scriptToSql(display.driver, script); + setConfirmSql(sql); + confirmSqlModalState.open(); + } + function handleGridKeyDown(event) { if ( !event.ctrlKey && @@ -310,6 +325,12 @@ export default function DataGridCore(props) { // console.log('event', event.nativeEvent); } + if (event.keyCode == keycodes.s && event.ctrlKey) { + event.preventDefault(); + handleSave(); + // this.saveAndFocus(); + } + const moved = handleCursorMove(event); if (moved) { @@ -555,6 +576,7 @@ export default function DataGridCore(props) { onScroll={handleRowScroll} viewportRatio={visibleRowCountUpperBound / rowCountNewIncluded} /> + ); } diff --git a/packages/web/src/modals/ConfirmSqlModal.js b/packages/web/src/modals/ConfirmSqlModal.js new file mode 100644 index 00000000..3c8b6e3d --- /dev/null +++ b/packages/web/src/modals/ConfirmSqlModal.js @@ -0,0 +1,31 @@ +import React from 'react'; +import axios from '../utility/axios'; +import ModalBase from './ModalBase'; +import { FormRow, FormButton, FormTextField, FormSelectField, FormSubmit } from '../utility/forms'; +import { TextField } from '../utility/inputs'; +import { Formik, Form } from 'formik'; +import SqlEditor from '../sqleditor/SqlEditor'; +// import FormikForm from '../utility/FormikForm'; +import styled from 'styled-components'; + +const SqlWrapper = styled.div` + position: relative; + height: 30vh; + width: 40vw; +`; + +export default function ConfirmSqlModal({ modalState, sql, engine }) { + return ( + +

Save changes

+ + + + + + + + +
+ ); +}