diff --git a/packages/sqltree/src/dumpSqlSource.ts b/packages/sqltree/src/dumpSqlSource.ts index 46590dc3f..f6b58a53a 100644 --- a/packages/sqltree/src/dumpSqlSource.ts +++ b/packages/sqltree/src/dumpSqlSource.ts @@ -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)); } diff --git a/packages/sqltree/src/types.ts b/packages/sqltree/src/types.ts index b6d3da364..8bb29a060 100644 --- a/packages/sqltree/src/types.ts +++ b/packages/sqltree/src/types.ts @@ -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[]; diff --git a/packages/web/src/designer/Designer.js b/packages/web/src/designer/Designer.js index d016020c9..b345ed28d 100644 --- a/packages/web/src/designer/Designer.js +++ b/packages/web/src/designer/Designer.js @@ -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, diff --git a/packages/web/src/designer/DesignerReference.js b/packages/web/src/designer/DesignerReference.js index 7a6ed6a8c..a02ab63d9 100644 --- a/packages/web/src/designer/DesignerReference.js +++ b/packages/web/src/designer/DesignerReference.js @@ -46,6 +46,7 @@ function ReferenceContextMenu({ remove, setJoinType }) { setJoinType('LEFT JOIN')}>Set LEFT JOIN setJoinType('RIGHT JOIN')}>Set RIGHT JOIN setJoinType('FULL OUTER JOIN')}>Set FULL OUTER JOIN + setJoinType('CROSS JOIN')}>Set CROSS JOIN setJoinType('WHERE EXISTS')}>Set WHERE EXISTS setJoinType('WHERE NOT EXISTS')}>Set WHERE NOT EXISTS @@ -159,7 +160,7 @@ export default function DesignerReference({ onContextMenu={handleContextMenu} > - {_.snakeCase(joinType || 'INNER JOIN') + {_.snakeCase(joinType || 'CROSS JOIN') .replace('_', '\xa0') .replace('_', '\xa0')} diff --git a/packages/web/src/designer/DomTableRef.ts b/packages/web/src/designer/DomTableRef.ts index b257a262c..ca37a54ba 100644 --- a/packages/web/src/designer/DomTableRef.ts +++ b/packages/web/src/designer/DomTableRef.ts @@ -1,6 +1,4 @@ -import { TableInfo } from 'dbgate-types'; - -type DesignerTableInfo = TableInfo & { designerId: string }; +import { DesignerTableInfo } from "./types"; export default class DomTableRef { domTable: Element; diff --git a/packages/web/src/designer/generateDesignedQuery.ts b/packages/web/src/designer/generateDesignedQuery.ts new file mode 100644 index 000000000..e33673384 --- /dev/null +++ b/packages/web/src/designer/generateDesignedQuery.ts @@ -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; +} diff --git a/packages/web/src/designer/types.ts b/packages/web/src/designer/types.ts new file mode 100644 index 000000000..b62577854 --- /dev/null +++ b/packages/web/src/designer/types.ts @@ -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[]; +// }; diff --git a/packages/web/src/tabs/QueryDesignTab.js b/packages/web/src/tabs/QueryDesignTab.js index b96abe373..ad17910ea 100644 --- a/packages/web/src/tabs/QueryDesignTab.js +++ b/packages/web/src/tabs/QueryDesignTab.js @@ -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({ - - + + + {sessionId && ( + + + + )} {/* {toolbarPortalRef &&