diff --git a/packages/datalib/src/GridDisplay.ts b/packages/datalib/src/GridDisplay.ts index d99c1b08b..5cc044690 100644 --- a/packages/datalib/src/GridDisplay.ts +++ b/packages/datalib/src/GridDisplay.ts @@ -189,25 +189,50 @@ export abstract class GridDisplay { } 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) { const groupColumns = this.groupColumns; if (groupColumns && groupColumns.length > 0) { - select.groupBy = groupColumns.map((col) => ({ - exprType: 'column', - columnName: displayedColumnInfo[col].columnName, - source: { alias: displayedColumnInfo[col].sourceAlias }, - })); + // @ts-ignore + select.groupBy = groupColumns.map((col) => { + const colExpr: Expression = { + 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)) { for (let i = 0; i < select.columns.length; i++) { const uniqueName = select.columns[i].alias; - if (groupColumns && groupColumns.includes(uniqueName)) continue; + // if (groupColumns && groupColumns.includes(uniqueName)) continue; const grouping = this.getGrouping(uniqueName); - if (grouping == 'NULL') { + if (grouping == 'GROUP') { + continue; + } else if (grouping == '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 { let func = 'MAX'; let argsPrefix = ''; diff --git a/packages/engines/default/SqlDumper.js b/packages/engines/default/SqlDumper.js index fbf2dd8b1..0452e07f6 100644 --- a/packages/engines/default/SqlDumper.js +++ b/packages/engines/default/SqlDumper.js @@ -76,6 +76,9 @@ class SqlDumper { case 'v': this.putValue(value); break; + case 'c': + value(this); + break; } } putFormattedList(c, collection) { @@ -255,6 +258,11 @@ class SqlDumper { 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 allow {boolean} diff --git a/packages/engines/mssql/MsSqlDumper.js b/packages/engines/mssql/MsSqlDumper.js index 1d465b317..673189012 100644 --- a/packages/engines/mssql/MsSqlDumper.js +++ b/packages/engines/mssql/MsSqlDumper.js @@ -13,7 +13,41 @@ class MsSqlDumper extends SqlDumper { } 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; + } } } diff --git a/packages/filterparser/src/filterTool.ts b/packages/filterparser/src/filterTool.ts index 0947ddaf9..d3ac937d7 100644 --- a/packages/filterparser/src/filterTool.ts +++ b/packages/filterparser/src/filterTool.ts @@ -1,7 +1,10 @@ +import { isTypeDateTime } from '@dbgate/tools'; + export type FilterMultipleValuesMode = 'is' | 'is_not' | 'contains' | 'begins' | 'ends'; -export function getFilterValueExpression(value) { +export function getFilterValueExpression(value, dataType) { if (value == null) return 'NULL'; + if (isTypeDateTime(dataType)) return value; return `="${value}"`; } diff --git a/packages/filterparser/src/parseFilter.ts b/packages/filterparser/src/parseFilter.ts index 655ec04dc..e44ab0c2b 100644 --- a/packages/filterparser/src/parseFilter.ts +++ b/packages/filterparser/src/parseFilter.ts @@ -1,6 +1,7 @@ import P from 'parsimmon'; import { FilterType } from './types'; import { Condition } from '@dbgate/sqltree'; +import { TransformType } from '@dbgate/types'; 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 langDef = { string1: () => @@ -123,6 +168,10 @@ const createParser = (filterType: FilterType) => { 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])), valueTestEq: (r) => r.value.map(binaryCondition('=')), valueTestStr: (r) => r.value.map(likeCondition('like', '%#VALUE#%')), @@ -173,6 +222,7 @@ const createParser = (filterType: FilterType) => { 'containsNot' ); if (filterType == 'logical') allowedElements.push('true', 'false', 'trueNum', 'falseNum'); + if (filterType == 'datetime') allowedElements.push('yearMonthDayNum', 'yearMonthNum', 'yearNum'); // must be last if (filterType == 'string') allowedElements.push('valueTestStr'); diff --git a/packages/sqltree/src/dumpSqlExpression.ts b/packages/sqltree/src/dumpSqlExpression.ts index aa385b6e1..0a90d5577 100644 --- a/packages/sqltree/src/dumpSqlExpression.ts +++ b/packages/sqltree/src/dumpSqlExpression.ts @@ -33,5 +33,9 @@ export function dumpSqlExpression(dmp: SqlDumper, expr: Expression) { dmp.putCollection(',', expr.args, (x) => dumpSqlExpression(dmp, x)); dmp.put(')'); break; + + case 'transform': + dmp.transform(expr.transform, () => dumpSqlExpression(dmp, expr.expr)); + break; } } diff --git a/packages/sqltree/src/types.ts b/packages/sqltree/src/types.ts index 683a57498..a1c211b7d 100644 --- a/packages/sqltree/src/types.ts +++ b/packages/sqltree/src/types.ts @@ -1,4 +1,4 @@ -import { NamedObjectInfo, RangeDefinition } from '@dbgate/types'; +import { NamedObjectInfo, RangeDefinition, TransformType } from '@dbgate/types'; // export interface Command { // } @@ -130,7 +130,19 @@ export interface CallExpression { 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 ResultField = Expression & { alias?: string }; diff --git a/packages/types/dumper.d.ts b/packages/types/dumper.d.ts index 3515b720d..4e031efcf 100644 --- a/packages/types/dumper.d.ts +++ b/packages/types/dumper.d.ts @@ -1,6 +1,8 @@ import { TableInfo } from './dbinfo'; import { SqlDialect } from './dialect'; +export type TransformType = 'GROUP:YEAR' | 'GROUP:MONTH' | 'GROUP:DAY' | 'YEAR' | 'MONTH' | 'DAY'; // | 'GROUP:HOUR' | 'GROUP:MINUTE'; + export interface SqlDumper { s: string; dialect: SqlDialect; @@ -10,6 +12,7 @@ export interface SqlDumper { putCmd(format: string, ...args); putValue(value: string | number | Date); putCollection(delimiter: string, collection: T[], lambda: (item: T) => void); + transform(type: TransformType, dumpExpr: () => void); endCommand(); createTable(table: TableInfo); diff --git a/packages/web/src/datagrid/ColumnHeaderControl.js b/packages/web/src/datagrid/ColumnHeaderControl.js index 7a603b5c1..a933fb117 100644 --- a/packages/web/src/datagrid/ColumnHeaderControl.js +++ b/packages/web/src/datagrid/ColumnHeaderControl.js @@ -5,6 +5,7 @@ import DropDownButton from '../widgets/DropDownButton'; import { DropDownMenuItem, DropDownMenuDivider } from '../modals/DropDownMenu'; import { useSplitterDrag } from '../widgets/Splitter'; import { FontIcon } from '../icons'; +import { isTypeDateTime } from '@dbgate/tools'; const HeaderDiv = styled.div` display: flex; @@ -69,6 +70,16 @@ export default function ColumnHeaderControl({ column, setSort, onResize, order, setGrouping('AVG')}>AVG setGrouping('COUNT')}>COUNT setGrouping('COUNT DISTINCT')}>COUNT DISTINCT + {isTypeDateTime(column.dataType) && ( + <> + + setGrouping('GROUP:YEAR')}>Group by YEAR + setGrouping('GROUP:MONTH')}>Group by MONTH + setGrouping('GROUP:DAY')}>Group by DAY + {/* setGrouping('GROUP:HOUR')}>Group by HOUR + setGrouping('GROUP:MINUTE')}>Group by MINUTE */} + + )} )} diff --git a/packages/web/src/datagrid/DataGridCore.js b/packages/web/src/datagrid/DataGridCore.js index 0b6b29638..740cc884d 100644 --- a/packages/web/src/datagrid/DataGridCore.js +++ b/packages/web/src/datagrid/DataGridCore.js @@ -484,14 +484,13 @@ export default function DataGridCore(props) { React.useEffect(() => { if (display.groupColumns) { - console.log('SET REFERENCE'); - props.onReferenceClick({ schemaName: display.baseTable.schemaName, pureName: display.baseTable.pureName, columns: display.groupColumns.map((col) => ({ baseName: col, refName: col, + dataType: _.get(display.baseTable && display.baseTable.columns.find((x) => x.columnName == col), 'dataType'), })), }); } diff --git a/packages/web/src/datagrid/TableDataGrid.js b/packages/web/src/datagrid/TableDataGrid.js index dd70a1860..6cdcf20b9 100644 --- a/packages/web/src/datagrid/TableDataGrid.js +++ b/packages/web/src/datagrid/TableDataGrid.js @@ -106,7 +106,7 @@ export default function TableDataGrid({ ..._.fromPairs( reference.columns.map((col) => [ col.refName, - selectedRows.map((x) => getFilterValueExpression(x[col.baseName])).join(', '), + selectedRows.map((x) => getFilterValueExpression(x[col.baseName], col.dataType)).join(', '), ]) ), };