generate update script

This commit is contained in:
Jan Prochazka
2020-03-23 20:41:40 +01:00
parent 1560b7c2e0
commit 464662cb18
17 changed files with 281 additions and 71 deletions

View File

@@ -1,4 +1,5 @@
import _ from 'lodash'; import _ from 'lodash';
import { Command, Insert, Update, Delete, UpdateField, Condition } from '@dbgate/sqltree';
export interface ChangeSetItem { export interface ChangeSetItem {
pureName: string; 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),
];
}

View File

@@ -1,6 +1,6 @@
import _ from 'lodash'; import _ from 'lodash';
import { GridConfig, GridCache, GridConfigColumns } from './GridConfig'; 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 { parseFilter, getFilterType } from '@dbgate/filterparser';
import { filterName } from './filterName'; import { filterName } from './filterName';
import { Select, Expression } from '@dbgate/sqltree'; import { Select, Expression } from '@dbgate/sqltree';
@@ -44,12 +44,14 @@ export abstract class GridDisplay {
protected setConfig: (config: GridConfig) => void, protected setConfig: (config: GridConfig) => void,
public cache: GridCache, public cache: GridCache,
protected setCache: (config: GridCache) => void, protected setCache: (config: GridCache) => void,
protected getTableInfo: ({ schemaName, pureName }) => Promise<TableInfo> protected getTableInfo: ({ schemaName, pureName }) => Promise<TableInfo>,
public driver: EngineDriver
) {} ) {}
abstract getPageQuery(offset: number, count: number): string; abstract getPageQuery(offset: number, count: number): string;
columns: DisplayColumn[]; columns: DisplayColumn[];
baseTable?: TableInfo; baseTable?: TableInfo;
changeSetKeyFields: string[] = null; changeSetKeyFields: string[] = null;
setColumnVisibility(uniquePath: string[], isVisible: boolean) { setColumnVisibility(uniquePath: string[], isVisible: boolean) {
const uniqueName = uniquePath.join('.'); const uniqueName = uniquePath.join('.');
if (uniquePath.length == 1) { if (uniquePath.length == 1) {
@@ -60,6 +62,10 @@ export abstract class GridDisplay {
} }
} }
get engine() {
return this.driver.engine;
}
reload() { reload() {
this.setCache({ this.setCache({
...this.cache, ...this.cache,

View File

@@ -7,20 +7,22 @@ import { GridConfig, GridCache } from './GridConfig';
export class TableGridDisplay extends GridDisplay { export class TableGridDisplay extends GridDisplay {
constructor( constructor(
public table: TableInfo, public table: TableInfo,
public driver: EngineDriver, driver: EngineDriver,
config: GridConfig, config: GridConfig,
setConfig: (config: GridConfig) => void, setConfig: (config: GridConfig) => void,
cache: GridCache, cache: GridCache,
setCache: (config: GridCache) => void, setCache: (config: GridCache) => void,
getTableInfo: ({ schemaName, pureName }) => Promise<TableInfo> getTableInfo: ({ schemaName, pureName }) => Promise<TableInfo>
) { ) {
super(config, setConfig, cache, setCache, getTableInfo); super(config, setConfig, cache, setCache, getTableInfo, driver);
this.columns = this.getDisplayColumns(table, []); this.columns = this.getDisplayColumns(table, []);
this.baseTable = table; this.baseTable = table;
if (table && table.columns) {
this.changeSetKeyFields = table.primaryKey this.changeSetKeyFields = table.primaryKey
? table.primaryKey.columns.map(x => x.columnName) ? table.primaryKey.columns.map(x => x.columnName)
: table.columns.map(x => x.columnName); : table.columns.map(x => x.columnName);
} }
}
createSelect() { createSelect() {
if (!this.table.columns) return null; if (!this.table.columns) return null;

View File

@@ -57,7 +57,8 @@ const driver = {
createDumper() { createDumper() {
return new MsSqlDumper(this); return new MsSqlDumper(this);
}, },
dialect dialect,
engine: 'mssql',
}; };
module.exports = driver; module.exports = driver;

View File

@@ -52,7 +52,8 @@ const driver = {
createDumper() { createDumper() {
return new MySqlDumper(this); return new MySqlDumper(this);
}, },
dialect dialect,
engine: 'mysql',
}; };
module.exports = driver; module.exports = driver;

View File

@@ -40,6 +40,7 @@ const driver = {
return rows; return rows;
}, },
dialect, dialect,
engine: 'postgres',
}; };
module.exports = driver; module.exports = driver;

View File

@@ -1,62 +1,111 @@
import { SqlDumper } from '@dbgate/types'; import { SqlDumper } from '@dbgate/types';
import { Command, Select } from './types'; import { Command, Select, Update, Delete, Insert } from './types';
import { dumpSqlExpression } from './dumpSqlExpression'; import { dumpSqlExpression } from './dumpSqlExpression';
import { dumpSqlFromDefinition } from './dumpSqlSource'; import { dumpSqlFromDefinition, dumpSqlSourceRef } from './dumpSqlSource';
import { dumpSqlCondition } from './dumpSqlCondition'; import { dumpSqlCondition } from './dumpSqlCondition';
export function dumpSqlSelect(dmp: SqlDumper, select: Select) { export function dumpSqlSelect(dmp: SqlDumper, cmd: Select) {
dmp.put('^select '); dmp.put('^select ');
if (select.topRecords) { if (cmd.topRecords) {
dmp.put('^top %s ', select.topRecords); dmp.put('^top %s ', cmd.topRecords);
} }
if (select.distinct) { if (cmd.distinct) {
dmp.put('^distinct '); dmp.put('^distinct ');
} }
if (select.selectAll) { if (cmd.selectAll) {
dmp.put('* '); dmp.put('* ');
} }
if (select.columns) { if (cmd.columns) {
if (select.selectAll) dmp.put('&n,'); if (cmd.selectAll) dmp.put('&n,');
dmp.put('&>&n'); dmp.put('&>&n');
dmp.putCollection(',&n', select.columns, fld => { dmp.putCollection(',&n', cmd.columns, fld => {
dumpSqlExpression(dmp, fld); dumpSqlExpression(dmp, fld);
if (fld.alias) dmp.put(' ^as %i', fld.alias); if (fld.alias) dmp.put(' ^as %i', fld.alias);
}); });
dmp.put('&n&<'); dmp.put('&n&<');
} }
dmp.put('^from '); dmp.put('^from ');
dumpSqlFromDefinition(dmp, select.from); dumpSqlFromDefinition(dmp, cmd.from);
if (select.where) { if (cmd.where) {
dmp.put('&n^where '); dmp.put('&n^where ');
dumpSqlCondition(dmp, select.where); dumpSqlCondition(dmp, cmd.where);
dmp.put('&n'); dmp.put('&n');
} }
if (select.groupBy) { if (cmd.groupBy) {
dmp.put('&n^group ^by '); dmp.put('&n^group ^by ');
dmp.putCollection(', ', select.groupBy, expr => dumpSqlExpression(dmp, expr)); dmp.putCollection(', ', cmd.groupBy, expr => dumpSqlExpression(dmp, expr));
dmp.put('&n'); dmp.put('&n');
} }
if (select.orderBy) { if (cmd.orderBy) {
dmp.put('&n^order ^by '); dmp.put('&n^order ^by ');
dmp.putCollection(', ', select.orderBy, expr => { dmp.putCollection(', ', cmd.orderBy, expr => {
dumpSqlExpression(dmp, expr); dumpSqlExpression(dmp, expr);
dmp.put(' %k', expr.direction); dmp.put(' %k', expr.direction);
}); });
dmp.put('&n'); dmp.put('&n');
} }
if (select.range) { if (cmd.range) {
if (dmp.dialect.offsetFetchRangeSyntax) { 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 { } 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) { export function dumpSqlUpdate(dmp: SqlDumper, cmd: Update) {
switch (command.commandType) { 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': 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; break;
} }
} }

View File

@@ -1,5 +1,5 @@
export * from './types'; export * from './types';
export * from './dumpSqlCommand'; export * from './dumpSqlCommand';
export * from './treeToSql'; export * from './utility';
export * from './dumpSqlSource'; export * from './dumpSqlSource';
export * from './dumpSqlCondition'; export * from './dumpSqlCondition';

View File

@@ -1,7 +0,0 @@
import { EngineDriver, SqlDumper } from '@dbgate/types';
export function treeToSql<T>(driver: EngineDriver, object: T, func: (dmp: SqlDumper, obj: T) => void) {
const dmp = driver.createDumper();
func(dmp, object);
return dmp.s;
}

View File

@@ -18,7 +18,31 @@ export interface Select {
where?: Condition; 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 { // export interface Condition {
// conditionType: "eq" | "not" | "binary"; // conditionType: "eq" | "not" | "binary";

View File

@@ -0,0 +1,18 @@
import { EngineDriver, SqlDumper } from '@dbgate/types';
import { Command } from './types';
import { dumpSqlCommand } from './dumpSqlCommand';
export function treeToSql<T>(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;
}

View File

@@ -4,6 +4,7 @@ import { SqlDumper } from "./dumper";
import { DatabaseInfo } from "./dbinfo"; import { DatabaseInfo } from "./dbinfo";
export interface EngineDriver { export interface EngineDriver {
engine: string;
connect(nativeModules, { server, port, user, password, database }): any; connect(nativeModules, { server, port, user, password, database }): any;
query(pool: any, sql: string): Promise<QueryResult>; query(pool: any, sql: string): Promise<QueryResult>;
getVersion(pool: any): Promise<{ version: string }>; getVersion(pool: any): Promise<{ version: string }>;

View File

@@ -1,11 +1,4 @@
import React from 'react';
import { ColumnIcon, SequenceIcon } from '../icons'; 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} */ /** @param columnProps {import('@dbgate/types').ColumnInfo} */
function getColumnIcon(columnProps) { function getColumnIcon(columnProps) {

View File

@@ -1,11 +1,4 @@
import React from 'react';
import { PrimaryKeyIcon, ForeignKeyIcon } from '../icons'; 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} */ /** @param props {import('@dbgate/types').ConstraintInfo} */
function getConstraintIcon(props) { function getConstraintIcon(props) {

View File

@@ -1,11 +1,7 @@
import React from 'react'; import React from 'react';
import { TableIcon } from '../icons'; import { TableIcon } from '../icons';
import { DropDownMenuItem } from '../modals/DropDownMenu'; 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 { openNewTab } from '../utility/common';
import { useSetOpenedTabs } from '../utility/globalState';
import getConnectionInfo from '../utility/getConnectionInfo'; import getConnectionInfo from '../utility/getConnectionInfo';
import fullDisplayName from '../utility/fullDisplayName'; import fullDisplayName from '../utility/fullDisplayName';

View File

@@ -24,6 +24,10 @@ import keycodes from '../utility/keycodes';
import InplaceEditor from './InplaceEditor'; import InplaceEditor from './InplaceEditor';
import DataGridRow from './DataGridRow'; import DataGridRow from './DataGridRow';
import { countColumnSizes, countVisibleRealColumns } from './gridutil'; 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` const GridContainer = styled.div`
position: absolute; position: absolute;
@@ -162,6 +166,8 @@ export default function DataGridCore(props) {
const [tableBodyRef] = useDimensions(); const [tableBodyRef] = useDimensions();
const [containerRef, { height: containerHeight, width: containerWidth }] = useDimensions(); const [containerRef, { height: containerHeight, width: containerWidth }] = useDimensions();
const [tableRef, { height: tableHeight, width: tableWidth }, tableElement] = 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), [ const columnSizes = React.useMemo(() => countColumnSizes(loadedRows, columns, containerWidth, display), [
loadedRows, loadedRows,
@@ -221,7 +227,8 @@ export default function DataGridCore(props) {
[columnSizes, firstVisibleColumnScrollIndex, gridScrollAreaWidth, columns] [columnSizes, firstVisibleColumnScrollIndex, gridScrollAreaWidth, columns]
); );
const cellIsSelected = React.useCallback((row, col) => { const cellIsSelected = React.useCallback(
(row, col) => {
const [currentRow, currentCol] = currentCell; const [currentRow, currentCol] = currentCell;
if (row == currentRow && col == currentCol) return true; if (row == currentRow && col == currentCol) return true;
for (const [selectedRow, selectedCol] of selectedCells) { for (const [selectedRow, selectedCol] of selectedCells) {
@@ -231,8 +238,9 @@ export default function DataGridCore(props) {
if (selectedRow == 'header' && selectedCol == 'header') return true; if (selectedRow == 'header' && selectedCol == 'header') return true;
} }
return false; return false;
}, [currentCell, selectedCells]); },
[currentCell, selectedCells]
);
if (!loadedRows || !columns) return null; if (!loadedRows || !columns) return null;
const rowCountNewIncluded = loadedRows.length; const rowCountNewIncluded = loadedRows.length;
@@ -297,6 +305,13 @@ export default function DataGridCore(props) {
setvScrollValueToSetDate(new Date()); setvScrollValueToSetDate(new Date());
} }
function handleSave() {
const script = changeSetToSql(changeSet);
const sql = scriptToSql(display.driver, script);
setConfirmSql(sql);
confirmSqlModalState.open();
}
function handleGridKeyDown(event) { function handleGridKeyDown(event) {
if ( if (
!event.ctrlKey && !event.ctrlKey &&
@@ -310,6 +325,12 @@ export default function DataGridCore(props) {
// console.log('event', event.nativeEvent); // console.log('event', event.nativeEvent);
} }
if (event.keyCode == keycodes.s && event.ctrlKey) {
event.preventDefault();
handleSave();
// this.saveAndFocus();
}
const moved = handleCursorMove(event); const moved = handleCursorMove(event);
if (moved) { if (moved) {
@@ -555,6 +576,7 @@ export default function DataGridCore(props) {
onScroll={handleRowScroll} onScroll={handleRowScroll}
viewportRatio={visibleRowCountUpperBound / rowCountNewIncluded} viewportRatio={visibleRowCountUpperBound / rowCountNewIncluded}
/> />
<ConfirmSqlModal modalState={confirmSqlModalState} sql={confirmSql} engine={display.engine} />
</GridContainer> </GridContainer>
); );
} }

View File

@@ -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 (
<ModalBase modalState={modalState}>
<h2>Save changes</h2>
<SqlWrapper>
<SqlEditor value={sql} engine={engine} />
</SqlWrapper>
<FormRow>
<input type="button" value="OK" onClick={modalState.close} />
<input type="button" value="Close" onClick={modalState.close} />
</FormRow>
</ModalBase>
);
}