diff --git a/packages/web/src/designer/Designer.js b/packages/web/src/designer/Designer.js index a7226b0ea..2aea4017d 100644 --- a/packages/web/src/designer/Designer.js +++ b/packages/web/src/designer/Designer.js @@ -6,7 +6,7 @@ import _ from 'lodash'; import useTheme from '../theme/useTheme'; import DesignerReference from './DesignerReference'; import cleanupDesignColumns from './cleanupDesignColumns'; -import { isConnectedByReference } from './generateDesignedQuery'; +import { isConnectedByReference } from './designerTools'; const Wrapper = styled.div` flex: 1; diff --git a/packages/web/src/designer/DesignerComponentCreator.ts b/packages/web/src/designer/DesignerComponentCreator.ts new file mode 100644 index 000000000..9b3730cb8 --- /dev/null +++ b/packages/web/src/designer/DesignerComponentCreator.ts @@ -0,0 +1,91 @@ +import _ from 'lodash'; +import { dumpSqlSelect, Select, JoinType, Condition, Relation, mergeConditions, Source } from 'dbgate-sqltree'; +import { EngineDriver } from 'dbgate-types'; +import { DesignerInfo, DesignerTableInfo, DesignerReferenceInfo, DesignerJoinType } from './types'; +import { findPrimaryTable, findConnectingReference, referenceIsJoin, referenceIsExists } from './designerTools'; + +export class DesignerComponent { + subComponents: DesignerComponent[] = []; + parentComponent: DesignerComponent; + parentReference: DesignerReferenceInfo; + + tables: DesignerTableInfo[] = []; + nonPrimaryReferences: DesignerReferenceInfo[] = []; + + get primaryTable() { + return this.tables[0]; + } + get nonPrimaryTables() { + return this.tables.slice(1); + } + get nonPrimaryTablesAndReferences() { + return _.zip(this.nonPrimaryTables, this.nonPrimaryReferences); + } + get myAndParentTables() { + return [...this.parentTables, ...this.tables]; + } + get parentTables() { + return this.parentComponent ? this.parentComponent.myAndParentTables : []; + } + get thisAndSubComponentsTables() { + return [...this.tables, ..._.flatten(this.subComponents.map((x) => x.thisAndSubComponentsTables))]; + } +} + +export class DesignerComponentCreator { + toAdd: DesignerTableInfo[]; + components: DesignerComponent[] = []; + + constructor(public designer: DesignerInfo) { + this.toAdd = [...designer.tables]; + while (this.toAdd.length > 0) { + const component = this.parseComponent(null); + this.components.push(component); + } + } + + parseComponent(root) { + if (root == null) { + root = findPrimaryTable(this.toAdd); + } + if (!root) return null; + _.remove(this.toAdd, (x) => x == root); + const res = new DesignerComponent(); + res.tables.push(root); + + for (;;) { + let found = false; + for (const test of this.toAdd) { + const ref = findConnectingReference(this.designer, res.tables, [test], referenceIsJoin); + if (ref) { + res.tables.push(test); + res.nonPrimaryReferences.push(ref); + _.remove(this.toAdd, (x) => x == test); + found = true; + break; + } + } + + if (!found) break; + } + + for (;;) { + let found = false; + for (const test of this.toAdd) { + const ref = findConnectingReference(this.designer, res.tables, [test], referenceIsExists); + if (ref) { + const subComponent = this.parseComponent(test); + res.subComponents.push(subComponent); + subComponent.parentComponent = res; + subComponent.parentReference = ref; + found = true; + break; + } + } + + if (!found) break; + } + + return res; + } +} diff --git a/packages/web/src/designer/DesignerQueryDumper.ts b/packages/web/src/designer/DesignerQueryDumper.ts new file mode 100644 index 000000000..3779f7422 --- /dev/null +++ b/packages/web/src/designer/DesignerQueryDumper.ts @@ -0,0 +1,105 @@ +import _ from 'lodash'; +import { dumpSqlSelect, Select, JoinType, Condition, Relation, mergeConditions, Source } from 'dbgate-sqltree'; +import { EngineDriver } from 'dbgate-types'; +import { DesignerInfo, DesignerTableInfo, DesignerReferenceInfo, DesignerJoinType } from './types'; +import { DesignerComponent } from './DesignerComponentCreator'; +import { + getReferenceConditions, + referenceIsCrossJoin, + referenceIsConnecting, + mergeSelectsFromDesigner, + findQuerySource, +} from './designerTools'; + +export class DesignerQueryDumper { + constructor(public designer: DesignerInfo, public components: DesignerComponent[]) {} + + get topLevelTables(): DesignerTableInfo[] { + return _.flatten(this.components.map((x) => x.tables)); + } + + dumpComponent(component: DesignerComponent) { + const select: Select = { + commandType: 'select', + from: { + name: component.primaryTable, + alias: component.primaryTable.alias, + relations: [], + }, + }; + + for (const [table, ref] of component.nonPrimaryTablesAndReferences) { + select.from.relations.push({ + name: table, + alias: table.alias, + joinType: ref.joinType as JoinType, + conditions: getReferenceConditions(ref, this.designer), + }); + } + + for (const subComponent of component.subComponents) { + const subQuery = this.dumpComponent(subComponent); + subQuery.selectAll = true; + select.where = mergeConditions(select.where, { + conditionType: subComponent.parentReference.joinType == 'WHERE NOT EXISTS' ? 'notExists' : 'exists', + subQuery, + }); + } + + if (component.parentReference) { + select.where = mergeConditions(select.where, { + conditionType: 'and', + conditions: getReferenceConditions(component.parentReference, this.designer), + }); + + // cross join conditions in subcomponents + for (const ref of this.designer.references) { + if (referenceIsCrossJoin(ref) && referenceIsConnecting(ref, component.tables, component.myAndParentTables)) { + select.where = mergeConditions(select.where, { + conditionType: 'and', + conditions: getReferenceConditions(ref, this.designer), + }); + } + } + } + + return select; + } + + run() { + let res: Select = null; + for (const component of this.components) { + const select = this.dumpComponent(component); + if (res == null) res = select; + else res = mergeSelectsFromDesigner(res, select); + } + + // top level cross join conditions + const topLevelTables = this.topLevelTables; + for (const ref of this.designer.references) { + if (referenceIsCrossJoin(ref) && referenceIsConnecting(ref, topLevelTables, topLevelTables)) { + res.where = mergeConditions(res.where, { + conditionType: 'and', + conditions: getReferenceConditions(ref, this.designer), + }); + } + } + + const topLevelColumns = this.designer.columns.filter((col) => + topLevelTables.find((tbl) => tbl.designerId == col.designerId) + ); + const outputColumns = topLevelColumns.filter((x) => x.isOutput); + if (outputColumns.length == 0) { + res.selectAll = true; + } else { + res.columns = outputColumns.map((col) => ({ + exprType: 'column', + columnName: col.columnName, + alias: col.alias, + source: findQuerySource(this.designer, col.designerId), + })); + } + + return res; + } +} diff --git a/packages/web/src/designer/DesignerReference.js b/packages/web/src/designer/DesignerReference.js index fb4d5d135..e1e33ea25 100644 --- a/packages/web/src/designer/DesignerReference.js +++ b/packages/web/src/designer/DesignerReference.js @@ -5,7 +5,7 @@ import _ from 'lodash'; import useTheme from '../theme/useTheme'; import { useShowMenu } from '../modals/showMenu'; import { DropDownMenuDivider, DropDownMenuItem } from '../modals/DropDownMenu'; -import { isConnectedByReference } from './generateDesignedQuery'; +import { isConnectedByReference } from './designerTools'; const StyledSvg = styled.svg` position: absolute; diff --git a/packages/web/src/designer/designerTools.ts b/packages/web/src/designer/designerTools.ts new file mode 100644 index 000000000..f50fcc9f4 --- /dev/null +++ b/packages/web/src/designer/designerTools.ts @@ -0,0 +1,131 @@ +import _ from 'lodash'; +import { dumpSqlSelect, Select, JoinType, Condition, Relation, mergeConditions, Source } from 'dbgate-sqltree'; +import { EngineDriver } from 'dbgate-types'; +import { DesignerInfo, DesignerTableInfo, DesignerReferenceInfo, DesignerJoinType } from './types'; +import { DesignerComponentCreator } from './DesignerComponentCreator'; +import { DesignerQueryDumper } from './DesignerQueryDumper'; + +export function referenceIsConnecting( + reference: DesignerReferenceInfo, + tables1: DesignerTableInfo[], + tables2: DesignerTableInfo[] +) { + return ( + (tables1.find((x) => x.designerId == reference.sourceId) && + tables2.find((x) => x.designerId == reference.targetId)) || + (tables1.find((x) => x.designerId == reference.targetId) && tables2.find((x) => x.designerId == reference.sourceId)) + ); +} + +export function referenceIsJoin(reference) { + return ['INNER JOIN', 'LEFT JOIN', 'RIGHT JOIN', 'FULL OUTER JOIN'].includes(reference.joinType); +} +export function referenceIsExists(reference) { + return ['WHERE EXISTS', 'WHERE NOT EXISTS'].includes(reference.joinType); +} +export function referenceIsCrossJoin(reference) { + return !reference.joinType || reference.joinType == 'CROSS JOIN'; +} + +export function findConnectingReference( + designer: DesignerInfo, + tables1: DesignerTableInfo[], + tables2: DesignerTableInfo[], + additionalCondition: (ref: DesignerReferenceInfo) => boolean +) { + for (const ref of designer.references) { + if (additionalCondition(ref) && referenceIsConnecting(ref, tables1, tables2)) { + return ref; + } + } + return null; +} + +export function findQuerySource(designer: DesignerInfo, designerId: string): Source { + const table = designer.tables.find((x) => x.designerId == designerId); + if (!table) return null; + return { + name: table, + alias: table.alias, + }; +} + +export function mergeSelectsFromDesigner(select1: Select, select2: Select): Select { + return { + commandType: 'select', + from: { + ...select1.from, + relations: [ + ...select1.from.relations, + { + joinType: 'CROSS JOIN', + name: select2.from.name, + alias: select2.from.alias, + }, + ...select2.from.relations, + ], + }, + where: mergeConditions(select1.where, select2.where), + }; +} + +export function findPrimaryTable(tables: DesignerTableInfo[]) { + return _.minBy(tables, (x) => x.top); +} + +export function getReferenceConditions(reference: DesignerReferenceInfo, designer: DesignerInfo): Condition[] { + const sourceTable = designer.tables.find((x) => x.designerId == reference.sourceId); + const targetTable = designer.tables.find((x) => x.designerId == reference.targetId); + + return 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, + }, + }, + })); +} + +export function generateDesignedQuery(designer: DesignerInfo, engine: EngineDriver) { + const { tables, columns, references } = designer; + const primaryTable = findPrimaryTable(designer.tables); + if (!primaryTable) return ''; + const componentCreator = new DesignerComponentCreator(designer); + const designerDumper = new DesignerQueryDumper(designer, componentCreator.components); + const select = designerDumper.run(); + + const dmp = engine.createDumper(); + dumpSqlSelect(dmp, select); + return dmp.s; +} + +export function isConnectedByReference( + designer: DesignerInfo, + table1: { designerId: string }, + table2: { designerId: string }, + withoutRef: { designerId: string } +) { + const creator = new DesignerComponentCreator({ + ...designer, + references: withoutRef + ? designer.references.filter((x) => x.designerId != withoutRef.designerId) + : designer.references, + }); + const arrays = creator.components.map((x) => x.thisAndSubComponentsTables); + const array1 = arrays.find((a) => a.find((x) => x.designerId == table1.designerId)); + const array2 = arrays.find((a) => a.find((x) => x.designerId == table2.designerId)); + return array1 == array2; +} diff --git a/packages/web/src/designer/generateDesignedQuery.ts b/packages/web/src/designer/generateDesignedQuery.ts deleted file mode 100644 index 0d1936bc7..000000000 --- a/packages/web/src/designer/generateDesignedQuery.ts +++ /dev/null @@ -1,306 +0,0 @@ -import _ from 'lodash'; -import { dumpSqlSelect, Select, JoinType, Condition, Relation, mergeConditions, Source } from 'dbgate-sqltree'; -import { EngineDriver } from 'dbgate-types'; -import { DesignerInfo, DesignerTableInfo, DesignerReferenceInfo, DesignerJoinType } from './types'; - -class DesignerComponent { - subComponents: DesignerComponent[] = []; - parentComponent: DesignerComponent; - parentReference: DesignerReferenceInfo; - - tables: DesignerTableInfo[] = []; - nonPrimaryReferences: DesignerReferenceInfo[] = []; - - get primaryTable() { - return this.tables[0]; - } - get nonPrimaryTables() { - return this.tables.slice(1); - } - get nonPrimaryTablesAndReferences() { - return _.zip(this.nonPrimaryTables, this.nonPrimaryReferences); - } - get myAndParentTables() { - return [...this.parentTables, ...this.tables]; - } - get parentTables() { - return this.parentComponent ? this.parentComponent.myAndParentTables : []; - } - get thisAndSubComponentsTables() { - return [...this.tables, ..._.flatten(this.subComponents.map((x) => x.thisAndSubComponentsTables))]; - } -} - -function referenceIsConnecting( - reference: DesignerReferenceInfo, - tables1: DesignerTableInfo[], - tables2: DesignerTableInfo[] -) { - return ( - (tables1.find((x) => x.designerId == reference.sourceId) && - tables2.find((x) => x.designerId == reference.targetId)) || - (tables1.find((x) => x.designerId == reference.targetId) && tables2.find((x) => x.designerId == reference.sourceId)) - ); -} - -function referenceIsJoin(reference) { - return ['INNER JOIN', 'LEFT JOIN', 'RIGHT JOIN', 'FULL OUTER JOIN'].includes(reference.joinType); -} -function referenceIsExists(reference) { - return ['WHERE EXISTS', 'WHERE NOT EXISTS'].includes(reference.joinType); -} -function referenceIsCrossJoin(reference) { - return !reference.joinType || reference.joinType == 'CROSS JOIN'; -} - -function findConnectingReference( - designer: DesignerInfo, - tables1: DesignerTableInfo[], - tables2: DesignerTableInfo[], - additionalCondition: (ref: DesignerReferenceInfo) => boolean -) { - for (const ref of designer.references) { - if (additionalCondition(ref) && referenceIsConnecting(ref, tables1, tables2)) { - return ref; - } - } - return null; -} - -function findQuerySource(designer: DesignerInfo, designerId: string): Source { - const table = designer.tables.find((x) => x.designerId == designerId); - if (!table) return null; - return { - name: table, - alias: table.alias, - }; -} -class DesignerComponentCreator { - toAdd: DesignerTableInfo[]; - components: DesignerComponent[] = []; - - constructor(public designer: DesignerInfo) { - this.toAdd = [...designer.tables]; - while (this.toAdd.length > 0) { - const component = this.parseComponent(null); - this.components.push(component); - } - } - - parseComponent(root) { - if (root == null) { - root = findPrimaryTable(this.toAdd); - } - if (!root) return null; - _.remove(this.toAdd, (x) => x == root); - const res = new DesignerComponent(); - res.tables.push(root); - - for (;;) { - let found = false; - for (const test of this.toAdd) { - const ref = findConnectingReference(this.designer, res.tables, [test], referenceIsJoin); - if (ref) { - res.tables.push(test); - res.nonPrimaryReferences.push(ref); - _.remove(this.toAdd, (x) => x == test); - found = true; - break; - } - } - - if (!found) break; - } - - for (;;) { - let found = false; - for (const test of this.toAdd) { - const ref = findConnectingReference(this.designer, res.tables, [test], referenceIsExists); - if (ref) { - const subComponent = this.parseComponent(test); - res.subComponents.push(subComponent); - subComponent.parentComponent = res; - subComponent.parentReference = ref; - found = true; - break; - } - } - - if (!found) break; - } - - return res; - } -} - -function mergeSelects(select1: Select, select2: Select): Select { - return { - commandType: 'select', - from: { - ...select1.from, - relations: [ - ...select1.from.relations, - { - joinType: 'CROSS JOIN', - name: select2.from.name, - alias: select2.from.alias, - }, - ...select2.from.relations, - ], - }, - where: mergeConditions(select1.where, select2.where), - }; -} -class DesignerQueryDumper { - constructor(public designer: DesignerInfo, public components: DesignerComponent[]) {} - - get topLevelTables(): DesignerTableInfo[] { - return _.flatten(this.components.map((x) => x.tables)); - } - - dumpComponent(component: DesignerComponent) { - const select: Select = { - commandType: 'select', - from: { - name: component.primaryTable, - alias: component.primaryTable.alias, - relations: [], - }, - }; - - for (const [table, ref] of component.nonPrimaryTablesAndReferences) { - select.from.relations.push({ - name: table, - alias: table.alias, - joinType: ref.joinType as JoinType, - conditions: getReferenceConditions(ref, this.designer), - }); - } - - for (const subComponent of component.subComponents) { - const subQuery = this.dumpComponent(subComponent); - subQuery.selectAll = true; - select.where = mergeConditions(select.where, { - conditionType: subComponent.parentReference.joinType == 'WHERE NOT EXISTS' ? 'notExists' : 'exists', - subQuery, - }); - } - - if (component.parentReference) { - select.where = mergeConditions(select.where, { - conditionType: 'and', - conditions: getReferenceConditions(component.parentReference, this.designer), - }); - - // cross join conditions in subcomponents - for (const ref of this.designer.references) { - if (referenceIsCrossJoin(ref) && referenceIsConnecting(ref, component.tables, component.myAndParentTables)) { - select.where = mergeConditions(select.where, { - conditionType: 'and', - conditions: getReferenceConditions(ref, this.designer), - }); - } - } - } - - return select; - } - - run() { - let res: Select = null; - for (const component of this.components) { - const select = this.dumpComponent(component); - if (res == null) res = select; - else res = mergeSelects(res, select); - } - - // top level cross join conditions - const topLevelTables = this.topLevelTables; - for (const ref of this.designer.references) { - if (referenceIsCrossJoin(ref) && referenceIsConnecting(ref, topLevelTables, topLevelTables)) { - res.where = mergeConditions(res.where, { - conditionType: 'and', - conditions: getReferenceConditions(ref, this.designer), - }); - } - } - - const topLevelColumns = this.designer.columns.filter((col) => - topLevelTables.find((tbl) => tbl.designerId == col.designerId) - ); - const outputColumns = topLevelColumns.filter((x) => x.isOutput); - if (outputColumns.length == 0) { - res.selectAll = true; - } else { - res.columns = outputColumns.map((col) => ({ - exprType: 'column', - columnName: col.columnName, - alias: col.alias, - source: findQuerySource(this.designer, col.designerId), - })); - } - - return res; - } -} - -function findPrimaryTable(tables: DesignerTableInfo[]) { - return _.minBy(tables, (x) => x.top); -} - -function getReferenceConditions(reference: DesignerReferenceInfo, designer: DesignerInfo): Condition[] { - const sourceTable = designer.tables.find((x) => x.designerId == reference.sourceId); - const targetTable = designer.tables.find((x) => x.designerId == reference.targetId); - - return 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, - }, - }, - })); -} - -export default function generateDesignedQuery(designer: DesignerInfo, engine: EngineDriver) { - const { tables, columns, references } = designer; - const primaryTable = findPrimaryTable(designer.tables); - if (!primaryTable) return ''; - const componentCreator = new DesignerComponentCreator(designer); - const designerDumper = new DesignerQueryDumper(designer, componentCreator.components); - const select = designerDumper.run(); - - const dmp = engine.createDumper(); - dumpSqlSelect(dmp, select); - return dmp.s; -} - -export function isConnectedByReference( - designer: DesignerInfo, - table1: { designerId: string }, - table2: { designerId: string }, - withoutRef: { designerId: string } -) { - const creator = new DesignerComponentCreator({ - ...designer, - references: withoutRef - ? designer.references.filter((x) => x.designerId != withoutRef.designerId) - : designer.references, - }); - const arrays = creator.components.map((x) => x.thisAndSubComponentsTables); - const array1 = arrays.find((a) => a.find((x) => x.designerId == table1.designerId)); - const array2 = arrays.find((a) => a.find((x) => x.designerId == table2.designerId)); - return array1 == array2; -} diff --git a/packages/web/src/tabs/QueryDesignTab.js b/packages/web/src/tabs/QueryDesignTab.js index ad17910ea..8359bf15d 100644 --- a/packages/web/src/tabs/QueryDesignTab.js +++ b/packages/web/src/tabs/QueryDesignTab.js @@ -24,7 +24,7 @@ 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'; +import { generateDesignedQuery } from '../designer/designerTools'; export default function QueryDesignTab({ tabid,