mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-04-27 23:06:01 +00:00
grouping - work with datetimes
This commit is contained in:
@@ -189,25 +189,50 @@ export abstract class GridDisplay {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get groupColumns() {
|
get groupColumns() {
|
||||||
return this.isGrouped ? _.keys(_.pickBy(this.config.grouping, (v) => v == 'GROUP')) : null;
|
return this.isGrouped
|
||||||
|
? _.keys(_.pickBy(this.config.grouping, (v) => v == 'GROUP' || v.startsWith('GROUP:')))
|
||||||
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
applyGroupOnSelect(select: Select, displayedColumnInfo: DisplayedColumnInfo) {
|
applyGroupOnSelect(select: Select, displayedColumnInfo: DisplayedColumnInfo) {
|
||||||
const groupColumns = this.groupColumns;
|
const groupColumns = this.groupColumns;
|
||||||
if (groupColumns && groupColumns.length > 0) {
|
if (groupColumns && groupColumns.length > 0) {
|
||||||
select.groupBy = groupColumns.map((col) => ({
|
// @ts-ignore
|
||||||
exprType: 'column',
|
select.groupBy = groupColumns.map((col) => {
|
||||||
columnName: displayedColumnInfo[col].columnName,
|
const colExpr: Expression = {
|
||||||
source: { alias: displayedColumnInfo[col].sourceAlias },
|
exprType: 'column',
|
||||||
}));
|
columnName: displayedColumnInfo[col].columnName,
|
||||||
|
source: { alias: displayedColumnInfo[col].sourceAlias },
|
||||||
|
};
|
||||||
|
const grouping = this.config.grouping[col];
|
||||||
|
if (grouping.startsWith('GROUP:')) {
|
||||||
|
return {
|
||||||
|
exprType: 'transform',
|
||||||
|
transform: grouping,
|
||||||
|
expr: colExpr,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return colExpr;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (!_.isEmpty(this.config.grouping)) {
|
if (!_.isEmpty(this.config.grouping)) {
|
||||||
for (let i = 0; i < select.columns.length; i++) {
|
for (let i = 0; i < select.columns.length; i++) {
|
||||||
const uniqueName = select.columns[i].alias;
|
const uniqueName = select.columns[i].alias;
|
||||||
if (groupColumns && groupColumns.includes(uniqueName)) continue;
|
// if (groupColumns && groupColumns.includes(uniqueName)) continue;
|
||||||
const grouping = this.getGrouping(uniqueName);
|
const grouping = this.getGrouping(uniqueName);
|
||||||
if (grouping == 'NULL') {
|
if (grouping == 'GROUP') {
|
||||||
|
continue;
|
||||||
|
} else if (grouping == 'NULL') {
|
||||||
select.columns[i].alias = null;
|
select.columns[i].alias = null;
|
||||||
|
} else if (grouping && grouping.startsWith('GROUP:')) {
|
||||||
|
select.columns[i] = {
|
||||||
|
exprType: 'transform',
|
||||||
|
// @ts-ignore
|
||||||
|
transform: grouping,
|
||||||
|
expr: select.columns[i],
|
||||||
|
alias: select.columns[i].alias,
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
let func = 'MAX';
|
let func = 'MAX';
|
||||||
let argsPrefix = '';
|
let argsPrefix = '';
|
||||||
|
|||||||
@@ -76,6 +76,9 @@ class SqlDumper {
|
|||||||
case 'v':
|
case 'v':
|
||||||
this.putValue(value);
|
this.putValue(value);
|
||||||
break;
|
break;
|
||||||
|
case 'c':
|
||||||
|
value(this);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
putFormattedList(c, collection) {
|
putFormattedList(c, collection) {
|
||||||
@@ -255,6 +258,11 @@ class SqlDumper {
|
|||||||
if (fk.updateAction) this.put(' ^on ^update %k', fk.updateAction);
|
if (fk.updateAction) this.put(' ^on ^update %k', fk.updateAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param type {import('@dbgate/types').TransformType} */
|
||||||
|
transform(type, dumpExpr) {
|
||||||
|
dumpExpr();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param table {import('@dbgate/types').NamedObjectInfo}
|
* @param table {import('@dbgate/types').NamedObjectInfo}
|
||||||
* @param allow {boolean}
|
* @param allow {boolean}
|
||||||
|
|||||||
@@ -13,7 +13,41 @@ class MsSqlDumper extends SqlDumper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
allowIdentityInsert(table, allow) {
|
allowIdentityInsert(table, allow) {
|
||||||
this.putCmd("^set ^identity_insert %f %k;&n", table, allow ? "on" : "off");
|
this.putCmd('^set ^identity_insert %f %k;&n', table, allow ? 'on' : 'off');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param type {import('@dbgate/types').TransformType} */
|
||||||
|
transform(type, dumpExpr) {
|
||||||
|
switch (type) {
|
||||||
|
case 'GROUP:YEAR':
|
||||||
|
case 'YEAR':
|
||||||
|
this.put('^datepart(^year, %c)', dumpExpr);
|
||||||
|
break;
|
||||||
|
case 'MONTH':
|
||||||
|
this.put('^datepart(^month, %c)', dumpExpr);
|
||||||
|
break;
|
||||||
|
case 'DAY':
|
||||||
|
this.put('^datepart(^day, %c)', dumpExpr);
|
||||||
|
break;
|
||||||
|
case 'GROUP:MONTH':
|
||||||
|
this.put(
|
||||||
|
"^convert(^varchar(100), ^datepart(^year, %c)) + '-' + ^convert(^varchar(100), ^datepart(^month, %c))",
|
||||||
|
dumpExpr,
|
||||||
|
dumpExpr
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'GROUP:DAY':
|
||||||
|
this.put(
|
||||||
|
"^convert(^varchar(100), ^datepart(^year, %c)) + '-' + ^convert(^varchar(100), ^datepart(^month, %c))+'-' + ^convert(^varchar(100), ^datepart(^day, %c))",
|
||||||
|
dumpExpr,
|
||||||
|
dumpExpr,
|
||||||
|
dumpExpr
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
dumpExpr();
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
import { isTypeDateTime } from '@dbgate/tools';
|
||||||
|
|
||||||
export type FilterMultipleValuesMode = 'is' | 'is_not' | 'contains' | 'begins' | 'ends';
|
export type FilterMultipleValuesMode = 'is' | 'is_not' | 'contains' | 'begins' | 'ends';
|
||||||
|
|
||||||
export function getFilterValueExpression(value) {
|
export function getFilterValueExpression(value, dataType) {
|
||||||
if (value == null) return 'NULL';
|
if (value == null) return 'NULL';
|
||||||
|
if (isTypeDateTime(dataType)) return value;
|
||||||
return `="${value}"`;
|
return `="${value}"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import P from 'parsimmon';
|
import P from 'parsimmon';
|
||||||
import { FilterType } from './types';
|
import { FilterType } from './types';
|
||||||
import { Condition } from '@dbgate/sqltree';
|
import { Condition } from '@dbgate/sqltree';
|
||||||
|
import { TransformType } from '@dbgate/types';
|
||||||
|
|
||||||
const whitespace = P.regexp(/\s*/m);
|
const whitespace = P.regexp(/\s*/m);
|
||||||
|
|
||||||
@@ -94,6 +95,50 @@ const negateCondition = (condition) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function getTransformCondition(transform: TransformType, value) {
|
||||||
|
return {
|
||||||
|
conditionType: 'binary',
|
||||||
|
operator: '=',
|
||||||
|
left: {
|
||||||
|
exprType: 'transform',
|
||||||
|
transform,
|
||||||
|
expr: {
|
||||||
|
exprType: 'placeholder',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
right: {
|
||||||
|
exprType: 'value',
|
||||||
|
value,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const yearCondition = () => (value) => {
|
||||||
|
return getTransformCondition('YEAR', value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const yearMonthCondition = () => (value) => {
|
||||||
|
const m = value.match(/(\d\d\d\d)-(\d\d?)/);
|
||||||
|
|
||||||
|
return {
|
||||||
|
conditionType: 'and',
|
||||||
|
conditions: [getTransformCondition('YEAR', m[1]), getTransformCondition('MONTH', m[2])],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const yearMonthDayCondition = () => (value) => {
|
||||||
|
const m = value.match(/(\d\d\d\d)-(\d\d?)-(\d\d?)/);
|
||||||
|
|
||||||
|
return {
|
||||||
|
conditionType: 'and',
|
||||||
|
conditions: [
|
||||||
|
getTransformCondition('YEAR', m[1]),
|
||||||
|
getTransformCondition('MONTH', m[2]),
|
||||||
|
getTransformCondition('DAY', m[3]),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const createParser = (filterType: FilterType) => {
|
const createParser = (filterType: FilterType) => {
|
||||||
const langDef = {
|
const langDef = {
|
||||||
string1: () =>
|
string1: () =>
|
||||||
@@ -123,6 +168,10 @@ const createParser = (filterType: FilterType) => {
|
|||||||
|
|
||||||
noQuotedString: () => P.regexp(/[^\s^,^'^"]+/).desc('string unquoted'),
|
noQuotedString: () => P.regexp(/[^\s^,^'^"]+/).desc('string unquoted'),
|
||||||
|
|
||||||
|
yearNum: () => P.regexp(/\d\d\d\d/).map(yearCondition()),
|
||||||
|
yearMonthNum: () => P.regexp(/\d\d\d\d-\d\d?/).map(yearMonthCondition()),
|
||||||
|
yearMonthDayNum: () => P.regexp(/\d\d\d\d-\d\d?-\d\d?/).map(yearMonthDayCondition()),
|
||||||
|
|
||||||
value: (r) => P.alt(...allowedValues.map((x) => r[x])),
|
value: (r) => P.alt(...allowedValues.map((x) => r[x])),
|
||||||
valueTestEq: (r) => r.value.map(binaryCondition('=')),
|
valueTestEq: (r) => r.value.map(binaryCondition('=')),
|
||||||
valueTestStr: (r) => r.value.map(likeCondition('like', '%#VALUE#%')),
|
valueTestStr: (r) => r.value.map(likeCondition('like', '%#VALUE#%')),
|
||||||
@@ -173,6 +222,7 @@ const createParser = (filterType: FilterType) => {
|
|||||||
'containsNot'
|
'containsNot'
|
||||||
);
|
);
|
||||||
if (filterType == 'logical') allowedElements.push('true', 'false', 'trueNum', 'falseNum');
|
if (filterType == 'logical') allowedElements.push('true', 'false', 'trueNum', 'falseNum');
|
||||||
|
if (filterType == 'datetime') allowedElements.push('yearMonthDayNum', 'yearMonthNum', 'yearNum');
|
||||||
|
|
||||||
// must be last
|
// must be last
|
||||||
if (filterType == 'string') allowedElements.push('valueTestStr');
|
if (filterType == 'string') allowedElements.push('valueTestStr');
|
||||||
|
|||||||
@@ -33,5 +33,9 @@ export function dumpSqlExpression(dmp: SqlDumper, expr: Expression) {
|
|||||||
dmp.putCollection(',', expr.args, (x) => dumpSqlExpression(dmp, x));
|
dmp.putCollection(',', expr.args, (x) => dumpSqlExpression(dmp, x));
|
||||||
dmp.put(')');
|
dmp.put(')');
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'transform':
|
||||||
|
dmp.transform(expr.transform, () => dumpSqlExpression(dmp, expr.expr));
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { NamedObjectInfo, RangeDefinition } from '@dbgate/types';
|
import { NamedObjectInfo, RangeDefinition, TransformType } from '@dbgate/types';
|
||||||
|
|
||||||
// export interface Command {
|
// export interface Command {
|
||||||
// }
|
// }
|
||||||
@@ -130,7 +130,19 @@ export interface CallExpression {
|
|||||||
argsPrefix?: string; // DISTINCT in case of COUNT DISTINCT
|
argsPrefix?: string; // DISTINCT in case of COUNT DISTINCT
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Expression = ColumnRefExpression | ValueExpression | PlaceholderExpression | RawExpression | CallExpression;
|
export interface TranformExpression {
|
||||||
|
exprType: 'transform';
|
||||||
|
expr: Expression;
|
||||||
|
transform: TransformType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Expression =
|
||||||
|
| ColumnRefExpression
|
||||||
|
| ValueExpression
|
||||||
|
| PlaceholderExpression
|
||||||
|
| RawExpression
|
||||||
|
| CallExpression
|
||||||
|
| TranformExpression;
|
||||||
export type OrderByExpression = Expression & { direction: 'ASC' | 'DESC' };
|
export type OrderByExpression = Expression & { direction: 'ASC' | 'DESC' };
|
||||||
|
|
||||||
export type ResultField = Expression & { alias?: string };
|
export type ResultField = Expression & { alias?: string };
|
||||||
|
|||||||
3
packages/types/dumper.d.ts
vendored
3
packages/types/dumper.d.ts
vendored
@@ -1,6 +1,8 @@
|
|||||||
import { TableInfo } from './dbinfo';
|
import { TableInfo } from './dbinfo';
|
||||||
import { SqlDialect } from './dialect';
|
import { SqlDialect } from './dialect';
|
||||||
|
|
||||||
|
export type TransformType = 'GROUP:YEAR' | 'GROUP:MONTH' | 'GROUP:DAY' | 'YEAR' | 'MONTH' | 'DAY'; // | 'GROUP:HOUR' | 'GROUP:MINUTE';
|
||||||
|
|
||||||
export interface SqlDumper {
|
export interface SqlDumper {
|
||||||
s: string;
|
s: string;
|
||||||
dialect: SqlDialect;
|
dialect: SqlDialect;
|
||||||
@@ -10,6 +12,7 @@ export interface SqlDumper {
|
|||||||
putCmd(format: string, ...args);
|
putCmd(format: string, ...args);
|
||||||
putValue(value: string | number | Date);
|
putValue(value: string | number | Date);
|
||||||
putCollection<T>(delimiter: string, collection: T[], lambda: (item: T) => void);
|
putCollection<T>(delimiter: string, collection: T[], lambda: (item: T) => void);
|
||||||
|
transform(type: TransformType, dumpExpr: () => void);
|
||||||
|
|
||||||
endCommand();
|
endCommand();
|
||||||
createTable(table: TableInfo);
|
createTable(table: TableInfo);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import DropDownButton from '../widgets/DropDownButton';
|
|||||||
import { DropDownMenuItem, DropDownMenuDivider } from '../modals/DropDownMenu';
|
import { DropDownMenuItem, DropDownMenuDivider } from '../modals/DropDownMenu';
|
||||||
import { useSplitterDrag } from '../widgets/Splitter';
|
import { useSplitterDrag } from '../widgets/Splitter';
|
||||||
import { FontIcon } from '../icons';
|
import { FontIcon } from '../icons';
|
||||||
|
import { isTypeDateTime } from '@dbgate/tools';
|
||||||
|
|
||||||
const HeaderDiv = styled.div`
|
const HeaderDiv = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -69,6 +70,16 @@ export default function ColumnHeaderControl({ column, setSort, onResize, order,
|
|||||||
<DropDownMenuItem onClick={() => setGrouping('AVG')}>AVG</DropDownMenuItem>
|
<DropDownMenuItem onClick={() => setGrouping('AVG')}>AVG</DropDownMenuItem>
|
||||||
<DropDownMenuItem onClick={() => setGrouping('COUNT')}>COUNT</DropDownMenuItem>
|
<DropDownMenuItem onClick={() => setGrouping('COUNT')}>COUNT</DropDownMenuItem>
|
||||||
<DropDownMenuItem onClick={() => setGrouping('COUNT DISTINCT')}>COUNT DISTINCT</DropDownMenuItem>
|
<DropDownMenuItem onClick={() => setGrouping('COUNT DISTINCT')}>COUNT DISTINCT</DropDownMenuItem>
|
||||||
|
{isTypeDateTime(column.dataType) && (
|
||||||
|
<>
|
||||||
|
<DropDownMenuDivider />
|
||||||
|
<DropDownMenuItem onClick={() => setGrouping('GROUP:YEAR')}>Group by YEAR</DropDownMenuItem>
|
||||||
|
<DropDownMenuItem onClick={() => setGrouping('GROUP:MONTH')}>Group by MONTH</DropDownMenuItem>
|
||||||
|
<DropDownMenuItem onClick={() => setGrouping('GROUP:DAY')}>Group by DAY</DropDownMenuItem>
|
||||||
|
{/* <DropDownMenuItem onClick={() => setGrouping('GROUP:HOUR')}>Group by HOUR</DropDownMenuItem>
|
||||||
|
<DropDownMenuItem onClick={() => setGrouping('GROUP:MINUTE')}>Group by MINUTE</DropDownMenuItem> */}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</DropDownButton>
|
</DropDownButton>
|
||||||
)}
|
)}
|
||||||
<ResizeHandle className="resizeHandleControl" onMouseDown={onResizeDown} />
|
<ResizeHandle className="resizeHandleControl" onMouseDown={onResizeDown} />
|
||||||
|
|||||||
@@ -484,14 +484,13 @@ export default function DataGridCore(props) {
|
|||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (display.groupColumns) {
|
if (display.groupColumns) {
|
||||||
console.log('SET REFERENCE');
|
|
||||||
|
|
||||||
props.onReferenceClick({
|
props.onReferenceClick({
|
||||||
schemaName: display.baseTable.schemaName,
|
schemaName: display.baseTable.schemaName,
|
||||||
pureName: display.baseTable.pureName,
|
pureName: display.baseTable.pureName,
|
||||||
columns: display.groupColumns.map((col) => ({
|
columns: display.groupColumns.map((col) => ({
|
||||||
baseName: col,
|
baseName: col,
|
||||||
refName: col,
|
refName: col,
|
||||||
|
dataType: _.get(display.baseTable && display.baseTable.columns.find((x) => x.columnName == col), 'dataType'),
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export default function TableDataGrid({
|
|||||||
..._.fromPairs(
|
..._.fromPairs(
|
||||||
reference.columns.map((col) => [
|
reference.columns.map((col) => [
|
||||||
col.refName,
|
col.refName,
|
||||||
selectedRows.map((x) => getFilterValueExpression(x[col.baseName])).join(', '),
|
selectedRows.map((x) => getFilterValueExpression(x[col.baseName], col.dataType)).join(', '),
|
||||||
])
|
])
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user