generated designer query SQL

This commit is contained in:
Jan Prochazka 2020-12-28 13:59:36 +01:00
parent 79759b1665
commit c10c6afbc5
8 changed files with 217 additions and 14 deletions

View File

@ -42,14 +42,14 @@ export function dumpSqlSourceRef(dmp: SqlDumper, source: Source) {
export function dumpSqlRelation(dmp: SqlDumper, from: Relation) {
dmp.put('&n %k ', from.joinType);
dumpSqlSourceDef(dmp, from);
if (from.conditions) {
if (from.conditions && from.conditions.length > 0) {
dmp.put(' ^on ');
dmp.putCollection(' ^and ', from.conditions, cond => dumpSqlCondition(dmp, cond));
dmp.putCollection(' ^and ', from.conditions, (cond) => dumpSqlCondition(dmp, cond));
}
}
export function dumpSqlFromDefinition(dmp: SqlDumper, from: FromDefinition) {
dumpSqlSourceDef(dmp, from);
dmp.put(' ');
if (from.relations) from.relations.forEach(rel => dumpSqlRelation(dmp, rel));
if (from.relations) from.relations.forEach((rel) => dumpSqlRelation(dmp, rel));
}

View File

@ -91,7 +91,7 @@ export interface Source {
subQueryString?: string;
}
export type JoinType = 'LEFT JOIN' | 'INNER JOIN' | 'RIGHT JOIN';
export type JoinType = 'LEFT JOIN' | 'INNER JOIN' | 'RIGHT JOIN' | 'CROSS JOIN';
export type Relation = Source & {
conditions: Condition[];

View File

@ -130,6 +130,7 @@ export default function Designer({ value, onChange }) {
designerId: uuidv1(),
sourceId: source.designerId,
targetId: target.designerId,
joinType: 'INNER JOIN',
columns: [
{
source: source.columnName,

View File

@ -46,6 +46,7 @@ function ReferenceContextMenu({ remove, setJoinType }) {
<DropDownMenuItem onClick={() => setJoinType('LEFT JOIN')}>Set LEFT JOIN</DropDownMenuItem>
<DropDownMenuItem onClick={() => setJoinType('RIGHT JOIN')}>Set RIGHT JOIN</DropDownMenuItem>
<DropDownMenuItem onClick={() => setJoinType('FULL OUTER JOIN')}>Set FULL OUTER JOIN</DropDownMenuItem>
<DropDownMenuItem onClick={() => setJoinType('CROSS JOIN')}>Set CROSS JOIN</DropDownMenuItem>
<DropDownMenuItem onClick={() => setJoinType('WHERE EXISTS')}>Set WHERE EXISTS</DropDownMenuItem>
<DropDownMenuItem onClick={() => setJoinType('WHERE NOT EXISTS')}>Set WHERE NOT EXISTS</DropDownMenuItem>
</>
@ -159,7 +160,7 @@ export default function DesignerReference({
onContextMenu={handleContextMenu}
>
<ReferenceText theme={theme}>
{_.snakeCase(joinType || 'INNER JOIN')
{_.snakeCase(joinType || 'CROSS JOIN')
.replace('_', '\xa0')
.replace('_', '\xa0')}
</ReferenceText>

View File

@ -1,6 +1,4 @@
import { TableInfo } from 'dbgate-types';
type DesignerTableInfo = TableInfo & { designerId: string };
import { DesignerTableInfo } from "./types";
export default class DomTableRef {
domTable: Element;

View File

@ -0,0 +1,143 @@
import _ from 'lodash';
import { dumpSqlSelect, Select, JoinType, Condition } from 'dbgate-sqltree';
import { EngineDriver } from 'dbgate-types';
import { DesignerInfo, DesignerTableInfo, DesignerReferenceInfo, DesignerJoinType } from './types';
function groupByComponents(
tables: DesignerTableInfo[],
references: DesignerReferenceInfo[],
joinTypes: string[],
primaryTable: DesignerTableInfo
) {
let components = tables.map((table) => [table]);
for (const ref of references) {
if (joinTypes.includes(ref.joinType)) {
const comp1 = components.find((comp) => comp.find((t) => t.designerId == ref.sourceId));
const comp2 = components.find((comp) => comp.find((t) => t.designerId == ref.targetId));
if (comp1 && comp2 && comp1 != comp2) {
// join components
components = [...components.filter((x) => x != comp1 && x != comp2), [...comp1, ...comp2]];
}
}
}
if (primaryTable) {
const primaryComponent = components.find((comp) => comp.find((t) => t == primaryTable));
if (primaryComponent) {
components = [primaryComponent, ...components.filter((x) => x != primaryComponent)];
}
}
return components;
}
function findPrimaryTable(tables: DesignerTableInfo[]) {
return _.minBy(tables, (x) => x.left + x.top);
}
function findJoinType(
table: DesignerTableInfo,
dumpedTables: DesignerTableInfo[],
references: DesignerReferenceInfo[],
joinTypes: DesignerJoinType[]
): DesignerJoinType {
const dumpedTableIds = dumpedTables.map((x) => x.designerId);
const reference = references.find(
(x) =>
(x.sourceId == table.designerId && dumpedTableIds.includes(x.targetId)) ||
(x.targetId == table.designerId && dumpedTableIds.includes(x.sourceId))
);
if (reference) return reference.joinType || 'CROSS JOIN';
return 'CROSS JOIN';
}
function findConditions(
table: DesignerTableInfo,
dumpedTables: DesignerTableInfo[],
references: DesignerReferenceInfo[],
tables: DesignerTableInfo[]
): Condition[] {
const dumpedTableIds = dumpedTables.map((x) => x.designerId);
const res = [];
for (const reference of references.filter(
(x) =>
(x.sourceId == table.designerId && dumpedTableIds.includes(x.targetId)) ||
(x.targetId == table.designerId && dumpedTableIds.includes(x.sourceId))
)) {
const sourceTable = tables.find((x) => x.designerId == reference.sourceId);
const targetTable = tables.find((x) => x.designerId == reference.targetId);
res.push(
...reference.columns.map((col) => ({
conditionType: 'binary',
operator: '=',
left: {
exprType: 'column',
columnName: col.source,
source: {
name: sourceTable,
alias: sourceTable.alias,
},
},
right: {
exprType: 'column',
columnName: col.target,
source: {
name: targetTable,
alias: targetTable.alias,
},
},
}))
);
}
return res;
}
export default function generateDesignedQuery(designer: DesignerInfo, engine: EngineDriver) {
const { tables, columns, references } = designer;
const primaryTable = findPrimaryTable(designer.tables);
if (!primaryTable) return '';
const components = groupByComponents(
designer.tables,
designer.references,
['INNER JOIN', 'LEFT JOIN', 'RIGHT JOIN', 'FULL OUTER JOIN', 'WHERE EXISTS', 'WHERE NOT EXISTS'],
primaryTable
);
const select: Select = {
commandType: 'select',
from: {
name: primaryTable,
alias: primaryTable.alias,
relations: [],
},
};
const dumpedTables = [primaryTable];
for (const component of components) {
const subComponents = groupByComponents(
component,
designer.references,
['INNER JOIN', 'LEFT JOIN', 'RIGHT JOIN', 'FULL OUTER JOIN'],
primaryTable
);
for (const subComponent of subComponents) {
for (const table of subComponent) {
if (dumpedTables.includes(table)) continue;
select.from.relations.push({
name: table,
alias: table.alias,
joinType: findJoinType(table, dumpedTables, designer.references, [
'INNER JOIN',
'LEFT JOIN',
'RIGHT JOIN',
'FULL OUTER JOIN',
]) as JoinType,
conditions: findConditions(table, dumpedTables, designer.references, designer.tables),
});
dumpedTables.push(table);
}
}
}
const dmp = engine.createDumper();
dumpSqlSelect(dmp, select);
return dmp.s;
}

View File

@ -0,0 +1,41 @@
import { JoinType } from 'dbgate-sqltree';
import { TableInfo } from 'dbgate-types';
export type DesignerTableInfo = TableInfo & {
designerId: string;
alias?: string;
left: number;
top: number;
};
export type DesignerJoinType = JoinType | 'WHERE EXISTS' | 'WHERE NOT EXISTS';
export type DesignerReferenceInfo = {
designerId: string;
joinType: DesignerJoinType;
sourceId: string;
targetId: string;
columns: {
source: string;
target: string;
}[];
};
export type DesignerColumnInfo = {
designerId: string;
columnName: string;
alias?: string;
isGrouped?: boolean;
isOutput?: boolean;
filter: string;
};
export type DesignerInfo = {
tables: DesignerTableInfo[];
columns: DesignerColumnInfo[];
references: DesignerReferenceInfo[];
};
// export type DesignerComponent = {
// tables: DesignerTableInfo[];
// };

View File

@ -23,6 +23,8 @@ import LoadingInfo from '../widgets/LoadingInfo';
import useExtensions from '../utility/useExtensions';
import QueryDesigner from '../designer/QueryDesigner';
import QueryDesignColumns from '../designer/QueryDesignColumns';
import { findEngineDriver } from 'dbgate-tools';
import generateDesignedQuery from '../designer/generateDesignedQuery';
export default function QueryDesignTab({
tabid,
@ -40,6 +42,9 @@ export default function QueryDesignTab({
const [busy, setBusy] = React.useState(false);
const saveFileModalState = useModalState();
const extensions = useExtensions();
const connection = useConnectionInfo({ conid });
const engine = findEngineDriver(connection, extensions);
const [sqlPreview, setSqlPreview] = React.useState('');
const { editorData, setEditorData, isLoading } = useEditorData({
tabid,
loadFromArgs:
@ -54,6 +59,16 @@ export default function QueryDesignTab({
setBusy(false);
}, []);
const generatePreview = (value, engine) => {
if (!engine || !value) return;
const sql = generateDesignedQuery(value, engine);
setSqlPreview(sql);
};
React.useEffect(() => {
generatePreview(editorData, engine);
}, [editorData, engine]);
React.useEffect(() => {
if (sessionId && socket) {
socket.on(`session-done-${sessionId}`, handleSessionDone);
@ -68,7 +83,6 @@ export default function QueryDesignTab({
}, [busy]);
useUpdateDatabaseForTab(tabVisible, conid, database);
const connection = useConnectionInfo({ conid });
const handleExecute = async () => {
if (busy) return;
@ -135,12 +149,17 @@ export default function QueryDesignTab({
<TabPage label="Columns" key="columns">
<QueryDesignColumns value={editorData || {}} onChange={setEditorData} />
</TabPage>
<TabPage label="SQL" key="sql">
<SqlEditor value={sqlPreview} engine={engine} readOnly />
</TabPage>
{sessionId && (
<TabPage label="Messages" key="messages">
<SocketMessagesView
eventName={sessionId ? `session-info-${sessionId}` : null}
executeNumber={executeNumber}
/>
</TabPage>
)}
</ResultTabs>
</VerticalSplitter>
{/* {toolbarPortalRef &&