Browse Source

Merge branch 'develop' into fix/formula-empty-result

pull/4644/head
Wing-Kam Wong 2 years ago
parent
commit
a8efff9fa0
  1. 3
      packages/nc-gui/components.d.ts
  2. 13
      packages/nc-gui/components/cell/DateTimePicker.vue
  3. 38
      packages/nc-gui/components/smartsheet/column/DateTimeOptions.vue
  4. 1
      packages/nc-gui/components/smartsheet/column/EditOrAdd.vue
  5. 57
      packages/nc-gui/components/smartsheet/column/FormulaOptions.vue
  6. 8
      packages/nc-gui/utils/dateTimeUtils.ts
  7. 23
      packages/nc-gui/utils/formulaUtils.ts
  8. 2
      packages/noco-docs/content/en/setup-and-usages/formulas.md
  9. 1
      packages/nocodb-sdk/src/lib/formulaHelpers.ts
  10. 12
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/mssql.ts
  11. 21
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/mysql.ts
  12. 51
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/pg.ts
  13. 63
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/sqlite.ts
  14. 137
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/helpers/convertUnits.ts
  15. 75
      packages/nocodb/src/lib/utils/dateTimeUtils.ts
  16. 26
      tests/playwright/pages/Dashboard/Grid/Column/index.ts
  17. 67
      tests/playwright/pages/Dashboard/common/Cell/DateTimeCell.ts
  18. 19
      tests/playwright/pages/Dashboard/common/Cell/index.ts
  19. 113
      tests/playwright/tests/columnDateTime.spec.ts
  20. 40
      tests/playwright/tests/columnFormula.spec.ts

3
packages/nc-gui/components.d.ts vendored

@ -170,8 +170,6 @@ declare module '@vue/runtime-core' {
MdiExport: typeof import('~icons/mdi/export')['default'] MdiExport: typeof import('~icons/mdi/export')['default']
MdiEyeCircleOutline: typeof import('~icons/mdi/eye-circle-outline')['default'] MdiEyeCircleOutline: typeof import('~icons/mdi/eye-circle-outline')['default']
MdiEyeOffOutline: typeof import('~icons/mdi/eye-off-outline')['default'] MdiEyeOffOutline: typeof import('~icons/mdi/eye-off-outline')['default']
MdiEyeSettings: typeof import('~icons/mdi/eye-settings')['default']
MdiEyeSettingsOutline: typeof import('~icons/mdi/eye-settings-outline')['default']
MdiFileDocumentOutline: typeof import('~icons/mdi/file-document-outline')['default'] MdiFileDocumentOutline: typeof import('~icons/mdi/file-document-outline')['default']
MdiFileExcel: typeof import('~icons/mdi/file-excel')['default'] MdiFileExcel: typeof import('~icons/mdi/file-excel')['default']
MdiFileEyeOutline: typeof import('~icons/mdi/file-eye-outline')['default'] MdiFileEyeOutline: typeof import('~icons/mdi/file-eye-outline')['default']
@ -203,7 +201,6 @@ declare module '@vue/runtime-core' {
MdiMagnify: typeof import('~icons/mdi/magnify')['default'] MdiMagnify: typeof import('~icons/mdi/magnify')['default']
MdiMenu: typeof import('~icons/mdi/menu')['default'] MdiMenu: typeof import('~icons/mdi/menu')['default']
MdiMenuDown: typeof import('~icons/mdi/menu-down')['default'] MdiMenuDown: typeof import('~icons/mdi/menu-down')['default']
MdiMenuIcon: typeof import('~icons/mdi/menu-icon')['default']
MdiMicrosoftTeams: typeof import('~icons/mdi/microsoft-teams')['default'] MdiMicrosoftTeams: typeof import('~icons/mdi/microsoft-teams')['default']
MdiMinusCircleOutline: typeof import('~icons/mdi/minus-circle-outline')['default'] MdiMinusCircleOutline: typeof import('~icons/mdi/minus-circle-outline')['default']
MdiMoonFull: typeof import('~icons/mdi/moon-full')['default'] MdiMoonFull: typeof import('~icons/mdi/moon-full')['default']

13
packages/nc-gui/components/cell/DateTimePicker.vue

@ -2,10 +2,13 @@
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { import {
ActiveCellInj, ActiveCellInj,
ColumnInj,
ReadonlyInj, ReadonlyInj,
dateFormats,
inject, inject,
isDrawerOrModalExist, isDrawerOrModalExist,
ref, ref,
timeFormats,
useProject, useProject,
useSelectedCellKeyupListener, useSelectedCellKeyupListener,
watch, watch,
@ -32,7 +35,11 @@ const column = inject(ColumnInj)!
let isDateInvalid = $ref(false) let isDateInvalid = $ref(false)
const dateFormat = isMysql(column.value.base_id) ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm:ssZ' const dateTimeFormat = $computed(() => {
const dateFormat = column?.value?.meta?.date_format ?? dateFormats[0]
const timeFormat = column?.value?.meta?.time_format ?? timeFormats[0]
return `${dateFormat} ${timeFormat}`
})
let localState = $computed({ let localState = $computed({
get() { get() {
@ -54,7 +61,7 @@ let localState = $computed({
} }
if (val.isValid()) { if (val.isValid()) {
emit('update:modelValue', val?.format(dateFormat)) emit('update:modelValue', val?.format(isMysql(column.value.base_id) ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm:ssZ'))
} }
}, },
}) })
@ -165,7 +172,7 @@ useSelectedCellKeyupListener(active, (e: KeyboardEvent) => {
:show-time="true" :show-time="true"
:bordered="false" :bordered="false"
class="!w-full !px-0 !border-none" class="!w-full !px-0 !border-none"
format="YYYY-MM-DD HH:mm" :format="dateTimeFormat"
:placeholder="isDateInvalid ? 'Invalid date' : ''" :placeholder="isDateInvalid ? 'Invalid date' : ''"
:allow-clear="!readOnly && !localState && !isPk" :allow-clear="!readOnly && !localState && !isPk"
:input-read-only="true" :input-read-only="true"

38
packages/nc-gui/components/smartsheet/column/DateTimeOptions.vue

@ -0,0 +1,38 @@
<script setup lang="ts">
import { dateFormats, timeFormats, useVModel } from '#imports'
const props = defineProps<{
value: any
}>()
const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit)
if (!vModel.value.meta?.date_format) {
if (!vModel.value.meta) vModel.value.meta = {}
vModel.value.meta.date_format = dateFormats[0]
}
if (!vModel.value.meta?.time_format) {
if (!vModel.value.meta) vModel.value.meta = {}
vModel.value.meta.time_format = timeFormats[0]
}
</script>
<template>
<a-form-item label="Date Format">
<a-select v-model:value="vModel.meta.date_format" class="nc-date-select" dropdown-class-name="nc-dropdown-date-format">
<a-select-option v-for="(format, i) of dateFormats" :key="i" :value="format">
{{ format }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Time Format">
<a-select v-model:value="vModel.meta.time_format" class="nc-time-select" dropdown-class-name="nc-dropdown-time-format">
<a-select-option v-for="(format, i) of timeFormats" :key="i" :value="format">
{{ format }}
</a-select-option>
</a-select>
</a-form-item>
</template>

1
packages/nc-gui/components/smartsheet/column/EditOrAdd.vue

@ -177,6 +177,7 @@ useEventListener('keydown', (e: KeyboardEvent) => {
<LazySmartsheetColumnCheckboxOptions v-if="formState.uidt === UITypes.Checkbox" v-model:value="formState" /> <LazySmartsheetColumnCheckboxOptions v-if="formState.uidt === UITypes.Checkbox" v-model:value="formState" />
<LazySmartsheetColumnLookupOptions v-if="!isEdit && formState.uidt === UITypes.Lookup" v-model:value="formState" /> <LazySmartsheetColumnLookupOptions v-if="!isEdit && formState.uidt === UITypes.Lookup" v-model:value="formState" />
<LazySmartsheetColumnDateOptions v-if="formState.uidt === UITypes.Date" v-model:value="formState" /> <LazySmartsheetColumnDateOptions v-if="formState.uidt === UITypes.Date" v-model:value="formState" />
<LazySmartsheetColumnDateTimeOptions v-if="formState.uidt === UITypes.DateTime" v-model:value="formState" />
<LazySmartsheetColumnRollupOptions v-if="!isEdit && formState.uidt === UITypes.Rollup" v-model:value="formState" /> <LazySmartsheetColumnRollupOptions v-if="!isEdit && formState.uidt === UITypes.Rollup" v-model:value="formState" />
<LazySmartsheetColumnLinkedToAnotherRecordOptions <LazySmartsheetColumnLinkedToAnotherRecordOptions
v-if="!isEdit && formState.uidt === UITypes.LinkToAnotherRecord" v-if="!isEdit && formState.uidt === UITypes.LinkToAnotherRecord"

57
packages/nc-gui/components/smartsheet/column/FormulaOptions.vue

@ -235,6 +235,63 @@ function validateAgainstMeta(parsedTree: any, errors = new Set(), typeErrors = n
}, },
typeErrors, typeErrors,
) )
} else if (parsedTree.callee.name === 'DATETIME_DIFF') {
// parsedTree.arguments[0] = date
validateAgainstType(
parsedTree.arguments[0],
formulaTypes.DATE,
(v: any) => {
if (!validateDateWithUnknownFormat(v)) {
typeErrors.add('The first parameter of DATETIME_DIFF() should have date value')
}
},
typeErrors,
)
// parsedTree.arguments[1] = date
validateAgainstType(
parsedTree.arguments[1],
formulaTypes.DATE,
(v: any) => {
if (!validateDateWithUnknownFormat(v)) {
typeErrors.add('The second parameter of DATETIME_DIFF() should have date value')
}
},
typeErrors,
)
// parsedTree.arguments[2] = ["milliseconds" | "ms" | "seconds" | "s" | "minutes" | "m" | "hours" | "h" | "days" | "d" | "weeks" | "w" | "months" | "M" | "quarters" | "Q" | "years" | "y"]
validateAgainstType(
parsedTree.arguments[2],
formulaTypes.STRING,
(v: any) => {
if (
![
'milliseconds',
'ms',
'seconds',
's',
'minutes',
'm',
'hours',
'h',
'days',
'd',
'weeks',
'w',
'months',
'M',
'quarters',
'Q',
'years',
'y',
].includes(v)
) {
typeErrors.add(
'The third parameter of DATETIME_DIFF() should have value either "milliseconds", "ms", "seconds", "s", "minutes", "m", "hours", "h", "days", "d", "weeks", "w", "months", "M", "quarters", "Q", "years", or "y"',
)
}
},
typeErrors,
)
} }
} }
} }

8
packages/nc-gui/utils/dateTimeUtils.ts

@ -5,17 +5,19 @@ export const timeAgo = (date: any) => {
} }
export const dateFormats = [ export const dateFormats = [
'YYYY-MM-DD',
'YYYY/MM/DD',
'DD-MM-YYYY', 'DD-MM-YYYY',
'MM-DD-YYYY', 'MM-DD-YYYY',
'YYYY-MM-DD',
'DD/MM/YYYY', 'DD/MM/YYYY',
'MM/DD/YYYY', 'MM/DD/YYYY',
'YYYY/MM/DD',
'DD MM YYYY', 'DD MM YYYY',
'MM DD YYYY', 'MM DD YYYY',
'YYYY MM DD', 'YYYY MM DD',
] ]
export const timeFormats = ['HH:mm', 'HH:mm:ss']
export const handleTZ = (val: any) => { export const handleTZ = (val: any) => {
if (val === undefined || val === null) { if (val === undefined || val === null) {
return return
@ -60,7 +62,7 @@ export function getDateFormat(v: string) {
export function getDateTimeFormat(v: string) { export function getDateTimeFormat(v: string) {
for (const format of dateFormats) { for (const format of dateFormats) {
for (const timeFormat of ['HH:mm', 'HH:mm:ss', 'HH:mm:ss.SSS']) { for (const timeFormat of timeFormats) {
const dateTimeFormat = `${format} ${timeFormat}` const dateTimeFormat = `${format} ${timeFormat}`
if (dayjs(v, dateTimeFormat, true).isValid() as any) { if (dayjs(v, dateTimeFormat, true).isValid() as any) {
return dateTimeFormat return dateTimeFormat

23
packages/nc-gui/utils/formulaUtils.ts

@ -51,6 +51,29 @@ const formulas: Record<string, any> = {
'DATEADD({column1}, -2, "year")', 'DATEADD({column1}, -2, "year")',
], ],
}, },
DATETIME_DIFF: {
type: formulaTypes.DATE,
validation: {
args: {
min: 2,
max: 3,
},
},
description: 'Calculate the difference of two given date / datetime in specified units.',
syntax:
'DATETIME_DIFF(date | datetime, date | datetime, ["milliseconds" | "ms" | "seconds" | "s" | "minutes" | "m" | "hours" | "h" | "days" | "d" | "weeks" | "w" | "months" | "M" | "quarters" | "Q" | "years" | "y"])',
examples: [
'DATEDIFF({column1}, {column2})',
'DATEDIFF({column1}, {column2}, "seconds")',
'DATEDIFF({column1}, {column2}, "s")',
'DATEDIFF({column1}, {column2}, "years")',
'DATEDIFF({column1}, {column2}, "y")',
'DATEDIFF({column1}, {column2}, "minutes")',
'DATEDIFF({column1}, {column2}, "m")',
'DATEDIFF({column1}, {column2}, "days")',
'DATEDIFF({column1}, {column2}, "d")',
],
},
AND: { AND: {
type: formulaTypes.COND_EXP, type: formulaTypes.COND_EXP,
validation: { validation: {

2
packages/noco-docs/content/en/setup-and-usages/formulas.md

@ -97,6 +97,8 @@ Example: ({Column1} + ({Column2} * {Column3}) / (3 - $Column4$ ))
| | | `DATEADD(date, 1, 'year')` | Supposing {DATE_COL} is 2022-03-14 03:14. The result is 2023-03-14 03:14. | DateTime columns and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -1, 'year')` | | | | `DATEADD(date, 1, 'year')` | Supposing {DATE_COL} is 2022-03-14 03:14. The result is 2023-03-14 03:14. | DateTime columns and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -1, 'year')` |
| | | `IF(NOW() < DATEADD(date,10,'day'), "true", "false")` | If the current date is less than {DATE_COL} plus 10 days, it returns true. Otherwise, it returns false. | DateTime columns and negative values are supported. | | | | `IF(NOW() < DATEADD(date,10,'day'), "true", "false")` | If the current date is less than {DATE_COL} plus 10 days, it returns true. Otherwise, it returns false. | DateTime columns and negative values are supported. |
| | | `IF(NOW() < DATEADD(date,10,'day'), "true", "false")` | If the current date is less than {DATE_COL} plus 10 days, it returns true. Otherwise, it returns false. | DateTime columns and negative values are supported. | | | | `IF(NOW() < DATEADD(date,10,'day'), "true", "false")` | If the current date is less than {DATE_COL} plus 10 days, it returns true. Otherwise, it returns false. | DateTime columns and negative values are supported. |
| **DATETIME_DIFF** | `DATETIME_DIFF(date, date, ["milliseconds" \| "ms" \| "seconds" \| "s" \| "minutes" \| "m" \| "hours" \| "h" \| "days" \| "d" \| "weeks" \| "w" \| "months" \| "M" \| "quarters" \| "Q" \| "years" \| "y"])` | `DATETIME_DIFF("2022/10/14", "2022/10/15", "second")` | Supposing {DATE_COL_1} is 2017-08-25 and {DATE_COL_2} is 2011-08-25. The result is 86400. | Compares two dates and returns the difference in the unit specified. Positive integers indicate the second date being in the past compared to the first and vice versa for negative ones. |
| | | `WEEKDAY(NOW(), "sunday")` | If today is Monday, it returns 1 | Get the week day of NOW() with the first day set as sunday |
| **WEEKDAY** | `WEEKDAY(date, [startDayOfWeek])` | `WEEKDAY(NOW())` | If today is Monday, it returns 0 | Returns the day of the week as an integer between 0 and 6 inclusive starting from Monday by default. You can optionally change the start day of the week by specifying in the second argument | | **WEEKDAY** | `WEEKDAY(date, [startDayOfWeek])` | `WEEKDAY(NOW())` | If today is Monday, it returns 0 | Returns the day of the week as an integer between 0 and 6 inclusive starting from Monday by default. You can optionally change the start day of the week by specifying in the second argument |
| | | `WEEKDAY(NOW(), "sunday")` | If today is Monday, it returns 1 | Get the week day of NOW() with the first day set as sunday | | | | `WEEKDAY(NOW(), "sunday")` | If today is Monday, it returns 1 | Get the week day of NOW() with the first day set as sunday |

1
packages/nocodb-sdk/src/lib/formulaHelpers.ts

@ -132,6 +132,7 @@ export function jsepTreeToFormula(node) {
'AVG', 'AVG',
'ADD', 'ADD',
'DATEADD', 'DATEADD',
'DATETIME_DIFF',
'WEEKDAY', 'WEEKDAY',
'AND', 'AND',
'OR', 'OR',

12
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/mssql.ts

@ -1,6 +1,7 @@
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { MapFnArgs } from '../mapFunctionName'; import { MapFnArgs } from '../mapFunctionName';
import commonFns from './commonFns'; import commonFns from './commonFns';
import { convertUnits } from '../helpers/convertUnits';
import { getWeekdayByText } from '../helpers/formulaFnHelper'; import { getWeekdayByText } from '../helpers/formulaFnHelper';
const mssql = { const mssql = {
@ -110,6 +111,17 @@ const mssql = {
END${colAlias}` END${colAlias}`
); );
}, },
DATETIME_DIFF: ({ fn, knex, pt, colAlias }: MapFnArgs) => {
const datetime_expr1 = fn(pt.arguments[0]);
const datetime_expr2 = fn(pt.arguments[1]);
const rawUnit = pt.arguments[2]
? fn(pt.arguments[2]).bindings[0]
: 'seconds';
const unit = convertUnits(rawUnit, 'mssql');
return knex.raw(
`DATEDIFF(${unit}, ${datetime_expr2}, ${datetime_expr1}) ${colAlias}`
);
},
WEEKDAY: ({ fn, knex, pt, colAlias }: MapFnArgs) => { WEEKDAY: ({ fn, knex, pt, colAlias }: MapFnArgs) => {
// DATEPART(WEEKDAY, DATE): sunday = 1, monday = 2, ..., saturday = 7 // DATEPART(WEEKDAY, DATE): sunday = 1, monday = 2, ..., saturday = 7
// WEEKDAY() returns an index from 0 to 6 for Monday to Sunday // WEEKDAY() returns an index from 0 to 6 for Monday to Sunday

21
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/mysql.ts

@ -1,6 +1,7 @@
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { MapFnArgs } from '../mapFunctionName'; import { MapFnArgs } from '../mapFunctionName';
import commonFns from './commonFns'; import commonFns from './commonFns';
import { convertUnits } from '../helpers/convertUnits';
import { getWeekdayByText } from '../helpers/formulaFnHelper'; import { getWeekdayByText } from '../helpers/formulaFnHelper';
const mysql2 = { const mysql2 = {
@ -61,6 +62,26 @@ const mysql2 = {
END${colAlias}` END${colAlias}`
); );
}, },
DATETIME_DIFF: ({ fn, knex, pt, colAlias }: MapFnArgs) => {
const datetime_expr1 = fn(pt.arguments[0]);
const datetime_expr2 = fn(pt.arguments[1]);
const unit = convertUnits(
pt.arguments[2] ? fn(pt.arguments[2]).bindings[0] : 'seconds',
'mysql'
);
if (unit === 'MICROSECOND') {
// MySQL doesn't support millisecond
// hence change from MICROSECOND to millisecond manually
return knex.raw(
`TIMESTAMPDIFF(${unit}, ${datetime_expr2}, ${datetime_expr1}) div 1000 ${colAlias}`
);
}
return knex.raw(
`TIMESTAMPDIFF(${unit}, ${datetime_expr2}, ${datetime_expr1}) ${colAlias}`
);
},
WEEKDAY: ({ fn, knex, pt, colAlias }: MapFnArgs) => { WEEKDAY: ({ fn, knex, pt, colAlias }: MapFnArgs) => {
// WEEKDAY() returns an index from 0 to 6 for Monday to Sunday // WEEKDAY() returns an index from 0 to 6 for Monday to Sunday
return knex.raw( return knex.raw(

51
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/pg.ts

@ -1,6 +1,7 @@
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { MapFnArgs } from '../mapFunctionName'; import { MapFnArgs } from '../mapFunctionName';
import commonFns from './commonFns'; import commonFns from './commonFns';
import { convertUnits } from '../helpers/convertUnits';
import { getWeekdayByText } from '../helpers/formulaFnHelper'; import { getWeekdayByText } from '../helpers/formulaFnHelper';
const pg = { const pg = {
@ -50,6 +51,56 @@ const pg = {
)}')::interval${colAlias}` )}')::interval${colAlias}`
); );
}, },
DATETIME_DIFF: ({ fn, knex, pt, colAlias }: MapFnArgs) => {
const datetime_expr1 = fn(pt.arguments[0]);
const datetime_expr2 = fn(pt.arguments[1]);
const rawUnit = pt.arguments[2]
? fn(pt.arguments[2]).bindings[0]
: 'seconds';
let sql;
const unit = convertUnits(rawUnit, 'pg');
switch (unit) {
case 'second':
sql = `EXTRACT(EPOCH from (${datetime_expr1}::TIMESTAMP - ${datetime_expr2}::TIMESTAMP))::INTEGER`;
break;
case 'minute':
sql = `EXTRACT(EPOCH from (${datetime_expr1}::TIMESTAMP - ${datetime_expr2}::TIMESTAMP))::INTEGER / 60`;
break;
case 'milliseconds':
sql = `EXTRACT(EPOCH from (${datetime_expr1}::TIMESTAMP - ${datetime_expr2}::TIMESTAMP))::INTEGER * 1000`;
break;
case 'hour':
sql = `EXTRACT(EPOCH from (${datetime_expr1}::TIMESTAMP - ${datetime_expr2}::TIMESTAMP))::INTEGER / 3600`;
break;
case 'week':
sql = `TRUNC(DATE_PART('day', ${datetime_expr1}::TIMESTAMP - ${datetime_expr2}::TIMESTAMP) / 7)`;
break;
case 'month':
sql = `(
DATE_PART('year', ${datetime_expr1}::TIMESTAMP) -
DATE_PART('year', ${datetime_expr2}::TIMESTAMP)
) * 12 + (
DATE_PART('month', ${datetime_expr1}::TIMESTAMP) -
DATE_PART('month', ${datetime_expr2}::TIMESTAMP)
)`;
break;
case 'quarter':
sql = `((EXTRACT(QUARTER FROM ${datetime_expr1}::TIMESTAMP) +
DATE_PART('year', AGE(${datetime_expr1}, '1900/01/01')) * 4) - 1) -
((EXTRACT(QUARTER FROM ${datetime_expr2}::TIMESTAMP) +
DATE_PART('year', AGE(${datetime_expr2}, '1900/01/01')) * 4) - 1)`;
break;
case 'year':
sql = `DATE_PART('year', ${datetime_expr1}::TIMESTAMP) - DATE_PART('year', ${datetime_expr2}::TIMESTAMP)`;
break;
case 'day':
sql = `DATE_PART('day', ${datetime_expr1}::TIMESTAMP - ${datetime_expr2}::TIMESTAMP)`;
break;
default:
sql = '';
}
return knex.raw(`${sql} ${colAlias}`);
},
WEEKDAY: ({ fn, knex, pt, colAlias }: MapFnArgs) => { WEEKDAY: ({ fn, knex, pt, colAlias }: MapFnArgs) => {
// isodow: the day of the week as Monday (1) to Sunday (7) // isodow: the day of the week as Monday (1) to Sunday (7)
// WEEKDAY() returns an index from 0 to 6 for Monday to Sunday // WEEKDAY() returns an index from 0 to 6 for Monday to Sunday

63
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/sqlite.ts

@ -1,7 +1,12 @@
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { MapFnArgs } from '../mapFunctionName'; import { MapFnArgs } from '../mapFunctionName';
import commonFns from './commonFns'; import commonFns from './commonFns';
import { convertUnits } from '../helpers/convertUnits';
import { getWeekdayByText } from '../helpers/formulaFnHelper'; import { getWeekdayByText } from '../helpers/formulaFnHelper';
import {
convertToTargetFormat,
getDateFormat,
} from '../../../../../utils/dateTimeUtils';
const sqlite3 = { const sqlite3 = {
...commonFns, ...commonFns,
@ -77,7 +82,63 @@ const sqlite3 = {
END${colAlias}` END${colAlias}`
); );
}, },
DATETIME_DIFF: ({ fn, knex, pt, colAlias }: MapFnArgs) => {
let datetime_expr1 = fn(pt.arguments[0]).bindings[0];
let datetime_expr2 = fn(pt.arguments[1]).bindings[0];
// JULIANDAY takes YYYY-MM-DD
datetime_expr1 = convertToTargetFormat(
datetime_expr1,
getDateFormat(datetime_expr1),
'YYYY-MM-DD'
);
datetime_expr2 = convertToTargetFormat(
datetime_expr2,
getDateFormat(datetime_expr2),
'YYYY-MM-DD'
);
const rawUnit = pt.arguments[2]
? fn(pt.arguments[2]).bindings[0]
: 'seconds';
let sql;
const unit = convertUnits(rawUnit, 'sqlite');
switch (unit) {
case 'seconds':
sql = `ROUND((JULIANDAY('${datetime_expr1}') - JULIANDAY('${datetime_expr2}')) * 86400)`;
break;
case 'minutes':
sql = `ROUND((JULIANDAY('${datetime_expr1}') - JULIANDAY('${datetime_expr2}')) * 1440)`;
break;
case 'hours':
sql = `ROUND((JULIANDAY('${datetime_expr1}') - JULIANDAY('${datetime_expr2}')) * 24)`;
break;
case 'milliseconds':
sql = `ROUND((JULIANDAY('${datetime_expr1}') - JULIANDAY('${datetime_expr2}')) * 86400000)`;
break;
case 'weeks':
sql = `ROUND((JULIANDAY('${datetime_expr1}') - JULIANDAY('${datetime_expr2}')) / 7)`;
break;
case 'months':
sql = `(ROUND((JULIANDAY('${datetime_expr1}') - JULIANDAY('${datetime_expr2}')) / 365))
* 12 + (ROUND((JULIANDAY('${datetime_expr1}') - JULIANDAY('${datetime_expr2}')) / 365 / 12))`;
break;
case 'quarters':
sql = `
ROUND((JULIANDAY('${datetime_expr1}')) / 365 / 4) -
ROUND((JULIANDAY('${datetime_expr2}')) / 365 / 4) +
(ROUND((JULIANDAY('${datetime_expr1}') - JULIANDAY('${datetime_expr2}')) / 365)) * 4
`;
break;
case 'years':
sql = `ROUND((JULIANDAY('${datetime_expr1}') - JULIANDAY('${datetime_expr2}')) / 365)`;
break;
case 'days':
sql = `JULIANDAY('${datetime_expr1}') - JULIANDAY('${datetime_expr2}')`;
break;
default:
sql = '';
}
return knex.raw(`${sql} ${colAlias}`);
},
WEEKDAY: ({ fn, knex, pt, colAlias }: MapFnArgs) => { WEEKDAY: ({ fn, knex, pt, colAlias }: MapFnArgs) => {
// strftime('%w', date) - day of week 0 - 6 with Sunday == 0 // strftime('%w', date) - day of week 0 - 6 with Sunday == 0
// WEEKDAY() returns an index from 0 to 6 for Monday to Sunday // WEEKDAY() returns an index from 0 to 6 for Monday to Sunday

137
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/helpers/convertUnits.ts

@ -0,0 +1,137 @@
export function convertUnits(
unit: string,
type: 'mysql' | 'mssql' | 'pg' | 'sqlite'
) {
switch (unit) {
case 'milliseconds':
case 'ms': {
switch (type) {
case 'mssql':
return 'millisecond';
case 'mysql':
// MySQL doesn't support millisecond
// hence change from MICROSECOND to millisecond manually
return 'MICROSECOND';
case 'pg':
case 'sqlite':
return 'milliseconds';
default:
return unit;
}
}
case 'seconds':
case 's': {
switch (type) {
case 'mssql':
case 'pg':
return 'second';
case 'mysql':
return 'SECOND';
case 'sqlite':
return 'seconds';
default:
return unit;
}
}
case 'minutes':
case 'm': {
switch (type) {
case 'mssql':
case 'pg':
return 'minute';
case 'mysql':
return 'MINUTE';
case 'sqlite':
return 'minutes';
default:
return unit;
}
}
case 'hours':
case 'h': {
switch (type) {
case 'mssql':
case 'pg':
return 'hour';
case 'mysql':
return 'HOUR';
case 'sqlite':
return 'hours';
default:
return unit;
}
}
case 'days':
case 'd': {
switch (type) {
case 'mssql':
case 'pg':
return 'day';
case 'mysql':
return 'DAY';
case 'sqlite':
return 'days';
default:
return unit;
}
}
case 'weeks':
case 'w': {
switch (type) {
case 'mssql':
case 'pg':
return 'week';
case 'mysql':
return 'WEEK';
case 'sqlite':
return 'weeks';
default:
return unit;
}
}
case 'months':
case 'M': {
switch (type) {
case 'mssql':
case 'pg':
return 'month';
case 'mysql':
return 'MONTH';
case 'sqlite':
return 'months';
default:
return unit;
}
}
case 'quarters':
case 'Q': {
switch (type) {
case 'mssql':
case 'pg':
return 'quarter';
case 'mysql':
return 'QUARTER';
case 'sqlite':
return 'quarters';
default:
return unit;
}
}
case 'years':
case 'y': {
switch (type) {
case 'mssql':
case 'pg':
return 'year';
case 'mysql':
return 'YEAR';
case 'sqlite':
return 'years';
default:
return unit;
}
}
default:
return unit;
}
}

75
packages/nocodb/src/lib/utils/dateTimeUtils.ts

@ -0,0 +1,75 @@
import dayjs from 'dayjs';
export const dateFormats = [
'DD-MM-YYYY',
'MM-DD-YYYY',
'YYYY-MM-DD',
'DD/MM/YYYY',
'MM/DD/YYYY',
'YYYY/MM/DD',
'DD MM YYYY',
'MM DD YYYY',
'YYYY MM DD',
];
export function validateDateFormat(v: string) {
return dateFormats.includes(v);
}
export function validateDateWithUnknownFormat(v: string) {
for (const format of dateFormats) {
if (dayjs(v, format, true).isValid() as any) {
return true;
}
for (const timeFormat of ['HH:mm', 'HH:mm:ss', 'HH:mm:ss.SSS']) {
if (dayjs(v, `${format} ${timeFormat}`, true).isValid() as any) {
return true;
}
}
}
return false;
}
export function getDateFormat(v: string) {
for (const format of dateFormats) {
if (dayjs(v, format, true).isValid()) {
return format;
}
}
return 'YYYY/MM/DD';
}
export function getDateTimeFormat(v: string) {
for (const format of dateFormats) {
for (const timeFormat of ['HH:mm', 'HH:mm:ss', 'HH:mm:ss.SSS']) {
const dateTimeFormat = `${format} ${timeFormat}`;
if (dayjs(v, dateTimeFormat, true).isValid() as any) {
return dateTimeFormat;
}
}
}
return 'YYYY/MM/DD';
}
export function parseStringDate(v: string, dateFormat: string) {
const dayjsObj = dayjs(v);
if (dayjsObj.isValid()) {
v = dayjsObj.format('YYYY-MM-DD');
} else {
v = dayjs(v, dateFormat).format('YYYY-MM-DD');
}
return v;
}
export function convertToTargetFormat(
v: string,
oldDataFormat,
newDateFormat: string
) {
if (
!dateFormats.includes(oldDataFormat) ||
!dateFormats.includes(newDateFormat)
)
return v;
return dayjs(v, oldDataFormat).format(newDateFormat);
}

26
tests/playwright/pages/Dashboard/Grid/Column/index.ts

@ -34,7 +34,9 @@ export class ColumnPageObject extends BasePage {
childColumn = '', childColumn = '',
relationType = '', relationType = '',
rollupType = '', rollupType = '',
format, format = '',
dateFormat = '',
timeFormat = '',
insertAfterColumnTitle, insertAfterColumnTitle,
insertBeforeColumnTitle, insertBeforeColumnTitle,
}: { }: {
@ -47,6 +49,8 @@ export class ColumnPageObject extends BasePage {
relationType?: string; relationType?: string;
rollupType?: string; rollupType?: string;
format?: string; format?: string;
dateFormat?: string;
timeFormat?: string;
insertBeforeColumnTitle?: string; insertBeforeColumnTitle?: string;
insertAfterColumnTitle?: string; insertAfterColumnTitle?: string;
}) { }) {
@ -90,6 +94,14 @@ export class ColumnPageObject extends BasePage {
.click(); .click();
} }
break; break;
case 'DateTime':
// Date Format
await this.get().locator('.nc-date-select').click();
await this.rootPage.locator('.ant-select-item').locator(`text="${dateFormat}"`).click();
// Time Format
await this.get().locator('.nc-time-select').click();
await this.rootPage.locator('.ant-select-item').locator(`text="${timeFormat}"`).click();
break;
case 'Formula': case 'Formula':
await this.get().locator('.nc-formula-input').fill(formula); await this.get().locator('.nc-formula-input').fill(formula);
break; break;
@ -222,11 +234,15 @@ export class ColumnPageObject extends BasePage {
type = 'SingleLineText', type = 'SingleLineText',
formula = '', formula = '',
format, format,
dateFormat = '',
timeFormat = '',
}: { }: {
title: string; title: string;
type?: string; type?: string;
formula?: string; formula?: string;
format?: string; format?: string;
dateFormat?: string;
timeFormat?: string;
}) { }) {
await this.getColumnHeader(title).locator('.nc-ui-dt-dropdown').click(); await this.getColumnHeader(title).locator('.nc-ui-dt-dropdown').click();
await this.rootPage.locator('li[role="menuitem"]:has-text("Edit")').click(); await this.rootPage.locator('li[role="menuitem"]:has-text("Edit")').click();
@ -245,6 +261,14 @@ export class ColumnPageObject extends BasePage {
}) })
.click(); .click();
break; break;
case 'DateTime':
// Date Format
await this.get().locator('.nc-date-select').click();
await this.rootPage.locator('.ant-select-item').locator(`text="${dateFormat}"`).click();
// Time Format
await this.get().locator('.nc-time-select').click();
await this.rootPage.locator('.ant-select-item').locator(`text="${timeFormat}"`).click();
break;
default: default:
break; break;
} }

67
tests/playwright/pages/Dashboard/common/Cell/DateTimeCell.ts

@ -0,0 +1,67 @@
import { CellPageObject } from '.';
import BasePage from '../../../Base';
export class DateTimeCellPageObject extends BasePage {
readonly cell: CellPageObject;
constructor(cell: CellPageObject) {
super(cell.rootPage);
this.cell = cell;
}
get({ index, columnHeader }: { index?: number; columnHeader: string }) {
return this.cell.get({ index, columnHeader });
}
async open({ index, columnHeader }: { index: number; columnHeader: string }) {
await this.rootPage.locator('.nc-grid-add-new-cell').click();
await this.cell.dblclick({
index,
columnHeader,
});
}
async save() {
await this.rootPage.locator('button:has-text("Ok")').click();
}
async selectDate({
// date formats in `YYYY-MM-DD`
date,
}: {
date: string;
}) {
// title date format needs to be YYYY-MM-DD
await this.rootPage.locator(`td[title="${date}"]`).click();
}
async selectTime({
// hour: 0 - 23
// minute: 0 - 59
// second: 0 - 59
hour,
minute,
second,
}: {
hour: number;
minute: number;
second?: number | null;
}) {
await this.rootPage
.locator(`.ant-picker-time-panel-column:nth-child(1) > .ant-picker-time-panel-cell:nth-child(${hour + 1})`)
.click();
await this.rootPage
.locator(`.ant-picker-time-panel-column:nth-child(2) > .ant-picker-time-panel-cell:nth-child(${minute + 1})`)
.click();
if (second != null) {
await this.rootPage
.locator(`.ant-picker-time-panel-column:nth-child(3) > .ant-picker-time-panel-cell:nth-child(${second + 1})`)
.click();
}
}
async close() {
await this.rootPage.keyboard.press('Escape');
}
}

19
tests/playwright/pages/Dashboard/common/Cell/index.ts

@ -7,6 +7,7 @@ import { SharedFormPage } from '../../../SharedForm';
import { CheckboxCellPageObject } from './CheckboxCell'; import { CheckboxCellPageObject } from './CheckboxCell';
import { RatingCellPageObject } from './RatingCell'; import { RatingCellPageObject } from './RatingCell';
import { DateCellPageObject } from './DateCell'; import { DateCellPageObject } from './DateCell';
import { DateTimeCellPageObject } from './DateTimeCell';
export interface CellProps { export interface CellProps {
index?: number; index?: number;
@ -20,6 +21,7 @@ export class CellPageObject extends BasePage {
readonly checkbox: CheckboxCellPageObject; readonly checkbox: CheckboxCellPageObject;
readonly rating: RatingCellPageObject; readonly rating: RatingCellPageObject;
readonly date: DateCellPageObject; readonly date: DateCellPageObject;
readonly dateTime: DateTimeCellPageObject;
constructor(parent: GridPage | SharedFormPage) { constructor(parent: GridPage | SharedFormPage) {
super(parent.rootPage); super(parent.rootPage);
@ -29,6 +31,7 @@ export class CellPageObject extends BasePage {
this.checkbox = new CheckboxCellPageObject(this); this.checkbox = new CheckboxCellPageObject(this);
this.rating = new RatingCellPageObject(this); this.rating = new RatingCellPageObject(this);
this.date = new DateCellPageObject(this); this.date = new DateCellPageObject(this);
this.dateTime = new DateTimeCellPageObject(this);
} }
get({ index, columnHeader }: CellProps): Locator { get({ index, columnHeader }: CellProps): Locator {
@ -113,6 +116,22 @@ export class CellPageObject extends BasePage {
} }
} }
async verifyDateCell({ index, columnHeader, value }: { index: number; columnHeader: string; value: string }) {
const _verify = async expectedValue => {
await expect
.poll(async () => {
const cell = await this.get({
index,
columnHeader,
}).locator('input');
return await cell.getAttribute('title');
})
.toEqual(expectedValue);
};
await _verify(value);
}
async verifyQrCodeCell({ async verifyQrCodeCell({
index, index,
columnHeader, columnHeader,

113
tests/playwright/tests/columnDateTime.spec.ts

@ -0,0 +1,113 @@
import { test } from '@playwright/test';
import { DashboardPage } from '../pages/Dashboard';
import setup from '../setup';
const dateTimeData = [
{
dateFormat: 'YYYY-MM-DD',
timeFormat: 'HH:mm',
date: '2022-12-12',
hour: 10,
minute: 20,
output: '2022-12-12 10:20',
},
{
dateFormat: 'YYYY-MM-DD',
timeFormat: 'HH:mm:ss',
date: '2022-12-11',
hour: 20,
minute: 30,
second: 40,
output: '2022-12-11 20:30:40',
},
{
dateFormat: 'YYYY/MM/DD',
timeFormat: 'HH:mm',
date: '2022-12-13',
hour: 10,
minute: 20,
output: '2022/12/13 10:20',
},
{
dateFormat: 'YYYY/MM/DD',
timeFormat: 'HH:mm:ss',
date: '2022-12-14',
hour: 5,
minute: 30,
second: 40,
output: '2022/12/14 05:30:40',
},
{
dateFormat: 'DD-MM-YYYY',
timeFormat: 'HH:mm',
date: '2022-12-10',
hour: 4,
minute: 30,
output: '10-12-2022 04:30',
},
{
dateFormat: 'DD-MM-YYYY',
timeFormat: 'HH:mm:ss',
date: '2022-12-26',
hour: 2,
minute: 30,
second: 40,
output: '26-12-2022 02:30:40',
},
];
test.describe('DateTime Column', () => {
let dashboard: DashboardPage;
let context: any;
test.beforeEach(async ({ page }) => {
context = await setup({ page });
dashboard = new DashboardPage(page, context.project);
});
test('Create DateTime Column', async () => {
await dashboard.treeView.createTable({ title: 'test_datetime' });
// Create DateTime column
await dashboard.grid.column.create({
title: 'NC_DATETIME_0',
type: 'DateTime',
dateFormat: dateTimeData[0].dateFormat,
timeFormat: dateTimeData[0].timeFormat,
});
for (let i = 0; i < dateTimeData.length; i++) {
// Edit DateTime column
await dashboard.grid.column.openEdit({
title: 'NC_DATETIME_0',
type: 'DateTime',
dateFormat: dateTimeData[i].dateFormat,
timeFormat: dateTimeData[i].timeFormat,
});
await dashboard.grid.column.save({ isUpdated: true });
await dashboard.grid.cell.dateTime.open({
index: 0,
columnHeader: 'NC_DATETIME_0',
});
await dashboard.grid.cell.dateTime.selectDate({
date: dateTimeData[i].date,
});
await dashboard.grid.cell.dateTime.selectTime({
hour: dateTimeData[i].hour,
minute: dateTimeData[i].minute,
second: dateTimeData[i].second,
});
await dashboard.grid.cell.dateTime.save();
await dashboard.grid.cell.verifyDateCell({
index: 0,
columnHeader: 'NC_DATETIME_0',
value: dateTimeData[i].output,
});
}
});
});

40
tests/playwright/tests/columnFormula.spec.ts

@ -30,6 +30,46 @@ const formulaDataByDbType = (context: NcContext) => [
formula: `WEEKDAY("2022-07-19", "sunday")`, formula: `WEEKDAY("2022-07-19", "sunday")`,
result: ['2', '2', '2', '2', '2'], result: ['2', '2', '2', '2', '2'],
}, },
{
formula: `DATETIME_DIFF("2022/10/14", "2022/10/15")`,
result: ['-86400', '-86400', '-86400', '-86400', '-86400'],
},
{
formula: `DATETIME_DIFF("2022/10/14", "2022/10/15", "minutes")`,
result: ['-1440', '-1440', '-1440', '-1440', '-1440'],
},
{
formula: `DATETIME_DIFF("2022/10/14", "2022/10/15", "seconds")`,
result: ['-86400', '-86400', '-86400', '-86400', '-86400'],
},
{
formula: `DATETIME_DIFF("2022/10/14", "2022/10/15", "milliseconds")`,
result: ['-86400000', '-86400000', '-86400000', '-86400000', '-86400000'],
},
{
formula: `DATETIME_DIFF("2022/10/14", "2022/10/15", "hours")`,
result: ['-24', '-24', '-24', '-24', '-24'],
},
{
formula: `DATETIME_DIFF("2022/10/14", "2023/10/14", "w")`,
result: ['-52', '-52', '-52', '-52', '-52'],
},
{
formula: `DATETIME_DIFF("2022/10/14", "2023/10/14", "M")`,
result: ['-12', '-12', '-12', '-12', '-12'],
},
{
formula: `DATETIME_DIFF("2022/10/14", "2023/10/14", "Q")`,
result: ['-4', '-4', '-4', '-4', '-4'],
},
{
formula: `DATETIME_DIFF("2022/10/14", "2023/10/14", "y")`,
result: ['-1', '-1', '-1', '-1', '-1'],
},
{
formula: `DATETIME_DIFF("2022/10/14", "2023/10/14", "d")`,
result: ['-365', '-365', '-365', '-365', '-365'],
},
{ {
formula: `CONCAT(UPPER({City}), LOWER({City}), TRIM(' trimmed '))`, formula: `CONCAT(UPPER({City}), LOWER({City}), TRIM(' trimmed '))`,
result: [ result: [

Loading…
Cancel
Save