diff --git a/packages/web/src/designer/ColumnLine.svelte b/packages/web/src/designer/ColumnLine.svelte
new file mode 100644
index 000000000..c7854c428
--- /dev/null
+++ b/packages/web/src/designer/ColumnLine.svelte
@@ -0,0 +1,57 @@
+
+
+
+ x.designerId == designerId && x.columnName == column.columnName && x.isOutput
+ )}
+ onChange={e => {
+ if (e.target.checked) {
+ onChangeColumn(
+ {
+ ...column,
+ designerId,
+ },
+ col => ({ ...col, isOutput: true })
+ );
+ } else {
+ onChangeColumn(
+ {
+ ...column,
+ designerId,
+ },
+ col => ({ ...col, isOutput: false })
+ );
+ }
+ }}
+ />
+
+ {#if designerColumn?.filter}
+
+ {/if}
+ {#if designerColumn?.sortOrder > 0}
+
+ {/if}
+ {#if designerColumn?.sortOrder < 0}
+
+ {/if}
+ {#if designerColumn?.isGrouped}
+
+ {/if}
+
diff --git a/packages/web/src/designer/Designer.svelte b/packages/web/src/designer/Designer.svelte
new file mode 100644
index 000000000..539b07945
--- /dev/null
+++ b/packages/web/src/designer/Designer.svelte
@@ -0,0 +1,261 @@
+
+
+
+
+
+ {#if !(tables?.length > 0)}
+
Drag & drop tables or views from left panel here
+ {/if}
+
+
+
+
+ {#each tables || [] as table (table.designerId)}
+
+ {/each}
+
+
+
+
diff --git a/packages/web/src/designer/DesignerComponentCreator.ts b/packages/web/src/designer/DesignerComponentCreator.ts
new file mode 100644
index 000000000..0f4ad809a
--- /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..d4c5e4af2
--- /dev/null
+++ b/packages/web/src/designer/DesignerQueryDumper.ts
@@ -0,0 +1,215 @@
+import _ from 'lodash';
+import {
+ dumpSqlSelect,
+ Select,
+ JoinType,
+ Condition,
+ Relation,
+ mergeConditions,
+ Source,
+ ResultField,
+} 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,
+ findDesignerFilterType,
+} from './designerTools';
+import { parseFilter } from 'dbgate-filterparser';
+
+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),
+ });
+ }
+ }
+ this.addConditions(select, component.tables);
+ }
+
+ return select;
+ }
+
+ addConditions(select: Select, tables: DesignerTableInfo[]) {
+ for (const column of this.designer.columns || []) {
+ if (!column.filter) continue;
+ const table = (this.designer.tables || []).find(x => x.designerId == column.designerId);
+ if (!table) continue;
+ if (!tables.find(x => x.designerId == table.designerId)) continue;
+
+ const condition = parseFilter(column.filter, findDesignerFilterType(column, this.designer));
+ if (condition) {
+ select.where = mergeConditions(
+ select.where,
+ _.cloneDeepWith(condition, expr => {
+ if (expr.exprType == 'placeholder')
+ return {
+ exprType: 'column',
+ columnName: column.columnName,
+ source: findQuerySource(this.designer, column.designerId),
+ };
+ })
+ );
+ }
+ }
+ }
+
+ addGroupConditions(select: Select, tables: DesignerTableInfo[], selectIsGrouped: boolean) {
+ for (const column of this.designer.columns || []) {
+ if (!column.groupFilter) continue;
+ const table = (this.designer.tables || []).find(x => x.designerId == column.designerId);
+ if (!table) continue;
+ if (!tables.find(x => x.designerId == table.designerId)) continue;
+
+ const condition = parseFilter(column.groupFilter, findDesignerFilterType(column, this.designer));
+ if (condition) {
+ select.having = mergeConditions(
+ select.having,
+ _.cloneDeepWith(condition, expr => {
+ if (expr.exprType == 'placeholder') {
+ return this.getColumnOutputExpression(column, selectIsGrouped);
+ }
+ })
+ );
+ }
+ }
+ }
+
+ getColumnOutputExpression(col, selectIsGrouped): ResultField {
+ const source = findQuerySource(this.designer, col.designerId);
+ const { columnName } = col;
+ let { alias } = col;
+ if (selectIsGrouped && !col.isGrouped) {
+ // use aggregate
+ const aggregate = col.aggregate == null || col.aggregate == '---' ? 'MAX' : col.aggregate;
+ if (!alias) alias = `${aggregate}(${columnName})`;
+
+ return {
+ exprType: 'call',
+ func: aggregate == 'COUNT DISTINCT' ? 'COUNT' : aggregate,
+ argsPrefix: aggregate == 'COUNT DISTINCT' ? 'DISTINCT' : null,
+ alias,
+ args: [
+ {
+ exprType: 'column',
+ columnName,
+ source,
+ },
+ ],
+ };
+ } else {
+ return {
+ exprType: 'column',
+ columnName,
+ alias,
+ source,
+ };
+ }
+ }
+
+ 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 selectIsGrouped = !!topLevelColumns.find(x => x.isGrouped || (x.aggregate && x.aggregate != '---'));
+ const outputColumns = topLevelColumns.filter(x => x.isOutput);
+ if (outputColumns.length == 0) {
+ res.selectAll = true;
+ } else {
+ res.columns = outputColumns.map(col => this.getColumnOutputExpression(col, selectIsGrouped));
+ }
+
+ const groupedColumns = topLevelColumns.filter(x => x.isGrouped);
+ if (groupedColumns.length > 0) {
+ res.groupBy = groupedColumns.map(col => ({
+ exprType: 'column',
+ columnName: col.columnName,
+ source: findQuerySource(this.designer, col.designerId),
+ }));
+ }
+
+ const orderColumns = _.sortBy(
+ topLevelColumns.filter(x => x.sortOrder),
+ x => Math.abs(x.sortOrder)
+ );
+ if (orderColumns.length > 0) {
+ res.orderBy = orderColumns.map(col => ({
+ exprType: 'column',
+ direction: col.sortOrder < 0 ? 'DESC' : 'ASC',
+ columnName: col.columnName,
+ source: findQuerySource(this.designer, col.designerId),
+ }));
+ }
+
+ this.addConditions(res, topLevelTables);
+ this.addGroupConditions(res, topLevelTables, selectIsGrouped);
+
+ return res;
+ }
+}
diff --git a/packages/web/src/designer/DesignerTable.svelte b/packages/web/src/designer/DesignerTable.svelte
new file mode 100644
index 000000000..727023c73
--- /dev/null
+++ b/packages/web/src/designer/DesignerTable.svelte
@@ -0,0 +1,86 @@
+
+
+
+
+
+ {#each columns || [] as column}
+
+ {/each}
+
+
+
+
diff --git a/packages/web/src/designer/DomTableRef.ts b/packages/web/src/designer/DomTableRef.ts
new file mode 100644
index 000000000..53aef90f0
--- /dev/null
+++ b/packages/web/src/designer/DomTableRef.ts
@@ -0,0 +1,39 @@
+import { DesignerTableInfo } from './types';
+
+export default class DomTableRef {
+ domTable: Element;
+ domWrapper: Element;
+ table: DesignerTableInfo;
+ designerId: string;
+ domRefs: { [column: string]: Element };
+
+ constructor(table: DesignerTableInfo, domRefs, domWrapper: Element) {
+ this.domTable = domRefs[''];
+ this.domWrapper = domWrapper;
+ this.table = table;
+ this.designerId = table.designerId;
+ this.domRefs = domRefs;
+ }
+
+ getRect() {
+ if (!this.domWrapper) return null;
+ if (!this.domTable) return null;
+
+ const wrap = this.domWrapper.getBoundingClientRect();
+ const rect = this.domTable.getBoundingClientRect();
+ return {
+ left: rect.left - wrap.left,
+ top: rect.top - wrap.top,
+ right: rect.right - wrap.left,
+ bottom: rect.bottom - wrap.top,
+ };
+ }
+
+ getColumnY(columnName: string) {
+ let col = this.domRefs[columnName];
+ if (!col) return null;
+ const rect = col.getBoundingClientRect();
+ const wrap = this.domWrapper.getBoundingClientRect();
+ return (rect.top + rect.bottom) / 2 - wrap.top;
+ }
+}
diff --git a/packages/web/src/designer/QueryDesigner.svelte b/packages/web/src/designer/QueryDesigner.svelte
new file mode 100644
index 000000000..782816e47
--- /dev/null
+++ b/packages/web/src/designer/QueryDesigner.svelte
@@ -0,0 +1,5 @@
+
+
+
diff --git a/packages/web/src/designer/cleanupDesignColumns.js b/packages/web/src/designer/cleanupDesignColumns.js
new file mode 100644
index 000000000..171eefd50
--- /dev/null
+++ b/packages/web/src/designer/cleanupDesignColumns.js
@@ -0,0 +1,5 @@
+export default function cleanupDesignColumns(columns) {
+ return (columns || []).filter(
+ x => x.isOutput || x.isGrouped || x.alias || (x.aggregate && x.aggregate != '---') || x.sortOrder || x.filter
+ );
+}
diff --git a/packages/web/src/designer/designerTools.ts b/packages/web/src/designer/designerTools.ts
new file mode 100644
index 000000000..5ed0152d8
--- /dev/null
+++ b/packages/web/src/designer/designerTools.ts
@@ -0,0 +1,144 @@
+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';
+import { getFilterType } from 'dbgate-filterparser';
+
+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 }
+) {
+ if (!designer.references) return false;
+ 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;
+}
+
+export function findDesignerFilterType({ designerId, columnName }, designer) {
+ const table = (designer.tables || []).find(x => x.designerId == designerId);
+ if (table) {
+ const column = (table.columns || []).find(x => x.columnName == columnName);
+ if (column) {
+ const { dataType } = column;
+ return getFilterType(dataType);
+ }
+ }
+ return 'string';
+}
diff --git a/packages/web/src/designer/types.ts b/packages/web/src/designer/types.ts
new file mode 100644
index 000000000..c746f7ac7
--- /dev/null
+++ b/packages/web/src/designer/types.ts
@@ -0,0 +1,44 @@
+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;
+ aggregate?: string;
+ isOutput?: boolean;
+ sortOrder?: number;
+ filter?: string;
+ groupFilter?: string;
+};
+
+export type DesignerInfo = {
+ tables: DesignerTableInfo[];
+ columns: DesignerColumnInfo[];
+ references: DesignerReferenceInfo[];
+};
+
+// export type DesignerComponent = {
+// tables: DesignerTableInfo[];
+// };
diff --git a/packages/web/src/plugins/ThemeDark.svelte b/packages/web/src/plugins/ThemeDark.svelte
index 2e7ddbe6e..08404467f 100644
--- a/packages/web/src/plugins/ThemeDark.svelte
+++ b/packages/web/src/plugins/ThemeDark.svelte
@@ -32,6 +32,8 @@
--theme-bg-green: #1d3712; /* green-2 */
--theme-bg-volcano: #441d12; /* volcano-2 */
--theme-bg-red: #431418; /* red-2 */
+ --theme-bg-blue: #15395b; /* blue-3 */
+ --theme-bg-magenta: #551c3b; /* magenta-3 */
--theme-font-inv-1: #ffffff;
--theme-font-inv-2: #b3b3b3;
diff --git a/packages/web/src/plugins/ThemeLight.svelte b/packages/web/src/plugins/ThemeLight.svelte
index 728f5285e..8edbf61fc 100644
--- a/packages/web/src/plugins/ThemeLight.svelte
+++ b/packages/web/src/plugins/ThemeLight.svelte
@@ -25,6 +25,8 @@
--theme-bg-green: #d9f7be; /* green-2 */
--theme-bg-volcano: #ffd8bf; /* volcano-2 */
--theme-bg-red: #ffccc7; /* red-2 */
+ --theme-bg-blue: #91d5ff; /* blue-3 */
+ --theme-bg-magenta: #ffadd2; /* magenta-3 */
--theme-font-inv-1: #ffffff;
--theme-font-inv-2: #b3b3b3;
diff --git a/packages/web/src/tabs/QueryDesignTab.svelte b/packages/web/src/tabs/QueryDesignTab.svelte
new file mode 100644
index 000000000..7b311fb6e
--- /dev/null
+++ b/packages/web/src/tabs/QueryDesignTab.svelte
@@ -0,0 +1,189 @@
+
+
+
+
+
+
diff --git a/packages/web/src/tabs/index.js b/packages/web/src/tabs/index.js
index 46cc527b0..d272c5a62 100644
--- a/packages/web/src/tabs/index.js
+++ b/packages/web/src/tabs/index.js
@@ -12,7 +12,7 @@ import * as MarkdownEditorTab from './MarkdownEditorTab.svelte';
// import MarkdownViewTab from './MarkdownViewTab';
// import MarkdownPreviewTab from './MarkdownPreviewTab';
// import FavoriteEditorTab from './FavoriteEditorTab';
-// import QueryDesignTab from './QueryDesignTab';
+import * as QueryDesignTab from './QueryDesignTab.svelte';
export default {
TableDataTab,
@@ -29,5 +29,5 @@ export default {
// MarkdownViewTab,
// MarkdownPreviewTab,
// FavoriteEditorTab,
- // QueryDesignTab,
+ QueryDesignTab,
};